From b190214fef48ba075d1ebc950e1edfe3fd706107 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:07 +0900 Subject: [PATCH 01/14] docs(decisions): add buildType URI ADRs and supersede ADR-0021 Replace the profile-specific buildType URI approach with acquired domains (buildtype.dev identifier, slsa-builder.dev documentation host) and record the superseded intermediate proposal. Signed-off-by: Yunseo Kim --- ...021-use-profile-specific-buildtype-uris.md | 2 +- ...-use-github-release-asset-buildtype-uri.md | 247 +++++++++++++++ ...use-acquired-domains-for-buildtype-uris.md | 283 ++++++++++++++++++ 3 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 docs/decisions/0041-use-github-release-asset-buildtype-uri.md create mode 100644 docs/decisions/0042-use-acquired-domains-for-buildtype-uris.md diff --git a/docs/decisions/0021-use-profile-specific-buildtype-uris.md b/docs/decisions/0021-use-profile-specific-buildtype-uris.md index 0dc2d2b..d04f584 100644 --- a/docs/decisions/0021-use-profile-specific-buildtype-uris.md +++ b/docs/decisions/0021-use-profile-specific-buildtype-uris.md @@ -1,7 +1,7 @@ --- parent: Decisions nav_order: 21 -status: accepted +status: superseded by ADR-0042 date: 12026-06-29 decision-makers: Yunseo Kim --- diff --git a/docs/decisions/0041-use-github-release-asset-buildtype-uri.md b/docs/decisions/0041-use-github-release-asset-buildtype-uri.md new file mode 100644 index 0000000..645ca13 --- /dev/null +++ b/docs/decisions/0041-use-github-release-asset-buildtype-uri.md @@ -0,0 +1,247 @@ +--- +parent: Decisions +nav_order: 41 +status: superseded by ADR-0042 +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use `github-release-asset` as the GitHub Release Asset `buildType` Profile URI + +## Context and Problem Statement + +ADR 0021 selected Windlass-owned, profile-specific, major-versioned repository-path `buildType` URIs +for profile-owned reusable workflows. ADR 0038 added the GitHub Release asset profile and requires a +profile-specific GitHub Release asset `buildType` URI and schema. ADR 0039 scoped that profile to +one explicitly named GitHub Release asset per run. ADR 0040 selected +`.github/workflows/github-release-asset-slsa3.yml` as the profile's reusable workflow entrypoint. + +The release asset build type specification will define how verifiers interpret the profile's asset +inputs, source ref, resolved source commit, artifact digest, GitHub Release linkage, and +verification rules. Because SLSA provenance uses `buildDefinition.buildType` as the schema identity +for `externalParameters`, `internalParameters`, and `resolvedDependencies`, the URI must identify +the GitHub Release asset profile semantics without confusing them with the reusable workflow path, +signing adapter, release orchestration layer, or a future generic artifact profile. + +What `buildType` URI should the initial GitHub Release asset profile emit? + +## Decision Drivers + +- Follow ADR 0021's Windlass-owned repository-path `buildType` URI convention. +- Preserve SLSA provenance v1's separation between `builder.id` as the trusted builder identity and + `buildType` as the parameterized build template and schema identity. +- Give verifiers a stable schema URI for interpreting release asset `externalParameters` and + rejecting unexpected fields. +- Make GitHub Release asset semantics first-class: release tag, asset name, local asset path, upload + target, uploaded asset identity, immutable release behavior, and GitHub attestation storage. +- Keep ADR 0039's one-asset-per-run scope visible in the profile segment. +- Avoid a generic artifact URI that hides GitHub Release-specific linkage and verification rules. +- Avoid embedding the SLSA level or workflow filename into the build type schema identity. +- Preserve a future path for a stable Windlass documentation-domain URI if documentation hosting + changes later. + +## Considered Options + +- Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1`. +- Use `https://github.com/windlasstech/slsa-builder/buildtypes/release-asset/v1`. +- Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-assets/v1`. +- Use `https://github.com/windlasstech/slsa-builder/buildtypes/generic-artifact/v1`. +- Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset-slsa3/v1`. +- Use `https://slsa-builder.windlass.dev/buildtypes/github-release-asset/v1`. + +## Decision Outcome + +Chosen option: "Use +`https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1`", because it +follows the existing Windlass build type URI convention while naming the selected GitHub Release +asset profile precisely and preserving one-asset-per-run semantics. + +The initial GitHub Release asset profile should emit this SLSA provenance build type: + +```text +https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1 +``` + +This URI identifies the release asset profile's schema and verification contract, not the workflow +filename, signing adapter, or SLSA Build level. The production reusable workflow identity selected +in ADR 0040 remains the `builder.id`, while this build type remains stable across +backward-compatible builder releases: + +```text +builder.id: https://github.com/windlasstech/slsa-builder/.github/workflows/github-release-asset-slsa3.yml@0123456789abcdef0123456789abcdef01234567 +buildType: https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1 +``` + +The build type specification for this URI should define at least: + +- the profile and artifact scope: exactly one explicitly named GitHub Release asset per production + profile run; +- supported and unsupported invocation modes; +- `externalParameters` for source repository, source ref, release tag, local asset path, release + asset name, upload intent, release policy, runtime policy, and other verifier-relevant + caller-controlled inputs; +- `internalParameters`, if any, for trusted workflow-controlled values useful for reproduction, + debugging, or incident response; +- `resolvedDependencies` rules, including source ref to resolved commit mapping; +- subject naming and digest rules for the single GitHub Release asset; +- byproduct rules for upload result, asset URL or API identifier, signed attestation bundle, release + metadata, or related diagnostic artifacts; +- GitHub Release linkage rules connecting source identity, release tag, asset name, uploaded asset + identity, and provenance subject digest; +- verifier expectations for Sigstore signature, signer identity, SHA-based `builder.id`, this + `buildType`, complete `externalParameters`, source commit, release tag, asset name, subject + digest, and GitHub Release linkage; +- rejection behavior for unknown `externalParameters` unless the specification explicitly allows + them; +- exclusion of full-release, multi-asset, checksum-manifest-centered, SBOM-bundled, + provenance-sidecar-bundled, detached-signature-bundled, and GitHub-generated source archive + semantics from this low-level build type; +- a complete example SLSA provenance predicate and version history. + +Breaking changes to required fields, field meanings, verification expectations, supported release +asset scope, or subject semantics should use a new major build type URI such as `/v2`. Backward- +compatible clarifications and optional fields may remain under `/v1` if verifiers can continue to +reject unexpected `externalParameters` according to the specification. + +### Consequences + +- Good, because the build type URI matches ADR 0021's existing convention. +- Good, because `github-release-asset` directly names the GitHub Release asset profile selected by + ADR 0038 and scoped by ADR 0039. +- Good, because singular `asset` keeps the one-primary-subject verification unit visible. +- Good, because the URI lets verifiers distinguish this profile from generic artifact, + whole-release, and multi-asset orchestration workflows. +- Good, because the URI stays independent of workflow release SHAs, signing adapter implementation, + and SLSA Build level claims. +- Neutral, because the URI is repository-hosted rather than hosted on a dedicated documentation + domain. +- Bad, because Windlass must maintain a concrete build type specification and fixtures for another + profile-specific schema. +- Bad, because generic GitHub or SLSA tooling will not understand this custom build type without + Windlass-specific policy or documentation. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1` as the + production release asset profile `buildType`; +- a repository-local specification document that explains that build type; +- complete schemas for `externalParameters`, `internalParameters`, `resolvedDependencies`, subjects, + byproducts, and release linkage; +- examples showing the distinction between SHA-based `builder.id` and this stable `buildType`; +- verifier behavior that rejects unexpected `externalParameters` unless explicitly allowed by the + build type specification; +- accepted and rejected fixture cases for source mismatch, release tag mismatch, asset name + mismatch, digest mismatch, upload mismatch, wrong `builder.id`, wrong `buildType`, unknown + parameters, and unsupported full-release or multi-asset semantics; +- versioning rules for backward-compatible clarifications and breaking schema changes; +- a future-decision boundary for moving to a stable Windlass documentation-domain URI. + +Implementation review should verify that production GitHub Release asset provenance does not reuse a +generic GitHub Actions workflow build type, SLSA GitHub Generator generic build type, generic +artifact build type, released workflow identity, or `actions/attest` default provenance build type +under this production builder identity. + +## Pros and Cons of the Options + +### Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset/v1` + +Use the existing Windlass repository-path convention with a singular GitHub Release asset profile +segment. + +- Good, because it is the most direct match for the selected profile and one-asset scope. +- Good, because it keeps GitHub Release-specific parameters first-class. +- Good, because it decouples the schema from builder release SHAs and workflow filenames. +- Good, because a later `/v2` can represent breaking release asset schema changes cleanly. +- Neutral, because URI-to-file mapping must be documented until a dedicated documentation site + exists. +- Bad, because it is intentionally GitHub Release-specific and not reusable for arbitrary artifacts. + +### Use `https://github.com/windlasstech/slsa-builder/buildtypes/release-asset/v1` + +Use a shorter profile segment that omits GitHub. + +- Good, because it is concise and still names release asset semantics. +- Good, because it might appear reusable for non-GitHub release surfaces. +- Bad, because the initial profile is tightly coupled to GitHub Release tags, assets, upload + behavior, immutable release behavior, and GitHub attestation storage. +- Bad, because future release surfaces could make the URI ambiguous. +- Bad, because GitHub-specific verifier expectations are less visible. + +### Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-assets/v1` + +Use a plural profile segment for GitHub Release assets. + +- Good, because GitHub Releases commonly contain multiple downloadable assets. +- Good, because it may feel natural to users thinking about complete release publication. +- Bad, because plural `assets` conflicts with ADR 0039's one-asset-per-run verification unit. +- Bad, because verifiers and users may infer multi-subject or whole-release semantics. +- Bad, because documentation would need to explain that the build type emits exactly one primary + release asset subject. + +### Use `https://github.com/windlasstech/slsa-builder/buildtypes/generic-artifact/v1` + +Use a broad artifact-bound build type and treat GitHub Release upload as one specialization. + +- Good, because SLSA build provenance is artifact-bound and generic file subjects are easy to model. +- Good, because it may be reusable across future file artifact profiles. +- Bad, because it broadens the profile beyond the GitHub Release asset contract selected in + ADR 0038. +- Bad, because it hides release tag, asset name, upload target, immutable release, and GitHub + attestation storage semantics. +- Bad, because it would conflict with a future truly generic artifact profile. + +### Use `https://github.com/windlasstech/slsa-builder/buildtypes/github-release-asset-slsa3/v1` + +Include the SLSA3 trust mode in the build type profile segment. + +- Good, because it mirrors the production workflow filename closely. +- Good, because it makes the SLSA Build L3-oriented claim visible in the build type URI. +- Bad, because SLSA level and trust mode are better represented by `builder.id`, signer identity, + and verifier roots of trust than by the build template schema name. +- Bad, because the same release asset schema may remain valid across future assurance modes, while + different assurance modes should use distinct builder identities. +- Bad, because it couples schema identity to the production workflow naming convention. + +### Use `https://slsa-builder.windlass.dev/buildtypes/github-release-asset/v1` + +Use a stable Windlass documentation-domain URI. + +- Good, because it provides the cleanest long-term human-readable documentation URI. +- Good, because it decouples build type identity from GitHub repository layout. +- Good, because it remains a good future direction if Windlass publishes stable versioned docs. +- Bad, because ADR 0021 already selected repository-path URIs for the current stage. +- Bad, because the project does not yet have the dedicated documentation domain, redirect policy, or + publication workflow needed to make this operational. +- Bad, because changing URI families now would require broader compatibility and transition rules. + +## More Information + +This decision applies ADR 0021's build type URI convention to the GitHub Release asset profile. It +decides only the `buildDefinition.buildType` URI and high-level scope of its specification. It does +not decide the exact public workflow input names, output names, job graph, release creation policy, +immutable release requirement, upload implementation, checksum manifest schema, SBOM format, +provenance sidecar naming, orchestration workflow interface, or standalone verifier CLI surface. + +Reference points considered: + +- SLSA v1.2 Build Provenance defines `buildDefinition.buildType` as the URI identifying how to + perform the build and interpret `externalParameters`, `internalParameters`, and + `resolvedDependencies`. +- SLSA v1.2 Build Provenance says `buildType` URIs should resolve to a human-readable specification + that includes parameter schemas, unambiguous initiation instructions, and a complete example. +- SLSA v1.2 Verifying Artifacts recommends verifying `buildType` and `externalParameters`, with + unrecognized `externalParameters` causing verification failure unless allowed by policy. +- SLSA v1.2 Distributing Provenance says attestations should be bound to artifacts rather than + releases and that source repository releases are a valid provenance distribution surface when they + publish artifacts. +- The community GitHub Actions workflow build type describes top-level workflow execution and is not + intended to describe reusable workflows, actions, or jobs. +- SLSA GitHub Generator uses project-owned build type URIs such as + `https://github.com/slsa-framework/slsa-github-generator/generic@v1`, but its generic profile does + not define Windlass GitHub Release asset semantics. +- GitHub `actions/attest` can sign custom predicates and identify subjects by path, digest, or + checksum input, but ADR 0038 treats it as a signing and storage adapter rather than the owner of + the Windlass release asset build type semantics. diff --git a/docs/decisions/0042-use-acquired-domains-for-buildtype-uris.md b/docs/decisions/0042-use-acquired-domains-for-buildtype-uris.md new file mode 100644 index 0000000..70e7dd0 --- /dev/null +++ b/docs/decisions/0042-use-acquired-domains-for-buildtype-uris.md @@ -0,0 +1,283 @@ +--- +parent: Decisions +nav_order: 42 +status: accepted +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use Acquired Domains for `buildType` URIs: `buildtype.dev` as Identifier, `slsa-builder.dev` as Documentation Host + +## Context and Problem Statement + +ADR 0021 selected Windlass-owned, profile-specific, major-versioned repository-path `buildType` URIs +for profile-owned reusable workflows. ADR 0041 applied that convention to the GitHub Release asset +profile. Both decisions intentionally left open a future move to a stable Windlass documentation +domain once such infrastructure existed. + +Windlass has now acquired the relevant domains for this project and future build type namespace +work: + +- `slsa-builder.dev` – the project documentation site. +- `buildtype.dev` – the canonical namespace for `buildType` URI identifiers. +- `buildtypes.dev` – a registry, index, or alias surface for build type discovery. +- `buildtype.com` and `buildtypes.com` – defensive domains that redirect to the `.dev` surfaces. + +The project is still in the architecture and decision phase; no build type specifications, workflow +implementations, or provenance attestations have been published. Because no provenance has been +issued, there is no existing corpus to migrate, no verifier policy to update for legacy URIs, and no +need for a grace period or alias chain. Switching the `buildType` URI family now carries the lowest +possible cost and prevents a disruptive migration later. + +The remaining question is how to assign roles to the two acquired domains and what exact URI shape +the project should adopt for profile `buildType` values going forward. + +## Decision Drivers + +- Separate the stable machine-readable `buildType` identifier from the human-readable documentation + hosting location. +- Replace repository-path `buildType` URIs before any provenance is emitted, avoiding future + migration or alias maintenance. +- Keep the `buildType` identifier stable and independent of GitHub repository layout, hosting + provider, or future repository renames. +- Preserve a clear organizational and project namespace so Windlass can register additional build + types under the same identifier domain without collision. +- Ensure `buildType` URIs still resolve to human-readable specifications, as recommended by SLSA + provenance v1. +- Follow SLSA community conventions for build type URI shape (`buildtypes//v`) on + the documentation side. +- Minimize operational complexity while achieving the above goals. + +## Considered Options + +- Keep the repository-path `buildType` URIs selected in ADR 0021 and ADR 0041. +- Use `slsa-builder.dev` as both the `buildType` identifier and the documentation host. +- Use `buildtype.dev` as both the `buildType` identifier and the documentation host. +- Use `buildtype.dev` as the `buildType` identifier and `slsa-builder.dev` as the documentation + host, with redirects from the identifier domain to the documentation domain. +- Use `slsa-builder.dev` as the `buildType` identifier and `buildtype.dev` as a registry/index site. + +## Decision Outcome + +Chosen option: "Use `buildtype.dev` as the `buildType` identifier and `slsa-builder.dev` as the +documentation host, with redirects from the identifier domain to the documentation domain", because +it separates identifier stability from documentation presentation, decouples the build type +namespace from the project documentation site, and avoids migration cost by switching before any +provenance is published. + +### Canonical `buildType` URI shape + +The canonical `buildType` URI value that profile-owned reusable workflows must emit in provenance +predicates is: + +```text +https://buildtype.dev////v +``` + +For the Windlass `slsa-builder` project, the concrete URIs are: + +- JS/TS npm package profile: + + ```text + https://buildtype.dev/windlass/slsa-builder/js-ts-npm-package/v1 + ``` + +- GitHub Release asset profile: + + ```text + https://buildtype.dev/windlass/slsa-builder/github-release-asset/v1 + ``` + +The `/` segments identify the owner of the build type schema and isolate the namespace +from other Windlass projects or third parties. The `` segment identifies the artifact and +profile semantics. The `` segment identifies the major version of the build type schema. +Backward-compatible schema clarifications and additional optional fields may remain on the same +major version. Breaking changes to meanings, required fields, verification expectations, or +supported build templates must use a new major version such as `/v2`. + +### Documentation host and redirect behavior + +The human-readable build type specification for each URI is hosted on `slsa-builder.dev` at: + +```text +https://slsa-builder.dev/buildtypes//v +``` + +Concrete documentation URLs: + +```text +https://slsa-builder.dev/buildtypes/js-ts-npm-package/v1 +https://slsa-builder.dev/buildtypes/github-release-asset/v1 +``` + +Requests to `https://buildtype.dev////v` must resolve through a +permanent redirect, preferably HTTP 308 or otherwise HTTP 301, to the corresponding +`https://slsa-builder.dev/buildtypes//v` documentation page. This lets +`buildtype.dev` act as a stable, opaque identifier namespace while still satisfying the SLSA +recommendation that `buildType` URIs resolve to human-readable specifications. Temporary redirects +such as HTTP 302 or HTTP 307 should not be used for canonical build type specification resolution +because they imply that the documentation target is not stable. + +The adjacent domains are supporting surfaces, not separate `buildType` URI authorities for this +decision. `buildtypes.dev` may serve as a registry, index, or alias surface that points users and +tools to canonical `buildtype.dev` identifiers. It may redirect to `buildtype.dev` or host a hub UI +that lists known build type specifications. `buildtype.com` and `buildtypes.com` are defensive +registrations and should redirect to the corresponding `.dev` surfaces rather than emit canonical +Windlass `buildType` URIs. + +Verifiers should compare the exact canonical `buildtype.dev` URI recorded in provenance. Redirects +exist for human-readable specification discovery and ecosystem navigation; they do not make redirect +targets or alias domains equivalent `buildType` identifiers. + +### Superseded decisions + +This decision supersedes the repository-path `buildType` URI choices in: + +- ADR 0021 – Use Profile-Specific `buildType` URIs +- ADR 0041 – Use `github-release-asset` as the GitHub Release Asset `buildType` Profile URI + +Those ADRs remain valid historical context but their chosen `buildType` URI values are no longer +authoritative. The existing repository-path URIs will not be retained as aliases because no +provenance has been issued and the cost of immediate replacement is zero. + +### Consequences + +- Good, because `buildType` identifiers are now stable domain-based URIs rather than repository-path + URIs. +- Good, because the identifier namespace is decoupled from GitHub repository layout and hosting + changes. +- Good, because `buildtype.dev` can host identifiers for other Windlass projects without mixing them + into the `slsa-builder.dev` documentation site. +- Good, because adjacent domains can support registry discovery and defensive redirects without + multiplying canonical `buildType` URI authorities. +- Good, because `slsa-builder.dev` remains a clean, branded project documentation site. +- Good, because redirecting from the identifier domain preserves the SLSA expectation that + `buildType` URIs resolve to specifications. +- Good, because no provenance has been published, so the switch requires no legacy alias or + migration period. +- Neutral, because two domains must be maintained instead of one. +- Neutral, because the URI is slightly longer than a single-domain project URI. +- Bad, because the redirect from `buildtype.dev` to `slsa-builder.dev` must be kept operational for + the identifier resolution contract to hold. +- Bad, because the `/` prefix assumes Windlass will continue to use GitHub-style org + names; a different organizational structure would require revisiting the namespace design. + +### Confirmation + +This decision is confirmed when: + +- ADR 0021 and ADR 0041 are updated to `status: superseded by ADR-0042`. +- This ADR is added to `docs/decisions/` with `status: accepted`. +- Architecture specifications for the JS/TS npm package profile and GitHub Release asset profile + list the new `buildtype.dev` URIs as their `buildType` values. +- Reusable workflow implementations emit the new `buildType` URIs in production provenance. +- Verifier documentation and example policies reference the new `buildType` URIs. +- `buildtype.dev` is configured to issue permanent HTTP 308 or HTTP 301 redirects from + `////v` to the matching + `slsa-builder.dev/buildtypes//v` documentation page. +- `buildtypes.dev`, `buildtype.com`, and `buildtypes.com` are documented or configured as registry, + alias, or defensive redirect surfaces rather than canonical `buildType` URI namespaces. +- Tests assert that emitted provenance predicates use the new `buildType` URI format and do not + reference the superseded repository-path URIs. + +## Pros and Cons of the Options + +### Keep the repository-path `buildType` URIs + +Continue using `https://github.com/windlasstech/slsa-builder/buildtypes//v`. + +- Good, because no new infrastructure or redirect configuration is required. +- Good, because existing ADRs remain authoritative without change. +- Bad, because the URI is tied to GitHub repository path and ownership; a repository rename or move + would break or complicate the identifier. +- Bad, because it wastes the acquired domains and misses the opportunity to switch before any + provenance is published. +- Bad, because it conflates source hosting with build type schema ownership. + +### Use `slsa-builder.dev` as both identifier and documentation host + +Use `https://slsa-builder.dev/buildtypes//v` for both `buildType` values and +specifications. + +- Good, because it is a single domain to operate and explain. +- Good, because it strongly brands each build type with the project name. +- Good, because it matches the future-documentation-domain example in ADR 0021. +- Bad, because it does not leverage `buildtype.dev` for its intended purpose as a build type + namespace. +- Bad, because the identifier namespace is tied to one project's documentation site, making + organization-wide reuse harder. +- Bad, because a future Windlass project would need a separate domain or a subpath under + `slsa-builder.dev`, which is semantically awkward. + +### Use `buildtype.dev` as both identifier and documentation host + +Use `https://buildtype.dev////v` for both identifiers and rendered +specifications. + +- Good, because it keeps build type identifiers in a dedicated namespace. +- Good, because organizational and project scoping is clear. +- Bad, because the documentation site loses the `slsa-builder.dev` project branding and URL. +- Bad, because users following a `buildType` URI would land on a generic `buildtype.dev` page rather + than the project documentation site. +- Bad, because it does not separate the concerns of identifier stability and human-readable + documentation presentation as cleanly as the chosen option. + +### Use `buildtype.dev` as identifier and `slsa-builder.dev` as documentation host + +Use `https://buildtype.dev////v` as the canonical `buildType` +identifier and `https://slsa-builder.dev/buildtypes//v` as the human-readable +specification, with redirects from the former to the latter. + +- Good, because it cleanly separates stable identifier namespace from project documentation hosting. +- Good, because `buildtype.dev` can scale to other Windlass projects under the same namespace + convention. +- Good, because `slsa-builder.dev` remains the branded, user-facing documentation site. +- Good, because redirects preserve SLSA's expectation that `buildType` URIs resolve to + specifications. +- Good, because the switch happens before any provenance is issued, eliminating migration cost. +- Neutral, because two domains must be configured and renewed. +- Bad, because redirect availability becomes part of the build type resolution contract. + +### Use `slsa-builder.dev` as identifier and `buildtype.dev` as registry/index + +Use `https://slsa-builder.dev/buildtypes//v` as the identifier and +`https://buildtype.dev/` as a cross-project build type registry or index. + +- Good, because it brands the identifier with the project. +- Bad, because it ties the identifier to a single project documentation domain, reintroducing the + coupling the dedicated `buildtype.dev` domain was meant to avoid. +- Bad, because a registry/index role for `buildtype.dev` is speculative and not currently needed. +- Bad, because it does not use `buildtype.dev` for its natural purpose as the identifier namespace. + +## More Information + +This decision supersedes the `buildType` URI choices in ADR 0021 and ADR 0041. Those ADRs retain +their reasoning about why `buildType` should be separate from `builder.id`, why the profile segment +should name artifact semantics rather than workflow filenames, and why major-versioned schema URIs +are preferable to tag-churning identities. Only the URI authority and namespace shape change here. + +Reference points considered: + +- SLSA provenance v1 requires `buildDefinition.buildType` and recommends that the URI resolve to a + human-readable specification with parameter schemas, initiation instructions, and a complete + example. +- SLSA provenance v1 treats `buildType` as the schema identity for `externalParameters`, + `internalParameters`, and `resolvedDependencies`. +- ADR 0021 separated `builder.id` (released reusable workflow identity) from `buildType` (profile + schema identity) and explicitly noted a future move to a stable documentation domain. +- ADR 0041 applied the ADR 0021 convention to the GitHub Release asset profile and likewise left + open a future documentation-domain move. +- No provenance, verifier policy, or published specification currently references the superseded + repository-path URIs, making immediate replacement cost-free. + +Future decisions may define: + +- the exact repository-local file path for build type specifications before they are published to + `slsa-builder.dev`; +- the redirect and hosting mechanics for `buildtype.dev` (e.g., static site generator, edge redirect + rules, DNS configuration); +- the registry, index, alias, or defensive redirect behavior for `buildtypes.dev`, `buildtype.com`, + and `buildtypes.com`; +- whether future Windlass projects follow the same `https://buildtype.dev///...` + convention; +- how to version and publish build type specifications alongside project releases. From 220eaef9562efef9f60c82b6d770feb353b03090 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:09 +0900 Subject: [PATCH 02/14] docs(decisions): add ADRs for npm publish provenance foundation Define Windlass-generated SLSA provenance for npm packages, registry URL handling that preserves npmjs semantics, and actions/attest as the initial Sigstore signing adapter. Signed-off-by: Yunseo Kim --- ...nerated-slsa-provenance-for-npm-publish.md | 268 ++++++++++++++++++ ...while-guaranteeing-only-npmjs-semantics.md | 236 +++++++++++++++ ...est-as-initial-sigstore-signing-adapter.md | 233 +++++++++++++++ 3 files changed, 737 insertions(+) create mode 100644 docs/decisions/0029-use-windlass-generated-slsa-provenance-for-npm-publish.md create mode 100644 docs/decisions/0030-accept-registry-url-while-guaranteeing-only-npmjs-semantics.md create mode 100644 docs/decisions/0035-use-actions-attest-as-initial-sigstore-signing-adapter.md diff --git a/docs/decisions/0029-use-windlass-generated-slsa-provenance-for-npm-publish.md b/docs/decisions/0029-use-windlass-generated-slsa-provenance-for-npm-publish.md new file mode 100644 index 0000000..955f6f0 --- /dev/null +++ b/docs/decisions/0029-use-windlass-generated-slsa-provenance-for-npm-publish.md @@ -0,0 +1,268 @@ +--- +parent: Decisions +nav_order: 29 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Use Windlass-Generated SLSA Provenance for npm Publish + +## Context and Problem Statement + +ADR 0002 chose an extensible trusted reusable workflow foundation, ADR 0003 refined it into a thin +core with profile-owned reusable workflows, ADR 0013 scoped the initial JS/TS profile to npm package +releases, ADR 0024 selected OIDC trusted publishing without npm publish secrets, and ADR 0028 chose +SHA-pinned reusable workflow builder identity. + +The remaining provenance publication decision is which attestation should be canonical for the +production JS/TS npm package SLSA3 profile. npm trusted publishing can automatically generate npm +provenance for supported public packages from supported public repositories. npm publish also +accepts an externally generated provenance bundle through `--provenance-file`. GitHub artifact +attestations can generate SLSA build provenance through `actions/attest`, but their primary +distribution and verification surface is GitHub's artifact attestation system rather than npm +package publication. + +SLSA v1.2 Build L3 expects provenance to be generated or verified by the trusted build platform's +control plane, with the verifier checking the provenance signature, artifact subject digest, +`predicateType`, trusted `builder.id`, `buildType`, and expected `externalParameters`. For this +project, the trusted builder identity, build type schema, external parameter schema, package subject +rules, and verification contract are Windlass-owned. The next decision is whether canonical npm +package provenance should be generated by npm's trusted-publishing automation, by the Windlass +reusable workflow and submitted to npm, or by a separate GitHub artifact attestation path. + +Should the production JS/TS npm package SLSA3 profile treat npm-generated provenance, Windlass- +generated SLSA provenance, GitHub artifact attestations, or a combination as the canonical +provenance and verification surface? + +## Decision Drivers + +- Keep canonical provenance generation inside the trusted reusable workflow boundary selected by ADR + 0002 and ADR 0003. +- Emit SLSA build provenance using `predicateType: https://slsa.dev/provenance/v1`. +- Preserve ADR 0028's SHA-based `builder.id` as the machine trust anchor for production builds. +- Preserve ADR 0021's Windlass-owned `buildType` URI and profile-specific `externalParameters` + schema as verifier-relevant contract, not merely documentation. +- Keep npm trusted publishing as the publish authentication mechanism selected by ADR 0024 while + avoiding long-lived npm publish secrets. +- Publish provenance through npm's package ecosystem when possible so package consumers can discover + provenance through npm registry tooling and package-version metadata. +- Avoid presenting two unrelated canonical provenance records for the same npm package tarball. +- Keep GitHub artifact attestations available as a future or secondary distribution path without + making them the canonical npm package provenance surface. + +## Considered Options + +- Generate canonical SLSA provenance in the Windlass reusable workflow and submit it to npm with + `npm publish --provenance-file`. +- Use npm trusted-publishing automatic provenance as the canonical provenance. +- Use npm trusted-publishing automatic provenance as canonical and emit Windlass or GitHub + attestations as secondary evidence. +- Use GitHub artifact attestations as canonical and do not publish canonical provenance through npm. +- Publish without provenance and rely only on package registry integrity metadata. + +## Decision Outcome + +Chosen option: "Generate canonical SLSA provenance in the Windlass reusable workflow and submit it +to npm with `npm publish --provenance-file`", because the production profile's SLSA Build +L3-oriented trust contract is Windlass-owned and must be verifiable through Windlass `builder.id`, +`buildType`, and `externalParameters` rather than through an npm-generated default predicate shape. + +The production JS/TS npm package SLSA3 profile should generate the canonical SLSA provenance v1 +predicate inside the Windlass reusable workflow. The provenance should use: + +- `_type: https://in-toto.io/Statement/v1`; +- `predicateType: https://slsa.dev/provenance/v1`; +- `subject` entries that identify the published npm package artifact and its verified tarball digest + values; +- `buildDefinition.buildType` set to + `https://github.com/windlasstech/slsa-builder/buildtypes/js-ts-npm-package/v1`; +- complete verifier-relevant `buildDefinition.externalParameters` as defined by the profile build + type specification; +- `runDetails.builder.id` set to the SHA-based reusable workflow identity selected by ADR 0028; +- `runDetails.builder.version`, `builderDependencies`, `metadata`, `resolvedDependencies`, and + byproducts as defined by the profile architecture specification. + +The profile should sign the provenance as a Sigstore DSSE bundle suitable for npm's +`provenance-file` publish path. The profile should publish the package with +`npm publish --provenance-file=`, or the equivalent npm configuration that submits the same +external provenance bundle. The profile should not rely on npm trusted publishing's automatic +provenance as the canonical SLSA provenance for this builder mode. + +The profile should continue to use npm trusted publishing and GitHub Actions OIDC for publish +authentication as decided in ADR 0024. In this decision, npm trusted publishing authenticates the +registry mutation; Windlass-generated SLSA provenance describes and authenticates the trusted build. + +The profile should fail rather than silently falling back to npm automatic provenance, token-based +publish, unsigned provenance, local-only provenance, or a GitHub-only attestation when the +production SLSA3 profile is expected to publish a package. A materially different provenance +publication mode should use a later ADR and, if its trust properties differ, a distinct builder +identity. + +Canonical verification for a package published by this profile should require all of the following: + +- the npm package name, version, registry package URL, tarball name, and tarball digest identify the + published package version expected by the verifier; +- the downloaded or registry-resolved tarball digest matches the provenance `subject` digest; +- the provenance envelope signature verifies against the trusted Sigstore root and expected GitHub + Actions signer identity for the Windlass reusable workflow; +- `predicateType` is exactly `https://slsa.dev/provenance/v1`; +- `runDetails.builder.id` matches the SHA-based Windlass reusable workflow identity trusted for the + release, as defined by ADR 0028 and the signed release metadata policy; +- `buildDefinition.buildType` is exactly + `https://github.com/windlasstech/slsa-builder/buildtypes/js-ts-npm-package/v1`; +- all `buildDefinition.externalParameters` match the selected build type specification and verifier + expectations, with unexpected fields rejected unless explicitly allowed by that specification; +- source repository, source ref, resolved source commit, package directory, publish intent, package + manager selection, runtime policy, and release event context match the profile's expected values; +- npm registry provenance metadata links the published package version to the submitted Windlass + provenance bundle. + +Unsigned workflow outputs such as `package-tarball-sha256` and `package-tarball-sha512` are useful +release handles, but they are not substitutes for signed provenance verification. + +### Consequences + +- Good, because the canonical provenance is generated within the Windlass trusted reusable workflow + boundary rather than by caller-controlled steps. +- Good, because Windlass `builder.id`, `buildType`, and `externalParameters` become enforceable + verifier contract fields. +- Good, because the provenance can use SLSA build provenance v1 from the beginning instead of + inheriting older npm or upstream builder predicate shapes. +- Good, because npm remains the package ecosystem distribution surface for provenance, aligning with + SLSA guidance that package registries should distribute provenance alongside artifacts when they + can. +- Good, because npm trusted publishing still removes long-lived npm write tokens from the production + profile. +- Good, because GitHub artifact attestations remain available for future release asset, SBOM, + monitor, or secondary evidence workflows without becoming the canonical npm package provenance. +- Neutral, because npm automatic provenance may still be useful for simpler npm workflows outside + the Windlass SLSA3 profile. +- Bad, because Windlass must implement, sign, test, publish, and document its own provenance bundle + generation and verification behavior. +- Bad, because the profile must keep npm `provenance-file` compatibility under test as npm CLI and + registry behavior evolves. +- Bad, because consumers need Windlass-specific verifier policy in addition to generic npm + provenance awareness. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, release documentation, and verifier guidance define: + +- Windlass-generated SLSA build provenance v1 as the canonical provenance for the production + profile; +- `npm publish --provenance-file` or an equivalent external provenance-file configuration as the npm + submission path; +- no reliance on npm trusted-publishing automatic provenance as the canonical production provenance; +- the exact provenance subject naming and `sha256`/`sha512` digest encoding for the published npm + package tarball; +- the complete `buildType`, `externalParameters`, `internalParameters`, `resolvedDependencies`, + `builder.id`, `builder.version`, `builderDependencies`, invocation metadata, and byproducts + schema; +- Sigstore DSSE bundle generation and signing requirements; +- npm trusted publishing requirements for publish authentication, separate from provenance + generation; +- failure behavior when the provenance bundle cannot be generated, signed, submitted, linked by npm, + or later verified; +- verifier checks for signature, subject digest, `predicateType`, SHA-based `builder.id`, Windlass + `buildType`, complete `externalParameters`, source identity, package identity, runtime policy, and + npm registry linkage; +- documentation that GitHub artifact attestations and workflow outputs are secondary evidence unless + a later ADR makes them canonical for a distinct profile or builder mode. + +Implementation review should verify that production publishes cannot silently downgrade to npm +automatic provenance, token-based publish, unsigned provenance, local-only provenance, or +GitHub-only attestations under the same production SLSA3 builder identity. + +## Pros and Cons of the Options + +### Generate Windlass provenance and submit it with `npm publish --provenance-file` + +The reusable workflow generates the SLSA v1 provenance predicate, signs it as a Sigstore DSSE +bundle, and submits that bundle to npm during publish. + +- Good, because canonical provenance is controlled by the same trusted reusable workflow boundary + that claims the SLSA3 builder identity. +- Good, because it lets Windlass define and verify a profile-specific `buildType` and + `externalParameters` schema. +- Good, because it keeps provenance distribution attached to the npm package version. +- Good, because it matches the SLSA requirement that the trusted build platform generate or verify + provenance fields. +- Bad, because implementation and compatibility responsibility belongs to Windlass. +- Bad, because npm's external provenance-file behavior must be treated as part of the production + compatibility surface. + +### Use npm automatic provenance as canonical + +npm trusted publishing automatically generates and publishes provenance when supported conditions +are met. + +- Good, because it follows npm's simplest supported trusted-publishing path. +- Good, because npm CLI and registry tooling understand the resulting provenance by default. +- Good, because implementation effort is small. +- Bad, because npm controls the predicate shape and builder identity semantics. +- Bad, because Windlass cannot make its `buildType` and `externalParameters` schema the canonical + verifier contract. +- Bad, because this weakens the distinction between an npm trusted-publishing workflow and a + Windlass SLSA3 reusable builder. + +### Use npm automatic provenance as canonical and emit secondary attestations + +npm provenance remains the primary package provenance while Windlass or GitHub also emits separate +attestations for audit or defense in depth. + +- Good, because it combines npm registry UX with extra evidence. +- Good, because GitHub artifact attestations can support repository-side audit and monitoring. +- Neutral, because this may be useful for future secondary evidence or SBOM workflows. +- Bad, because consumers must understand which attestation is authoritative. +- Bad, because divergent subjects, digests, builder IDs, or build types can create confusing or + contradictory verification results. + +### Use GitHub artifact attestations as canonical + +The workflow uses GitHub artifact attestations, for example through `actions/attest`, as the primary +provenance and does not submit canonical provenance through npm. + +- Good, because GitHub provides maintained attestation generation, storage, and + `gh attestation verify` tooling. +- Good, because this may be a strong fit for future GitHub Release asset, container, or SBOM + profiles. +- Bad, because npm package consumers do not get the canonical provenance through the npm registry + package-version surface. +- Bad, because it does not use npm's package ecosystem provenance distribution path for the initial + npm package profile. + +### Publish without provenance + +The profile publishes the npm package with registry integrity metadata but without SLSA provenance. + +- Good, because implementation is minimal. +- Bad, because it does not satisfy the project's SLSA builder purpose. +- Bad, because consumers cannot verify the package against trusted builder, build type, source, and + external parameter expectations. +- Bad, because it conflicts with the production SLSA3 profile name and prior ADR direction. + +## More Information + +Reference points considered: + +- SLSA v1.2 Build Provenance defines `predicateType: https://slsa.dev/provenance/v1`, separates + `builder.id` from `buildType`, and requires `externalParameters` to capture the external build + interface. +- SLSA v1.2 Build Requirements require Build L3 provenance to be strongly resistant to tenant + forgery and generated or verified by the trusted control plane. +- SLSA v1.2 Verifying Artifacts recommends verifying provenance signature, subject digest, + `predicateType`, trusted `builder.id`, `buildType`, and expected `externalParameters`. +- SLSA v1.2 Distributing Provenance prefers package registries distributing provenance alongside + artifacts when the ecosystem supports it. +- npm trusted publishing uses OIDC for publish authentication and can automatically generate npm + provenance for supported public packages from supported public repositories. +- npm publish supports `provenance-file` for submitting an externally generated provenance bundle + and treats it as mutually exclusive with automatic `provenance` generation. +- GitHub artifact attestations and `actions/attest` can generate signed SLSA provenance + attestations, but their primary discovery and verification path is GitHub's attestation API and + `gh attestation verify`. +- SLSA GitHub Generator's Node.js builder is a relevant precedent for a reusable workflow that + builds npm package tarballs, generates provenance, and publishes with an externally supplied + provenance file. diff --git a/docs/decisions/0030-accept-registry-url-while-guaranteeing-only-npmjs-semantics.md b/docs/decisions/0030-accept-registry-url-while-guaranteeing-only-npmjs-semantics.md new file mode 100644 index 0000000..93c3864 --- /dev/null +++ b/docs/decisions/0030-accept-registry-url-while-guaranteeing-only-npmjs-semantics.md @@ -0,0 +1,236 @@ +--- +parent: Decisions +nav_order: 30 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Accept Registry URL While Guaranteeing Only npmjs Semantics + +## Context and Problem Statement + +ADR 0013 scoped the initial JS/TS profile to npm package releases, ADR 0024 selected OIDC trusted +publishing without publish secrets, and ADR 0029 selected Windlass-generated SLSA provenance +submitted with `npm publish --provenance-file` as the canonical production provenance path. + +The remaining registry-scope question is how the profile should treat its npm registry input. The +npm CLI can publish to the public npm registry by default, and it can also be configured with a +different registry URL or a scope-specific registry. However, npm trusted publishing, npm registry +provenance linkage, and `provenance-file` behavior are ecosystem features whose production semantics +are documented for npmjs.com, not as a portable guarantee for every npm-compatible registry API. + +The profile therefore needs to decide whether it should hard-code `registry.npmjs.org`, accept any +npm-compatible registry URL, support token fallback for registries that do not implement OIDC, or +split registry families into separate profiles. The choice affects the public `workflow_call` input +contract, the provenance subject and registry linkage rules, verifier expectations, and whether the +same production SLSA3 builder identity would accidentally imply support for registries whose +authentication and provenance behavior Windlass has not validated. + +Should the production JS/TS npm package SLSA3 profile accept configurable registry URLs, and if so, +which registry behaviors does Windlass guarantee? + +## Decision Drivers + +- Preserve ADR 0024's no-publish-secret invariant and avoid token fallback in the production SLSA3 + profile. +- Preserve ADR 0029's canonical `npm publish --provenance-file` submission path and npm registry + provenance linkage expectations. +- Avoid claiming production compatibility for npm-compatible registries whose OIDC authentication, + provenance-file acceptance, digest semantics, or provenance discovery behavior Windlass has not + validated. +- Keep the workflow API flexible enough for users to test compatible registries without requiring a + new input shape later. +- Make `https://registry.npmjs.org/` the only registry with guaranteed production semantics for the + initial profile. +- Keep non-npm package registries, including JSR, outside the initial npm package profile while + leaving room for future dedicated profiles. +- Avoid overloading one `builder.id` and `buildType` with materially different package ecosystem + semantics. + +## Considered Options + +- Accept a configurable npm registry URL, guarantee only npmjs production semantics, and never fall + back to token-based publishing. +- Require `https://registry.npmjs.org/` and reject every other registry URL. +- Accept arbitrary npm-compatible registry URLs and claim production support for all of them. +- Accept arbitrary npm-compatible registry URLs and use token fallback for registries without OIDC + trusted publishing. +- Add JSR and other non-npm registries to the initial JS/TS package profile. + +## Decision Outcome + +Chosen option: "Accept a configurable npm registry URL, guarantee only npmjs production semantics, +and never fall back to token-based publishing", because this preserves the no-secret production +security model while avoiding an unnecessary hard block on users who want to validate npm-compatible +custom registries themselves. + +The production JS/TS npm package SLSA3 profile should expose or preserve a registry URL input for +npm publishing. The officially guaranteed production target for that input is +`https://registry.npmjs.org/`. The profile may attempt to publish to another npm-compatible registry +URL when a caller explicitly configures one, but that configuration is outside Windlass's guaranteed +production compatibility surface unless a later ADR and specification add support for that registry +class. + +The profile must not use `NPM_TOKEN`, `NODE_AUTH_TOKEN`, OTP, or any other publish-capable token as +a fallback when a non-npmjs registry cannot complete OIDC trusted publishing, external +provenance-file submission, or provenance linkage. A custom registry path must either complete the +same tokenless publish and provenance submission flow or fail clearly. + +Documentation for the registry URL input should state that custom npm-compatible registries are +unsupported-but-not-blocked. Users who choose such registries are responsible for validating that +the registry supports all required semantics for their use case, including: + +- OIDC or equivalent tokenless publish authentication compatible with the profile's + no-publish-secret contract; +- `npm publish --provenance-file` or equivalent external provenance bundle submission behavior; +- stable npm package identity, version, tarball URL, and digest semantics compatible with the + profile's provenance subject rules; +- discoverable registry metadata that can link the package version to the submitted provenance + bundle; +- verifier-accessible package and provenance data after publication. + +Windlass verifier guidance for the initial production profile should define normative registry +linkage checks for npmjs.com. For non-npmjs npm-compatible registries, verifier behavior is +registry-specific unless a later ADR defines a supported registry class and its verification +contract. + +JSR and other non-npm package registries are out of scope for the initial JS/TS npm package profile. +They may be supported later through separate profiles, build types, provenance subject rules, +publication flows, and verifier policies. Their lower priority for the initial implementation should +not be interpreted as a rejection of future support; it only keeps the first production profile +focused on npm package publication. + +### Consequences + +- Good, because the profile remains flexible for callers that want to experiment with npm-compatible + custom registries. +- Good, because npmjs.com remains the only officially guaranteed production registry target for the + initial profile. +- Good, because the no-publish-secret and no-token-fallback invariant from ADR 0024 remains intact. +- Good, because Windlass avoids claiming SLSA3 registry compatibility for ecosystems it has not + specified or validated. +- Good, because custom registry behavior can be added later through a narrower support contract + rather than by weakening the initial profile. +- Good, because JSR and other non-npm registries can receive dedicated profiles instead of being + forced into an npm-shaped build type. +- Neutral, because users may still see a `registry-url` input and assume broader support unless + documentation is explicit. +- Bad, because custom registry users must validate compatibility themselves before relying on the + profile. +- Bad, because implementation and documentation must distinguish "accepted input" from "guaranteed + production support". + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, release documentation, and verifier guidance define: + +- a registry URL input or equivalent npm registry configuration surface; +- `https://registry.npmjs.org/` as the only officially guaranteed production registry target; +- no token-based publish fallback for any registry URL in the production SLSA3 profile; +- clear failure behavior when OIDC trusted publishing, external provenance-file submission, package + digest resolution, or provenance linkage is unavailable; +- documentation that custom npm-compatible registries are unsupported-but-not-blocked and require + user validation; +- normative npmjs.com registry linkage checks for package identity, tarball digest, and provenance + metadata; +- registry-specific verifier guidance as future work for any custom registry class that becomes + officially supported; +- JSR and other non-npm registries as out of scope for the initial npm profile and candidates for + later dedicated profiles. + +Implementation review should verify that selecting a custom registry cannot make the production +profile use publish tokens, silently omit provenance, silently switch to npm automatic provenance, +skip provenance registry linkage checks without documenting the weaker support level, or claim npmjs +verification semantics for a registry that Windlass has not specified. + +## Pros and Cons of the Options + +### Accept registry URL, guarantee only npmjs, and forbid token fallback + +The profile accepts a configurable npm registry URL, officially supports npmjs.com production +semantics, and requires all registry choices to use the same no-publish-secret production flow. + +- Good, because it keeps the workflow input flexible without over-claiming compatibility. +- Good, because npmjs.com remains the documented and tested production path. +- Good, because security properties stay stable across registry choices: no publish secret and no + token fallback. +- Good, because custom registry users can test their registry-specific behavior without Windlass + blessing it as supported. +- Bad, because documentation must be precise to avoid support ambiguity. +- Bad, because custom registries may fail at publish or verification time if they do not implement + compatible OIDC, provenance-file, or metadata behavior. + +### Require npmjs and reject every other registry URL + +The profile hard-codes or validates the registry URL to `https://registry.npmjs.org/`. + +- Good, because the support boundary is unambiguous. +- Good, because verifier semantics can be fully npmjs-specific. +- Good, because unsupported custom registry failures happen early. +- Bad, because users cannot experiment with registries that may already support compatible behavior. +- Bad, because a future custom-registry profile may need to introduce a new API surface that could + have been preserved safely from the start. + +### Accept arbitrary npm-compatible registries as officially supported + +The profile treats any npm-compatible registry URL as part of the production support contract. + +- Good, because it maximizes apparent compatibility. +- Good, because it aligns with npm CLI's general ability to target alternate registries. +- Bad, because npm-compatible package APIs do not imply compatible OIDC trusted publishing, + provenance-file submission, provenance storage, or verifier discovery semantics. +- Bad, because Windlass would be claiming support for behavior it has not specified, tested, or + secured. +- Bad, because registry-specific behavior could weaken or confuse the same `builder.id` and + `buildType` trust contract. + +### Accept custom registries with token fallback + +The profile publishes to npmjs with OIDC but falls back to publish tokens for registries that do not +support tokenless publishing. + +- Good, because it matches many existing private registry workflows. +- Good, because it would make custom registries more likely to work operationally. +- Bad, because long-lived publish credentials re-enter the production workflow boundary. +- Bad, because it conflicts with ADR 0024's OIDC-only publish authentication decision. +- Bad, because token and OIDC publish modes have materially different security properties under the + same production profile unless split by a later ADR and distinct identity. + +### Add JSR and other non-npm registries to the initial profile + +The initial JS/TS package profile supports npm plus other JavaScript/TypeScript package registries. + +- Good, because it acknowledges the broader JS/TS package ecosystem. +- Good, because JSR and similar registries may have modern provenance or OIDC-oriented publishing + models that deserve support. +- Bad, because non-npm registries have different package identity, publication, provenance, and + verifier semantics. +- Bad, because combining multiple registry families would enlarge the initial profile and dilute its + npm-specific `buildType` contract. +- Bad, because npm remains the higher-priority initial ecosystem for this project. + +## More Information + +This decision follows ADR 0024's publish authentication decision and ADR 0029's provenance +submission decision. It decides only registry URL scope for the initial production JS/TS npm package +profile. It does not decide private dependency registry authentication, staged publishing, +lower-assurance token-based profiles, registry mirrors used only for dependency installation, or +future JSR support. + +Reference points considered: + +- npm publish defaults to the public npm registry but can be configured with another registry URL or + scope-specific registry. +- npm trusted publishing is documented for npmjs.com package settings and supported cloud CI/CD + providers, with OIDC replacing long-lived publish tokens. +- npm provenance and trusted publishing have public package, public repository, supported CI, and + registry-specific limitations. +- npm publish supports `provenance-file` for submitting an externally generated provenance bundle + and treats it as mutually exclusive with automatic `provenance` generation. +- Windlass organization policy prefers OIDC over long-lived secrets and targets signed provenance + for production releases. +- SLSA verification requires checking artifact subject digest, provenance signature, + `predicateType`, trusted `builder.id`, `buildType`, and expected external parameters; registry + linkage is ecosystem-specific evidence that must be specified before it can be guaranteed. diff --git a/docs/decisions/0035-use-actions-attest-as-initial-sigstore-signing-adapter.md b/docs/decisions/0035-use-actions-attest-as-initial-sigstore-signing-adapter.md new file mode 100644 index 0000000..d79ccd0 --- /dev/null +++ b/docs/decisions/0035-use-actions-attest-as-initial-sigstore-signing-adapter.md @@ -0,0 +1,233 @@ +--- +parent: Decisions +nav_order: 35 +status: accepted +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use actions/attest as the Initial Sigstore Signing Adapter + +## Context and Problem Statement + +ADR 0029 selected Windlass-generated SLSA provenance as the canonical provenance for the initial +production JS/TS npm package SLSA3 profile. That provenance must be signed as a Sigstore DSSE bundle +that can be submitted through npm's external `provenance-file` path. ADR 0029 intentionally left the +implementation boundary for signing open. + +The signing adapter choice affects the trusted core dependency set, GitHub Actions permissions, SLSA +Build L3 signing-key isolation story, verifier compatibility, and the long-term path for a Go +implementation. The project needs a production-friendly initial adapter without foreclosing a later +move to an in-process Go Sigstore implementation. + +Should the initial production profile use GitHub `actions/attest`, the `cosign` CLI, `sigstore-go`, +npm automatic provenance, or a custom Sigstore protocol implementation to sign Windlass-generated +SLSA provenance? + +## Decision Drivers + +- Preserve ADR 0029's Windlass-generated SLSA provenance as the canonical provenance record. +- Produce a Sigstore DSSE bundle using short-lived GitHub Actions OIDC identity rather than a + long-lived signing key. +- Keep signing-key material inaccessible to caller-controlled build steps, as required for SLSA + Build L3 provenance unforgeability. +- Minimize initial implementation risk while the repository is still defining architecture + specifications. +- Use a signing path compatible with GitHub reusable workflow signer identity verification. +- Keep workflow permissions explicit and narrowly scoped to the signing job. +- Avoid making npm automatic provenance the canonical builder provenance for the Windlass profile. +- Leave a clear future path toward a Go-native `sigstore-go` signing adapter when the trusted core + is ready to own that complexity. + +## Considered Options + +- Use GitHub `actions/attest` as the initial Sigstore signing adapter, with a future migration path + toward `sigstore-go`. +- Use `sigstore-go` directly in the initial Go trusted core. +- Invoke the `cosign` CLI from the reusable workflow. +- Rely on npm trusted publishing or `npm publish --provenance` automatic provenance. +- Implement the Sigstore protocol directly without a maintained client or action. + +## Decision Outcome + +Chosen option: "Use GitHub `actions/attest` as the initial Sigstore signing adapter, with a future +migration path toward `sigstore-go`", because it gives the initial GitHub Actions reusable workflow +a well-supported OIDC and Sigstore bundle path while deferring Go-native signing implementation work +until the trusted core has enough specification and test coverage to own it safely. + +The initial production JS/TS npm package SLSA3 profile should treat `actions/attest` as the signing +adapter for Windlass-generated SLSA provenance. Windlass remains responsible for generating and +validating the SLSA provenance statement selected by ADR 0029, including `subject`, `predicateType`, +`buildType`, `externalParameters`, `builder.id`, and other profile-defined fields. `actions/attest` +is responsible only for creating the Sigstore-signed attestation bundle and, when configured, +uploading it to GitHub's attestation storage. + +The reusable workflow should pin `actions/attest` to a full commit SHA, not a mutable tag. The +signing job should grant only the permissions required for the selected signing and storage +behavior: + +- `contents: read` for repository access; +- `id-token: write` for GitHub Actions OIDC token minting; +- `attestations: write` for GitHub artifact attestation persistence when that storage path is used; +- `artifact-metadata: write` only when linked artifact storage metadata is intentionally created. + +The production profile should not grant signing permissions to caller-controlled install, build, +pack, or publish-script steps. If signing and build execution are split across jobs, artifacts and +provenance inputs handed to the signing job must be authenticated by the trusted workflow design +described in the architecture specification. The signing adapter must not accept caller-provided +signing keys, long-lived Sigstore credentials, arbitrary OIDC tokens, or npm publish tokens as +provenance-signing credentials for the initial profile. + +Verifier guidance for the initial profile should include GitHub artifact attestation verification +where applicable, including reusable workflow signer identity checks such as `--signer-workflow` or +`--signer-repo`. npm package verification remains governed by ADR 0029's canonical package +provenance requirements: the verifier must check the Sigstore signature, expected GitHub Actions +signer identity, SLSA predicate type, Windlass `builder.id`, Windlass `buildType`, expected +`externalParameters`, and published package tarball digest. + +Long term, the project should prefer a `sigstore-go` adapter when the Go trusted core is ready to +implement and test Sigstore signing directly. A future migration ADR should define how to preserve +or intentionally change signer identity, bundle format, verifier behavior, npm `provenance-file` +compatibility, GitHub attestation storage behavior, and workflow permissions. If the migration +materially changes security properties, verifier expectations, or `builder.id` meaning, it should +use a distinct builder identity. + +### Consequences + +- Good, because the initial production profile gets a maintained GitHub-native Sigstore signing + path. +- Good, because the workflow can use short-lived GitHub Actions OIDC identity instead of managing a + long-lived signing key. +- Good, because GitHub artifact attestation verification can validate reusable workflow signer + identity directly. +- Good, because `actions/attest` keeps Fulcio, Rekor, timestamp, bundle, and GitHub attestation API + integration out of the first Go implementation milestone. +- Good, because a later `sigstore-go` implementation remains aligned with ADR 0004's Go trusted core + direction. +- Neutral, because GitHub artifact attestations may be secondary evidence for npm package releases + unless a later ADR changes the canonical distribution model. +- Bad, because `actions/attest` becomes part of the initial trusted core dependency set and must be + SHA-pinned, monitored, and updated deliberately. +- Bad, because the initial signing path is GitHub-specific and does not by itself support GitHub + Enterprise Server or non-GitHub build platforms. +- Bad, because a later `sigstore-go` migration may require verifier documentation and compatibility + testing even if the emitted bundle shape stays compatible. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, workflow documentation, and verifier guidance define: + +- `actions/attest` as the initial Sigstore signing adapter for Windlass-generated SLSA provenance; +- full-SHA pinning for the `actions/attest` action reference; +- job-level permissions limited to the required `contents: read`, `id-token: write`, + `attestations: write`, and optional `artifact-metadata: write` permissions; +- no caller-provided signing keys, Sigstore credentials, arbitrary OIDC tokens, npm publish tokens, + or inherited secrets for provenance signing; +- the exact handoff boundary between provenance statement generation, subject digest verification, + signing, npm `provenance-file` submission, and optional GitHub attestation upload; +- verifier guidance for expected GitHub Actions signer identity, Windlass `builder.id`, Windlass + `buildType`, SLSA predicate type, `externalParameters`, and package tarball digest; +- a documented future path for a `sigstore-go` adapter without requiring that implementation in the + initial profile. + +Implementation review should verify that signing permissions are not available to caller-controlled +build steps and that production releases cannot silently fall back to npm automatic provenance, +unsigned provenance, long-lived key signing, or a different signer identity under the same +production builder identity. + +## Pros and Cons of the Options + +### Use GitHub `actions/attest` as the initial signing adapter + +The reusable workflow uses a SHA-pinned `actions/attest` step to sign the Windlass-generated SLSA +statement with GitHub Actions OIDC-backed Sigstore identity and to produce a Sigstore bundle. + +- Good, because it is a GitHub-native path with first-party support for GitHub artifact + attestations. +- Good, because it uses short-lived OIDC identity and avoids local signing-key management. +- Good, because it provides a practical initial path for DSSE bundle generation before the Go + trusted core implements Sigstore signing directly. +- Good, because `gh attestation verify` supports reusable workflow signer checks. +- Bad, because it adds a pinned third-party action dependency to the trusted builder boundary. +- Bad, because its storage and verification conveniences are GitHub-specific. + +### Use `sigstore-go` directly in the initial Go trusted core + +The Go implementation calls Sigstore libraries directly to create and verify Sigstore bundles. + +- Good, because it aligns with ADR 0004's Go trusted core direction. +- Good, because it gives Windlass direct control over signing, bundle generation, error handling, + and tests. +- Good, because it can reduce workflow shell/action glue over time. +- Bad, because it requires the initial implementation to own OIDC acquisition, Fulcio/Rekor/TSA/TUF + behavior, failure handling, and compatibility testing immediately. +- Bad, because GitHub attestation storage and linked artifact metadata integration would still need + separate design. + +### Invoke the `cosign` CLI from the reusable workflow + +The workflow installs or vendors `cosign` and signs the provenance through `cosign attest-blob` or +an equivalent keyless command. + +- Good, because `cosign` is the widely recognized Sigstore CLI and supports keyless signing, + bundles, and broad verification workflows. +- Good, because it is less tied to GitHub artifact attestation storage than `actions/attest`. +- Bad, because installing, pinning, and verifying a CLI binary adds operational and supply-chain + surface. +- Bad, because `cosign` has a broad container- and registry-oriented feature set beyond the initial + npm package profile's needs. +- Bad, because GitHub artifact attestation API integration would be separate. + +### Rely on npm automatic provenance + +npm trusted publishing or `npm publish --provenance` creates npm ecosystem provenance automatically +instead of Windlass signing its own canonical provenance. + +- Good, because it is npm ecosystem-native and naturally discoverable through npm tooling. +- Good, because trusted publishing can generate provenance without an npm publish token. +- Bad, because it conflicts with ADR 0029's Windlass-generated canonical provenance decision. +- Bad, because it does not make Windlass `builder.id`, `buildType`, and `externalParameters` the + canonical verifier contract. +- Bad, because it would couple the production profile's SLSA semantics to npm's generated predicate + shape rather than the Windlass build type specification. + +### Implement the Sigstore protocol directly + +The project implements Fulcio, Rekor, timestamp, bundle, and trust-root interactions without using a +maintained Sigstore client or GitHub action. + +- Good, because it gives maximum implementation control. +- Bad, because it duplicates security-sensitive client behavior that maintained Sigstore tools + already provide. +- Bad, because direct protocol implementation increases compatibility, maintenance, and + vulnerability response burden. +- Bad, because it is unnecessary for the initial profile and would slow specification-driven + development. + +## More Information + +This decision follows ADR 0029 and decides only the initial implementation adapter for signing +Windlass-generated SLSA provenance. It does not change the canonical provenance contents, package +manager policy, npm trusted publishing authentication decision, private dependency credential +policy, or SHA-pinned reusable workflow builder identity. + +Reference points considered: + +- SLSA v1.2 Build Requirements require authentic provenance at Build L2 and unforgeable provenance + at Build L3. Secret material used to authenticate provenance must not be accessible to + user-defined build steps. +- SLSA v1.2 Build Provenance separates signer identity from `builder.id` and requires consumers to + accept only expected signer-builder pairs. +- GitHub `actions/attest` generates signed in-toto attestations with Sigstore-issued short-lived + certificates, writes Sigstore bundle JSON, and can upload attestations to GitHub's attestation + API. +- GitHub `actions/attest-build-provenance` v4 is a wrapper around `actions/attest`; new + implementations should use `actions/attest` directly. +- GitHub `gh attestation verify` documents reusable workflow verification through signer workflow or + signer repository checks and warns that predicate contents can be falsified if the workflow + context is compromised. +- `sigstore-go` is production-ready and provides Go APIs for signing and verifying Sigstore bundles, + but adopting it directly requires the Windlass trusted core to own more Sigstore behavior. +- `cosign` remains a stable Sigstore CLI with keyless signing, bundles, and attestation support, but + its CLI installation and broad feature surface are larger than the initial adapter needs. From 5336b80103ecdb2713a0eb9bdc4b6ca31ad88f35 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:11 +0900 Subject: [PATCH 03/14] docs(decisions): add ADRs for npm publish workflow constraints Constrain manual dispatch releases to version tags, run build scripts only when declared, and exclude private dependency credentials from the initial profile. Signed-off-by: Yunseo Kim --- ...anual-dispatch-releases-to-version-tags.md | 239 ++++++++++++++++++ ...033-run-build-script-only-when-declared.md | 200 +++++++++++++++ ...pendency-credentials-in-initial-profile.md | 237 +++++++++++++++++ 3 files changed, 676 insertions(+) create mode 100644 docs/decisions/0032-constrain-manual-dispatch-releases-to-version-tags.md create mode 100644 docs/decisions/0033-run-build-script-only-when-declared.md create mode 100644 docs/decisions/0034-do-not-support-private-dependency-credentials-in-initial-profile.md diff --git a/docs/decisions/0032-constrain-manual-dispatch-releases-to-version-tags.md b/docs/decisions/0032-constrain-manual-dispatch-releases-to-version-tags.md new file mode 100644 index 0000000..b51447a --- /dev/null +++ b/docs/decisions/0032-constrain-manual-dispatch-releases-to-version-tags.md @@ -0,0 +1,239 @@ +--- +parent: Decisions +nav_order: 32 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Constrain Manual Dispatch Releases to Version Tags + +## Context and Problem Statement + +ADR 0026 selected tag-push caller workflows plus constrained `workflow_dispatch` as the supported +production release caller patterns for the initial JS/TS npm package SLSA3 profile. It intentionally +left the exact manual-dispatch constraints open, including whether the selected ref must be a tag, +how the selected ref relates to the package version, and how reruns differ from new releases. + +ADR 0028 later required production consumers to execute the Windlass reusable workflow by full +commit SHA and to use SHA-based `builder.id` values. That decision does not weaken the caller +repository's release-ref requirements: package provenance still needs a clear source ref, release +intent, and external-parameter contract for downstream verification. + +Manual dispatch is operationally useful for recovering from transient runner, registry, or trusted +publishing failures. It is also risky if treated as an arbitrary release button, because a +maintainer could accidentally publish from a moving branch ref, provide a version input that +disagrees with the package metadata, or rerun a workflow after npm has already accepted an immutable +package version. + +Should production `workflow_dispatch` publishes require a tag ref, require the tag name to match the +package version, and define rerun semantics before npm publish? + +## Decision Drivers + +- Preserve ADR 0026's constrained manual dispatch support without broadening manual dispatch into an + arbitrary branch-based release mechanism. +- Keep package releases tied to immutable, protected, signed release tags rather than moving branch + refs or free-form version inputs. +- Preserve SLSA Build L3 expectations that externally controlled release inputs and invocation + context are complete and verifier-meaningful. +- Align with npm's immutable package name/version model: the same package version cannot be + republished as a new release. +- Distinguish a retry of the same release tag from a new package release. +- Ensure provenance can distinguish separate build invocations through GitHub run identity and run + attempt values. +- Avoid giving the production workflow permission or responsibility to create release tags. + +## Considered Options + +- Require manual dispatch to run on a tag ref, require `v${package.json.version}` tag matching, and + treat reruns or repeated dispatches as distinct invocations for the same release tag. +- Require manual dispatch to run on a tag ref, but only warn when the tag and package version + differ. +- Allow manual dispatch from branch refs with a required version input. +- Allow manual dispatch from branch refs and let the workflow create the release tag. +- Allow manual dispatch only for retry after a pre-publish failure. +- Remove manual dispatch support and allow tag-push releases only. + +## Decision Outcome + +Chosen option: "Require manual dispatch to run on a tag ref, require `v${package.json.version}` tag +matching, and treat reruns or repeated dispatches as distinct invocations for the same release tag", +because it preserves a practical manual recovery path while keeping production release identity tied +to immutable Git tags and npm package versions. + +The production JS/TS npm package SLSA3 profile should accept `workflow_dispatch` only when the +selected ref is a tag. The called workflow should fail before package packing, provenance +generation, or npm publish when `github.event_name == "workflow_dispatch"` and +`github.ref_type != "tag"`. Branch refs, default-branch manual runs, arbitrary commit-SHA dispatches +without a tag ref, and manual version inputs are unsupported for production publish under this +profile. + +The selected tag name must match the package version using the initial profile's release tag +convention: + +```text +v${package.json.version} +``` + +For example, tag `v1.2.3` must publish package version `1.2.3`, and tag `v1.2.3-beta.1` must publish +package version `1.2.3-beta.1`. The workflow should read the packed package metadata, not only the +working-tree `package.json`, when checking the final publish version. A mismatch between the +selected tag and the package version should fail clearly. + +Manual dispatch is a supported manual retry or approved release path for an existing release tag. It +is not a tag-creation path. The production profile should not create, move, sign, or delete release +tags. Tag creation, tag signing, protected tag policy, and release approval happen outside this +reusable workflow before the publish attempt. + +GitHub reruns and repeated manual dispatches for the same tag are distinct build invocations for the +same release tag, not new releases. Provenance and workflow outputs should distinguish invocations +with the GitHub run id and run attempt where available. The profile should include or derive a build +invocation identifier equivalent to: + +```text +${github.run_id}-${github.run_attempt} +``` + +The profile should fail clearly when the target npm package name and version are already published +in the selected registry. It should not silently skip publish, overwrite provenance, treat the rerun +as a successful new release, or attempt to republish the immutable npm version. If a previous +attempt failed before registry mutation, a rerun or repeated manual dispatch may publish the same +tag and package version. If the registry already contains that package version, verification or +inspection is the correct operation, not another production publish. + +### Consequences + +- Good, because manual dispatch keeps a supported operational recovery path without weakening the + release-ref boundary. +- Good, because package version, Git tag, and provenance source ref remain aligned. +- Good, because branch refs and free-form version inputs cannot become production release sources in + the initial SLSA3 profile. +- Good, because reruns are represented as distinct build invocations instead of ambiguous duplicate + releases. +- Good, because the production publish workflow does not need `contents: write` permission to create + or move tags. +- Neutral, because callers still need repository-level protections such as signed/protected tags and + protected environments where appropriate. +- Bad, because users must create the release tag before manual dispatch. +- Bad, because custom release tag conventions are out of scope until a later ADR or profile revision + changes the tag/version contract. +- Bad, because checking already-published package versions requires registry interaction and careful + error reporting. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, documentation, and verifier guidance define: + +- `workflow_dispatch` production publishes as tag-ref-only manual retries or approved release paths; +- failure before pack, provenance generation, or publish when manual dispatch is not running on a + tag ref; +- the initial release tag convention `v${package.json.version}`; +- package-version checks against the packed artifact metadata; +- failure behavior for tag/package-version mismatches; +- no workflow-created, moved, signed, or deleted release tags in the production publish profile; +- provenance or byproduct fields that include event name, ref, ref type, ref SHA, run id, run + attempt, actor, and triggering actor where available; +- build invocation identity that distinguishes reruns, such as + `${github.run_id}-${github.run_attempt}`; +- clear failure when the package name/version is already published; +- documentation that verification or inspection, not republish, is the correct path after a package + version has already been accepted by the registry. + +Implementation review should verify that branch-based manual dispatch, arbitrary version inputs, +workflow-created tags, and already-published package versions cannot proceed to production npm +publish through the initial SLSA3 profile. + +## Pros and Cons of the Options + +### Require tag ref, require tag/version match, and define reruns as same-release invocations + +Manual dispatch is allowed only when the selected ref is a tag whose name equals +`v${package.json.version}`. Reruns and repeated dispatches produce distinct build invocations for +the same release tag. + +- Good, because this preserves tag-push release semantics while allowing manual recovery. +- Good, because verifier expectations can rely on a stable tag-to-package-version relationship. +- Good, because npm's immutable version model is respected. +- Good, because run id and run attempt can uniquely identify each build invocation. +- Bad, because it reduces flexibility for projects with nonstandard tag naming. +- Bad, because manual dispatch cannot create a release from a branch without prior tag creation. + +### Require tag ref but only warn on tag/version mismatch + +Manual dispatch must use a tag ref, but the workflow only warns when the tag and package version do +not match. + +- Good, because it supports projects whose Git tags and package versions intentionally differ. +- Good, because it is easier to adopt during migration. +- Bad, because accidental mismatches can still publish a wrong package version. +- Bad, because release notes, npm version, Git tag, and verifier expectations can diverge. +- Bad, because a warning is too weak for a production registry mutation in the SLSA3 profile. + +### Allow branch refs with a required version input + +Manual dispatch may run from a branch ref when the caller provides an explicit version input. + +- Good, because the GitHub UI flow is convenient. +- Good, because maintainers can initiate releases before creating tags. +- Bad, because branch refs are moving release sources. +- Bad, because version input becomes another externally controlled parameter that can disagree with + package metadata. +- Bad, because tag protection and signed-tag release integrity can be bypassed. + +### Allow branch refs and let the workflow create the tag + +Manual dispatch runs on a branch, derives or accepts a version, creates the release tag, then +publishes. + +- Good, because it can provide a one-button release workflow. +- Good, because tag creation and publish can be ordered by automation. +- Bad, because the publish workflow needs source-control write permissions. +- Bad, because tag signing, protected tags, release approval, and source mutation become coupled to + the package publish path. +- Bad, because this gives the reusable workflow a broader trust boundary than registry publishing. + +### Allow manual dispatch only after a pre-publish failure + +Manual dispatch is accepted only when it can prove an earlier attempt failed before registry +mutation. + +- Good, because this is the narrowest retry semantics. +- Good, because it minimizes duplicate release attempts. +- Bad, because proving exact prior failure state across GitHub Actions and the registry is brittle. +- Bad, because registry/network failures can leave ambiguous publish status. +- Bad, because the workflow still needs a simple already-published-version failure rule. + +### Remove manual dispatch and support tag-push only + +Only pushed tags can publish production npm packages. + +- Good, because the release trigger model is simplest. +- Good, because verifier expectations and documentation are concise. +- Good, because manual misuse risk is minimized. +- Bad, because maintainers lack a supported recovery path for transient release infrastructure + failures. +- Bad, because users may create unsupported wrapper workflows to recover from failures. + +## More Information + +This decision follows ADR 0026 and decides the remaining manual-dispatch constraints for the initial +production JS/TS npm package profile. It does not change the reusable workflow entrypoint, the +SHA-pinned builder identity requirement from ADR 0028, the release manifest decision from ADR 0031, +npm trusted publishing requirements, registry scope, or future non-publish validation workflows. + +Reference points considered: + +- SLSA v1.2 Build Requirements require a consistent build process and complete, trustworthy + provenance generation for the chosen build level. +- SLSA v1.2 Verifying Artifacts recommends checking trusted `builder.id`, `buildType`, and expected + `externalParameters` against verifier expectations. +- SLSA GitHub Generator's Node.js builder documents `workflow_dispatch` as a supported trigger while + showing release-oriented examples that guard on tag refs. +- SLSA GitHub Generator's Node.js provenance records GitHub event context such as event name, ref, + ref type, run id, and run attempt, and uses run id plus run attempt to distinguish reruns. +- GitHub manual `workflow_dispatch` runs can be started against a selected ref, so a production + release profile must define which selected refs are valid. +- npm package versions are immutable after publication, so rerunning a successful publish should not + be treated as a new production release. diff --git a/docs/decisions/0033-run-build-script-only-when-declared.md b/docs/decisions/0033-run-build-script-only-when-declared.md new file mode 100644 index 0000000..a374f69 --- /dev/null +++ b/docs/decisions/0033-run-build-script-only-when-declared.md @@ -0,0 +1,200 @@ +--- +parent: Decisions +nav_order: 33 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Run Build Script Only When Declared + +## Context and Problem Statement + +ADR 0014 selected npm, pnpm, and Yarn as the supported package managers for install, build, and pack +stages in the initial JS/TS npm package profile. ADR 0015 through ADR 0017 defined package-manager +selection, Corepack usage, and exact version enforcement. ADR 0019 selected source metadata +validation followed by packed artifact inspection, and ADR 0023 rejected arbitrary install, build, +publish, environment, step, service, and default inputs for the production SLSA3 profile. + +The remaining build-stage contract needs to define what the reusable workflow does between +dependency installation and package packing. In particular, npm packages vary widely: some require a +build script before packing, while others are source-only, type-only, configuration-only, or rely on +package manager lifecycle scripts that run during pack. Requiring every package to define +`scripts.build` would make release intent explicit but would reject many valid npm packages. +Skipping a build script entirely would improve compatibility but would make it easier to publish +packages that forgot to run their expected build. + +Should the initial production JS/TS npm package profile require a build script, skip build entirely, +or run a declared build script while treating an absent build script as an intentional no-op? + +## Decision Drivers + +- Preserve the production SLSA3 profile's small public input surface and avoid caller-provided + arbitrary commands. +- Keep install, build, and pack behavior deterministic and package-manager selected from repository + metadata rather than workflow inputs. +- Support common npm packages that do not need a build step before packing. +- Run a package's declared build step when the package declares one. +- Make the build-step result visible to provenance and verifier policy without making the builder a + package quality linter. +- Avoid silently expanding the trusted workflow boundary into monorepo orchestration, task-runner + selection, or caller-defined shell execution. +- Keep future stricter modes possible through a later ADR or distinct builder identity. + +## Considered Options + +- Use fixed package-manager commands and require `scripts.build`. +- Use fixed package-manager commands, run `scripts.build` when present, and treat an absent build + script as a successful no-op. +- Use fixed package-manager commands and require an explicit package metadata marker when no build + script is present. +- Allow a caller-provided build command. +- Omit a separate build stage and rely only on package-manager pack or publish lifecycle behavior. + +## Decision Outcome + +Chosen option: "Use fixed package-manager commands, run `scripts.build` when present, and treat an +absent build script as a successful no-op", because it preserves the trusted workflow boundary while +supporting valid npm packages that do not require a build step. + +The initial production JS/TS npm package profile should use builder-defined commands for install, +build, and pack stages. The concrete command forms for each supported package manager belong in the +JS/TS npm package profile architecture specification, but the public contract should follow these +rules: + +- the selected package manager is determined by ADR 0015 through ADR 0017; +- dependency installation uses the selected package manager's frozen or immutable install behavior; +- the build stage checks the selected source package manifest for `scripts.build`; +- if `scripts.build` is present, the workflow runs the selected package manager's normal `build` + script command for the selected package; +- if `scripts.build` is absent, the build stage succeeds as an explicit no-op; +- package packing then uses the selected package manager's pack behavior, and packed artifact + inspection remains authoritative as decided by ADR 0019; +- final registry publishing continues to use `npm publish` as decided by ADR 0013 and ADR 0029. + +The production profile should not expose `install-command`, `build-command`, `pack-command`, +`publish-command`, task-runner, arbitrary shell, arbitrary environment, arbitrary step, service, or +defaults inputs. Packages that need Turbo, Nx, Changesets, Make, or another project-specific build +orchestration layer should invoke that orchestration through their declared `scripts.build` rather +than through a reusable workflow command override. + +The workflow should record whether the build script was present, whether it was executed or skipped, +and the selected package manager used for the build stage in logs and verifier-relevant provenance +or byproduct fields where appropriate. An absent build script should be represented as "skipped +because absent" or an equivalent explicit value, not hidden as an unobservable implementation +detail. + +This decision does not guarantee that the package's consumer surface is correct. It does not +semantically validate `exports`, `main`, `bin`, TypeScript declarations, generated files, or package +quality beyond the metadata and packed artifact checks selected by ADR 0019. Repositories that +require tests, type checks, linting, or stricter release validation should run those checks before +calling the production publish profile, or use a future validation profile if one is defined. + +### Consequences + +- Good, because packages that declare a build script get a real build stage before pack. +- Good, because valid packages without a build script are not forced to add a meaningless script. +- Good, because caller workflows cannot inject arbitrary trusted build commands through workflow + inputs. +- Good, because monorepo-specific build tools can still be used behind the package's own + `scripts.build` contract. +- Good, because provenance can distinguish an executed build from an intentionally skipped build. +- Neutral, because packages that require tests or type checks must enforce those outside this + publish profile unless a later validation profile is added. +- Bad, because the builder cannot detect every accidental omission of `scripts.build`. +- Bad, because some repositories may need to add or adjust `scripts.build` to fit the fixed profile + contract. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, documentation, and verifier guidance define: + +- exact install commands for npm, pnpm, and Yarn using frozen or immutable dependency installation; +- exact build script invocation commands for npm, pnpm, and Yarn when `scripts.build` is present; +- explicit no-op success behavior when `scripts.build` is absent; +- exact pack commands for npm, pnpm, and Yarn; +- stage ordering of install, optional build, pack, packed artifact inspection, provenance + generation, and publish; +- provenance or byproduct fields that identify build script presence, execution or skip status, + selected package manager, and selected package-manager version; +- documentation that package tests, type checks, linting, and consumer-surface validation are + outside this publish profile unless encoded in `scripts.build` or provided by another workflow; +- rejection of arbitrary command, environment, step, service, task-runner, and default inputs in the + production SLSA3 profile. + +Implementation review should verify that the production workflow cannot be made to execute +caller-provided install, build, pack, or publish commands through workflow inputs, and that an +absent `scripts.build` is observable as an explicit skipped build rather than being silently +conflated with an executed build. + +## Pros and Cons of the Options + +### Use fixed commands and require `scripts.build` + +The profile runs fixed package-manager install, build, and pack commands, and fails when the +selected package does not declare `scripts.build`. + +- Good, because every release artifact has an explicit build command. +- Good, because a missing expected build script fails before publish. +- Good, because verifier guidance can treat build execution as mandatory. +- Bad, because valid no-build npm packages need dummy scripts or cannot use the initial profile. +- Bad, because the profile becomes a stronger package-shape gate than npm itself. + +### Use fixed commands, run build when present, and no-op when absent + +The profile runs fixed package-manager install and pack commands. It runs the selected package's +normal build script only when `scripts.build` is present; otherwise the build stage succeeds as an +explicit no-op. + +- Good, because it follows common npm package practice without exposing arbitrary commands. +- Good, because build execution remains package-owned through `scripts.build`. +- Good, because no-build packages remain supported. +- Good, because provenance can record whether build was executed or skipped. +- Bad, because a package that forgot to declare `scripts.build` may still publish. + +### Require an explicit no-build package metadata marker + +The profile runs `scripts.build` when present and fails without a build script unless the package +declares a Windlass-specific no-build marker. + +- Good, because a skipped build is explicitly declared by the package author. +- Good, because accidental build omission is less likely than with a default no-op. +- Bad, because it introduces Windlass-specific package metadata into npm packages. +- Bad, because the initial profile and verifier schema become more complex. +- Bad, because it may surprise maintainers whose package already has valid npm semantics. + +### Allow a caller-provided build command + +The reusable workflow accepts a workflow input such as `build-command` and executes it before pack. + +- Good, because it can support complex monorepo, task-runner, and migration workflows. +- Good, because callers can preserve existing release commands more easily. +- Bad, because caller-controlled command execution weakens the production trusted workflow boundary. +- Bad, because verifier-relevant behavior becomes much harder to constrain. +- Bad, because it conflicts with the current small input surface and no arbitrary command decisions. + +### Omit a separate build stage + +The profile only installs dependencies and packs the package, relying on package-manager lifecycle +scripts such as `prepack`, `prepare`, or equivalent pack behavior. + +- Good, because it stays close to native package-manager pack behavior. +- Good, because the reusable workflow has one fewer explicit stage. +- Bad, because build intent is less visible in provenance and documentation. +- Bad, because packages that conventionally use `scripts.build` before pack would not be built + unless they also wire build behavior into lifecycle scripts. +- Bad, because package-manager lifecycle differences become more important to the trusted profile + contract. + +## More Information + +This decision follows ADR 0014 through ADR 0019 and ADR 0023. It decides the build-stage policy for +the initial production JS/TS npm package profile only. It does not change package-manager selection, +Corepack enforcement, packed artifact validation, publish authentication, provenance generation, +registry scope, release trigger policy, or SHA-pinned builder identity. + +The architecture specification should still define the exact npm, pnpm, and Yarn command lines, +including frozen install flags, workspace package invocation behavior, pack output parsing, +lifecycle script expectations, and failure handling for unsupported package-manager versions or +package layouts. diff --git a/docs/decisions/0034-do-not-support-private-dependency-credentials-in-initial-profile.md b/docs/decisions/0034-do-not-support-private-dependency-credentials-in-initial-profile.md new file mode 100644 index 0000000..6267517 --- /dev/null +++ b/docs/decisions/0034-do-not-support-private-dependency-credentials-in-initial-profile.md @@ -0,0 +1,237 @@ +--- +parent: Decisions +nav_order: 34 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Do Not Support Private Dependency Credentials in the Initial Profile + +## Context and Problem Statement + +ADR 0024 selected npm trusted publishing with GitHub Actions OIDC and no npm publish secret for the +initial production JS/TS npm package SLSA3 profile. That decision intentionally left private +dependency installation credentials undecided. npm trusted publishing authenticates `npm publish`; +it does not authenticate `npm install`, `npm ci`, `pnpm install`, or `yarn install` when those +commands need to fetch private packages. + +The npm ecosystem commonly handles private dependency installation in CI with read-only granular +access tokens. npm documentation recommends trusted publishing for publish operations, while also +recommending read-only granular tokens when CI needs to install private npm packages. SLSA v1.2 does +not require hermetic builds, and dependency fetching during a build is allowed. However, the +production SLSA3 builder identity selected for this project has a small, verifier-oriented public +contract: workflow inputs are intentionally constrained, publish tokens are forbidden, and modes +with materially different security properties require distinct builder identities. + +Allowing dependency-fetch credentials inside the initial reusable workflow would add a +secret-bearing mode to the same production profile. Even if the credential is intended to be +read-only, install-time lifecycle scripts and package-manager behavior can observe the install +environment. The workflow also cannot prove every caller-provided token is read-only, short-lived, +registry-scoped, or unable to publish. Private dependency usage can also make downstream verifier +checks less reproducible because dependency metadata or tarballs may not be publicly resolvable. + +Should the initial production JS/TS npm package SLSA3 profile support read-only private dependency +credentials, reject them entirely, or split private-dependency support into a future distinct +profile or builder mode? + +## Decision Drivers + +- Preserve ADR 0024's no-publish-secret invariant and avoid confusing dependency-fetch credentials + with publish credentials. +- Keep the initial production profile's `workflow_call.secrets` contract empty. +- Preserve a small and easily verifiable `externalParameters` and secret surface for the initial + SLSA3 builder identity. +- Avoid exposing long-lived or caller-controlled credentials to install-time package lifecycle + scripts in the initial production profile. +- Keep private dependency and private registry semantics out of the first npmjs-focused production + compatibility surface. +- Align with SLSA v1.2's guidance that `externalParameters` should be complete and minimized, and + that different security modes should be distinguishable through builder identity. +- Leave a deliberate future path for read-only dependency credential support under a distinct trust + contract if needed. + +## Considered Options + +- Do not support private dependency credentials in the initial production profile, and define a + future path for a distinct credential-bearing profile or mode. +- Allow one optional read-only npm dependency token secret in the initial production profile. +- Allow registry- or scope-specific read-only credential maps for npm-compatible registries. +- Support only OIDC or brokered short-lived credentials for dependency reads. +- Allow `secrets: inherit` or arbitrary environment passthrough for dependency installation. + +## Decision Outcome + +Chosen option: "Do not support private dependency credentials in the initial production profile, and +define a future path for a distinct credential-bearing profile or mode", because it keeps the first +production SLSA3 builder identity small, tokenless, and straightforward to verify while leaving room +for a separately specified private-dependency mode later. + +The initial production JS/TS npm package SLSA3 profile should not define any `workflow_call.secrets` +for dependency installation. It should not accept `npm-read-token`, `NPM_READ_TOKEN`, `NPM_TOKEN`, +`NODE_AUTH_TOKEN`, GitHub Packages tokens, registry tokens, cloud credentials, `.npmrc` secret +contents, `.yarnrc.yml` secret contents, or equivalent dependency-fetch credentials through the +production reusable workflow contract. + +The profile should not support `secrets: inherit`, arbitrary environment passthrough, arbitrary +registry credential maps, or caller-provided package-manager auth configuration as part of the +initial production SLSA3 profile. If the selected package's install step requires private dependency +authentication, the workflow should fail clearly rather than prompting users to pass a token, +falling back to a publish token, or silently changing the builder security mode. + +This decision does not claim that private dependency credentials are incompatible with SLSA in +general. SLSA v1.2 does not require hermetic builds, and read-only dependency credentials can be a +reasonable CI practice when they are scoped, protected, and documented. The decision is narrower: +the initial Windlass production JS/TS npm package profile will not include that secret-bearing +behavior under the same builder identity. + +A future ADR may add private dependency support through a separate reusable workflow, separate mode, +or revised profile contract. If the future mode exposes dependency-fetch credentials to install-time +commands, it should use a distinct builder identity when its security properties, verifier +expectations, or secret exposure differ materially from the initial tokenless profile. That future +decision should define at least: + +- which registries and package managers are supported; +- whether credentials are long-lived read-only tokens, OIDC-exchanged short-lived tokens, or + brokered credentials; +- how credential names avoid confusion with publish credentials; +- how credentials are scoped to install only and removed before build, pack, provenance generation, + and publish stages where possible; +- what credential mode, registry scope, and dependency-fetch policy are recorded in provenance or + byproducts without recording secret values; +- how verifier policy can accept or reject private-dependency credential mode; +- how package-manager lifecycle scripts, logs, caches, and artifact handoff avoid leaking secrets. + +### Consequences + +- Good, because the initial production profile keeps an empty `workflow_call.secrets` surface. +- Good, because dependency-fetch credentials cannot be confused with publish credentials in the + initial reusable workflow contract. +- Good, because install-time lifecycle scripts cannot access caller-provided registry tokens through + this profile. +- Good, because verifier expectations for the first builder identity remain smaller and easier to + document. +- Good, because future private-dependency support can receive its own ADR, tests, documentation, and + possibly distinct builder identity. +- Neutral, because SLSA itself permits non-hermetic dependency fetching and best-effort dependency + recording; this decision is a product-scope constraint rather than a SLSA prohibition. +- Bad, because packages that require private dependency authentication cannot use the initial + production profile without changing their dependency model. +- Bad, because some users may need to wait for a future credential-bearing profile or publish + through a less constrained workflow outside the initial SLSA3 profile. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +implementation, documentation, and verifier guidance define: + +- no `workflow_call.secrets` entries for dependency installation credentials; +- no accepted `npm-read-token`, `NPM_READ_TOKEN`, `NPM_TOKEN`, `NODE_AUTH_TOKEN`, GitHub Packages + token, registry token, cloud credential, `.npmrc` secret, `.yarnrc.yml` secret, or equivalent + dependency-fetch credential in the production profile; +- no `secrets: inherit`, arbitrary environment passthrough, or arbitrary registry credential map in + caller examples or supported workflow contract; +- clear failure behavior when dependency installation requires credentials not available through the + selected public dependency configuration; +- documentation that private dependency installation is unsupported in the initial production + profile and may require a future distinct profile or mode; +- verifier guidance that does not treat private dependency credential behavior as part of the + initial tokenless `buildType` contract. + +Implementation review should verify that dependency installation cannot receive caller-provided +secrets through the production SLSA3 workflow API and that no implementation path falls back to an +npm publish token or inherited secret for installing private dependencies. + +## Pros and Cons of the Options + +### Do not support private dependency credentials in the initial production profile + +The production reusable workflow exposes no dependency credential secrets. Packages whose install +stage requires private dependency authentication fail clearly under the initial profile. + +- Good, because the secret surface stays empty and easy to audit. +- Good, because the initial profile remains aligned with ADR 0024's tokenless publish direction. +- Good, because verifier policy does not need to reason about secret-bearing dependency-fetch modes. +- Good, because a future credential-bearing mode can be isolated under a distinct trust contract. +- Bad, because private-dependency packages are outside the first production compatibility surface. +- Bad, because adoption is harder for organizations with internal npm packages. + +### Allow one optional read-only npm dependency token secret + +The profile accepts a single optional secret intended only for installing private npm dependencies. + +- Good, because it matches npm's documented CI guidance for private dependencies. +- Good, because many real npm packages could adopt the profile without changing dependency hosting. +- Good, because publish authentication can remain OIDC-only with no publish token fallback. +- Bad, because install-time scripts and package-manager behavior can observe the token environment. +- Bad, because the workflow cannot fully prove the token is read-only, short-lived, or narrowly + scoped. +- Bad, because the same builder identity would cover both tokenless and secret-bearing install + behavior unless split by a later decision. + +### Allow registry- or scope-specific read-only credential maps + +The profile supports multiple private registries or scopes through explicit credential mappings. + +- Good, because it fits enterprise npm, GitHub Packages, Artifactory, Verdaccio, and mixed-scope + dependency layouts. +- Good, because registry-specific policy can be represented more precisely than a single token. +- Bad, because initial public API and verifier expectations become much larger. +- Bad, because custom registry semantics are outside ADR 0030's guaranteed npmjs production surface. +- Bad, because multiple credentials increase leakage and misconfiguration risks. + +### Support only OIDC or brokered short-lived dependency credentials + +The profile supports private dependency reads only through OIDC exchange or a credential broker +rather than long-lived read tokens. + +- Good, because it follows the same short-lived credential direction as trusted publishing. +- Good, because it can reduce long-lived secret exposure if registry support exists. +- Bad, because npm private dependency install does not have the same simple trusted-publishing OIDC + path as `npm publish`. +- Bad, because registry support and broker design would require a larger separate architecture + decision. +- Bad, because it is too broad for the initial profile's first production specification. + +### Allow `secrets: inherit` or arbitrary environment passthrough + +The caller can pass all secrets or arbitrary environment variables into the reusable workflow so its +normal install commands can access whatever credentials the caller uses. + +- Good, because it is highly flexible for existing workflows. +- Good, because it can support almost any private dependency setup. +- Bad, because the workflow cannot enumerate or verify all external influences on the build. +- Bad, because publish tokens, cloud tokens, and unrelated secrets may enter the build environment. +- Bad, because this conflicts with the small input and secret surface required for the initial + production SLSA3 profile. + +## More Information + +This decision follows ADR 0024 and decides only private dependency installation credentials for the +initial production JS/TS npm package profile. It does not change publish authentication, +Windlass-generated provenance, supported package managers, package-manager install command policy, +registry URL scope, release trigger policy, or SHA-pinned builder identity. + +Reference points considered: + +- SLSA v1.2 Build Requirements require security best practices for secrets and require SLSA Build L3 + provenance to be strongly resistant to tenant forgery. Secret material used for provenance + authenticity must not be accessible to user-defined build steps. +- SLSA v1.2 Build Requirements do not require hermetic builds. Dependency fetching is allowed, and + resolved dependency completeness is best effort at Build L3. +- SLSA v1.2 Build Provenance defines `externalParameters` as the externally controlled build + interface, requires those parameters to be complete at Build L3, and recommends minimizing their + size and complexity for verifier usability. +- SLSA v1.2 Build Provenance says modes with materially different security attributes or SLSA Build + levels must have different `builder.id` values and should have different signer identities. +- npm trusted publishing documentation recommends trusted publishing for publish operations while + recommending read-only granular access tokens when CI needs to install private npm packages. +- npm documentation notes that trusted publishing applies to publish operations; private dependency + install commands still require traditional authentication when private packages are used. +- GitHub Actions documentation notes that secrets are not automatically passed to reusable + workflows, that callers can pass named secrets or use `secrets: inherit`, and that commit SHA + references are safest for reusable workflows. +- The SLSA GitHub Generator Node.js builder exposes inputs such as `run-scripts` but does not define + a dependency-read token secret on its reusable workflow. Its publish examples pass publish tokens + in a separate publishing path rather than making private dependency credentials part of the + builder contract. From 2e451a60e395ff55f585ebdd34343c738448c945 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:12 +0900 Subject: [PATCH 04/14] docs(decisions): add ADRs for npm publish job graph and composition Establish a three-job digest-verified publish graph and compose the npm package tarball producer with the GitHub release asset publisher. Signed-off-by: Yunseo Kim --- ...three-job-digest-verified-publish-graph.md | 269 ++++++++++++++++++ ...l-producer-with-release-asset-publisher.md | 219 ++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 docs/decisions/0036-use-three-job-digest-verified-publish-graph.md create mode 100644 docs/decisions/0052-compose-npm-package-tarball-producer-with-release-asset-publisher.md diff --git a/docs/decisions/0036-use-three-job-digest-verified-publish-graph.md b/docs/decisions/0036-use-three-job-digest-verified-publish-graph.md new file mode 100644 index 0000000..1d206ab --- /dev/null +++ b/docs/decisions/0036-use-three-job-digest-verified-publish-graph.md @@ -0,0 +1,269 @@ +--- +parent: Decisions +nav_order: 36 +status: accepted +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use a Three-Job Digest-Verified Publish Graph + +## Context and Problem Statement + +ADR 0029 selected Windlass-generated SLSA provenance as the canonical npm package provenance, ADR +0035 selected `actions/attest` as the initial Sigstore signing adapter, and ADR 0024 selected OIDC +trusted publishing without npm publish secrets. Together, those decisions require a workflow graph +that builds a package, signs provenance for the exact packed artifact, and mutates the npm registry +only after the signed provenance and package artifact have been checked. + +The open boundary is how the initial production JS/TS npm package SLSA3 reusable workflow should +split build, provenance-signing, and publish responsibilities across GitHub Actions jobs. The split +must preserve SLSA Build L3 signing-key isolation, keep caller-controlled package scripts away from +signing and publish permissions, and make the publish step verify the exact tarball and signed +provenance bundle it submits to npm. + +Should the initial production profile use one job, two jobs, three jobs, four jobs, or leave publish +to the caller after a build-and-sign reusable workflow? + +## Decision Drivers + +- Keep caller-controlled install, build, pack, and package lifecycle scripts away from provenance + signing permissions and npm registry mutation capability. +- Preserve SLSA v1.2 Build L3's requirement that provenance authentication material is not + accessible to user-defined build steps. +- Make each cross-job artifact handoff digest-verified rather than name- or path-trusted. +- Require the publish job to verify both the packed artifact and signed provenance bundle before npm + registry mutation. +- Keep Windlass-generated SLSA provenance, not unsigned workflow outputs, as the source of truth for + verifier policy. +- Keep `actions/attest` signing permissions scoped to a signing job, as selected by ADR 0035. +- Keep npm trusted publishing authentication separate from provenance generation and signing. +- Follow the SLSA GitHub Generator precedent of reusable workflow isolation, job-level VM isolation, + and hash-protected artifact handoff. +- Define a job graph that is precise enough for architecture specifications and implementation + tests. + +## Considered Options + +- Use a three-job digest-verified graph: `build` → `provenance-sign` → `publish`. +- Use one job for build, provenance signing, and publish. +- Use two jobs: `build` → `publish-sign`. +- Use four jobs: `build` → `provenance-generate` → `sign` → `publish`. +- Build and sign inside the reusable workflow, then leave publish to the caller workflow. + +## Decision Outcome + +Chosen option: "Use a three-job digest-verified graph: `build` → `provenance-sign` → `publish`", +because it gives the initial production profile clear job isolation, minimal permission scopes, and +a publish-time verification gate for both the package tarball and signed provenance bundle before +npm registry mutation. + +The initial production JS/TS npm package SLSA3 reusable workflow should define three primary jobs: + +```text +build -> provenance-sign -> publish +``` + +The `build` job should run the caller-selected package build contract, produce the packed npm +tarball, validate package metadata from the packed artifact, calculate the package tarball `sha256` +and `sha512` digests, upload the tarball as a workflow artifact, and expose only the package +identity and digest handles needed by later jobs and public workflow outputs. + +The `provenance-sign` job should download the package tarball artifact, recalculate its digest, +compare it with the `build` job's expected digest outputs, generate the Windlass SLSA provenance v1 +statement for exactly that tarball, verify that the statement subject matches the tarball digest, +sign the statement through the SHA-pinned `actions/attest` adapter selected by ADR 0035, and upload +the resulting signed Sigstore bundle as a workflow artifact. The signing job should not have npm +publish credentials or npm registry mutation authority. + +The `publish` job should download the package tarball and signed provenance bundle artifacts, +recalculate both artifact digests, verify that those digests match the handoff values produced by +the previous jobs, verify the signed provenance bundle against the expected Sigstore root and GitHub +Actions reusable workflow signer identity, extract and verify the SLSA statement expectations, and +only then run `npm publish --provenance-file ` or the equivalent +npm external provenance-file submission. The publish job should fail before registry mutation if any +digest, signature, signer identity, package identity, subject, `predicateType`, `builder.id`, +`buildType`, `externalParameters`, source identity, package-directory, or runtime-policy check +fails. + +The initial job permission boundary should be: + +- `build`: `contents: read`; no `id-token: write`; no `attestations: write`; no npm publish + authentication; no publish secrets. +- `provenance-sign`: `contents: read`, `id-token: write`, `attestations: write` when GitHub + attestation storage is used, and optional `artifact-metadata: write` only when linked artifact + metadata is intentionally created; no npm publish authentication; no registry mutation authority. +- `publish`: `contents: read` and only the OIDC or registry permissions required for npm trusted + publishing; no `attestations: write` unless a later ADR adds post-publish attestation behavior. + +Cross-job handoff should use immutable GitHub workflow artifacts and trusted job outputs as handles. +The architecture specification should define exact artifact names or IDs, digest encodings, digest +algorithms, download locations, and failure behavior. Jobs must not trust artifact names, paths, +workflow outputs, or logs as substitutes for recalculating digests and verifying the signed +provenance bundle. Unsigned public workflow outputs such as package identity and tarball digest +remain release handles only; they are not substitutes for signed provenance verification. + +After publishing, the `publish` job should resolve npm registry metadata for the published package +version when possible and verify that the registry-visible package identity, tarball URL or name, +integrity or digest metadata, and provenance linkage correspond to the verified tarball and +submitted Windlass provenance bundle. The architecture specification may refine which npm registry +metadata is mandatory versus best-effort when npm registry behavior or tooling support changes. + +### Consequences + +- Good, because package build scripts cannot access provenance signing or npm publish authority. +- Good, because `actions/attest` permissions are scoped to the signing job rather than the build + job. +- Good, because npm registry mutation is gated by digest checks and signed provenance verification. +- Good, because the job graph mirrors SLSA GitHub Generator's isolated-job and hash-protected + handoff model while keeping Windlass-specific provenance semantics. +- Good, because architecture specifications can assign clear inputs, outputs, permissions, and + failure behavior to each job. +- Good, because the future `sigstore-go` migration path can replace signing internals without + collapsing the build/publish boundary. +- Neutral, because the publish job performs verifier-like checks inside the producer workflow; final + consumers should still verify published artifacts independently. +- Bad, because the workflow has more jobs, handoff artifacts, and verification code than a one- or + two-job design. +- Bad, because Windlass must maintain tooling to verify its own signed bundle before publish. +- Bad, because artifact handoff implementation must be tested carefully to avoid name/path + confusion, stale artifacts, digest encoding drift, or accidental fallback behavior. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- exactly three primary production jobs named or equivalent to `build`, `provenance-sign`, and + `publish`; +- the job-level permissions and absence of signing or publish authority from caller-controlled build + steps; +- package tarball artifact upload, download, digest calculation, and digest comparison behavior; +- signed provenance bundle artifact upload, download, digest calculation, and digest comparison + behavior; +- Windlass SLSA statement generation and subject digest verification before signing; +- Sigstore bundle verification in the publish job before `npm publish`; +- publish-time checks for signer identity, `predicateType`, `builder.id`, `buildType`, + `externalParameters`, source identity, package identity, package directory, runtime policy, and + tarball digest; +- `npm publish --provenance-file` or equivalent external provenance-file submission using only the + verified tarball and verified signed bundle paths; +- post-publish npm registry metadata checks and documented best-effort versus mandatory behavior; +- hard-failure behavior for missing artifacts, digest mismatch, signature mismatch, unexpected + provenance fields, unsupported registry metadata, and any attempted fallback to npm automatic + provenance, unsigned provenance, token-based publish, or caller-managed publish. + +Implementation review should verify that no path can publish a package under the production SLSA3 +builder identity unless the package tarball and signed Windlass provenance bundle have both passed +the publish job's verification gate. + +## Pros and Cons of the Options + +### Use a three-job digest-verified graph + +The reusable workflow separates package creation, provenance signing, and npm registry mutation into +`build`, `provenance-sign`, and `publish` jobs. Each cross-job artifact handoff is checked by +digest, and the publish job verifies the signed provenance before publishing. + +- Good, because it creates the clearest permission and trust boundary for the initial profile. +- Good, because build scripts cannot access signing credentials or publish authority. +- Good, because the publish job can behave like a producer-side verifier before registry mutation. +- Good, because it follows the SLSA GitHub Generator pattern of job isolation plus hash-protected + artifact exchange. +- Good, because it keeps Windlass-generated provenance, not workflow outputs, as the verifier source + of truth. +- Bad, because it requires more implementation and testing than simpler job graphs. +- Bad, because bundle verification logic must exist before publish, not only in downstream verifier + tooling. + +### Use one job for build, signing, and publish + +One job checks out source, runs install/build/pack, generates and signs provenance, and publishes to +npm. + +- Good, because it is simple and avoids artifact upload/download handoff. +- Good, because local paths are easy to reason about during an early prototype. +- Bad, because caller-controlled package scripts share a runner environment with signing and publish + permissions. +- Bad, because it weakens the SLSA Build L3 signing-key isolation story. +- Bad, because it conflicts with the digest-verified isolated handoff direction. + +### Use two jobs: `build` then `publish-sign` + +The build job creates the tarball, and a second job verifies the tarball, signs provenance, and +publishes. + +- Good, because build scripts are isolated from signing and publish permissions. +- Good, because it is simpler than a three-job graph. +- Good, because the publish-capable job can verify the tarball before signing and publishing. +- Bad, because provenance signing and registry mutation share one job boundary. +- Bad, because publish does not consume a previously signed bundle through an explicit verification + handoff. +- Bad, because it is less precise for specifying publish-time verification of signed provenance. + +### Use four jobs: `build`, `provenance-generate`, `sign`, and `publish` + +The workflow separates unsigned provenance statement generation from Sigstore signing. + +- Good, because the adapter boundary between statement generation and signing is explicit. +- Good, because replacing `actions/attest` with `sigstore-go` later may be mechanically simpler. +- Good, because unsigned statement validation can happen before signing. +- Bad, because it creates another cross-job artifact handoff and more digest state to verify. +- Bad, because unsigned provenance statement artifacts require their own integrity and + path-confusion controls. +- Bad, because it is too much ceremony for the initial production profile. + +### Build and sign in the reusable workflow, then leave publish to the caller + +The trusted reusable workflow returns package and provenance artifacts; the caller workflow +downloads them and performs npm publish. + +- Good, because it resembles SLSA GitHub Generator Node.js custom publishing examples. +- Good, because callers can customize publishing workflows. +- Bad, because the production profile would not control the registry mutation path. +- Bad, because callers could publish without verifying the signed provenance bundle or could submit + a different provenance file. +- Bad, because it conflicts with ADR 0029's production profile requirement to submit Windlass + canonical provenance through npm's external provenance-file path. +- Bad, because publish failures and downgrade behavior would become caller-specific rather than + profile-specified. + +## More Information + +This decision follows ADR 0024, ADR 0025, ADR 0029, ADR 0034, and ADR 0035. It decides the initial +production workflow job graph and artifact handoff boundary only. It does not change package manager +selection, private dependency credential support, canonical provenance contents, signing adapter +choice, public workflow outputs, or signed release metadata. + +Reference points considered: + +- SLSA v1.2 Build Requirements require authentic provenance at Build L2 and unforgeable provenance + at Build L3. Secret material used to authenticate provenance must not be accessible to + user-defined build steps, and builds must run in isolated, ephemeral environments. +- SLSA v1.2 Build Provenance defines `subject`, `builder.id`, `buildType`, `externalParameters`, + `internalParameters`, `resolvedDependencies`, and verifier-relevant provenance semantics. It also + separates signer identity from builder identity and requires consumers to accept only expected + signer-builder pairs. +- SLSA v1.2 Verifying Artifacts recommends checking the provenance envelope signature, subject + digest, `predicateType`, trusted builder identity, `buildType`, and `externalParameters`; + unrecognized external parameters should generally fail verification. +- The SLSA GitHub Generator technical design uses reusable workflows to prevent caller workflow + interference, GitHub-hosted job isolation as the VM boundary, GitHub artifacts for larger job + handoff data, and job outputs as a trusted channel for hashes and handles. +- The SLSA GitHub Generator Node.js builder produces package and provenance artifacts, exposes + artifact names and SHA-256 values, and shows publish flows that securely download the package and + provenance before running `npm publish --provenance-file`. +- The SLSA GitHub Generator `secure-package-download`, `secure-attestations-download`, and + `nodejs/publish` actions use artifact names plus expected SHA-256 values as a package/provenance + handoff model for publishing. +- GitHub `actions/attest` can create Sigstore bundle JSON, supports custom predicates, exposes a + `bundle-path` output, and requires `id-token: write` plus attestation permissions only where the + signing/storage action runs. +- GitHub `actions/upload-artifact` v4 and later make uploaded artifacts immutable, expose artifact + IDs, URLs, and SHA-256 digests, and reject accidental same-name mutation patterns. +- GitHub `actions/download-artifact` supports downloading by name or artifact ID and has digest + mismatch handling, but Windlass should still recalculate and compare package and bundle digests as + part of its own trust boundary. +- npm provenance documentation supports publishing packages with provenance from cloud-hosted CI and + third-party publish tooling that supplies an external provenance file, while npm trusted + publishing can separately authenticate publish operations without long-lived npm tokens. diff --git a/docs/decisions/0052-compose-npm-package-tarball-producer-with-release-asset-publisher.md b/docs/decisions/0052-compose-npm-package-tarball-producer-with-release-asset-publisher.md new file mode 100644 index 0000000..8c78c27 --- /dev/null +++ b/docs/decisions/0052-compose-npm-package-tarball-producer-with-release-asset-publisher.md @@ -0,0 +1,219 @@ +--- +parent: Decisions +nav_order: 52 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Compose the npm Package Tarball Producer with the GitHub Release Asset Publisher + +## Context and Problem Statement + +ADR 0049 redefined GitHub Release asset handling as publication rather than artifact production. ADR +0050 then required the publisher profile to accept a producer-to-publisher handoff that binds +artifact bytes, final release asset name, expected digest, and producer-generated SLSA provenance. +ADR 0051 requires the publisher to redistribute the verified producer provenance bundle as an +unchanged GitHub Release asset sidecar. + +Those decisions intentionally keep the GitHub Release asset publisher from becoming a generic build +profile. They also leave an initial composition question: which producer profile should first feed +the publisher with source-to-artifact provenance and final artifact bytes? + +The project already has a detailed initial producer candidate: the JS/TS npm package profile. That +profile produces an npm package tarball, generates Windlass-owned SLSA provenance for that tarball, +and returns package identity plus tarball digest outputs. The same tarball can be useful as a GitHub +Release asset for projects that want a release-page copy in addition to npm registry publication. + +Should the first GitHub Release asset producer composition use the JS/TS npm package profile's +published package tarball, introduce a generic release asset builder, wait for a Go binary or +archive producer profile, or leave producer composition unspecified until multiple producer profiles +exist? + +## Decision Drivers + +- Preserve ADR 0049's boundary that GitHub Release asset publication does not build or mint + source-to-artifact provenance in the default production path. +- Reuse the already-specified JS/TS npm package producer semantics instead of inventing a generic + artifact build model. +- Provide a concrete first end-to-end composition for projects that publish npm packages and also + attach the produced package tarball to a GitHub Release. +- Keep the GitHub Release asset publisher generic enough to compose with future producer profiles, + including Go archives, container images, SBOM producers, checksum producers, or other ecosystem + profiles. +- Require the producer provenance subject to match the final GitHub Release asset name and digest, + as required by ADR 0050, without adding a publisher-owned rename predicate. +- Avoid coupling the publisher's public contract, verification model, or implementation internals to + npm-specific fields that belong to the JS/TS npm package producer profile. +- Keep future producer profile additions possible without redesigning the publisher contract. + +## Considered Options + +- Use the JS/TS npm package profile's npm package tarball as the initial producer composition for + the GitHub Release asset publisher. +- Create a generic release asset builder profile before composing any ecosystem producer. +- Wait for a Go binary or archive producer profile before defining a first publisher composition. +- Leave the initial producer composition unspecified until several producer profiles exist. + +## Decision Outcome + +Chosen option: "Use the JS/TS npm package profile's npm package tarball as the initial producer +composition for the GitHub Release asset publisher", because it provides the first concrete +producer-to-publisher path while preserving the publisher as an abstract verifier and distributor +for producer-owned artifacts. + +The first production GitHub Release asset publisher composition should use the npm package tarball +produced by the JS/TS npm package profile as the primary release asset bytes. The JS/TS npm package +profile remains the source-to-artifact producer: it owns package-manager execution, package packing, +npm package identity, tarball digest calculation, Windlass-generated SLSA provenance, npm registry +publication, and npm-specific verification expectations. + +The GitHub Release asset publisher should receive the npm tarball through the producer-to-publisher +handoff contract selected by ADR 0050. The handoff should expose the artifact handle, expected +SHA-256 digest, final GitHub Release asset name, release tag, producer provenance bundle or locator, +producer `builder.id`, producer `buildType`, producer subject name, producer subject digest, source +identity, and producer-native provenance locators using the generic handoff semantics. The publisher +must treat npm-specific fields as producer policy inputs or opaque producer metadata unless the +generic handoff contract explicitly requires them. + +The publisher must verify that the npm tarball bytes it will upload match the expected digest, that +the producer provenance signature and trusted producer policy verify, and that the producer +provenance subject name and digest match the final GitHub Release asset name and bytes. It should +then upload the tarball as the primary GitHub Release asset and redistribute the unchanged producer +provenance bundle as the sidecar selected by ADR 0051. + +This composition does not make the GitHub Release asset publisher an npm-specific workflow. The +publisher's architecture specification, inputs, verification phases, failure behavior, and outputs +should be expressed in producer-neutral terms: artifact handle, expected digest, final asset name, +release target, producer provenance, trusted producer policy, native provenance locators, sidecar +publication, and linked-artifact options. npm package name, package version, registry URL, dist-tag, +package URL, npm integrity values, and package-manager metadata remain JS/TS npm package producer +fields unless a generic publisher rule explicitly needs a normalized value. + +Future producer profiles should be able to compose with the same publisher without changing the +publisher's trust boundary. Adding producers such as Go binary/archive, container image, checksum, +SBOM, or other ecosystem profiles should require those producers to satisfy the same handoff +contract, not require the publisher to learn the full semantics of each producer ecosystem. If a +future producer needs a materially different handoff, subject-name mapping, publication evidence, or +trust model, that change should be recorded by a future ADR and should preserve a clear distinction +between producer-owned source-to-artifact provenance and publisher-owned distribution behavior. + +The initial npm tarball composition should not support generic raw file upload, arbitrary caller +artifacts, unpublished package tarballs without accepted producer provenance, or publisher-generated +SLSA provenance. Those remain outside the production publisher path unless a future ADR defines a +distinct producer profile, lower-assurance workflow, or publication evidence model. + +### Consequences + +- Good, because the project gets a concrete first release asset composition without reintroducing a + generic release asset builder. +- Good, because the JS/TS npm package profile's existing package identity, tarball digest, and + provenance decisions become immediately useful for GitHub Release distribution. +- Good, because the publisher remains a verifier and distributor rather than an npm package profile + extension. +- Good, because future producer profiles can target the same publisher handoff contract. +- Good, because the architecture avoids treating raw caller-provided files as production release + assets without producer provenance. +- Neutral, because the first composition primarily serves projects that publish npm packages and + want a GitHub Release copy of the package tarball. +- Neutral, because the JS/TS npm package profile may need to expose handoff outputs that are more + precise than ordinary public workflow outputs. +- Bad, because projects that need Go binaries, generic archives, containers, SBOM-only assets, or + other release asset types still need later producer profiles. +- Bad, because requiring the producer subject name to match the final GitHub Release asset name may + force npm tarball naming to be decided before provenance signing. + +### Confirmation + +This decision is confirmed when architecture specifications, workflow implementations, tests, and +documentation define: + +- the JS/TS npm package profile's npm package tarball as the initial producer artifact for GitHub + Release asset publisher composition; +- JS/TS npm package profile outputs or internal handoff artifacts that provide the generic + producer-to-publisher fields required by ADR 0050; +- the GitHub Release asset publisher contract in producer-neutral terms rather than npm-specific + terms; +- publisher verification of npm tarball bytes, expected digest, producer provenance signature, + trusted producer `builder.id`, trusted producer `buildType`, producer subject name, producer + subject digest, source identity, and release asset target before upload; +- redistribution of the unchanged JS/TS npm package producer provenance bundle as the GitHub Release + sidecar selected by ADR 0051; +- rejection of raw npm tarballs or arbitrary files that lack acceptable producer provenance; +- documentation that future producer profiles can compose with the publisher by satisfying the same + handoff contract; +- tests proving that the publisher does not depend on npm-only fields except through producer policy + checks for this specific composition. + +Implementation review should verify that the GitHub Release asset publisher cannot silently become +an npm-specific publisher, cannot accept unauthenticated tarball transport as proof of artifact +trust, and cannot upload a release asset whose bytes, final asset name, or digest differ from the +verified producer provenance subject. + +## Pros and Cons of the Options + +### Use the JS/TS npm package tarball as the initial producer composition + +The JS/TS npm package profile produces the package tarball and SLSA provenance. The publisher +verifies the handoff, uploads the tarball to an existing GitHub Release, and publishes the unchanged +producer provenance sidecar. + +- Good, because it composes existing profile decisions into the first concrete release asset path. +- Good, because it keeps npm ecosystem semantics inside the npm producer profile. +- Good, because the publisher contract can be tested with a real producer before additional + producers exist. +- Neutral, because the first supported producer composition is npm-package-specific. +- Bad, because it does not help projects whose primary release assets are binaries, archives, or + containers. + +### Create a generic release asset builder profile first + +The project would define a builder that accepts arbitrary files, generates new provenance, and then +publishes those files as GitHub Release assets. + +- Good, because it appears flexible for many asset types. +- Bad, because it reverses ADR 0049's decision to separate artifact production from publication. +- Bad, because generic arbitrary-file provenance would have weak or unclear source-to-artifact + semantics. +- Bad, because it risks accepting raw caller-produced artifacts under a production SLSA-equivalent + identity. + +### Wait for a Go binary or archive producer profile + +The project would defer release asset publisher composition until a Go binary or archive producer +profile exists. + +- Good, because Go archives are likely to be common GitHub Release assets for this project family. +- Good, because it avoids npm-specific first examples. +- Bad, because it delays validation of the publisher handoff even though a specified npm producer is + already available. +- Bad, because it leaves npm package tarball redistribution unspecified despite being a natural + first composition. + +### Leave initial producer composition unspecified + +The project would write only an abstract publisher contract and wait for future producers to bind to +it. + +- Good, because it maximizes initial abstraction. +- Bad, because specifications and tests would lack a concrete end-to-end composition. +- Bad, because untested abstraction may hide fields that are too generic, too npm-specific, or + insufficient for real producer handoff. + +## More Information + +This decision follows ADR 0049, ADR 0050, and ADR 0051. It uses the JS/TS npm package profile +defined by ADR 0013 through ADR 0037 as the first producer profile that can hand off artifact bytes +and producer-generated SLSA provenance to the GitHub Release asset publisher. + +This decision does not add a generic file producer, Go binary/archive producer, container producer, +SBOM producer, checksum producer, publisher-owned publication predicate, raw artifact upload mode, +or lower-assurance mirror workflow. Those remain future producer-profile or workflow decisions. + +Reference points considered: + +- ADR 0049 requires ecosystem-specific producer profiles to own source-to-artifact provenance. +- ADR 0050 requires the publisher to verify producer provenance before release mutation. +- ADR 0051 requires unchanged producer provenance redistribution as a release asset sidecar. +- The JS/TS npm package profile already models a package tarball as the physical release artifact + for npm publication and exposes tarball digest outputs useful for composition. From e7820c7818171868204cbe99f1b8c8f9232f4c10 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:13 +0900 Subject: [PATCH 05/14] docs(decisions): add ADRs for GitHub release asset workflow scope Scope the release asset profile to one asset per run, record the initial workflow entrypoint proposal, and require uploading assets to an existing GitHub release. Signed-off-by: Yunseo Kim --- ...ease-asset-profile-to-one-asset-per-run.md | 247 ++++++++++++++++++ ...release-asset-slsa3-workflow-entrypoint.md | 211 +++++++++++++++ ...ease-assets-to-existing-github-releases.md | 229 ++++++++++++++++ 3 files changed, 687 insertions(+) create mode 100644 docs/decisions/0039-scope-release-asset-profile-to-one-asset-per-run.md create mode 100644 docs/decisions/0040-use-github-release-asset-slsa3-workflow-entrypoint.md create mode 100644 docs/decisions/0043-upload-release-assets-to-existing-github-releases.md diff --git a/docs/decisions/0039-scope-release-asset-profile-to-one-asset-per-run.md b/docs/decisions/0039-scope-release-asset-profile-to-one-asset-per-run.md new file mode 100644 index 0000000..32195d0 --- /dev/null +++ b/docs/decisions/0039-scope-release-asset-profile-to-one-asset-per-run.md @@ -0,0 +1,247 @@ +--- +parent: Decisions +nav_order: 39 +status: accepted +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Scope the GitHub Release Asset Profile to One Asset per Run + +## Context and Problem Statement + +ADR 0038 added the initial GitHub Release asset profile direction: Windlass trusted logic generates +canonical SLSA provenance for release assets, while `actions/attest` acts as the initial Sigstore +signing and storage adapter. The next decision is the release asset profile's artifact scope and +verification unit. + +GitHub Releases commonly contain multiple downloadable files: OS-specific binaries, architecture +variants, checksum files, SBOMs, provenance sidecars, signatures, archives, and sometimes generated +source archives. SLSA v1.2 recommends binding attestations to artifacts rather than releases, while +GitHub `actions/attest` models attestation subjects as named artifacts with digests and supports +both one subject and multiple subjects per attestation. The project needs an initial scope that +keeps the production SLSA3 profile easy to verify, while still allowing full releases to contain +multiple assets through composition. + +Should the initial GitHub Release asset profile treat one profile run as one release asset, an +explicit set of release assets, a full GitHub Release, a generated archive from a directory, or a +checksum-manifest-centered asset group? + +## Decision Drivers + +- Keep the profile's SLSA subject and verifier policy artifact-bound, not release-bound. +- Make one profile run's verification unit as simple and auditable as the npm profile's + one-package-per-run decision in ADR 0018. +- Avoid partial-success and partial-retry ambiguity for multi-platform or multi-file releases. +- Keep release asset upload, duplicate-name handling, immutable release behavior, and provenance + verification straightforward for the initial profile. +- Preserve a path for releases that publish multiple OS or architecture assets, checksum files, + SBOMs, provenance sidecars, and signatures. +- Avoid treating GitHub-generated source archives as ordinary release assets in the initial build + artifact profile. +- Avoid making checksum manifests the primary SLSA subject when the real downloadable artifacts are + the files referenced by the checksum manifest. +- Keep future multi-asset orchestration separate from the low-level trusted profile contract. + +## Considered Options + +- Publish and verify one explicitly named GitHub Release asset file per profile run, with an + orchestration layer for multi-asset releases. +- Publish and verify an explicit set of GitHub Release asset files per profile run. +- Treat the full GitHub Release as the profile run's verification unit. +- Treat one generated archive from a selected directory as the profile run's release asset. +- Treat a checksum manifest plus referenced files as the profile run's verification unit. +- Include GitHub-generated source archives in the release asset profile. + +## Decision Outcome + +Chosen option: "Publish and verify one explicitly named GitHub Release asset file per profile run, +with an orchestration layer for multi-asset releases", because it keeps the trusted profile's SLSA +subject model small and artifact-bound while still allowing a higher-level release workflow to +compose complete multi-asset GitHub Releases. + +The initial GitHub Release asset profile should define one profile run as exactly one GitHub Release +asset file. That asset is the profile's primary output artifact and primary verification unit. The +profile's SLSA provenance should have exactly one release asset subject for the production path +unless a later ADR defines a multi-subject profile or mode. The release asset subject should +identify the uploaded asset by a profile-defined asset name and digest, with release tag, source +identity, and upload metadata represented in the profile's provenance parameters, dependencies, +byproducts, or verification policy as specified by the architecture documentation. + +The initial profile should not treat these as the same low-level verification unit: + +- a full GitHub Release; +- a set of OS or architecture assets; +- checksum files plus all referenced artifacts; +- SBOM files plus the artifact they describe; +- provenance sidecars plus the artifact they describe; +- detached signatures plus the artifact they sign; +- GitHub-generated source archives. + +Repositories that need to publish multiple release assets should use a higher-level orchestration +layer. That orchestration layer may call the one-asset profile once per OS or architecture artifact, +once per SBOM, once per checksum file, or once per provenance sidecar when those files are intended +to be published release assets. The orchestration layer should own cross-asset concerns such as +release creation order, draft or immutable release workflow, grouping by tag, aggregate release +notes, checksum manifest generation, SBOM set publication, provenance sidecar naming, and +whole-release user experience. It should not weaken the one-asset profile's artifact-bound SLSA +subject contract. + +Checksum files, SBOMs, provenance sidecars, and detached signatures may be published as release +assets through the same one-asset profile when the project wants those files to be independently +downloadable and independently attested. They may also be generated or referenced as byproducts of +an orchestration workflow where appropriate. The architecture specification should define when such +files are release assets, byproducts, or verification inputs; this ADR only decides that they are +not implicitly bundled into the low-level profile's verification unit for another asset. + +GitHub-generated source archives are excluded from the initial GitHub Release asset profile because +they are not uploaded release assets produced by the trusted profile. Source integrity and generated +source archive verification should be handled by source-track documentation, GitHub release +verification, or a later dedicated decision if needed. + +### Consequences + +- Good, because the profile's SLSA subject is simple: one release asset file and its digest. +- Good, because release asset upload and verification can fail closed without partial multi-asset + state handling. +- Good, because the model matches SLSA's artifact-bound provenance guidance more closely than a + full-release verification unit. +- Good, because the profile remains easy to compose for OS and architecture matrices by calling it + once per asset. +- Good, because checksum files, SBOMs, provenance sidecars, and detached signatures can still be + published and attested as first-class release assets when desired. +- Good, because a future orchestration workflow can provide a friendly whole-release abstraction + without changing the trusted asset-level profile. +- Neutral, because complete release publication will require orchestration for projects with more + than one asset. +- Bad, because users do not get a one-call multi-platform release asset workflow from the low-level + profile alone. +- Bad, because cross-asset consistency, such as "all assets in the checksum file were published and + attested", must be specified in the orchestration layer rather than in the one-asset profile. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and documentation define: + +- one explicitly named GitHub Release asset file as the unit of one profile run; +- exactly one SLSA subject for the production release asset profile path; +- release asset name, local path, digest, upload target, and GitHub Release tag handling for one + asset; +- failure behavior for missing asset, unexpected directory input, duplicate asset names, upload + mismatch, digest mismatch, and unsupported source archive inputs; +- outputs for the uploaded asset identity, asset URL or API identifier, and digest values; +- exclusion of full-release, multi-asset, checksum-manifest-centered, SBOM-bundled, + provenance-sidecar-bundled, detached-signature-bundled, and GitHub-generated source archive + verification units from the low-level profile; +- an orchestration-layer boundary for multi-OS, multi-architecture, checksum, SBOM, provenance + sidecar, detached signature, and whole-release workflows; +- documentation explaining that the orchestration layer composes multiple one-asset profile runs + rather than changing the one-asset SLSA subject contract. + +Implementation review should verify that the low-level production release asset profile cannot +silently publish, sign, or verify multiple primary release assets in one run, and that orchestration +logic does not bypass the one-asset profile's provenance, digest, signing, and upload checks for any +published release asset. + +## Pros and Cons of the Options + +### Publish and verify one explicitly named GitHub Release asset file per profile run + +Each invocation produces, signs, uploads, and verifies one named release asset file. Multi-file +releases are composed by a separate orchestration layer that invokes the profile once per asset. + +- Good, because it provides the smallest clear artifact-bound verification unit. +- Good, because one subject, one digest, one upload result, and one signed provenance bundle are + easy to explain and test. +- Good, because failed upload, duplicate asset name, or digest mismatch handling is localized to one + asset. +- Good, because OS and architecture matrices can map naturally to repeated profile invocations. +- Neutral, because multi-asset releases still require an orchestration layer. +- Bad, because it can feel lower-level than users expect from a release-publishing workflow. + +### Publish and verify an explicit set of GitHub Release asset files per profile run + +One invocation accepts a manifest, glob, or list of release asset files and creates one provenance +attestation with multiple subjects. + +- Good, because it matches `actions/attest` support for multiple subjects in a single attestation. +- Good, because a release with several platform artifacts can be published by one workflow call. +- Good, because shared release metadata appears once in the provenance predicate. +- Bad, because partial upload, partial attestation, already-existing asset, and retry behavior + become more complex. +- Bad, because all assets in the set must share one build definition and verifier expectation, which + may not be true for OS or architecture variants built differently. +- Bad, because adding or rebuilding one platform asset after the fact is harder to model cleanly. + +### Treat the full GitHub Release as the verification unit + +One invocation treats the release tag and all attached assets as the artifact being verified. + +- Good, because it matches a user's intuitive concept of "verify this release". +- Good, because it aligns with GitHub immutable release and release attestation concepts. +- Bad, because SLSA recommends artifact-bound build attestations rather than release-bound + attestations. +- Bad, because releases can contain assets from different builds, environments, and times. +- Bad, because this conflates build provenance with release integrity and orchestration policy. + +### Treat one generated archive from a selected directory as the release asset + +The profile accepts a directory and creates a tarball or zip file that becomes the single release +asset. + +- Good, because the final verification unit is still one release asset file. +- Good, because profile-owned archive generation can make file ordering, permissions, and timestamps + explicit. +- Bad, because archive format, deterministic packing, symlink behavior, permissions, timestamps, and + path normalization become part of the trusted profile contract. +- Bad, because accepting arbitrary directories can blur the boundary between build output and + release packaging. +- Bad, because this is a packaging feature, not the minimal release asset scope decision. + +### Treat a checksum manifest plus referenced files as the verification unit + +One invocation centers verification on a checksum file that names all release assets and their +digests. + +- Good, because checksum manifests are common in GitHub Releases. +- Good, because they are convenient for human and script-based download verification. +- Bad, because the checksum file is not the same artifact as the files it references. +- Bad, because the SLSA subject can become ambiguous: the manifest, the referenced files, or both. +- Bad, because synchronization between the manifest, uploaded assets, and signed provenance requires + orchestration-level policy. + +### Include GitHub-generated source archives + +The profile treats GitHub-generated source zip or tarball downloads as release assets. + +- Good, because those archives appear on GitHub Release pages and are commonly downloaded. +- Neutral, because GitHub release verification can cover release integrity around immutable + releases. +- Bad, because GitHub-generated source archives are not uploaded artifacts produced by this trusted + profile. +- Bad, because they are source snapshots or generated source distributions rather than build outputs + owned by the release asset profile. +- Bad, because mixing source archive verification into this profile would blur SLSA Build Track and + Source Track responsibilities. + +## More Information + +This decision follows ADR 0038 and decides only the initial GitHub Release asset profile's artifact +scope and verification unit. It does not decide the profile workflow filename, exact public input +names, job graph, build or pack command policy, immutable release requirement, release creation +policy, checksum manifest schema, SBOM format, sidecar naming convention, or orchestration workflow +interface. + +Reference points considered: + +- SLSA v1.2 Distributing Provenance says attestations should be bound to artifacts rather than + releases, and package ecosystems should support multiple individual attestations per release. +- SLSA v1.2 Build Provenance represents build outputs through the top-level `subject` list, with + each subject identified by digest. +- GitHub `actions/attest` models subjects as named artifacts with digest maps. It supports one + subject, multiple subjects by glob or explicit list, and subject enumeration through checksum + files, but multiple subjects are still artifact subjects rather than a full-release subject. +- GitHub immutable releases protect release tags and release assets after publication and can + produce release attestations, but that release-level mechanism remains complementary to + artifact-bound build provenance. diff --git a/docs/decisions/0040-use-github-release-asset-slsa3-workflow-entrypoint.md b/docs/decisions/0040-use-github-release-asset-slsa3-workflow-entrypoint.md new file mode 100644 index 0000000..15e8e43 --- /dev/null +++ b/docs/decisions/0040-use-github-release-asset-slsa3-workflow-entrypoint.md @@ -0,0 +1,211 @@ +--- +parent: Decisions +nav_order: 40 +status: superseded by ADR-0049 +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use `github-release-asset-slsa3.yml` as the GitHub Release Asset Workflow Entrypoint + +## Context and Problem Statement + +ADR 0002 chose an extensible trusted reusable workflow foundation, and ADR 0003 refined that +foundation into a thin shared core with profile-owned reusable workflows. ADR 0022 selected +`.github/workflows/js-ts-npm-package-slsa3.yml` as the initial JS/TS npm package profile entrypoint. +ADR 0028 later made the reusable workflow file path plus full commit SHA part of the production +`builder.id` trust anchor. ADR 0038 added the GitHub Release asset profile direction, and ADR 0039 +scoped that profile to one explicitly named GitHub Release asset per profile run. + +The GitHub Release asset profile now needs a concrete reusable workflow filename before its +architecture specification fixes `runDetails.builder.id`, caller examples, and verifier policy. +Since the workflow filename appears in both the caller's `jobs..uses` reference and the SLSA +provenance `runDetails.builder.id`, this is a verifier-visible public contract, not an internal YAML +detail. + +What reusable workflow filename and entrypoint should the initial GitHub Release asset SLSA3 profile +expose? + +## Decision Drivers + +- Make the public entrypoint identify the selected profile scope: one GitHub Release asset per run. +- Keep the workflow path aligned with ADR 0028's SHA-pinned reusable workflow `builder.id` pattern. +- Preserve consistency with ADR 0022's `-slsa3.yml` naming style for production SLSA3 + profile-owned reusable workflows. +- Make the SLSA Build L3-oriented trust mode visible in caller workflows, provenance, and verifier + policy. +- Avoid implying that the low-level profile publishes or verifies multiple release assets in one + run. +- Avoid generic artifact naming that would hide GitHub Release-specific parameters such as release + tag, asset name, upload target, immutable release behavior, and GitHub attestation storage. +- Leave room for a separate higher-level orchestration workflow for multi-asset releases. + +## Considered Options + +- Use `.github/workflows/github-release-asset-slsa3.yml`. +- Use `.github/workflows/release-asset-slsa3.yml`. +- Use `.github/workflows/github-release-assets-slsa3.yml`. +- Use `.github/workflows/generic-artifact-slsa3.yml`. +- Use `.github/workflows/github-release-asset.yml`. + +## Decision Outcome + +Chosen option: "Use `.github/workflows/github-release-asset-slsa3.yml`", because it names the GitHub +Release asset profile directly, uses singular `asset` to reinforce ADR 0039's one-asset-per-run +scope, and follows the existing production profile naming pattern selected for the JS/TS npm package +profile. + +The initial GitHub Release asset profile should expose this reusable workflow entrypoint: + +```text +.github/workflows/github-release-asset-slsa3.yml +``` + +Production callers should invoke the released workflow using a full 40-character commit SHA as +selected by ADR 0028: + +```yaml +jobs: + release-asset: + uses: windlasstech/slsa-builder/.github/workflows/github-release-asset-slsa3.yml@0123456789abcdef0123456789abcdef01234567 +``` + +The production SLSA provenance emitted by this profile should use a `builder.id` shaped like: + +```text +https://github.com/windlasstech/slsa-builder/.github/workflows/github-release-asset-slsa3.yml@0123456789abcdef0123456789abcdef01234567 +``` + +The workflow should be the public `workflow_call` entrypoint for the low-level one-asset GitHub +Release asset profile. Caller workflows own their local triggers and orchestration choices, but they +interact with this trusted profile through declared workflow inputs and secrets only. The workflow +filename decision does not decide the full input, output, secret, runner, permission, job graph, +release creation, immutable release, upload, or verification contract; those details belong in the +release asset architecture specification or follow-up ADRs. + +A higher-level multi-asset release orchestration workflow, if added later, should use a distinct +workflow filename and should compose this one-asset profile rather than sharing its `builder.id`. +Any preview, experimental, lower-assurance, baseline attestation, generic artifact, or full-release +workflow should also use a distinct filename if it has materially different security properties, +artifact scope, isolation guarantees, signing behavior, runner trust, or claimed SLSA Build level. + +### Consequences + +- Good, because the filename directly matches the profile selected in ADR 0038 and scoped in + ADR 0039. +- Good, because singular `asset` makes the one-profile-run-one-asset verification unit visible in + the builder identity. +- Good, because the `slsa3` suffix clearly marks the production SLSA Build L3-oriented reusable + workflow contract. +- Good, because the chosen name stays consistent with the existing + `.github/workflows/js-ts-npm-package-slsa3.yml` profile entrypoint pattern. +- Good, because later multi-asset orchestration, generic artifact, and lower-assurance workflows can + receive distinct filenames and builder identities. +- Neutral, because the filename is longer than `release-asset-slsa3.yml`. +- Bad, because including `github` and `slsa3` makes the entrypoint intentionally GitHub-specific and + commits the implementation to satisfying the documented SLSA3-oriented trust contract before + release. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification +and implementation define: + +- `.github/workflows/github-release-asset-slsa3.yml` as the public reusable workflow entrypoint; +- `on.workflow_call` as the supported entry mechanism for callers; +- production caller examples using + `windlasstech/slsa-builder/.github/workflows/github-release-asset-slsa3.yml@`; +- production `builder.id` values using the workflow file path plus the same full commit SHA used for + reusable workflow execution; +- exactly one explicitly named GitHub Release asset as the primary output and verification unit for + this workflow; +- separate filenames for multi-asset orchestration, preview, experimental, lower-assurance, + non-production, generic artifact, or full-release workflows; +- review checks that prevent arbitrary caller-defined steps, services, defaults, or environment from + becoming part of this trusted workflow entrypoint. + +Implementation review should verify that production release asset provenance does not emit a +different workflow filename in `builder.id` unless a later ADR changes this public entrypoint +decision. + +## Pros and Cons of the Options + +### Use `.github/workflows/github-release-asset-slsa3.yml` + +Expose the low-level release asset profile as a GitHub-specific, one-asset reusable workflow whose +name includes the SLSA3 trust mode. + +- Good, because it is the most direct match for GitHub Release asset scope. +- Good, because singular `asset` avoids implying multi-asset handling in one profile run. +- Good, because it keeps `builder.id` verifier policies readable and profile-specific. +- Good, because it matches the established `-slsa3.yml` production profile naming pattern. +- Neutral, because the name is long. +- Bad, because the entrypoint is explicitly GitHub-specific rather than reusable for arbitrary file + artifacts. + +### Use `.github/workflows/release-asset-slsa3.yml` + +Expose the profile with a shorter release-asset-focused filename. + +- Good, because it is concise and still names the release asset artifact scope. +- Good, because it includes the SLSA3 trust mode. +- Bad, because it does not identify GitHub Release as the distribution surface. +- Bad, because future release surfaces could make the name ambiguous. +- Bad, because GitHub-specific verifier parameters are less visible in `builder.id`. + +### Use `.github/workflows/github-release-assets-slsa3.yml` + +Use a plural filename to reflect that GitHub Releases commonly contain multiple assets. + +- Good, because it names the GitHub Release asset domain clearly. +- Good, because it may feel natural to users thinking about complete releases. +- Bad, because plural `assets` conflicts with ADR 0039's one-asset-per-run verification unit. +- Bad, because users may expect one invocation to publish, sign, or verify multiple release assets. +- Bad, because documentation would need to explain why a plural builder identity emits exactly one + primary SLSA subject. + +### Use `.github/workflows/generic-artifact-slsa3.yml` + +Expose the profile as a generic artifact SLSA3 workflow and treat GitHub Release upload as one use +case. + +- Good, because it appears more reusable for future artifact surfaces. +- Good, because SLSA subjects are artifact-bound and can be represented by generic name-and-digest + pairs. +- Bad, because it broadens the profile beyond ADR 0038 and ADR 0039 before the GitHub Release asset + contract is specified. +- Bad, because it hides release tag, asset name, upload target, immutable release, and GitHub + attestation storage semantics. +- Bad, because generic naming would make a future truly generic artifact profile harder to + distinguish from this GitHub-specific profile. + +### Use `.github/workflows/github-release-asset.yml` + +Expose the profile without encoding the SLSA trust mode in the filename. + +- Good, because it is shorter than the selected filename. +- Good, because it avoids putting the SLSA level directly in the workflow path. +- Bad, because it is inconsistent with the existing JS/TS npm package production entrypoint name. +- Bad, because it makes security mode separation less visible in caller workflows and `builder.id`. +- Bad, because future preview, SLSA2, baseline attestation, or non-claiming workflows could be + tempted to share the same filename and blur verifier expectations. + +## More Information + +This decision follows ADR 0038 and ADR 0039. It decides only the reusable workflow filename and +public entrypoint for the initial GitHub Release asset SLSA3 profile. It does not decide the exact +public input contract, output names, job graph, build or pack command policy, release creation +policy, immutable release requirement, asset upload behavior, checksum or SBOM handling, provenance +sidecar naming, orchestration workflow interface, or standalone verifier CLI surface. + +Reference points considered: + +- GitHub reusable workflows must live directly under `.github/workflows/`, must include + `on.workflow_call`, and are invoked by callers through + `{owner}/{repo}/.github/workflows/{filename}@{ref}`. +- ADR 0028 requires production consumers to pin reusable workflows by full commit SHA and requires + production `builder.id` values to include the same full commit SHA. +- SLSA v1.2 provenance defines `runDetails.builder.id` as the trusted build platform identity and + requires different builder IDs for modes with different security attributes or SLSA Build levels. +- ADR 0039 scopes the initial GitHub Release asset profile to exactly one explicitly named GitHub + Release asset per profile run. diff --git a/docs/decisions/0043-upload-release-assets-to-existing-github-releases.md b/docs/decisions/0043-upload-release-assets-to-existing-github-releases.md new file mode 100644 index 0000000..cfeaffe --- /dev/null +++ b/docs/decisions/0043-upload-release-assets-to-existing-github-releases.md @@ -0,0 +1,229 @@ +--- +parent: Decisions +nav_order: 43 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Upload GitHub Release Assets to Existing Releases + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance with `actions/attest` as the initial signing +and storage adapter for GitHub Release assets. ADR 0039 scoped the low-level profile to one +explicitly named GitHub Release asset per profile run. ADR 0040 selected the reusable workflow +entrypoint for that one-asset profile. + +The remaining lifecycle policy question is how much GitHub Release state the low-level profile +should own. GitHub Releases can be created from existing tags, can create tags when the tag is +missing in some tooling flows, can be drafts or prereleases, can become immutable after publication, +and can contain existing assets with the same name. The profile needs a failure-closed policy that +preserves artifact-bound SLSA provenance without turning the one-asset profile into a whole-release +orchestrator. + +Should the initial GitHub Release asset profile create or update GitHub Releases, or should it +upload one asset only to an existing release selected by an existing tag? + +## Decision Drivers + +- Preserve ADR 0039's one-asset profile boundary and keep whole-release lifecycle concerns in an + orchestration layer. +- Keep SLSA provenance artifact-bound rather than treating a GitHub Release as the build subject. +- Avoid giving the low-level profile responsibility for creating, moving, signing, deleting, or + selecting release tags. +- Avoid GitHub tooling behavior that can create a missing tag from a default branch or selected + target commit during release creation. +- Support GitHub immutable release workflows, where the recommended flow is draft release creation, + asset upload, then publication. +- Fail clearly when the target release or asset slot is not in the expected state. +- Avoid overwriting, deleting, or replacing existing release assets under the same production + profile invocation. + +## Considered Options + +- Upload one asset to an existing GitHub Release selected by an existing tag, failing when the tag + or release is missing. +- Create the GitHub Release when it is missing, then upload the asset. +- Create a draft GitHub Release, upload the asset, and publish the release from the same profile + run. +- Allow asset overwrite or clobber behavior for existing asset names. +- Treat release creation, asset upload, and publish as one high-level orchestration workflow. + +## Decision Outcome + +Chosen option: "Upload one asset to an existing GitHub Release selected by an existing tag, failing +when the tag or release is missing", because it keeps the low-level profile focused on one +artifact-bound release asset while leaving release lifecycle, draft creation, publication, notes, +and multi-asset coordination to a higher-level orchestration layer. + +The initial GitHub Release asset profile should require both: + +- an existing Git tag matching the requested release tag; and +- an existing GitHub Release associated with that tag. + +The profile should fail before provenance signing or asset upload when either the tag or release is +missing. It should not create a tag, create a release, select a default branch target, move a +release tag, publish a draft release, update release notes, change the prerelease flag, change the +latest release marker, or otherwise mutate release lifecycle metadata. + +Draft releases are valid upload targets. This supports the immutable-release-friendly flow where an +orchestration layer creates a draft release, invokes the one-asset profile once per asset, and then +publishes the release after all required assets are attached. Published releases are also valid +upload targets when GitHub and repository policy allow adding assets after publication, but the +recommended production flow is to upload through draft releases when immutable releases are enabled +or desired. + +Prerelease GitHub Releases are valid upload targets. The low-level profile should record the +observed release state if the architecture specification makes it verifier-relevant, but it should +not decide or change whether the release is a prerelease. + +The profile should reject an existing release asset with the requested asset name. It should not +delete, overwrite, replace, or clobber an existing asset as part of the production path. If a +previous attempt uploaded the asset but later failed, the correct follow-up is verification or +manual inspection, not silent replacement under the same tag and asset name. Retrying after a +failure is supported only when the asset was not uploaded or when the caller chooses a distinct +asset name under an explicit new release policy outside this low-level profile. + +The profile's required GitHub token permissions should not exceed what is needed to read repository +contents, attest provenance, and upload the selected release asset. Any additional permissions +needed to create releases, publish drafts, edit release notes, mark prereleases, or manage tags +belong to the orchestration layer or caller workflow that performs those lifecycle operations. + +### Consequences + +- Good, because the low-level profile remains a one-asset upload and provenance contract rather than + a whole-release lifecycle manager. +- Good, because missing tags and releases fail closed instead of being created implicitly from a + default branch or caller-selected target. +- Good, because draft releases can still be used for GitHub immutable release publication flows. +- Good, because existing asset names cannot be silently replaced with different bytes or provenance. +- Good, because release notes, draft publication, prerelease flags, latest markers, and multi-asset + completeness checks stay in the orchestration layer that owns whole-release user experience. +- Neutral, because callers must create the release before invoking the low-level profile. +- Neutral, because published mutable releases may still accept new assets when repository policy + allows it. +- Bad, because a single call to the low-level profile cannot create a complete GitHub Release from + scratch. +- Bad, because callers need orchestration when they want draft creation, multiple asset uploads, and + publish as one workflow. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and documentation define: + +- a required release tag input that must identify an existing Git tag; +- failure before provenance signing or upload when the release tag does not exist; +- failure before provenance signing or upload when no GitHub Release exists for the tag; +- upload of exactly one explicitly named asset to the existing release; +- support for draft release targets without publishing them; +- support for prerelease targets without changing prerelease state; +- no tag creation, tag movement, release creation, draft publication, release note editing, + prerelease flag editing, or latest marker editing in the low-level profile; +- failure when an existing release asset already uses the requested asset name; +- no overwrite, delete, replace, or clobber behavior for existing release assets; +- documentation that draft release creation, multi-asset sequencing, immutable release publication, + release notes, and whole-release completeness checks belong to an orchestration layer. + +Implementation review should verify that the production path cannot silently create missing tags or +releases, upload to the wrong release target, publish a draft release, modify release metadata, or +replace an existing asset under the same release tag and asset name. + +## Pros and Cons of the Options + +### Upload one asset to an existing release and fail when tag or release is missing + +The caller or orchestration layer creates the tag and release first. The low-level profile validates +that both exist, uploads one named asset, signs or stores the associated provenance, and fails if +the asset name already exists. + +- Good, because it matches ADR 0039's low-level one-asset scope. +- Good, because release governance remains outside the trusted asset upload profile. +- Good, because it avoids implicit tag creation during release creation flows. +- Good, because it composes cleanly with draft releases and immutable release publication. +- Good, because duplicate asset handling is simple and failure-closed. +- Neutral, because an orchestration layer is required for a one-command whole-release workflow. +- Bad, because first-time users must create the GitHub Release before the asset profile can run. + +### Create the release when it is missing + +The profile accepts release metadata, creates the GitHub Release when the selected tag has no +release, and then uploads the asset. + +- Good, because it reduces setup for simple single-asset releases. +- Good, because the release and asset upload can happen in one profile invocation. +- Bad, because release creation brings release notes, title, target commit, latest marker, draft, + and prerelease policy into the low-level profile. +- Bad, because some tooling can create a missing tag as part of release creation unless explicitly + guarded. +- Bad, because it blurs the boundary between artifact-bound provenance and whole-release lifecycle + management. + +### Create a draft release, upload the asset, and publish it + +The profile owns the full immutable-release-friendly sequence for one release: create draft, upload +the selected asset, and publish. + +- Good, because it matches GitHub's recommended immutable release publication flow for complete + releases. +- Good, because users get a convenient single workflow for simple releases. +- Bad, because publishing a release is a whole-release decision that must know whether all required + assets, checksums, SBOMs, and notes are complete. +- Bad, because multi-asset releases would need partial-success and publish-order handling inside the + low-level one-asset profile. +- Bad, because the profile would need broader `contents: write` behavior and release metadata + inputs. + +### Allow asset overwrite or clobber behavior + +The profile deletes or replaces an existing release asset when the requested asset name already +exists. + +- Good, because retries after partial failure can be operationally convenient. +- Good, because it resembles common mutable release upload tooling that supports clobber behavior. +- Bad, because deleting the old asset before uploading the new one can lose the original asset if + the new upload fails. +- Bad, because replacing bytes under the same release tag and asset name weakens artifact identity + and consumer expectations. +- Bad, because it conflicts with immutable release behavior and SLSA's preference for immutable + attestations corresponding to artifacts. + +### Treat release creation, asset upload, and publish as one orchestration workflow + +A higher-level workflow creates or selects a release, invokes the one-asset profile for each asset, +checks whole-release completeness, and publishes the draft when appropriate. + +- Good, because it can provide the user-friendly whole-release flow that the low-level profile + avoids. +- Good, because it can own release notes, draft status, prerelease status, latest marker, checksums, + SBOMs, and multi-asset completeness checks together. +- Good, because it composes the one-asset profile without changing the artifact-bound SLSA subject + contract. +- Neutral, because this is a separate product surface with a distinct lifecycle contract. +- Bad, because it requires additional design work and possibly a distinct workflow entrypoint or + builder identity. + +## More Information + +This decision follows ADR 0038, ADR 0039, and ADR 0040. It decides only the low-level GitHub Release +asset profile's release creation and upload policy. It does not decide the future orchestration +workflow interface, release notes schema, checksum manifest policy, immutable release requirement, +release asset sidecar naming, or standalone verifier CLI behavior. + +Reference points considered: + +- SLSA v1.2 Distributing Provenance says attestations should be bound to artifacts rather than + releases, and that provenance should accompany the artifact at publish time. +- SLSA v1.2 Distributing Provenance says attestations should be immutable and should not be + overwritten later with a different attestation for the same artifact. +- GitHub CLI `gh release create` can automatically create a matching tag when one does not exist; + `--verify-tag` aborts release creation when the tag is missing. +- GitHub CLI `gh release upload --clobber` deletes existing assets before uploading replacements and + warns that the original asset is lost if the upload fails. +- GitHub immutable releases protect release tags and assets from modification or deletion after + publication, and GitHub recommends creating the release as a draft, attaching all assets, and then + publishing it. +- GitHub artifact attestations and `actions/attest` bind named artifact subjects and digests to + signed predicates; they complement GitHub release attestations but do not require the low-level + profile to own release lifecycle operations. From 118a7a5f94266e469912ab0af00222329c40a46f Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:14 +0900 Subject: [PATCH 06/14] docs(decisions): add release asset subject and linked-artifacts ADRs Use the release asset name as the SLSA subject name, keep checksums and SBOMs out of the subject digest, and make linked-artifacts storage records explicit opt-in. Signed-off-by: Yunseo Kim --- ...release-asset-name-as-slsa-subject-name.md | 230 +++++++++++++++++ ...oms-out-of-release-asset-subject-digest.md | 239 ++++++++++++++++++ ...ords-explicit-opt-in-for-release-assets.md | 227 +++++++++++++++++ 3 files changed, 696 insertions(+) create mode 100644 docs/decisions/0045-use-release-asset-name-as-slsa-subject-name.md create mode 100644 docs/decisions/0046-keep-checksums-and-sboms-out-of-release-asset-subject-digest.md create mode 100644 docs/decisions/0048-make-linked-artifacts-storage-records-explicit-opt-in-for-release-assets.md diff --git a/docs/decisions/0045-use-release-asset-name-as-slsa-subject-name.md b/docs/decisions/0045-use-release-asset-name-as-slsa-subject-name.md new file mode 100644 index 0000000..fb4d195 --- /dev/null +++ b/docs/decisions/0045-use-release-asset-name-as-slsa-subject-name.md @@ -0,0 +1,230 @@ +--- +parent: Decisions +nav_order: 45 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Use the GitHub Release Asset Name as the SLSA Subject Name + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance for GitHub Release assets, with +`actions/attest` acting only as the initial Sigstore signing and storage adapter. ADR 0039 scoped +the initial production profile to exactly one GitHub Release asset per profile run. ADR 0043 +selected an upload-only lifecycle for one explicitly named asset on an existing GitHub Release, and +ADR 0044 selected a three-job graph where `release-upload` verifies the signed provenance bundle +before mutating the release. + +The next subject and digest semantics decision is the `subject[0].name` value in the in-toto +Statement. The profile needs a stable name that identifies the one asset the user expects to upload, +without confusing local build paths, GitHub Release URLs, GitHub API IDs, or whole-release identity +with the artifact-bound SLSA subject. + +What should the initial GitHub Release asset profile use as the SLSA subject name for the one +release asset artifact? + +## Decision Drivers + +- Keep the profile's SLSA subject artifact-bound and aligned with ADR 0039's one-asset scope. +- Match the GitHub Release asset identity that callers and consumers see on the release page. +- Avoid making workspace-local paths part of the verifier contract when the asset may be renamed for + upload. +- Avoid making mutable or provider-specific URLs the primary subject identifier. +- Avoid using GitHub API asset IDs that do not exist until after the provenance is signed. +- Keep digest verification as the cryptographic identity check, with `name` as the policy-relevant + asset identifier. +- Align with `actions/attest`, in-toto, and SLSA GitHub Generator conventions for named artifact + subjects with digest maps. + +## Considered Options + +- Use the final GitHub Release asset name as `subject[0].name`. +- Use the repository-relative local artifact path as `subject[0].name`. +- Use the GitHub Release browser or download URL as `subject[0].name`. +- Use the GitHub Release asset API ID or GraphQL node ID as `subject[0].name`. +- Use a package-url-like GitHub Release asset identifier as `subject[0].name`. + +## Decision Outcome + +Chosen option: "Use the final GitHub Release asset name as `subject[0].name`", because it matches +the one explicitly named upload target selected by ADR 0039 and ADR 0043 while keeping digest +matching as the cryptographic artifact identity. + +The initial GitHub Release asset profile should emit exactly one production SLSA subject for the +release asset: + +```json +"subject": [ + { + "name": "", + "digest": { + "sha256": "" + } + } +] +``` + +The subject name is the final GitHub Release asset name requested by the caller and accepted by the +profile for upload. It is not the local workspace path, repository-relative build output path, +workflow artifact name, GitHub Release URL, GitHub API asset ID, provenance sidecar name, or package +URL. + +The subject name should be a single release asset name, not a path. The architecture specification +should reject subject asset names that contain path separators, empty path segments, URL syntax, +query strings, fragments, control characters, or values that normalize differently across supported +platforms. The same normalized asset name should be used for duplicate-asset checks, upload, public +workflow output handles, and subject-name verification. + +The subject digest should be the SHA-256 digest of the exact bytes uploaded as the GitHub Release +asset. It should be encoded as lowercase hexadecimal in `subject[0].digest.sha256`, without a +`sha256:` prefix. The implementation may expose or use prefixed digest strings at tool boundaries +where required, such as `actions/attest` `subject-digest`, but the in-toto Statement digest map +should use the algorithm key `sha256` and the raw lowercase hex value. + +Release repository, release tag, release URL, release asset API ID, draft or prerelease state, +download URL, upload metadata, and GitHub attestation storage identifiers should not be folded into +`subject[0].name`. Those values should be represented in the profile's `externalParameters`, +`resolvedDependencies`, byproducts, upload verification policy, workflow outputs, or release +metadata fields as defined by later architecture specifications. + +Producer-side verification in the `release-upload` job should require the signed provenance subject +name to equal the expected release asset name and the recalculated release asset SHA-256 digest to +equal `subject[0].digest.sha256` before upload. Consumer-side verification should similarly check +the downloaded asset bytes against the subject digest and check the subject name against the +expected release asset name when policy cares about the release asset slot. + +### Consequences + +- Good, because the SLSA subject name matches the GitHub Release asset identity that users upload + and consumers download. +- Good, because local workspace layout does not become part of the long-term verifier contract. +- Good, because the duplicate asset-name check selected by ADR 0043 and the subject-name check use + the same asset slot identifier. +- Good, because the digest remains the cryptographic artifact identity while the name remains the + stable policy label. +- Good, because the shape aligns with `sha256sum`-style SLSA GitHub Generator subjects and + `actions/attest` named artifact subjects. +- Neutral, because release repository, tag, URL, and API ID still need explicit representation in + other provenance fields or verification policy. +- Bad, because the same asset name can exist in different repositories, releases, or tags, so + subject name alone is not globally unique. +- Bad, because callers must choose final release asset names before provenance signing. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- exactly one SLSA subject for the production one-asset release profile path; +- `subject[0].name` as the final GitHub Release asset name requested for upload; +- rejection of local paths, repository-relative paths, URLs, GitHub API IDs, package URLs, and + provenance sidecar names as subject names; +- normalization and validation rules for release asset names; +- `subject[0].digest.sha256` as the lowercase hexadecimal SHA-256 digest of the exact uploaded asset + bytes, without a `sha256:` prefix; +- tool-boundary conversion rules for any APIs that require prefixed digest strings; +- producer-side verification that the signed subject name and digest match the expected asset before + release upload; +- consumer-side verifier guidance that checks both subject digest and expected release asset name; +- separate provenance or verification-policy fields for repository, release tag, release state, + release URLs, asset API IDs, and attestation storage identifiers. + +Implementation review should verify that production provenance cannot be signed or uploaded when the +subject name describes a different release asset slot than the upload target, or when the subject +digest describes bytes different from the uploaded asset. + +## Pros and Cons of the Options + +### Use the final GitHub Release asset name + +The subject name is the basename-style GitHub Release asset name that the profile will upload, such +as `slsa-builder_0.1.0_linux_amd64.tar.gz`. + +- Good, because it matches the user-visible release asset slot. +- Good, because it aligns subject verification with duplicate asset-name checks. +- Good, because it stays stable when local workspace paths or workflow artifact names change. +- Good, because it follows the filename-style subject convention used by checksum files, + `actions/attest` subject checksums, and the SLSA GitHub Generator generic workflow. +- Neutral, because globally unique release identity must still come from repository and tag fields. +- Bad, because the asset name must be known before signing and cannot be inferred only from the + local file path. + +### Use the repository-relative local artifact path + +The subject name is a path such as `dist/slsa-builder_0.1.0_linux_amd64.tar.gz`. + +- Good, because it can directly identify the file generated by the build job. +- Good, because it resembles `actions/attest` `subject-path` usage for local binaries. +- Bad, because it binds verifier policy to workspace layout rather than release asset identity. +- Bad, because upload-time renaming would make the subject name differ from the release asset name. +- Bad, because path normalization differs across platforms and shell tools. + +### Use the GitHub Release browser or download URL + +The subject name is a URL such as +`https://github.com/windlasstech/slsa-builder/releases/download/v0.1.0/foo.tar.gz`. + +- Good, because it can be globally recognizable to humans and scripts. +- Good, because it points at the consumer download surface after release publication. +- Bad, because URL shape, redirects, repository renames, and CDN behavior are provider-specific and + mutable. +- Bad, because draft-release upload flows may not have a final consumer-facing URL at signing time. +- Bad, because in-toto ResourceDescriptor has `uri` and `downloadLocation` fields for URI semantics; + overloading `name` with a URL weakens the distinction. + +### Use the GitHub Release asset API ID or node ID + +The subject name is a GitHub REST asset ID, GraphQL node ID, or API URL for the uploaded release +asset. + +- Good, because GitHub can identify the uploaded asset object precisely after upload. +- Good, because it avoids ambiguity among same-named files in different releases. +- Bad, because the asset ID does not exist until after upload, while ADR 0044 requires provenance to + be signed and verified before release mutation. +- Bad, because it would make the subject name GitHub-API-specific and less meaningful to consumers. +- Bad, because it conflicts with the failure-closed no-overwrite upload path: the expected asset + slot must be known before creating the asset object. + +### Use a package-url-like GitHub Release asset identifier + +The subject name is a custom package URL or URI-like identifier containing repository, tag, and +asset name. + +- Good, because it could make the subject name globally scoped without relying on GitHub download + URL stability. +- Good, because it can be useful for future package-ecosystem-style verifier indexing. +- Bad, because GitHub Release asset package URL conventions are not as established as npm package or + OCI image naming conventions. +- Bad, because it duplicates repository and tag fields that should be verified through the profile's + `externalParameters` and source or release metadata. +- Bad, because it is less compatible with `actions/attest` and SLSA GitHub Generator filename-style + subject conventions. + +## More Information + +This decision follows ADR 0038, ADR 0039, ADR 0043, and ADR 0044. It decides only the initial +production GitHub Release asset subject name and SHA-256 subject digest encoding. It does not decide +the complete release asset `externalParameters` schema, release URL outputs, GitHub asset API ID +outputs, `downloadLocation` usage, provenance sidecar naming, checksum file policy, SBOM policy, or +future multi-asset orchestration model. + +Reference points considered: + +- in-toto Statement v1 requires each subject to have a digest, treats subjects as immutable, and + says subject artifacts are matched purely by digest. The `name` field can distinguish artifacts + within the subject and may be used by policy when meaningful. +- in-toto ResourceDescriptor says `name` should be a stable machine-readable identifier such as a + filename, while `uri` and `downloadLocation` carry URI semantics. +- SLSA v1.2 Build Provenance represents build outputs as the top-level in-toto Statement `subject` + list and uses digest maps keyed by algorithm names such as `sha256`. +- SLSA v1.2 Verifying Artifacts recommends verifying that the Statement subject matches the digest + of the artifact being verified before checking `predicateType`, trusted builder identity, + `buildType`, and `externalParameters`. +- GitHub `actions/attest` binds a named artifact and digest to a predicate. Its `subject-digest` + input requires the form `sha256:HEX_DIGEST` and a `subject-name`, while checksum-file subjects use + `shasum`-style lines containing a hex digest and filename. +- The SLSA GitHub Generator generic workflow accepts subjects formatted like `sha256sum` output, + where the decoded input is a SHA-256 hash followed by an artifact name, and its examples use + artifact filenames as subject names for release artifacts. diff --git a/docs/decisions/0046-keep-checksums-and-sboms-out-of-release-asset-subject-digest.md b/docs/decisions/0046-keep-checksums-and-sboms-out-of-release-asset-subject-digest.md new file mode 100644 index 0000000..84404ad --- /dev/null +++ b/docs/decisions/0046-keep-checksums-and-sboms-out-of-release-asset-subject-digest.md @@ -0,0 +1,239 @@ +--- +parent: Decisions +nav_order: 46 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Keep Checksums and SBOMs Out of the Release Asset Subject Digest + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance for GitHub Release assets. ADR 0039 scoped the +initial production profile to exactly one GitHub Release asset per profile run and explicitly kept +checksum files, SBOM files, provenance sidecars, and detached signatures out of the low-level +verification unit for another asset. ADR 0044 selected a three-job graph that recalculates asset and +bundle digests across handoffs. ADR 0045 selected the final GitHub Release asset name as +`subject[0].name` and lowercase hexadecimal SHA-256 of the uploaded asset bytes as +`subject[0].digest.sha256`. + +The remaining digest relationship question is how the selected release asset subject digest relates +to checksum files and SBOMs. GitHub Releases commonly publish checksum manifests and SBOM files next +to binaries. Those files can be useful verification aids and may themselves be release assets, but +they are not the same bytes as the primary release asset. + +Should checksum files and SBOMs extend, replace, or otherwise participate in the initial GitHub +Release asset profile's SLSA subject digest? + +## Decision Drivers + +- Keep the one-asset profile's SLSA subject digest bound to exactly the selected release asset + bytes. +- Preserve ADR 0039's separation between the low-level one-asset profile and multi-asset release + orchestration. +- Avoid treating checksum manifests or SBOM documents as substitutes for SLSA build provenance over + the artifact they describe. +- Allow checksum files, SBOMs, provenance sidecars, and detached signatures to be independently + published and attested when they are release assets. +- Keep consumer verification simple: the downloaded asset bytes must match the SLSA subject digest. +- Keep cross-asset completeness, checksum manifest generation, SBOM set publication, and + whole-release policy in the orchestration layer. +- Align with SLSA v1.2's artifact-bound provenance guidance and GitHub `actions/attest` SBOM mode, + where the artifact remains the subject and the SBOM is the predicate. + +## Considered Options + +- Keep checksum files and SBOMs out of the release asset subject digest. +- Add checksum file and SBOM digests to the primary release asset subject digest map. +- Include the release asset, checksum file, and SBOM as multiple subjects in one provenance + statement. +- Use the checksum manifest as the primary SLSA subject. +- Use the SBOM file as the primary SLSA subject. + +## Decision Outcome + +Chosen option: "Keep checksum files and SBOMs out of the release asset subject digest", because the +initial GitHub Release asset profile's SLSA subject should describe exactly the selected asset +bytes, while checksum files and SBOMs are either independent release assets or supporting metadata. + +The production GitHub Release asset profile should keep the SLSA subject digest limited to the one +selected release asset: + +```json +"subject": [ + { + "name": "", + "digest": { + "sha256": "" + } + } +] +``` + +Checksum files, SBOM files, provenance sidecars, detached signatures, and other supporting files do +not extend or replace `subject[0].digest`. Their digests should not be inserted into the selected +release asset subject digest map under custom keys. The subject digest map should contain only +cryptographic digests of the selected release asset bytes, keyed by digest algorithm. + +When a checksum file, SBOM file, provenance sidecar, detached signature, or similar supporting file +is published as a GitHub Release asset, it should be handled by its own one-asset profile +invocation. For example, a `checksums.txt` release asset can have +`subject[0].name == "checksums.txt"` and a subject digest over the `checksums.txt` bytes. An SBOM +release asset can similarly be attested as the asset whose bytes are uploaded. + +When checksum files or SBOMs are generated only to support verification, diagnostics, release notes, +or orchestration policy, they may be recorded as byproducts, referenced by orchestration-layer +metadata, linked through a future release manifest, or attested with a separate predicate. They are +not part of the low-level profile's primary SLSA subject for another release asset. + +The architecture specification may require optional consistency checks when the caller supplies a +checksum file or SBOM for the selected asset. For example, a checksum file entry for the selected +asset may be required to match `subject[0].digest.sha256`, or an SBOM package/component hash for the +selected asset may be required to match the same bytes. Such checks are consistency gates; they do +not make the checksum file or SBOM bytes part of the selected asset subject digest. + +Cross-asset completeness checks remain orchestration-layer responsibilities. A higher-level release +workflow may verify that every asset listed in a checksum manifest was uploaded and attested, that +every binary has an SBOM, or that all SBOMs and checksum files are present before publishing an +immutable release. That orchestration must compose one-asset profile invocations rather than +changing the low-level profile's subject digest semantics. + +### Consequences + +- Good, because the SLSA subject digest has one clear meaning: the digest of the selected release + asset bytes. +- Good, because checksum manifests and SBOMs cannot accidentally become substitutes for + artifact-bound build provenance. +- Good, because checksum files, SBOMs, provenance sidecars, and detached signatures can still be + independently published and attested as release assets. +- Good, because consumer verification remains a direct comparison between downloaded asset bytes and + `subject[0].digest.sha256`. +- Good, because whole-release completeness stays in the orchestration layer selected by ADR 0039 and + ADR 0043. +- Neutral, because optional checksum or SBOM consistency checks may still be specified later without + changing the primary subject digest. +- Bad, because the low-level profile alone cannot prove that a complete release has a correct + checksum manifest and SBOM set. +- Bad, because callers that want whole-release guarantees must use additional orchestration or + release manifest policy. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- `subject[0].digest.sha256` as the digest of the selected release asset bytes only; +- no checksum file, SBOM file, provenance sidecar, detached signature, release manifest, or other + supporting-file digest in the selected release asset subject digest map; +- separate one-asset profile invocations when checksum files, SBOMs, provenance sidecars, detached + signatures, or similar files are themselves published GitHub Release assets; +- optional consistency checks, if supported, that compare checksum-file entries or SBOM component + hashes with the selected asset subject digest without changing subject semantics; +- byproduct or orchestration metadata rules for non-primary checksum files and SBOMs; +- documentation that whole-release checksum/SBOM completeness belongs to an orchestration layer or + future release manifest, not the low-level one-asset profile. + +Implementation review should verify that no production path can sign or upload provenance where the +selected release asset subject digest describes a checksum file, SBOM, sidecar, signature, manifest, +or any bytes other than the uploaded release asset. + +## Pros and Cons of the Options + +### Keep checksum files and SBOMs out of the release asset subject digest + +The selected release asset is the only SLSA subject. Checksum files and SBOMs are either independent +release assets, byproducts, consistency inputs, or orchestration metadata. + +- Good, because it preserves one subject and one primary digest per profile run. +- Good, because it matches SLSA's artifact-bound provenance model. +- Good, because it composes cleanly with releases that publish many binaries, checksum files, SBOMs, + provenance sidecars, and signatures. +- Good, because SBOM attestation can still describe the selected asset using an SBOM predicate + rather than changing the build provenance subject. +- Neutral, because the architecture still needs to define optional consistency checks and byproduct + fields. +- Bad, because whole-release completeness requires another layer. + +### Add checksum file and SBOM digests to the primary subject digest map + +The selected release asset subject contains additional digest-map keys for related checksum files or +SBOM files. + +- Good, because one subject entry appears to carry all related digest handles. +- Good, because producer-side checks could compare related files in one place. +- Bad, because a SLSA subject digest map is naturally an algorithm-keyed map for the same subject + bytes, not a collection of digests for different files. +- Bad, because custom digest keys for other files can confuse generic in-toto and SLSA tooling. +- Bad, because it blurs whether the subject is the release asset, the checksum file, the SBOM, or a + bundle of all three. + +### Include the release asset, checksum file, and SBOM as multiple subjects + +One provenance statement contains separate subject entries for the primary release asset, checksums, +SBOMs, and related files. + +- Good, because in-toto and `actions/attest` can represent multiple named subjects. +- Good, because each listed file has its own digest without overloading one digest map. +- Bad, because ADR 0039 selected exactly one production release asset subject for the initial + profile. +- Bad, because partial upload, duplicate names, retries, and verification failures become + multi-asset problems. +- Bad, because checksum files and SBOMs may describe assets outside the current profile run. + +### Use the checksum manifest as the primary SLSA subject + +The SLSA subject is the checksum file, and the checksum file lists the release asset digest. + +- Good, because checksum manifests are familiar release verification artifacts. +- Good, because one manifest can reference many release assets. +- Bad, because the SLSA provenance describes the manifest bytes rather than the selected release + asset bytes. +- Bad, because ADR 0039 rejected checksum-manifest-centered verification for the low-level profile. +- Bad, because manifest completeness and synchronization with uploaded assets are orchestration + concerns. + +### Use the SBOM file as the primary SLSA subject + +The SLSA subject is the SBOM file, and the SBOM package or component hashes describe the release +asset. + +- Good, because SPDX and CycloneDX can record package or component checksums. +- Good, because SBOM tooling can consume dependency and component metadata directly. +- Bad, because the SLSA build provenance subject becomes metadata rather than the built artifact. +- Bad, because GitHub `actions/attest` SBOM mode keeps the artifact as the subject and the SBOM as + the predicate, not the other way around. +- Bad, because SBOM format choices and optional availability would affect the primary build + provenance verification path. + +## More Information + +This decision follows ADR 0038, ADR 0039, ADR 0043, ADR 0044, and ADR 0045. It decides only the +relationship between the selected release asset subject digest and checksum or SBOM files. It does +not decide the checksum manifest schema, SBOM format, SBOM predicate policy, provenance sidecar +naming, detached signature policy, release manifest schema, or future whole-release orchestration +workflow. + +Reference points considered: + +- SLSA v1.2 Distributing Provenance says attestations should be bound to artifacts rather than + releases and that provenance should accompany the artifact at publish time. +- SLSA v1.2 Build Provenance represents build outputs through the top-level in-toto Statement + `subject` list and uses `byproducts` for additional artifacts generated during the build that are + not considered build outputs. +- SLSA v1.2 Verifying Artifacts recommends verifying that the Statement subject matches the digest + of the artifact being verified before checking `predicateType`, trusted builder identity, + `buildType`, and `externalParameters`. +- in-toto Statement v1 requires subject elements to have digest values and says subject artifacts + are matched purely by digest. +- in-toto ResourceDescriptor says a digest-bearing descriptor is assumed to refer to an immutable + resource or artifact, and the predicate type should document which field consumers match. +- GitHub `actions/attest` SBOM mode takes both `subject-path` for the artifact and `sbom-path` for + the SBOM predicate. Its checksum-file subject mode enumerates subjects from `shasum`-style lines, + but those lines identify attestation subjects rather than modifying one subject's digest map. +- The SLSA GitHub Generator generic workflow accepts `sha256sum`-style subject inputs, and common + release tooling such as GoReleaser and JReleaser can generate checksum files for release + artifacts. +- SPDX and CycloneDX SBOM formats can carry package or component hashes, but those hashes are SBOM + metadata about described artifacts rather than replacements for SLSA build provenance subject + digests. diff --git a/docs/decisions/0048-make-linked-artifacts-storage-records-explicit-opt-in-for-release-assets.md b/docs/decisions/0048-make-linked-artifacts-storage-records-explicit-opt-in-for-release-assets.md new file mode 100644 index 0000000..440981e --- /dev/null +++ b/docs/decisions/0048-make-linked-artifacts-storage-records-explicit-opt-in-for-release-assets.md @@ -0,0 +1,227 @@ +--- +parent: Decisions +nav_order: 48 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Make Linked Artifacts Storage Records Explicit Opt-In for Release Assets + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance for GitHub Release assets with `actions/attest` +as the initial Sigstore signing and storage adapter. ADR 0044 selected a three-job permission +boundary and allowed `artifact-metadata: write` only when linked artifact metadata is intentionally +created. ADR 0047 then selected GitHub artifact attestation storage as the canonical release asset +provenance distribution channel, with release asset bundle sidecars only as an optional export path. + +GitHub's linked artifacts page can display storage and deployment records for artifacts built by an +organization. GitHub recommends uploading attested assets to the linked artifacts page so teams can +connect vulnerable artifacts to owning repositories, build runs, storage locations, and deployment +context. GitHub `actions/attest` can automatically create linked-artifact storage records, but the +documented automatic path is gated by `push-to-registry: true` and `artifact-metadata: write`, which +fits registry-published artifacts such as container images. GitHub Release assets are not pushed to +a package registry by the release asset profile. + +Should the initial GitHub Release asset profile upload linked-artifact storage records by default, +make them optional through the artifact metadata REST API, rely on `actions/attest` automatic +storage records, or leave the feature to higher-level orchestration? + +## Decision Drivers + +- Preserve the low-level one-release-asset profile boundary from ADR 0039. +- Avoid granting `artifact-metadata: write` unless linked artifact metadata is explicitly requested. +- Avoid treating a GitHub Release asset as a registry-pushed artifact just to trigger + `actions/attest` automatic storage record creation. +- Keep the default release asset path focused on signed provenance, producer-side verification, and + upload to an existing GitHub Release. +- Allow organizations that use GitHub linked artifacts to record release asset storage metadata. +- Ensure storage record metadata describes the uploaded release asset URL, name, digest, version, + and repository accurately. +- Avoid rollback or clobber behavior when linked metadata creation fails after the release asset has + already been uploaded. +- Keep deployment records and whole-release inventory policy outside the low-level asset profile. + +## Considered Options + +- Do not upload linked-artifact storage records by default; support explicit REST API opt-in after + verified release asset upload. +- Never support linked-artifact storage records in the low-level release asset profile. +- Require linked-artifact storage records for every release asset upload. +- Use `actions/attest` automatic storage records through `push-to-registry: true`. +- Leave all linked artifact storage and deployment records to a higher-level orchestration workflow. + +## Decision Outcome + +Chosen option: "Do not upload linked-artifact storage records by default; support explicit REST API +opt-in after verified release asset upload", because it preserves the default least-privilege +release asset path while allowing organizations to populate GitHub's linked artifacts inventory when +they intentionally accept the extra permission and metadata lifecycle. + +The initial GitHub Release asset profile should not create linked-artifact storage records by +default. The default production path should not request `artifact-metadata: write`, should not call +the artifact metadata REST API, and should not configure `actions/attest` only to make linked +artifact metadata appear. + +The profile may support an explicit opt-in input for linked-artifact storage record creation. When +enabled, storage record creation should happen only after the `release-upload` job has verified the +signed provenance bundle and successfully uploaded the selected release asset to the existing GitHub +Release. The implementation should use GitHub's artifact metadata REST API rather than +`actions/attest` `push-to-registry: true`, because the subject is a GitHub Release asset rather than +a registry-pushed package or container image. + +The linked-artifact storage record should describe the uploaded release asset, not the provenance +bundle, checksum file, SBOM, or whole GitHub Release. The architecture specification should define +the exact mapping, including at least: + +- `name` from the final GitHub Release asset name; +- `digest` as `sha256:`; +- `version` from the release tag or a profile-defined normalized version; +- `artifact_url` from the uploaded GitHub Release asset URL when available; +- `registry_url` as the profile-defined GitHub Release storage surface base URL; +- `repository` as the profile-defined repository or release asset storage path component; +- `github_repository` only when required by the API because an associated provenance attestation is + not available or not discoverable. + +When linked-artifact upload is enabled, the workflow should grant `artifact-metadata: write` only to +the job that creates the storage record. That job should not have `id-token: write`, +`attestations: write`, or GitHub Release mutation authority unless a later ADR explicitly combines +those responsibilities. Prefer a post-upload metadata job or a tightly scoped post-upload step over +adding metadata permissions to caller-controlled build steps. + +If the selected release asset uploads successfully but linked-artifact storage record creation +fails, the workflow should fail clearly without deleting, replacing, or clobbering the uploaded +release asset. The failure message and outputs should make the partial state explicit: the release +asset was uploaded, but linked artifact metadata was not recorded. Operators can then retry metadata +creation or use the artifact metadata REST API manually. + +Deployment records are out of scope for the low-level release asset profile. They describe runtime +or environment deployment state and should be handled by deployment systems, integrations, custom +REST API automation, or a higher-level orchestration workflow. + +### Consequences + +- Good, because the default release asset profile remains least-privilege and does not require + `artifact-metadata: write`. +- Good, because GitHub Release assets are not forced into the `actions/attest` registry-push model. +- Good, because organizations that use linked artifacts can still opt into accurate storage records. +- Good, because metadata creation happens after the signed asset has been verified and uploaded. +- Good, because metadata permissions are isolated from caller-controlled build steps and signing + authority. +- Neutral, because projects that want linked artifacts must provide additional inputs and accept a + post-upload metadata step. +- Bad, because linked artifacts inventory is incomplete for default release asset profile users. +- Bad, because an opt-in metadata failure can leave an uploaded release asset without its linked + storage record and requires manual or automated follow-up. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and documentation define: + +- linked-artifact storage record creation as disabled by default; +- no `artifact-metadata: write` permission in the default production path; +- an explicit opt-in input for storage record creation, if supported; +- artifact metadata REST API usage for GitHub Release asset storage records rather than + `actions/attest` `push-to-registry: true`; +- release asset metadata field mapping for `name`, `digest`, `version`, `artifact_url`, + `registry_url`, `repository`, and `github_repository` when applicable; +- storage record creation after verified release asset upload; +- clear failure behavior that does not delete, overwrite, or clobber uploaded release assets after + metadata failure; +- job-level permission isolation for `artifact-metadata: write`; +- documentation that deployment records and whole-release inventory policy belong outside the + low-level one-asset profile. + +Implementation review should verify that no default path grants artifact metadata write permission, +that no path pretends a GitHub Release asset is a registry-pushed artifact to trigger automatic +storage records, and that metadata failures cannot cause release asset overwrite or deletion. + +## Pros and Cons of the Options + +### Do not upload by default; support explicit REST API opt-in + +The low-level profile defaults to no linked-artifact storage record. When the caller opts in, a +post-upload step or job calls the artifact metadata REST API with metadata for the uploaded GitHub +Release asset. + +- Good, because it keeps the common path minimal and least-privilege. +- Good, because release asset metadata can use the final uploaded asset URL and digest. +- Good, because the feature composes with GitHub organizations that use linked artifacts for alert + prioritization. +- Good, because metadata permissions can be isolated to a narrow job or step. +- Neutral, because the profile must define a GitHub Release storage mapping for a registry-oriented + API shape. +- Bad, because the optional path requires more implementation and failure-state documentation. + +### Never support linked-artifact storage records in the low-level profile + +The profile produces signed provenance and uploads release assets, but it never records linked +artifact storage metadata. + +- Good, because it is the simplest low-level profile boundary. +- Good, because no additional permission or API lifecycle is needed. +- Bad, because organizations using GitHub linked artifacts cannot see release asset storage context + from this profile. +- Bad, because every repository would need custom follow-up automation for storage inventory. + +### Require linked-artifact storage records for every release asset upload + +Every production release asset upload also creates or updates a linked-artifact storage record. + +- Good, because organization inventory is complete for all release assets produced by the profile. +- Good, because security alert prioritization can rely on consistent storage metadata. +- Bad, because the default profile requires broader permissions and a GitHub organization feature. +- Bad, because metadata API failure becomes a required release failure after asset upload. +- Bad, because this adds lifecycle complexity before the initial release asset profile needs it. + +### Use `actions/attest` automatic storage records through `push-to-registry: true` + +The profile configures `actions/attest` to create storage records automatically by enabling +`push-to-registry` and granting `artifact-metadata: write`. + +- Good, because it follows GitHub's automatic storage record path for registry artifacts. +- Good, because storage record IDs can be exposed from the `actions/attest` outputs. +- Bad, because `push-to-registry` is documented for registry-published subjects with fully qualified + image or package names, not ordinary GitHub Release assets. +- Bad, because it can misrepresent the release asset as a registry artifact and blur storage + semantics. +- Bad, because it couples signing adapter configuration to linked-artifact inventory behavior. + +### Leave all linked artifact records to a higher-level orchestration workflow + +A release orchestration workflow creates storage and deployment records after all one-asset profile +invocations have completed. + +- Good, because whole-release inventory and deployment context naturally belong above the low-level + one-asset profile. +- Good, because the orchestrator can handle multi-asset releases, checksum files, SBOMs, and release + manifests together. +- Good, because the low-level profile remains simpler. +- Neutral, because this can coexist with a low-level opt-in storage record mode if scopes are clear. +- Bad, because users of only the low-level profile do not get linked artifact storage records + without additional tooling. + +## More Information + +This decision follows ADR 0038, ADR 0039, ADR 0043, ADR 0044, ADR 0045, ADR 0046, and ADR 0047. It +decides only linked-artifact storage record policy for the initial GitHub Release asset profile. It +does not decide deployment record policy, whole-release inventory aggregation, linked artifact UI +usage, alert prioritization rules, sidecar bundle naming, or release manifest schema. + +Reference points considered: + +- GitHub's artifact attestation guide recommends uploading attested assets to the organization's + linked artifacts page for build history, deployment records, storage details, and security alert + prioritization. +- GitHub's `actions/attest` automatic storage record path requires both `push-to-registry: true` and + `artifact-metadata: write`. +- GitHub's linked artifacts guide describes artifact metadata storage records as creatable through + artifact attestations, JFrog integration, or the artifact metadata REST API. +- GitHub's artifact metadata storage record REST API accepts artifact `name`, `digest`, `version`, + `artifact_url`, `registry_url`, `repository`, `status`, and `github_repository` metadata. +- GitHub's linked artifacts guide says unwanted records cannot be deleted from the linked artifacts + page, though storage or deployment records can be updated to reflect artifact status. +- Windlass workflow hardening guidance requires explicit minimal permissions and job-level elevation + only where required, including `artifact-metadata: write` only for linked artifact metadata. From 455df38acf134aeaf53496401a54ae30776a6182 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:16 +0900 Subject: [PATCH 07/14] docs(decisions): add release asset permission boundary ADRs ADR-0044 introduces a three-job permission boundary; ADR-0049 supersedes it by separating artifact production from release asset publication. Signed-off-by: Yunseo Kim --- ...e-job-release-asset-permission-boundary.md | 270 ++++++++++++++++++ ...n-from-github-release-asset-publication.md | 263 +++++++++++++++++ 2 files changed, 533 insertions(+) create mode 100644 docs/decisions/0044-use-three-job-release-asset-permission-boundary.md create mode 100644 docs/decisions/0049-separate-artifact-production-from-github-release-asset-publication.md diff --git a/docs/decisions/0044-use-three-job-release-asset-permission-boundary.md b/docs/decisions/0044-use-three-job-release-asset-permission-boundary.md new file mode 100644 index 0000000..75c9805 --- /dev/null +++ b/docs/decisions/0044-use-three-job-release-asset-permission-boundary.md @@ -0,0 +1,270 @@ +--- +parent: Decisions +nav_order: 44 +status: superseded by ADR-0049 +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Use a Three-Job Permission Boundary for GitHub Release Assets + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance with `actions/attest` as the signing and +storage adapter for GitHub Release assets. ADR 0039 scoped the profile to one explicitly named +release asset per profile run. ADR 0043 then selected an upload-only lifecycle policy: the profile +uploads one asset to an existing GitHub Release selected by an existing tag, fails when the tag or +release is missing, and never overwrites an existing asset. + +The remaining boundary is how the GitHub Actions jobs and `GITHUB_TOKEN` permissions should be +split. The profile needs to run caller-controlled build or packaging steps, sign Windlass-generated +SLSA provenance, and mutate a GitHub Release by uploading one asset. Combining those +responsibilities in a single job would put provenance signing authority and release mutation +authority in the same runner environment as user-defined build steps. + +Should the initial GitHub Release asset profile use one job, two jobs, three jobs, or leave release +upload outside the reusable profile? + +## Decision Drivers + +- Keep caller-controlled build and packaging steps away from provenance signing permissions. +- Keep caller-controlled build and packaging steps away from GitHub Release mutation authority. +- Preserve SLSA v1.2 Build L3's requirement that provenance authentication material is not + accessible to user-defined build steps. +- Keep `actions/attest` OIDC and attestation-storage permissions scoped to the job that signs + provenance. +- Keep `contents: write` release mutation authority scoped to the job that uploads the verified + release asset. +- Require release upload to verify both the asset and signed provenance bundle before mutating the + existing GitHub Release. +- Match ADR 0036's npm package precedent: build, signing, and publish or upload mutation are + separate digest-verified jobs. +- Follow Windlass workflow hardening guidance: minimal top-level permissions and job-level elevation + only where required. + +## Considered Options + +- Use a three-job digest-verified graph: `build` → `provenance-sign` → `release-upload`. +- Use one job for build, provenance signing, and release upload. +- Use two jobs: `build` → `sign-and-upload`. +- Use two jobs: `build-and-sign` → `release-upload`. +- Build and sign inside the reusable workflow, then leave release upload to the caller or + orchestration workflow. + +## Decision Outcome + +Chosen option: "Use a three-job digest-verified graph: `build` → `provenance-sign` → +`release-upload`", because it gives the initial GitHub Release asset profile clear job isolation, +minimal permission scopes, and an upload-time verification gate before GitHub Release mutation. + +The initial production GitHub Release asset SLSA3 reusable workflow should define three primary +jobs: + +```text +build -> provenance-sign -> release-upload +``` + +The `build` job should run the profile's caller-controlled build or packaging contract, produce the +single release asset file, validate that the input resolves to exactly one file, calculate the asset +digest, upload the asset as a workflow artifact, and expose only the asset identity and digest +handles needed by later jobs and public workflow outputs. + +The `provenance-sign` job should download the asset artifact, recalculate its digest, compare it +with the `build` job's expected digest outputs, generate the Windlass SLSA provenance v1 statement +for exactly that asset, verify that the statement subject matches the asset digest, sign the +statement through the SHA-pinned `actions/attest` adapter selected by ADR 0038, and upload the +resulting signed Sigstore bundle as a workflow artifact. The signing job should not have GitHub +Release mutation authority. + +The `release-upload` job should download the asset and signed provenance bundle artifacts, +recalculate both artifact digests, verify that those digests match the handoff values produced by +the previous jobs, verify the signed provenance bundle against the expected Sigstore root and GitHub +Actions reusable workflow signer identity, extract and verify the SLSA statement expectations, and +only then upload the verified asset to the existing GitHub Release selected by the requested tag. +The upload job should fail before release mutation if any digest, signature, signer identity, asset +identity, subject, `predicateType`, `builder.id`, `buildType`, `externalParameters`, source +identity, release tag, release state, or duplicate-asset check fails. + +The initial job permission boundary should be: + +- `build`: `contents: read`; no `id-token: write`; no `attestations: write`; no `contents: write`; + no release mutation authority. +- `provenance-sign`: `contents: read`, `id-token: write`, `attestations: write` when GitHub + attestation storage is used, and optional `artifact-metadata: write` only when linked artifact + metadata is intentionally created; no `contents: write`; no release mutation authority. +- `release-upload`: `contents: write`; no `id-token: write`; no `attestations: write`; no signing + authority. The job should use its write permission only for the ADR 0043-selected operation of + uploading one non-duplicate asset to an existing GitHub Release. + +The reusable workflow should set minimal top-level permissions, preferably `permissions: {}` or the +smallest read-only default that GitHub Actions permits for the implementation, and should grant the +above permissions at the job level. Caller documentation should make the required caller workflow +permissions explicit so that the called workflow can receive the needed job-level permissions +without granting broad default write access to every job. + +Cross-job handoff should use immutable GitHub workflow artifacts and trusted job outputs as handles. +The architecture specification should define exact artifact names or IDs, digest encodings, digest +algorithms, download locations, and failure behavior. Jobs must not trust artifact names, paths, +workflow outputs, or logs as substitutes for recalculating digests and verifying the signed +provenance bundle. Unsigned public workflow outputs such as uploaded asset name, asset URL, or +digest remain release handles only; they are not substitutes for signed provenance verification. + +The `release-upload` job should not create tags, create releases, publish draft releases, edit +release notes, change prerelease state, change latest-release state, delete assets, overwrite +assets, or invoke clobber behavior. Those lifecycle mutations remain outside the low-level profile +under ADR 0043. + +### Consequences + +- Good, because caller-controlled build steps cannot access provenance signing authority. +- Good, because caller-controlled build steps cannot access GitHub Release mutation authority. +- Good, because `actions/attest` OIDC and attestation permissions are scoped to the signing job. +- Good, because `contents: write` release mutation authority is scoped to the upload job. +- Good, because release upload is gated by digest checks and signed provenance verification. +- Good, because the job graph mirrors the npm package profile's ADR 0036 split while adapting the + mutation boundary from npm publish to GitHub Release asset upload. +- Good, because a future `sigstore-go` migration can replace signing internals without collapsing + the build/sign/upload boundary. +- Neutral, because the upload job performs producer-side verification; final consumers should still + verify downloaded assets independently. +- Bad, because the workflow has more jobs, handoff artifacts, and verification logic than one- or + two-job designs. +- Bad, because Windlass must maintain tooling to verify its own signed bundle before release upload. +- Bad, because artifact handoff implementation must be tested carefully to avoid name/path + confusion, stale artifacts, digest encoding drift, or accidental fallback behavior. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- exactly three primary production jobs named or equivalent to `build`, `provenance-sign`, and + `release-upload`; +- top-level workflow permissions that default to no access or minimal read-only access; +- job-level permission requirements for `build`, `provenance-sign`, and `release-upload`; +- absence of signing and release mutation authority from caller-controlled build steps; +- absence of release mutation authority from the signing job; +- absence of signing authority from the release upload job; +- release asset artifact upload, download, digest calculation, and digest comparison behavior; +- signed provenance bundle artifact upload, download, digest calculation, and digest comparison + behavior; +- Windlass SLSA statement generation and subject digest verification before signing; +- Sigstore bundle verification in the `release-upload` job before GitHub Release mutation; +- upload-time checks for signer identity, `predicateType`, `builder.id`, `buildType`, + `externalParameters`, source identity, release tag, release state, asset name, and asset digest; +- hard-failure behavior for missing artifacts, digest mismatch, signature mismatch, unexpected + provenance fields, missing tag, missing release, duplicate asset name, and any attempted fallback + to unsigned provenance, `actions/attest` default provenance, release creation, draft publication, + or asset clobbering. + +Implementation review should verify that no path can upload a GitHub Release asset under the +production SLSA3 builder identity unless the asset and signed Windlass provenance bundle have both +passed the `release-upload` job's verification gate. + +## Pros and Cons of the Options + +### Use a three-job digest-verified graph + +The reusable workflow separates asset creation, provenance signing, and GitHub Release mutation into +`build`, `provenance-sign`, and `release-upload` jobs. Each cross-job artifact handoff is checked by +digest, and the upload job verifies the signed provenance before uploading the asset. + +- Good, because it creates the clearest permission and trust boundary for the initial GitHub Release + asset profile. +- Good, because build steps cannot access signing credentials or release mutation authority. +- Good, because signing and release upload authorities are never present in the same job. +- Good, because the upload job can behave like a producer-side verifier before mutating the release. +- Good, because it follows the SLSA GitHub Generator and ADR 0036 pattern of job isolation plus + hash-protected artifact exchange. +- Good, because it keeps Windlass-generated provenance, not workflow outputs, as the verifier source + of truth. +- Bad, because it requires more implementation and testing than simpler job graphs. +- Bad, because bundle verification logic must exist before upload, not only in downstream verifier + tooling. + +### Use one job for build, signing, and release upload + +One job checks out source, runs build or packaging steps, generates and signs provenance, and +uploads the release asset. + +- Good, because it is simple and avoids artifact upload/download handoff. +- Good, because local paths are easy to reason about during an early prototype. +- Bad, because caller-controlled build steps share a runner environment with signing and release + mutation permissions. +- Bad, because it weakens the SLSA Build L3 signing-key isolation story. +- Bad, because a compromised build step could both influence provenance inputs and attempt release + mutation. + +### Use two jobs: `build` then `sign-and-upload` + +The build job creates the asset, and a second job verifies the asset, signs provenance, and uploads +the release asset. + +- Good, because build steps are isolated from signing and release mutation permissions. +- Good, because it is simpler than a three-job graph. +- Good, because the privileged job can verify the asset before signing and uploading. +- Bad, because provenance signing and GitHub Release mutation share one job boundary. +- Bad, because upload does not consume a previously signed bundle through an explicit verification + handoff. +- Bad, because a signing-adapter failure or fallback path is closer to the release mutation + boundary. + +### Use two jobs: `build-and-sign` then `release-upload` + +The first job creates the asset and signs provenance. The second job verifies the signed bundle and +uploads the asset. + +- Good, because release mutation authority is isolated from signing authority. +- Good, because the upload job can verify the signed provenance before mutation. +- Bad, because caller-controlled build steps share a runner environment with provenance signing + permissions. +- Bad, because this is a weaker fit for SLSA Build L3 signing-key isolation than the selected + three-job split. +- Bad, because it conflicts with ADR 0038's requirement to keep signing and GitHub attestation + storage permissions away from caller-controlled build steps. + +### Build and sign in the reusable workflow, then leave upload to the caller + +The trusted reusable workflow returns the release asset and signed provenance bundle; the caller or +a separate orchestration workflow downloads them and performs GitHub Release upload. + +- Good, because the low-level profile would not need `contents: write` release mutation authority. +- Good, because callers could integrate custom release upload or orchestration logic. +- Bad, because the production release asset profile would not control the release mutation path + selected by ADR 0043. +- Bad, because callers could upload without verifying the signed provenance bundle or could upload a + different asset from the one that was signed. +- Bad, because upload failures, duplicate asset behavior, and downgrade behavior would become + caller-specific rather than profile-specified. + +## More Information + +This decision follows ADR 0038, ADR 0039, ADR 0040, and ADR 0043. It decides the initial production +GitHub Release asset profile's job graph and GitHub token permission boundary only. It does not +change the release asset lifecycle policy, release creation policy, immutable release requirement, +canonical provenance contents, signing adapter choice, public workflow outputs, or future +orchestration workflow interface. + +Reference points considered: + +- SLSA v1.2 Build Requirements require authentic provenance at Build L2 and unforgeable provenance + at Build L3. Secret material used to authenticate provenance must not be accessible to + user-defined build steps, and builds must run in isolated, ephemeral environments. +- SLSA v1.2 Verifying Artifacts recommends checking the provenance envelope signature, subject + digest, `predicateType`, trusted builder identity, `buildType`, and `externalParameters`; + unrecognized external parameters should generally fail verification. +- GitHub Actions supports `permissions` at workflow and job level, and when any explicit permission + set is specified, unspecified permissions are set to `none`. +- Windlass workflow hardening guidance requires explicit minimal top-level permissions and job-level + elevation only when required. +- GitHub `actions/attest` generates Sigstore-signed in-toto attestations, supports custom predicate + mode, exposes a `bundle-path` output, and requires `id-token: write` plus `attestations: write` + only where signing and attestation storage occur. +- GitHub Release asset upload uses GitHub Release mutation authority, represented in GitHub Actions + by `contents: write`; ADR 0043 narrows that mutation to one non-duplicate asset upload on an + existing release. +- The SLSA GitHub Generator generic workflow examples use a separate provenance job with + `id-token: write` and `contents: write` when uploading provenance to releases, while the Node.js + builder examples use expected SHA-256 values and secure download actions before package publish. +- ADR 0036 selected the stricter Windlass npm pattern of separate build, signing, and mutation jobs + with digest-verified handoff, which this decision adapts to GitHub Release assets. diff --git a/docs/decisions/0049-separate-artifact-production-from-github-release-asset-publication.md b/docs/decisions/0049-separate-artifact-production-from-github-release-asset-publication.md new file mode 100644 index 0000000..5af04e7 --- /dev/null +++ b/docs/decisions/0049-separate-artifact-production-from-github-release-asset-publication.md @@ -0,0 +1,263 @@ +--- +parent: Decisions +nav_order: 49 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Separate Artifact Production from GitHub Release Asset Publication + +## Context and Problem Statement + +ADR 0038 through ADR 0048 defined an initial GitHub Release asset profile as a sibling production +profile that would generate Windlass-owned SLSA provenance, sign it with `actions/attest`, and +upload one release asset to an existing GitHub Release. Those decisions intentionally deferred the +last unresolved boundary: how the release asset bytes are produced or ingested before signing and +upload. + +Further review of SLSA v1.2, `slsa-github-generator`, GitHub reusable workflows, and +`actions/attest` shows that a generic GitHub Release asset build profile would either need to run a +broad repository-defined build recipe or accept prebuilt artifacts from caller workflows. The first +path risks becoming a lowest-common-denominator generic build runner. The second path more closely +resembles a provenance generator or promotion workflow, because the release asset bytes are produced +outside the GitHub Release asset profile's trusted build boundary. + +Should Windlass keep a generic GitHub Release asset build profile, redefine the GitHub Release asset +workflow as a publisher that creates its own publication attestation, or redefine it as a verified +distributor that republishes producer-generated provenance unchanged? + +## Decision Drivers + +- Preserve the profile-owned architecture from ADR 0002 and ADR 0003 without turning the GitHub + Release asset surface into a generic arbitrary build runner. +- Keep source-to-artifact build semantics in ecosystem-specific profiles that can define precise + toolchain, dependency, subject, and verifier contracts. +- Keep GitHub Release upload policy separate from artifact production policy. +- Avoid claiming that a publisher workflow built an artifact from source when it only received, + verified, and uploaded already-produced bytes. +- Preserve SLSA v1.2 Build L3 expectations that `externalParameters` are complete and that signing + authority is isolated from user-controlled build steps. +- Keep room for Go, Rust, JS/TS, container, archive, and other producer profiles to publish through + the same GitHub Release asset publication surface. +- Require any production publication path to fail closed when upstream producer provenance is + missing or does not match the artifact being uploaded. +- Avoid the cost and semantic ambiguity of a publisher-owned SLSA provenance or custom publication + predicate in the default production path. + +## Considered Options + +- Redefine GitHub Release asset handling as a verified distributor for ecosystem-produced artifacts + and producer-generated provenance. +- Redefine GitHub Release asset handling as a publisher that signs its own publication or binding + attestation. +- Keep a generic GitHub Release asset build profile that runs a source-controlled build recipe. +- Accept arbitrary caller-provided build commands in the GitHub Release asset profile. +- Accept caller-produced workflow artifacts without mandatory upstream provenance verification. +- Skip a GitHub Release asset profile and leave publication entirely to ecosystem-specific profiles. + +## Decision Outcome + +Chosen option: "Redefine GitHub Release asset handling as a verified distributor for +ecosystem-produced artifacts and producer-generated provenance", because GitHub Release upload is a +distribution concern while the artifact's source-to-bytes SLSA provenance belongs to the producer +profile that knows the ecosystem. + +Windlass will not ship a generic production GitHub Release asset build profile. Ecosystem-specific +producer profiles should produce final artifact bytes and source-to-artifact SLSA provenance. A +GitHub Release asset publisher profile should then verify the produced artifact and its upstream +producer provenance, upload the exact verified bytes to an existing GitHub Release, and publish or +expose the producer-generated provenance without modifying or re-signing it. + +The publisher profile's public workflow entrypoint should be renamed away from +`.github/workflows/github-release-asset-slsa3.yml`. The production entrypoint should use publication +language such as `.github/workflows/github-release-asset-publish.yml` or an equivalent name selected +by the architecture specification. The old `slsa3` filename is misleading for a workflow that does +not itself build the asset from source. + +The publisher profile should not define a production SLSA `buildType` for GitHub Release asset +publication in the default path, because it does not produce the artifact from source and does not +generate a new SLSA provenance statement. The concrete GitHub Release asset `buildType` value +implied by ADR 0042 is removed for this surface. The producer profile's `buildType` remains the +source-to-artifact build type that consumers must verify. + +The semantic boundary is fixed by this ADR: the GitHub Release asset publisher profile verifies and +publishes. It does not claim to compile, package, otherwise produce arbitrary release asset bytes +from source, or issue an independent SLSA provenance statement for the publication operation. + +The publisher profile must not accept raw unsigned artifacts as a production SLSA-equivalent path. +If a produced artifact lacks acceptable upstream producer provenance, the default production +publisher path must fail before mutating the GitHub Release. A separate lower assurance ingestion +workflow or experimental mode may be added only through a future ADR with a distinct workflow name, +predicate or evidence model, and verifier guidance. + +This ADR supersedes the source-to-release-asset build-profile interpretation in: + +- ADR 0038, which selected Windlass-generated SLSA build provenance for GitHub Release assets as if + the release asset profile owned artifact production; +- ADR 0040, which selected `.github/workflows/github-release-asset-slsa3.yml` as the production + workflow entrypoint; +- ADR 0044, which selected a `build` → `provenance-sign` → `release-upload` graph where the release + asset profile runs caller-controlled build or packaging steps; +- the concrete GitHub Release asset `buildType` value implied by ADR 0042, while preserving ADR + 0042's domain and URI namespace decision. + +ADR 0039's one-asset-per-run boundary, ADR 0043's existing-release upload policy, ADR 0045's final +release asset subject name, ADR 0046's selected-asset digest semantics, and ADR 0048's +linked-artifact opt-in policy remain valid where they are interpreted as publication-profile rules +rather than artifact-production rules. + +### Consequences + +- Good, because each producer profile can define precise ecosystem build semantics instead of + forcing all release assets through a generic build recipe. +- Good, because the GitHub Release asset workflow becomes smaller and focused on verification, + upload policy, provenance distribution, and release asset binding. +- Good, because the architecture avoids overclaiming that a publisher workflow produced bytes it + only received. +- Good, because the default path avoids defining a Windlass custom publication predicate before the + project has a concrete need for one. +- Good, because future ecosystem profiles can compose with the same publication surface while + keeping distinct `buildType` and `builder.id` values. +- Neutral, because users need an ecosystem producer profile before they can use the production + publisher path. +- Neutral, because consumers verify producer provenance directly and rely on GitHub Release state + for publication discovery rather than a publisher-signed attestation. +- Bad, because the previous GitHub Release asset ADR sequence needs supersession and terminology + cleanup before stable architecture specifications are written. +- Bad, because the project must define a producer-to-publisher handoff contract before implementing + the publisher workflow. + +### Confirmation + +This decision is confirmed when architecture specifications, workflow implementations, tests, and +verifier guidance define: + +- no generic production GitHub Release asset build profile; +- ecosystem-specific producer profiles as the source-to-artifact build surfaces; +- a renamed GitHub Release asset publisher workflow entrypoint that does not use the old + `github-release-asset-slsa3.yml` name; +- no publisher-owned SLSA provenance or custom publication attestation in the default production + path; +- producer-generated SLSA provenance as the only source-to-artifact provenance for the uploaded + release asset; +- mandatory upstream producer provenance verification before production publication; +- failure before GitHub Release mutation when upstream producer provenance is missing, untrusted, or + mismatched with the artifact digest; +- verifier guidance that treats producer provenance as the provenance consumers must verify for the + uploaded release asset; +- separate builder identities for producer profiles and the publisher profile. + +Implementation review should verify that no production path silently accepts raw caller-produced +artifacts, generates publisher-owned SLSA provenance for the release asset, labels publication-only +evidence as source-to-artifact build provenance, or reuses the old generic release asset builder +identity after this boundary change. + +## Pros and Cons of the Options + +### Redefine GitHub Release asset handling as a verified distributor + +Ecosystem-specific producer profiles build final artifacts and generate source-to-artifact +provenance. The GitHub Release asset publisher profile verifies the artifact and upstream producer +provenance, uploads the verified bytes to an existing GitHub Release, and publishes or exposes the +producer-generated provenance unchanged. + +- Good, because artifact production semantics stay with the profile that understands the ecosystem. +- Good, because GitHub Release upload is treated as a distribution and publication concern. +- Good, because the publisher profile can be reused across producer profiles without becoming a + generic build runner. +- Good, because the trust statement is honest: producer profiles build and attest, the publisher + verifies and distributes. +- Good, because the default path does not require a new Windlass publication predicate. +- Neutral, because the publisher's verification action is not itself represented by a new signed + attestation. +- Bad, because implementation depends on a precise producer-to-publisher handoff contract. + +### Sign publisher-owned publication or binding evidence + +The GitHub Release asset publisher verifies producer provenance, then creates a new SLSA provenance +statement or custom publication predicate for the publication operation. + +- Good, because the publisher's verification result and release-slot binding are signed explicitly. +- Good, because downstream automation can verify a producer attestation and a publisher attestation + as a chain. +- Bad, because SLSA provenance is awkward for a workflow that does not build the artifact from + source. +- Bad, because a custom publication predicate adds schema design, versioning, documentation, and + verifier implementation cost. +- Bad, because consumers may mistake publisher evidence for source-to-artifact provenance. + +### Keep a generic release asset build profile with source-controlled recipe execution + +The GitHub Release asset profile checks out source, runs a repository-declared build script or +recipe, signs provenance for the resulting asset, and uploads it. + +- Good, because one workflow can build, sign, and upload one release asset. +- Good, because the signing and upload authorities can be isolated from the build job. +- Neutral, because SLSA v1.2 does not prohibit user-defined build steps when provenance and signing + controls are correct. +- Bad, because the profile becomes a broad generic build runner with weak ecosystem semantics. +- Bad, because verifier expectations must account for arbitrary repository recipes, output paths, + toolchains, environment behavior, and network access. +- Bad, because the profile can drift toward arbitrary command execution over time. + +### Accept arbitrary caller-provided build commands + +The caller passes a command string such as `make release` and the release asset profile executes it +in the build job. + +- Good, because existing workflows are easy to adapt. +- Bad, because command strings become verifier-relevant external parameters that are difficult to + review and constrain. +- Bad, because shell behavior, quoting, environment expansion, and network access create a broad and + fragile build interface. +- Bad, because this undermines the goal of keeping build definitions in source-controlled ecosystem + profiles. + +### Accept caller-produced artifacts without mandatory upstream provenance verification + +The caller builds an artifact in another workflow job and passes the bytes, workflow artifact name, +or artifact ID to the GitHub Release asset workflow for signing and upload. + +- Good, because it composes with existing release workflows and complex matrix builds. +- Good, because it resembles the `slsa-github-generator` generic generator pattern. +- Bad, because the publisher workflow did not produce the bytes and should not claim + source-to-artifact build provenance for them. +- Bad, because workflow artifact transport is not a substitute for trusted producer provenance. +- Bad, because missing upstream provenance would let arbitrary bytes enter the production publisher + path. + +### Leave publication entirely to ecosystem-specific profiles + +Each ecosystem-specific profile builds, signs, and uploads its own GitHub Release assets directly. + +- Good, because every ecosystem profile can fully own its build and release lifecycle. +- Good, because no separate producer-to-publisher handoff is needed. +- Bad, because release upload policy, existing-release checks, duplicate-asset behavior, optional + sidecars, and linked-artifact metadata would be duplicated across ecosystem profiles. +- Bad, because whole-project GitHub Release publication behavior would become inconsistent across + profiles. + +## More Information + +This decision follows ADR 0002, ADR 0003, ADR 0013, ADR 0038, ADR 0039, ADR 0040, ADR 0042, ADR +0043, ADR 0044, ADR 0045, ADR 0046, ADR 0047, and ADR 0048. It decides the architecture boundary +between artifact production and GitHub Release asset publication. It does not decide the exact +handoff fields, upstream provenance policy format, producer provenance distribution channel, or +final workflow implementation details; those are decided by ADR 0050, ADR 0051, and the architecture +specifications. + +Reference points considered: + +- SLSA v1.2 Build L3 requires unforgeable provenance, isolated builds, and complete + `externalParameters`, but it does not require one generic build interface for all artifact types. +- SLSA v1.2 Build Provenance separates `buildType`, `externalParameters`, `resolvedDependencies`, + `builder.id`, and subject digest so different build or publication operations can use distinct + schemas and trust boundaries. +- `slsa-github-generator` distinguishes ecosystem-specific builders, which build artifacts and + generate provenance, from generic generators, which attest subjects produced by an existing + workflow. +- GitHub reusable workflows accept typed inputs, secrets, and outputs; callers cannot inject steps + into a reusable workflow job. +- GitHub `actions/attest` can sign a subject name and digest with a custom predicate and publish the + resulting bundle to GitHub artifact attestation storage. From c4ddeae71250255e52844ceea855767d49150a97 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:17 +0900 Subject: [PATCH 08/14] docs(decisions): add ADRs for release asset provenance distribution ADR-0047 initially chose GitHub attestation storage as canonical distribution; ADR-0051 supersedes it by distributing producer provenance with release assets. Signed-off-by: Yunseo Kim --- ...-with-actions-attest-for-release-assets.md | 251 ++++++++++++++++++ ...l-release-asset-provenance-distribution.md | 235 ++++++++++++++++ ...producer-provenance-with-release-assets.md | 223 ++++++++++++++++ 3 files changed, 709 insertions(+) create mode 100644 docs/decisions/0038-use-windlass-generated-provenance-with-actions-attest-for-release-assets.md create mode 100644 docs/decisions/0047-use-github-attestation-storage-as-canonical-release-asset-provenance-distribution.md create mode 100644 docs/decisions/0051-distribute-producer-provenance-with-release-assets.md diff --git a/docs/decisions/0038-use-windlass-generated-provenance-with-actions-attest-for-release-assets.md b/docs/decisions/0038-use-windlass-generated-provenance-with-actions-attest-for-release-assets.md new file mode 100644 index 0000000..7e43c9d --- /dev/null +++ b/docs/decisions/0038-use-windlass-generated-provenance-with-actions-attest-for-release-assets.md @@ -0,0 +1,251 @@ +--- +parent: Decisions +nav_order: 38 +status: superseded by ADR-0049 +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Use Windlass-Generated Provenance with actions/attest for GitHub Release Assets + +## Context and Problem Statement + +ADR 0013 scoped the initial JS/TS profile to npm package releases and deferred GitHub Release assets +to a future composable profile. The initial release scope now needs to include a GitHub Release +asset profile as a sibling profile rather than waiting for a later release. The first architectural +choice for that profile is whether it should be a thin reusable workflow wrapper around GitHub +`actions/attest` default provenance behavior, or whether Windlass should directly implement the +profile's provenance and signing behavior in Go or another trusted runtime. + +GitHub Release assets differ from npm packages. They are distributed by the source repository +hosting provider rather than by npm, may be protected by GitHub immutable releases, and can be +verified with GitHub release and artifact-attestation tooling. However, SLSA v1.2 still treats +provenance as artifact-bound build metadata that should identify the output artifact by digest, +describe how it was produced, and be generated or verified by the trusted build platform for Build +L3. GitHub's artifact attestation documentation also distinguishes direct artifact attestations, +which provide SLSA Build Level 2 by themselves, from reusable-workflow-based builds that can provide +the additional isolation needed for Build Level 3. + +Should the initial GitHub Release asset profile rely on `actions/attest` default provenance as a +wrapper, generate Windlass-owned SLSA provenance in the Go trusted core and use `actions/attest` +only as a signing and storage adapter, implement Sigstore signing directly in Go from the start, or +rely on GitHub immutable release attestations instead of a separate build provenance profile? + +## Decision Drivers + +- Preserve ADR 0002 and ADR 0003's trusted reusable workflow and profile-owned workflow boundary. +- Keep the GitHub Release asset profile aligned with SLSA v1.2 Build L3 expectations for + unforgeable, artifact-bound provenance. +- Reuse ADR 0004's Go trusted core direction for profile-specific provenance, subject, digest, and + policy logic. +- Use a Windlass-owned `builder.id`, `buildType`, and complete `externalParameters` schema rather + than inheriting a generic GitHub Actions workflow build type as the production verifier contract. +- Avoid giving caller-controlled build steps the ability to inject or alter provenance contents + other than allowed artifact subject information. +- Avoid implementing Fulcio, Rekor, timestamp, Sigstore bundle, and GitHub attestations API behavior + directly before the trusted core has enough specification and fixture coverage. +- Keep GitHub immutable releases and release attestations available as release-integrity evidence + without treating them as a substitute for artifact-bound build provenance. +- Preserve a future migration path toward a Go-native `sigstore-go` signing adapter if the project + later needs to own signing internals directly. + +## Considered Options + +- Use `actions/attest` default provenance as a thin reusable workflow wrapper. +- Generate Windlass SLSA provenance in Go and use `actions/attest` as the signing and storage + adapter. +- Implement provenance generation, Sigstore signing, and GitHub attestation persistence directly in + Go for the initial profile. +- Rely on GitHub immutable release attestations and release verification instead of a separate + artifact-bound SLSA provenance profile. + +## Decision Outcome + +Chosen option: "Generate Windlass SLSA provenance in Go and use `actions/attest` as the signing and +storage adapter", because it gives the GitHub Release asset profile a Windlass-owned SLSA verifier +contract while still using GitHub's maintained Sigstore and artifact-attestation integration for the +initial signing path. + +The initial GitHub Release asset profile should generate canonical SLSA build provenance v1 inside +Windlass trusted logic. The trusted implementation, expected to be Go by default under ADR 0004, +should own at least: + +- release asset subject naming and digest normalization; +- the profile-specific `buildType` URI and schema; +- complete verifier-relevant `buildDefinition.externalParameters`; +- `runDetails.builder.id` using the SHA-pinned reusable workflow identity model from ADR 0028; +- source repository, source ref, resolved source commit, release tag, asset path or name, runtime + policy, and release event context fields; +- validation that the provenance subject identifies exactly the release asset artifact being signed + and later uploaded or verified. + +The profile should invoke a full-SHA-pinned `actions/attest` step in custom attestation mode to sign +the Windlass-generated SLSA predicate and, when configured, upload the resulting Sigstore bundle to +GitHub's attestation storage. `actions/attest` should not be used in its default provenance mode as +the canonical production provenance generator for this profile. In this profile, `actions/attest` is +an adapter for Sigstore bundle generation and GitHub attestation storage, not the owner of the +Windlass release asset build type semantics. + +GitHub immutable releases, release attestations, `gh release verify`, and `gh release verify-asset` +should be treated as complementary release-integrity and asset-immutability mechanisms. They may be +required or recommended by the profile's release process, but they do not replace artifact-bound +SLSA build provenance for the release asset. + +The initial profile should not implement Sigstore signing directly in Go. A later ADR may replace +the `actions/attest` adapter with `sigstore-go` or another Go-native signing path after the project +has specified bundle compatibility, signer identity, GitHub attestation storage behavior, release +asset verification policy, trust roots, and migration rules. If that migration materially changes +security properties, verifier expectations, or `builder.id` meaning, it should use a distinct +builder identity or explicitly document compatibility. + +### Consequences + +- Good, because the release asset profile can define a Windlass-owned `buildType` and + `externalParameters` schema instead of inheriting a generic GitHub Actions provenance contract. +- Good, because SLSA predicate generation, subject rules, digest handling, and release asset policy + stay in the Go trusted core direction selected by ADR 0004. +- Good, because `actions/attest` still provides a maintained GitHub-native Sigstore signing and + attestation-storage path for the initial release. +- Good, because reusable workflow signer checks through GitHub artifact attestation tooling remain + available. +- Good, because GitHub immutable release verification can complement, rather than replace, + artifact-bound SLSA provenance. +- Neutral, because the profile will need Windlass-specific verifier policy in addition to generic + `gh attestation verify` or `gh release verify` success. +- Bad, because the release asset profile must implement and test its own SLSA predicate generation, + release asset subject model, and provenance policy before shipping. +- Bad, because `actions/attest` remains a trusted, SHA-pinned GitHub-specific dependency in the + initial profile. +- Bad, because a future Go-native signing migration will require compatibility and verifier-policy + work. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- Windlass-generated SLSA build provenance v1 as the canonical provenance for GitHub Release assets; +- a profile-specific GitHub Release asset `buildType` URI and schema; +- complete `externalParameters`, `internalParameters`, `resolvedDependencies`, `builder.id`, + `builder.version`, `builderDependencies`, invocation metadata, and byproducts rules for the + release asset profile; +- release asset subject naming, digest algorithms, and digest encodings; +- full-SHA pinning for the `actions/attest` action reference; +- use of `actions/attest` custom attestation mode with + `predicate-type: https://slsa.dev/provenance/v1` or an equivalent path that signs the + Windlass-generated SLSA predicate; +- job-level permissions that keep Sigstore signing and GitHub attestation storage permissions away + from caller-controlled build steps; +- release asset upload, immutable release, release attestation, and `gh release verify` behavior as + complementary release-integrity checks rather than substitutes for SLSA provenance; +- verifier guidance for Sigstore signature, reusable workflow signer identity, SHA-based + `builder.id`, Windlass `buildType`, `externalParameters`, source identity, release tag, release + asset name, release asset digest, and release manifest linkage; +- a documented future path for replacing `actions/attest` with a Go-native Sigstore adapter without + requiring that implementation in the initial release asset profile. + +Implementation review should verify that production GitHub Release asset provenance cannot silently +fall back to `actions/attest` default provenance, unsigned release assets, release-level attestation +only, mutable release assets, long-lived signing keys, or a different signer identity under the same +production builder identity. + +## Pros and Cons of the Options + +### Use `actions/attest` default provenance as a thin reusable workflow wrapper + +The release asset profile builds or receives an artifact in a reusable workflow and invokes +`actions/attest` with only `subject-path`, `subject-digest`, or `subject-checksums`, allowing the +action to generate its default SLSA provenance predicate. + +- Good, because it is the smallest implementation and uses GitHub's official artifact attestation + action directly. +- Good, because it naturally integrates with GitHub attestations storage and + `gh attestation verify`. +- Good, because GitHub documents reusable workflows plus artifact attestations as a path toward SLSA + Build Level 3. +- Bad, because the default predicate is not the Windlass release asset build type contract. +- Bad, because Windlass would have less control over `buildType`, `externalParameters`, and + profile-specific release asset verification semantics. +- Bad, because it risks creating a release asset profile whose public verifier contract differs from + the Windlass-generated provenance model selected for the npm package profile. + +### Generate Windlass SLSA provenance in Go and use `actions/attest` as the adapter + +The trusted core creates the SLSA build provenance predicate for the release asset profile, then a +SHA-pinned `actions/attest` step signs and stores that predicate in custom attestation mode. + +- Good, because Windlass owns the release asset subject, build type, parameter, and verification + schema. +- Good, because it keeps the security-critical JSON, digest, subject, and policy logic in the Go + trusted-core direction. +- Good, because it avoids implementing Sigstore signing and GitHub attestation persistence directly + in the first release asset milestone. +- Good, because it is consistent with ADR 0035's adapter model while adapting it to a non-npm + profile. +- Neutral, because generic GitHub verification tools remain useful but incomplete without + Windlass-specific policy checks. +- Bad, because it requires a real provenance generator and conformance fixture set before the + profile can ship. +- Bad, because `actions/attest` behavior, versioning, and GitHub support boundaries must be + monitored as part of the trusted release path. + +### Implement signing and attestation persistence directly in Go + +The Go trusted core generates provenance, obtains or uses GitHub Actions OIDC identity, creates the +Sigstore bundle, and uploads or distributes the attestation without invoking `actions/attest`. + +- Good, because it gives Windlass maximum control over signing, bundle generation, error handling, + tests, and compatibility. +- Good, because it may eventually reduce the number of trusted workflow action dependencies. +- Bad, because the initial profile would immediately own Fulcio, Rekor, timestamp, bundle, + trust-root, GitHub attestations API, and private/public Sigstore behavior. +- Bad, because the larger initial trusted implementation surface increases security and maintenance + risk. +- Bad, because a direct implementation is unnecessary to decide the release asset profile's build + semantics. + +### Rely on GitHub immutable release attestations instead + +The profile depends on GitHub immutable release protections and release attestations to prove the +release tag, commit, and assets, without generating separate artifact-bound SLSA build provenance. + +- Good, because immutable releases protect release tags and assets from modification or deletion + after publication. +- Good, because GitHub release verification commands provide a simple consumer-facing integrity + check. +- Good, because release attestations can record the release tag, commit SHA, and release assets. +- Bad, because SLSA v1.2 recommends binding build attestations to artifacts, not only to releases. +- Bad, because release-level attestation does not define the Windlass build process, `buildType`, or + complete `externalParameters` needed for the profile's SLSA verifier contract. +- Bad, because this would make GitHub release integrity a substitute for build provenance rather + than defense-in-depth evidence. + +## More Information + +This decision extends ADR 0002, ADR 0003, ADR 0004, ADR 0028, ADR 0031, ADR 0035, and ADR 0037 to +the initial GitHub Release asset profile. It does not decide the full release asset profile scope, +workflow filename, public input contract, job graph, release creation policy, immutable release +requirement, asset upload behavior, or standalone verifier CLI surface. Those details belong in +follow-up ADRs or the release asset architecture specification. + +Reference points considered: + +- SLSA v1.2 Build Requirements require authentic provenance at Build L2 and unforgeable provenance + at Build L3. At Build L3, every provenance field must be generated or verified by the trusted + build platform, except permitted tenant-controlled subject information, and `externalParameters` + must be fully enumerated. +- SLSA v1.2 Build Provenance defines `predicateType: https://slsa.dev/provenance/v1`, separates + `builder.id` from signer identity, and expects build type specifications to define the schema for + `externalParameters` and `internalParameters`. +- SLSA v1.2 Distributing Provenance says attestations should be bound to artifacts rather than + releases, and recommends publishing provenance alongside source repository releases when that is + the artifact distribution surface. +- GitHub artifact attestations documentation states that artifact attestations by themselves provide + SLSA Build Level 2, while reusable workflows can provide the known build instructions and + isolation needed for Build Level 3. +- GitHub `actions/attest` supports provenance, SBOM, and custom attestation modes; custom mode + accepts `predicate-type` with `predicate` or `predicate-path`, emits Sigstore bundle JSON, and can + upload attestations to GitHub's attestations API. +- GitHub immutable releases protect tags and release assets from modification or deletion after + publication and create release attestations, but they do not replace artifact-bound SLSA build + provenance for a Windlass-owned builder profile. diff --git a/docs/decisions/0047-use-github-attestation-storage-as-canonical-release-asset-provenance-distribution.md b/docs/decisions/0047-use-github-attestation-storage-as-canonical-release-asset-provenance-distribution.md new file mode 100644 index 0000000..73790ad --- /dev/null +++ b/docs/decisions/0047-use-github-attestation-storage-as-canonical-release-asset-provenance-distribution.md @@ -0,0 +1,235 @@ +--- +parent: Decisions +nav_order: 47 +status: superseded by ADR-0051 +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Use GitHub Attestation Storage as Canonical Release Asset Provenance Distribution + +## Context and Problem Statement + +ADR 0038 selected Windlass-generated SLSA provenance for GitHub Release assets, with +`actions/attest` acting as the initial Sigstore signing and storage adapter. ADR 0039 scoped the +initial production profile to one GitHub Release asset per profile run. ADR 0044 selected a +three-job graph where `provenance-sign` signs the Windlass provenance and `release-upload` verifies +the signed bundle before uploading the release asset. ADR 0045 and ADR 0046 then fixed the release +asset subject name, digest, checksum, and SBOM semantics. + +The remaining distribution question is how consumers and downstream automation should discover and +retrieve the signed SLSA provenance for the release asset. GitHub `actions/attest` can upload the +Sigstore bundle to GitHub's artifact attestation storage and also exposes a local `bundle-path` JSON +file. GitHub Releases can also publish that bundle as a downloadable release asset sidecar, but +doing so creates another release asset with its own naming, duplicate handling, and orchestration +concerns. + +Should the initial GitHub Release asset profile distribute provenance primarily through GitHub +artifact attestation storage, through a GitHub Release asset sidecar, through both channels, or +through release-level attestation only? + +## Decision Drivers + +- Keep the production provenance discovery path aligned with ADR 0038's `actions/attest` adapter + choice. +- Preserve ADR 0039's one-primary-release-asset profile boundary. +- Keep the SLSA provenance bound to the selected release asset digest rather than to the whole + GitHub Release. +- Support GitHub-native verification with `gh attestation verify` and the artifact attestations API. +- Allow archival, air-gapped, or mirror-friendly bundle export when callers need it. +- Avoid making release asset sidecar upload mandatory for the low-level profile. +- Keep provenance sidecar naming, sidecar attestation, and whole-release UX in the orchestration + layer unless explicitly enabled. +- Preserve the `release-upload` job's producer-side verification of the signed bundle before release + mutation. + +## Considered Options + +- Use GitHub artifact attestation storage as the canonical distribution channel, with optional + release asset bundle export. +- Require both GitHub artifact attestation storage and a GitHub Release asset bundle sidecar. +- Use only a GitHub Release asset bundle sidecar. +- Use only workflow artifacts or job outputs for provenance distribution. +- Rely on GitHub immutable release attestations or release verification instead of artifact-bound + SLSA provenance distribution. + +## Decision Outcome + +Chosen option: "Use GitHub artifact attestation storage as the canonical distribution channel, with +optional release asset bundle export", because this keeps the initial profile on GitHub's native +attestation discovery and verification path while still allowing callers to publish a downloadable +Sigstore bundle when they need portability or archival behavior. + +The production GitHub Release asset profile should treat GitHub artifact attestation storage as the +canonical distribution channel for the signed SLSA provenance bundle. The `provenance-sign` job +should invoke the SHA-pinned `actions/attest` custom attestation mode selected by ADR 0038 with +GitHub attestation storage enabled. The resulting attestation should be associated with the +repository and release asset subject digest through GitHub's attestations API. + +The `provenance-sign` job should also capture the local JSON-serialized Sigstore bundle produced by +`actions/attest` through `bundle-path` and pass it to `release-upload` as a digest-checked workflow +artifact. That workflow artifact is an internal handoff artifact. It is required for ADR 0044's +producer-side verification before release mutation, but it is not the canonical public distribution +channel by itself. + +The `release-upload` job should verify the signed bundle before uploading the selected release +asset. Verification should include the expected Sigstore root, GitHub Actions reusable workflow +signer identity, SLSA `predicateType`, `builder.id`, `buildType`, `externalParameters`, release tag, +subject name, and subject digest. The job should fail before release mutation if the GitHub +attestation storage upload did not produce the expected attestation identity or if the local bundle +cannot be verified. + +Publishing the signed bundle as a GitHub Release asset sidecar should be optional. When enabled, it +is an export and compatibility channel, not the canonical provenance distribution channel. The +architecture specification should define the option name, default value, sidecar asset name, sidecar +digest outputs, and duplicate-name behavior. The production default should not require a sidecar +release asset. + +A sidecar bundle release asset should not change the selected release asset's SLSA subject. It +should not be inserted into `subject[0].digest`, should not replace GitHub artifact attestation +storage, and should not be required for ordinary `gh attestation verify` based verification. If the +sidecar is itself treated as a published release asset requiring SLSA provenance, that attestation +is a separate one-asset profile invocation or an orchestration-layer responsibility under ADR 0039 +and ADR 0046. + +The profile should not rely on workflow artifacts, unsigned job outputs, logs, release notes, or +GitHub immutable release attestations as the public SLSA provenance distribution mechanism. Those +surfaces may provide handles, diagnostics, or complementary release integrity evidence, but they do +not replace artifact-bound signed SLSA provenance discoverable through GitHub attestation storage. + +### Consequences + +- Good, because the canonical path matches GitHub's official artifact attestation storage and + `gh attestation verify` workflow. +- Good, because the release asset list does not need to contain provenance sidecars for the default + production path. +- Good, because ADR 0044 can still verify the local signed bundle before release mutation. +- Good, because projects that need offline, mirrored, or archival verification can opt into a + downloadable bundle sidecar. +- Good, because release-level attestations and immutable release verification remain complementary + evidence rather than substitutes for artifact-bound build provenance. +- Neutral, because consumers that avoid GitHub's attestations API need the optional sidecar or a + separate export workflow. +- Bad, because the canonical public discovery path depends on GitHub artifact attestation storage + availability and support boundaries. +- Bad, because optional sidecar support still requires naming, duplicate handling, and verification + rules when enabled. + +### Confirmation + +This decision is confirmed when the initial GitHub Release asset profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- GitHub artifact attestation storage as the canonical public distribution channel for the signed + Windlass SLSA provenance bundle; +- `actions/attest` custom mode with GitHub attestation storage enabled in the `provenance-sign` job; +- capture of the local `bundle-path` JSON bundle as a digest-checked workflow artifact for the + `release-upload` job; +- producer-side verification of the local signed bundle before GitHub Release asset upload; +- public outputs for the attestation ID, attestation URL, bundle digest, subject name, and subject + digest as appropriate; +- ordinary consumer verification guidance based on GitHub artifact attestation storage and + `gh attestation verify` or equivalent API-backed verification; +- an optional release asset sidecar export mode with explicit default, sidecar asset naming, + duplicate-name failure behavior, and verifier guidance; +- documentation that workflow artifacts, logs, release notes, and release-level attestations are not + the canonical SLSA provenance distribution mechanism. + +Implementation review should verify that no production path silently skips GitHub attestation +storage upload, treats an unsigned or unverified bundle as distributable provenance, requires a +release asset sidecar for the default verification path, or uses GitHub release-level attestation as +a substitute for the Windlass artifact-bound SLSA provenance bundle. + +## Pros and Cons of the Options + +### Use GitHub artifact attestation storage as canonical distribution, with optional sidecar export + +The signing job uploads the signed bundle to GitHub artifact attestation storage through +`actions/attest` and also passes the local bundle to the upload job for verification. Callers may +optionally publish the bundle as a release asset sidecar for portability. + +- Good, because it uses the native storage and discovery path provided by the selected signing + adapter. +- Good, because it supports `gh attestation verify` and digest-based attestation lookup. +- Good, because it keeps the low-level profile centered on one primary release asset. +- Good, because it leaves room for downloadable sidecars without making them mandatory. +- Neutral, because offline consumers need an explicit sidecar or prior bundle export. +- Bad, because GitHub artifact attestation storage is a provider-specific dependency. + +### Require both GitHub artifact attestation storage and a release asset bundle sidecar + +Every production profile run uploads the signed attestation to GitHub storage and publishes the same +bundle as a GitHub Release asset sidecar. + +- Good, because consumers get both GitHub-native verification and a downloadable bundle file. +- Good, because provenance can be mirrored or archived with the release assets. +- Bad, because the low-level one-asset profile would always perform at least two release asset + uploads. +- Bad, because sidecar upload failure creates ambiguity about whether the primary asset should be + uploaded or rolled back. +- Bad, because sidecar naming, duplicate detection, and sidecar attestation policy become mandatory + initial-profile complexity. + +### Use only a GitHub Release asset bundle sidecar + +The signed bundle is uploaded as a release asset next to the primary artifact, and GitHub artifact +attestation storage is not the canonical discovery channel. + +- Good, because provenance is visible and downloadable from the GitHub Release page. +- Good, because consumers can archive the artifact and bundle together. +- Bad, because it avoids the native GitHub artifact attestation storage and lookup path selected by + ADR 0038. +- Bad, because `gh attestation verify` defaults and GitHub attestation UI/API integration become + less central. +- Bad, because every release asset provenance file becomes release upload lifecycle surface. + +### Use only workflow artifacts or job outputs for provenance distribution + +The signed bundle is retained only as a workflow artifact or output from the reusable workflow. + +- Good, because it is simple for producer-side handoff between jobs. +- Good, because it avoids adding another release asset. +- Bad, because workflow artifacts are not the artifact's durable public provenance distribution + surface. +- Bad, because job outputs and logs are not signed provenance and are unsuitable for long-term + consumer verification. +- Bad, because consumers outside the workflow run would not have a stable discovery path. + +### Rely on GitHub immutable release attestations or release verification instead + +Consumers use GitHub release attestation and `gh release verify` or `gh release verify-asset` rather +than artifact-bound Windlass SLSA provenance distribution. + +- Good, because release verification is a simple GitHub-native integrity check for immutable + releases. +- Good, because it can prove that release assets match GitHub's signed release record. +- Bad, because release-level attestation does not carry the Windlass `buildType`, `builder.id`, or + complete SLSA `externalParameters` contract. +- Bad, because it treats release integrity as a substitute for artifact-bound build provenance. +- Bad, because it conflicts with ADR 0038's decision to generate Windlass-owned SLSA provenance for + release assets. + +## More Information + +This decision follows ADR 0038, ADR 0039, ADR 0043, ADR 0044, ADR 0045, and ADR 0046. It decides +only the public distribution channel for signed release asset SLSA provenance and the role of +optional bundle sidecar export. It does not decide the final sidecar filename convention, verifier +CLI UX, release manifest schema, SBOM attestation policy, or future whole-release orchestration +workflow. + +Reference points considered: + +- GitHub `actions/attest` creates Sigstore-signed in-toto attestations, uploads them to GitHub's + attestations API, associates them with the repository, and exposes a local JSON-serialized + Sigstore bundle through `bundle-path`. +- GitHub artifact attestation verification supports `gh attestation verify` against GitHub storage + and local bundle verification through downloaded or supplied bundles. +- GitHub's artifact attestations API stores attestations by subject digest and repository, enabling + digest-based discovery for artifacts. +- GitHub immutable releases and release attestations can verify release integrity and asset bytes + but do not replace artifact-bound SLSA build provenance with Windlass-specific verifier policy. +- SLSA v1.2 recommends binding provenance to artifacts rather than releases and distributing + provenance so consumers can map a downloaded artifact to its attestation. +- Windlass workflow hardening guidance requires SHA-pinned actions, minimal top-level permissions, + job-level permission elevation, OIDC for signing, and explicit permissions for artifact + attestations and release asset upload. diff --git a/docs/decisions/0051-distribute-producer-provenance-with-release-assets.md b/docs/decisions/0051-distribute-producer-provenance-with-release-assets.md new file mode 100644 index 0000000..60c0fd5 --- /dev/null +++ b/docs/decisions/0051-distribute-producer-provenance-with-release-assets.md @@ -0,0 +1,223 @@ +--- +parent: Decisions +nav_order: 51 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Distribute Producer Provenance with GitHub Release Assets + +## Context and Problem Statement + +ADR 0049 redefined the GitHub Release asset profile as a verified distributor rather than a generic +release asset builder or independent provenance issuer. ADR 0050 then required producer profiles to +handoff artifact bytes and producer-generated SLSA provenance for the exact final GitHub Release +asset. That leaves a distribution question previously answered by ADR 0047 under an older premise: +ADR 0047 assumed the GitHub Release asset profile generated and signed its own Windlass SLSA +provenance bundle through `actions/attest` and then used GitHub attestation storage as the canonical +distribution channel. + +Under the new model, the publisher profile should not create new SLSA provenance or a custom +publication predicate in the default production path. It verifies producer-generated provenance and +publishes the artifact. The project now needs to decide how that already-signed producer provenance +is made discoverable to GitHub Release asset consumers. + +Should the GitHub Release asset publisher redistribute the producer provenance bundle as a release +asset sidecar, rely only on the producer's native attestation storage, generate new publisher +attestation evidence, or leave provenance distribution entirely to the producer profile? + +## Decision Drivers + +- Preserve the meaning of SLSA provenance as source-to-artifact build provenance generated by the + ecosystem-specific producer profile. +- Avoid creating a publisher-owned SLSA provenance statement or custom publication predicate in the + default production path. +- Make GitHub Release consumers able to find the producer provenance for the downloaded asset + without knowing the producer workflow internals. +- Keep the producer provenance bundle unchanged so its signature, subject, `builder.id`, + `buildType`, and `externalParameters` remain exactly as produced and verified. +- Keep the selected release asset subject digest bound to the primary asset bytes, not to provenance + sidecars, checksum files, SBOMs, or whole-release manifests. +- Support producer-native attestation storage when available, while providing a portable + release-page distribution path for archival, mirrored, or air-gapped consumers. +- Avoid treating publisher-side workflow artifacts, job outputs, logs, or release notes as durable + signed provenance. + +## Considered Options + +- Publish the verified producer provenance bundle as a release asset sidecar and expose native + provenance locators when available. +- Use only the producer's native attestation storage or registry provenance distribution. +- Generate a new publisher-owned attestation that references the producer provenance. +- Leave provenance distribution entirely to the producer profile. +- Store producer provenance only as a workflow artifact or job output. + +## Decision Outcome + +Chosen option: "Publish the verified producer provenance bundle as a release asset sidecar and +expose native provenance locators when available", because the publisher should not mint new +provenance but GitHub Release consumers still need a durable, release-adjacent way to retrieve the +provenance that the publisher verified. + +The production GitHub Release asset publisher profile should redistribute the exact verified +producer-generated provenance bundle as a GitHub Release asset sidecar. The sidecar is a +distribution copy of the producer provenance, not a new attestation created by the publisher. The +publisher must not alter, re-sign, wrap, or regenerate the producer provenance bundle before +publishing it. + +The publisher should also expose producer-native provenance locators when they exist, such as GitHub +artifact attestation URLs, registry provenance handles, or other ecosystem-owned discovery surfaces. +Those locators are useful for online verification and producer-system audit trails. The release +asset sidecar remains the GitHub Release distribution copy that travels with the released artifact. + +The sidecar asset name should be deterministic and derived from the final release asset name. The +architecture specification should define the exact naming convention, but it should preserve the +primary asset name visibly and avoid colliding with ordinary release assets. For example, a future +specification may choose a suffix such as: + +```text +.intoto.jsonl +``` + +The sidecar must not change the primary release asset's SLSA subject. The producer provenance +subject must already identify the final release asset name and digest selected by ADR 0045 and +ADR 0050. The sidecar's bytes are not inserted into `subject[0].digest`, are not treated as checksum +or SBOM coverage for the primary asset, and are not a substitute for verifying the producer +provenance signature and subject digest. + +The publisher must fail before uploading either the primary asset or sidecar when producer +provenance verification fails. If primary asset upload succeeds but sidecar upload fails, the +workflow should fail clearly without deleting, replacing, or clobbering the primary asset. The +failure message and outputs should make the partial state explicit so operators can retry sidecar +publication or remove the incomplete release asset according to repository policy. + +This ADR supersedes ADR 0047's canonical GitHub attestation storage decision for GitHub Release +assets. ADR 0047 assumed publisher-generated Windlass SLSA provenance. Under the current model, +producer-generated provenance and producer-native attestation storage remain authoritative for the +source-to-artifact claim, while the GitHub Release sidecar provides a durable publication copy. + +### Consequences + +- Good, because the publisher distributes the same signed SLSA provenance that the producer profile + generated and the publisher verified. +- Good, because consumers can download a GitHub Release asset and its provenance bundle from the + same release page or mirror. +- Good, because the project avoids defining and signing a new publisher-owned predicate solely to + describe publication. +- Good, because producer-native attestation storage can still be surfaced for online verification. +- Neutral, because ordinary releases now include at least one provenance sidecar per published + asset. +- Neutral, because sidecar naming and duplicate-asset behavior become part of the publisher profile + specification. +- Bad, because sidecar upload can fail after the primary asset upload, creating a partial release + state that must be documented and handled. +- Bad, because consumers that only use native attestation lookup may need guidance to choose between + native locators and release sidecar bundles. + +### Confirmation + +This decision is confirmed when architecture specifications, workflow implementations, tests, and +documentation define: + +- producer-generated provenance as the only SLSA provenance for the primary release asset in the + default production publisher path; +- mandatory verification of producer provenance before primary asset or provenance sidecar upload; +- publication of the verified producer provenance bundle as an unchanged GitHub Release asset + sidecar; +- deterministic sidecar naming, duplicate-name failure behavior, and outputs for sidecar name, URL, + and digest; +- exposure of producer-native provenance locators when available; +- consumer guidance for verifying the primary asset against the producer provenance bundle; +- failure behavior for sidecar upload failure after primary asset upload; +- documentation that workflow artifacts, job outputs, logs, release notes, and publisher-owned + unsigned metadata are not provenance distribution channels. + +Implementation review should verify that the publisher does not re-sign or mutate the producer +provenance bundle, that the sidecar corresponds to the exact primary asset digest and subject name, +and that a missing or unverifiable producer provenance bundle cannot reach the production upload +path. + +## Pros and Cons of the Options + +### Publish verified producer provenance as a sidecar and expose native locators + +The publisher uploads the primary release asset and a deterministic sidecar containing the unchanged +producer provenance bundle. It also exposes native producer attestation locators when available. + +- Good, because it keeps source-to-artifact provenance owned by the producer profile. +- Good, because release consumers have a durable bundle next to the asset. +- Good, because native attestation storage remains available for tools that prefer it. +- Neutral, because the release asset list includes provenance sidecars by default. +- Bad, because sidecar upload lifecycle and duplicate-name handling must be specified. + +### Use only producer-native attestation storage + +The publisher does not upload any provenance sidecar. It only exposes or documents the producer's +native provenance lookup surface. + +- Good, because it avoids extra release assets. +- Good, because it preserves the producer ecosystem's canonical discovery path. +- Bad, because GitHub Release consumers and mirrors may not have a durable provenance bundle next to + the downloaded asset. +- Bad, because producer-native storage may vary by ecosystem, repository, registry, or hosting + provider. + +### Generate a new publisher-owned attestation + +The publisher verifies producer provenance and then signs a new SLSA provenance statement or custom +publication predicate that references the producer provenance. + +- Good, because the publisher's verification and release-slot binding are signed explicitly. +- Good, because downstream automation can check a two-attestation chain. +- Bad, because SLSA provenance is semantically awkward for a workflow that did not build the + artifact from source. +- Bad, because a custom publication predicate adds schema, documentation, versioning, and verifier + implementation cost. +- Bad, because consumers may mistake publisher evidence for source-to-artifact provenance. + +### Leave provenance distribution entirely to producer profiles + +Producer profiles generate and distribute provenance, and the GitHub Release asset publisher uploads +only the primary release asset. + +- Good, because the publisher profile remains smallest. +- Good, because producer profiles fully own their artifact and provenance lifecycle. +- Bad, because GitHub Release consumers may not know where to find provenance for the downloaded + asset. +- Bad, because release-page and mirror-friendly provenance distribution becomes inconsistent across + producer profiles. + +### Store producer provenance only as a workflow artifact or job output + +The publisher uses workflow artifacts or outputs to move producer provenance internally but does not +publish it to the release page or expose native locators. + +- Good, because it is simple for internal handoff. +- Bad, because workflow artifacts and job outputs are not durable public provenance distribution + channels. +- Bad, because consumers outside the workflow run would not have a stable discovery path. + +## More Information + +This decision follows ADR 0049 and ADR 0050. It supersedes ADR 0047 for the GitHub Release asset +publisher profile because ADR 0047 was written for a profile that generated its own Windlass SLSA +provenance bundle. It preserves ADR 0039's one-primary-asset boundary, ADR 0045's subject name and +digest model, ADR 0046's rule that sidecars do not change the primary subject digest, and ADR 0048's +separate linked-artifact metadata policy. + +This decision does not define whole-release manifests, checksum or SBOM sidecar policy, detached +signature policy, a publisher-owned custom predicate, or deployment records. Those remain separate +future decisions. + +Reference points considered: + +- SLSA provenance identifies build outputs by subject name and digest; the producer profile's + provenance should already name the final artifact bytes selected for publication. +- GitHub Release assets provide a durable distribution surface for binary artifacts and sidecar + metadata files. +- GitHub artifact attestation storage can remain a producer-native provenance locator when the + producer profile uses GitHub `actions/attest`, but the publisher should not depend on minting a + new attestation to distribute already-generated producer provenance. +- Workflow artifacts and job outputs are useful handoff mechanisms but are not durable public + provenance distribution channels for release consumers. From ba264310ee2f953ac5c05f6112c48bc5e5bc191d Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:18 +0900 Subject: [PATCH 09/14] docs(decisions): add ADR for Sigstore-signed in-toto release manifest Define a Sigstore-signed in-toto release manifest that records release asset metadata and supports the producer-to-publisher handoff. Signed-off-by: Yunseo Kim --- ...igstore-signed-in-toto-release-manifest.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/decisions/0031-use-sigstore-signed-in-toto-release-manifest.md diff --git a/docs/decisions/0031-use-sigstore-signed-in-toto-release-manifest.md b/docs/decisions/0031-use-sigstore-signed-in-toto-release-manifest.md new file mode 100644 index 0000000..ef1f0a4 --- /dev/null +++ b/docs/decisions/0031-use-sigstore-signed-in-toto-release-manifest.md @@ -0,0 +1,292 @@ +--- +parent: Decisions +nav_order: 31 +status: accepted +date: 12026-07-05 +decision-makers: Yunseo Kim +--- + +# Use Sigstore-Signed in-toto Release Manifest + +## Context and Problem Statement + +ADR 0028 selected full commit SHA pins as the production reusable workflow reference form and +required signed release metadata that maps each human-readable release version to the exact workflow +commit SHA and SHA-based `builder.id` values. It intentionally left the metadata format and +signature or attestation mechanism open. + +SLSA v1.2 verification expects verifiers to compare provenance against preconfigured roots of trust, +including recognized signing identities, trusted `builder.id` values, and the maximum SLSA Build +level assigned to those builders. For this project, the release metadata is not the package build +provenance itself. Instead, it is producer-defined expectation metadata that tells a verifier which +Windlass release version corresponds to which immutable reusable workflow commit and SHA-based +builder identities. + +The project needs a release metadata format that is machine-readable, signed, compatible with the +Sigstore and in-toto ecosystem used elsewhere in the profile, easy to distribute with GitHub +Releases, and precise enough for verifier trust decisions. It should also leave room to adopt a more +complete update-metadata system later if release metadata distribution grows beyond a simple signed +manifest model. + +Should Windlass release metadata use a custom signed manifest, SLSA provenance, Git tag signatures, +repository-committed metadata, TUF-style metadata, verifier-embedded allowlists, or another trust +root mechanism? + +## Decision Drivers + +- Satisfy ADR 0028's requirement for signed version-to-SHA metadata without reintroducing mutable + tag refs as the primary machine trust input. +- Align with SLSA v1.2's verifier root-of-trust model: trusted signer identity, trusted + `builder.id`, expected `buildType`, and expected external parameters. +- Use a signing mechanism that supports transparency logs and avoids long-lived signing keys in + release workflows. +- Keep release metadata separate from SLSA build provenance while using a familiar in-toto + attestation envelope. +- Let verifiers make deterministic SHA-based trust decisions without querying current GitHub tag + state as the primary check. +- Keep the initial metadata system small enough for a 1-person organization and early profile + implementation. +- Preserve a path to stronger TUF-style metadata if rollback, freeze, threshold-signing, delegation, + or metadata-mirroring needs become material. + +## Considered Options + +- Publish a Windlass-defined JSON release manifest wrapped as an in-toto Statement and signed as a + Sigstore DSSE bundle. +- Treat the release metadata file as a build artifact and publish SLSA provenance for that artifact. +- Store the version-to-SHA mapping only in GPG-signed annotated Git tag messages. +- Commit release metadata files into the repository and trust signed commits plus protected + branches. +- Use TUF-style role-separated metadata with root, targets, snapshot, and timestamp roles. +- Embed the version-to-SHA allowlist directly in verifier releases or verifier configuration. +- Publish unsigned or checksum-only release metadata. + +## Decision Outcome + +Chosen option: "Publish a Windlass-defined JSON release manifest wrapped as an in-toto Statement and +signed as a Sigstore DSSE bundle", because it gives the verifier authenticated, machine-readable +version-to-SHA expectations while staying aligned with the Sigstore/in-toto trust model already used +for package provenance. + +Each Windlass release that publishes reusable profile workflows should publish a release metadata +manifest as a GitHub Release asset. The manifest should use a Windlass-defined JSON schema carried +in an in-toto Statement. The Statement should use a Windlass-owned release metadata predicate type, +for example: + +```text +https://github.com/windlasstech/slsa-builder/predicate/release-manifest/v1 +``` + +The Statement should be signed as a Sigstore DSSE bundle by the Windlass release workflow. The +bundle is the canonical machine-verifiable release metadata artifact. Plain JSON copies, checksums, +release notes, and Git tag messages may help humans discover the mapping, but they are not +substitutes for the signed DSSE bundle. + +The release manifest predicate should identify at least: + +- the manifest schema version; +- the Windlass release version, such as `vX.Y.Z`; +- the source repository URL; +- the release tag ref; +- the release commit SHA; +- each released profile name; +- each profile workflow path; +- the exact full workflow commit SHA production consumers should pin; +- each SHA-based `builder.id` value trusted for the release; +- each profile `buildType` URI associated with that builder identity; +- the metadata generation time and release workflow identity fields needed by verifier diagnostics. + +The in-toto Statement `subject` should identify the canonical release manifest artifact by digest, +or an equivalent stable release metadata artifact whose digest lets verifiers detect tampering. The +predicate itself should carry the version-to-SHA and builder mapping. The profile architecture +specification may refine exact filenames, digest algorithms, JSON field names, and canonicalization +rules. + +Verifier trust roots for this metadata should include: + +- the configured Sigstore trust root, such as the Sigstore public-good Fulcio/Rekor root unless a + later ADR selects a different root; +- the expected GitHub Actions OIDC signer identity for the Windlass release workflow; +- the Windlass release metadata predicate type and schema version; +- the `github.com/windlasstech/slsa-builder` repository identity; +- the signed and protected release tag policy as release-integrity evidence; +- the allowed profile workflow paths, SHA-based `builder.id` shapes, and `buildType` URIs. + +The verifier should trust a SHA-based `builder.id` through this metadata only when the DSSE +signature verifies against the configured Sigstore root, the signing certificate identity matches +the expected Windlass release workflow identity, the Statement and predicate use the expected types +and schema version, and the metadata maps the requested release version to the exact workflow SHA +and `builder.id` observed in package provenance. + +Git signed annotated tags remain required release-integrity evidence and human discovery handles, +but they are not the canonical machine-readable version-to-SHA trust root. Verifiers should not need +to resolve current GitHub tag state before making the primary machine trust decision, although they +may optionally check tag signatures and tag-to-commit consistency as defense in depth. + +TUF-style metadata is not adopted for the initial release metadata mechanism. The project should +reconsider TUF-style role-separated metadata if at least one of the following becomes true: + +- Windlass maintains multiple release channels, repositories, profile families, or delegated + metadata authorities that need independent signing roles; +- there are multiple trusted maintainers or automated signers that can provide meaningful threshold + signing for root or targets metadata; +- verifier clients need automatic metadata refresh with strong rollback and freeze attack defenses; +- release metadata is mirrored outside GitHub Releases or served through a registry-like metadata + repository; +- key rotation, metadata expiry, timestamping, or delegated ownership becomes difficult to manage + with a single signed manifest per release; +- consumers require a long-lived update framework rather than per-release metadata verification. + +### Consequences + +- Good, because the release metadata is machine-readable and directly supports ADR 0028's + version-to-SHA trust model. +- Good, because Sigstore keyless signing avoids introducing long-lived release metadata signing keys + into the workflow. +- Good, because DSSE and in-toto are consistent with the project's provenance signing direction. +- Good, because verifiers can validate release metadata without trusting mutable Git tag state as + the primary machine input. +- Good, because a Windlass-owned predicate can represent release-to-builder expectations without + overloading SLSA build provenance semantics. +- Good, because Git signed tags remain useful release integrity evidence without becoming the + primary executable reference or metadata format. +- Neutral, because the manifest is Windlass-specific and requires a dedicated verifier policy. +- Bad, because Windlass must define and version a JSON schema, predicate type, file naming policy, + and verifier support. +- Bad, because this does not provide TUF's built-in rollback, freeze, delegation, threshold-signing, + or metadata-expiry model. + +### Confirmation + +This decision is confirmed when the release process, profile architecture specification, +documentation, and verifier define: + +- the Windlass release manifest predicate type and schema version; +- the exact release manifest JSON fields and canonical field semantics; +- the in-toto Statement subject rules and digest algorithms; +- Sigstore DSSE bundle generation by the Windlass release workflow; +- GitHub Release asset names and distribution expectations; +- verifier trust root configuration for Sigstore root, signer identity, repository, predicate type, + schema version, workflow path, `builder.id`, and `buildType` values; +- verifier behavior for mapping a human release version to a full workflow SHA and SHA-based + `builder.id`; +- optional defense-in-depth checks for signed tag consistency; +- documentation of the criteria that would trigger reconsideration of TUF-style metadata. + +Implementation review should verify that package provenance trust does not depend on unsigned +release notes, mutable GitHub tag lookup, repository-committed metadata alone, checksum-only files, +or verifier-embedded allowlists when a signed Windlass release manifest is expected. + +## Pros and Cons of the Options + +### Publish a Sigstore-signed in-toto release manifest + +Windlass defines a JSON release manifest predicate, wraps it in an in-toto Statement, signs the +Statement as a Sigstore DSSE bundle, and publishes the bundle with the GitHub Release. + +- Good, because it authenticates the release version to workflow SHA and `builder.id` mapping needed + by ADR 0028. +- Good, because it fits SLSA's expectation-based verification model without pretending the mapping + is package build provenance. +- Good, because it uses keyless signing, transparency logging, and an attestation envelope already + familiar to SLSA tooling. +- Good, because distribution through GitHub Releases is simple for humans and automation. +- Bad, because Windlass must maintain a custom predicate and verifier policy. +- Bad, because a single per-release manifest is weaker than TUF for update metadata lifecycle + management. + +### Publish SLSA provenance for the release metadata artifact + +The release metadata file is treated as an artifact and receives SLSA build provenance from the +release workflow. + +- Good, because it reuses the standard SLSA provenance predicate and existing provenance tooling. +- Good, because it can prove how the metadata file was produced. +- Bad, because the important payload is not how the metadata was built; it is the producer-defined + expectation mapping inside the metadata. +- Bad, because verifiers still need a Windlass-specific schema to interpret the version-to-SHA + mapping. +- Bad, because this can confuse release expectation metadata with package build provenance. + +### Store the mapping in GPG-signed annotated tags + +The release tag message contains the workflow SHA and builder IDs, and verifiers trust the signed +tag. + +- Good, because release tags are already required to be signed and protected by organization policy. +- Good, because humans can inspect tag messages with standard Git tooling. +- Bad, because tag messages are a poor machine-readable schema and schema evolution surface. +- Bad, because verifiers need Git tag object and signature verification support. +- Bad, because it risks making tag state feel like the primary trust root again, contrary to ADR + 0028's SHA-based machine trust decision. + +### Commit release metadata into the repository + +Release metadata files live in the repository and are protected by signed commits, protected +branches, and signed release tags. + +- Good, because changes are visible in normal source review and history. +- Good, because repository files are easy to browse and diff. +- Bad, because the metadata can be changed after a release unless additional immutability rules are + enforced and verified. +- Bad, because verification can become dependent on current repository or GitHub API state. +- Bad, because repository-committed metadata does not by itself create a per-release signed artifact + distributed with the release. + +### Use TUF-style metadata + +Windlass adopts role-separated metadata such as root, targets, snapshot, and timestamp metadata with +versioning, expiry, threshold signatures, and delegation. + +- Good, because TUF directly addresses rollback, freeze, key rotation, delegation, and threshold + signing for update metadata. +- Good, because it scales well to many release channels, signers, and mirrored metadata stores. +- Bad, because it is operationally heavy for the initial project and a 1-person organization. +- Bad, because threshold-signing benefits are limited if one maintainer controls all signing roles. +- Bad, because verifier and release automation complexity would grow before the project has a clear + need for TUF's stronger lifecycle guarantees. + +### Embed the allowlist in verifier releases or configuration + +The verifier binary or its local configuration contains trusted version-to-SHA mappings. + +- Good, because offline verification is simple once the verifier is updated. +- Good, because no separate metadata download path is needed. +- Bad, because verifier releases become coupled to builder releases. +- Bad, because third-party verifiers and consumers need a separate trust distribution channel. +- Bad, because metadata fixes or new builder releases require verifier updates or config changes. + +### Publish unsigned or checksum-only metadata + +Windlass publishes JSON metadata plus checksums without an authenticated attestation envelope. + +- Good, because it is easy to implement. +- Good, because checksums catch accidental corruption when delivered over a trusted channel. +- Bad, because checksums alone do not identify an authorized signer or trusted release workflow. +- Bad, because this does not satisfy ADR 0028's signed metadata requirement. +- Bad, because verifiers would need to place excessive trust in the release asset distribution + channel. + +## More Information + +This decision follows ADR 0028 and decides only the release metadata format and trust root for +mapping Windlass release versions to SHA-based reusable workflow builder identities. It does not +decide package provenance format, npm provenance submission, registry scope, release-note format, +changelog format, or final verifier command-line UX. + +Reference points considered: + +- SLSA v1.2 verification recommends verifying provenance envelope signatures, artifact subjects, + `predicateType`, trusted `builder.id`, `buildType`, and expected `externalParameters` against + configured roots of trust. +- SLSA v1.2 describes verifier roots of trust as mappings from recognized signing identities and + `builder.id` values to trusted SLSA Build levels. +- SLSA v1.2 distinguishes package build provenance from producer-defined expectations used by + verifiers. +- SLSA v1.2 Build L2 and L3 require signed provenance and stronger protection of provenance + generation, but they do not prescribe this project's release version metadata format. +- Windlass security policy requires signed release tags, signed commits on protected branches, + OIDC-preferred workflows, SHA-pinned workflow references, and signed release provenance wherever + feasible. +- ADR 0028 already made the primary machine trust decision SHA-based and required signed metadata to + preserve human-readable release versions. From 402fa03cb3210c7909bae4602b4ed1bc8ad71e35 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:19 +0900 Subject: [PATCH 10/14] docs(decisions): add release manifest handoff and signing ADRs Define the producer-to-publisher handoff contract and a three-job release manifest signing boundary. Signed-off-by: Yunseo Kim --- ...-producer-to-publisher-handoff-contract.md | 252 ++++++++++++++++ ...e-job-release-manifest-signing-boundary.md | 273 ++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 docs/decisions/0050-define-producer-to-publisher-handoff-contract.md create mode 100644 docs/decisions/0053-use-three-job-release-manifest-signing-boundary.md diff --git a/docs/decisions/0050-define-producer-to-publisher-handoff-contract.md b/docs/decisions/0050-define-producer-to-publisher-handoff-contract.md new file mode 100644 index 0000000..096c143 --- /dev/null +++ b/docs/decisions/0050-define-producer-to-publisher-handoff-contract.md @@ -0,0 +1,252 @@ +--- +parent: Decisions +nav_order: 50 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Define the Producer-to-Publisher Handoff Contract + +## Context and Problem Statement + +ADR 0049 separated artifact production from GitHub Release asset publication. Ecosystem-specific +producer profiles now own source-to-artifact builds and provenance, while the GitHub Release asset +publisher profile verifies an already-produced artifact and publishes it to an existing GitHub +Release. + +That separation creates a new contract surface. The publisher profile must receive artifact bytes, +the release tag, and producer-generated SLSA provenance for the exact final release asset. The +workflow artifact, download path, job output, or other transport used to hand the bytes to the +publisher is not itself a trust root. The publisher must verify digest and producer provenance +before mutating the GitHub Release, then distribute the producer provenance unchanged according to +ADR 0051. + +What information must a producer profile hand to the GitHub Release asset publisher profile, and +what must the publisher verify before upload? + +## Decision Drivers + +- Make the publisher profile safe to compose with multiple ecosystem-specific producer profiles. +- Treat byte transport between producer and publisher jobs as untrusted until digest and provenance + verification succeeds. +- Require a complete, verifier-friendly interface for the publication profile's + `externalParameters`. +- Preserve ADR 0039's one-asset publication unit and ADR 0045's final release asset subject name. +- Keep upstream source-to-artifact provenance mandatory for the production publisher path. +- Require producer provenance to describe the final GitHub Release asset name and exact bytes, so + the publisher does not need a separate signed name-mapping predicate. +- Fail closed before uploading to GitHub Release when any handoff input is missing, ambiguous, or + mismatched. +- Keep future lower-assurance ingestion or mirror workflows separate from the production publisher + identity. + +## Considered Options + +- Require a digest- and provenance-verified producer handoff contract. +- Pass only artifact bytes and recompute the digest in the publisher. +- Pass artifact bytes plus an expected digest, but do not require upstream producer provenance. +- Use a producer-generated manifest as the only handoff input. +- Let each ecosystem producer profile define its own publisher input shape. + +## Decision Outcome + +Chosen option: "Require a digest- and provenance-verified producer handoff contract", because the +publisher profile must not treat arbitrary transported bytes as production release assets unless it +can bind those bytes to trusted upstream producer provenance and the requested GitHub Release asset +slot. + +The production GitHub Release asset publisher profile should accept a fixed, profile-owned handoff +contract from producer profiles. The exact input names belong in the architecture specification, but +the contract must include at least these semantic fields: + +- an artifact handle that lets the publisher retrieve the produced bytes within the same workflow + run or approved workflow boundary; +- the expected artifact digest as lowercase SHA-256, plus any tool-boundary representation such as + `sha256:` where required; +- the final GitHub Release asset name to upload; +- the release tag identifying the existing Git tag and existing GitHub Release target; +- the producer-generated SLSA provenance bundle or a locator from which the publisher can retrieve + and verify the bundle; +- the expected upstream producer `builder.id` or trusted builder policy; +- the expected upstream producer `buildType` or trusted build type policy; +- the expected upstream subject digest and subject name, both of which must match the final GitHub + Release asset; +- the source repository and resolved source revision when they are needed for release policy or + verifier expectations; +- optional sidecar and linked-artifact publication settings selected by earlier ADRs. + +The artifact handle is a transport handle only. It may identify a workflow artifact, file path, +artifact ID, job output, or future internal handoff format, but it must not be treated as proof that +the artifact is trusted. The publisher must retrieve the bytes, compute their digest, and compare +the result with the expected digest before upload. + +The upstream producer provenance must be verified before publication. Verification should include at +least: + +- the upstream attestation signature and trusted signer identity; +- the upstream `predicateType` expected for source-to-artifact SLSA provenance; +- the upstream `builder.id` and `buildType` allowed by the caller's or profile's trusted producer + policy; +- the upstream subject digest matching the expected artifact digest and the bytes to upload; +- the upstream subject name exactly matching the final GitHub Release asset name; +- source repository, source revision, release tag, package or artifact identity, and other producer + `externalParameters` required by the selected producer policy; +- absence of unrecognized or unexpected producer `externalParameters` where the policy requires + strict matching. + +The publisher profile should not sign independent publication evidence in the default production +path. Instead, it should publish or expose the producer-generated provenance bundle unchanged after +successful verification. If a future workflow needs signed publication evidence or a subject-name +mapping predicate, that workflow requires a separate ADR, predicate model, and verifier guidance. + +The publisher must fail before GitHub Release mutation when: + +- the artifact bytes cannot be retrieved; +- the computed digest differs from the expected digest; +- the release tag or target GitHub Release does not exist; +- the final release asset name is invalid or already present under the target release; +- upstream producer provenance is missing, unsigned, unverifiable, untrusted, or mismatched; +- the upstream subject digest does not match the bytes to upload; +- the upstream subject name differs from the final release asset name; +- the producer policy does not allow the upstream `builder.id`, `buildType`, source, release ref, or + external parameters. + +The production publisher profile must not expose an option to bypass upstream provenance +verification. A future lower-assurance upload or mirror workflow may accept raw bytes, rename +artifacts, or sign publication evidence, but it must use a distinct workflow name, evidence model, +and verifier guidance. + +### Consequences + +- Good, because the publisher profile can safely compose with many ecosystem producer profiles + without trusting their transport mechanism. +- Good, because release publication is bound to both artifact digest and upstream source-to-artifact + provenance. +- Good, because consumers verify the same producer provenance that the publisher verified before + upload. +- Good, because requiring the producer subject name to match the final release asset name avoids a + separate signed rename or mapping predicate. +- Neutral, because the publisher profile needs a policy format for trusted producer builders and + build types. +- Neutral, because publisher-side verification is an operational gate rather than independent signed + evidence. +- Bad, because producer profiles must expose stable handoff outputs and upstream provenance + locators. +- Bad, because producer profiles must produce final release asset names before provenance signing. + +### Confirmation + +This decision is confirmed when architecture specifications, workflow implementations, tests, and +documentation define: + +- the public producer-to-publisher input and output names; +- artifact handle retrieval rules and digest calculation behavior; +- mandatory expected SHA-256 digest comparison before upload; +- upstream producer provenance locator or bundle requirements; +- trusted producer policy fields for `builder.id`, `buildType`, signer identity, source, release + ref, subject, and external parameters; +- rejection of producer subject names that differ from the final GitHub Release asset name; +- producer provenance distribution rules and outputs, as selected by ADR 0051; +- failure behavior for missing artifact bytes, digest mismatch, missing or invalid upstream + provenance, invalid release target, duplicate asset name, and release upload failure; +- verifier guidance for checking producer provenance against the downloaded GitHub Release asset; +- tests that prove raw caller-produced artifacts cannot reach the production upload path without + successful upstream provenance verification. + +Implementation review should verify that no workflow artifact name, job output, file path, GitHub +artifact ID, or caller-provided digest can bypass upstream provenance verification, and that the +exact bytes uploaded to the GitHub Release match the expected digest, the verified upstream subject +digest, and the verified upstream subject name. + +## Pros and Cons of the Options + +### Require a digest- and provenance-verified producer handoff contract + +Producer profiles expose artifact bytes, digest, upstream provenance, and policy-relevant identity +metadata. The publisher verifies all of them before upload and republishes the producer provenance +unchanged. + +- Good, because the contract matches ADR 0049's separation of production and publication. +- Good, because the publisher can reject arbitrary bytes even when they arrive through a valid + workflow artifact handle. +- Good, because producer and publisher responsibilities are explicit and independently verifiable. +- Good, because the handoff is stable enough for multiple ecosystem producer profiles. +- Good, because the publisher does not need to define or sign a new attestation format. +- Neutral, because policy configuration is required to describe which producers are trusted. +- Bad, because this is the most implementation-heavy initial handoff option. +- Bad, because producers must align their SLSA subject name with the final GitHub Release asset + name. + +### Pass only artifact bytes and recompute the digest + +The publisher receives or downloads bytes, computes their digest, and uploads them without an +expected digest or upstream producer provenance. + +- Good, because the publisher implementation is small. +- Good, because it can publish any file-like artifact. +- Bad, because the publisher has no trusted source-to-artifact build evidence for the bytes. +- Bad, because a compromised or incorrect caller can substitute arbitrary bytes before publication. +- Bad, because this collapses the production publisher into a raw upload helper rather than a + trusted release asset publisher. + +### Pass artifact bytes plus expected digest, without producer provenance + +The publisher verifies the transported bytes against an expected digest but does not verify how +those bytes were produced. + +- Good, because it prevents transport corruption or accidental artifact mixups. +- Good, because it is compatible with many existing workflows. +- Bad, because the expected digest itself can be caller-controlled without a trusted provenance + root. +- Bad, because SLSA source-to-artifact claims remain absent. +- Bad, because consumers could mistake release upload success for build provenance. + +### Use a producer-generated manifest as the only handoff input + +The producer emits a manifest that names the artifact, digest, release tag, upstream provenance, and +publication metadata. The publisher accepts only the manifest and retrieves everything from it. + +- Good, because one manifest can make the handoff easy to pass through workflow boundaries. +- Good, because the manifest can be reused by release orchestration. +- Neutral, because this may become useful as the stable API after producer profiles mature. +- Bad, because the manifest itself must be authenticated or linked to producer provenance to avoid + becoming a caller-controlled bundle of claims. +- Bad, because it adds a manifest schema before the project has implemented any + producer-to-publisher handoff. + +### Let each ecosystem producer define its own publisher inputs + +Every producer profile exposes whatever outputs it wants, and the publisher adapts to each +ecosystem. + +- Good, because each producer can optimize its own ergonomics. +- Bad, because the publisher becomes a matrix of ecosystem-specific adapters. +- Bad, because verifier guidance and tests fragment across producer profiles. +- Bad, because adding a new ecosystem profile would require changing the publisher's public + contract. + +## More Information + +This decision follows ADR 0049 and replaces the unresolved build-or-ingest portion of the earlier +GitHub Release asset ADR sequence. It depends on ADR 0039's one-asset unit, ADR 0043's existing +release requirement, ADR 0045's final release asset subject name, ADR 0046's selected-asset digest +semantics, ADR 0048's linked-artifact opt-in policy, and ADR 0051's producer provenance distribution +model as publication-profile rules. + +This decision does not define the first ecosystem-specific producer profile, the exact JSON schema +for a future handoff manifest, a future publisher predicate type, or a lower-assurance raw upload +workflow. Those require architecture specifications or future ADRs. + +Reference points considered: + +- SLSA v1.2 Build Provenance treats `externalParameters` as the complete external interface to the + build or operation and expects verifiers to reject unexpected fields. +- SLSA v1.2 Build L3 requires provenance fields to be generated or verified by the trusted build + platform, except permitted tenant-controlled subject information. +- `slsa-github-generator` generic workflows accept caller-provided subject digests, while ecosystem + builders own artifact production semantics. +- GitHub reusable workflows exchange typed inputs, secrets, outputs, and artifacts; those transport + mechanisms are not by themselves provenance or trust roots. +- GitHub `actions/attest` can bind a subject name and digest to a custom predicate and expose a + local Sigstore bundle plus GitHub attestation storage handles for downstream verification. diff --git a/docs/decisions/0053-use-three-job-release-manifest-signing-boundary.md b/docs/decisions/0053-use-three-job-release-manifest-signing-boundary.md new file mode 100644 index 0000000..bad17a2 --- /dev/null +++ b/docs/decisions/0053-use-three-job-release-manifest-signing-boundary.md @@ -0,0 +1,273 @@ +--- +parent: Decisions +nav_order: 53 +status: accepted +date: 12026-07-07 +decision-makers: Yunseo Kim +--- + +# Use a Three-Job Release Manifest Signing Boundary + +## Context and Problem Statement + +ADR 0028 requires production consumers to execute profile-owned reusable workflows by full commit +SHA, and requires signed release metadata that maps human-readable Windlass release versions to the +exact workflow SHAs and SHA-based `builder.id` values. ADR 0031 then selected a Windlass-defined +release manifest carried in an in-toto Statement and signed as a Sigstore DSSE bundle as the +machine-verifiable release metadata mechanism. + +The project now needs to decide the workflow trust boundary for generating, signing, and publishing +that release manifest. The manifest is security-critical because verifiers use it to decide whether +a profile provenance `builder.id`, workflow path, release version, and `buildType` mapping are +trusted. If the same job can freely generate metadata, sign it, and mutate the GitHub Release, then +a bug or compromise in that job can affect both the trust metadata and its distribution channel. + +How should the release manifest signing workflow separate manifest generation, Sigstore signing, and +GitHub Release asset publication for the initial production release process? + +## Decision Drivers + +- Preserve the release manifest as the canonical machine-verifiable version-to-SHA trust metadata + selected by ADR 0031. +- Keep release metadata generation, signing authority, and release asset mutation in distinct trust + and permission boundaries where practical. +- Follow the project pattern of digest-verified handoff between isolated jobs selected for npm + publishing and release asset publication. +- Use GitHub-hosted ephemeral runners, full-SHA-pinned actions, minimal top-level permissions, and + job-level permission elevation. +- Avoid long-lived signing keys and use GitHub Actions OIDC with Sigstore as the initial signing + mechanism. +- Keep the initial implementation smaller than a dedicated reusable workflow or TUF-style metadata + system while leaving a clear migration path. +- Make future migration to a reusable signing workflow, direct Sigstore tooling, `sigstore-go`, or + TUF-style metadata an explicit follow-up decision rather than an accidental implementation drift. + +## Considered Options + +- Use a three-job release manifest workflow: `manifest-generate` → `manifest-sign` → + `manifest-upload`. +- Use a dedicated reusable workflow for release manifest signing from the beginning. +- Generate, sign, and upload the release manifest in one job. +- Use direct Sigstore tooling, such as `cosign` or future `sigstore-go`, instead of `actions/attest` + for the initial signing path. +- Rely on GitHub immutable release attestations, signed tags, release notes, or checksum files + rather than a Windlass-signed release manifest bundle. +- Adopt TUF-style release metadata from the beginning. + +## Decision Outcome + +Chosen option: "Use a three-job release manifest workflow: `manifest-generate` → `manifest-sign` → +`manifest-upload`", because it provides a clear least-privilege boundary for the initial release +metadata trust root while staying small enough for the first implementation. + +The initial Windlass release process should produce release manifest metadata through three primary +jobs: + +```text +manifest-generate -> manifest-sign -> manifest-upload +``` + +The `manifest-generate` job should create the unsigned Windlass release manifest JSON and the +in-toto Statement content required by ADR 0031. It should calculate the manifest digest, upload the +unsigned manifest material as workflow artifacts, and expose only the digest and artifact handles +needed by the signing job. This job should not have `id-token: write`, `attestations: write`, +`contents: write`, release mutation authority, long-lived signing credentials, or access to +publisher secrets. + +The `manifest-sign` job should download the manifest artifacts, recalculate their digests, verify +that the contents match the `manifest-generate` handoff, and sign the release manifest Statement as +a Sigstore bundle. The initial signing adapter should be full-SHA-pinned `actions/attest` in custom +predicate mode or an equivalent `actions/attest` path that signs the Windlass release manifest +predicate selected by ADR 0031. The signing job should have only the permissions needed for signing +and optional GitHub attestation storage: `contents: read`, `id-token: write`, and +`attestations: write`. It should not have `contents: write` or GitHub Release mutation authority. + +The `manifest-upload` job should download the unsigned manifest material and signed Sigstore bundle, +recalculate their digests, verify that those digests match the previous job handoff values, and +upload the release manifest artifacts to the selected existing GitHub Release. This job should have +only the release mutation permission it needs, normally `contents: write`, and should not have +`id-token: write`, `attestations: write`, signing credentials, or authority to create new signed +metadata. The upload job should fail rather than re-signing, regenerating, or mutating manifest +contents after signing. + +The signed Sigstore bundle is the canonical machine-verifiable release metadata artifact. Plain JSON +manifests, checksums, release notes, GPG-signed annotated tags, GitHub immutable release evidence, +and GitHub release-level attestations may provide discovery, diagnostics, defense-in-depth, or +human-readable release integrity evidence, but they do not replace the signed release manifest +bundle as the version-to-SHA trust root selected by ADR 0031. + +The release manifest architecture specification should define exact artifact names, bundle names, +digest encodings, predicate type, schema version, subject rules, signer identity, allowed workflow +path, release tag checks, release asset duplicate behavior, retry behavior, and verification +commands. Jobs must not trust workflow artifact names, logs, job outputs, release notes, or +unchecked files as substitutes for recalculating digests and verifying the signed bundle. + +This decision does not permanently reject a dedicated reusable workflow, direct Sigstore tooling, +`sigstore-go`, or TUF-style metadata. Those options should be reconsidered through follow-up ADRs if +one or more of the following conditions becomes true: + +- multiple release workflows or repositories need to reuse the same release manifest signing + contract; +- verifier policy needs a distinct, stable release-manifest signer identity separate from the main + release orchestration workflow; +- `actions/attest` cannot represent the required custom predicate, bundle, signer identity, + verification, offline validation, or distribution behavior; +- a Go-native trusted core is ready to own Sigstore bundle creation and verification through + `sigstore-go` or equivalent direct libraries; +- release metadata needs threshold signing, delegated roles, expiry, rollback/freeze attack defense, + multiple release channels, mirrored metadata, or independently managed signing authorities; +- GitHub artifact attestation storage, immutable releases, or release verification features change + enough to materially alter the release metadata trust model. + +Any migration to a dedicated reusable workflow, direct `cosign`/`sigstore-go` signing, or TUF-style +metadata must preserve or explicitly replace the verifier-visible trust contract for release +manifest predicate type, schema version, signer identity, release version, workflow path, workflow +SHA, `builder.id`, and `buildType` mapping. + +### Consequences + +- Good, because manifest generation, signing, and release upload have separate job permissions. +- Good, because release mutation authority cannot create or modify signed metadata. +- Good, because signing authority cannot upload or replace GitHub Release assets. +- Good, because the design follows the repository's existing digest-verified handoff pattern. +- Good, because the initial signing path uses GitHub Actions OIDC and avoids long-lived signing + keys. +- Good, because future migration paths are acknowledged without making the first implementation too + large. +- Neutral, because the workflow still depends on GitHub-hosted runners and GitHub-native Sigstore + integration. +- Neutral, because the signed manifest remains Windlass-specific and requires Windlass verifier + policy. +- Bad, because three jobs require more handoff artifacts, digest checks, and failure handling than a + single-job release process. +- Bad, because `actions/attest` remains a trusted action dependency until a future migration + replaces it. + +### Confirmation + +This decision is confirmed when release process specifications, workflow implementations, tests, and +verifier guidance define: + +- exactly three primary release manifest jobs named or equivalent to `manifest-generate`, + `manifest-sign`, and `manifest-upload`; +- minimal top-level workflow permissions and explicit job-level permission elevation; +- no signing authority or release mutation authority in `manifest-generate`; +- `manifest-sign` permissions limited to `contents: read`, `id-token: write`, and + `attestations: write` unless a later ADR changes the signing adapter; +- no `contents: write` or release mutation authority in `manifest-sign`; +- `manifest-upload` release mutation permission, normally `contents: write`, without signing + authority; +- digest-verified handoff from manifest generation to signing and from signing to upload; +- full-SHA pinning for `actions/attest` and any other external action used in the release manifest + signing path; +- the Windlass release manifest predicate type, schema version, subject rules, bundle artifact name, + plain manifest artifact name, digest encoding, signer identity, and verifier policy; +- release upload behavior for missing releases, duplicate manifest assets, partial upload failure, + retries, and immutable release publication flows; +- documentation that plain JSON manifests, checksums, release notes, signed tags, GitHub immutable + release evidence, and release-level attestations are complementary evidence, not substitutes for + the signed release manifest bundle; +- follow-up ADR criteria for migration to a dedicated reusable workflow, direct Sigstore or + `sigstore-go` signing, or TUF-style metadata. + +Implementation review should verify that no production path can publish or trust release manifest +metadata unless the manifest was generated, signed, digest-verified, and uploaded through the +selected boundary, and that no job combines signing authority with GitHub Release mutation authority +without a later ADR. + +## Pros and Cons of the Options + +### Use a three-job release manifest workflow + +Release manifest generation, signing, and upload run in distinct jobs with digest-verified handoff +between jobs. + +- Good, because each job has a small, auditable permission set. +- Good, because signing and release mutation authorities are separated. +- Good, because it aligns with the project's existing three-job profile patterns. +- Neutral, because it is GitHub Actions-specific. +- Bad, because it introduces cross-job artifact and digest-handling complexity. + +### Use a dedicated reusable workflow from the beginning + +Release manifest signing is moved into a standalone reusable workflow with its own public contract +and signer identity. + +- Good, because multiple release workflows can reuse the same signing contract. +- Good, because verifier policy can trust a distinct release-manifest signer workflow. +- Bad, because it adds public API, builder identity, release metadata, and bootstrap complexity + before reuse pressure exists. +- Bad, because the first implementation must version and document another reusable workflow surface. + +### Generate, sign, and upload in one job + +One release job creates the manifest, signs it, and uploads it to the GitHub Release. + +- Good, because it is simplest to implement. +- Good, because there are no cross-job handoff artifacts. +- Bad, because one job needs both signing authority and release mutation authority. +- Bad, because a compromise or bug in that job affects both the trust metadata and its distribution + channel. +- Bad, because it does not match the repository's least-privilege release profile pattern. + +### Use direct Sigstore tooling instead of `actions/attest` + +The release workflow invokes `cosign`, `sigstore-go`, or equivalent direct Sigstore tooling to +create the signed bundle. + +- Good, because it can provide more direct control over bundle format, offline verification, and + non-GitHub distribution. +- Good, because it aligns with the long-term Go trusted core direction if implemented through + `sigstore-go`. +- Bad, because it requires Windlass to own more Sigstore integration, compatibility, and verifier + behavior in the first release. +- Bad, because it bypasses the existing `actions/attest` adapter decision unless a follow-up ADR + changes that boundary. + +### Rely on GitHub release evidence or signed tags instead of a signed manifest bundle + +The release process uses GitHub immutable releases, release attestations, GPG-signed annotated tags, +release notes, checksums, or a plain manifest rather than a Windlass-signed release manifest bundle. + +- Good, because these mechanisms are familiar and useful as defense-in-depth evidence. +- Good, because signed tags and immutable release evidence remain valuable release integrity + signals. +- Bad, because they do not provide the Windlass-specific machine-readable version-to-SHA, + `builder.id`, workflow path, and `buildType` mapping selected by ADR 0031. +- Bad, because verifiers would have to infer trust from loosely related evidence instead of checking + one signed predicate schema. + +### Adopt TUF-style metadata from the beginning + +Release metadata is modeled as TUF targets, roles, expiry, and signed metadata rather than a single +signed in-toto release manifest. + +- Good, because TUF is strong for delegated roles, threshold signing, expiry, mirrors, rollback + defense, and multi-channel release metadata. +- Bad, because it is operationally heavy for the initial single-repository, single-maintainer + release process. +- Bad, because threshold and delegated-role benefits are limited until the organization has multiple + trusted authorities or release channels. + +## More Information + +This decision follows ADR 0028 and ADR 0031. It complements ADR 0035's initial `actions/attest` +signing adapter decision and the three-job permission-boundary pattern selected for production npm +publishing and release asset publication. + +This decision does not define the full release manifest JSON schema, exact artifact filenames, +consumer verifier CLI interface, TUF metadata repository, dedicated reusable workflow contract, +direct `sigstore-go` signing implementation, or long-term release-channel policy. Those belong in +architecture specifications or future ADRs. + +Reference points considered: + +- SLSA Build L3 requires provenance or equivalent metadata authenticity and strong resistance to + tenant forgery, including keeping signing material away from user-controlled build steps. +- GitHub Actions artifact attestations use short-lived OIDC identity and require explicit + `id-token: write` and `attestations: write` permissions. +- GitHub Actions hardening guidance recommends full commit SHA pinning, least-privilege + `GITHUB_TOKEN` permissions, hosted ephemeral runners, and caution around privileged workflows. +- Windlass organization security policy requires SHA-pinned actions, least-privilege job + permissions, OIDC instead of long-lived credentials, signed releases, protected release refs, and + SLSA Build L3-oriented provenance wherever feasible. From e40a91ed2e3d6dce0c8105617deafc120f468ff1 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 12:53:21 +0900 Subject: [PATCH 11/14] docs(decisions): add ADR for initial verification deliverables Summarize the verification deliverables and validation strategy for the JS/TS npm and GitHub release asset profiles. Signed-off-by: Yunseo Kim --- ...efine-initial-verification-deliverables.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/decisions/0037-define-initial-verification-deliverables.md diff --git a/docs/decisions/0037-define-initial-verification-deliverables.md b/docs/decisions/0037-define-initial-verification-deliverables.md new file mode 100644 index 0000000..61d2e6d --- /dev/null +++ b/docs/decisions/0037-define-initial-verification-deliverables.md @@ -0,0 +1,264 @@ +--- +parent: Decisions +nav_order: 37 +status: accepted +date: 12026-07-06 +decision-makers: Yunseo Kim +--- + +# Define Initial Verification Deliverables + +## Context and Problem Statement + +ADR 0028 selected a SHA-pinned reusable workflow `builder.id`, ADR 0029 selected Windlass-generated +SLSA provenance as the canonical npm package provenance, ADR 0031 selected a Sigstore-signed release +manifest, ADR 0035 selected `actions/attest` as the initial signing adapter, and ADR 0036 selected a +three-job graph with publish-time verification before npm registry mutation. + +Those decisions require verifier behavior, but they do not yet define what verifier-facing artifacts +Windlass must ship in the initial production JS/TS npm package SLSA3 profile. The open question is +whether the first release must include a standalone consumer verifier CLI, only document verifier +policy, rely on ecosystem tools, or provide a smaller set of policy and conformance deliverables +that can support a later standalone verifier without committing to one too early. + +What verification deliverables should the initial production profile ship? + +## Decision Drivers + +- Preserve SLSA v1.2's expectation that provenance is useful only when checked against trusted roots + and package-specific expectations. +- Ensure npm registry mutation is blocked unless the package tarball and signed Windlass provenance + bundle pass producer-side verification. +- Avoid making a premature public CLI/API commitment before the Go trusted core owns Sigstore bundle + verification and Windlass policy evaluation. +- Keep consumer guidance explicit enough to verify Windlass-produced artifacts using existing tools + where possible. +- Make full-SHA builder identity, release manifest, package identity, source identity, and runtime + policy expectations testable. +- Avoid delegating Windlass-specific SLSA policy entirely to generic ecosystem tools that do not + know the profile's required `builder.id`, `buildType`, `externalParameters`, and package-directory + semantics. +- Preserve a clear future path for a standalone verifier CLI or upstream verifier integration. + +## Considered Options + +- Ship a standalone verifier consumer CLI in the initial release. +- Ship only documented verifier policy. +- Implement only the publish job's producer-side verification gate. +- Ship producer-side publish verification, documented verifier policy, fixtures, and reference + commands, but no standalone consumer verifier CLI. +- Ship a thin wrapper around `gh attestation verify`. +- Make `slsa-verifier` compatibility the official initial verifier deliverable. + +## Decision Outcome + +Chosen option: "Ship producer-side publish verification, documented verifier policy, fixtures, and +reference commands, but no standalone consumer verifier CLI", because this gives the first +production profile concrete verification behavior and conformance evidence without locking Windlass +into a consumer verifier interface before the trusted Go implementation and Sigstore policy surface +are ready. + +The initial production JS/TS npm package SLSA3 profile should include these verification +deliverables: + +- a producer-side verification gate in the `publish` job selected by ADR 0036; +- an architecture specification section or document that defines Windlass verifier policy; +- conformance fixtures for accepted and rejected package/provenance/bundle combinations; +- reference commands and guidance for existing tools such as `gh attestation verify`, + `npm audit signatures`, and, where compatible, `slsa-verifier`; +- explicit non-goal language that the initial profile does not ship a standalone consumer verifier + CLI. + +The `publish` job remains in scope as production logic, not merely documentation. It should verify +the package tarball digest, signed bundle digest, Sigstore signer identity, SLSA statement subject, +`predicateType`, `builder.id`, `buildType`, `externalParameters`, source identity, package identity, +package version, package directory, runtime policy, and release manifest expectations before running +`npm publish --provenance-file` or an equivalent external provenance-file submission. It should fail +closed before registry mutation if any required check fails. + +The documented verifier policy should explain which expectations are enforced by Windlass before +publish, which expectations consumers should independently verify after publication, and which +checks generic ecosystem tools do and do not cover. It should describe expected roots of trust and +identity bindings in terms of the Sigstore root, GitHub Actions reusable workflow signer identity, +SHA-pinned Windlass `builder.id`, SLSA `predicateType`, SLSA `buildType`, external parameters, +package identity, source identity, and release manifest linkage. + +The conformance fixture set should include at least one valid package tarball and signed provenance +bundle shape, plus invalid cases for digest mismatch, signature or signer mismatch, wrong +`predicateType`, wrong `builder.id`, wrong `buildType`, unrecognized or mismatched +`externalParameters`, source identity mismatch, package identity or version mismatch, +package-directory mismatch, runtime-policy mismatch, and release manifest mismatch. Fixtures may be +synthetic until the implementation can generate real signed bundles, but their expected pass/fail +semantics must match the architecture specification. + +The reference guidance may show existing tools as partial verifier building blocks. +`gh attestation verify` can verify GitHub/Sigstore attestation signature material, predicate type, +signer workflow, source ref, and source digest, and can emit JSON for additional policy checks. +`npm audit signatures` can report registry signatures and npm-recognized attestations for installed +packages, but it is not a complete Windlass policy verifier. `slsa-verifier` may be documented as an +ecosystem verifier or future compatibility target only where it can express the profile's required +expectations. + +Windlass should add a standalone verifier CLI only after a later ADR decides the command scope, +input model, offline/online Sigstore behavior, release manifest policy, error taxonomy, and +compatibility responsibilities. Upstream `slsa-verifier` support may also be pursued later, but the +initial profile should not make its release criteria depend on external verifier support for +Windlass-specific policy. + +### Consequences + +- Good, because initial production publishing cannot proceed without producer-side verification. +- Good, because consumers receive explicit policy guidance instead of implicit trust in unsigned + workflow outputs. +- Good, because fixtures make verifier semantics testable before a public verifier CLI exists. +- Good, because the future standalone verifier can reuse the same policy and fixture corpus. +- Good, because `gh attestation verify`, `npm audit signatures`, and `slsa-verifier` are positioned + accurately as useful tools with specific limits. +- Good, because Windlass avoids prematurely owning a consumer CLI surface for Sigstore verification, + npm registry lookup, release manifest policy, and error compatibility. +- Neutral, because the publish job performs verifier-like checks inside the producer workflow while + consumers still need independent verification for defense in depth. +- Bad, because users do not get a single verify command in the initial release. +- Bad, because reference commands cannot fully enforce Windlass-specific policy without additional + manual or scripted checks. +- Bad, because fixture design must be kept aligned with the future implementation to avoid stale + policy examples. + +### Confirmation + +This decision is confirmed when the initial JS/TS npm package profile architecture specification, +workflow implementation, tests, and verifier guidance define: + +- the `publish` job's producer-side verification gate before npm registry mutation; +- exact pass/fail checks for tarball digest, signed bundle digest, Sigstore signature, signer + identity, `subject`, `predicateType`, `builder.id`, `buildType`, `externalParameters`, source + identity, package identity, package version, package directory, runtime policy, and release + manifest linkage; +- documented roots of trust and identity bindings for the initial Windlass production builder; +- documented consumer-side verification expectations and the limits of npm registry verification; +- reference commands for `gh attestation verify`, `npm audit signatures`, and any supported + `slsa-verifier` compatibility path; +- conformance fixtures that include both accepted and rejected verification cases; +- hard-failure behavior for every invalid fixture category; +- an explicit statement that a standalone consumer verifier CLI is not part of the initial profile; +- a future-decision note for standalone verifier CLI or upstream verifier integration. + +Implementation review should verify that no documentation or workflow output implies that generic +tool success alone is equivalent to complete Windlass policy verification unless all +Windlass-specific expectations have also been checked. + +## Pros and Cons of the Options + +### Ship a standalone verifier consumer CLI + +Windlass would include a first-party command such as `windlass verify npm-package` that consumes a +package tarball, signed provenance bundle, release manifest, and expected policy inputs. + +- Good, because it gives users the clearest consumer-side verification UX. +- Good, because Windlass-specific policy can be enforced by one tool. +- Good, because full-SHA `builder.id`, release manifest, and profile-specific expectations can be + checked without generic-tool gaps. +- Bad, because it expands the initial trusted implementation surface significantly. +- Bad, because Windlass would immediately own Sigstore root handling, online/offline bundle + verification, registry lookup, error taxonomy, and CLI compatibility. +- Bad, because a premature verifier interface could become difficult to change after consumers + automate it. + +### Ship only documented verifier policy + +Windlass would define what consumers should verify, but would not implement publish-time +verification or provide fixtures and reference commands. + +- Good, because it is the smallest documentation-only scope. +- Good, because SLSA expectation fields can be described before implementation. +- Bad, because it would not satisfy ADR 0036's publish-time verification gate. +- Bad, because users would have no concrete examples or conformance evidence. +- Bad, because policy could drift from implementation without fixtures. + +### Implement only the publish job's producer-side verification gate + +Windlass would verify package and provenance artifacts before publish but would not document +consumer policy or provide reusable fixtures. + +- Good, because it protects npm registry mutation from bad package/provenance combinations. +- Good, because the checks live in the workflow path that must be correct for release. +- Bad, because it does not explain what downstream consumers should verify. +- Bad, because internal verification behavior would be harder to audit without documented policy and + fixtures. +- Bad, because producer-side verification does not cover registry compromise or download-time + threats. + +### Ship publish verification, documented policy, fixtures, and reference commands + +Windlass includes the producer-side verification gate, policy documentation, conformance fixtures, +and examples using existing tools, but does not ship a standalone consumer verifier CLI. + +- Good, because it balances initial security value with implementation scope. +- Good, because it makes verifier semantics testable before public CLI stabilization. +- Good, because it supports defense-in-depth guidance without overclaiming generic verifier + coverage. +- Good, because it creates a stable base for a later verify command. +- Bad, because users must combine reference tools and policy guidance rather than run one Windlass + verifier command. +- Bad, because Windlass must clearly communicate which tool results are partial. + +### Ship a thin wrapper around `gh attestation verify` + +Windlass would provide a small command or action that shells out to, or directly mirrors, +`gh attestation verify`. + +- Good, because GitHub CLI already verifies GitHub/Sigstore attestation identity and predicate type. +- Good, because it aligns with the initial `actions/attest` signing adapter. +- Bad, because it would still need extra Windlass policy checks for `builder.id`, `buildType`, + `externalParameters`, release manifest, and npm package semantics. +- Bad, because a wrapper could look complete while only enforcing part of the policy. +- Bad, because it would inherit GitHub CLI versioning and output-shape compatibility concerns. + +### Make `slsa-verifier` compatibility the official initial verifier deliverable + +Windlass would treat existing `slsa-verifier` support as the initial verification surface, or block +release on upstream compatibility. + +- Good, because `slsa-verifier` is the main SLSA ecosystem verifier precedent. +- Good, because it already supports source, builder, package, and provenance checks for several + builders, including experimental npm package verification. +- Bad, because npm package support is still experimental. +- Bad, because Windlass-specific profile policy may not be expressible without upstream changes. +- Bad, because initial Windlass release criteria would depend on external support for the profile's + exact builder identity and policy semantics. + +## More Information + +This decision follows ADR 0028, ADR 0029, ADR 0031, ADR 0035, and ADR 0036. It decides initial +verification deliverables only. It does not change the canonical provenance format, signing adapter, +job graph, release manifest decision, or future option to provide a standalone verifier. + +Reference points considered: + +- SLSA v1.2 Verifying Artifacts recommends checking the provenance envelope signature, artifact + subject digest, `predicateType`, trusted builder identity, `buildType`, and `externalParameters`; + unrecognized external parameters should generally fail verification. +- SLSA v1.2 describes package ecosystem, consumer, and monitor verification as valid verification + locations, with package upload-time verification useful but not a replacement for consumer-side + verification against registry or download-time compromise. +- SLSA v1.2 Build Requirements require authentic provenance at Build L2 and unforgeable provenance + at Build L3, including protection of provenance authentication material from user-defined build + steps. +- The SLSA GitHub Generator technical design treats the provenance verifier as part of the trusted + computing base and describes verification of Fulcio/Sigstore identity, subject digest, builder + identity, source identity, and policy fields. +- The SLSA GitHub Generator Node.js builder documents `npm audit signatures` and `slsa-verifier` as + verification paths while also using secure package/provenance download and hash checks during + publish. +- `slsa-verifier` provides established consumer-side SLSA verification UX and policy flags, but npm + package verification is documented as experimental and cannot be assumed to cover all + Windlass-specific policy without compatibility work. +- GitHub `gh attestation verify` can verify artifact attestations, predicate type, signer workflow, + source ref, and source digest, and can emit JSON for further policy checks; GitHub warns that + predicate contents need trusted-builder context and additional policy care. +- GitHub `actions/attest` emits Sigstore bundle JSON and is verifiable with GitHub attestation + tooling, but generic attestation success does not by itself prove Windlass package, release + manifest, or profile policy expectations. +- npm provenance documentation exposes `npm audit signatures` for installed package registry + signatures and attestations, but npm provenance does not guarantee absence of malicious code and + is not a complete Windlass policy verifier. From e323d123e14f2fe3ef3d3828057614c78725e549 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 13:27:00 +0900 Subject: [PATCH 12/14] docs(decisions): update AGENTS.md for ADR range 0000-0053 Expand the ADR index in docs/decisions/AGENTS.md to reflect the current sequence of 54 ADRs (0000-0053) and group the JS/TS npm, release asset, and manifest signing topic ranges. Signed-off-by: Yunseo Kim --- docs/decisions/AGENTS.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/decisions/AGENTS.md b/docs/decisions/AGENTS.md index c98fba4..1ce61bf 100644 --- a/docs/decisions/AGENTS.md +++ b/docs/decisions/AGENTS.md @@ -3,37 +3,37 @@ ## OVERVIEW Architecture decision records for the SLSA builder. Each ADR is a MADR 4.0.0 document with a -sequential four-digit number and a kebab-case title. +sequential four-digit number and a kebab-case title. The sequence currently runs from `0000` through +`0053`. ## STRUCTURE ```text docs/decisions/ -├── 0000-use-markdown-architectural-decision-records.md +├── 0000-use-markdown-architectural-decision-records.md # MADR template ├── 0001-start-slsa-builder-as-clean-repository.md ├── 0002-use-extensible-trusted-reusable-workflow-foundation.md -├── 0003-use-thin-core-with-profile-owned-reusable-workflows.md -├── 0004-use-go-as-primary-implementation-language.md -├── 0005-use-dedicated-linter-toolchain.md -├── 0006-use-golangci-lint-as-go-linter-runner.md -├── 0007-use-shellcheck-for-shell-glue-linting.md -├── 0008-use-dedicated-formatters-for-go-markdown-and-shell.md -├── 0009-use-node-js-as-development-tool-runtime.md -├── 0010-use-pnpm-for-node-js-development-tooling.md -├── 0011-use-lefthook-for-local-git-hook-orchestration.md -└── 0012-development-tool-runtime-and-bootstrap.md +├── ... # 0003–0052 +├── 0053-use-three-job-release-manifest-signing-boundary.md +└── AGENTS.md ``` +ADR numbering is sequential from `0000` through `0053`. See the WHERE TO LOOK table below for topic +groupings. + ## WHERE TO LOOK -| Topic | ADR | Notes | -| ----------------------------- | ------------------------------ | ----------------------------------------------- | -| Why the repo exists | `0001` | Clean-repository foundation. | -| Trusted workflow architecture | `0002`, `0003` | Core vs. profile-owned reusable workflows. | -| Implementation language | `0004` | Go for trusted core; shell stays glue. | -| Linter choices | `0005`, `0006`, `0007` | golangci-lint, ShellCheck, no universal bundle. | -| Formatter choices | `0008` | gofmt/goimports, shfmt, Prettier for Markdown. | -| Dev tooling runtime | `0009`, `0010`, `0011`, `0012` | Node/pnpm, Lefthook, mise bootstrap. | +| Topic | ADR | Notes | +| ----------------------------- | ------------------------------ | --------------------------------------------------------------- | +| Why the repo exists | `0001` | Clean-repository foundation. | +| Trusted workflow architecture | `0002`, `0003` | Core vs. profile-owned reusable workflows. | +| Implementation language | `0004` | Go for trusted core; shell stays glue. | +| Linter choices | `0005`, `0006`, `0007` | golangci-lint, ShellCheck, no universal bundle. | +| Formatter choices | `0008` | gofmt/goimports, shfmt, Prettier for Markdown. | +| Dev tooling runtime | `0009`, `0010`, `0011`, `0012` | Node/pnpm, Lefthook, mise bootstrap. | +| JS/TS npm package profile | `0013`–`0035` | Package manager selection, OIDC publishing, SLSA3 npm workflow. | +| GitHub release asset profile | `0036`–`0052` | Release asset subject handling, attestation distribution. | +| Release manifest signing | `0053` | Three-job signing boundary. | ## CONVENTIONS From 8a8a36fb635dd0be95829d2fa8af6d40e568e14e Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 13:27:07 +0900 Subject: [PATCH 13/14] ci(workflows): add AGENTS.md documenting CI workflow conventions Add a dedicated knowledge base file for GitHub Actions workflows that covers SHA-pinning, hardened-runner usage, minimal permissions, and the reusable-org-workflow patterns used across the repository. Signed-off-by: Yunseo Kim --- .github/workflows/AGENTS.md | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/AGENTS.md diff --git a/.github/workflows/AGENTS.md b/.github/workflows/AGENTS.md new file mode 100644 index 0000000..07cd07c --- /dev/null +++ b/.github/workflows/AGENTS.md @@ -0,0 +1,50 @@ +# CI/Workflow Knowledge Base + +## OVERVIEW + +GitHub Actions workflows for lint, autoformat, dependency review, OSV scanning, and OpenSSF +Scorecard. All workflows are hardened by default; the authoritative security policy lives in +`windlasstech/.github`. + +## STRUCTURE + +```text +.github/workflows/ +├── autofix.yml # autoformat PRs/pushes with Prettier, golangci-lint fmt, shfmt +├── dependency-review.yml # PR/merge-group dependency review gate +├── lint.yml # markdownlint, actionlint, golangci-lint +├── osv-scanner.yml # scheduled + PR/push vulnerability scanning +└── scorecard.yml # OpenSSF Scorecard on branch-protection/main/schedule events +``` + +## WHERE TO LOOK + +| Task | File | Notes | +| -------------------------- | ----------------------- | ------------------------------------------------------ | +| Required code-quality gate | `lint.yml` | Runs on PR and push to `main`. | +| Autoformat a PR | `autofix.yml` | Pushes formatter fixes via `autofix.ci`. | +| Dependency review | `dependency-review.yml` | Ignores docs/markdown-only changes. | +| Vulnerability scanning | `osv-scanner.yml` | Reusable org workflow; runs on schedule too. | +| Supply-chain scorecard | `scorecard.yml` | Reusable org workflow. | +| Local hook equivalents | `lefthook.yml` | Pre-commit formatters → linters; commit-msg DCO check. | + +## CONVENTIONS + +- **SHA-pinning**: every third-party action is pinned to a full SHA with a comment tag. +- **Hardened runner**: every job starts with `step-security/harden-runner` in + `egress-policy: audit`. +- **Minimal permissions**: top-level `permissions: {}` with job-level elevation only where required. +- **Reusable org workflows**: security workflows (`dependency-review.yml`, `osv-scanner.yml`, + `scorecard.yml`) call `windlasstech/.github` workflows on the main branch. +- **No secrets**: initial workflows avoid publishing credentials; see ADRs for trusted publishing + plans. +- **Go formatting**: use `golangci-lint fmt`/`golangci-lint run`; there is no standalone `gofmt` CI + step. + +## ANTI-PATTERNS + +- Do not use floating tags for third-party actions (e.g., `@v3`). Always pin a SHA. +- Do not add `contents: write` to a job that does not need it. +- Do not add a workflow that bypasses `windlasstech/.github` dependency-review or OSV scanner gates. +- Do not reference reusable workflows by mutable branch or tag; use the org’s main branch refs. +- Do not add long-lived cloud credentials; prefer OIDC where elevation is required. From 20f12055646b0dfd8da7fdc6f07086e012500ad2 Mon Sep 17 00:00:00 2001 From: Yunseo Kim Date: Tue, 7 Jul 2026 13:27:13 +0900 Subject: [PATCH 14/14] docs(agents): update root AGENTS.md metadata and links Refresh the generated metadata to the current commit and branch, add the .github/workflows directory to the project structure, and point readers to the new docs/decisions and .github/workflows AGENTS.md files. Signed-off-by: Yunseo Kim --- AGENTS.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6a6fc32..ceb1146 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,8 @@ # PROJECT KNOWLEDGE BASE -- **Generated:** 12026-06-28 -- **Commit:** 7c6fcbe -- **Branch:** setup/initial-tooling +- **Generated:** 12026-07-07 +- **Commit:** e40a91e +- **Branch:** docs/adr-npm-and-release-asset-profiles ## OVERVIEW @@ -16,6 +16,7 @@ the Go implementation tree has not been added yet. ├── AGENTS.md # this file ├── README.md / README.ko.md ├── docs/decisions/ # architecture ADRs (see docs/decisions/AGENTS.md) +├── .github/workflows/ # CI/CD workflows (see .github/workflows/AGENTS.md) ├── .golangci.yml # Go format/lint policy ├── lefthook.yml # git hooks ├── mise.toml / mise.lock # pinned runtimes and tools @@ -27,8 +28,9 @@ the Go implementation tree has not been added yet. | Task | Location | Notes | | ----------------------- | ---------------------------------------------------------- | ---------------------------------------- | -| Why a decision was made | `docs/decisions/` | MADR 4.0.0 ADRs, numbered `0000`–`0012`. | +| Why a decision was made | `docs/decisions/` | MADR 4.0.0 ADRs, numbered `0000`–`0053`. | | Bootstrap / dev setup | `README.md`, `mise.toml` | `mise install` + `pnpm install`. | +| CI / workflow security | `.github/workflows/` | See `.github/workflows/AGENTS.md`. | | Lint/format policy | `.golangci.yml`, `.prettierrc`, `.markdownlint-cli2.jsonc` | Go, Markdown, shell. | | Git hooks / DCO | `lefthook.yml` | Commit-msg `Signed-off-by:` check. | | Dependency security | `pnpm-workspace.yaml` | Cooldown, trust policy, frozen lockfile. |