fix(dsh): the gate now says when it judged a capped payload #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Publishing is tag-driven on purpose: a human decides the version, the tag | |
| # records when, and nothing reaches npm from a working branch. | |
| # | |
| # Authentication is OIDC trusted publishing — there is no NPM_TOKEN to store, and | |
| # therefore none to leak or rotate. Configure it once per package at npmjs.com, | |
| # under the package's Settings -> Trusted Publisher -> GitHub Actions: | |
| # | |
| # organization or user : PerryLink | |
| # repository : jevcore | |
| # workflow filename : release.yml | |
| # environment : (leave empty) | |
| # | |
| # The filename is matched exactly, extension included, so it is load-bearing: | |
| # renaming this file silently breaks the release, and a mismatch shows up as an | |
| # authentication failure rather than as a missing workflow. `release.yml` is also | |
| # what 38 of the sibling plugin repositories call theirs (`publish.yml` appears in | |
| # 7), which is why this file is not named after what it does. | |
| # | |
| # `id-token: write` below is what lets the runner mint the OIDC token that npm | |
| # exchanges for publish rights for every package listed in the Publish step. | |
| # | |
| # Do NOT reintroduce a registry token. pnpm treats a configured | |
| # `//registry.npmjs.org/:_authToken` as an instruction to publish with that token | |
| # and skips the OIDC exchange, so a stale or empty token silently downgrades this | |
| # workflow from trusted publishing to token publishing and then fails on auth. | |
| # pnpm's own release workflow carries a comment about deleting that variable | |
| # before its trusted-publishing step for exactly this reason. `actions/setup-node` | |
| # writes the key into `.npmrc` whenever it is given `registry-url`, which is why | |
| # no step below passes it. | |
| # | |
| # KNOWN BLOCKER, and not a configuration error. This repository was created | |
| # 2026-09-20, so GitHub issues its OIDC tokens with an immutable subject claim | |
| # (`repo:PerryLink@255665900/jevcore@1377893932:ref:...`) and npm's registry | |
| # cannot match that format yet: the exchange is rejected and the step ends at | |
| # "Skipped OIDC". Tracked upstream as https://github.com/npm/cli/issues/9969, | |
| # with this repository's reproduction in the first comment | |
| # (https://github.com/npm/cli/issues/9969#issuecomment-5749565836), and | |
| # it cannot be switched off on this side -- PUT to | |
| # /repos/PerryLink/jevcore/actions/oidc/customization/sub with | |
| # {"use_immutable_subject":false} answers 422 and the setting stays true. | |
| # PUBLISHING.md records the staged fallback that works until npm fixes it; this | |
| # configuration stays as it is so it starts working the day the registry does. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # Required for OIDC: without it the publish step cannot mint the token. | |
| id-token: write | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check what triggered this run | |
| run: | | |
| echo "event = ${{ github.event_name }}" | |
| echo "ref = ${{ github.ref }}" | |
| echo "ref_name = ${{ github.ref_name }}" | |
| - uses: actions/setup-node@v5 | |
| with: | |
| # No `registry-url` here — see the note at the top of this file. It | |
| # would write an `_authToken` line into `.npmrc` and silently downgrade | |
| # the publish below. | |
| node-version: "22" | |
| # setup-node v5 turns package-manager caching on by itself when | |
| # package.json declares `packageManager`, and this repository pins one. | |
| # That runs `pnpm` before `corepack enable` has put it on PATH and the | |
| # step fails before the publish is ever reached. Caching is not worth | |
| # reordering the toolchain for: the pnpm that performs the OIDC | |
| # exchange must be the one corepack resolves. | |
| package-manager-cache: false | |
| - name: Enable corepack | |
| run: corepack enable | |
| - name: Report the toolchain | |
| # Printed because trusted publishing depends on the pnpm and npm versions | |
| # actually in use, and a runner image change is easier to read here than | |
| # to infer from an auth failure three steps later. | |
| run: | | |
| node --version | |
| pnpm --version | |
| npm --version | |
| - name: Install | |
| run: pnpm install --no-frozen-lockfile | |
| - name: Confirm the tag matches the manifests | |
| if: github.event_name == 'push' | |
| run: | | |
| node -e " | |
| const tag = process.env.GITHUB_REF_NAME.replace(/^v/, ''); | |
| const { readFileSync } = require('node:fs'); | |
| const names = ['package.json', 'packages/core/package.json', 'packages/dsh/package.json', 'packages/mcp/package.json', 'packages/cli/package.json']; | |
| let failed = false; | |
| for (const file of names) { | |
| const version = JSON.parse(readFileSync(file, 'utf8')).version; | |
| if (version !== tag) { | |
| console.error(file + ': version ' + version + ' does not match tag ' + process.env.GITHUB_REF_NAME); | |
| failed = true; | |
| } | |
| } | |
| if (failed) process.exit(1); | |
| console.log('every manifest is at ' + tag); | |
| " | |
| - name: Verify before publishing | |
| run: pnpm run check | |
| - name: Diagnose trusted publishing | |
| # Manual runs only, and it publishes nothing: this is what to reach for | |
| # when a release fails with `Skipped OIDC: ... Unknown error (status code | |
| # 404)`. That 404 means npm did not match the run against a trusted | |
| # publisher, and pnpm reports it without npm's response body, so the two | |
| # halves that must agree are printed side by side instead: | |
| # | |
| # - `job_workflow_ref`, the exact string npm matches its "Workflow | |
| # filename" field against (filename *with* extension, no path); | |
| # - npm's own answer per package, which distinguishes "no trusted | |
| # publisher for this package" from "one exists but does not match". | |
| # | |
| # Claims and response bodies only. The OIDC token is never printed. | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| set -euo pipefail | |
| response="$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org")" | |
| token="$(node -e "process.stdout.write(JSON.parse(process.argv[1]).value)" "$response")" | |
| echo "--- claims npm matches against ---" | |
| node -e " | |
| const payload = process.argv[1].split('.')[1]; | |
| const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')); | |
| for (const key of ['job_workflow_ref', 'repository', 'repository_owner', 'ref', 'event_name', 'aud']) { | |
| if (claims[key] !== undefined) console.log(key + ' = ' + claims[key]); | |
| } | |
| " "$token" | |
| echo "--- npm's answer per package ---" | |
| # The path comes from npm's own CLI (lib/utils/oidc.js) and matches | |
| # pnpm's (releasing/commands/lib/publish/oidc/authToken.js), both of | |
| # which POST to `/-/npm/v1/oidc/token/exchange/package/<name>`. A path | |
| # that is merely close -- `token-exchange/<name>`, say -- answers | |
| # `ResourceNotFound: ... does not exist`, which reads like a missing | |
| # trusted publisher and is not. | |
| for pkg in jevcore jevcore-dsh jevcore-mcp jevcore-cli; do | |
| code="$(curl -sS -o exchange.json -w '%{http_code}' -X POST \ | |
| -H "Authorization: bearer $token" -H 'Accept: application/json' --data '' \ | |
| "https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/$pkg")" | |
| if [ "$code" = "200" ]; then | |
| # A successful exchange returns a publish token in the body, and | |
| # this log is public. Report that it worked; never the token. | |
| echo "$pkg -> HTTP $code (exchange succeeded; body withheld)" | |
| else | |
| echo "$pkg -> HTTP $code $(head -c 400 exchange.json)" | |
| fi | |
| done | |
| - name: Publish | |
| # Core first: every other package depends on `jevcore`, and pnpm rewrites | |
| # the `workspace:*` specifier to the real version at pack time, so a | |
| # dependent published against an unpublished core would point at a version | |
| # that does not exist yet. | |
| if: github.event_name == 'push' | |
| run: | | |
| set -euo pipefail | |
| # A version already on the registry is skipped, not failed: a re-run, or | |
| # a release finished by hand after a partial failure, should not turn | |
| # green work red. A version that is *staged* on the registry is | |
| # invisible to this check -- npm's stage list reports total: 0 for it -- | |
| # and publishing over it fails with 409. | |
| publish() { | |
| local pkg="$1" dir="$2" version | |
| version="$(node -p "require('./packages/$dir/package.json').version")" | |
| if npm view "$pkg@$version" version >/dev/null 2>&1; then | |
| echo "$pkg@$version is already published; skipping" | |
| return 0 | |
| fi | |
| echo "publishing $pkg@$version" | |
| pnpm --filter "$pkg" publish --access public --provenance --no-git-checks | |
| } | |
| publish jevcore core | |
| publish jevcore-dsh dsh | |
| publish jevcore-mcp mcp | |
| publish jevcore-cli cli | |
| - name: Check the MCP Registry manifest before publishing | |
| # Runs only where the npm publish runs: the registry verifies npm | |
| # ownership by reading `mcpName` from the PUBLISHED package, so it has | |
| # nothing to say about a version npm does not have. | |
| # | |
| # This one HARD-FAILS, unlike the publish below, because a server.json | |
| # that disagrees with the package it describes is a repository mistake | |
| # rather than a registry problem: no retry fixes it, and the registry | |
| # only rejects it after the npm release has already gone out. Both halves | |
| # that must agree are printed either way. | |
| if: github.event_name == 'push' | |
| run: | | |
| set -euo pipefail | |
| node -e " | |
| const { existsSync, readFileSync } = require('node:fs'); | |
| const manifest = 'packages/mcp/server.json'; | |
| if (!existsSync(manifest)) { | |
| console.error('::error::' + manifest + ' is missing, so there is nothing to publish to the MCP Registry'); | |
| process.exit(1); | |
| } | |
| const pkg = JSON.parse(readFileSync('packages/mcp/package.json', 'utf8')); | |
| const server = JSON.parse(readFileSync(manifest, 'utf8')); | |
| const advertised = server.packages && server.packages[0] ? server.packages[0].version : undefined; | |
| console.log('package.json version=' + pkg.version + ' mcpName=' + pkg.mcpName); | |
| console.log('server.json name=' + server.name + ' version=' + server.version + ' packages[0].version=' + advertised); | |
| const problems = []; | |
| if (server.name !== pkg.mcpName) { | |
| problems.push('server.json name (' + server.name + ') must equal package.json mcpName (' + pkg.mcpName + '); the registry grants the io.github.<owner>/* namespace from the OIDC claim, case sensitively'); | |
| } | |
| if (server.version !== pkg.version) { | |
| problems.push('server.json version (' + server.version + ') must equal package.json version (' + pkg.version + ')'); | |
| } | |
| if (advertised !== pkg.version) { | |
| problems.push('server.json packages[0].version (' + advertised + ') must equal package.json version (' + pkg.version + ')'); | |
| } | |
| if (problems.length > 0) { | |
| for (const problem of problems) console.error('::error::' + problem); | |
| process.exit(1); | |
| } | |
| console.log('the registry manifest describes ' + server.name + '@' + server.version); | |
| " | |
| - name: Publish to the official MCP Registry | |
| # ONE STEP, TWO FAILURE MODES: what a retry can fix is not fatal, what it | |
| # cannot fix is. The manifest mismatch above fails the run; a registry | |
| # failure here does not. | |
| # | |
| # `continue-on-error` is deliberate, and not because the registry does | |
| # not matter. npm is the release contract, and the MCP Registry is a | |
| # discovery surface for a service still in preview, where an outage or a | |
| # breaking change is outside this repository's control. Turning an | |
| # already-successful npm release red for that teaches a releaser to | |
| # ignore red runs, which is the expensive failure. Being allowed to fail | |
| # is not being allowed to fail QUIETLY, so the step annotates the run with | |
| # a ::warning::, stays visibly failed in the UI, and names its own | |
| # recovery: re-run the failed jobs. That retry is free -- the npm steps | |
| # above skip versions already on the registry, and the preflight below | |
| # skips a version already in the registry. | |
| # | |
| # OIDC here is NOT the OIDC that is blocked. npm's registry cannot match | |
| # this repository's immutable OIDC subject claim, tracked as | |
| # https://github.com/npm/cli/issues/9969, which is why the publish step | |
| # above can end at "Skipped OIDC". The MCP Registry is a different service | |
| # performing its own exchange: the CLI asks the runner for a token with | |
| # audience `mcp-registry` (the audience npm never sees), and the registry | |
| # mints a short-lived publish token scoped to io.github.<owner>/*, so | |
| # npm's matching bug is not in that path. NOT CONFIRMED END TO END: the | |
| # strongest evidence is that dsh-cert-mcp, by the same owner, is in the | |
| # registry with a listing published 2026-09-19, but that repository was | |
| # created before this one, so whether the registry tolerates the | |
| # immutable subject format is unproven until this runs on a real tag. If | |
| # it does not, the :warning: says so rather than the run going green in | |
| # silence. | |
| # | |
| # No token is configured anywhere, and none may be: `login github-oidc` | |
| # mints the short-lived one from the runner's own identity. `id-token: | |
| # write` at the top of this file is what makes that possible and was | |
| # already there for npm. | |
| # | |
| # The CLI is pinned to a release tag instead of `latest`: a download from | |
| # a moving tag is a supply-chain decision nobody made. v1.8.1 is the | |
| # current release of the official registry repository; bump it on purpose. | |
| if: github.event_name == 'push' | |
| continue-on-error: true | |
| run: | | |
| set -euo pipefail | |
| name="$(node -p "require('./packages/mcp/server.json').name")" | |
| version="$(node -p "require('./packages/mcp/package.json').version")" | |
| mcpName="$(node -p "require('./packages/mcp/package.json').mcpName")" | |
| # A version already in the registry is skipped, not failed -- the same | |
| # rule the npm publish above follows, so a re-run is green. The API | |
| # path and shape were checked against the live registry; if the call | |
| # fails for any reason the publish below is attempted anyway, because a | |
| # preflight that cannot see the registry must not become the gate. | |
| already="$(node -e " | |
| const [name, version] = process.argv.slice(1); | |
| fetch('https://registry.modelcontextprotocol.io/v0/servers?search=' + encodeURIComponent(name)) | |
| .then((response) => (response.ok ? response.json() : { servers: [] })) | |
| .then((body) => { | |
| const listed = (body.servers || []).some((entry) => entry.server && entry.server.name === name && entry.server.version === version); | |
| process.stdout.write(listed ? 'yes' : 'no'); | |
| }) | |
| .catch(() => process.stdout.write('no')); | |
| " "$name" "$version")" | |
| if [ "$already" = "yes" ]; then | |
| echo "$name@$version is already in the MCP Registry; skipping" | |
| exit 0 | |
| fi | |
| # npm's read side lags minutes behind a successful publish, and the | |
| # registry reads that same metadata to decide whether this repository | |
| # owns the package. Wait for it, but bounded: the publish below stays | |
| # the authority, and a lagging mirror must not be the reason a release | |
| # never reaches the registry. | |
| for attempt in $(seq 1 6); do | |
| got="$(npm view "jevcore-mcp@${version}" mcpName 2>/dev/null || true)" | |
| if [ "$got" = "$mcpName" ]; then | |
| echo "npm exposes mcpName=$got for jevcore-mcp@$version" | |
| break | |
| fi | |
| echo "attempt $attempt: npm reports mcpName='$got' for jevcore-mcp@$version, want '$mcpName'; retrying" | |
| sleep 15 | |
| done | |
| mkdir -p "$RUNNER_TEMP/mcp-publisher" | |
| curl -fsSL "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz -C "$RUNNER_TEMP/mcp-publisher" mcp-publisher | |
| publisher="$RUNNER_TEMP/mcp-publisher/mcp-publisher" | |
| "$publisher" --help >/dev/null | |
| # server.json is the manifest, so the publish runs from its directory: | |
| # packages/mcp/README.md is the server's own page and its manifest sits | |
| # beside it. | |
| cd packages/mcp | |
| "$publisher" login github-oidc | |
| if "$publisher" publish; then | |
| echo "published $name@$version to the MCP Registry" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "::warning::the MCP Registry publish of $name@$version failed. npm is published; the registry entry is not. Re-run the failed jobs of this workflow to retry it -- every step here is idempotent, and an already-published version is skipped rather than re-published." | |
| exit 1 | |
| fi |