Skip to content

Commit 9a140e6

Browse files
authored
docs(decisions): Add ADR 0083-0085 pinning the npm CLI toolchain across build and publish stages (#100)
## Summary - What changed: three architecture decision records completing the npm CLI version-pinning decision chain for the JS/TS npm package profile: - **ADR 0083** — defer the npm M1 publish remediation (#97) to the upstream npm/cli#9882 fix; the first reviewed npm release containing the fix becomes the initial publish npm pin, with an explicit revisit trigger (unmerged close, materially different precedence semantics, or no merge by 12026-10-01). - **ADR 0084** — select the publish-stage npm provisioning mechanism: download the exact recorded registry tarball URL, verify the committed SHA-512 digest (SRI form, byte-identical to the registry-native `dist.integrity`) over the compressed bytes before extraction, and execute via a basename-`npm` launcher. Partially supersedes ADR 0082's SHA-256 designation; amends its open mechanism choice. - **ADR 0085** — pin the build-stage toolchain pair: the reusable workflow resolves Node.js to an exact 24 patch recorded together with the npm version that patch bundles, and each job that uses the bundled npm asserts `node --version`/`npm --version` equality with the recorded pair before the first npm invocation. Partially supersedes ADR 0027's floating toolchain resolution clause only. - Why is this needed: the fourth M1 dogfood (#97) showed a floating npm on the publish path can publish-then-reject and permanently burn version numbers. ADR 0082 pinned the publish npm and deliberately left two questions open — what the #97 fix is (settled by 0083), which mechanism provisions the pin (settled by 0084) — plus the build-stage version selection review (settled by 0085 with an escalation trigger toward an independently provisioned build npm). - How to test: documentation-only change. Validation evidence below. ## Related Issues - Related #97 — this PR records the remediation *decisions*; implementation lands when the fixed npm release ships (ADR 0083 deferral scope), so #97 stays open. - Related #30 — dogfood attempt evidence referenced by the ADRs. ## Change Type - [ ] Bug fix - [ ] Feature - [ ] Refactor - [x] Documentation - [ ] Test/CI - [ ] Breaking change - [ ] Other: <!-- describe --> ## Changelog - Category: None - User-facing note: Architecture decision records only — no user-facing behavior changes. The implementing specification, workflow, and Go changes land later under ADR 0083's deferral scope and ADR 0085's own schedule. Changelog update: - [ ] `CHANGELOG.md` `[Unreleased]` updated - [x] Not needed because this change is not user-facing ## Checklist ### General - [x] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): Summary` - [x] This PR does not expose backend/internal implementation details in a public repo. - [x] No secrets, tokens, keys, or private endpoints are included. - [x] Changes stay within this repository's intended scope. ### Protocol / Compatibility Impact - [x] No protocol/spec impact - [ ] Protocol/spec updated - [ ] Conformance tests updated - [ ] Breaking change is versioned and migration notes are included ADR texts only; no specification files are modified. Each ADR records which future specification amendments it governs. ## Testing - [ ] Unit tests added/updated - [ ] Integration or conformance tests added/updated - [ ] Tests pass - [x] Lint and format pass - [ ] Type check passes - [x] Manual verification performed Describe test evidence: - `prettier --write` on all touched files: clean. - `markdownlint-cli2 docs/decisions/**/*.md`: 89 files, 0 errors. - `python3 .agents/skills/adr-relations-check/scripts/check_relations.py`: **86 ADRs, 216 edges — NO PROBLEMS** (forward + reverse relation edges symmetric, status grammar valid). - Lefthook pre-commit (prettier, markdownlint) and commit-msg (dco-signoff) passed on all three commits. ## Documentation - [x] README updated — `docs/decisions/README.md` and `README.ko.md` sequence, inventory, and traceability tables (bilingual, same change). - [x] Spec/docs updated — three new ADRs; reverse relation edges added to ADR 0016/0017/0027/0067/0082/0083/0084; `docs/decisions/AGENTS.md` index updated. - [x] Changelog decision completed above ## Rollout / Risk - Risk level: Low — documentation-only; no runtime, workflow, or specification behavior changes. - Rollback plan: revert the merge commit; ADR 0065's immutability rule applies after acceptance, so any post-merge decision change would be a new ADR instead. ## Reviewer Checklist - [x] Scope is clear and minimal - [x] Security and boundary checks passed - [x] Tests and docs are sufficient - [x] Compatibility impact is correctly handled --------- Signed-off-by: Yunseo Kim <git@yunseo.kim>
1 parent b98dfaf commit 9a140e6

15 files changed

Lines changed: 874 additions & 47 deletions

docs/decisions/0016-use-corepack-for-pnpm-and-yarn-build-stages.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ decision-makers: Yunseo Kim
77
relations:
88
- type: see-also
99
target: ADR-0082
10+
- type: see-also
11+
target: ADR-0085
1012
---
1113

1214
# Use Corepack for pnpm and Yarn Build Stages

docs/decisions/0017-require-explicit-package-manager-version-enforcement.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ relations:
99
target: ADR-0070
1010
- type: see-also
1111
target: ADR-0082
12+
- type: see-also
13+
target: ADR-0085
1214
---
1315

1416
# Require Explicit Package Manager Version Enforcement

docs/decisions/0024-use-oidc-trusted-publishing-without-publish-secrets.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ relations:
1414
before registry mutation by an early OIDC token exchange preflight; residual publish-time
1515
authorization failures are classified, and the absolute must-stop-before-registry-mutation
1616
wording is scoped to what the exchange and observation preflights can establish"
17+
- type: see-also
18+
target: ADR-0083
1719
---
1820

1921
# Use OIDC Trusted Publishing Without Publish Secrets

docs/decisions/0027-use-github-hosted-ubuntu-2404-and-node-24-runtime.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ relations:
1212
by the selected Node.js 24 toolchain, floating with the node-24 image); the Node.js 24 runtime
1313
selection, the runner and OS image constraints, the caller-input prohibitions, the build-stage
1414
npm clauses, and the floor check and actual-version recording clauses remain in force"
15+
- type: partially-superseded-by
16+
target: ADR-0085
17+
scope:
18+
"the toolchain resolution clause under which the exact Node.js patch version, and therefore
19+
the bundled npm CLI, floats within the Node.js 24 major line between runs of the same builder
20+
release; the Node.js 24 major selection, the runner and OS image constraints, the caller-input
21+
prohibitions, the build-stage bundled-npm coupling, the npm floor check, and the
22+
actual-version recording clauses remain in force"
23+
- type: see-also
24+
target: ADR-0084
1525
---
1626

1727
# Use GitHub-Hosted Ubuntu 24.04 and Node.js 24 Runtime

docs/decisions/0029-use-windlass-generated-slsa-provenance-for-npm-publish.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ relations:
2222
target: ADR-0071
2323
- type: see-also
2424
target: ADR-0082
25+
- type: see-also
26+
target: ADR-0083
2527
---
2628

2729
# Use Windlass-Generated SLSA Provenance for npm Publish

docs/decisions/0067-converge-repeated-runs-within-run-identity.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ relations:
3434
target: ADR-0075
3535
- type: see-also
3636
target: ADR-0076
37+
- type: see-also
38+
target: ADR-0085
3739
---
3840

3941
# Converge Repeated Runs Within Run Identity

docs/decisions/0076-use-observation-preflights-and-first-mutation-classification.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ relations:
2828
"the npm OIDC exchange success-response contract (unpinned in ADR-0076; pinned in ADR-0081 to
2929
the empirically observed shape) and the exchange-token lifetime assumption (the 'typically 1
3030
hour' documentation note cited in ADR-0076; the observed lifetime is 15 minutes)"
31+
- type: see-also
32+
target: ADR-0083
3133
---
3234

3335
# Use Observation Preflights and First-Mutation Classification

docs/decisions/0081-pin-npm-oidc-exchange-response-contract-and-correct-token-lifetime-assumption.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ relations:
1313
hour' documentation note cited in ADR-0076; the observed lifetime is 15 minutes)"
1414
- type: see-also
1515
target: ADR-0082
16+
- type: see-also
17+
target: ADR-0083
1618
---
1719

1820
# Pin the npm OIDC Exchange Response Contract to the Observed Shape and Correct the Token Lifetime Assumption

docs/decisions/0082-pin-publish-stage-npm-cli-version-with-integrity-verified-provisioning-and-reviewed-allowlist.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ relations:
2020
target: ADR-0029
2121
- type: see-also
2222
target: ADR-0081
23+
- type: amended-by
24+
target: ADR-0083
25+
scope:
26+
"the identification of the initial pinned publish-stage npm version: ADR-0082 records the
27+
allowlist as initially a single version pinned when the #97 fix lands; ADR-0083 defines the
28+
#97 remediation as adopting the upstream npm/cli#9882 fix and selects the initial pin as the
29+
first reviewed npm release containing that fix. ADR-0082's provisioning,
30+
integrity-verification, allowlist, and bump-procedure clauses are unchanged and remain in
31+
force"
32+
- type: partially-superseded-by
33+
target: ADR-0084
34+
scope:
35+
"the designation of SHA-256 as the publish-stage npm distribution digest algorithm in the
36+
provisioning clause ('one shared definition of version plus distribution SHA-256'): the pinned
37+
digest is SHA-512 in SRI form, byte-identical to the registry-native dist.integrity field. The
38+
pre-use digest-verification obligation, the single shared definition, the allowlist, and the
39+
bump procedure are unaffected and remain in force"
40+
- type: amended-by
41+
target: ADR-0084
42+
scope:
43+
"the selection of the concrete publish-stage npm provisioning mechanism, which ADR-0082 left
44+
unspecified: ADR-0082 mandates explicit provisioning before any npm invocation with the
45+
distribution digest verified before first use but does not name the mechanism; ADR-0084
46+
narrows the open choice to the digest-verified npm registry tarball mechanism"
47+
- type: see-also
48+
target: ADR-0085
2349
---
2450

2551
# Pin the Publish-Stage npm CLI Version with Integrity-Verified Provisioning and a Reviewed Allowlist
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
---
2+
parent: Decisions
3+
nav_order: 83
4+
status: accepted
5+
date: 12026-08-17
6+
decision-makers: Yunseo Kim
7+
relations:
8+
- type: amends
9+
target: ADR-0082
10+
scope:
11+
"the identification of the initial pinned publish-stage npm version: ADR-0082 records the
12+
allowlist as initially a single version pinned when the #97 fix lands; ADR-0083 defines the
13+
#97 remediation as adopting the upstream npm/cli#9882 fix and selects the initial pin as the
14+
first reviewed npm release containing that fix. ADR-0082's provisioning,
15+
integrity-verification, allowlist, and bump-procedure clauses are unchanged and remain in
16+
force"
17+
- type: see-also
18+
target: ADR-0024
19+
- type: see-also
20+
target: ADR-0029
21+
- type: see-also
22+
target: ADR-0076
23+
- type: see-also
24+
target: ADR-0081
25+
- type: see-also
26+
target: ADR-0084
27+
- type: see-also
28+
target: ADR-0085
29+
---
30+
31+
# Defer the npm M1 Publish Remediation to the Upstream provenance-file Fix and Adopt the Fixed Release as the Initial Publish npm Pin
32+
33+
## Context and Problem Statement
34+
35+
The fourth M1 dogfood
36+
([vers-js run 31840088262](https://github.com/windlasstech/vers-js/actions/runs/31840088262),
37+
tracked as [issue #97](https://github.com/windlasstech/slsa-builder/issues/97)) pinned the npm
38+
defect that blocks the JS/TS npm profile's publish path: npm trusted publishing auto-enables
39+
provenance and silently discards a caller-supplied `--provenance-file` bundle, publishing an
40+
npm-generated statement instead of the Windlass-signed Statement. The run published
41+
`@windlass/vers-js@0.1.2` with the foreign evidence; the fail-closed read-back rejected it exactly
42+
as designed, but only after the registry mutation had committed, and npm policy makes the burned
43+
version number permanently unreusable. The RCA identified three interacting layers: `oidc.js`
44+
auto-enables provenance whenever the `provenance` config is at its default, the inner
45+
`provenance === true` branch in libnpmpublish's `buildMetadata()` then never reaches the
46+
`verifyProvenance()` path that reads the supplied file, and every documented way to disable
47+
automatic provenance is unusable in combination with `--provenance-file` (config-layer mutual
48+
exclusivity, the env-layer carve-out, and publishConfig flatten timing).
49+
50+
Two candidate remediations were on the table:
51+
52+
1. A local workaround: suppress npm's CI/OIDC detection for the publish subprocess, inject the
53+
self-exchanged trusted-publishing token into an isolated npmrc as the registry credential, and
54+
enforce a closed `publishConfig` allowlist. This mechanism is source-dependent on npm internals —
55+
exactly the verification surface ADR 0082 exists to close.
56+
2. The upstream fix. The project filed [npm/cli#9879](https://github.com/npm/cli/issues/9879)
57+
(report) and [npm/cli#9882](https://github.com/npm/cli/pull/9882) (fix PR, OPEN at decision
58+
time).
59+
60+
ADR 0082 pinned the publish-stage npm provisioning mechanics and recorded the allowlist as
61+
"initially a single version, pinned when the #97 fix lands and re-verified there" — deliberately
62+
leaving open what the #97 fix is, and therefore which npm version opens the allowlist.
63+
64+
A diff review of npm/cli#9882 establishes what the fixed npm does on the profile's exact path:
65+
66+
- `lib/utils/oidc.js` skips auto-enabling provenance when `opts.provenanceFile` is configured; the
67+
option already carries the value from every config source (CLI, env, npmrc, publishConfig) by the
68+
time the OIDC flow runs, so all entry paths are covered. The OIDC exchange itself still runs —
69+
trusted publishing continues to authenticate the publish.
70+
- libnpmpublish's `buildMetadata()` throws `EPROVENANCECONFLICT` when both `provenance: true` and
71+
`provenanceFile` are set programmatically. The profile's pinned argv never sets `provenance=true`,
72+
so this branch is unreachable on the profile's path.
73+
- The documentation updates state the precedence rule, and the PR's new CLI regression test asserts
74+
the profile's exact scenario: OIDC trusted publishing with a `provenance-file` publishes the
75+
packument with a sigstore attachment deep-equal to the supplied bundle, with provenance generation
76+
never invoked.
77+
78+
The residual review surface (the error-code naming for the conflict branch, docs wording) does not
79+
intersect the profile's path. The only outcome that would break the profile's path is a wholesale
80+
rejection of the precedence direction, which would contradict npm's own published documentation of
81+
`--provenance-file`.
82+
83+
Two further facts frame the choice. First, ADR 0029 already decided that npm trusted publishing
84+
authenticates the registry mutation while the Windlass-signed bundle is the canonical provenance
85+
(building on ADR 0024's trusted-publishing authentication decision). The workaround would replace
86+
npm's own exchange with a self-performed exchange plus an injected credential — a defect-forced
87+
deviation from that accepted design, whose reason to exist disappears when the fix ships. Second,
88+
the wait is expected to cost little or no schedule time: npm 12.0.1 shipped two days after 12.0.0
89+
and 12.0.2 seventeen days later, while the workaround path requires a specification amendment, a TDD
90+
implementation across the publish boundary, and a dogfood run before it could land.
91+
92+
## Decision Drivers
93+
94+
- A mechanism that is never written is strictly cheaper than one that is written, verified, and
95+
later retired; ADR 0082 already earmarks workaround retirement on fixed npm versions as a separate
96+
later decision.
97+
- The workaround's dependence on npm internals (`oidc()` ordering, config exclusivity, publishConfig
98+
flatten timing) is precisely the verification surface ADR 0082 closes; adopting the workaround
99+
even temporarily would expand the surface that must be reviewed per bump.
100+
- The fix's semantics for the profile's argv are diff-verified today; the uncertain residue does not
101+
intersect the profile's path.
102+
- Waiting restores the ADR 0024/ADR 0029 authentication design instead of entrenching a deviation
103+
from it.
104+
- The ADR 0076 exchange preflight retains its value — early, mutation-free detection of
105+
trusted-publishing misconfiguration — regardless of which party consumes the minted token, so ADR
106+
0081's pinned exchange contract stays in force untouched.
107+
- Dependence on an external release schedule is the principal risk of waiting and must be bounded by
108+
an explicit revisit trigger rather than left open-ended.
109+
110+
## Considered Options
111+
112+
1. Defer the #97 remediation to the first reviewed npm release containing npm/cli#9882; adopt that
113+
release as the initial publish npm pin; authenticate the publish with npm-native trusted
114+
publishing.
115+
2. Implement the #97 workaround now on a pinned npm 11.17.0; adopt the upstream fix later through
116+
the ADR 0082 bump procedure.
117+
3. Hybrid: implement the version-agnostic ADR 0082 machinery now (the workflow provisioning step,
118+
pin-data plumbing, and Go-side pin validation) and defer only the version selection and the
119+
authentication-path finalization.
120+
121+
## Decision Outcome
122+
123+
Chosen option: "Defer the #97 remediation to the first reviewed npm release containing
124+
npm/cli#9882", because the fix's semantics on the profile's path are already diff-verified, because
125+
waiting restores the accepted authentication design rather than entrenching a deviation, and because
126+
waiting is expected to add little or no schedule cost compared with specifying, implementing, and
127+
dogfooding the workaround first.
128+
129+
### Initial pin selection at adoption time
130+
131+
When an npm release containing the #9882 fix ships, the initial allowlist entry is selected under
132+
the ADR 0082 bump procedure, preferring:
133+
134+
1. an npm 11-line backport, if one is released — minimal internals delta from the dogfood-exercised
135+
11.17.0 and alignment with the Node 24 LTS line; otherwise
136+
2. the first 12.x release containing the fix, with bundle-verification compatibility evidence
137+
against its bundled sigstore-js major (v5) as an added acceptance criterion, because npm's
138+
`verifyProvenance()` path has never been exercised live against a Windlass-signed bundle.
139+
140+
The pin data (version, distribution URL, distribution SHA-256) is recorded in the JS/TS npm
141+
provenance and publish specification's allowlist per ADR 0082.
142+
143+
### Publish authentication path
144+
145+
With the fix in place, the publish job uses npm-native trusted publishing exactly as ADR 0024 and
146+
ADR 0029 intended: npm performs its own OIDC exchange inside `npm publish`, and the pinned argv is
147+
unchanged. No OIDC-environment suppression, no token injection into an isolated npmrc, and no
148+
`publishConfig` credential allowlist is specified or implemented. The ADR 0076 exchange preflight is
149+
retained as a non-mutating observation preflight for early detection of trusted-publishing
150+
misconfiguration; the minted token is discarded, not consumed. The fail-closed post-publish
151+
read-back remains the permanent safety net, unchanged.
152+
153+
### Deferral scope and revisit trigger
154+
155+
All #97 remediation implementation is deferred until the fixed release exists and is selected: the
156+
specification amendment (authentication path, publish-side pin validation replacing the
157+
build/publish npm-version equality check, the allowlist initial entry), the workflow provisioning
158+
step, the Go changes, and the compatibility fixtures.
159+
160+
This decision is revisited — with option 2 (the workaround on a pinned npm 11.17.0) as the default
161+
fallback — when any of the following occurs:
162+
163+
- npm/cli#9882 is closed unmerged;
164+
- npm/cli#9882 is merged with precedence semantics that differ materially for the profile's argv
165+
(for example, automatic provenance taking precedence over a supplied `--provenance-file`);
166+
- npm/cli#9882 has not merged by 12026-10-01.
167+
168+
### Relationship to existing decisions
169+
170+
This ADR amends ADR 0082's identification of the initial pinned version only; ADR 0082's
171+
provisioning, integrity-verification, allowlist, and bump-procedure clauses are unchanged and remain
172+
in force. ADR 0024 and ADR 0029 are not modified — this ADR removes a planned deviation from them.
173+
ADR 0076 and ADR 0081 remain fully in force: the exchange preflight keeps its observation role and
174+
its pinned response contract.
175+
176+
## Pros and Cons of the Options
177+
178+
### Defer to the fixed release and adopt it as the initial pin
179+
180+
- Good, because no workaround code, specification text, or fixtures are written up front for a
181+
mechanism that becomes unnecessary if the fix ships before the initial pin is selected.
182+
- Good, because publish authentication returns to the ADR 0024/ADR 0029 design: npm trusted
183+
publishing authenticates the registry mutation; the Windlass bundle is the canonical provenance.
184+
- Good, because the verification surface ADR 0082 must close stays minimal — one pinned npm whose
185+
behavior includes first-class support for the profile's exact publish mode, covered by npm's own
186+
regression test.
187+
- Good, because the decision is evidence-based today: the fix's behavior on the profile's path is
188+
pinned by the reviewed diff, not by a guess about maintainer intent.
189+
- Bad, because npm M1 completion becomes dependent on the npm maintainers' review and release
190+
schedule, which the project does not control; the revisit trigger bounds but does not eliminate
191+
this dependence.
192+
- Bad, because the fixed release's line is unknown: if the fix ships only in npm 12.x, the initial
193+
pin carries the sigstore-js v5 bundle-verification compatibility question and a larger internals
194+
diff-review than an 11.x pin would require.
195+
196+
### Implement the workaround now on pinned 11.17.0
197+
198+
- Good, because npm M1 unblocks immediately on the project's own schedule, and 11.17.0 is the
199+
dogfood-exercised version with live evidence.
200+
- Bad, because the workaround is specified, implemented, and dogfooded at a calendar cost comparable
201+
to the expected wait, and it becomes dead weight if the fix ships before the initial pin is
202+
selected.
203+
- Bad, because the workaround is source-dependent on npm internals across the allowed range and
204+
entrenches a deviation from ADR 0029's authentication design, with its retirement already known to
205+
require a separate later decision.
206+
207+
### Hybrid: build version-agnostic machinery now
208+
209+
- Good, because part of the wait becomes productive: the provisioning step and pin plumbing do not
210+
depend on the selected version.
211+
- Bad, because the specification's authentication path and the pin-validation contract cannot be
212+
finalized before the fixed release's behavior is confirmed, so a meaningful fraction of the work
213+
risks rework, and a partial-implementation state is harder to reason about than a clean deferral.
214+
215+
## More Information
216+
217+
- Motivating incident and RCA: [issue #97](https://github.com/windlasstech/slsa-builder/issues/97)
218+
and the fourth-attempt evidence in
219+
[issue #30](https://github.com/windlasstech/slsa-builder/issues/30).
220+
- Upstream: [npm/cli#9879](https://github.com/npm/cli/issues/9879) (report),
221+
[npm/cli#9882](https://github.com/npm/cli/pull/9882) (fix PR; OPEN and diff-verified as described
222+
above at decision time).
223+
- Issue #97's fix-requirement section described the workaround; it is superseded by this ADR and
224+
will be rewritten when the remediation implementation begins.
225+
- The burned `@windlass/vers-js@0.1.2` publication remains the foreign-conflict test target for the
226+
publish read-back, and the preserved dogfood-4 bundle remains the reference bundle the fixed npm's
227+
`verifyProvenance()` path must accept.
228+
- If this decision is revisited under the trigger above, the fallback selects npm 11.17.0 as the
229+
initial pin per the analysis recorded in ADR 0082's context.

0 commit comments

Comments
 (0)