Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -833,9 +833,16 @@ jobs:
# nexus-flow-76u.15), while staging always runs to rehearse the packaging — registry-free, see the
# publish step for why that distinction is the whole point (6j6v.xk2t). Auth is OIDC trusted
# publishing (no long-lived NXF_NPM_TOKEN — nexus-flow-76u.15): once the var is enabled, a prod
# publish only succeeds when the trusted publisher is configured on the @nexus-flow/mcp package
# release only reaches npm when the trusted publisher is configured on the @nexus-flow/mcp package
# (npmjs.com → package → Trusted Publisher → GitHub Actions → this repo + release.yml). Absent
# that config npm rejects the publish — fail-closed by default. Runbook: docs/specs/release-management.md.
# that config npm rejects it — fail-closed by default. Runbook: docs/specs/release-management.md.
#
# STAGED, NOT PUBLISHED (6j6v.c7dd). The job runs `npm stage publish`: the version lands on npm
# UNAVAILABLE to the public, and only a maintainer's approval WITH 2FA (npmjs.com, or
# `npm stage approve <id>`) makes it live. This is npm's default for trusted publishers since
# 2026-09-03 (a new one is created stage-only) and the owner's choice for this repo: no CI run —
# not even a compromised one — can put a package in front of `npx` users on its own. The job goes
# green when the version is STAGED; the release is not complete on npm until it is approved.
publish-npm:
needs: [meta, publish]
# Run for staging (packaging dry-run) always; for a prod release only once npm publishing is
Expand All @@ -855,16 +862,17 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
# Node 24 for a modern bundled npm; the upgrade step below then pins npm >= 11.5.1, the
# floor for OIDC trusted publishing (npm docs). registry-url points publish at npmjs.org.
# Node 24 for a modern bundled npm; the upgrade step below then pins npm >= 11.15.0, the
# floor for staged publishing (npm docs). registry-url points publish at npmjs.org.
node-version: 24
registry-url: https://registry.npmjs.org

# Trusted publishing (OIDC) requires npm CLI >= 11.5.1; pin the range instead of `@latest` so a
# future npm major can't silently change publish behaviour in this credential-minting job.
# Zero-dep shim, so this touches nothing the package ships.
- name: Ensure npm supports trusted publishing
run: npm install -g "npm@>=11.5.1 <12"
# Staged publishing (`npm stage publish`) requires npm CLI >= 11.15.0 (trusted publishing alone
# needed 11.5.1); pin the range instead of `@latest` so a future npm major can't silently change
# publish behaviour in this credential-minting job. Zero-dep shim, so this touches nothing the
# package ships.
- name: Ensure npm supports staged trusted publishing
run: npm install -g "npm@>=11.15.0 <12"

# Stamp the release version into the package (lockstep). The published file list is unaffected.
- name: Stamp the package version
Expand Down Expand Up @@ -948,6 +956,11 @@ jobs:
# succeeds once the trusted publisher is configured on the package (else npm rejects it —
# fail-closed by default; see the runbook in docs/specs/release-management.md).
# Idempotent: a re-run of the same tag must not fail on an already-published version.
# A version that is STAGED but not yet approved is invisible to `npm view`, and a re-run
# then fails on npm's own uniqueness check. That is deliberate: the short-lived OIDC token
# may only stage or publish, not list the queue, so this job cannot tell "already staged"
# from any other refusal, and guessing from error text would turn a real refusal green.
# Approve (or reject) the staged version instead of re-running.
if npm view "@nexus-flow/mcp@${VERSION}" version >/dev/null 2>&1; then
echo "@nexus-flow/mcp@${VERSION} already published — skipping"
exit 0
Expand All @@ -968,8 +981,24 @@ jobs:
else
echo "repo not public (private=${REPO_PRIVATE:-unknown}) → provenance omitted (npm cannot attest a non-public repo)"
fi
npm publish --access public "${PROVENANCE[@]}"
echo "published @nexus-flow/mcp@${VERSION}"
npm stage publish --access public "${PROVENANCE[@]}" 2>&1 | tee "$RUNNER_TEMP/stage.log"
echo "staged @nexus-flow/mcp@${VERSION} — NOT live until a maintainer approves it"
{
echo "## npm: @nexus-flow/mcp@${VERSION} is STAGED, not live"
echo
echo "A maintainer approves it with 2FA — on npmjs.com (package → Staged versions), or:"
echo
echo '```'
echo "npm stage list @nexus-flow/mcp"
echo "npm stage approve <stage-id>"
echo '```'
echo
echo "npm's own output:"
echo
echo '```'
cat "$RUNNER_TEMP/stage.log"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

# Refresh the STAGING federated content after a real beta release so the staging landing's
# /open-source changelog reflects the new beta (nexus-flow-s8a, re-homed to the content provider
Expand Down
32 changes: 24 additions & 8 deletions docs/specs/release-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,10 +853,14 @@ new installs/checks; already-affected users are healed by a higher fix release.
8. Rehearsal: `release.yml` via dispatch against staging (with `NXF_MACOS_SIGNING_ENABLED=true`
the run verifies codesign + notarization + `spctl --assess`); then the first real
tag release.
9. **npm publishing of `@nexus-flow/mcp` via trusted publishing.** The `publish-npm`
9. **npm publishing of `@nexus-flow/mcp` via trusted, STAGED publishing.** The `publish-npm`
job authenticates by **OIDC trusted publishing — no long-lived npm token** (validated against the
current npm docs; this supersedes the earlier `NXF_NPM_TOKEN` plan). It is fail-closed: a prod publish
only succeeds once the trusted publisher is configured on the package, so nothing ships prematurely.
current npm docs; this supersedes the earlier `NXF_NPM_TOKEN` plan). It is fail-closed: a prod release
only reaches npm once the trusted publisher is configured on the package, so nothing ships prematurely.
**Since 6j6v.c7dd it STAGES rather than publishes** (`npm stage publish`): the version sits on npm
unavailable to the public until a maintainer approves it with 2FA. That is npm's default for trusted
publishers created since 2026-09-03, and the owner's choice: no CI run can put a package in front of
`npx` users on its own. **Every release therefore ends with one human step** — see g.
One-time owner steps (require the npm account that will own the org):
a. **Own the scope.** Create the **`@nexus-flow`** org on npmjs.com (this reserves the `@nexus-flow/*`
scope) and confirm the **`@nexus-flow/mcp`** name is free.
Expand All @@ -868,9 +872,13 @@ new installs/checks; already-affected users are healed by a higher fix release.
use that and skip the manual publish.)
c. **Configure the trusted publisher.** npmjs.com → `@nexus-flow/mcp` → *Settings* → *Trusted Publisher*
→ **GitHub Actions**, with: organization/owner **`nxsflow`**, repository **`nexus-flow`**, workflow
filename **`release.yml`** (filename only). Leave *Environment* blank (the job uses none); if the
form offers an allowed-actions choice, select **npm publish**. These must match the `publish-npm`
job exactly (`id-token: write`, `release.yml`).
filename **`release.yml`** (filename only). Leave *Environment* blank (the job uses none). Under
*Allowed actions* keep **stage publish only** (npm's default); do NOT tick direct publish — the job
never uses it, and leaving it off is what makes the approval step binding. A trusted publisher that
allows only staging answers a direct `npm publish` with `403 … OIDC permission denied for this
action`. These must match the `publish-npm` job exactly (`id-token: write`, `release.yml`). The entry
is bound to the repository, not only its name: after the 2026-09 cutover to the history-free repo
the old entry answered `404` on the PUT until it was removed and re-added.
d. **Lock it down.** Once a trusted publish works, set the package to *Require two-factor authentication
and disallow tokens* (npm recommendation) — OIDC still works, and stray tokens can no longer publish.
*Optional defense-in-depth (yk2c / review #160.4).* The trust today is "any `release.yml` run with
Expand All @@ -896,8 +904,16 @@ new installs/checks; already-affected users are healed by a higher fix release.
a machine **without** `nxs`: `npx -y @nexus-flow/mcp -- --workspace <path>` fetches the signed `nxs`
from the live CDN, passes **sha256 + minisign**, caches, and serves. Confirm a **tampered/wrong-key**
artifact still **aborts** against the live pubkey (the fail-closed guarantee — already unit-proven by
the shim's `node --test` suite, re-asserted against the real CDN here). npm CLI ≥ 11.5.1 + Node ≥ 22.14
are required for OIDC; the job pins them (Node 24 + `npm install -g npm@latest`).
the shim's `node --test` suite, re-asserted against the real CDN here). npm CLI ≥ 11.15.0 (staged
publishing; OIDC alone needed 11.5.1) + Node ≥ 22.14 are required; the job pins them (Node 24 +
`npm install -g "npm@>=11.15.0 <12"`).
g. **Approve every release (the human step).** The `publish-npm` job goes green when the version is
STAGED, and writes the approval instructions into its job summary. Approve on npmjs.com
(`@nexus-flow/mcp` → staged versions) or with `npm stage list @nexus-flow/mcp` then
`npm stage approve <stage-id>`; both ask for 2FA. `npm stage reject <stage-id>` discards a bad one.
Until then `npx -y @nexus-flow/mcp` keeps resolving the previous version. Do not re-run the job for
a version that is staged but unapproved: npm refuses a second copy of the same version, and the
job cannot see the queue (its OIDC token may only stage or publish), so it fails rather than guess.

## 13. Implementation phases

Expand Down
Loading