Skip to content

fix(dsh): the gate now says when it judged a capped payload #6

fix(dsh): the gate now says when it judged a capped payload

fix(dsh): the gate now says when it judged a capped payload #6

Workflow file for this run

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