From a5d73d88b03fd2617edea5714326faa6f6e54e12 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 25 Sep 2026 14:24:15 +0200 Subject: [PATCH] ci: publish to npm from changesets with trusted publishing A release workflow built from changesets/action v2's sub-actions: with changesets on main it opens a "Version Packages" pull request; once that is merged, it packs the packages without credentials and publishes the tarballs in a separate job, the only one allowed to mint an OIDC token for npm trusted publishing. No npm token is stored anywhere. Needs two settings before it can work: the trusted publisher on the package's npmjs.com page (this repository, workflow release.yml), and "Allow GitHub Actions to create and approve pull requests" in the repository's Actions settings. --- .github/workflows/release.yml | 114 ++++++++++++++++++++++++++++++++++ AGENTS.md | 7 ++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c62560d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +# Versions and publishes the packages with Changesets. +# +# With changesets on main, `version` opens or updates the "Version Packages" +# pull request. Merging it leaves no changesets, so the next run packs and +# publishes whatever version npm does not have yet. +# +# Publishing uses npm trusted publishing: npm accepts an OIDC token from this +# workflow file in this repository, so there is no npm token to store or leak. +# The file name is part of that trust configuration on npmjs.com; renaming the +# file breaks publishing until the setting is changed too. +# +# Only the publish job may mint the OIDC token. It installs nothing from the +# lockfile's scripts and builds nothing: the pack job builds without any +# credential and hands over tarballs. +name: release + +on: + push: + branches: [main] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + select-mode: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + mode: ${{ steps.select-mode.outputs.mode }} + publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - id: select-mode + uses: changesets/action/select-mode@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 + + version: + needs: select-mode + if: needs.select-mode.outputs.mode == 'version' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: changesets/action/version@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 + with: + commit-message: "chore: version packages" + pr-title: "chore: version packages" + + pack: + needs: select-mode + if: needs.select-mode.outputs.mode == 'publish' + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + pack-dir-artifact-id: ${{ steps.pack.outputs.pack-dir-artifact-id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - id: pack + uses: changesets/action/pack@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 + with: + publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }} + + publish: + needs: pack + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + package-manager-cache: false + # Trusted publishing needs pnpm 11.1.3 or later (the OIDC fix for an + # .npmrc written by setup-node) and, for npm itself, 11.5.1 or later. + - run: pnpm --version && npm --version + - run: pnpm install --frozen-lockfile --ignore-scripts + - uses: changesets/action/publish@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 + with: + pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }} diff --git a/AGENTS.md b/AGENTS.md index 43766b3..f2231c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,11 @@ Never name a package script `publish`, `version` or `prepare`: npm and pnpm run scripts with those names on their own during a publish or a version bump. `emdash-plugin init` generates `"publish": "emdash-plugin publish"`, which would push to the EmDash registry after every `npm publish`; here it -is `registry:publish`, and `prepublishOnly` builds before any publish. The workspace +is `registry:publish`, and `prepublishOnly` builds before any publish. + +`.github/workflows/release.yml` does the npm side: with changesets on main +it opens a "Version Packages" pull request, and merging that publishes +through npm trusted publishing. npm trusts that workflow by its file name, +so renaming it breaks publishing until the setting on npmjs.com changes too. The workspace installs with pnpm 11; `allowBuilds` in `pnpm-workspace.yaml` lets esbuild and workerd run their install scripts, which the test host needs.