From 462b0d39222678a9d555c34094e8c84a43a9789e Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 11 Aug 2026 10:12:41 +0200 Subject: [PATCH] =?UTF-8?q?fix(release):=20unwedge=20publish-registries=20?= =?UTF-8?q?=E2=80=94=20twine=207=20for=20Metadata-Version=202.5,=20idempot?= =?UTF-8?q?ent=20re-runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.10.1 publish failed twice, for two stacked defects: 1. `python -m build` resolves the UNPINNED hatchling backend fresh inside its isolated build env, and current hatchling emits Metadata-Version 2.5 — which the pinned twine==6.2.0 rejects as "'2.5' is not a valid metadata version". Deterministic, and a time bomb: v0.10.0 published cleanly days ago on the same pins. Verified locally on identical artifacts: twine 6.2.0 rejects, twine 7.0.0 passes. The pin moves to 7.0.0 and `twine check` now runs at build time so a metadata regression fails BEFORE upload. 2. The job was not idempotent, breaking its own design comment ("if publish fails, the release stays in draft so we can re-run"). Attempt 1 published npm 0.10.1 and then died at twine; the re-run 403'd on its own success ("cannot publish over the previously published versions") and the release wedged in draft. npm publish now skips when the exact version already exists on the registry, and twine uploads with --skip-existing — both registries treat immutable prior success as done, not as a collision. Workflow-only diff. Unblocks re-dispatching the wedged v0.10.1 release. Signed-off-by: Martin Vogel --- .github/workflows/release.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f806737e8..464d8f8aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -335,8 +335,17 @@ jobs: # A prerelease must never become npm `latest` — plain `npm install` # would serve the RC to everyone. Publish prereleases under the # `next` dist-tag; testers opt in via `npm i codebase-memory-mcp@next`. + # + # Idempotency: npm versions are immutable, so a re-run of this job + # after a partial success (v0.10.1: npm published, then the twine + # step died) can never republish — it 403s on its own earlier + # success and the release wedges in draft forever. If the exact + # version already exists on the registry, publishing is DONE; skip. run: | - if [[ "$RELEASE_VERSION" == *-* ]]; then + V="${RELEASE_VERSION#v}" + if npm view "codebase-memory-mcp@$V" version >/dev/null 2>&1; then + echo "npm already has codebase-memory-mcp@$V — skipping publish (idempotent re-run)" + elif [[ "$RELEASE_VERSION" == *-* ]]; then npm publish --access public --provenance --tag next else npm publish --access public --provenance @@ -354,15 +363,25 @@ jobs: # Pin to specific versions (typosquatting / supply-chain mitigation). # OSSF scorecard prefers --require-hashes, but that needs full # transitive deps; version pinning is the practical compromise. - python -m pip install --upgrade 'build==1.3.0' 'twine==6.2.0' + # + # twine >= 7 is required: `python -m build` resolves the UNPINNED + # hatchling backend fresh in its isolated env, and current hatchling + # emits Metadata-Version 2.5, which twine 6.2.0 rejects as invalid + # ("'2.5' is not a valid metadata version" — the v0.10.1 failure). + # Verified locally: identical artifacts, 6.2.0 rejects / 7.0.0 passes. + python -m pip install --upgrade 'build==1.3.0' 'twine==7.0.0' python -m build + twine check dist/* - name: Publish to PyPI working-directory: pkg/pypi env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload --non-interactive dist/* + # --skip-existing: PyPI files are immutable like npm versions; a + # re-run after partial success must treat already-uploaded files + # as done, not as a fatal collision. + run: twine upload --non-interactive --skip-existing dist/* # ── 8b. Publish server.json to the official MCP Registry ────── # Runs after npm + PyPI so the registry can verify package ownership