diff --git a/.changeset/config.json b/.changeset/config.json index cb87a81138..fdcd0b9735 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,17 +1,15 @@ { "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", "changelog": [ - "@changesets/cli/changelog", - { "repo": "0xsequence/sequence.js" } + "@changesets/changelog-github", + { + "repo": "0xsequence/sequence.js" + } ], "commit": false, - "linked": [ - [ - "@0xsequence/*" - ] - ], + "linked": [["@0xsequence/*"]], "access": "public", - "baseBranch": "master", + "baseBranch": "v2", "ignore": [], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "updateInternalDependents": "always" diff --git a/.changeset/dry-jeans-lie.md b/.changeset/dry-jeans-lie.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/dry-jeans-lie.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/changeset-check-trigger.yml b/.github/workflows/changeset-check-trigger.yml new file mode 100644 index 0000000000..8ad09537d5 --- /dev/null +++ b/.github/workflows/changeset-check-trigger.yml @@ -0,0 +1,13 @@ +name: Changeset check + +on: + pull_request: + branches: [v2] + +permissions: + contents: read + pull-requests: write + +jobs: + check: + uses: 0xPolygon/pipelines/.github/workflows/apps-changeset-check.yml@main diff --git a/.github/workflows/npm-release-trigger.yml b/.github/workflows/npm-release-trigger.yml new file mode 100644 index 0000000000..ae259c4a3c --- /dev/null +++ b/.github/workflows/npm-release-trigger.yml @@ -0,0 +1,126 @@ +name: Release + +# Staged npm publishing for v2 via changesets + OIDC trusted publishing. See PUBLISHING.md. +# master has its own copy of this file (same name, so the same npm trusted publisher applies). +# v2 releases stage under the `v2-legacy` dist-tag so they never move `latest`, which is v3. + +on: + push: + branches: [v2] + workflow_dispatch: + inputs: + snapshot_tag: + description: 'npm dist-tag for snapshot (e.g. v2-canary). Non-semver values only.' + required: true + default: 'v2-snapshot' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +# Default-deny: each job declares its own permissions. +permissions: {} + +jobs: + release: + if: github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: write + pull-requests: write + id-token: write # required for npm OIDC trusted publishing + steps: + # Nothing pushes over git (changesets github-api mode writes through the API with the + # app token below), so do not persist the read-only job token into .git/config. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + registry-url: 'https://registry.npmjs.org' + - name: Install npm with staged-publishing support + run: npm install --global npm@11.15.0 + # The release PR changes package manifests without regenerating the lockfile. Link + # co-released workspace packages locally so the not-yet-public versions still install. + # --ignore-scripts: releasing needs no install-time script, and this job holds id-token: write. + - run: pnpm install --no-frozen-lockfile --config.link-workspace-packages=true --ignore-scripts + # postinstall (preconstruct dev) is skipped by --ignore-scripts; changeset:version needs its links. + - run: pnpm dev + - name: Detect release PR merge + id: pending + run: | + if find .changeset -name '*.md' ! -name README.md | grep -q .; then + echo "changesets=true" >> "$GITHUB_OUTPUT" + else + echo "changesets=false" >> "$GITHUB_OUTPUT" + fi + # Only a merged release PR (no pending changesets) stages packages. Run the gates before + # the release bot token exists so build and test code never sees it. + - name: Release gates + if: steps.pending.outputs.changesets == 'false' + run: pnpm run ci:release-gates + # Mint the app token last, scoped to what changesets/action uses, so only that step has it. + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + app-id: ${{ secrets.CHANGESET_RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.CHANGESET_RELEASE_BOT_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Create release PR or stage versioned packages + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + title: 'changesets: Release / Deploy (v2)' + commit: 'release: version packages' + commitMode: github-api + # Also bumps the embedded version constant and the network names doc. + version: pnpm changeset:version + # When a merged release PR has consumed all changesets, this command packs with + # pnpm (rewriting workspace: ranges) and stages the tarballs with npm. Its + # `New tag:` output lets the action create package tags and GitHub Releases. + publish: pnpm run ci:stage-release + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + NPM_STAGE_TAG: v2-legacy + + snapshot: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + pull-requests: read # changelog-github PR lookups + id-token: write # required for npm OIDC trusted publishing + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + registry-url: 'https://registry.npmjs.org' + - name: Install npm with staged-publishing support + run: npm install --global npm@11.15.0 + - run: pnpm install --no-frozen-lockfile --config.link-workspace-packages=true --ignore-scripts + # postinstall (preconstruct dev) is skipped by --ignore-scripts; changeset:version needs its links. + - run: pnpm dev + - name: Version snapshot + env: + GITHUB_TOKEN: ${{ github.token }} + SNAPSHOT_TAG: ${{ inputs.snapshot_tag }} + run: node scripts/prepare-npm-snapshot.mjs + - name: Release gates + run: pnpm run ci:release-gates + - name: Stage snapshot packages + env: + NPM_STAGE_CREATE_GIT_TAGS: 'false' + NPM_STAGE_TAG: ${{ inputs.snapshot_tag }} + run: pnpm run ci:stage-release diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000000..452a642a85 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,26 @@ +# Publishing (v2) + +> **Publishing is CI-only. Never publish or stage from a local machine.** Do not run `changeset:version`, +> `changeset:publish`, `npm publish`, `npm stage publish`, or the internal `ci:stage-release` script yourself. + +v2 uses the same staged publishing flow as `master`; see `PUBLISHING.md` on `master` for the full description, +approval steps, failure recovery, and one-time setup. The differences on this branch: + +- **Dist-tag:** releases stage under **`v2-legacy`**, never `latest` (which belongs to v3). Install v2 with + `pnpm add @0xsequence/@v2-legacy`. +- **Trigger:** merging changesets into `v2` opens a `changesets: Release / Deploy (v2)` PR; merging that PR stages + the packages. Versioning runs `pnpm changeset:version`, which also updates the embedded version constant and the + network names doc. +- **Release gates:** build (which includes typecheck), lint, and the release script tests, run before the release bot + token is minted. The per-package test matrix in + `tests.yml` already runs on every push to `v2`, including the release PR merge. +- **Linked versions:** all `@0xsequence/*` packages are `linked`, so a release stages every package at one version. + +## Day-to-day: add a changeset + +```bash +pnpm changeset +``` + +The `Changeset check` CI job fails a PR into `v2` that touches a package without one +(`pnpm changeset add --empty` for changes with no consumer impact). diff --git a/package.json b/package.json index ed55fe6920..1d2716df3e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "dev": "preconstruct dev", "postinstall": "preconstruct dev", "coverage": "rimraf ./coverage && rimraf ./.nyc_output && nyc pnpm test", - "prepare": "husky install" + "prepare": "husky install", + "test:release": "node --test scripts/*.test.mjs", + "ci:stage-release": "node scripts/stage-npm-packages.mjs", + "ci:release-gates": "pnpm build && pnpm lint && pnpm test:release" }, "husky": { "hooks": { @@ -78,6 +81,7 @@ "prettier": "^3.0.0", "puppeteer": "^23.10.3", "rimraf": "^6.0.1", + "semver": "^7.7.2", "ts-node": "^10.9.2", "tsx": "^4.19.1", "typescript": "~5.6.3", diff --git a/packages/0xsequence/package.json b/packages/0xsequence/package.json index f010b679d3..3cd5db4446 100644 --- a/packages/0xsequence/package.json +++ b/packages/0xsequence/package.json @@ -2,7 +2,11 @@ "name": "0xsequence", "version": "2.3.47", "description": "Sequence: a modular web3 stack and smart wallet for Ethereum chains", - "repository": "https://github.com/0xsequence/sequence.js", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/0xsequence" + }, "source": "src/index.ts", "main": "dist/0xsequence.cjs.js", "module": "dist/0xsequence.esm.js", diff --git a/packages/abi/package.json b/packages/abi/package.json index 8ea838ac7d..1075662ffc 100644 --- a/packages/abi/package.json +++ b/packages/abi/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/abi", "version": "2.3.47", "description": "abi sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/abi", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/abi" + }, "source": "src/index.ts", "main": "dist/0xsequence-abi.cjs.js", "module": "dist/0xsequence-abi.esm.js", diff --git a/packages/account/package.json b/packages/account/package.json index 55890a3baf..93e3c70e7b 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/account", "version": "2.3.47", "description": "tools for migrating sequence wallets to new versions", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/account", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/account" + }, "source": "src/index.ts", "main": "dist/0xsequence-account.cjs.js", "module": "dist/0xsequence-account.esm.js", diff --git a/packages/api/package.json b/packages/api/package.json index d23656c33c..aa17899c7d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/api", "version": "2.3.47", "description": "api sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/api", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/api" + }, "source": "src/index.ts", "main": "dist/0xsequence-api.cjs.js", "module": "dist/0xsequence-api.esm.js", diff --git a/packages/auth/package.json b/packages/auth/package.json index 33fdd84d7b..5a9ecbe2a5 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/auth", "version": "2.3.47", "description": "auth sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/auth", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/auth" + }, "source": "src/index.ts", "main": "dist/0xsequence-auth.cjs.js", "module": "dist/0xsequence-auth.esm.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 5bc33001a3..c232ad167f 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/builder", "version": "2.3.47", "description": "builder sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/builder", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/builder" + }, "source": "src/index.ts", "main": "dist/0xsequence-builder.cjs.js", "module": "dist/0xsequence-builder.esm.js", diff --git a/packages/core/package.json b/packages/core/package.json index c2dcc47935..30524d723d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/core", "version": "2.3.47", "description": "core primitives for interacting with the sequence wallet contracts", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/core", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/core" + }, "source": "src/index.ts", "main": "dist/0xsequence-core.cjs.js", "module": "dist/0xsequence-core.esm.js", diff --git a/packages/deployer/package.json b/packages/deployer/package.json index 187f08f039..748f7a52b0 100644 --- a/packages/deployer/package.json +++ b/packages/deployer/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/deployer", "version": "2.3.47", "description": "deployer sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/deployer", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/deployer" + }, "source": "src/index.ts", "main": "dist/0xsequence-deployer.cjs.js", "module": "dist/0xsequence-deployer.esm.js", diff --git a/packages/guard/package.json b/packages/guard/package.json index a3b07e3948..d3bceba892 100644 --- a/packages/guard/package.json +++ b/packages/guard/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/guard", "version": "2.3.47", "description": "guard sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/guard", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/guard" + }, "source": "src/index.ts", "main": "dist/0xsequence-guard.cjs.js", "module": "dist/0xsequence-guard.esm.js", diff --git a/packages/indexer/package.json b/packages/indexer/package.json index e09840fa09..85361a2153 100644 --- a/packages/indexer/package.json +++ b/packages/indexer/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/indexer", "version": "2.3.47", "description": "indexer sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/indexer", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/indexer" + }, "source": "src/index.ts", "main": "dist/0xsequence-indexer.cjs.js", "module": "dist/0xsequence-indexer.esm.js", diff --git a/packages/marketplace/package.json b/packages/marketplace/package.json index da20abee5d..e0a836ef1c 100644 --- a/packages/marketplace/package.json +++ b/packages/marketplace/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/marketplace", "version": "2.3.47", "description": "marketplace sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/marketplace", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/marketplace" + }, "source": "src/index.ts", "main": "dist/0xsequence-marketplace.cjs.js", "module": "dist/0xsequence-marketplace.esm.js", diff --git a/packages/metadata/package.json b/packages/metadata/package.json index e24fb93907..72782c8e94 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/metadata", "version": "2.3.47", "description": "metadata sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/metadata", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/metadata" + }, "source": "src/index.ts", "main": "dist/0xsequence-metadata.cjs.js", "module": "dist/0xsequence-metadata.esm.js", diff --git a/packages/migration/package.json b/packages/migration/package.json index add2e48863..2121287544 100644 --- a/packages/migration/package.json +++ b/packages/migration/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/migration", "version": "2.3.47", "description": "tools for migrating sequence wallets to new versions", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/migration", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/migration" + }, "source": "src/index.ts", "main": "dist/0xsequence-migration.cjs.js", "module": "dist/0xsequence-migration.esm.js", diff --git a/packages/network/package.json b/packages/network/package.json index d2cc197b13..79f9c5cb34 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/network", "version": "2.3.47", "description": "network sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/network", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/network" + }, "source": "src/index.ts", "main": "dist/0xsequence-network.cjs.js", "module": "dist/0xsequence-network.esm.js", diff --git a/packages/provider/package.json b/packages/provider/package.json index 21e1acaf17..e47245ede4 100644 --- a/packages/provider/package.json +++ b/packages/provider/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/provider", "version": "2.3.47", "description": "provider sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/provider", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/provider" + }, "source": "src/index.ts", "main": "dist/0xsequence-provider.cjs.js", "module": "dist/0xsequence-provider.esm.js", diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 0e623ca7b6..af9bea4720 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/react-native", "version": "2.3.47", "description": "react-native compat-lib sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/react-native", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/react-native" + }, "source": "src/index.ts", "main": "dist/0xsequence-react-native.cjs.js", "module": "dist/0xsequence-react-native.esm.js", diff --git a/packages/relayer/package.json b/packages/relayer/package.json index e5aa26db5e..9582a0d1e9 100644 --- a/packages/relayer/package.json +++ b/packages/relayer/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/relayer", "version": "2.3.47", "description": "relayer sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/relayer", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/relayer" + }, "source": "src/index.ts", "main": "dist/0xsequence-relayer.cjs.js", "module": "dist/0xsequence-relayer.esm.js", diff --git a/packages/replacer/package.json b/packages/replacer/package.json index f59344e5ac..76c5794fb8 100644 --- a/packages/replacer/package.json +++ b/packages/replacer/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/replacer", "version": "2.3.47", "description": "EIP-5719 client implementation", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/replacer", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/replacer" + }, "source": "src/index.ts", "main": "dist/0xsequence-replacer.cjs.js", "module": "dist/0xsequence-replacer.esm.js", diff --git a/packages/sessions/package.json b/packages/sessions/package.json index 7473cd4ff3..dd419598fc 100644 --- a/packages/sessions/package.json +++ b/packages/sessions/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/sessions", "version": "2.3.47", "description": "tools for migrating sequence wallets to new versions", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/sessions", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/sessions" + }, "source": "src/index.ts", "main": "dist/0xsequence-sessions.cjs.js", "module": "dist/0xsequence-sessions.esm.js", diff --git a/packages/signhub/package.json b/packages/signhub/package.json index 940e130105..7d68105aff 100644 --- a/packages/signhub/package.json +++ b/packages/signhub/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/signhub", "version": "2.3.47", "description": "orchestrates a series of signers, provides visibility into the signing process, and to the signers themselves", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/signhub", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/signhub" + }, "source": "src/index.ts", "main": "dist/0xsequence-signhub.cjs.js", "module": "dist/0xsequence-signhub.esm.js", diff --git a/packages/tests/package.json b/packages/tests/package.json index 83a08bf659..68889df72c 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/tests", "version": "2.3.47", "description": "test tools for sequence.js", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/tests", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/tests" + }, "source": "src/index.ts", "main": "dist/0xsequence-tests.cjs.js", "module": "dist/0xsequence-tests.esm.js", diff --git a/packages/utils/package.json b/packages/utils/package.json index 1d573765ab..661b1cbbc3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/utils", "version": "2.3.47", "description": "utils sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/utils", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/utils" + }, "source": "src/index.ts", "main": "dist/0xsequence-utils.cjs.js", "module": "dist/0xsequence-utils.esm.js", diff --git a/packages/waas-ethers/package.json b/packages/waas-ethers/package.json index bb6cd0173c..7006f896d6 100644 --- a/packages/waas-ethers/package.json +++ b/packages/waas-ethers/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/waas-ethers", "version": "2.3.47", "description": "waas ethers wrapper", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/waas", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/waas-ethers" + }, "source": "src/index.ts", "main": "dist/0xsequence-waas-ethers.cjs.js", "module": "dist/0xsequence-waas-ethers.esm.js", diff --git a/packages/waas/package.json b/packages/waas/package.json index f8f39e24d8..014184aee2 100644 --- a/packages/waas/package.json +++ b/packages/waas/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/waas", "version": "2.3.47", "description": "waas session client", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/waas", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/waas" + }, "source": "src/index.ts", "main": "dist/0xsequence-waas.cjs.js", "module": "dist/0xsequence-waas.esm.js", diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 8fcb2d18fd..1ddb66c669 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -2,7 +2,11 @@ "name": "@0xsequence/wallet", "version": "2.3.47", "description": "wallet sub-package for Sequence", - "repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/wallet", + "repository": { + "type": "git", + "url": "git+https://github.com/0xsequence/sequence.js.git", + "directory": "packages/wallet" + }, "source": "src/index.ts", "main": "dist/0xsequence-wallet.cjs.js", "module": "dist/0xsequence-wallet.esm.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7be2d7bd75..475d44c0d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + semver: + specifier: ^7.7.2 + version: 7.7.2 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.7.9)(typescript@5.6.3) @@ -2287,6 +2290,7 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vercel/nft@0.26.5': resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==} @@ -2779,6 +2783,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} @@ -4380,6 +4385,7 @@ packages: glob@11.0.0: resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@5.0.15: @@ -4388,20 +4394,20 @@ packages: glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -6201,7 +6207,7 @@ packages: puppeteer@23.10.3: resolution: {integrity: sha512-ODG+L9vCSPkQ1j+yDtNDdkSsWt2NXNrQO5C8MlwkYgE2hYnXdqVRbBpsHnoP7+EULJJKbWyR2Q4BdfohjQor3A==} engines: {node: '>=18'} - deprecated: < 24.10.2 is no longer supported + deprecated: < 24.15.0 is no longer supported hasBin: true qs@6.13.0: @@ -6276,6 +6282,7 @@ packages: realistic-structured-clone@3.0.0: resolution: {integrity: sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} @@ -6524,11 +6531,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -6954,10 +6956,12 @@ packages: tar@4.4.19: resolution: {integrity: sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==} engines: {node: '>=4.5'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me temp-dir@3.0.0: resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} @@ -7349,15 +7353,17 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -8550,7 +8556,7 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.2 '@changesets/assemble-release-plan@6.0.4': dependencies: @@ -8559,7 +8565,7 @@ snapshots: '@changesets/should-skip-package': 0.1.1 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 + semver: 7.7.2 '@changesets/changelog-git@0.2.0': dependencies: @@ -8600,7 +8606,7 @@ snapshots: package-manager-detector: 0.2.2 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.2 spawndamnit: 2.0.0 term-size: 2.2.1 @@ -8623,7 +8629,7 @@ snapshots: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 - semver: 7.6.3 + semver: 7.7.2 '@changesets/get-github-info@0.6.0': dependencies: @@ -9357,7 +9363,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.3 + semver: 7.7.2 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -9615,7 +9621,7 @@ snapshots: quick-lru: 5.1.1 resolve-from: 5.0.0 rollup: 2.79.2 - semver: 7.6.3 + semver: 7.7.2 terser: 5.36.0 v8-compile-cache: 2.4.0 zod: 3.23.8 @@ -9637,7 +9643,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.6.3 + semver: 7.7.2 tar-fs: 3.0.6 unbzip2-stream: 1.4.3 yargs: 17.7.2 @@ -10008,7 +10014,7 @@ snapshots: fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -11101,7 +11107,7 @@ snapshots: js-string-escape: 1.0.1 lodash: 4.17.21 md5-hex: 3.0.1 - semver: 7.6.3 + semver: 7.7.2 well-known-symbols: 2.0.0 concurrently@9.0.1: @@ -13519,7 +13525,7 @@ snapshots: '@babel/parser': 7.25.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -13800,7 +13806,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.2 make-error@1.3.6: {} @@ -14049,7 +14055,7 @@ snapshots: performance-now: 2.1.0 portfinder: 1.0.32(supports-color@6.1.0) read-tls-client-hello: 1.0.1 - semver: 7.6.3 + semver: 7.7.2 socks-proxy-agent: 7.0.0 typed-error: 3.2.2 urlpattern-polyfill: 8.0.2 @@ -15094,8 +15100,6 @@ snapshots: semver@6.3.1: {} - semver@7.6.3: {} - semver@7.7.2: {} send@0.19.0(supports-color@6.1.0): diff --git a/scripts/prepare-npm-snapshot.mjs b/scripts/prepare-npm-snapshot.mjs new file mode 100644 index 0000000000..e2871994de --- /dev/null +++ b/scripts/prepare-npm-snapshot.mjs @@ -0,0 +1,106 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { readFile, unlink, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +import semver from 'semver' + +// Versions every publishable package as `0.0.0--` on the CI runner (never +// committed) so stage-npm-packages.mjs can stage a throwaway snapshot. See PUBLISHING.md. + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const changesetDirectory = path.join(repositoryRoot, '.changeset') + +export function validateSnapshotTag(tag) { + assert(tag, 'SNAPSHOT_TAG is required.') + assert.notEqual(tag, 'latest', "The snapshot tag must not be 'latest'.") + assert.notEqual(tag, 'v2-legacy', "The snapshot tag must not be 'v2-legacy'.") + assert( + /^[a-z][a-z0-9._-]*$/.test(tag), + 'The snapshot tag must start with a lowercase letter and contain only lowercase letters, numbers, dots, underscores, or hyphens.' + ) + // npm rejects any dist-tag it can parse as a semver range (e.g. '1.2.3', 'v1.x', 'x'). + assert.equal(semver.validRange(tag), null, 'The snapshot tag must not be a version or semver range.') +} + +// Every publishable package is bumped so snapshot packages only ever depend on each other, +// never on a workspace version that is not on npm. +export function snapshotChangeset(packageNames) { + const releases = packageNames.map(name => `'${name}': patch`).join('\n') + return `---\n${releases}\n---\n\nCreate a temporary CI snapshot release.\n` +} + +export function assertSnapshotVersions(before, after, tag) { + assert(after.length > 0, 'No publishable packages were found.') + assert.deepEqual( + after.map(({ name }) => name).sort(), + before.map(({ name }) => name).sort(), + 'The publishable package set changed unexpectedly.' + ) + + for (const currentPackage of after) { + const previousPackage = before.find(({ name }) => name === currentPackage.name) + assert.notEqual(currentPackage.version, previousPackage.version, `Snapshot versioning did not change ${currentPackage.name}.`) + assert( + currentPackage.version.includes(`-${tag}-`), + `${currentPackage.name} did not receive the requested '${tag}' snapshot version.` + ) + } +} + +function runChecked(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: repositoryRoot, + encoding: 'utf8', + stdio: 'inherit', + ...options + }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.`) + } + return result +} + +async function readReleasePackages() { + const result = runChecked('pnpm', ['--recursive', 'list', '--depth', '-1', '--json'], { stdio: 'pipe' }) + const workspaces = JSON.parse(result.stdout).filter(workspace => workspace.private !== true) + return Promise.all( + workspaces.map(async workspace => { + const manifest = JSON.parse(await readFile(path.join(workspace.path, 'package.json'), 'utf8')) + return { name: manifest.name, version: manifest.version } + }) + ) +} + +async function main() { + const tag = process.env.SNAPSHOT_TAG + validateSnapshotTag(tag) + + const before = await readReleasePackages() + const temporaryChangeset = path.join(changesetDirectory, `ci-snapshot-${process.pid}.md`) + await writeFile(temporaryChangeset, snapshotChangeset(before.map(({ name }) => name)), { + encoding: 'utf8', + flag: 'wx' + }) + + try { + runChecked('pnpm', ['exec', 'changeset', 'version', '--snapshot', tag]) + } finally { + await unlink(temporaryChangeset).catch(error => { + if (error.code !== 'ENOENT') throw error + }) + } + + const after = await readReleasePackages() + assertSnapshotVersions(before, after, tag) + console.log(`Prepared snapshot versions:\n${after.map(({ name, version }) => ` ${name}@${version}`).join('\n')}`) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch(error => { + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 + }) +} diff --git a/scripts/prepare-npm-snapshot.test.mjs b/scripts/prepare-npm-snapshot.test.mjs new file mode 100644 index 0000000000..b5bcec0a15 --- /dev/null +++ b/scripts/prepare-npm-snapshot.test.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { assertSnapshotVersions, snapshotChangeset, validateSnapshotTag } from './prepare-npm-snapshot.mjs' + +test('accepts safe snapshot tags', () => { + for (const tag of ['snapshot', 'canary', 'pre-3.0.14']) { + assert.doesNotThrow(() => validateSnapshotTag(tag)) + } +}) + +test('rejects release dist-tags, version-like, and malformed snapshot tags', () => { + for (const tag of [ + '', + 'latest', + 'v2-legacy', + '3.0.14', + 'v3.0.14', + '3.0.14-beta.1', + 'beta tag', + 'Beta', + 'x', + 'x.x', + 'v1.x', + 'v3' + ]) { + assert.throws(() => validateSnapshotTag(tag)) + } +}) + +test('builds a changeset that bumps every publishable package', () => { + assert.equal( + snapshotChangeset(['@0xsequence/wallet-core', '@0xsequence/abi']), + "---\n'@0xsequence/wallet-core': patch\n'@0xsequence/abi': patch\n---\n\nCreate a temporary CI snapshot release.\n" + ) +}) + +test('requires every publishable package to receive the requested snapshot version', () => { + const before = [ + { name: '@0xsequence/wallet-core', version: '3.0.14' }, + { name: '@0xsequence/wallet-wdk', version: '3.1.2' } + ] + const after = before.map(({ name }) => ({ name, version: '0.0.0-canary-20261014000000' })) + + assert.doesNotThrow(() => assertSnapshotVersions(before, after, 'canary')) + assert.throws(() => assertSnapshotVersions(before, before, 'canary'), /Snapshot versioning did not change/) + assert.throws(() => assertSnapshotVersions(before, [after[0], before[1]], 'canary'), /did not change @0xsequence\/wallet-wdk/) + assert.throws(() => assertSnapshotVersions(before, [after[0]], 'canary'), /package set changed/) +}) diff --git a/scripts/stage-npm-packages.mjs b/scripts/stage-npm-packages.mjs new file mode 100644 index 0000000000..577cad78ab --- /dev/null +++ b/scripts/stage-npm-packages.mjs @@ -0,0 +1,211 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { appendFile, mkdtemp, readFile } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +// Packs every publishable workspace package whose version is not yet on npm and sends the +// tarballs to `npm stage publish` via OIDC trusted publishing. CI-only: see PUBLISHING.md. + +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +const dependencyFields = ['dependencies', 'peerDependencies', 'optionalDependencies'] + +export function selectReleasePackages(workspaces) { + const releasePackages = workspaces.filter(workspace => workspace.private !== true) + assert(releasePackages.length > 0, 'No publishable workspace packages were found.') + for (const workspace of releasePackages) { + assert(workspace.name && workspace.version, `Publishable package at ${workspace.path} needs a name and version.`) + } + const names = releasePackages.map(workspace => workspace.name) + assert.equal(new Set(names).size, names.length, 'Publishable package names must be unique.') + return releasePackages +} + +// Orders packages so that every package comes after the workspace packages it depends on. +// Maintainers approve staged packages in this order, so dependencies become installable first. +export function orderByDependencies(releasePackages) { + const names = new Set(releasePackages.map(({ name }) => name)) + const remaining = releasePackages.map(releasePackage => ({ + releasePackage, + dependsOn: new Set((releasePackage.dependsOn ?? []).filter(name => names.has(name) && name !== releasePackage.name)) + })) + const ordered = [] + const placed = new Set() + while (remaining.length > 0) { + const index = remaining.findIndex(({ dependsOn }) => [...dependsOn].every(name => placed.has(name))) + assert(index !== -1, `Workspace dependency cycle between: ${remaining.map(r => r.releasePackage.name).join(', ')}`) + const [{ releasePackage }] = remaining.splice(index, 1) + ordered.push(releasePackage) + placed.add(releasePackage.name) + } + return ordered +} + +export function workspaceDependencyNames(manifest) { + return [...new Set(dependencyFields.flatMap(field => Object.keys(manifest[field] ?? {})))] +} + +export function findWorkspaceReferences(value, currentPath = 'package.json') { + if (typeof value === 'string') { + return value.startsWith('workspace:') ? [currentPath] : [] + } + if (Array.isArray(value)) { + return value.flatMap((item, index) => findWorkspaceReferences(item, `${currentPath}[${index}]`)) + } + if (value && typeof value === 'object') { + return Object.entries(value).flatMap(([key, item]) => findWorkspaceReferences(item, `${currentPath}.${key}`)) + } + return [] +} + +export function assertTrustedPublishingEnvironment(environment) { + assert.equal(environment.GITHUB_ACTIONS, 'true', 'Staged publishing is CI-only and must run in GitHub Actions.') + assert( + environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN && environment.ACTIONS_ID_TOKEN_REQUEST_URL, + 'GitHub Actions OIDC is unavailable; the job needs id-token: write permission.' + ) +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: repositoryRoot, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + ...options + }) + if (result.error) throw result.error + return result +} + +function runChecked(command, args, options = {}) { + const result = run(command, args, options) + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}.\n${result.stderr ?? ''}`) + } + return result +} + +function hasGitTag(tag) { + const result = run('git', ['show-ref', '--verify', '--quiet', `refs/tags/${tag}`]) + if (result.status !== 0 && result.status !== 1) { + throw new Error(`Unable to check git tag ${tag}.`) + } + return result.status === 0 +} + +function isPublished({ name, version }) { + const packageSpec = `${name}@${version}` + const result = run('npm', ['view', packageSpec, 'version', '--json']) + if (result.status === 0) { + // npm prints nothing (exit 0) when the package exists but the version does not. + return result.stdout.trim() !== '' && JSON.parse(result.stdout) === version + } + if (/E404|404 Not Found/.test(`${result.stdout}\n${result.stderr}`)) { + return false + } + throw new Error(`Unable to determine whether ${packageSpec} is published.\n${result.stderr}`) +} + +function tarballName({ name, version }) { + return `${name.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz` +} + +async function packPackage(releasePackage, outputDirectory) { + const tarballPath = path.join(outputDirectory, tarballName(releasePackage)) + runChecked('pnpm', ['--dir', releasePackage.path, 'pack', '--out', tarballPath], { + stdio: 'inherit' + }) + + const manifestResult = runChecked('tar', ['-xOf', tarballPath, 'package/package.json']) + const packedManifest = JSON.parse(manifestResult.stdout) + assert.equal(packedManifest.name, releasePackage.name, 'Packed package name changed unexpectedly.') + assert.equal(packedManifest.version, releasePackage.version, 'Packed package version changed unexpectedly.') + assert.deepEqual( + findWorkspaceReferences(packedManifest), + [], + `pnpm did not rewrite every workspace: reference in ${releasePackage.name}.` + ) + return tarballPath +} + +async function readReleasePackages() { + const workspacesResult = runChecked('pnpm', ['--recursive', 'list', '--depth', '-1', '--json']) + const releasePackages = selectReleasePackages(JSON.parse(workspacesResult.stdout)) + const withDependencies = await Promise.all( + releasePackages.map(async releasePackage => { + const manifest = JSON.parse(await readFile(path.join(releasePackage.path, 'package.json'), 'utf8')) + return { ...releasePackage, dependsOn: workspaceDependencyNames(manifest) } + }) + ) + return orderByDependencies(withDependencies) +} + +async function writeSummary(lines) { + if (!process.env.GITHUB_STEP_SUMMARY) return + await appendFile(process.env.GITHUB_STEP_SUMMARY, `${lines.join('\n')}\n`, 'utf8') +} + +async function main() { + assertTrustedPublishingEnvironment(process.env) + const distTag = process.env.NPM_STAGE_TAG || 'latest' + const createGitTags = process.env.NPM_STAGE_CREATE_GIT_TAGS !== 'false' + + const releasePackages = await readReleasePackages() + const releaseStates = releasePackages.map(releasePackage => { + const tag = `${releasePackage.name}@${releasePackage.version}` + // A tagged but unpublished version was staged by an earlier run and still awaits approval. + const tagged = createGitTags && hasGitTag(tag) + const published = isPublished(releasePackage) + return { releasePackage, tag, tagged, published } + }) + const packagesToStage = releaseStates.filter(state => !state.tagged && !state.published) + + const outputDirectory = packagesToStage.length > 0 ? await mkdtemp(path.join(os.tmpdir(), 'sequence-npm-stage-')) : null + const stagedNow = new Set() + for (const { releasePackage } of packagesToStage) { + const tarballPath = await packPackage(releasePackage, outputDirectory) + runChecked('npm', ['stage', 'publish', tarballPath, '--tag', distTag, '--access', 'public', '--provenance'], { + stdio: 'inherit' + }) + stagedNow.add(releasePackage.name) + // changesets/action v1 creates this package's tag and GitHub Release from this line, + // even if a later package fails. A retry can then skip the already-staged package by tag. + if (createGitTags) { + console.log(`New tag: ${releasePackage.name}@${releasePackage.version}`) + } + } + + const summary = approvalSummary(releaseStates, stagedNow, distTag) + console.log(summary.join('\n')) + await writeSummary(summary) +} + +// Lists every unpublished version of the release in dependency order, including versions an +// earlier (partially failed) run staged, so maintainers approve dependencies first. +export function approvalSummary(releaseStates, stagedNow, distTag) { + const pending = releaseStates.filter(state => !state.published) + if (pending.length === 0) { + return ['### npm staged publishing', '', 'No package versions needed staging.'] + } + return [ + '### npm staged publishing', + '', + `Approve in this order (dependencies first); all are staged under \`${distTag}\`:`, + '', + ...pending.map(({ releasePackage: { name, version } }) => { + const origin = stagedNow.has(name) ? 'staged by this run' : 'staged by an earlier run' + return `1. \`${name}@${version}\` (${origin})` + }), + '', + 'A maintainer must approve every staged package on npm with 2FA before it is installable.' + ] +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch(error => { + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 + }) +} diff --git a/scripts/stage-npm-packages.test.mjs b/scripts/stage-npm-packages.test.mjs new file mode 100644 index 0000000000..44fa34278f --- /dev/null +++ b/scripts/stage-npm-packages.test.mjs @@ -0,0 +1,113 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { + approvalSummary, + assertTrustedPublishingEnvironment, + findWorkspaceReferences, + orderByDependencies, + selectReleasePackages, + workspaceDependencyNames +} from './stage-npm-packages.mjs' + +test('selects only publishable workspace packages', () => { + const workspaces = [ + { name: 'sequence-core', version: null, private: true, path: '/repo' }, + { name: '@0xsequence/wallet-core', version: '3.0.14', private: false, path: '/repo/packages/wallet/core' }, + { name: '@0xsequence/abi', version: '3.0.13', path: '/repo/packages/utils/abi' } + ] + assert.deepEqual( + selectReleasePackages(workspaces).map(({ name }) => name), + ['@0xsequence/wallet-core', '@0xsequence/abi'] + ) +}) + +test('rejects publishable packages without a version', () => { + assert.throws( + () => selectReleasePackages([{ name: '@0xsequence/abi', path: '/repo/packages/utils/abi' }]), + /needs a name and version/ + ) +}) + +test('rejects an empty release set', () => { + assert.throws(() => selectReleasePackages([{ name: 'root', private: true }]), /No publishable/) +}) + +test('orders packages after the workspace packages they depend on', () => { + const ordered = orderByDependencies([ + { name: 'wdk', dependsOn: ['core', 'primitives', 'ox'] }, + { name: 'core', dependsOn: ['primitives'] }, + { name: 'abi', dependsOn: [] }, + { name: 'primitives', dependsOn: [] } + ]) + assert.deepEqual( + ordered.map(({ name }) => name), + ['abi', 'primitives', 'core', 'wdk'] + ) +}) + +test('rejects workspace dependency cycles', () => { + assert.throws( + () => + orderByDependencies([ + { name: 'a', dependsOn: ['b'] }, + { name: 'b', dependsOn: ['a'] } + ]), + /dependency cycle/ + ) +}) + +test('collects runtime dependency names but not devDependencies', () => { + assert.deepEqual( + workspaceDependencyNames({ + dependencies: { a: 'workspace:^' }, + peerDependencies: { b: '^1.0.0', a: 'workspace:^' }, + devDependencies: { c: 'workspace:*' } + }), + ['a', 'b'] + ) +}) + +test('finds nested workspace protocol references in packed manifests', () => { + assert.deepEqual( + findWorkspaceReferences({ + peerDependencies: { sdk: 'workspace:^' }, + publishConfig: { dependencies: { sdk: '^1.2.3' } } + }), + ['package.json.peerDependencies.sdk'] + ) +}) + +test('requires GitHub Actions OIDC before staging', () => { + assert.throws(() => assertTrustedPublishingEnvironment({}), /CI-only/) + assert.throws(() => assertTrustedPublishingEnvironment({ GITHUB_ACTIONS: 'true' }), /OIDC is unavailable/) + assert.doesNotThrow(() => + assertTrustedPublishingEnvironment({ + GITHUB_ACTIONS: 'true', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', + ACTIONS_ID_TOKEN_REQUEST_URL: 'https://example.test/oidc' + }) + ) +}) + +test('approval summary keeps earlier-run stages in dependency order and skips published versions', () => { + const state = (name, published) => ({ releasePackage: { name, version: '3.0.14' }, published }) + const summary = approvalSummary( + [state('primitives', false), state('abi', true), state('core', false), state('wdk', false)], + new Set(['core', 'wdk']), + 'latest' + ) + assert.deepEqual( + summary.filter(line => line.startsWith('1. ')), + [ + '1. `primitives@3.0.14` (staged by an earlier run)', + '1. `core@3.0.14` (staged by this run)', + '1. `wdk@3.0.14` (staged by this run)' + ] + ) +}) + +test('approval summary reports when nothing is pending', () => { + const summary = approvalSummary([{ releasePackage: { name: 'abi', version: '3.0.14' }, published: true }], new Set(), 'latest') + assert.match(summary.join('\n'), /No package versions needed staging/) +})