Skip to content

chore(release): v1.34.0 - what the gates could not see #25

chore(release): v1.34.0 - what the gates could not see

chore(release): v1.34.0 - what the gates could not see #25

Workflow file for this run

# Release (npm + MCP registry) via trusted publishing (OIDC)
#
# WHAT THIS DOES
# On a version tag (v*) — or a manual dispatch — this publishes, in order:
# 1. the core package @chanmeng666/archlang (repo root) → npm, with provenance
# 2. the MCP shim @chanmeng666/archlang-mcp (packages/mcp) → npm, with provenance
# 3. the MCP server manifest (packages/mcp/server.json) → the official MCP registry
# Each step is idempotent: it reads the version from the package's own package.json and
# skips the publish when that exact version already exists on the registry, so re-running
# a partially-failed release (or dispatching after a fix) is safe.
#
# TRUSTED PUBLISHING — NO NPM TOKEN EXISTS ANYWHERE
# Publishing authenticates via GitHub OIDC (`permissions: id-token: write`), not a stored
# secret. There is deliberately NO npm automation token in repo secrets, org secrets, or
# .npmrc. This works only because each package is registered ONCE, by hand, as a trusted
# publisher on npmjs.com:
# npmjs.com → each package → Settings → Trusted Publisher → GitHub Actions, with
# Owner/Repo = ChanMeng666/archlang and Workflow filename = release.yml
# Both @chanmeng666/archlang AND @chanmeng666/archlang-mcp must have that one-time
# registration pointing at THIS file. Without it, `npm publish` here fails with an auth
# error — that is the expected signal to (re)do the npmjs registration, not to add a token.
#
# The MCP registry publish (step 3) also uses GitHub OIDC (`mcp-publisher login
# github-oidc`) — no secret. The registry validates server.json against the just-published
# npm package (mcpName ↔ name, case-sensitive io.github.ChanMeng666/… owner, ≤100-char
# description). NOTE: the MCP registry is in public preview and may reset data / change
# behaviour; this step is kept strict (fails loudly) so a regression surfaces here.
#
# House style mirrors ci.yml / deploy.yml (actions/checkout@v7.0.1, actions/setup-node@v7.0.0).
name: Release (npm + MCP registry via OIDC)
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
id-token: write # OIDC token exchange for npm trusted publishing + mcp-publisher
contents: write # create the GitHub Release for the pushed v* tag (read is not enough)
jobs:
publish:
name: Publish (npm + MCP registry)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org' # required for OIDC token exchange
cache: npm
- name: Install dependencies
run: npm ci # with the bundled npm, like ci.yml
# Trusted publishing needs npm >= 11.5.1. Pin to 11.x on purpose, NOT npm@latest:
# npm 12 defaults allowScripts off, which would block esbuild's postinstall — and we
# only upgrade AFTER `npm ci` above has already run its install scripts.
- name: Pin npm to 11.x (trusted-publishing minimum)
run: |
npm install -g npm@11.18.0
npm --version
# Idempotency: resolve each package's declared version and check whether it is already
# on the registry. `npm view pkg@ver version` prints the version if it exists, nothing
# if that version is absent, and errors if the package name is unknown (|| true).
- name: Resolve versions and decide what to publish
id: check
run: |
core_version=$(node -p "require('./package.json').version")
mcp_version=$(node -p "require('./packages/mcp/package.json').version")
echo "core_version=$core_version" >> "$GITHUB_OUTPUT"
echo "mcp_version=$mcp_version" >> "$GITHUB_OUTPUT"
core_on_registry=$(npm view "@chanmeng666/archlang@$core_version" version 2>/dev/null || true)
mcp_on_registry=$(npm view "@chanmeng666/archlang-mcp@$mcp_version" version 2>/dev/null || true)
if [ "$core_on_registry" = "$core_version" ]; then
echo "core_exists=true" >> "$GITHUB_OUTPUT"
echo "::notice::@chanmeng666/archlang@$core_version already on npm — skipping core publish."
else
echo "core_exists=false" >> "$GITHUB_OUTPUT"
echo "::notice::Will publish @chanmeng666/archlang@$core_version to npm."
fi
if [ "$mcp_on_registry" = "$mcp_version" ]; then
echo "mcp_exists=true" >> "$GITHUB_OUTPUT"
echo "::notice::@chanmeng666/archlang-mcp@$mcp_version already on npm — skipping mcp npm publish."
else
echo "mcp_exists=false" >> "$GITHUB_OUTPUT"
echo "::notice::Will publish @chanmeng666/archlang-mcp@$mcp_version to npm."
fi
# The MCP-registry state is checked INDEPENDENTLY of npm: if a previous run
# published to npm but died before the registry sync, `mcp_exists` is true on the
# re-run and must not also skip the sync. The search API is best-effort (the
# registry is in preview): if the probe fails or the shape changes, we attempt the
# sync and let `mcp-publisher publish` be the arbiter (a duplicate fails loudly).
reg_json=$(curl -fsSL "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.ChanMeng666/archlang-mcp" || echo "")
if echo "$reg_json" | grep -q "\"version\": *\"$mcp_version\""; then
echo "registry_synced=true" >> "$GITHUB_OUTPUT"
echo "::notice::MCP registry already has $mcp_version — skipping registry sync."
else
echo "registry_synced=false" >> "$GITHUB_OUTPUT"
echo "::notice::Will sync io.github.ChanMeng666/archlang-mcp@$mcp_version to the MCP registry."
fi
# prepublishOnly is `npm run build && npm run test`, and `npm run test` is the WHOLE
# monorepo suite — which includes `editors/vscode/test/stdio.test.ts`. That suite hard-fails
# under CI when the core is built but `editors/vscode/dist/server.js` is not, on the
# (correct) reasoning that a present core plus an absent bundle means nothing about the
# shipped bundle was verified. `npm run build` does not build that bundle, so the publish
# path has to, or the gate fires on an artifact the CORE publish never had a reason to make.
# Building it here keeps the gate at full strength instead of narrowing it: the bundle is
# genuinely verified during the release. (v1.25.0 was the first release after the bundle
# tests landed, and the first to hit this.)
- name: Build the core and the VS Code bundle (prepublishOnly runs the bundle tests)
run: npm run build && npm run vscode:build:only
# prepublishOnly (build + full test suite) is the release gate for the core.
- name: Publish core to npm (@chanmeng666/archlang)
if: ${{ success() && steps.check.outputs.core_exists != 'true' }}
run: npm publish --provenance --access public
# Runs only after the core step. prepack builds the shim (core stays external — the
# shim never re-bundles it, so it does not need the core's dist/ to be freshly built).
- name: Publish MCP shim to npm (@chanmeng666/archlang-mcp)
if: ${{ success() && steps.check.outputs.mcp_exists != 'true' }}
run: npm publish -w packages/mcp --provenance --access public
# MCP registry sync — whenever the registry lags server.json's version (independent of
# whether npm published this run: by this point npm has the version either way, and the
# registry validates against npm). Official install/auth flow from
# https://modelcontextprotocol.io/registry/github-actions (GitHub OIDC variant).
# server.json lives in packages/mcp, so run from there.
- name: Install mcp-publisher CLI
if: ${{ success() && steps.check.outputs.registry_synced != 'true' }}
working-directory: packages/mcp
# Pinned + checksum-verified (the runner is linux/amd64). To bump: update VERSION
# and SHA256 together (sha256sum of the linux_amd64 tarball).
env:
MCP_PUBLISHER_VERSION: v1.7.9
MCP_PUBLISHER_SHA256: ab128162b0616090b47cf245afe0a23f3ef08936fdce19074f5ba0a4469281ac
run: |
curl -fsSLo mcp-publisher.tar.gz "https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz"
echo "${MCP_PUBLISHER_SHA256} mcp-publisher.tar.gz" | sha256sum -c -
tar xz -f mcp-publisher.tar.gz mcp-publisher
test -x ./mcp-publisher && echo "mcp-publisher installed"
- name: Authenticate to MCP Registry (GitHub OIDC)
if: ${{ success() && steps.check.outputs.registry_synced != 'true' }}
working-directory: packages/mcp
run: ./mcp-publisher login github-oidc
# Bounded retry, and the reason is a RACE we have actually seen. The registry validates
# `server.json` against the npm package it names — and on the v1.30.0 release that
# validation 404'd on a version this same job had published moments earlier, because
# the npm registry had not yet made it visible to a third party. `gh run rerun --failed`
# then succeeded with no change, which is the signature of a race and not of a broken
# manifest. Six attempts, 20 s apart, is ~2 minutes of patience against a lookup that
# normally settles in seconds; a genuinely bad manifest still fails, just two minutes
# later, and the last attempt's output is what the log shows.
- name: Publish server to MCP Registry
if: ${{ success() && steps.check.outputs.registry_synced != 'true' }}
working-directory: packages/mcp
run: |
for attempt in 1 2 3 4 5 6; do
if ./mcp-publisher publish; then
echo "::notice::MCP registry publish succeeded on attempt $attempt."
exit 0
fi
if [ "$attempt" -lt 6 ]; then
echo "::warning::MCP registry publish failed (attempt $attempt/6) — retrying in 20s."
sleep 20
fi
done
echo "::error::MCP registry publish failed after 6 attempts."
exit 1
# The GitHub Release. This step used to not exist: releases were created by hand, so
# they drifted — 23 tags shipped with NO Release at all, including the two newest, and
# the Releases page advertised v1.13.0 as "Latest" while npm served 1.15.0.
#
# Notes are EXTRACTED from CHANGELOG.md (the canonical release narrative — AGENTS.md
# keeps it there and nowhere else) rather than hand-written or derived from commit
# subjects, so the Release page cannot say something the changelog doesn't. The
# extractor exits non-zero when a version has no section, so an undocumented release
# fails loudly instead of shipping with an empty body.
#
# Last, and only on success: a Release is the human-visible announcement, so it must
# not appear for a run whose npm/registry publish failed. Idempotent — a re-run over an
# existing Release skips instead of failing (matching how the publish steps behave).
- name: Create the GitHub Release
if: ${{ success() && startsWith(github.ref, 'refs/tags/v') }}
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::notice::GitHub Release $TAG already exists — skipping."
exit 0
fi
node scripts/changelog-section.mjs "$TAG" > release-notes.md
gh release create "$TAG" \
--title "$TAG" \
--notes-file release-notes.md \
--verify-tag
echo "::notice::Created GitHub Release $TAG from its CHANGELOG.md section."