From 7d9697589d95e19d56dfe257e1b6cf1ee9dff0ac Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubaczkowski Date: Wed, 19 Aug 2026 18:18:18 +0200 Subject: [PATCH] ci: release workflow publishing via npm trusted publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releases needed a TTY: release-it shells out to `npm publish`, npm only offers the browser authorization flow from an interactive terminal, and without one it fails with EOTP asking for a code from an authenticator. Move the whole thing into a manually dispatched workflow. GitHub's OIDC token stands in for npm credentials, so there is nothing to authenticate interactively and no publish token to store — one browser step registers this workflow as the package's trusted publisher and that is the last of it. Provenance comes along for free. The two halves are split because their auth is: release-it does version, commit, tag, push and the GitHub release on GITHUB_TOKEN, then `npm publish` runs on OIDC. `--npm.skipChecks` because release-it's `npm whoami` precheck has no token to check. Also set `git.requireCommits`, which is what stops a stray dispatch from cutting a version with an empty changelog. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 69 +++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 71 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..4278005 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Release + +# Dispatched by hand from the Actions tab. The version comes from the commits +# via conventional-changelog, so there is no bump input to get wrong. +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Compute the version and changelog without publishing or pushing' + type: boolean + default: false + +# Two releases at once would leave the tag and package.json disagreeing. +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + permissions: + # the `chore: release` commit, the tag and the GitHub release + contents: write + # OIDC for npm trusted publishing — this is what replaces a token and 2FA + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # release-it builds the changelog from the last tag onwards, so it + # needs the full history and the tags. + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: yarn typecheck + + - name: Test + run: yarn test + + # Trusted publishing landed in npm 11.5.1; the node from .nvmrc ships 10.x. + - name: Upgrade npm + run: npm install -g npm@latest + + - name: Configure git author + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + # Everything except the publish: bump, commit, tag, push, GitHub release. + # `--no-npm.publish` because the npm half runs separately on OIDC, which + # release-it's `npm whoami` precheck cannot see (hence `--npm.skipChecks`). + - name: Version, tag and GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: yarn release --ci --no-npm.publish --npm.skipChecks ${{ inputs.dry_run && '--dry-run' || '' }} + + # No token: npm exchanges the workflow's OIDC token for publish rights. + # `prepare` (bob build) runs here automatically, and trusted publishing + # generates provenance as a side effect. + - name: Publish to npm + if: ${{ !inputs.dry_run }} + run: npm publish diff --git a/package.json b/package.json index 8414008..4f7760c 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,8 @@ "release-it": { "git": { "commitMessage": "chore: release ${version}", - "tagName": "v${version}" + "tagName": "v${version}", + "requireCommits": true }, "npm": { "publish": true