From 8e7fe2a4c76734586687f2b70be7e2dbeb743145 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:38:10 +0000 Subject: [PATCH 1/8] Initial plan From c2b6d64d1ccc572d4ab7129a92e1ad9aead4073d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:45:14 +0000 Subject: [PATCH 2/8] Add Changesets-based release tracking flow Co-authored-by: TylerJDev <26746305+TylerJDev@users.noreply.github.com> --- .changeset/README.md | 8 + .changeset/config.json | 11 + .github/workflows/pr.yml | 2 + .github/workflows/publish.yml | 27 - .github/workflows/release.yml | 47 ++ README.md | 9 + package-lock.json | 1088 +++++++++++++++++++++++++++--- package.json | 12 +- scripts/prepare-release.mjs | 188 ++++++ scripts/prepare-release.test.mjs | 200 ++++++ 10 files changed, 1474 insertions(+), 118 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 scripts/prepare-release.mjs create mode 100644 scripts/prepare-release.test.mjs diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..226856f --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": ["@changesets/changelog-github", {"repo": "github/remote-input-element"}], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index df048b1..85558d6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,3 +24,5 @@ jobs: run: npm run build - name: Run test run: npm run test + - name: Run release script tests + run: npm run test:release-script diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 855bc2b..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Publish - -on: - release: - types: [created] - -jobs: - publish-npm: - name: Publish to npm - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - registry-url: https://registry.npmjs.org/ - cache: npm - - run: npm install -g npm@latest - - run: npm ci - - run: npm test - - run: npm version ${TAG_NAME} --git-tag-version=false - env: - TAG_NAME: ${{ github.event.release.tag_name }} - - run: npm --ignore-scripts publish --provenance --access public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f0983fc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: {} + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write + pull-requests: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history and tags are required so the release-preparation + # script can compare against the previous release tag. + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Prepare release (synthetic changeset for unreleased changes) + run: npm run prepare-release + + - name: Create release pull request or publish + uses: changesets/action@v2 + with: + title: 'Release: version packages' + commit: 'chore: version packages' + publish: npm run release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index d5e0699..1df877d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,15 @@ npm install npm test ``` +## Releasing + +This project uses [Changesets](https://github.com/changesets/changesets) to manage versioning, changelogs, and publishing to npm. + +- **Explicit changesets**: When you make a user-facing change, run `npx changeset` and follow the prompts to describe the change and pick a bump type (`patch`, `minor`, or `major`). Commit the generated file in `.changeset/`. Explicit changesets always take precedence over the automatic behavior described below. +- **Automatic patch releases**: Many merges (for example, Dependabot dependency bumps) don't come with a changeset. The [`release` workflow](.github/workflows/release.yml) runs `scripts/prepare-release.mjs` before invoking `changesets/action`. If no explicit changeset exists, but there are commits merged since the last published release tag, it generates a single synthetic `patch` changeset summarizing those commits (linking pull request numbers when available) so a release PR still gets opened. +- **Release pull request**: On every push to `main` (or a manual run via `workflow_dispatch`), the workflow opens or updates a "Release: version packages" pull request with the version bump and changelog entry. Merging that pull request triggers the same workflow again, which publishes the new version to npm (with [provenance](https://docs.npmjs.com/generating-provenance-statements)) and to GitHub Packages, and creates the matching git tag. +- **Manual runs**: Use the "Run workflow" button on the `Release` workflow in the Actions tab (`workflow_dispatch`) at any time to regenerate or refresh the release pull request without waiting for a push to `main`. + ## License Distributed under the MIT license. See LICENSE for details. diff --git a/package-lock.json b/package-lock.json index 3fdcf58..2f86f99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "@github/remote-input-element", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@github/remote-input-element", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "devDependencies": { + "@changesets/changelog-github": "^1.0.0", + "@changesets/cli": "^3.0.1", "@typescript-eslint/parser": "^7.4.0", "chai": "^4.1.2", "eslint": "8.56.0", @@ -44,6 +46,332 @@ "node": ">=6.9.0" } }, + "node_modules/@changesets/apply-release-plan": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.0.0.tgz", + "integrity": "sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^4.0.0", + "@changesets/format": "^0.1.1", + "@changesets/git": "^4.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-github": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-1.0.0.tgz", + "integrity": "sha512-PNCSH5CGJrqOKxRjrMp2NLhceQv2QBs6HkjrLvYQqsEE2/ItN5R8GD/mskZZluoNWYvjPYFsY9T6swXfp5E0UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/get-github-info": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/cli": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.1.tgz", + "integrity": "sha512-3IVpRSgiKDj2aRbBHKtWTXSRH2/iR3bWxau9iQUoEsZjDhRIj9nhl90k7FWMxZAJvLgJBbgMq8+qS0xQsenYsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^8.0.0", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.0", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.0", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.1", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.0" + }, + "bin": { + "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/config": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.0.tgz", + "integrity": "sha512-mw95/YrkOuhZZxfnVAA4bSXOFUi+KlhzOBTM8C4x777NhUU6HWIl9Z+K+nME+E4PVsv5NQVQwTfiHihAS1A/ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/config/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/get-github-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-1.0.0.tgz", + "integrity": "sha512-nf5zPSqEKW0eXiJ4ltbjyIQ/srUHfxbghkLCjKdOLI3LehKXhiqxXcpja39eGPaj6qqGx2lFCrLJ7VWYUf9H2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dataloader": "^2.2.3" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.0.tgz", + "integrity": "sha512-uIEswpPUgzBBqrC0qg13byNaPorzhf05LE2T+gRizEKCXvMsJC6NPJp5iNDSV/gYj4Viqh09UiOF5E7XWeH5lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@changesets/parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/pre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/read": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.0.tgz", + "integrity": "sha512-8TdE2PwG6yArPt5Ozej83Z6iHz1G8BDBKvIEKZ455MccR4K3GNbLR2XqjBxa+5yLFnEd3xAOPXYboBg1pt8stQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^4.0.0", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/write": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.1.tgz", + "integrity": "sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -148,6 +476,48 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@manypkg/find-root": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -195,6 +565,19 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -376,21 +759,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", @@ -828,6 +1196,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -1191,6 +1569,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "dev": true, + "license": "MIT" + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -1928,21 +2313,6 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-github/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-i18n-text": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", @@ -2351,6 +2721,33 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2879,6 +3276,16 @@ "node": ">=8.0.0" } }, + "node_modules/human-id": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.1.tgz", + "integrity": "sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2916,6 +3323,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3374,6 +3792,13 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", @@ -3414,6 +3839,13 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3578,6 +4010,17 @@ "node": ">=0.10" } }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3674,21 +4117,9 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" } }, "node_modules/math-intrinsics": { @@ -4183,6 +4614,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4256,10 +4694,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4608,6 +5047,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -4676,6 +5128,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -4748,6 +5213,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5037,6 +5509,64 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -5579,11 +6109,21 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } }, "node_modules/yargs": { "version": "16.2.0", @@ -5656,6 +6196,240 @@ "regenerator-runtime": "^0.14.0" } }, + "@changesets/apply-release-plan": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.0.0.tgz", + "integrity": "sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==", + "dev": true, + "requires": { + "@changesets/config": "^4.0.0", + "@changesets/format": "^0.1.1", + "@changesets/git": "^4.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + } + }, + "@changesets/assemble-release-plan": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", + "dev": true, + "requires": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + } + }, + "@changesets/changelog-git": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", + "dev": true, + "requires": { + "@changesets/types": "^7.0.0" + } + }, + "@changesets/changelog-github": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-1.0.0.tgz", + "integrity": "sha512-PNCSH5CGJrqOKxRjrMp2NLhceQv2QBs6HkjrLvYQqsEE2/ItN5R8GD/mskZZluoNWYvjPYFsY9T6swXfp5E0UA==", + "dev": true, + "requires": { + "@changesets/get-github-info": "^1.0.0", + "@changesets/types": "^7.0.0" + } + }, + "@changesets/cli": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.1.tgz", + "integrity": "sha512-3IVpRSgiKDj2aRbBHKtWTXSRH2/iR3bWxau9iQUoEsZjDhRIj9nhl90k7FWMxZAJvLgJBbgMq8+qS0xQsenYsQ==", + "dev": true, + "requires": { + "@changesets/apply-release-plan": "^8.0.0", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.0", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.0", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.1", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.0" + } + }, + "@changesets/config": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.0.tgz", + "integrity": "sha512-mw95/YrkOuhZZxfnVAA4bSXOFUi+KlhzOBTM8C4x777NhUU6HWIl9Z+K+nME+E4PVsv5NQVQwTfiHihAS1A/ow==", + "dev": true, + "requires": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true + } + } + }, + "@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", + "dev": true + }, + "@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", + "dev": true, + "requires": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + } + }, + "@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", + "dev": true, + "requires": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + } + }, + "@changesets/get-github-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-1.0.0.tgz", + "integrity": "sha512-nf5zPSqEKW0eXiJ4ltbjyIQ/srUHfxbghkLCjKdOLI3LehKXhiqxXcpja39eGPaj6qqGx2lFCrLJ7VWYUf9H2w==", + "dev": true, + "requires": { + "dataloader": "^2.2.3" + } + }, + "@changesets/git": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.0.tgz", + "integrity": "sha512-uIEswpPUgzBBqrC0qg13byNaPorzhf05LE2T+gRizEKCXvMsJC6NPJp5iNDSV/gYj4Viqh09UiOF5E7XWeH5lQ==", + "dev": true, + "requires": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "dependencies": { + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true + } + } + }, + "@changesets/parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", + "dev": true, + "requires": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + } + }, + "@changesets/pre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", + "dev": true, + "requires": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + } + }, + "@changesets/read": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.0.tgz", + "integrity": "sha512-8TdE2PwG6yArPt5Ozej83Z6iHz1G8BDBKvIEKZ455MccR4K3GNbLR2XqjBxa+5yLFnEd3xAOPXYboBg1pt8stQ==", + "dev": true, + "requires": { + "@changesets/git": "^4.0.0", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + } + }, + "@changesets/should-skip-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", + "dev": true, + "requires": { + "@changesets/types": "^7.0.0" + } + }, + "@changesets/types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", + "dev": true + }, + "@changesets/write": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.1.tgz", + "integrity": "sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==", + "dev": true, + "requires": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + } + }, + "@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "requires": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + } + }, + "@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "requires": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + } + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -5729,6 +6503,36 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "@manypkg/find-root": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", + "dev": true, + "requires": { + "@manypkg/tools": "^2.1.0" + } + }, + "@manypkg/get-packages": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", + "dev": true, + "requires": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + } + }, + "@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", + "dev": true, + "requires": { + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5761,6 +6565,12 @@ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true }, + "@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -5888,15 +6698,6 @@ "requires": { "brace-expansion": "^2.0.1" } - }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -6215,6 +7016,12 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, + "cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -6484,6 +7291,12 @@ "is-data-view": "^1.0.1" } }, + "dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "dev": true + }, "date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -7117,15 +7930,6 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "requires": {} - }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -7347,6 +8151,30 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true + }, + "fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "requires": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "requires": { + "fast-string-width": "^3.0.2" + } + }, "fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -7721,6 +8549,12 @@ "requires-port": "^1.0.0" } }, + "human-id": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.1.tgz", + "integrity": "sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7746,6 +8580,12 @@ "resolve-from": "^4.0.0" } }, + "import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8045,6 +8885,12 @@ "set-function-name": "^2.0.1" } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "js-yaml": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", @@ -8072,6 +8918,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -8210,6 +9062,16 @@ "language-subtag-registry": "^0.3.20" } }, + "launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "requires": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8296,15 +9158,6 @@ "get-func-name": "^2.0.1" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8658,6 +9511,12 @@ "p-limit": "^3.0.2" } }, + "package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8710,9 +9569,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "picomatch": { @@ -8937,6 +9796,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true + }, "serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -8993,6 +9858,12 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true + }, "side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -9041,6 +9912,12 @@ "side-channel-map": "^1.0.1" } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -9247,6 +10124,37 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true + }, + "tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true + } + } + }, "tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -9618,10 +10526,10 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index 60f3e18..d24f1ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@github/remote-input-element", - "version": "0.3.1", + "version": "0.4.0", "description": "An input element that sends its value to a server endpoint and renders the response body.", "main": "dist/umd/index.js", "module": "dist/index.js", @@ -10,6 +10,10 @@ "files": [ "dist" ], + "publishConfig": { + "access": "public", + "provenance": true + }, "scripts": { "clean": "rm -rf dist", "lint": "eslint './src/**' './test/**.js'", @@ -17,10 +21,16 @@ "build": "tsc --outDir dist/umd --module umd && tsc", "pretest": "npm run build", "test": "karma start test/karma.config.js", + "test:release-script": "node --test scripts/*.test.mjs", + "prepare-release": "node scripts/prepare-release.mjs", + "changeset": "changeset", + "release": "changeset publish", "prepublishOnly": "npm run build", "postpublish": "npm publish --ignore-scripts --@github:registry='https://npm.pkg.github.com'" }, "devDependencies": { + "@changesets/changelog-github": "^1.0.0", + "@changesets/cli": "^3.0.1", "@typescript-eslint/parser": "^7.4.0", "chai": "^4.1.2", "eslint": "8.56.0", diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs new file mode 100644 index 0000000..163f4c2 --- /dev/null +++ b/scripts/prepare-release.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node +// Repository-local Changesets helper. +// +// This script is intentionally small and dependency-free (besides Node's +// built-ins) so it can run as a plain preparation step before +// `changesets/action` in `.github/workflows/release.yml`, without requiring +// any changes to the upstream `changesets/action` project. +// +// It generates a single, deterministic "synthetic" patch changeset that +// summarizes the commits merged since the last released/tagged version, but +// only when there is no explicit changeset already checked in. This keeps +// the normal Changesets release-PR flow working for repositories (like this +// one) where changes are frequently merged (e.g. via Dependabot) without an +// accompanying changeset. +import {execFileSync} from 'node:child_process' +import {existsSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs' +import path from 'node:path' +import {fileURLToPath} from 'node:url' + +export const AUTO_CHANGESET_FILENAME = 'auto-release.md' +const NON_EXPLICIT_FILENAMES = new Set(['README.md', 'config.json', AUTO_CHANGESET_FILENAME]) + +function git(args, cwd) { + return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim() +} + +// Detects a shallow clone, which would make `${tag}..HEAD` comparisons +// unreliable (missing commits/tags). The release workflow must check out +// with `fetch-depth: 0`. +export function isShallowRepository(cwd) { + return git(['rev-parse', '--is-shallow-repository'], cwd) === 'true' +} + +// Returns true when `tag` exists in the repository at `cwd`. +export function tagExists(tag, cwd) { + try { + git(['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], cwd) + return true + } catch { + return false + } +} + +// Returns the list of changeset markdown files that were authored explicitly +// by a maintainer, i.e. everything in `.changeset` except the config/readme +// files and the synthetic changeset this script maintains. +export function getExplicitChangesets(changesetDir) { + if (!existsSync(changesetDir)) return [] + return readdirSync(changesetDir).filter(name => name.endsWith('.md') && !NON_EXPLICIT_FILENAMES.has(name)) +} + +// Parses `git log --first-parent` output for `range` into one entry per +// mainline commit (i.e. one entry per merged pull request, when merge +// commits are used), extracting a PR number when possible so the generated +// changeset can link back to it. +export function getCommitsSinceTag(range, cwd, repository) { + const SEP = '\x1f' + const REC = '\x1e' + const format = `%H${SEP}%s${SEP}%b${REC}` + let raw + try { + raw = execFileSync('git', ['log', '--first-parent', `--pretty=format:${format}`, range], { + cwd, + encoding: 'utf8', + }) + } catch (error) { + throw new Error(`Unable to read git history for range "${range}": ${error.message}`) + } + + const records = raw + .split(REC) + .map(entry => entry.trim()) + .filter(Boolean) + + const entries = [] + for (const record of records) { + const [sha, subject, body = ''] = record.split(SEP) + entries.push(parseCommit({sha, subject: subject ?? '', body}, repository)) + } + return entries +} + +function parseCommit({sha, subject, body}, repository) { + const shortSha = sha.slice(0, 7) + const mergeMatch = subject.match(/^Merge pull request #(\d+) from/) + if (mergeMatch) { + const prNumber = mergeMatch[1] + const title = body.split('\n').map(line => line.trim()).find(Boolean) || subject + return {sha, shortSha, prNumber, title} + } + + const squashMatch = subject.match(/\(#(\d+)\)\s*$/) + if (squashMatch) { + const prNumber = squashMatch[1] + const title = subject.slice(0, squashMatch.index).trim() + return {sha, shortSha, prNumber, title} + } + + return {sha, shortSha, prNumber: undefined, title: subject} +} + +// Renders the deterministic body of the synthetic changeset from the given +// commit/PR entries. +export function renderSummary(entries, repository) { + const lines = entries.map(entry => { + if (entry.prNumber) { + return `- ${entry.title} in [#${entry.prNumber}](https://github.com/${repository}/pull/${entry.prNumber})` + } + return `- ${entry.title} ([\`${entry.shortSha}\`](https://github.com/${repository}/commit/${entry.sha}))` + }) + return lines.join('\n') +} + +export function buildChangesetContent(pkgName, entries, repository) { + const summary = + entries.length === 1 && !entries[0].prNumber + ? renderSummary(entries, repository).replace(/^- /, '') + : `Release unreleased changes:\n\n${renderSummary(entries, repository)}` + return `---\n"${pkgName}": patch\n---\n\n${summary}\n` +} + +// Orchestrates the whole "prepare release" step. Returns a small result +// object describing what happened, primarily for logging/testing purposes. +export function prepareRelease({cwd = process.cwd()} = {}) { + const pkgPath = path.join(cwd, 'package.json') + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) + const changesetDir = path.join(cwd, '.changeset') + const autoChangesetPath = path.join(changesetDir, AUTO_CHANGESET_FILENAME) + + const explicit = getExplicitChangesets(changesetDir) + if (explicit.length > 0) { + if (existsSync(autoChangesetPath)) { + rmSync(autoChangesetPath) + } + return {action: 'skipped-explicit-changeset', changesets: explicit} + } + + const tag = `v${pkg.version}` + if (!tagExists(tag, cwd)) { + if (existsSync(autoChangesetPath)) { + rmSync(autoChangesetPath) + } + return {action: 'skipped-pending-publication', tag, version: pkg.version} + } + + if (isShallowRepository(cwd)) { + throw new Error( + `Cannot compute changes since ${tag} in a shallow clone. ` + + 'Configure `actions/checkout` with `fetch-depth: 0` in the release workflow.', + ) + } + + const range = `${tag}..HEAD` + const entries = getCommitsSinceTag(range, cwd, pkg.repository) + if (entries.length === 0) { + if (existsSync(autoChangesetPath)) { + rmSync(autoChangesetPath) + } + return {action: 'skipped-no-changes', tag} + } + + const content = buildChangesetContent(pkg.name, entries, pkg.repository) + writeFileSync(autoChangesetPath, content) + return {action: 'created', tag, path: autoChangesetPath, entries} +} + +const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) +if (isMain) { + const result = prepareRelease({cwd: process.cwd()}) + switch (result.action) { + case 'skipped-explicit-changeset': + console.log(`Found explicit changeset(s): ${result.changesets.join(', ')}. Skipping synthetic changeset.`) + break + case 'skipped-pending-publication': + console.log( + `Release tag ${result.tag} not found; version ${result.version} in package.json is pending publication. Skipping synthetic changeset.`, + ) + break + case 'skipped-no-changes': + console.log(`No changes found since ${result.tag}. Skipping synthetic changeset.`) + break + case 'created': + console.log(`Created synthetic patch changeset at ${result.path} for ${result.entries.length} change(s) since ${result.tag}.`) + break + default: + console.log(`Unhandled result: ${JSON.stringify(result)}`) + } +} diff --git a/scripts/prepare-release.test.mjs b/scripts/prepare-release.test.mjs new file mode 100644 index 0000000..f7c4e97 --- /dev/null +++ b/scripts/prepare-release.test.mjs @@ -0,0 +1,200 @@ +import assert from 'node:assert/strict' +import {execFileSync} from 'node:child_process' +import {mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync} from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import {test, describe, beforeEach, afterEach} from 'node:test' + +import {prepareRelease, AUTO_CHANGESET_FILENAME} from './prepare-release.mjs' + +function git(args, cwd) { + return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim() +} + +function writePackageJson(cwd, version) { + writeFileSync( + path.join(cwd, 'package.json'), + JSON.stringify({name: '@github/remote-input-element', version, repository: 'github/remote-input-element'}, null, 2) + '\n', + ) +} + +function commit(cwd, message, {merge, body} = {}) { + git(['add', '-A'], cwd) + const fullMessage = merge ? `${message}\n\n${body ?? ''}` : message + git(['commit', '--allow-empty', '-m', fullMessage], cwd) + return git(['rev-parse', 'HEAD'], cwd) +} + +let repoDir + +beforeEach(() => { + repoDir = mkdtempSync(path.join(os.tmpdir(), 'prepare-release-')) + git(['init', '-b', 'main'], repoDir) + git(['config', 'user.email', 'test@example.com'], repoDir) + git(['config', 'user.name', 'Test'], repoDir) + mkdirSync(path.join(repoDir, '.changeset')) + writeFileSync(path.join(repoDir, '.changeset', 'README.md'), '# changesets\n') + writeFileSync(path.join(repoDir, '.changeset', 'config.json'), '{}\n') +}) + +afterEach(() => { + rmSync(repoDir, {recursive: true, force: true}) +}) + +describe('prepareRelease', () => { + test('does nothing when the checked-in version has no matching tag (pending publication)', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'skipped-pending-publication') + assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + }) + + test('does nothing when there are no changes after the current release tag', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'skipped-no-changes') + assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + }) + + test('creates a synthetic patch changeset for changes after the current release tag', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { + merge: true, + body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + }) + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'created') + const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + assert.match(content, /"@github\/remote-input-element": patch/) + assert.match(content, /Bump the npm_and_yarn group across 1 directory with 2 updates/) + assert.match(content, /\[#53\]\(https:\/\/github\.com\/github\/remote-input-element\/pull\/53\)/) + }) + + test('handles a single commit without an associated PR', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + const sha = commit(repoDir, 'Direct fix to main') + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'created') + const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + assert.match(content, /Direct fix to main/) + assert.match(content, new RegExp(sha.slice(0, 7))) + }) + + test('does not create a synthetic changeset when an explicit changeset exists', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #60 from github/some-feature', {merge: true, body: 'Add a feature'}) + writeFileSync( + path.join(repoDir, '.changeset', 'some-feature.md'), + '---\n"@github/remote-input-element": minor\n---\n\nAdd a feature\n', + ) + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'skipped-explicit-changeset') + assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + }) + + test('removes a stale synthetic changeset once an explicit changeset is added', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #60 from github/dependabot/some-bump', {merge: true, body: 'Bump something'}) + + const first = prepareRelease({cwd: repoDir}) + assert.equal(first.action, 'created') + + writeFileSync( + path.join(repoDir, '.changeset', 'explicit.md'), + '---\n"@github/remote-input-element": minor\n---\n\nAdd a feature\n', + ) + + const second = prepareRelease({cwd: repoDir}) + assert.equal(second.action, 'skipped-explicit-changeset') + assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + }) + + test('is idempotent across repeated runs', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { + merge: true, + body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + }) + + const first = prepareRelease({cwd: repoDir}) + const firstContent = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + + const second = prepareRelease({cwd: repoDir}) + const secondContent = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + + assert.equal(first.action, 'created') + assert.equal(second.action, 'created') + assert.equal(firstContent, secondContent) + }) + + test('summarizes multiple merged PRs since the release tag', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { + merge: true, + body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + }) + commit(repoDir, 'Merge pull request #54 from github/dependabot/other', { + merge: true, + body: 'Bump other dependency', + }) + + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'created') + assert.equal(result.entries.length, 2) + const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + assert.match(content, /#53/) + assert.match(content, /#54/) + }) + + test('does not advance the proposed version when the release tag stays the same across runs', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + git(['tag', 'v0.4.0'], repoDir) + commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { + merge: true, + body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + }) + prepareRelease({cwd: repoDir}) + + // A second, unrelated change is merged before the release PR is merged. + commit(repoDir, 'Merge pull request #54 from github/dependabot/other', { + merge: true, + body: 'Bump other dependency', + }) + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'created') + assert.equal(result.tag, 'v0.4.0') + const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + assert.match(content, /"@github\/remote-input-element": patch/) + // Still only one patch entry in frontmatter -- the release PR would + // still propose 0.4.1, not 0.4.2. + assert.equal((content.match(/patch/g) || []).length, 1) + }) +}) From 3a91f60a2838d41f710debef8d8cec93d3aa2bc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:48:13 +0000 Subject: [PATCH 3/8] Address review feedback: schema version, node engine, repo URL parsing Co-authored-by: TylerJDev <26746305+TylerJDev@users.noreply.github.com> --- .changeset/config.json | 2 +- .github/workflows/release.yml | 2 +- scripts/prepare-release.mjs | 29 +++++++++++++++++++++++++++-- scripts/prepare-release.test.mjs | 26 +++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 226856f..0500dbf 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,5 +1,5 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json", "changelog": ["@changesets/changelog-github", {"repo": "github/remote-input-element"}], "commit": false, "fixed": [], diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0983fc..9af81bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 22 registry-url: https://registry.npmjs.org - name: Install dependencies diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index 163f4c2..0038e03 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -80,6 +80,30 @@ export function getCommitsSinceTag(range, cwd, repository) { return entries } +// Normalizes the `repository` field of package.json (which may be a plain +// "owner/repo" shorthand string, a full git/https URL string, or an object +// with a `url` property) into a plain "owner/repo" string suitable for +// building GitHub URLs. +export function normalizeRepository(repository) { + const raw = typeof repository === 'string' ? repository : repository?.url + if (!raw) { + throw new Error('Unable to determine the GitHub "owner/repo" from package.json\'s "repository" field.') + } + + // Plain "owner/repo" shorthand, e.g. "github/remote-input-element". + if (/^[^/\s:]+\/[^/\s]+$/.test(raw)) { + return raw + } + + // A GitHub URL (git/https/ssh), e.g. + // "git+https://github.com/owner/repo.git" or "git@github.com:owner/repo.git". + const match = raw.match(/(?:^|\/\/|@)github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/) + if (!match) { + throw new Error(`Unable to parse a GitHub "owner/repo" from repository field: ${JSON.stringify(raw)}`) + } + return match[1] +} + function parseCommit({sha, subject, body}, repository) { const shortSha = sha.slice(0, 7) const mergeMatch = subject.match(/^Merge pull request #(\d+) from/) @@ -151,7 +175,8 @@ export function prepareRelease({cwd = process.cwd()} = {}) { } const range = `${tag}..HEAD` - const entries = getCommitsSinceTag(range, cwd, pkg.repository) + const repository = normalizeRepository(pkg.repository) + const entries = getCommitsSinceTag(range, cwd, repository) if (entries.length === 0) { if (existsSync(autoChangesetPath)) { rmSync(autoChangesetPath) @@ -159,7 +184,7 @@ export function prepareRelease({cwd = process.cwd()} = {}) { return {action: 'skipped-no-changes', tag} } - const content = buildChangesetContent(pkg.name, entries, pkg.repository) + const content = buildChangesetContent(pkg.name, entries, repository) writeFileSync(autoChangesetPath, content) return {action: 'created', tag, path: autoChangesetPath, entries} } diff --git a/scripts/prepare-release.test.mjs b/scripts/prepare-release.test.mjs index f7c4e97..6b3d6f7 100644 --- a/scripts/prepare-release.test.mjs +++ b/scripts/prepare-release.test.mjs @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import {test, describe, beforeEach, afterEach} from 'node:test' -import {prepareRelease, AUTO_CHANGESET_FILENAME} from './prepare-release.mjs' +import {prepareRelease, AUTO_CHANGESET_FILENAME, normalizeRepository} from './prepare-release.mjs' function git(args, cwd) { return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim() @@ -41,6 +41,30 @@ afterEach(() => { rmSync(repoDir, {recursive: true, force: true}) }) +describe('normalizeRepository', () => { + test('accepts the "owner/repo" shorthand string', () => { + assert.equal(normalizeRepository('github/remote-input-element'), 'github/remote-input-element') + }) + + test('accepts a full git URL string', () => { + assert.equal( + normalizeRepository('git+https://github.com/github/remote-input-element.git'), + 'github/remote-input-element', + ) + }) + + test('accepts an object with a url property', () => { + assert.equal( + normalizeRepository({type: 'git', url: 'https://github.com/github/remote-input-element.git'}), + 'github/remote-input-element', + ) + }) + + test('rejects a non-GitHub URL instead of extracting the wrong owner/repo', () => { + assert.throws(() => normalizeRepository('https://notgithub.com/owner/repo')) + }) +}) + describe('prepareRelease', () => { test('does nothing when the checked-in version has no matching tag (pending publication)', () => { writePackageJson(repoDir, '0.4.0') From 3525f04a75c957600623c360e823c8e40a1d6fe2 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 21 Aug 2026 16:36:04 -0400 Subject: [PATCH 4/8] Gate dependency-only releases by age and security impact --- scripts/prepare-release.mjs | 197 +++++++++++++++++++++--------------- 1 file changed, 116 insertions(+), 81 deletions(-) diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index 0038e03..94cba1a 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -1,37 +1,28 @@ #!/usr/bin/env node // Repository-local Changesets helper. // -// This script is intentionally small and dependency-free (besides Node's -// built-ins) so it can run as a plain preparation step before -// `changesets/action` in `.github/workflows/release.yml`, without requiring -// any changes to the upstream `changesets/action` project. -// -// It generates a single, deterministic "synthetic" patch changeset that -// summarizes the commits merged since the last released/tagged version, but -// only when there is no explicit changeset already checked in. This keeps -// the normal Changesets release-PR flow working for repositories (like this -// one) where changes are frequently merged (e.g. via Dependabot) without an -// accompanying changeset. +// Explicit changesets are released immediately. When changes have merged +// without a changeset, this script creates one automatically. Dependency-only +// changes are held until the current release is 30 days old, unless one of the +// dependency updates contains a security fix. import {execFileSync} from 'node:child_process' import {existsSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs' import path from 'node:path' import {fileURLToPath} from 'node:url' export const AUTO_CHANGESET_FILENAME = 'auto-release.md' +export const DEFAULT_DEPENDENCY_RELEASE_AGE_DAYS = 30 const NON_EXPLICIT_FILENAMES = new Set(['README.md', 'config.json', AUTO_CHANGESET_FILENAME]) +const DAY_MS = 24 * 60 * 60 * 1000 function git(args, cwd) { return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim() } -// Detects a shallow clone, which would make `${tag}..HEAD` comparisons -// unreliable (missing commits/tags). The release workflow must check out -// with `fetch-depth: 0`. export function isShallowRepository(cwd) { return git(['rev-parse', '--is-shallow-repository'], cwd) === 'true' } -// Returns true when `tag` exists in the repository at `cwd`. export function tagExists(tag, cwd) { try { git(['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], cwd) @@ -41,18 +32,15 @@ export function tagExists(tag, cwd) { } } -// Returns the list of changeset markdown files that were authored explicitly -// by a maintainer, i.e. everything in `.changeset` except the config/readme -// files and the synthetic changeset this script maintains. +export function getTagDate(tag, cwd) { + return new Date(Number(git(['log', '-1', '--format=%ct', tag], cwd)) * 1000) +} + export function getExplicitChangesets(changesetDir) { if (!existsSync(changesetDir)) return [] return readdirSync(changesetDir).filter(name => name.endsWith('.md') && !NON_EXPLICIT_FILENAMES.has(name)) } -// Parses `git log --first-parent` output for `range` into one entry per -// mainline commit (i.e. one entry per merged pull request, when merge -// commits are used), extracting a PR number when possible so the generated -// changeset can link back to it. export function getCommitsSinceTag(range, cwd, repository) { const SEP = '\x1f' const REC = '\x1e' @@ -67,36 +55,24 @@ export function getCommitsSinceTag(range, cwd, repository) { throw new Error(`Unable to read git history for range "${range}": ${error.message}`) } - const records = raw + return raw .split(REC) .map(entry => entry.trim()) .filter(Boolean) - - const entries = [] - for (const record of records) { - const [sha, subject, body = ''] = record.split(SEP) - entries.push(parseCommit({sha, subject: subject ?? '', body}, repository)) - } - return entries + .map(record => { + const [sha, subject, body = ''] = record.split(SEP) + return parseCommit({sha, subject: subject ?? '', body}, repository) + }) } -// Normalizes the `repository` field of package.json (which may be a plain -// "owner/repo" shorthand string, a full git/https URL string, or an object -// with a `url` property) into a plain "owner/repo" string suitable for -// building GitHub URLs. export function normalizeRepository(repository) { const raw = typeof repository === 'string' ? repository : repository?.url if (!raw) { throw new Error('Unable to determine the GitHub "owner/repo" from package.json\'s "repository" field.') } - // Plain "owner/repo" shorthand, e.g. "github/remote-input-element". - if (/^[^/\s:]+\/[^/\s]+$/.test(raw)) { - return raw - } + if (/^[^/\s:]+\/[^/\s]+$/.test(raw)) return raw - // A GitHub URL (git/https/ssh), e.g. - // "git+https://github.com/owner/repo.git" or "git@github.com:owner/repo.git". const match = raw.match(/(?:^|\/\/|@)github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/) if (!match) { throw new Error(`Unable to parse a GitHub "owner/repo" from repository field: ${JSON.stringify(raw)}`) @@ -104,35 +80,68 @@ export function normalizeRepository(repository) { return match[1] } -function parseCommit({sha, subject, body}, repository) { +function parseCommit({sha, subject, body}) { const shortSha = sha.slice(0, 7) - const mergeMatch = subject.match(/^Merge pull request #(\d+) from/) + const mergeMatch = subject.match(/^Merge pull request #(\d+) from\s+([^\s]+)/) if (mergeMatch) { - const prNumber = mergeMatch[1] const title = body.split('\n').map(line => line.trim()).find(Boolean) || subject - return {sha, shortSha, prNumber, title} + return {sha, shortSha, prNumber: mergeMatch[1], sourceBranch: mergeMatch[2], title} } const squashMatch = subject.match(/\(#(\d+)\)\s*$/) if (squashMatch) { - const prNumber = squashMatch[1] - const title = subject.slice(0, squashMatch.index).trim() - return {sha, shortSha, prNumber, title} + return { + sha, + shortSha, + prNumber: squashMatch[1], + sourceBranch: undefined, + title: subject.slice(0, squashMatch.index).trim(), + } } - return {sha, shortSha, prNumber: undefined, title: subject} + return {sha, shortSha, prNumber: undefined, sourceBranch: undefined, title: subject} } -// Renders the deterministic body of the synthetic changeset from the given -// commit/PR entries. -export function renderSummary(entries, repository) { - const lines = entries.map(entry => { - if (entry.prNumber) { - return `- ${entry.title} in [#${entry.prNumber}](https://github.com/${repository}/pull/${entry.prNumber})` - } - return `- ${entry.title} ([\`${entry.shortSha}\`](https://github.com/${repository}/commit/${entry.sha}))` +export function isDependencyUpdate(entry) { + return entry.sourceBranch?.includes('dependabot/') === true || /^Bump\b/i.test(entry.title) +} + +export function isSecurityUpdate(pullRequest) { + const text = `${pullRequest.title ?? ''}\n${pullRequest.body ?? ''}` + const labels = (pullRequest.labels ?? []).map(label => (typeof label === 'string' ? label : label.name ?? '')) + return ( + labels.some(label => /security|vulnerability/i.test(label)) || + /\s*Security\s*<\/h[1-6]>/i.test(text) || + /^\s{0,3}#{1,6}\s+Security\b/im.test(text) || + /\b(?:CVE-\d{4}-\d+|GHSA-[a-z0-9-]+)\b/i.test(text) + ) +} + +export function fetchPullRequestWithGh({repository, prNumber, cwd, env = process.env}) { + const token = env.GH_TOKEN || env.GITHUB_TOKEN + if (!token) { + throw new Error( + `GITHUB_TOKEN is required to inspect dependency pull request #${prNumber} for security release notes.`, + ) + } + + const output = execFileSync('gh', ['api', `repos/${repository}/pulls/${prNumber}`], { + cwd, + encoding: 'utf8', + env: {...env, GH_TOKEN: token}, }) - return lines.join('\n') + return JSON.parse(output) +} + +export function renderSummary(entries, repository) { + return entries + .map(entry => { + if (entry.prNumber) { + return `- ${entry.title} in [#${entry.prNumber}](https://github.com/${repository}/pull/${entry.prNumber})` + } + return `- ${entry.title} ([\`${entry.shortSha}\`](https://github.com/${repository}/commit/${entry.sha}))` + }) + .join('\n') } export function buildChangesetContent(pkgName, entries, repository) { @@ -143,27 +152,31 @@ export function buildChangesetContent(pkgName, entries, repository) { return `---\n"${pkgName}": patch\n---\n\n${summary}\n` } -// Orchestrates the whole "prepare release" step. Returns a small result -// object describing what happened, primarily for logging/testing purposes. -export function prepareRelease({cwd = process.cwd()} = {}) { - const pkgPath = path.join(cwd, 'package.json') - const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) +function removeAutoChangeset(autoChangesetPath) { + if (existsSync(autoChangesetPath)) rmSync(autoChangesetPath) +} + +export function prepareRelease({ + cwd = process.cwd(), + now = new Date(), + minimumDependencyReleaseAgeDays = Number( + process.env.DEPENDENCY_RELEASE_AGE_DAYS || DEFAULT_DEPENDENCY_RELEASE_AGE_DAYS, + ), + fetchPullRequest = fetchPullRequestWithGh, +} = {}) { + const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8')) const changesetDir = path.join(cwd, '.changeset') const autoChangesetPath = path.join(changesetDir, AUTO_CHANGESET_FILENAME) const explicit = getExplicitChangesets(changesetDir) if (explicit.length > 0) { - if (existsSync(autoChangesetPath)) { - rmSync(autoChangesetPath) - } + removeAutoChangeset(autoChangesetPath) return {action: 'skipped-explicit-changeset', changesets: explicit} } const tag = `v${pkg.version}` if (!tagExists(tag, cwd)) { - if (existsSync(autoChangesetPath)) { - rmSync(autoChangesetPath) - } + removeAutoChangeset(autoChangesetPath) return {action: 'skipped-pending-publication', tag, version: pkg.version} } @@ -174,19 +187,37 @@ export function prepareRelease({cwd = process.cwd()} = {}) { ) } - const range = `${tag}..HEAD` const repository = normalizeRepository(pkg.repository) - const entries = getCommitsSinceTag(range, cwd, repository) + const entries = getCommitsSinceTag(`${tag}..HEAD`, cwd, repository) if (entries.length === 0) { - if (existsSync(autoChangesetPath)) { - rmSync(autoChangesetPath) - } + removeAutoChangeset(autoChangesetPath) return {action: 'skipped-no-changes', tag} } - const content = buildChangesetContent(pkg.name, entries, repository) - writeFileSync(autoChangesetPath, content) - return {action: 'created', tag, path: autoChangesetPath, entries} + const dependencyOnly = entries.every(isDependencyUpdate) + if (dependencyOnly) { + const releaseAgeDays = Math.floor((now.getTime() - getTagDate(tag, cwd).getTime()) / DAY_MS) + if (releaseAgeDays < minimumDependencyReleaseAgeDays) { + const securityEntry = entries.find(entry => { + if (!entry.prNumber) return false + const pullRequest = fetchPullRequest({repository, prNumber: entry.prNumber, cwd}) + return isSecurityUpdate(pullRequest) + }) + + if (!securityEntry) { + removeAutoChangeset(autoChangesetPath) + return { + action: 'skipped-recent-dependencies', + tag, + releaseAgeDays, + minimumDependencyReleaseAgeDays, + } + } + } + } + + writeFileSync(autoChangesetPath, buildChangesetContent(pkg.name, entries, repository)) + return {action: 'created', tag, path: autoChangesetPath, entries, dependencyOnly} } const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) @@ -194,15 +225,19 @@ if (isMain) { const result = prepareRelease({cwd: process.cwd()}) switch (result.action) { case 'skipped-explicit-changeset': - console.log(`Found explicit changeset(s): ${result.changesets.join(', ')}. Skipping synthetic changeset.`) + console.log(`Found explicit changeset(s): ${result.changesets.join(', ')}. Preparing an immediate release.`) break case 'skipped-pending-publication': - console.log( - `Release tag ${result.tag} not found; version ${result.version} in package.json is pending publication. Skipping synthetic changeset.`, - ) + console.log(`Release tag ${result.tag} not found; version ${result.version} is pending publication.`) break case 'skipped-no-changes': - console.log(`No changes found since ${result.tag}. Skipping synthetic changeset.`) + console.log(`No changes found since ${result.tag}.`) + break + case 'skipped-recent-dependencies': + console.log( + `Only non-security dependency updates have merged and ${result.tag} is ${result.releaseAgeDays} day(s) old. ` + + `Waiting until it is at least ${result.minimumDependencyReleaseAgeDays} days old.`, + ) break case 'created': console.log(`Created synthetic patch changeset at ${result.path} for ${result.entries.length} change(s) since ${result.tag}.`) From f16055210a0f06ad3eb0708bb66e466ca447857e Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 21 Aug 2026 17:20:52 -0400 Subject: [PATCH 5/8] Align release workflow with validated Changesets inputs --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9af81bf..245d7f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,8 +40,8 @@ jobs: - name: Create release pull request or publish uses: changesets/action@v2 with: - title: 'Release: version packages' - commit: 'chore: version packages' - publish: npm run release + pr-title: 'Release tracking' + commit-message: 'chore: version packages' + publish-script: npm run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0e0e61ff17501dcfb2189c5696a69c6d47606a43 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 21 Aug 2026 17:23:52 -0400 Subject: [PATCH 6/8] Create per-entry release changesets with dependency gating --- scripts/prepare-release.mjs | 77 ++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index 94cba1a..0c7c301 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -2,17 +2,17 @@ // Repository-local Changesets helper. // // Explicit changesets are released immediately. When changes have merged -// without a changeset, this script creates one automatically. Dependency-only -// changes are held until the current release is 30 days old, unless one of the -// dependency updates contains a security fix. +// without a changeset, this script creates one automatic changeset per commit. +// Dependency-only changes are held until the current release is 30 days old, +// unless one of the dependency updates contains a security fix. import {execFileSync} from 'node:child_process' import {existsSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs' import path from 'node:path' import {fileURLToPath} from 'node:url' -export const AUTO_CHANGESET_FILENAME = 'auto-release.md' +export const AUTO_CHANGESET_PREFIX = 'auto-release-' export const DEFAULT_DEPENDENCY_RELEASE_AGE_DAYS = 30 -const NON_EXPLICIT_FILENAMES = new Set(['README.md', 'config.json', AUTO_CHANGESET_FILENAME]) +const NON_EXPLICIT_FILENAMES = new Set(['README.md', 'config.json']) const DAY_MS = 24 * 60 * 60 * 1000 function git(args, cwd) { @@ -36,9 +36,26 @@ export function getTagDate(tag, cwd) { return new Date(Number(git(['log', '-1', '--format=%ct', tag], cwd)) * 1000) } +export function isAutoChangeset(name) { + return name.startsWith(AUTO_CHANGESET_PREFIX) && name.endsWith('.md') +} + +export function getAutoChangesets(changesetDir) { + if (!existsSync(changesetDir)) return [] + return readdirSync(changesetDir).filter(isAutoChangeset) +} + export function getExplicitChangesets(changesetDir) { if (!existsSync(changesetDir)) return [] - return readdirSync(changesetDir).filter(name => name.endsWith('.md') && !NON_EXPLICIT_FILENAMES.has(name)) + return readdirSync(changesetDir).filter( + name => name.endsWith('.md') && !NON_EXPLICIT_FILENAMES.has(name) && !isAutoChangeset(name), + ) +} + +export function removeAutoChangesets(changesetDir) { + for (const name of getAutoChangesets(changesetDir)) { + rmSync(path.join(changesetDir, name)) + } } export function getCommitsSinceTag(range, cwd, repository) { @@ -133,27 +150,26 @@ export function fetchPullRequestWithGh({repository, prNumber, cwd, env = process return JSON.parse(output) } -export function renderSummary(entries, repository) { - return entries - .map(entry => { - if (entry.prNumber) { - return `- ${entry.title} in [#${entry.prNumber}](https://github.com/${repository}/pull/${entry.prNumber})` - } - return `- ${entry.title} ([\`${entry.shortSha}\`](https://github.com/${repository}/commit/${entry.sha}))` - }) - .join('\n') +export function renderEntry(entry, repository) { + const commit = `[\`${entry.shortSha}\`](https://github.com/${repository}/commit/${entry.sha})` + if (entry.prNumber) { + return `[#${entry.prNumber}](https://github.com/${repository}/pull/${entry.prNumber}) ${commit} - ${entry.title}` + } + return `${commit} - ${entry.title}` } -export function buildChangesetContent(pkgName, entries, repository) { - const summary = - entries.length === 1 && !entries[0].prNumber - ? renderSummary(entries, repository).replace(/^- /, '') - : `Release unreleased changes:\n\n${renderSummary(entries, repository)}` - return `---\n"${pkgName}": patch\n---\n\n${summary}\n` +export function buildChangesetContent(pkgName, entry, repository) { + return `---\n"${pkgName}": patch\n---\n\n${renderEntry(entry, repository)}\n` } -function removeAutoChangeset(autoChangesetPath) { - if (existsSync(autoChangesetPath)) rmSync(autoChangesetPath) +export function writeAutoChangesets({changesetDir, pkgName, entries, repository}) { + removeAutoChangesets(changesetDir) + return entries.map(entry => { + const filename = `${AUTO_CHANGESET_PREFIX}${entry.shortSha}.md` + const changesetPath = path.join(changesetDir, filename) + writeFileSync(changesetPath, buildChangesetContent(pkgName, entry, repository)) + return changesetPath + }) } export function prepareRelease({ @@ -166,17 +182,16 @@ export function prepareRelease({ } = {}) { const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8')) const changesetDir = path.join(cwd, '.changeset') - const autoChangesetPath = path.join(changesetDir, AUTO_CHANGESET_FILENAME) const explicit = getExplicitChangesets(changesetDir) if (explicit.length > 0) { - removeAutoChangeset(autoChangesetPath) + removeAutoChangesets(changesetDir) return {action: 'skipped-explicit-changeset', changesets: explicit} } const tag = `v${pkg.version}` if (!tagExists(tag, cwd)) { - removeAutoChangeset(autoChangesetPath) + removeAutoChangesets(changesetDir) return {action: 'skipped-pending-publication', tag, version: pkg.version} } @@ -190,7 +205,7 @@ export function prepareRelease({ const repository = normalizeRepository(pkg.repository) const entries = getCommitsSinceTag(`${tag}..HEAD`, cwd, repository) if (entries.length === 0) { - removeAutoChangeset(autoChangesetPath) + removeAutoChangesets(changesetDir) return {action: 'skipped-no-changes', tag} } @@ -205,7 +220,7 @@ export function prepareRelease({ }) if (!securityEntry) { - removeAutoChangeset(autoChangesetPath) + removeAutoChangesets(changesetDir) return { action: 'skipped-recent-dependencies', tag, @@ -216,8 +231,8 @@ export function prepareRelease({ } } - writeFileSync(autoChangesetPath, buildChangesetContent(pkg.name, entries, repository)) - return {action: 'created', tag, path: autoChangesetPath, entries, dependencyOnly} + const paths = writeAutoChangesets({changesetDir, pkgName: pkg.name, entries, repository}) + return {action: 'created', tag, paths, entries, dependencyOnly} } const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) @@ -240,7 +255,7 @@ if (isMain) { ) break case 'created': - console.log(`Created synthetic patch changeset at ${result.path} for ${result.entries.length} change(s) since ${result.tag}.`) + console.log(`Created ${result.paths.length} synthetic patch changeset(s) for changes since ${result.tag}.`) break default: console.log(`Unhandled result: ${JSON.stringify(result)}`) From 6208b54a6b65e0a513eb4f3cda65bb6aa1f4f978 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 21 Aug 2026 17:33:31 -0400 Subject: [PATCH 7/8] Update release preparation policy tests --- scripts/prepare-release.test.mjs | 284 +++++++++++++++++++++++-------- 1 file changed, 212 insertions(+), 72 deletions(-) diff --git a/scripts/prepare-release.test.mjs b/scripts/prepare-release.test.mjs index 6b3d6f7..a05ed84 100644 --- a/scripts/prepare-release.test.mjs +++ b/scripts/prepare-release.test.mjs @@ -1,11 +1,19 @@ import assert from 'node:assert/strict' import {execFileSync} from 'node:child_process' -import {mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync} from 'node:fs' +import {existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs' import os from 'node:os' import path from 'node:path' -import {test, describe, beforeEach, afterEach} from 'node:test' +import {afterEach, beforeEach, describe, test} from 'node:test' -import {prepareRelease, AUTO_CHANGESET_FILENAME, normalizeRepository} from './prepare-release.mjs' +import { + AUTO_CHANGESET_PREFIX, + getTagDate, + isSecurityUpdate, + normalizeRepository, + prepareRelease, +} from './prepare-release.mjs' + +const DAY_MS = 24 * 60 * 60 * 1000 function git(args, cwd) { return execFileSync('git', args, {cwd, encoding: 'utf8'}).trim() @@ -25,6 +33,25 @@ function commit(cwd, message, {merge, body} = {}) { return git(['rev-parse', 'HEAD'], cwd) } +function tagCurrentVersion(cwd) { + git(['tag', 'v0.4.0'], cwd) +} + +function autoChangesets(cwd) { + const changesetDir = path.join(cwd, '.changeset') + return readdirSync(changesetDir) + .filter(name => name.startsWith(AUTO_CHANGESET_PREFIX) && name.endsWith('.md')) + .sort() +} + +function autoChangesetContents(cwd) { + return autoChangesets(cwd).map(name => readFileSync(path.join(cwd, '.changeset', name), 'utf8')) +} + +function daysAfterTag(cwd, days) { + return new Date(getTagDate('v0.4.0', cwd).getTime() + days * DAY_MS) +} + let repoDir beforeEach(() => { @@ -42,7 +69,7 @@ afterEach(() => { }) describe('normalizeRepository', () => { - test('accepts the "owner/repo" shorthand string', () => { + test('accepts the owner/repo shorthand string', () => { assert.equal(normalizeRepository('github/remote-input-element'), 'github/remote-input-element') }) @@ -60,165 +87,278 @@ describe('normalizeRepository', () => { ) }) - test('rejects a non-GitHub URL instead of extracting the wrong owner/repo', () => { + test('rejects a non-GitHub URL', () => { assert.throws(() => normalizeRepository('https://notgithub.com/owner/repo')) }) }) +describe('isSecurityUpdate', () => { + test('detects security labels', () => { + assert.equal(isSecurityUpdate({title: 'Bump a dependency', labels: [{name: 'security'}]}), true) + }) + + test('detects a Dependabot HTML security section', () => { + assert.equal( + isSecurityUpdate({title: 'Bump js-yaml', body: '

Security

Fix potential DoS.

', labels: []}), + true, + ) + }) + + test('detects CVE and GHSA identifiers', () => { + assert.equal(isSecurityUpdate({title: 'Fix CVE-2026-12345', labels: []}), true) + assert.equal(isSecurityUpdate({body: 'Resolves GHSA-abcd-1234-efgh', labels: []}), true) + }) + + test('does not classify an ordinary dependency update as security-related', () => { + assert.equal(isSecurityUpdate({title: 'Bump a dependency', body: 'Routine maintenance', labels: []}), false) + }) +}) + describe('prepareRelease', () => { - test('does nothing when the checked-in version has no matching tag (pending publication)', () => { + test('does nothing when the checked-in version has no matching tag', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') const result = prepareRelease({cwd: repoDir}) assert.equal(result.action, 'skipped-pending-publication') - assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + assert.deepEqual(autoChangesets(repoDir), []) }) test('does nothing when there are no changes after the current release tag', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) + tagCurrentVersion(repoDir) const result = prepareRelease({cwd: repoDir}) assert.equal(result.action, 'skipped-no-changes') - assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + assert.deepEqual(autoChangesets(repoDir), []) }) - test('creates a synthetic patch changeset for changes after the current release tag', () => { + test('creates one top-level synthetic changeset for a feature PR', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) - commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { + tagCurrentVersion(repoDir) + const sha = commit(repoDir, 'Merge pull request #60 from github/some-feature', { merge: true, - body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + body: 'Add a feature', }) const result = prepareRelease({cwd: repoDir}) assert.equal(result.action, 'created') - const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + assert.equal(result.dependencyOnly, false) + assert.deepEqual(autoChangesets(repoDir), [`${AUTO_CHANGESET_PREFIX}${sha.slice(0, 7)}.md`]) + + const [content] = autoChangesetContents(repoDir) assert.match(content, /"@github\/remote-input-element": patch/) - assert.match(content, /Bump the npm_and_yarn group across 1 directory with 2 updates/) - assert.match(content, /\[#53\]\(https:\/\/github\.com\/github\/remote-input-element\/pull\/53\)/) + assert.match(content, /\[#60\]\(https:\/\/github\.com\/github\/remote-input-element\/pull\/60\)/) + assert.match(content, new RegExp(`\\[\\\`${sha.slice(0, 7)}\\\`\\]`)) + assert.match(content, / - Add a feature/) + assert.doesNotMatch(content, /Release unreleased changes:/) + assert.doesNotMatch(content, /^\s*-\s/m) }) - test('handles a single commit without an associated PR', () => { + test('creates a commit-only entry without adding its own list marker', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) + tagCurrentVersion(repoDir) const sha = commit(repoDir, 'Direct fix to main') const result = prepareRelease({cwd: repoDir}) assert.equal(result.action, 'created') - const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') - assert.match(content, /Direct fix to main/) + const [content] = autoChangesetContents(repoDir) assert.match(content, new RegExp(sha.slice(0, 7))) + assert.match(content, / - Direct fix to main/) + assert.doesNotMatch(content, /^\s*-\s/m) }) - test('does not create a synthetic changeset when an explicit changeset exists', () => { + test('creates one deterministic changeset file per release entry', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) - commit(repoDir, 'Merge pull request #60 from github/some-feature', {merge: true, body: 'Add a feature'}) - writeFileSync( - path.join(repoDir, '.changeset', 'some-feature.md'), - '---\n"@github/remote-input-element": minor\n---\n\nAdd a feature\n', - ) + tagCurrentVersion(repoDir) + const firstSha = commit(repoDir, 'Merge pull request #60 from github/feature-one', { + merge: true, + body: 'Add feature one', + }) + const secondSha = commit(repoDir, 'Merge pull request #61 from github/feature-two', { + merge: true, + body: 'Add feature two', + }) const result = prepareRelease({cwd: repoDir}) - assert.equal(result.action, 'skipped-explicit-changeset') - assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + assert.equal(result.action, 'created') + assert.equal(result.paths.length, 2) + assert.deepEqual(autoChangesets(repoDir), [ + `${AUTO_CHANGESET_PREFIX}${firstSha.slice(0, 7)}.md`, + `${AUTO_CHANGESET_PREFIX}${secondSha.slice(0, 7)}.md`, + ].sort()) + + const contents = autoChangesetContents(repoDir) + assert.equal(contents.every(content => content.includes('"@github/remote-input-element": patch')), true) + assert.equal(contents.every(content => !/^\s*-\s/m.test(content)), true) + assert.equal(contents.filter(content => content.includes('#60')).length, 1) + assert.equal(contents.filter(content => content.includes('#61')).length, 1) }) - test('removes a stale synthetic changeset once an explicit changeset is added', () => { + test('is idempotent and rewrites the same automatic files', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) - commit(repoDir, 'Merge pull request #60 from github/dependabot/some-bump', {merge: true, body: 'Bump something'}) + tagCurrentVersion(repoDir) + commit(repoDir, 'Merge pull request #60 from github/some-feature', {merge: true, body: 'Add a feature'}) const first = prepareRelease({cwd: repoDir}) + const firstNames = autoChangesets(repoDir) + const firstContents = autoChangesetContents(repoDir) + const second = prepareRelease({cwd: repoDir}) + assert.equal(first.action, 'created') + assert.equal(second.action, 'created') + assert.deepEqual(autoChangesets(repoDir), firstNames) + assert.deepEqual(autoChangesetContents(repoDir), firstContents) + }) + + test('removes stale automatic files before rewriting the release plan', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + tagCurrentVersion(repoDir) + commit(repoDir, 'Merge pull request #60 from github/some-feature', {merge: true, body: 'Add a feature'}) + writeFileSync(path.join(repoDir, '.changeset', `${AUTO_CHANGESET_PREFIX}stale.md`), 'stale\n') + + prepareRelease({cwd: repoDir}) + + assert.equal(existsSync(path.join(repoDir, '.changeset', `${AUTO_CHANGESET_PREFIX}stale.md`)), false) + assert.equal(autoChangesets(repoDir).length, 1) + }) + + test('explicit changesets take precedence and remove automatic files', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + tagCurrentVersion(repoDir) + commit(repoDir, 'Merge pull request #60 from github/some-feature', {merge: true, body: 'Add a feature'}) + prepareRelease({cwd: repoDir}) + assert.equal(autoChangesets(repoDir).length, 1) writeFileSync( path.join(repoDir, '.changeset', 'explicit.md'), '---\n"@github/remote-input-element": minor\n---\n\nAdd a feature\n', ) - const second = prepareRelease({cwd: repoDir}) - assert.equal(second.action, 'skipped-explicit-changeset') - assert.equal(existsSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME)), false) + const result = prepareRelease({cwd: repoDir}) + + assert.equal(result.action, 'skipped-explicit-changeset') + assert.deepEqual(autoChangesets(repoDir), []) }) - test('is idempotent across repeated runs', () => { + test('waits when only ordinary dependency updates exist and the release is under 30 days old', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) + tagCurrentVersion(repoDir) commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { merge: true, - body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + body: 'Bump the npm_and_yarn group', }) - const first = prepareRelease({cwd: repoDir}) - const firstContent = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') - - const second = prepareRelease({cwd: repoDir}) - const secondContent = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') + let inspected = 0 + const result = prepareRelease({ + cwd: repoDir, + now: daysAfterTag(repoDir, 10), + fetchPullRequest: () => { + inspected += 1 + return {title: 'Bump dependencies', body: 'Routine maintenance', labels: [{name: 'dependencies'}]} + }, + }) - assert.equal(first.action, 'created') - assert.equal(second.action, 'created') - assert.equal(firstContent, secondContent) + assert.equal(result.action, 'skipped-recent-dependencies') + assert.equal(result.releaseAgeDays, 10) + assert.equal(inspected, 1) + assert.deepEqual(autoChangesets(repoDir), []) }) - test('summarizes multiple merged PRs since the release tag', () => { + test('releases dependency updates once the current release is at least 30 days old', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) + tagCurrentVersion(repoDir) commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { merge: true, - body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', - }) - commit(repoDir, 'Merge pull request #54 from github/dependabot/other', { - merge: true, - body: 'Bump other dependency', + body: 'Bump the npm_and_yarn group', }) - const result = prepareRelease({cwd: repoDir}) + const result = prepareRelease({ + cwd: repoDir, + now: daysAfterTag(repoDir, 30), + fetchPullRequest: () => { + throw new Error('security metadata should not be fetched after the age threshold') + }, + }) assert.equal(result.action, 'created') - assert.equal(result.entries.length, 2) - const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') - assert.match(content, /#53/) - assert.match(content, /#54/) + assert.equal(result.dependencyOnly, true) + assert.equal(autoChangesets(repoDir).length, 1) }) - test('does not advance the proposed version when the release tag stays the same across runs', () => { + test('releases a security dependency update immediately', () => { writePackageJson(repoDir, '0.4.0') commit(repoDir, 'chore: initial commit') - git(['tag', 'v0.4.0'], repoDir) + tagCurrentVersion(repoDir) commit(repoDir, 'Merge pull request #53 from github/dependabot/npm_and_yarn', { merge: true, - body: 'Bump the npm_and_yarn group across 1 directory with 2 updates', + body: 'Bump js-yaml from 4.1.0 to 4.2.0', }) - prepareRelease({cwd: repoDir}) - // A second, unrelated change is merged before the release PR is merged. - commit(repoDir, 'Merge pull request #54 from github/dependabot/other', { + const result = prepareRelease({ + cwd: repoDir, + now: daysAfterTag(repoDir, 2), + fetchPullRequest: () => ({ + title: 'Bump js-yaml from 4.1.0 to 4.2.0', + body: '

Security

Fix potential DoS via quadratic complexity.

', + labels: [{name: 'dependencies'}], + }), + }) + + assert.equal(result.action, 'created') + assert.equal(result.dependencyOnly, true) + assert.equal(autoChangesets(repoDir).length, 1) + }) + + test('releases substantive code changes immediately even when the last release is recent', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + tagCurrentVersion(repoDir) + commit(repoDir, 'Merge pull request #60 from github/new-behavior', { merge: true, - body: 'Bump other dependency', + body: 'Add new custom element behavior', + }) + + const result = prepareRelease({ + cwd: repoDir, + now: daysAfterTag(repoDir, 1), + fetchPullRequest: () => { + throw new Error('non-dependency changes should not fetch dependency PR metadata') + }, }) - const result = prepareRelease({cwd: repoDir}) assert.equal(result.action, 'created') - assert.equal(result.tag, 'v0.4.0') - const content = readFileSync(path.join(repoDir, '.changeset', AUTO_CHANGESET_FILENAME), 'utf8') - assert.match(content, /"@github\/remote-input-element": patch/) - // Still only one patch entry in frontmatter -- the release PR would - // still propose 0.4.1, not 0.4.2. - assert.equal((content.match(/patch/g) || []).length, 1) + assert.equal(result.dependencyOnly, false) + assert.equal(autoChangesets(repoDir).length, 1) + }) + + test('multiple patch changesets all target the same single package release', () => { + writePackageJson(repoDir, '0.4.0') + commit(repoDir, 'chore: initial commit') + tagCurrentVersion(repoDir) + commit(repoDir, 'Merge pull request #60 from github/feature-one', {merge: true, body: 'Add feature one'}) + commit(repoDir, 'Merge pull request #61 from github/feature-two', {merge: true, body: 'Add feature two'}) + + prepareRelease({cwd: repoDir}) + + const contents = autoChangesetContents(repoDir) + assert.equal(contents.length, 2) + for (const content of contents) { + assert.match(content, /^---\n"@github\/remote-input-element": patch\n---/) + } }) }) From 701271e2f6af65cb1a81301ac242172ad209130f Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 21 Aug 2026 17:37:47 -0400 Subject: [PATCH 8/8] Document finalized release tracking policy --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1df877d..a10e436 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,11 @@ npm test This project uses [Changesets](https://github.com/changesets/changesets) to manage versioning, changelogs, and publishing to npm. -- **Explicit changesets**: When you make a user-facing change, run `npx changeset` and follow the prompts to describe the change and pick a bump type (`patch`, `minor`, or `major`). Commit the generated file in `.changeset/`. Explicit changesets always take precedence over the automatic behavior described below. -- **Automatic patch releases**: Many merges (for example, Dependabot dependency bumps) don't come with a changeset. The [`release` workflow](.github/workflows/release.yml) runs `scripts/prepare-release.mjs` before invoking `changesets/action`. If no explicit changeset exists, but there are commits merged since the last published release tag, it generates a single synthetic `patch` changeset summarizing those commits (linking pull request numbers when available) so a release PR still gets opened. -- **Release pull request**: On every push to `main` (or a manual run via `workflow_dispatch`), the workflow opens or updates a "Release: version packages" pull request with the version bump and changelog entry. Merging that pull request triggers the same workflow again, which publishes the new version to npm (with [provenance](https://docs.npmjs.com/generating-provenance-statements)) and to GitHub Packages, and creates the matching git tag. -- **Manual runs**: Use the "Run workflow" button on the `Release` workflow in the Actions tab (`workflow_dispatch`) at any time to regenerate or refresh the release pull request without waiting for a push to `main`. +- **Explicit changesets**: For a user-facing change, run `npx changeset`, describe the change, and choose a `patch`, `minor`, or `major` bump. Commit the generated changeset with the change. Explicit changesets take precedence over automatic release entries and are released immediately. +- **Automatic patch changesets**: On pushes to `main`, the [`release` workflow](.github/workflows/release.yml) runs `scripts/prepare-release.mjs`. When there is no explicit changeset, it creates one deterministic patch changeset per unreleased PR or commit. Each release-note entry links the PR (when available) and commit, followed by the change description. +- **Dependency-only releases**: Non-security dependency updates wait until the current release is at least 30 days old. Dependency updates identified as security fixes are released immediately. Substantive non-dependency changes are also released immediately. +- **Release pull request**: The workflow opens or updates a **Release tracking** pull request using the unmodified `changesets/action@v2`. Merging that pull request publishes the package to npm and records the new version and changelog in the repository. +- **Manual runs**: Use the **Run workflow** button for the `Release` workflow (`workflow_dispatch`) to regenerate or refresh release tracking without waiting for another push to `main`. ## License