@@ -144,16 +144,25 @@ jobs:
144144 merge-multiple : true
145145
146146 - name : List artifacts
147- run : ls -la *.tar.gz *.zip
147+ run : ls -la *.tar.gz *.zip *.mcpb
148148
149149 - name : Generate checksums
150- run : sha256sum *.tar.gz *.zip > checksums.txt
150+ run : sha256sum *.tar.gz *.zip *.mcpb > checksums.txt
151151
152152 - name : Attest checksum provenance
153153 uses : actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
154154 with :
155155 subject-path : checksums.txt
156156
157+ # publish-mcp-registry reads the *.mcpb sha256 lines from here — a
158+ # same-run workflow artifact, not a draft-release download, so that
159+ # job keeps contents: read and no gh dependency.
160+ - name : Preserve checksums for the registry job
161+ uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
162+ with :
163+ name : release-checksums
164+ path : checksums.txt
165+
157166 # SBOM content lives in the canonical scripts/ci entry, not inline YAML
158167 # (venue-parity contract): vendored versions are reviewable there.
159168 - name : Generate SBOM
@@ -170,7 +179,7 @@ jobs:
170179
171180 - name : Sign artifacts
172181 run : |
173- for f in *.tar.gz *.zip checksums.txt; do
182+ for f in *.tar.gz *.zip *.mcpb checksums.txt; do
174183 cosign sign-blob --yes --bundle "${f}.bundle" "$f"
175184 done
176185
@@ -202,6 +211,7 @@ jobs:
202211 files : |
203212 *.tar.gz
204213 *.zip
214+ *.mcpb
205215 checksums.txt
206216 sbom.json
207217 *.bundle
@@ -229,11 +239,14 @@ jobs:
229239 run : |
230240 ARCHIVE_DIR="$RUNNER_TEMP/release-archives"
231241 mkdir -p "$ARCHIVE_DIR" binaries
232- gh release download "$VERSION" --dir "$ARCHIVE_DIR" --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip'
242+ gh release download "$VERSION" --dir "$ARCHIVE_DIR" --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip' --pattern '*.mcpb'
243+ # 14 = 8 archives + 6 MCPB bundles; runtime 42 = 8×3 archive
244+ # sidecars + 6×3 bundle members (manifest.json, LICENSE, notices).
245+ # The bundle binaries dedupe to the archive objects byte-for-byte.
233246 scripts/ci/extract-release-archives.sh "$ARCHIVE_DIR" binaries \
234- --expect-archives=8 \
235- --expect-binaries=8 \
236- --expect-runtime-files=24
247+ --expect-archives=14 \
248+ --expect-binaries=14 \
249+ --expect-runtime-files=42
237250
238251 - name : Security audits on all unique extracted release objects
239252 run : |
@@ -350,8 +363,17 @@ jobs:
350363 # A prerelease must never become npm `latest` — plain `npm install`
351364 # would serve the RC to everyone. Publish prereleases under the
352365 # `next` dist-tag; testers opt in via `npm i codebase-memory-mcp@next`.
366+ #
367+ # Idempotency: npm versions are immutable, so a re-run of this job
368+ # after a partial success (v0.10.1: npm published, then the twine
369+ # step died) can never republish — it 403s on its own earlier
370+ # success and the release wedges in draft forever. If the exact
371+ # version already exists on the registry, publishing is DONE; skip.
353372 run : |
354- if [[ "$RELEASE_VERSION" == *-* ]]; then
373+ V="${RELEASE_VERSION#v}"
374+ if npm view "codebase-memory-mcp@$V" version >/dev/null 2>&1; then
375+ echo "npm already has codebase-memory-mcp@$V — skipping publish (idempotent re-run)"
376+ elif [[ "$RELEASE_VERSION" == *-* ]]; then
355377 npm publish --access public --provenance --tag next
356378 else
357379 npm publish --access public --provenance
@@ -366,18 +388,30 @@ jobs:
366388 - name : Build PyPI distribution
367389 working-directory : pkg/pypi
368390 run : |
369- # Pin to specific versions (typosquatting / supply-chain mitigation).
370- # OSSF scorecard prefers --require-hashes, but that needs full
371- # transitive deps; version pinning is the practical compromise.
372- python -m pip install --upgrade 'build==1.3.0' 'twine==6.2.0'
391+ # Every byte of the publish toolchain is bound by hash: version pins
392+ # alone still let a compromised index serve different content, and a
393+ # bare `pip install` resolves the whole transitive graph at run time
394+ # (OSSF Scorecard pinned-dependencies). requirements-publish.txt is
395+ # generated on a linux/amd64 python:3.12 image — see its header for
396+ # the exact regeneration command.
397+ #
398+ # twine must stay >= 7 and hatchling is pinned in pyproject.toml:
399+ # `python -m build` resolves its backend fresh in an isolated env, and
400+ # a hatchling emitting Metadata-Version 2.5 against twine 6.2.0 is
401+ # what broke the v0.10.1 publish.
402+ python -m pip install --require-hashes -r requirements-publish.txt
373403 python -m build
404+ twine check dist/*
374405
375406 - name : Publish to PyPI
376407 working-directory : pkg/pypi
377408 env :
378409 TWINE_USERNAME : __token__
379410 TWINE_PASSWORD : ${{ secrets.PYPI_TOKEN }}
380- run : twine upload --non-interactive dist/*
411+ # --skip-existing: PyPI files are immutable like npm versions; a
412+ # re-run after partial success must treat already-uploaded files
413+ # as done, not as a fatal collision.
414+ run : twine upload --non-interactive --skip-existing dist/*
381415
382416 # ── 8b. Publish server.json to the official MCP Registry ──────
383417 # Runs after npm + PyPI so the registry can verify package ownership
@@ -412,6 +446,20 @@ jobs:
412446 jq --arg v "$VERSION" '.version = $v | (.packages[].version) = $v' \
413447 server.json > server.tmp && mv server.tmp server.json
414448 echo "server.json pinned to $VERSION"
449+
450+ # The MCPB entries need each bundle's sha256; checksums.txt comes from
451+ # the release-draft job as a same-run artifact (not a draft-release
452+ # download — that would need contents beyond read plus gh).
453+ - name : Fetch release checksums
454+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
455+ with :
456+ name : release-checksums
457+
458+ - name : Append MCPB package entries to server.json
459+ env :
460+ RELEASE_VERSION : ${{ inputs.version }}
461+ run : |
462+ scripts/ci/gen-mcpb-registry-entries.sh server.json checksums.txt "$RELEASE_VERSION"
415463 cat server.json
416464
417465 - name : Install mcp-publisher
@@ -435,6 +483,16 @@ jobs:
435483 permissions :
436484 contents : write
437485 steps :
486+ - uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
487+
488+ # Legacy ui-* aliases so 0.9.x updaters stop 404ing (#1538). Runs after
489+ # verify: same bytes as archives the VirusTotal gate already cleared, and
490+ # publishing earlier would duplicate the scan set and provenance manifest.
491+ - name : Publish legacy ui-* archive aliases
492+ env :
493+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
494+ run : scripts/ci/publish-legacy-aliases.sh "${{ inputs.version }}" "$GITHUB_REPOSITORY"
495+
438496 - name : Un-draft GitHub release
439497 env :
440498 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
0 commit comments