From f66a9a574d7216d04cde8ee6945d608b96ec30c4 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 6 Nov 2026 12:43:25 +0100 Subject: [PATCH] ci: publish each release to the MCP registry The registry does not follow npm, so every release waited on a by-hand mcp-publisher publish, behind a browser login that expires between releases. Release now calls publish-mcp.yml once npm took a release. It signs in with the workflow's OIDC token, which the registry accepts for the io.github.Ark0N/ namespace, so it needs no secret and no browser. It skips a version the registry already has, waits until npm serves a new one, and can be run by hand with gh workflow run publish-mcp.yml. The id-token right sits on its own job, which runs nothing from the repository: it reads server.json and runs one pinned, checksummed binary. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-mcp.yml | 85 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 13 +++++ docs/releasing.md | 4 +- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-mcp.yml diff --git a/.github/workflows/publish-mcp.yml b/.github/workflows/publish-mcp.yml new file mode 100644 index 00000000..bb98e5e9 --- /dev/null +++ b/.github/workflows/publish-mcp.yml @@ -0,0 +1,85 @@ +name: Publish to the MCP registry + +# The official MCP registry does not follow npm, so every version is published to it. +# Release calls this after npm took a release. To publish the version on master by hand, +# after a failed run for example: gh workflow run publish-mcp.yml +# +# No secret and no browser login: the registry takes this repository's GitHub Actions OIDC +# token as proof of the io.github.Ark0N/ namespace. That id-token right is why this job +# runs nothing from the repository: it reads server.json and runs one pinned binary. + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +env: + # Bump both together, the digest from the release's asset. Never releases/latest. + MCP_PUBLISHER_VERSION: v1.8.7 + MCP_PUBLISHER_SHA256: 00b21004149a4ce66f0782508ae4a2bce67d1bea395a8da0bd8028159fb59579 + REGISTRY_VERSIONS_URL: https://registry.modelcontextprotocol.io/v0/servers/io.github.Ark0N%2Fcodeman/versions + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + persist-credentials: false + + # Also what ends a run for a release that only moved xterm-zerolag-input. + - name: Skip a version the registry already has + id: check + run: | + VERSION=$(jq -r .version config/mcp-registry/server.json) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "$REGISTRY_VERSIONS_URL/$VERSION") + case "$STATUS" in + 200) echo "::notice::$VERSION is already in the MCP registry" ;; + 404) echo "needed=true" >> "$GITHUB_OUTPUT" ;; + *) echo "::error::The MCP registry answered $STATUS for $VERSION"; exit 1 ;; + esac + + # The registry refuses a version npm does not serve yet, and reads mcpName from it. + - name: Wait until npm serves this version + if: steps.check.outputs.needed == 'true' + env: + VERSION: ${{ steps.check.outputs.version }} + run: | + EXPECTED=$(jq -r .name config/mcp-registry/server.json) + for _ in $(seq 1 60); do + FOUND=$(npm view "aicodeman@$VERSION" mcpName 2>/dev/null || true) + [ "$FOUND" = "$EXPECTED" ] && exit 0 + sleep 20 + done + echo "::error::npm did not serve aicodeman@$VERSION with mcpName $EXPECTED within 20 minutes" + exit 1 + + - name: Download mcp-publisher + if: steps.check.outputs.needed == 'true' + run: | + ARCHIVE="$RUNNER_TEMP/mcp-publisher.tar.gz" + curl -fsSL -o "$ARCHIVE" "https://github.com/modelcontextprotocol/registry/releases/download/$MCP_PUBLISHER_VERSION/mcp-publisher_linux_amd64.tar.gz" + echo "$MCP_PUBLISHER_SHA256 $ARCHIVE" | sha256sum -c - + tar xzf "$ARCHIVE" -C "$RUNNER_TEMP" mcp-publisher + + - name: Publish + if: steps.check.outputs.needed == 'true' + working-directory: config/mcp-registry + run: | + "$RUNNER_TEMP/mcp-publisher" login github-oidc + "$RUNNER_TEMP/mcp-publisher" publish + + - name: Confirm the registry lists it as latest + if: steps.check.outputs.needed == 'true' + env: + VERSION: ${{ steps.check.outputs.version }} + run: | + curl -fsS --retry 4 --retry-all-errors "$REGISTRY_VERSIONS_URL/$VERSION" \ + | jq -e '._meta["io.modelcontextprotocol.registry/official"].isLatest == true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca7f2b9e..22a12caa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ jobs: permissions: contents: write pull-requests: write + outputs: + published: ${{ steps.changesets.outputs.published }} steps: - name: Checkout repo uses: actions/checkout@v6 @@ -72,3 +74,14 @@ jobs: git tag "$NEW_TAG" "$OLD_TAG" 2>/dev/null || true git tag -d "$OLD_TAG" 2>/dev/null || true git push origin "$NEW_TAG" ":refs/tags/$OLD_TAG" 2>/dev/null || true + + # Its own job: this one alone holds id-token, and a registry failure shows as itself + # without touching the npm publish or the tag above. + mcp-registry: + name: MCP registry + needs: release + if: needs.release.outputs.published == 'true' + permissions: + contents: read + id-token: write + uses: ./.github/workflows/publish-mcp.yml diff --git a/docs/releasing.md b/docs/releasing.md index 097a03a9..cb960dab 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -24,9 +24,9 @@ When user says "COM": 5. **Commit and deploy**: verify the branch first (`git branch --show-current`), then stage EXPLICIT paths — never `git add -A`, which has swept another session's WIP into a release. `git status --short` and account for every line before committing: `git add && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web` 6. **Refresh the getcodeman.com version badge**: the landing page's status bar carries the release version (`v · getcodeman.com · MIT`), so it goes stale on every release if nobody bumps it. The site source and its deploy script are maintained outside this repository, on the maintainer's machine only; follow the local site handbook there, which also covers the numbers strip and `sitemap.xml` refresh that belong in the same pass. Poll production (`curl -s https://getcodeman.com/ | grep v`) before calling it done, since the edge lags a deploy by up to a minute. Not applicable to contributor clones — skip it and say so. -7. **Wait for CI**: after `git push`, TWO workflows fire per master push — `CI` and `Release` (the npm publish + GitHub release). List both runs for the pushed commit with `gh run list --commit $(git rev-parse HEAD) --json databaseId,workflowName` and watch EACH with `gh run watch --exit-status`. Confirm both pass before considering the release done (`gh run list -L 1` returns only one of the two). +7. **Wait for CI**: after `git push`, TWO workflows fire per master push: `CI` and `Release` (the npm publish, the GitHub release and the MCP registry publish). List both runs for the pushed commit with `gh run list --commit $(git rev-parse HEAD) --json databaseId,workflowName` and watch EACH with `gh run watch --exit-status`. Confirm both pass before considering the release done (`gh run list -L 1` returns only one of the two). -8. **Publish the new version to the MCP registry**: `codeman mcp` is listed in the official MCP registry as `io.github.Ark0N/codeman`, and the registry does not follow npm, so every release is published there by hand or the listing stays on the old version. `npm run version-packages` has already written the new version into `config/mcp-registry/server.json` (`scripts/sync-plugin.mjs`). Wait until npm serves it (`npm view aicodeman version`; it can lag the green `Release` run by several minutes), because the registry checks that the npm package exists and carries `mcpName: io.github.Ark0N/codeman`. Then run `cd config/mcp-registry && mcp-publisher publish`, preceded by `mcp-publisher login github` when the saved login has expired, signed in as the account that owns the `io.github.Ark0N/` namespace. Use mcp-publisher 1.8.1 or newer (`mcp-publisher --version`), the release binary from https://github.com/modelcontextprotocol/registry/releases: the Snap Store package is stuck at 1.1.0 and rejects the current `$schema` with a false "deprecated schema detected", so never edit `server.json` for that error. Older publishers also save their login as `.mcpregistry_github_token` and `.mcpregistry_registry_token` (live tokens) in the current directory; move those out of the repo, never commit them. Confirm with `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=io.github.Ark0N/codeman' | jq -r '.servers[] | "\(.server.version) latest=\(._meta["io.modelcontextprotocol.registry/official"].isLatest)"'`: the new version must be the one with `latest=true`. Maintainer only (it needs that GitHub account); contributor clones skip it and say so. +8. **Check the MCP registry**: `codeman mcp` is listed in the official MCP registry as `io.github.Ark0N/codeman`, and the registry does not follow npm, so every release is published there. The `Release` workflow does this itself in its `MCP registry` job (`.github/workflows/publish-mcp.yml`): it waits until npm serves the version, because the registry checks that the npm package exists and carries `mcpName: io.github.Ark0N/codeman`, signs in with the workflow's GitHub OIDC token (no secret, no browser) and publishes the `config/mcp-registry/server.json` that `npm run version-packages` wrote (`scripts/sync-plugin.mjs`). A version the registry already has is skipped. Confirm with `curl -s 'https://registry.modelcontextprotocol.io/v0/servers?search=io.github.Ark0N/codeman' | jq -r '.servers[] | "\(.server.version) latest=\(._meta["io.modelcontextprotocol.registry/official"].isLatest)"'`: the new version must be the one with `latest=true`. If the job failed, `gh workflow run publish-mcp.yml` publishes the version on master again. Its publisher is pinned: bump `MCP_PUBLISHER_VERSION` and `MCP_PUBLISHER_SHA256` together, the digest from the release's asset. The local fallback is `cd config/mcp-registry && ~/.local/bin/mcp-publisher login github && ~/.local/bin/mcp-publisher publish`, signed in as the account that owns the `io.github.Ark0N/` namespace, with mcp-publisher 1.8.1 or newer, the release binary from https://github.com/modelcontextprotocol/registry/releases: the Snap Store package is stuck at 1.1.0 and rejects the current `$schema` with a false "deprecated schema detected", so never edit `server.json` for that error. Older publishers also save their login as `.mcpregistry_github_token` and `.mcpregistry_registry_token` (live tokens) in the current directory; move those out of the repo, never commit them. 9. **Announce it in Discussions**: every release gets a post in the [Announcements](https://github.com/Ark0N/Codeman/discussions/categories/announcements) category, shaped like #418 and #302: the short version first (one bold lead-in per theme, features before fixes, what it does for the user rather than how it works), contributor @-mentions inline where their work is described (a mention notifies them, and a contributor reposting is the cheapest reach this project has), a link to the release, and the Thanks names at the end. Casual first-person voice, no em-dashes, humanizer pass when the skill is available. A same-day follow-on patch (1.28.1 after 1.28.0) is folded into the previous post as an edit, never a second thread. Post it with `gh api graphql -F body=@ -f title='Codeman : ' -f repo=R_kgDOQ-SMDg -f cat=DIC_kwDOQ-SMDs4DCHZE -f query='mutation($repo:ID!,$cat:ID!,$title:String!,$body:String!){createDiscussion(input:{repositoryId:$repo,categoryId:$cat,title:$title,body:$body}){discussion{number url}}}'` (the repo's node id and its Announcements category id; `pinDiscussion` does not exist in the API, so pinning stays a click in the UI). This step exists because announcements stopped at 1.18 (#302) while ten releases shipped with nobody notified; #418 is the backfill covering 1.19.0 to 1.28.1, and release notes only count as content once they reach a surface people are subscribed to.