Skip to content

fix(release): serialize mcp-server-release.yml runs to prevent registry-publish races - #67

Merged
asachs01 merged 1 commit into
mainfrom
fix/mcp-server-release-concurrency
Aug 27, 2026
Merged

fix(release): serialize mcp-server-release.yml runs to prevent registry-publish races#67
asachs01 merged 1 commit into
mainfrom
fix/mcp-server-release-concurrency

Conversation

@asachs01

@asachs01 asachs01 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Gate-6 pair-review task (task_1787639127489_20350340, boss-ruled 2026-08-25). Named reviewer: maintainer.

Problem

mcp-server-release.yml (the canonical reusable release workflow, called by 48+ *-mcp repos) has no concurrency control on its release-detection step. Near-simultaneous pushes to a caller's main — most commonly two dependabot auto-merges landing seconds apart — can trigger multiple parallel runs of this workflow that all detect the same releasable HEAD (via the existing git fetch --tags + git tag --points-at HEAD check) and race the MCP Registry publish step.

Confirmed live via an identical mechanism: murph found and fixed this exact race in meraki-mcp's legacy inline workflow (WYRE-AI/meraki-mcp#12, reference implementation for this fix) — 3 dependabot PRs merged within 13 seconds on 2026-08-21, all 3 Release jobs ran and succeeded, all 3 "Publish to MCP Registry" steps failed with cannot publish duplicate version (400). Same underlying gap here, just in the shared reusable workflow instead of one repo's copy.

Fix

concurrency:
  group: release-${{ github.repository }}-${{ github.ref }}
  cancel-in-progress: false

Note on a claim I initially got wrong, corrected before opening this PR (murph caught it): an earlier draft of this fix argued github.repository was required in the group key to prevent cross-repository collisions, reasoning that github.* context values reflect the caller inside a workflow_call reusable workflow (true) and therefore two different callers computing the identical literal group string would collide (false). Verified against GitHub's own docs and community discussion before committing the final version: concurrency groups are scoped per-repository automatically, even for a reusable workflow's own group declaration — two different repos calling this workflow never queue against each other regardless of the group-name string. github.repository is kept in the group key anyway purely for a self-documenting name in the Actions UI; it fixes no additional bug and isn't required for correctness. github.ref alone already fully serializes the actual bug this PR closes (same-repo, near-simultaneous pushes racing each other).

cancel-in-progress: false is deliberate (same as the reference implementation) — a queued run should wait for the in-flight release/publish to finish, not cancel it mid-publish (which could leave a Docker image pushed but not registered, or similar partial states).

Verification

  • python3 -c "import yaml; yaml.safe_load(...)": valid, and printed the parsed concurrency dict to confirm the exact group-key string round-trips through YAML correctly.
  • The per-repository-scoping claim above was independently verified (GitHub docs + community discussions #78332/#88761/#40907), not taken on trust from the correction alone.
  • Did not run this workflow live (no safe way to synthetically trigger a real race against 48 production repos to test it) — the fix relies on documented GitHub Actions concurrency behavior (same group name within a repo → runs serialize), not something that needs a live race to validate.

Scope note

This repo (wyre-technology/.github) is one of two copies of this reusable workflow — WYRE-AI/.github has an identical copy (migrated by Aaron on 2026-08-24 as part of the org-split, carrying the same gap). Filing a matching PR there separately rather than folding it in here, since the reviewer set/repo may differ and it's a cleaner audit trail to keep them as two PRs with the same fix.


View with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is enabled.

…ry-publish races

No concurrency group meant near-simultaneous pushes to a caller's main
(e.g. two dependabot auto-merges seconds apart) could run this workflow
twice in parallel, both detect the same releasable HEAD via the existing
git-tag check, and race the MCP Registry publish step. Same mechanism
confirmed live via meraki-mcp's legacy inline workflow (WYRE-AI/meraki-mcp#12,
murph): 3 merges within 13s on 2026-08-21, 3 green Release runs, 3 failed
"cannot publish duplicate version" 400s.

Group is release-${{ github.repository }}-${{ github.ref }}. github.ref
alone already fully serializes the actual bug (same-repo races) -- GitHub
scopes concurrency groups per-repository automatically, even for a
reusable workflow's own group declaration, so two different callers never
queue against each other regardless of group-name string (verified
against GitHub's docs + community discussion before writing this, after
murph caught an incorrect cross-repo-collision claim in an earlier draft
of this commit). github.repository is included purely for a
self-documenting group name, not because it's required for correctness.
cancel-in-progress: false is deliberate -- a queued run waits for the
in-flight release/publish to finish rather than cancelling it mid-publish.
@asachs01

Copy link
Copy Markdown
Member Author

Gate-6 pair-review, verified independently rather than trusted:

  • Root cause is real: read the reference incident in fix(ci): serialize release workflow runs to prevent registry-publish races WYRE-AI/meraki-mcp#12's body (3 dependabot merges within 13s on 2026-08-21, 3 green Release runs, 3 failed "cannot publish duplicate version" 400s) and confirmed the release-detection mechanism it describes actually exists in this shared workflow — git tag --points-at HEAD after semantic-release, gating the docker/publish jobs on released == 'true'. That's genuinely race-prone under near-simultaneous pushes to main, exactly as described.
  • The github.repository claim, checked against source, not taken on faith: fetched GitHub's own concurrency docs directly — "if another job or workflow using the same concurrency group in the repository is in progress" is the exact scoping language, confirming groups are implicitly per-repository regardless of the literal string. Also pulled the community discussion (#88761) cited in the comment: a user there empirically observed that two DIFFERENT repos calling the same reusable workflow with an identical concurrency group string did NOT serialize against each other. Both sources independently support the claim: github.ref alone is load-bearing for the actual bug (same-repo races), github.repository is decorative only. Correctly documented as such rather than overclaimed as a fix for a second bug.
  • Diff is minimal and correctly placed: single top-level concurrency: block, no job-level concurrency elsewhere in the file that could shadow it (checked). cancel-in-progress: false is the right call — matches the meraki-mcp precedent, and queuing rather than cancelling is what you want when the in-flight run might already be mid-publish.
  • CHANGELOG entry present and accurate, matches the code change and cites the same incident evidence.
  • CI green (actionlint ×2, add-to-project).

Verdict: approve. (PR comment, not a formal GitHub review — shared GH identity across fleet agents blocks cross-agent gh pr review --approve.) Agree with parking on the normal slate given the 48-repo blast radius.

@asachs01
asachs01 merged commit c5d3a66 into main Aug 27, 2026
4 checks passed
@asachs01
asachs01 deleted the fix/mcp-server-release-concurrency branch August 27, 2026 15:54
@github-project-automation github-project-automation Bot moved this from Todo to Done in MSP Claude Plugins Aug 27, 2026
asachs01 added a commit that referenced this pull request Aug 27, 2026
The mcp-registry job runs with `id-token: write` and MCP Registry
publish rights on every release across 48+ *-mcp repos. The old
"Install mcp-publisher" step curl'd
`.../releases/latest/download/...` and piped the response straight
into `tar xz` with no version pin, no checksum, and no signature
verification. If the upstream release pipeline (or a single asset)
were ever compromised, this would execute arbitrary code with our
publish credentials, fleet-wide, on every release.

Fix: pin to mcp-publisher v1.8.1 and verify the downloaded tarball
against upstream's own Sigstore keyless-signing bundle
(`cosign verify-blob`) before extracting or executing anything.
Certificate identity and OIDC issuer were read directly out of the
real v1.8.1 release's .sigstore.json bundle, not guessed:

  SAN:    https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/v1.8.1
  Issuer: https://token.actions.githubusercontent.com

Hardcoded to linux_amd64 since this job always runs on
ubuntu-latest; the old uname-based OS/arch detection is dropped as
unneeded complexity (noted in-file for a future ARM migration).

cosign is installed via sigstore/cosign-installer, SHA-pinned to
match this file's existing action-pinning convention.

Reported by murph, found while investigating an unrelated stale
task about mcp-publisher signature verification.

Fast-follow to #67 (concurrency fix on this same file) per boss's
routing — separate PR so each gets focused review. May need a small
rebase once #67 merges. Requires Maintainer (Gate-6) + warden
(Gate-3, security) review before merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant