From 5435ccda21cf7da5c6edc38605781d5e2b8fe41d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 19:05:46 +0000 Subject: [PATCH 1/2] ci: promote over Trusted Publishing, and close the release there Promotion was the last workflow holding an NPM_TOKEN, and it had gone stale unnoticed: publishing moved to Trusted Publishing, which does not read the secret, so nothing exercised it until 2.7.0 needed the tag moved and the registry answered 401. npm authorizes dist-tag over OIDC from 11.21.0 (npm/cli#10038), so the job now upgrades npm and asserts the version rather than failing later inside the registry call. The grant is separate from publish and off by default, so the dist-tag failure path names the setting to enable. Linear and Slack ran only from a `latest` publish, which the beta-then-promote flow never reaches, so promoting left the release open. They run here too. The checkout read `github.event.pull_request.head.sha`, empty on a dispatch, and `npm ci` installed dependencies that `npm dist-tag` does not use. The `latest` guard accepted `refs/tags/release/*`, which release-it cannot produce: it tags `v${version}`. Only the branch arm could ever match, and the tag of a release failing the guard reads as a mistake. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y4EZAEvxLfXuMeNz5ZgCMz --- .github/workflows/promote.yaml | 59 ++++++++++++++++++++++++++++------ .github/workflows/publish.yaml | 5 +-- RELEASE.md | 8 ++++- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.github/workflows/promote.yaml b/.github/workflows/promote.yaml index 9acaeac..78241a9 100644 --- a/.github/workflows/promote.yaml +++ b/.github/workflows/promote.yaml @@ -8,6 +8,10 @@ on: type: string description: The version that should be promoted +permissions: + contents: read + id-token: write + jobs: publish: runs-on: ubuntu-latest @@ -17,20 +21,57 @@ jobs: steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 with: node-version: "24.x" registry-url: "https://registry.npmjs.org" - - run: npm ci - - name: Publish to NPM + # The runner's bundled npm predates OIDC support for dist-tag. + - name: Update npm to support Trusted Publishing + run: npm install -g npm@latest + + - name: Check npm can authorize dist-tag over OIDC + run: | + have=$(npm --version) + need=11.21.0 + if [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -1)" != "$need" ]; then + echo "::error::npm $have cannot authorize dist-tag over OIDC; needs >= $need (npm/cli#10038)." + exit 1 + fi + + - name: Promote to latest run: | - git config user.name "Currents NPM Bot" - git config user.email "npm@currents.dev" - npm dist-tag add @currents/mcp@${{ github.event.inputs.packageVersion }} latest + if ! npm dist-tag add @currents/mcp@${{ github.event.inputs.packageVersion }} latest; then + echo "::error::The registry refused the dist-tag. Trusted publishing grants it separately from publish, and it is off by default: enable 'Allow npm dist-tag' on the trusted publisher configuration for @currents/mcp at https://www.npmjs.com/package/@currents/mcp/access" + exit 1 + fi npm dist-tag rm @currents/mcp beta - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Mark Linear release as complete + id: release + uses: linear/linear-release-action@v0 + with: + access_key: ${{ secrets.LINEAR_ACCESS_KEY }} + command: complete + version: ${{ github.event.inputs.packageVersion }} + + - name: Post to Slack + if: success() && steps.release.outputs.release-url + uses: slackapi/slack-github-action@v2 + with: + webhook: ${{ secrets.SLACK_RELEASE_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + { + "text": "πŸš€ Release: @currents/mcp@${{ github.event.inputs.packageVersion }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*πŸš€Release: @currents/mcp@${{ github.event.inputs.packageVersion }}* has been marked as complete in Linear.\n\n<${{ steps.release.outputs.release-url }}|View Release Notes>" + } + } + ] + } diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4300057..8ffa92b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -34,9 +34,10 @@ jobs: - name: Validate release branch for latest channel if: github.event.inputs.channel == 'latest' run: | - if [[ ! "${{ github.ref }}" =~ ^refs/(heads|tags)/release/ ]]; then - echo "Error: Publishing to 'latest' channel requires a release/* branch or tag" + if [[ ! "${{ github.ref }}" =~ ^refs/heads/release/ ]]; then + echo "Error: Publishing to 'latest' channel requires a release/* branch" echo "Current ref: ${{ github.ref }}" + echo "Release tags are named v, so the tag of a release does not match." exit 1 fi diff --git a/RELEASE.md b/RELEASE.md index e44c251..91ee3a1 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -105,9 +105,15 @@ The workflow will: 1. Add the `latest` dist-tag to the specified version 2. Remove the `beta` dist-tag from that version +3. Mark the release complete in Linear and post it to Slack This is useful when you've tested a beta release and want to make it the default install without rebuilding. +The workflow authenticates with npm through Trusted Publishing, so it needs no +`NPM_TOKEN`. Trusted publishing grants dist-tag separately from publish, and the +grant is off by default: **Allow npm dist-tag** has to be enabled on the trusted +publisher configuration for `@currents/mcp`, or the registry refuses the tag. + ## Linear Release The repository follows a branch-cut release model with Linear Releases integration: @@ -116,7 +122,7 @@ The repository follows a branch-cut release model with Linear Releases integrati 2. **Release branch creation** β€” when a `release/*` branch is pushed, it automatically: - Syncs with Linear Release 3. **Stabilization** β€” only bug fixes and critical changes are allowed on the release branch -4. **Publishing** β€” when publishing to npm via the "Publish NPM Package" workflow with `latest` channel, the release is automatically marked as completed in Linear and posted to Slack +4. **Publishing** β€” the release is marked complete in Linear and posted to Slack when a version reaches the `latest` channel, whether by "NPM - Promote beta to latest" or by "Publish NPM Package" with the `latest` channel The workflow is defined in `.github/workflows/linear-release.yaml` and syncs the release state between Git branches and Linear Releases. From 0b7838c2977991dd4881cda22a90e63d6d573f92 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 20:03:52 +0000 Subject: [PATCH 2/2] ci: pin the npm that holds the OIDC credential, and isolate it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The job that promotes holds `id-token: write`, so anything running in it can mint an npm credential for this package. `npm install -g npm@latest` resolved that at release time to whatever the registry served, unreviewed. Pinned to 12.2.0, which clears the 12.x floor npm/cli#10038 set for dist-tag over OIDC. `--ignore-scripts` costs nothing today β€” the npm package declares no install scripts β€” and bounds a release that adds one. The version assertion is gone: it guarded `latest` resolving below the floor, which a pin cannot do. The Linear and Slack steps move to a job without `id-token`. Neither needs it, both resolve a mutable tag, and `complete` reads no checkout: the action scans commits only for `sync`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Y4EZAEvxLfXuMeNz5ZgCMz --- .github/workflows/promote.yaml | 35 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/promote.yaml b/.github/workflows/promote.yaml index 78241a9..99ffd5e 100644 --- a/.github/workflows/promote.yaml +++ b/.github/workflows/promote.yaml @@ -8,13 +8,12 @@ on: type: string description: The version that should be promoted -permissions: - contents: read - id-token: write - jobs: publish: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write defaults: run: working-directory: ./mcp-server @@ -27,18 +26,13 @@ jobs: node-version: "24.x" registry-url: "https://registry.npmjs.org" - # The runner's bundled npm predates OIDC support for dist-tag. - - name: Update npm to support Trusted Publishing - run: npm install -g npm@latest - - - name: Check npm can authorize dist-tag over OIDC - run: | - have=$(npm --version) - need=11.21.0 - if [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -1)" != "$need" ]; then - echo "::error::npm $have cannot authorize dist-tag over OIDC; needs >= $need (npm/cli#10038)." - exit 1 - fi + # The runner's bundled npm predates OIDC support for dist-tag, which + # npm/cli#10038 added in 11.21.0 and 12.2.0. Pinned rather than `latest`: + # this runs in the job holding `id-token: write`, so whatever it installs + # can mint an npm credential for this package. Bumping it requires node + # >= 24.15.0, which `node-version` above must keep satisfying. + - name: Install npm with OIDC dist-tag support + run: npm install -g npm@12.2.0 --ignore-scripts - name: Promote to latest run: | @@ -48,6 +42,15 @@ jobs: fi npm dist-tag rm @currents/mcp beta + # Separate job so the third-party actions below do not run beside + # `id-token: write`: neither needs OIDC, and both resolve a mutable tag. + announce: + needs: publish + runs-on: ubuntu-latest + permissions: + contents: read + + steps: - name: Mark Linear release as complete id: release uses: linear/linear-release-action@v0