Repository navigation
feat(npmprofile): support building a caller-specified source ref for dispatch retries #81
Description
Problem
js-ts-npm-package-slsa3.yml always builds the caller's github.ref: the build job performs a plain actions/checkout (no ref:) and the identity binding consumes REF/REVISION straight from the caller's event context. There is no input to override the built source revision.
This blocks the standard "retry a failed release with a fixed pipeline" scenario for workflow_dispatch callers:
- A release tag run fails because of a builder-side defect (real case: vers-js v0.1.2, run
31622651874, failed on the settings-only pnpm workspace selection bug fixed in fix(npmprofile): Support pnpm settings-only root packages #80). - The fix lands on the caller's
main(re-pin of the reusable workflow), but the release tag still contains the old caller workflow file and the old pin. - Dispatching from the tag uses the tag's workflow file (old pin, same failure). Dispatching from
mainuses the fixed pipeline but buildsmainHEAD instead of the tag content, and the provenance identity binds torefs/heads/mainrather than the signed release tag.
Why this matters
The release trust model (vers-js ADR-0051/0054) anchors on a maintainer-signed tag: the published artifact must provably derive from the signed revision. With no way to name the built ref, a dispatch retry either cannot run (caller-side guard fails closed) or must weaken the provenance claim to an unsigned, movable branch head. The only fully canonical workaround today is deleting and recreating the signed tag on the fixed commit, which requires temporarily relaxing tag-protection rulesets.
Prior art (expected semantics)
The previous hand-rolled vers-js pipeline expressed exactly the desired semantics and used them in production for the v0.1.1 publish: the workflow file came from the dispatch ref (including post-tag fixes, e.g. commit 81d2bba), but every job checked out ref: inputs.release_tag, so the built tarball came from the signed tag commit (cfd032d). Logic from the dispatch ref, content from the signed tag.
Requested behavior
Add an optional input to js-ts-npm-package-slsa3.yml (name TBD, e.g. source-ref):
- When set, the build job checks out that ref instead of defaulting to
github.sha. - Build metadata, the signed SLSA v1 predicate, and the publish convergence inputs record the built ref and its resolved commit SHA as the source revision (the value the provenance attests).
run_invocationcontinues to record the actual invocation context (dispatch ref, event name, run id) for audit, so "workflow file came from X, built content came from Y" stays explicit and verifiable.
Design considerations (non-binding)
- Constraint choice matters for the threat model: an unconstrained
source-reflets a dispatcher build any branch and call it a release. Options: restrict torefs/tags/*(matches the release-retry use case), or leave unconstrained and rely on honest provenance plus verification policy. The former composes better with caller-side guards. - Resolution should fail closed if the ref does not resolve to a commit (and, if restricted, if it is not a tag).
- Digest-verified handoffs should carry the resolved SHA so
provenance-sign/publishcannot disagree withbuildabout what was built.
Acceptance sketch
- A caller dispatching from
mainwithsource-ref: refs/tags/vX.Y.Zproduces a provenance Statement whose source revision is the tag's commit SHA, whilerun_invocation.refremainsrefs/heads/main. - Tag-push flows (input unset) behave exactly as today.
Context
- vers-js dogfood: F00 [F00] Dogfood resource readiness #7, P06 [P06] Live M1 dogfood #30; failed first run https://github.com/windlasstech/vers-js/actions/runs/31622651874; caller pin-bump PR ci: bump slsa-builder pin and wire source-ref for release retries vers-js#42.
- Related: fix(npmprofile): Support pnpm settings-only root packages #80 (the defect that triggered this retry need).
Activity
docs(decisions): add ADRs 0079/0080 for caller-specified build source…
docs(specs): Specify tags-only source-ref input and ADR 0080 verifica…
yunseo-kim commentedon Aug 13, 2026
Design resolution and specification revision complete
This issue's root cause was re-analyzed from scratch and resolved as a design decision, then specified. Summary of what landed:
Root cause (verified)
Release retry was structurally blocked by a three-layer conflation of the invocation ref (where the workflow was dispatched from) with the built source ref (what content is built and attested):
main) could never rebuild an existing tag.github.ref/github.shato byte-equal the release tag/commit..13/.14— always the invocation context) directly against the release tag, making a dispatch retry unverifiable even if built.Ecosystem research confirmed this is platform-inherent: GitHub OIDC claims always describe the invocation context, and the predecessor ecosystem (
slsa-github-generator) never shipped dispatch tag identity either — slsa-github-generator#1947 remained open through the end of maintenance. The same problem class was independently reported for osv-scanner in google/osv-scanner#632.Accepted ADRs (merged in #83)
source-refinput (npm is the first adopter). When supplied, the built source is that tag instead of the invocation ref; existence, resolution, and version-match checks are fail-closed. Tags-only was chosen over arbitrary refs to avoid resurrecting an "arbitrary release button" while structuring away retag cost. Partially supersedes ADR 0032's dispatch-ref restriction.externalParameters), while the platform still anchors builder identity, repository, invocation origin, and run. Whensource-refis unset, signed bundles are byte-identical to the single-identity contract. Partially supersedes ADR 0068 section 4 only.Specification revision (merged in #84)
core-profile-contract.md: new cross-profile built source identity invariant.js-ts-npm-package-profile.md: ninth public inputsource-ref; runtime guards, supported triggers, and PRE-mutation/mutation concurrency keys re-anchored on the built release tag; new pre-mutation diagnosticwindlass.verify.error.source-ref-invalid(registered in the diagnostic registry ahead of implementation, per themutation-queue-overflowprecedent);dispatch-retry-successandsingle-identity-defaultfixtures.js-ts-npm-provenance-publish.md: conditionalinput_ref/invocation_ref/invocation_revisionmembers in the closed source group; nine-input completeness mapping; signer-identity table rebound per ADR 0080.slsa-provenance-v1.md,identity-and-buildtypes.md,verification-policy-and-fixtures.md: two-identity model throughout — policy binds signed built-source fields, certificate.13/.14claims bind the signed invocation record;source-digest-mismatch/source-ref-mismatchdiagnostics redefined to the invocation comparison.Remaining before this issue closes
source-refinput + build checkout ref,workflow_metadata.goguard split, provenance source-group generation, verification policy/cert-claim rebinding, fixtures and conformance tests.v0.1.2frommainwithsource-ref: refs/tags/v0.1.2after the pin bump, exercising the path that originally failed in run31622651874.This issue closes when the implementation merges and the dogfood confirms the retry path end-to-end.
feat(npmprofile): Add tags-only source-ref input for fixed-pipeline r…
yunseo-kim commentedon Aug 13, 2026
Implementation update: the
source-refcapability has landed onmainvia #86 (merge commit496e40e).Delivered behavior
source-refinput onjs-ts-npm-package-slsa3.yml(fullrefs/tags/<name>only). When omitted, behavior is byte-compatible with the previous contract (golden-tested).npm-profile-source-refsubcommand validates the input, resolves and peels the tag in an isolated fetch, proves the target is a commit, and the package checkout is pinned to the resolved commit SHA. Missing, unresolvable, non-commit, version-mismatched, or invocation-conflicting refs fail closed withwindlass.verify.error.source-ref-invalidbefore any registry or build work. No tag is ever created, moved, or deleted.source.ref/source.revision), and whensource-refis supplied additionally records the invocation context as signed conditional members (source.input_ref,source.invocation_ref,source.invocation_revision; all-present-or-absent, explicitnullrejected). Omitted input keeps the previous predicate bytes exactly.release-ref-mismatch).${{ inputs.source-ref || github.ref }}across build/sign/publish and enforced by workflowcheck, so a dispatch retry and a tag-triggered run for the same release intent share one mutation group regardless of invocation ref. Because the raw input participates in group computation and GitHub expressions cannot normalize it, only the exact empty string counts as omitted; whitespace-only or padded spellings are rejected (source-ref-invalid).provenance-inputsphase with their canonical primary diagnostics.Verification
mainbefore its implementation landed; every commit is independently green with DCO sign-off.go test ./...,go test -race ./...,golangci-lint,actionlint,fixture-check(full index +provenance-inputsphase), andworkflow-check(all jobs +npm-onlyprofile) all pass; PR checks (CodeQL, OSV, dependency-review, semgrep, DCO, etc.) are green.Remaining
The live acceptance step is still pending: a real
workflow_dispatchretry of vers-jsv0.1.2frommainwithsource-ref: refs/tags/v0.1.2(ADR 0079 confirmation). This issue stays open until that dogfood run confirms the provenance binds to the tag commit whilerun_invocationrecords the dispatch ref.docs(dogfood): Add npm M1 verification procedure (#87)
yunseo-kim commentedon Aug 14, 2026
Closing: the capability this issue asked for is implemented (#86) and was exercised live end-to-end up to an unrelated, separately tracked failure.
Live evidence from the second vers-js dogfood attempt (run 31737001312, dispatched from
mainwithrelease_tag=v0.1.2):source-ref: refs/tags/v0.1.2(full-ref form on dispatch) and the reusable workflow accepted it under the tags-only validation rules.BUILT_REF=refs/tags/v0.1.2,BUILT_REVISION=68e6f0018d6824167ec490243b17a9edd6ba8fc7(the annotated tag's peeled commit), while the invocation context stayedrefs/heads/main@7851c5f832abce998fa7071941a94ad580afef60— the dispatch retry carrying the release tag identity, which is precisely this issue's scenario.The run did not reach provenance emission: it failed in the sign job's early npm OIDC exchange preflight for an unrelated response-contract gap (full report on #30; follow-up ADR in progress). The signed-Statement field values (
externalParameters.source.*,internalParametersexactly{}) remain pinned by #86's unit/golden coverage and will be observed on the live registry attestation in the next dogfood run as part of #30's acceptance, so no work remains under this issue.docs(decisions): Add ADR 0081 npm OIDC exchange response contract (#88)