Skip to content

build(reproducibility): pin deterministic Node and npm toolchain - #78

Closed
seonghobae wants to merge 55 commits into
mainfrom
build/deterministic-npm-toolchain
Closed

build(reproducibility): pin deterministic Node and npm toolchain#78
seonghobae wants to merge 55 commits into
mainfrom
build/deterministic-npm-toolchain

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • pins the reviewed development/lockfile toolchain to Node.js 24.19.0 and distribution-bundled npm 11.17.0 using packageManager plus fail-closed devEngines;
  • pins CI to Node-24-native actions/checkout 6.0.2 and actions/setup-node 6.4.0 by immutable full commit SHA, verifies exact checkout and Node/npm identities before install, and avoids deprecated Node 20 action runtimes;
  • enforces strict-allow-scripts=true with exact reviewed install-script identities: esbuild@0.28.1 and workerd@1.20260625.1 are explicitly allowed, while fsevents@2.3.3 is explicitly denied;
  • now executes a hermetic local-file dependency fixture proving a newly introduced unreviewed postinstall cannot run under the pinned npm 11.17 strict policy, rather than relying only on configuration-string assertions;
  • binds lockfile validation to the exact pull-request base SHA and schema-v2 evidence for every changed packages key plus canonical SHA-256 digests of the exact before/after package objects;
  • independently resolves the live base branch before lockfile validation and again after successful release verification, refusing stale event-time base evidence;
  • rejects undeclared metadata churn, malformed/oversized/unreadable evidence, symlink/race-prone reads, top-level lock metadata drift, tampered resolved/integrity, and duplicate decoded JSON object keys;
  • maintains configured 100% statement/branch/function/line coverage for the owned production/security-control surface.

Why

Issue #77 exposed that an unbound npm toolchain could regenerate a targeted nanoid security update together with unrelated lock metadata churn. Issue #79 separately requires install-time code execution to be explicitly reviewed and fail closed rather than inferred from a green dependency audit.

The repository therefore treats package-manager identity, lockfile change authority, install-script authority, vulnerability audit, model/reviewer judgement, and merge authority as distinct controls.

Install-script execution proof

The existing static contract already required project strict-allow-scripts=true plus exact pinned allowScripts entries. The latest change adds a real offline npm fixture to test/package-manager-reproducibility.test.ts:

  1. a temporary local dependency contains a postinstall that would write a marker file if executed;
  2. a lockfile is generated with scripts disabled and no network requirement;
  3. the pinned npm 11.17.0 executes npm ci with project strict-allow-scripts=true and no matching allowScripts entry;
  4. the install must terminate non-zero with allow-script diagnostics; and
  5. the marker file must remain absent.

This is direct execution evidence for issue #79's fail-closed requirement. It does not approve any additional dependency script or weaken the project policy.

Primary npm documentation for the pinned v11 line defines strict-allow-scripts=true as converting an unreviewed dependency install script into a hard install error, while an explicit false entry remains non-executable. The repository retains the exact Node/npm identity instead of silently tracking a moving npm release.

Current exact source identity

  • Base branch: main.
  • Independently revalidated live base tip during this update: 9adb784902cae19a150146a8fadd6a0491f67814.
  • Head branch: build/deterministic-npm-toolchain.
  • Exact current head: 23391f31410ad9d258b52c21c8b58d7ae2c6e82b.
  • Draft by dependency/governance design while fix(security): update transitive nanoid for CVE-2026-67213 #76 is unintegrated and live protected-main governance remains unresolved.

Current exact-head evidence

For exact head 23391f31410ad9d258b52c21c8b58d7ae2c6e82b:

  • ci run 31341608914: exact-head checkout and SHA assertion passed; Node 24.19.0 / npm 11.17.0 identity passed; independently resolved live main equality passed; lockfile change control passed with zero changed package nodes; strict project install succeeded for the reviewed dependency set; typecheck passed; 68 test files / 684 tests passed; the new real unreviewed-script refusal regression passed; configured production/security-control statements, branches, functions and lines are all 100%. The run then failed only at the unchanged repository-wide npm audit --audit-level=high boundary because protected main still resolves nanoid <3.3.17 / GHSA-2v37-7h3g-55p8. That root remediation remains isolated in fix(security): update transitive nanoid for CVE-2026-67213 #76; no audit waiver or duplicate lockfile patch is introduced here.
  • reviewer-ci run 31341608910: terminal success.
  • central Security Scan run 31341608937: terminal success.
  • Formal GitHub reviews: none at the latest review read.
  • Inline review threads: none at the latest thread read.

Queued, pending, cancelled, skipped-required, status-only, stale-head, predecessor-head, model-only, synthetic-only, or failed evidence is not treated as success or approval.

Documentation and evidence boundary

docs/doctoring/package-manager-reproducibility.md records the immutable Node/npm identity, exact allow/deny lifecycle-script policy, lockfile evidence semantics, and stale-base invariant. CHANGELOG.md records the package-manager, install-script, and lockfile-provenance control under ## Unreleased.

This PR does not modify the vulnerable nanoid lockfile node, weaken npm audit, create a VEX/ignore exception, add self-modifying/repair workflows, add branch-patching contents:write, alter OpenCode/NVIDIA NIM reviewer credentials, authorize merge, or authorize release/deployment.

Merge boundary

Keep this PR Draft until #76 integrates under actual live governance. Then refresh/revalidate against the protected lineage and require fresh exact-head/current-base CI, Security Scan, reviewer evidence, all applicable provenance gates, zero valid unresolved findings, and the qualifying formal review/ruleset requirements actually enforced by Noema/CWL governance. No predecessor-head evidence transfers.

Related: #77, #79, #75, #76, #27, #29

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c3e0c1f8-985a-4c6e-ac13-7fcec0238e10

📥 Commits

Reviewing files that changed from the base of the PR and between 9adb784 and 23391f3.

📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • .npmrc
  • CHANGELOG.md
  • docs/doctoring/package-manager-reproducibility.md
  • package.json
  • scripts/lockfile-change-control.mjs
  • test/lockfile-change-control-boundaries.test.ts
  • test/lockfile-change-control-canonical-array.test.ts
  • test/lockfile-change-control-duplicate-json.test.ts
  • test/lockfile-change-control-policy-schema.test.ts
  • test/lockfile-change-control-race-boundaries.test.ts
  • test/lockfile-change-control.test.ts
  • test/package-manager-reproducibility.test.ts
  • test/workflow-readiness.test.ts
  • vitest.config.ts

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.

@seonghobae
seonghobae changed the base branch from main to fix/nanoid-cve-2026-67213 August 8, 2026 10:31
@seonghobae
seonghobae changed the base branch from fix/nanoid-cve-2026-67213 to main August 8, 2026 10:32
@seonghobae
seonghobae changed the base branch from main to fix/nanoid-cve-2026-67213 August 9, 2026 14:20
@seonghobae
seonghobae changed the base branch from fix/nanoid-cve-2026-67213 to main August 9, 2026 14:23
@seonghobae
seonghobae changed the base branch from main to fix/nanoid-cve-2026-67213 August 9, 2026 18:02
@seonghobae
seonghobae changed the base branch from fix/nanoid-cve-2026-67213 to main August 9, 2026 18:05

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review the unchanged exact head 23391f31410ad9d258b52c21c8b58d7ae2c6e82b. Focus on immutable Node/npm identity, strict allowScripts semantics and the real unreviewed-script refusal fixture, exact live-base/lockfile evidence binding, duplicate-key/race defenses, and whether any package-manager or lockfile path can false-pass while using stale or unreviewed execution authority. This review is diagnostic evidence only and does not substitute for qualifying independent merge approval.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Exact Git identity =='
git rev-parse HEAD
git status --short
git branch --show-current
printf '%s\n' '== Commit metadata =='
git show -s --format='%H%n%P%n%s' HEAD

printf '%s\n' '== Changed paths against main merge base =='
base="$(git merge-base HEAD main)"
printf 'merge-base=%s\n' "$base"
git diff --name-status "$base"...HEAD

printf '%s\n' '== Candidate files =='
fd -t f . | rg '(^|/)(package\.json|package-lock\.json|package-manager-reproducibility\.test\.ts|package-manager-reproducibility\.md|CHANGELOG\.md)$|(^|/)\.github/workflows/.*\.(yml|yaml)$'

printf '%s\n' '== Workflow action and package-manager references =='
rg -n -C 3 -e 'actions/(checkout|setup-node)@' -e 'node-version' -e 'npm (ci|install|audit)' -e 'packageManager|devEngines|strict-allow-scripts|allowScripts' \
  package.json .npmrc .github 2>/dev/null || true

printf '%s\n' '== Source outlines =='
test_file="$(fd -t f '^package-manager-reproducibility\.test\.ts$' | head -n 1 || true)"
if [ -n "$test_file" ]; then
  ast-grep outline "$test_file" --items all --view expanded
fi

Length of output: 22025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== package and CI policy =='
cat -n package.json
cat -n .npmrc
sed -n '1,180p' .github/workflows/ci.yml | cat -n

printf '%s\n' '== Lockfile-control implementation outline =='
ast-grep outline scripts/lockfile-change-control.mjs --items all --view expanded
printf '%s\n' '== Lockfile-control implementation =='
cat -n scripts/lockfile-change-control.mjs

printf '%s\n' '== Package-manager reproducibility fixture =='
cat -n test/package-manager-reproducibility.test.ts

printf '%s\n' '== Lockfile control test outlines =='
for f in test/lockfile-change-control*.test.ts test/workflow-readiness.test.ts; do
  printf '\n-- %s --\n' "$f"
  ast-grep outline "$f" --items all --view expanded
done

printf '%s\n' '== Lockfile control test assertion terms =='
rg -n -C 4 \
  -e 'duplicate|race|symlink|base|sha256|canonical|allowScripts|strict-allow-scripts|resolved|integrity|schema' \
  test/lockfile-change-control*.test.ts test/workflow-readiness.test.ts

Length of output: 92183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== package-lock diff status =='
base="$(git merge-base HEAD main)"
git diff --stat "$base"...HEAD -- package-lock.json
git diff --exit-code "$base"...HEAD -- package-lock.json || true

printf '%s\n' '== Every workflow npm install invocation with surrounding setup =='
rg -n -U -C 16 \
  '(uses: actions/setup-node@|node-version:|run: npm (ci|install)|npm ci|npm install|strict-allow-scripts|allowScripts)' \
  .github/workflows --glob '*.{yml,yaml}'

printf '%s\n' '== Workflow reproducibility coverage =='
cat -n test/workflow-readiness.test.ts
rg -n -C 4 \
  -e 'package-manager-reproducibility|actions/setup-node|actions/checkout|npm ci|strict-allow-scripts|lockfile' \
  test --glob '*.test.ts'

printf '%s\n' '== Race-boundary tests =='
cat -n test/lockfile-change-control-race-boundaries.test.ts

printf '%s\n' '== Changed workflow diff only =='
git diff --find-renames --find-copies "$base"...HEAD -- .github/workflows/ci.yml

Length of output: 94122


@seonghobae I reviewed exact head 23391f31410ad9d258b52c21c8b58d7ae2c6e82b.

I confirmed the local merge base is 9adb784902cae19a150146a8fadd6a0491f67814. package-lock.json is unchanged against that base.

The changed central CI path has the requested static controls:

  • It checks out and verifies the exact PR head.
  • It uses SHA-pinned actions/checkout@v6.0.2 and actions/setup-node@v6.4.0.
  • It verifies Node v24.19.0 and npm 11.17.0 before npm ci.
  • It resolves the live base before lockfile validation and after release verification.
  • The lockfile control rejects duplicate decoded JSON keys, non-regular or changed inputs, oversized or invalid UTF-8 inputs, undeclared package changes, top-level metadata changes, and non-exact digest evidence.
  • The lifecycle-script fixture uses an offline local dependency. It requires npm ci to fail with strict allow-script diagnostics and verifies that postinstall did not create its marker.

I found one execution-authority gap outside .github/workflows/ci.yml.

.github/workflows/hourly-commercial-readiness.yml uses node-version: "24" on Line 55 and runs root npm ci on Line 59. It does not verify the npm version before install. This workflow later uses a maintainer App token and can apply merges. A moving Node 24 distribution can provide an npm version with different devEngines or strict-allow-scripts behavior. packageManager does not force a preinstalled npm binary to be 11.17.0.

The same unpinned install pattern exists in release, deployment, and scheduled scan workflows. This means the repository-wide package-manager and lifecycle-script authority claim can false-pass outside the changed central CI path.

Update each root dependency-install workflow to use the immutable Node setup action, Node 24.19.0, and an explicit pre-install check for npm 11.17.0. Apply the lockfile-control policy where PR lockfile validation is applicable. Add a static regression test that enumerates these workflows so this coverage cannot regress.

This is diagnostic review evidence only. It is not merge approval.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

Superseded by dependency-ordered replacement #89. Fresh verification on #89 exact head adcc42d2b0fec3106faabb59bbf7e79124714f7a now passes both ci (run 31350201695) and reviewer-ci (run 31350201710) after a test-first RED→GREEN completion of the missing ## Unreleased handoff. The replacement starts from #76 exact head e0106ce16b7b8b493f46bf075ec5baf58762bd95, preserves nanoid@3.3.17, and retains the package-manager/install-script controls. Representative unique artifacts are byte-identical across #78/#89 (package.json blob 1959a094..., .npmrc 4a6e0c1..., scripts/lockfile-change-control.mjs 12eab3a..., and test/package-manager-reproducibility.test.ts a3b4c4e...); #89 additionally has predecessor-integration and CHANGELOG contract tests. #78 checks/reviews do not transfer to #89. Closing only the superseded branch; #89 remains Draft until #76 integrates, the protected-base Security Scan becomes eligible, and live governance/formal-review gates are satisfied.

@seonghobae seonghobae closed this Aug 10, 2026
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.

1 participant