Recipe-Inhalt ist auf Englisch. Englisches Original lesen →
← Alle Recipes
Phase 9 · Distribution·8 steps

Trusted publishing, token-less npm releases via GitHub OIDC

Replace NPM_TOKEN secrets with OIDC. publish.yml that triggers on tag push. npm provenance badges. Cleaner trust signal for security-conscious users.

8 steps0%
Du liest ohne Account. Mit Login speichern wir Step-Fortschritt + Notes.

Trusted publishing, token-less npm releases via GitHub OIDC

Storing NPM_TOKEN as a GitHub secret has worked forever, but it's a long-lived credential, leak it and an attacker can publish malicious versions of your package. Trusted publishing swaps the static token for OIDC: GitHub Actions exchanges a short-lived signed token at publish time, no static secret on either side. This recipe is the 5-minute setup plus the npm provenance badge that tells users "this came from a verified GitHub Actions build".

Schritt 1: Why this matters

Three failure modes the static token pattern allows:

  • Repo compromise, NPM_TOKEN env var leaks through a malicious dep, attacker publishes [email protected] with a backdoor.
  • Maintainer compromise, npm account stolen, attacker publishes from elsewhere.
  • Misuse drift, the token gets reused in random scripts over time, eventually exposed.

OIDC fixes #1 and partially #3. The token is created at publish time, valid for ~10 minutes, scoped to the exact repo + workflow + ref. There's no static secret to leak.

npm provenance (the bonus) adds a verifiable claim to the published version: "this package was built by GitHub Actions in you/your-mcp-server from commit SHA abc123 at timestamp Y." Users can verify the chain.

Schritt 2: Configure trusted publisher on npm

  1. Go to https://www.npmjs.com/package/your-mcp-server/access (you must be the maintainer).
  2. Trusted Publisher ManagementAdd trusted publisher.
  3. Fill:
    • Repository owner: your GitHub username/org
    • Repository name: your-mcp-server
    • Workflow name: publish.yml (must match the file in .github/workflows/)
    • Environment (optional): production (recommended, adds another layer)
  4. Save.

After this, npm will accept publishes only from this exact workflow / repo / branch combo.

Schritt 3: The publish workflow

# .github/workflows/publish.yml
name: Publish to npm

on:
  push:
    tags: ['v*']  # triggers on v1.0.0, v1.0.1, etc.

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: production  # if you set the optional environment in Step 2
    permissions:
      contents: read
      id-token: write  # ← required for OIDC token exchange
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
          cache: 'npm'

      - run: npm ci
      - run: npm run build
      - run: npm test

      - run: npm publish --provenance --access public
        # No NODE_AUTH_TOKEN! OIDC handles auth via the id-token permission above.

Key bits:

  • permissions.id-token: write, grants the workflow the right to request an OIDC token from GitHub. Without this, npm rejects.
  • No NODE_AUTH_TOKEN, that's the whole point. Compare to the old pattern where this would be ${{ secrets.NPM_TOKEN }}.
  • --provenance, generates the signed provenance attestation. Requires id-token: write.
  • --access public, required for scoped packages on first publish.
  • npm test before publish, bad version doesn't ship.

Schritt 4: Trigger a publish

npm version patch         # 1.0.0 → 1.0.1, creates git tag v1.0.1, commits
git push origin main --tags

The tag push triggers the workflow. Watch it under Actions:

Run npm publish --provenance --access public
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice ✓ Provenance statement uploaded to https://search.sigstore.dev/
+ [email protected]

Sigstore link is the verifiable claim. Anyone can check it later.

Schritt 5: Provenance badge in README

After your first provenance-enabled publish, add to README:

[![npm provenance](https://img.shields.io/npm/v/your-mcp-server?label=npm%20provenance&color=brightgreen&logo=npm)](https://www.npmjs.com/package/your-mcp-server?activeTab=provenance)

The npm package page now shows a "Provenance" tab listing every release built via OIDC, with the source commit and workflow. Reviewers can verify the published bytes match the GitHub source.

Schritt 6: Confirm setup

# 1. Check that OIDC publish worked (no token used)
gh run list --workflow=publish.yml --limit=1
# Look for "success" status

# 2. Verify provenance on npm
npm view your-mcp-server --json | jq '.dist'
# Should include `signatures` array
# Plus an `attestations` block referencing GitHub Actions

# 3. Public verification (anyone can run this)
npm audit signatures your-mcp-server
# verified registry signatures, audited Y packages in Xs
# Y packages have verified registry signatures
Client-Check · auf Deinem Rechner ausführen
cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); print(\"name:\", p.get(\"name\")); print(\"version:\", p.get(\"version\")); print(\"private:\", p.get(\"private\"))" 2>/dev/null || echo "no package.json"
Erwartet: name + version are set, private is false (or omitted).
Falls hängen geblieben: Remove `"private": true`, set a unique name, then `npm publish --access public`.

Schritt 7: Verify

Run academy_validate_step. The validator checks npm view your-mcp-server version returns a version.

For trusted-publishing specifically, the package page on npmjs.com should show:

  • ✅ "Provenance" tab with attestations
  • ✅ "Built and signed on GitHub Actions" badge
  • ✅ Source commit visible per release

If you don't see those, the workflow either didn't request the OIDC token (id-token: write missing) or didn't pass --provenance to npm publish.

Schritt 8: Migration from NPM_TOKEN

If you currently publish with NPM_TOKEN:

  1. Set up trusted publisher on npm (Step 2).
  2. Update workflow to remove NODE_AUTH_TOKEN and add id-token: write + --provenance (Step 3).
  3. Test with a npm version prerelease first to verify.
  4. After successful provenance publish, revoke the old NPM_TOKEN in npm Settings → Access Tokens.
  5. Remove the NPM_TOKEN GitHub secret in Settings → Secrets → Actions.

The old static token is now useless. If anyone leaked it, the leak is no longer dangerous, npm rejects it.

Common traps

  • Forgetting id-token: write, workflow runs but npm publish fails with auth error. Always include the permission.
  • Workflow filename mismatch, npm trusted publisher config requires exact filename. If you renamed publish.ymlrelease.yml, update both sides.
  • Branch / tag mismatch, if you set the trusted publisher to "main" branch but trigger on tag push, the OIDC claim ref doesn't match. Use tags: ['v*'] trigger and configure the publisher accordingly.
  • environment: production set on workflow but not on npm, npm rejects with "environment claim mismatch". Set both or neither.
  • --provenance without id-token: write, error "OIDC unavailable". Fix permissions block.
  • Forgetting to revoke the old NPM_TOKEN, security debt. After OIDC works, delete the static token.

What good looks like

Tag push triggers the workflow, OIDC exchanges a short-lived token, npm publish --provenance succeeds, npm package page shows the Provenance tab with the signed attestation pointing at the exact commit. No long-lived secrets anywhere. Reviewers see the provenance badge and trust your supply chain a notch more.

For OSS MCP servers, trusted publishing is becoming the table-stakes signal. Set it up once and never think about NPM_TOKEN rotation again.

MCP marketplaces, where to subWriting a README that actually