From e4cb304a11dda2bf4c4e4cf9d45c013190c8825d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 13 Aug 2026 11:00:51 +0200 Subject: [PATCH] fix(release): publish the MCP registry entry after the release is public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.10.3 published, then failed: MCPB package '...codebase-memory-mcp-darwin-amd64.mcpb' is not publicly accessible (status: 404) publish-mcp-registry and publish-final both needed only publish-registries, so they ran in PARALLEL — and publish-final is the job that un-drafts the release. The registry validates every package URL it is handed by fetching it, and a draft release's assets are not publicly readable. The registry lost the race by five seconds; the identical URL served 200 once the release went public, and the job passed on a plain re-run. The registry now needs publish-final. This keeps the documented intent exactly: publish-final still does NOT depend on the registry, so a registry-preview outage can never block shipping — the registry simply runs after the assets it validates exist to the outside world. The gate-chain contract now pins BOTH directions, because either one alone is a bug: the registry must depend on publish-final, and publish-final must never depend on the registry. Verified by reverting the ordering — the contract goes red with the exact v0.10.3 failure, and green again with it restored. This is the second latent defect the .mcpb path produced on its first real release (after the security-strings audit treating a manifest as a binary). Both were invisible until a stable release ran; an end-to-end bundle gate is worth adding before the next one. Signed-off-by: Martin Vogel --- .github/workflows/release.yml | 16 +++++++++++++-- tests/test_release_gate_chain_contract.sh | 25 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d8cd4027..91b42b0a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -409,11 +409,23 @@ jobs: # product, the registry entry is metadata. A registry-preview outage # must never block shipping. Re-run this job alone to retry — it does # not touch npm/PyPI, so retries are safe. + # + # It DOES depend on publish-final, which is the job that un-drafts the + # release. Both used to need only publish-registries, so they raced: the + # registry validates every package URL it is given by fetching it, and a + # DRAFT release's assets are not publicly readable. In v0.10.3 the registry + # ran 5s before the un-draft and was told its own .mcpb URL was a 404: + # MCPB package '...codebase-memory-mcp-darwin-amd64.mcpb' is not publicly + # accessible (status: 404) + # The same URL served 200 once the release went public, and the job passed on + # a plain re-run. Depending on publish-final keeps the documented intent + # exactly — the registry still cannot block the release, it just runs after + # the assets it validates actually exist to the outside world. publish-mcp-registry: - needs: [publish-registries] + needs: [publish-registries, publish-final] # Skipped for prereleases: the MCP Registry has no channel concept, so an # RC would present itself as the current version to every registry consumer. - if: ${{ !cancelled() && !failure() && needs.publish-registries.result == 'success' && !contains(inputs.version, '-') }} + if: ${{ !cancelled() && !failure() && needs.publish-registries.result == 'success' && needs.publish-final.result == 'success' && !contains(inputs.version, '-') }} runs-on: ubuntu-latest permissions: id-token: write # GitHub OIDC auth to the MCP Registry diff --git a/tests/test_release_gate_chain_contract.sh b/tests/test_release_gate_chain_contract.sh index 3d7ceb7ac..f59440c46 100755 --- a/tests/test_release_gate_chain_contract.sh +++ b/tests/test_release_gate_chain_contract.sh @@ -84,6 +84,31 @@ if "needs.soak.result" not in draft: " legitimately skipped soak (soak_level=none) is distinguishable\n" " from a soak that never ran.") +# The MCP registry validates every package URL it is handed by FETCHING it, and +# a DRAFT release's assets are not publicly readable. publish-final is the job +# that un-drafts. Both jobs used to need only publish-registries, so they raced +# and the registry was told its own .mcpb URL was a 404 (v0.10.3). The registry +# must therefore run AFTER the release is public — while still never gating it. +def needs(job): + """The job's `needs:` list as a single line.""" + m = re.search(r"^ needs:\s*(.*)$", blocks.get(job, ""), re.M) + return m.group(1).strip() if m else "" + +if "publish-mcp-registry" not in blocks: + failures.append("publish-mcp-registry: job missing from release.yml — update this contract") +elif "publish-final" not in needs("publish-mcp-registry"): + failures.append( + "publish-mcp-registry: must `needs:` publish-final. The registry fetches\n" + " the .mcpb URLs it validates, and a draft release's assets 404 —\n" + " running it in parallel with the un-draft is a race it loses.") + +# ...and the reverse must NEVER hold: a registry outage must not block shipping. +if "publish-final" in blocks and "publish-mcp-registry" in needs("publish-final"): + failures.append( + "publish-final: must NOT depend on publish-mcp-registry. The binary\n" + " release is the product and the registry entry is metadata; a\n" + " registry-preview outage must never hold up a release.") + if failures: for f in failures: print("FAIL: " + f, file=sys.stderr)