ci: harden workflows, add npm provenance publishing and supply-chain checks - #31
Conversation
…checks Hardens the CI pipeline and adds automated release publishing with provenance attestation. ci.yml: - Add an explicit 'permissions: contents: read' block. The workflow previously inherited the repository default token scope. - Pin third-party actions by commit SHA with a version comment, so a retargeted or compromised tag cannot change what executes. - Set persist-credentials: false, so the checkout token is not left in the git config for later steps to pick up. - Scope the push trigger to main. It was previously unfiltered and ran the full three-node matrix on every branch push, duplicating the pull_request run. - Add concurrency cancel-in-progress and fail-fast: false. - Add an audit job running 'npm audit --audit-level=high'. Nothing watched dependencies between manual bumps before this. Currently reports 0 vulnerabilities. release.yml (new): - Publishes on GitHub Release, with a manual dispatch that defaults to dry-run. - Publishes with --provenance under 'id-token: write', linking each tarball to the commit and workflow run that produced it. Consumers can verify with 'npm audit signatures'. - Re-runs lint, typecheck, test and build before publishing rather than trusting the commit's earlier CI result. - Guards against a tag that disagrees with package.json, and against republishing an existing version. Both guards were verified locally. scorecard.yml (new): - Weekly OpenSSF Scorecard analysis uploaded to code scanning, with published results backing the README badge. dependabot.yml (new): - Weekly npm and github-actions updates. Dev minor/patch bumps are grouped into one PR; majors stay separate for individual review. README: add CI and Scorecard badges, and a Supply Chain section documenting provenance verification.
PR Summary by QodoCI hardening: pinned actions, npm provenance releases, Scorecard checks
AI Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1.
|
Actions expressions parse `inputs.dry-run` as `inputs.dry - run`, so the hyphenated input name could never be referenced. The publish guard would mis-evaluate (or fail to parse), risking a publish on a run the operator intended as a dry run. Rename the input to `dry_run` and make the publish condition explicit about the workflow_dispatch case.
npm revoked classic automation tokens in December 2025, and granular tokens now cap at a 90-day lifetime. Switch to OIDC trusted publishing instead: the npm CLI detects the Actions OIDC environment and exchanges it for short-lived publish credentials, so there is no NPM_TOKEN secret and nothing to rotate. Drops the NODE_AUTH_TOKEN env block and --access public (already covered by publishConfig.access). Keeps --provenance so the attestation stays explicit.
Why
PR 2 of 2 following the Snyk package health report. Where #30 covered the community files Snyk scores, this one covers the things Snyk doesn't score yet but that actually matter for a package doing ~25k downloads/week.
Independent of #30 — both branch from
main.ci.ymlhardeningThe existing workflow was functional but permissive:
permissions:block → it inherited the repository default token scope. Now explicitlycontents: read.actions/checkout@v4) → a retargeted or compromised tag silently changes what executes in CI. Now pinned by commit SHA with a version comment. Dependabot keeps them current (see below).persist-credentials: falseadded, so the checkout token isn't left in the git config for subsequent steps.on: pushwas unfiltered → every push to every branch ran the full three-node matrix, duplicating thepull_requestrun. Now scoped tomain, withconcurrency+cancel-in-progress.fail-fast: falseso one Node version failing doesn't hide the others.release.yml(new) — the main eventPublishing is manual today via
prepublishOnly. This replaces it with a workflow triggered by a published GitHub Release (plus a manual dispatch that defaults to dry-run).The important part is
npm publish --provenanceunderid-token: write. Every published tarball gets a signed attestation binding it to the exact commit and workflow run that built it, verifiable by anyone withnpm audit signatures, and surfaced publicly on the npm page. For a package with this download count, it's the single highest-value supply-chain change available.It also re-runs the full lint/typecheck/test/build gate immediately before publishing rather than trusting that CI passed on the commit — an npm publish is immutable, so it's worth the extra two minutes.
Two guards, both verified locally against the real registry:
package.jsonv2.0.2against2.0.1→ blocks.v2.0.1against2.0.1→ proceeds.2.0.1→ detected, blocks.2.0.2→ not found, proceeds.scorecard.yml+dependabot.yml(new)npm audit --audit-level=highas a CI job. Nothing watched dependencies between your manual bumps before this. Currently clean: 0 vulnerabilities.Before this can publish — required setup
The release workflow will fail without these:
NPM_TOKENsecret — an npm automation token (granular or classic automation; a regular publish token won't work with 2FA). Settings → Secrets and variables → Actions.npmdeployment environment; GitHub creates it on first use. Add a required reviewer to it if you want a manual approval gate before publish.Suggested first use: Actions → Release → Run workflow with dry-run left checked. It runs everything including
npm pack --dry-runand stops short of publishing.Testing
All four YAML files validated with
js-yaml; Prettier clean. Release guard logic exercised locally as tabulated above. Note the shell steps use the runner-provided$GITHUB_REF_NAMErather than${{ }}interpolation, so there's no workflow-injection surface.