Fork note: this pipeline was inherited from upstream
add-mcp. The package was published asadd-sg-mcpup to 1.1.0 and is now@stackguardian/mcp-wizard. Before the first release under the new name, an npm owner of@stackguardian/mcp-wizardmust register the Trusted Publisher for theStackGuardian/stackguardian-mcprepository and therelease.ymlworkflow (see the one-time setup below); nothing publishes until that is done.
This document is the authoritative guide for publishing @stackguardian/mcp-wizard to npm. It covers the threat model, the one-time setup of the secure publishing pipeline, the per-release runbook, and the break-glass procedure if something goes wrong.
@stackguardian/mcp-wizard does not support publishing from a maintainer's laptop. Every published version must pass through the GitHub Actions release workflow with OIDC trusted publishing, provenance, and a manual stage-approval step on npm.
Recent npm supply-chain incidents (Shai-Hulud, repeated maintainer-account takeovers, leaked CI tokens) almost all stem from one root cause: long-lived npm publish tokens stored somewhere. The pipeline below removes that root cause and adds two independent gates between "commit on main" and "live on the registry":
- Click "Publish release" on GitHub — confirms the commit and version are intentional.
npm stage approve @stackguardian/mcp-wizardwith WebAuthn — confirms the actual tarball CI produced is what should go live.
Either gate alone is enough to stop a bad publish.
flowchart LR
devCommit[Commit on main] --> ghRelease["Maintainer publishes GitHub Release vX.Y.Z"]
ghRelease --> tagProt["Tag-protection rule allows tag"]
tagProt --> ciRun["release.yml runs on GitHub-hosted runner"]
ciRun --> oidc["GitHub mints OIDC token (id-token: write)"]
oidc --> npmStage["npm stage publish --provenance"]
npmStage --> staging["Package sits in npm staging area"]
staging --> review["Maintainer: npm stage view @stackguardian/mcp-wizard"]
review --> approve["npm stage approve @stackguardian/mcp-wizard (WebAuthn)"]
approve --> live["Live on registry.npmjs.org with provenance"]
- npm Trusted Publishing (OIDC) — npm only accepts a publish from
release.ymlinStackGuardian/stackguardian-mcp, authenticated by a short-lived OIDC token GitHub mints per run. No long-lived tokens exist anywhere. - npm provenance attestation — every published version is cryptographically signed via Sigstore and bound to the exact GitHub commit and workflow that built it. Anyone can run
npm audit signaturesto verify. - "Require 2FA and disallow tokens" on the package — registry-side switch that refuses any publish that is not OIDC + 2FA, even if an attacker steals a legacy token.
- npm staged publishing —
release.ymlrunsnpm stage publish, notnpm publish. The tarball lands in a staging area; the maintainer inspects it and approves with WebAuthn before it goes live. - GitHub tag-protection rules — only maintainers can create the
v*tag that ships with a Release. - WebAuthn/passkey 2FA on the npm account — phishing-resistant (TOTP is being phased out by npm).
- SHA-pinned third-party Actions in
release.yml— neutralises Action-side compromises (a malicious force-push to a tag cannot move the pin).
Work through these top to bottom. None of them can be automated; they are all in the npm and GitHub web UIs. Tick each off when done.
Done under your personal npm user, not the package.
- A1. Enable WebAuthn/passkey 2FA at
https://www.npmjs.com/settings/<your-username>/2fa. If TOTP is your only second factor, enroll a passkey and then remove TOTP. New TOTP setups are disabled by npm and existing TOTP is being phased out. - A2. Revoke every existing publish token at
https://www.npmjs.com/settings/<your-username>/tokensthat grants write access to@stackguardian/mcp-wizard. After trusted publishing is configured, nothing should need one.
Done on the
@stackguardian/mcp-wizardpackage page on npmjs.com.
-
B1. Go to
https://www.npmjs.com/package/@stackguardian/mcp-wizard/access. Under Trusted Publisher, click GitHub Actions and enter:- Organization:
StackGuardian - Repository:
stackguardian-mcp - Workflow filename:
release.yml - Allowed actions: select
npm stage publishonly. Do not checknpm publish. - Save.
Selecting stage-only is the single most important hardening switch in this whole pipeline: it forces every CI publish through the stage-approve gate. Even if
release.ymlwere compromised, the attacker still cannot push live without your WebAuthn approval. - Organization:
-
B2. On the same page, find Publishing access and choose "Require two-factor authentication and disallow tokens". Save. From this point any publish that is not OIDC + 2FA-approved staged release is rejected by the registry.
- C1. Settings -> Rules -> Rulesets -> New tag ruleset. Name it "release tags". Target tags matching
v*. Enable "Restrict creations", "Restrict updates", and "Restrict deletions". Add yourself (and any future co-maintainers) to the bypass list. Save. - C2. Settings -> Actions -> General -> Workflow permissions = "Read repository contents and packages permissions" (the more restrictive of the two radio buttons). Job-level
id-token: writeinrelease.ymlstill works because workflow files can request additional permissions explicitly, but they cannot exceed what the repo allows globally. - C3. Settings -> Actions -> General -> Allow GitHub Actions to create and approve pull requests = OFF.
- C4. (Recommended) Settings -> Branches -> branch-protection rule on
main: require pull requests, require CI green, require signed commits, no force-push, no deletions. This is your last line of defence against a malicious commit landing right before a tag.
- D1. Cut a patch release through the runbook below (e.g.
v1.9.1with an empty CHANGELOG entry like "internal: validate release pipeline"). Walk through the stage-review step slowly: open the staged tarball on npmjs.com, confirm the file list matchespackage.json#files(dist/,skills/,README.md), confirm the provenance attestation badge appears, then approve. Once the version is live, runnpm audit signatures @stackguardian/mcp-wizard@1.9.1to verify Sigstore signatures end to end.
Follow this every time you ship a new version. The whole sequence is typically 5-10 minutes of attended work; everything in step 5 is automated.
-
Sync
main.git checkout main && git pull -
Make sure everything you want shipped is on
mainand the existing CI is green for that commit. All feature work lands via normal PRs first. -
Bump the version and changelog locally.
- Edit
package.jsonversionaccording to semver (see "Versioning" below). - Add an entry at the top of
CHANGELOG.mdmatching the existing tone. - Commit as
chore: release vX.Y.Zand push tomain.
- Edit
-
Draft and publish the GitHub Release.
- On
https://github.com/StackGuardian/stackguardian-mcp/releases, click Draft a new release. - "Choose a tag" -> type
vX.Y.Z(must matchpackage.json#version; the workflow will fail fast if they disagree). - Target:
main. - Title:
vX.Y.Z. - Body: paste the CHANGELOG entry.
- Click Publish release. This creates the tag and fires
release.yml.
- On
-
Wait for CI.
release.ymlruns typecheck, tests, build, thennpm stage publish --provenance. Watch it on the Actions tab. On green, the package is in npm staging, not live.The same release fires
binaries.yml: it compiles the standalone executables (macOS, Linux, Windows), runs each one on its own OS, and then waits at its Publish job for a reviewer of thereleaseenvironment. Approve it under Review deployments on the run. It uploads the archives,install.sh,install.ps1andSHA256SUMStodl.stackguardian.io, where the install scripts download from, and attaches them with build provenance attestations to the release. If it fails, use Re-run failed jobs on that run. To publish binaries later, use Run workflow with the tag as Use workflow from and as thetaginput; the tag must match that commit'spackage.jsonversion. That run builds the binaries again, so it fails ifdl.stackguardian.ioalready has files for the tag: published files are never replaced. Release a new version instead, or have an AWS admin removeadd-sg-mcp/<tag>/from the downloads bucket first.latestmoves only when the tag is the latest GitHub release. -
Review the staged tarball.
npm stage list # shows pending staged versions for packages you maintain npm stage view @stackguardian/mcp-wizard # inspect the staged release for @stackguardian/mcp-wizard
Verify:
- The version number matches what you intended.
- The file list is exactly
dist/,skills/andREADME.md(whateverpackage.json#fileswhitelists), with no surprise files. - The shasum matches the workflow run's published artifact.
- The provenance attestation references the correct commit SHA.
-
Approve.
npm stage approve @stackguardian/mcp-wizard # prompts for WebAuthnThe package goes live.
-
Verify it landed correctly.
npm view @stackguardian/mcp-wizard version npm view @stackguardian/mcp-wizard dist-tags npm audit signatures @stackguardian/mcp-wizard@X.Y.Z
-
If anything looked wrong at step 6, reject instead of approving.
npm stage reject @stackguardian/mcp-wizard
Nothing is published. Investigate, fix on a branch, cut a new patch release.
@stackguardian/mcp-wizard follows semver. As a rough guide:
- patch — bug fixes, registry adds, internal-only changes that do not alter behaviour.
- minor — new features, new agent support, new flags. Backward-compatible.
- major — breaking changes to flags, config locations, or behaviour. Add a migration note to the CHANGELOG.
- On a separate trusted device, sign in to npmjs.com.
- Remove the compromised device from
https://www.npmjs.com/settings/<your-username>/2fa. - Enroll a fresh passkey.
- No tokens need rotating; trusted publishing keeps working unchanged.
- Disable the trusted-publisher binding on
https://www.npmjs.com/package/@stackguardian/mcp-wizard/accessto halt all CI publishes immediately. - Audit
main, therelease.ymlworkflow, and thev*tag history. - Once clean, re-enable the trusted publisher and ship a clean release.
Note: even without step 1, an attacker cannot directly publish from a PR branch or a fork because OIDC only accepts publishes from release.yml on StackGuardian/stackguardian-mcp's default branch flow. Tag protection prevents them from triggering the real release.yml. Step 1 is belt and braces.
By design you cannot publish from a laptop. The escape hatch:
- On npmjs.com, Publishing access -> flip "Require two-factor authentication and disallow tokens" off temporarily.
- Create a granular access token with a 7-day expiry scoped to
@stackguardian/mcp-wizardonly. - Publish locally with that token.
- Revoke the token immediately.
- Flip "Require 2FA and disallow tokens" back on.
- Open an issue describing what broke in CI and fix it before the next release.
If/when there is a second maintainer, this escape hatch must require their explicit sign-off before being used.
- Dependency pinning / Dependabot / lockfile-lint. Complementary supply-chain hardening; not in this doc.
- Reproducible builds.
tsupoutput is not byte-reproducible across Node versions. The provenance attestation proves "this exact tarball came from this exact commit via this exact workflow," which is the security property that matters. - Sigstore verification of installed dependencies (
npm audit signaturesin CI). Worth adding as a follow-up; not required for the publish-side hardening this document covers.