From bc14f53fecc6d04acedcf0db8f4f93a42089a271 Mon Sep 17 00:00:00 2001 From: I4cDeath Date: Sat, 11 Jul 2026 01:59:55 -0500 Subject: [PATCH] fix(release): dispatch publish/homebrew instead of workflow_call (npm OIDC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first v0.13.0 run created the GitHub Release but npm rejected the publish with its misleading E404: npm trusted publishing validates the OIDC token's top-level workflow file, which is release.yml when publish.yml runs via workflow_call — but the trusted publisher on npmjs.com is registered for publish.yml. release.yml now dispatches publish.yml and update-homebrew.yml on the tag ref via the workflow_dispatch API (allowed with GITHUB_TOKEN; the recursion block only suppresses event-triggered runs). Dispatched runs have publish.yml as their top-level workflow, so the OIDC claim matches the existing npm configuration and the manual re-run path keeps working unchanged. - release.yml: permissions actions:write (id-token no longer needed here), dispatch step after release creation - publish.yml: drop the now-unused workflow_call trigger - update-homebrew.yml: workflow_call -> workflow_dispatch (it already polls npm for up to 5 min, so dispatch order doesn't matter) - docs/releasing.md: document the dispatch architecture and both failure modes hit on the first tag run Co-Authored-By: Claude Fable 5 --- .github/workflows/publish.yml | 21 +++++----- .github/workflows/release.yml | 56 ++++++++++++++------------- .github/workflows/update-homebrew.yml | 7 ++-- docs/releasing.md | 12 ++++-- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0f46509..634eab2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,20 +5,19 @@ on: # "published" (not "created") so draft releases never trigger a publish; # matches the trigger update-homebrew.yml already uses. types: [published] - # Allow re-publishing a tag manually if the release-triggered run fails. - # Pick the tag (or any branch) when dispatching from the Actions tab. + # The tag-driven release path: release.yml dispatches this workflow on the + # tag ref. Also used to re-publish manually if a run fails — pick the tag + # (or any branch) when dispatching from the Actions tab. + # + # Deliberately NOT workflow_call: npm trusted publishing validates the OIDC + # token's top-level workflow file, and the trusted publisher on npmjs.com is + # registered for publish.yml. A called workflow presents release.yml as the + # top-level file and npm rejects the token with a misleading E404 (this + # failed the first v0.13.0 run). workflow_dispatch: inputs: ref: - description: "Git ref to publish (defaults to the current branch)" - required: false - type: string - # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't - # emit events, so the release:published trigger can't fire for that path). - workflow_call: - inputs: - ref: - description: "Git ref to publish (defaults to the calling ref)" + description: "Git ref to publish (defaults to the dispatched ref)" required: false type: string diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5187671..2beef60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,26 @@ name: Release # Pushing a v* tag drives the whole release: create the GitHub Release with -# notes from the matching CHANGELOG section, then chain the npm/MCP publish -# and the Homebrew tap update via workflow_call. +# notes from the matching CHANGELOG section, then DISPATCH the npm/MCP publish +# and the Homebrew tap update on the same tag ref. # -# Why the explicit chaining: releases created here use GITHUB_TOKEN, and -# GITHUB_TOKEN-created events do not trigger other workflows, so the -# release:published triggers on publish.yml / update-homebrew.yml would never -# fire for tag-driven releases. Those event triggers still work for releases -# created manually in the UI; every downstream step is idempotent, so the -# occasional double-run is harmless. +# Why dispatch instead of events or workflow_call: +# - Releases created here use GITHUB_TOKEN, and GITHUB_TOKEN-created events do +# not trigger other workflows, so the release:published triggers on +# publish.yml / update-homebrew.yml never fire for tag-driven releases. +# (Those event triggers still work for releases created manually in the UI.) +# - workflow_call does not work for the npm publish: npm trusted publishing +# validates the OIDC token's top-level workflow file, which would be +# release.yml for a called workflow, but the trusted publisher on npmjs.com +# is registered for publish.yml (this failed the first v0.13.0 run with +# npm's misleading E404). An explicitly dispatched run has publish.yml as +# its top-level workflow, so the claim matches. +# - Explicit workflow_dispatch API calls ARE allowed with GITHUB_TOKEN — the +# recursion prevention only suppresses event-triggered runs. +# +# Ordering: both are dispatched immediately; update-homebrew.yml already polls +# npm for up to 5 minutes for the new version, so it tolerates the publish +# finishing after it starts. Every downstream step is idempotent. on: push: @@ -17,13 +28,10 @@ on: permissions: contents: write - id-token: write + actions: write -# NOTE: this must NOT be the "publish" group — the called publish.yml declares -# that group itself, and a reusable workflow's concurrency still applies when -# called, so parent+child sharing a group is an instant deadlock-cancel -# (bit us on the v0.13.0 tag). The child's group is what serializes actual -# publishing; this one only keeps two release runs from racing each other. +# The dispatched publish.yml serializes real publishing via its own "publish" +# concurrency group; this group only keeps two release runs from racing. concurrency: group: release cancel-in-progress: false @@ -61,16 +69,10 @@ jobs: --notes-file /tmp/notes.md fi - publish: - needs: github-release - uses: ./.github/workflows/publish.yml - permissions: - contents: read - id-token: write - - homebrew: - needs: publish - uses: ./.github/workflows/update-homebrew.yml - secrets: inherit - permissions: - contents: read + - name: Dispatch publish and homebrew on this tag + env: + GH_TOKEN: ${{ github.token }} + run: | + gh workflow run publish.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" + gh workflow run update-homebrew.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" + echo "Dispatched publish.yml and update-homebrew.yml on $GITHUB_REF_NAME." diff --git a/.github/workflows/update-homebrew.yml b/.github/workflows/update-homebrew.yml index b82b06b..cab89ba 100644 --- a/.github/workflows/update-homebrew.yml +++ b/.github/workflows/update-homebrew.yml @@ -3,9 +3,10 @@ name: Update Homebrew Tap on: release: types: [published] - # Called from release.yml on tag push (GITHUB_TOKEN-created releases don't - # emit events). GITHUB_REF_NAME is the tag in both paths. - workflow_call: + # The tag-driven release path: release.yml dispatches this workflow on the + # tag ref (GITHUB_TOKEN-created releases don't emit events). GITHUB_REF_NAME + # is the tag in both paths — dispatch on a v* tag, never a branch. + workflow_dispatch: permissions: contents: read diff --git a/docs/releasing.md b/docs/releasing.md index 8f161f7..8238c7d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -29,10 +29,16 @@ git push --tags `.github/workflows/release.yml` (on `v*` tags): 1. **GitHub Release** — created idempotently, with the release notes extracted from that version's CHANGELOG section. Tagging without a matching CHANGELOG section fails the run on purpose. -2. **Publish** (`publish.yml` via `workflow_call`) — npm publish with provenance (idempotent: skips if the version is already on npm), then MCP Registry publish via a pinned `mcp-publisher`. -3. **Homebrew** (`update-homebrew.yml` via `workflow_call`) — waits for the version to appear on npm, regenerates the tap formula with the new tarball sha, and pushes to `I4cTime/homebrew-tap` (skips if unchanged). +2. **Publish** (`publish.yml`, dispatched on the tag ref) — npm publish with provenance (idempotent: skips if the version is already on npm), then MCP Registry publish via a pinned `mcp-publisher`. +3. **Homebrew** (`update-homebrew.yml`, dispatched on the tag ref) — waits up to 5 minutes for the version to appear on npm, regenerates the tap formula with the new tarball sha, and pushes to `I4cTime/homebrew-tap` (skips if unchanged). -Why `workflow_call` instead of the `release: published` event for this path: the workflow creates the release with `GITHUB_TOKEN`, and GitHub does not fire workflow triggers for events created by `GITHUB_TOKEN`. The event triggers remain in place so a release created manually in the GitHub UI still publishes; every downstream step is idempotent, so overlap is harmless (a shared `publish` concurrency group serializes runs). +Why **dispatch** instead of events or `workflow_call` (both were tried and failed on the first v0.13.0 attempt): + +- The release is created with `GITHUB_TOKEN`, and GitHub does not fire workflow triggers for events created by `GITHUB_TOKEN` — so the `release: published` triggers can't fire on this path. (They remain in place so a release created manually in the UI still publishes.) +- `workflow_call` breaks npm trusted publishing: npm validates the OIDC token's *top-level* workflow file, which is `release.yml` for a called workflow, but the trusted publisher on npmjs.com is registered for `publish.yml` — npm rejects the token with a misleading `E404`. +- Explicit `workflow_dispatch` API calls **are** allowed with `GITHUB_TOKEN` (recursion prevention only suppresses event-triggered runs), and a dispatched run has `publish.yml` as its top-level workflow, so the OIDC claim matches. + +Also learned the hard way: `release.yml` must not share `publish.yml`'s concurrency group — a called/dispatched workflow's own group is what serializes publishing, and a parent holding the same group deadlocks. Every downstream step is idempotent, so overlap between the manual-release and tag-driven paths is harmless. ## Recovering from a failed release