Skip to content

ci(publish): keep the dev toolchain out of the job that can mint the npm OIDC token (PHARN-07) - #201

Merged
PrzemekGalarowicz merged 1 commit into
mainfrom
claude/bold-archimedes-5czyyn
Sep 24, 2026
Merged

PrzemekGalarowicz merged 1 commit into
mainfrom
claude/bold-archimedes-5czyyn

Conversation

@PrzemekGalarowicz

Copy link
Copy Markdown
Contributor

What this changes

publish.yml did everything in one job that has id-token: write: npm ci (which runs dev-dependency install scripts) and then npm publish, which in turn ran the prepublishOnly gates (prettier, eslint, markdownlint, tsc, vitest) and the prepack esbuild step. With id-token: write, any step in the job can request the OIDC token that npm exchanges for publish rights. So a single compromised dev dependency, out of about 280, could have published a malicious version, and it would have carried a valid provenance attestation.

The workflow is now split into two jobs:

  • build — no id-token. Before installing anything it checks three things:

    • the tag matches ^v[0-9]+\.[0-9]+\.[0-9]+$;
    • the tag equals package.json version;
    • git merge-base --is-ancestor "$GITHUB_SHA" origin/main, i.e. the tagged commit is on main.

    Then it runs npm ci, npm run check, npm run test:coverage and npm pack (the build runs as part of pack). It installs the packed tarball into a scratch directory, runs pharn --version, and uploads the tarball as an artifact.

  • publish — the only job with id-token: write and the npm-publish environment. It does no checkout and no install. It runs the existing Assert npm floor step, downloads the artifact, and runs npm publish pkg/pharn-dev-pharn-<version>.tgz --provenance --access public --ignore-scripts. <version> is the one the build job verified.

Supporting changes:

  • New check in .dev/floor/check-run-pins.test.mjs: id-token: write must appear exactly once, inside the publish job, and that job must not check out or install anything and must use --ignore-scripts. It fails on the old file. The same file's count of workflow installs goes from 9 to 10.
  • Docs: docs/RELEASING.md step 5 is rewritten for the two jobs; CLAUDE.md Releasing section updated.

Built with /pharn-dev-ship; stage artifacts are in .dev/features/publish-split-oidc/. Results:

  • validate: exit 0
  • regress: no-regressions
  • verify: PASS
  • full stdlib floor suite: 749/749

Type of change

  • feat — new stack option, wizard step, or command capability
  • fix — bug fix
  • docs — docs-only change
  • chore / refactor — tooling or internal restructure, no behavior change

Area(s) touched

repo tooling (publish.yml, floor test) | docs

Checklist

  • Read the existing file(s) before editing.
  • Added a floor test that fails on the old workflow.
  • Updated docs/RELEASING.md.
  • No secrets or tokens were added. Every uses: is pinned to a SHA; check-action-pins and check-run-pins report no violations.

Quality gates

  • npm run check passes locally (1335/1335; non-root user, node 22).
  • npm run build / npm run test:coverage (left to CI).

Notes for the reviewer

  • ⚠️ Artifact action SHAs not verified. actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 and actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 are pinned from memory. I had no GitHub access outside this repo to check them, and you asked me to assume they work. The first release run is their real test. If a SHA is wrong, the run fails before anything is published. Please check them against the upstream tags before the next release.
  • Prerelease tags are now refused. Publishing a release candidate would need a workflow change.
  • What this does not fix: a compromised dev dependency in build can still change the contents of the tarball. What it can no longer do is get the OIDC token or publish by itself.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TvcuVhk8hTeDskp5pAJhnc


Generated by Claude Code

…npm OIDC token (PHARN-07)

publish.yml ran `npm ci` (dev-dependency install scripts) and `npm publish`
(prepublishOnly: prettier, eslint, markdownlint, tsc, vitest; prepack: esbuild)
in ONE job holding `id-token: write`, so any of ~280 dev dependencies could
request the OIDC token and publish a malicious version with valid provenance.

Now two jobs:
- build (no id-token): strict `^vX.Y.Z$` tag == package.json version on a
  commit contained in main, checked BEFORE npm ci; then check + test:coverage,
  npm pack, smoke-install of the tarball, upload as an artifact.
- publish (the only job with id-token + npm-publish env): Assert npm floor,
  download the tarball, `npm publish pkg/pharn-dev-pharn-<version>.tgz
  --provenance --access public --ignore-scripts`. No checkout, no install.

check-run-pins.test.mjs pins that the grant appears once, inside `publish`,
which installs nothing; the live install count goes 9 -> 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvcuVhk8hTeDskp5pAJhnc
@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 3a8012ea-8954-4ff9-9e9f-f57889cf2f8f


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PrzemekGalarowicz
PrzemekGalarowicz merged commit a2fe83e into main Sep 24, 2026
14 checks passed
PrzemekGalarowicz added a commit that referenced this pull request Sep 24, 2026
… workflow (#213)

publish.yml's build job ran `npm pack --pack-destination "$RUNNER_TEMP/pkg"`
without creating `pkg/`, and npm does not create the destination (ENOENT on
npm 10.9.7 and 11.20.0). Every Release run would fail at Pack and never reach
the publish job. The defect came in with the build/publish split (#201) and
was invisible to PR CI, because publish.yml only runs on a published Release.

The Pack step now runs `mkdir -p` first. A live test in
check-run-pins.test.mjs pins that every workflow's pack destination is either
the runner temp root or created earlier in the same job, with a positive
control against the live publish.yml.


Claude-Session: https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants