diff --git a/.archgate/adrs/CI-001-pin-github-actions-by-hash.md b/.archgate/adrs/CI-001-pin-github-actions-by-hash.md index c6182569..00690c1a 100644 --- a/.archgate/adrs/CI-001-pin-github-actions-by-hash.md +++ b/.archgate/adrs/CI-001-pin-github-actions-by-hash.md @@ -58,6 +58,11 @@ uses: owner/action@<40-char-sha> # - Local workflow references (e.g., `uses: ./.github/workflows/smoke-test.yml`) — these reference the same repository and do not carry supply chain risk - Local composite actions (e.g., `uses: ./.github/actions/my-action`) — same repository, same trust boundary - Docker container references (e.g., `uses: docker://image:tag`) — governed by separate container image policies +- The SLSA reusable workflow `slsa-framework/slsa-github-generator/.github/workflows/*` — see "Carved-out exceptions" below + +**Carved-out exceptions:** + +- **`slsa-framework/slsa-github-generator/.github/workflows/*`** — The SLSA generator's bootstrap script (`generate-builder.sh`) extracts the version from the workflow ref to download the prebuilt builder binary from a GitHub release. It explicitly rejects non-tag refs with `Invalid ref: ... Expected ref of the form refs/tags/vX.Y.Z`. This is documented upstream as [slsa-framework/slsa-github-generator#150](https://github.com/slsa-framework/slsa-github-generator/issues/150). The reusable workflow MUST therefore be referenced by tag (e.g., `@v2.1.0`). Trust is anchored in the SLSA project's own signing/verification chain rather than in SHA pinning at the call site. Confirmed empirically: pinning by SHA broke the `v0.31.0` release pipeline (run [25107195589](https://github.com/archgate/cli/actions/runs/25107195589)). **Version comment format:** The comment after the SHA MUST contain the human-readable version that the SHA corresponds to (e.g., `# v6`, `# v2.4.3`, `# v2.1.0`). This enables: @@ -110,8 +115,8 @@ uses: owner/action@<40-char-sha> # - **Mitigation**: Renovate is configured in the repository and understands SHA-pinned GitHub Action references. The `renovate.json` configuration includes GitHub Actions as an update target. Regular Renovate PRs ensure pins stay current. - **Incorrect SHA resolution**: A contributor might resolve the SHA for the wrong tag, or the tag might be an annotated tag whose SHA differs from the commit SHA. - **Mitigation**: The automated rule checks that all third-party `uses:` references match the `@<40-char-hex>` pattern. Code review MUST verify that the SHA matches the intended version by cross-referencing the action's releases page. For annotated tags, resolve the underlying commit via `gh api repos///git/ref/tags/` and follow the `object` if `type` is `"tag"`. -- **Reusable workflow compatibility**: Some reusable workflow providers (e.g., SLSA framework) have historically recommended tag references for verifier compatibility. SHA pinning may not be forward-compatible with all verification tools. - - **Mitigation**: The SLSA GitHub Generator v2.x explicitly supports SHA-based builder identity verification. Test SLSA provenance verification after pinning by running `slsa-verifier verify-artifact` against a release built with the SHA-pinned workflow. +- **Reusable workflow compatibility**: Some reusable workflow providers cannot be referenced by SHA. The SLSA GitHub Generator (`slsa-framework/slsa-github-generator/.github/workflows/*`) is the known case for this project — its bootstrap script reads the workflow ref to fetch the prebuilt builder from a GitHub release and rejects non-tag refs (upstream issue [#150](https://github.com/slsa-framework/slsa-github-generator/issues/150)). This was confirmed empirically when the `v0.31.0` release failed after SHA pinning. + - **Mitigation**: The SLSA reusable workflow is carved out as a documented exception (see "Scope" above). It is referenced by tag (`@v2.1.0`). The automated `no-unpinned-actions` rule allowlists this specific path so the exception is enforced rather than being a silent gap. Any other provider claiming SHA pinning is unsupported MUST be evaluated on a case-by-case basis and added to the allowlist with an explicit justification before merging. ## Compliance and Enforcement @@ -131,7 +136,9 @@ Code reviewers MUST verify: Local workflow and action references (`uses: ./.github/workflows/...` or `uses: ./.github/actions/...`) are exempt — they reference code in the same repository and are governed by the repository's own access controls. Docker container references (`uses: docker://...`) are also exempt from this ADR. -No exceptions for third-party references. If an upstream provider claims SHA pinning is unsupported, escalate to the project maintainer for evaluation before merging. +The SLSA reusable workflow (`slsa-framework/slsa-github-generator/.github/workflows/*`) is exempt because its bootstrap script requires a tag-format ref to fetch the builder binary; see "Carved-out exceptions" under Decision. The `no-unpinned-actions` rule explicitly allowlists this path. + +For any other third-party reference where an upstream provider claims SHA pinning is unsupported: escalate to the project maintainer, document the upstream limitation in this ADR's "Carved-out exceptions" list, and update the rule allowlist before merging. Silent exceptions are not permitted. ## References diff --git a/.archgate/adrs/CI-001-pin-github-actions-by-hash.rules.ts b/.archgate/adrs/CI-001-pin-github-actions-by-hash.rules.ts index 882dfe37..85630a1e 100644 --- a/.archgate/adrs/CI-001-pin-github-actions-by-hash.rules.ts +++ b/.archgate/adrs/CI-001-pin-github-actions-by-hash.rules.ts @@ -13,11 +13,28 @@ const USES_PATTERN = /uses:\s+(?!\.\/|docker:\/\/)(\S+@\S+)/g; */ const PINNED_SHA_PATTERN = /^.+@[0-9a-f]{40}\b/; +/** + * Carved-out exceptions where the upstream provider does not support SHA pinning. + * Each entry is matched against the part of the `uses:` value before the `@`. + * See CI-001 "Carved-out exceptions" for the rationale per entry. + */ +const SHA_PIN_EXEMPT_PREFIXES = [ + // SLSA generator reads the workflow ref to download the prebuilt builder from + // a GitHub release; rejects non-tag refs. Upstream issue: + // https://github.com/slsa-framework/slsa-github-generator/issues/150 + "slsa-framework/slsa-github-generator/.github/workflows/", +]; + +function isShaPinExempt(ref: string): boolean { + const beforeAt = ref.split("@")[0]; + return SHA_PIN_EXEMPT_PREFIXES.some((prefix) => beforeAt.startsWith(prefix)); +} + export default { rules: { "no-unpinned-actions": { description: - "Third-party GitHub Actions and reusable workflows must be pinned by full commit SHA", + "Third-party GitHub Actions and reusable workflows must be pinned by full commit SHA (with documented exceptions)", async check(ctx) { const matches = await ctx.grepFiles( USES_PATTERN, @@ -32,6 +49,7 @@ export default { if (!usesMatch) continue; const ref = usesMatch[1]; + if (isShaPinExempt(ref)) continue; if (!PINNED_SHA_PATTERN.test(ref)) { ctx.report.violation({ message: `Unpinned action reference: "${ref}". Pin by full 40-character commit SHA with a version comment (e.g., \`actions/checkout@ # v6\`).`, diff --git a/.claude/agent-memory/archgate-developer/MEMORY.md b/.claude/agent-memory/archgate-developer/MEMORY.md index 50d7b1d1..0fc9c5bf 100644 --- a/.claude/agent-memory/archgate-developer/MEMORY.md +++ b/.claude/agent-memory/archgate-developer/MEMORY.md @@ -41,6 +41,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv - **Module-level `{ ...Bun.env }` captures env at import time** — Spreading `Bun.env` into a module-level constant freezes the env snapshot. Tests that override `Bun.env.HOME` after import won't affect the constant. Fix: use a function that returns `{ ...Bun.env, ... }` on each call so it picks up test-time overrides. Applied in `src/helpers/credential-store.ts`. - **`Bun.Glob.scan({ dot: false })` silently drops dot-prefixed segments — even on explicit paths** — `dot: false` (the default) skips matches whose path contains a `.`-prefixed segment, including patterns that explicitly name the dir (e.g. `.github/workflows/release.yml`). Behavior also varies across platforms — Windows reliably drops the match while Linux can match the same pattern, so a rule appears to "work in CI" but no-ops locally. For code repos where `.github/`, `.husky/`, `.vscode/` are first-class source dirs, ALWAYS pass `dot: true` to `Bun.Glob.scan()`. Applied in `src/engine/runner.ts` (`ctx.glob`, `ctx.grepFiles`) and `src/engine/git-files.ts` (`resolveScopedFiles`). See archgate/cli#222. - **PowerShell 5.1 reads BOM-less `.ps1` files as ANSI — never use non-ASCII chars in `install.ps1`** — `install.ps1` has no UTF-8 BOM (first bytes are `# A...`). On Windows PowerShell 5.1, this means the file is decoded as the system codepage (Windows-1252), so multi-byte UTF-8 characters like em-dash (`—`, `\xE2\x80\x94`) get split into garbage bytes that break later string parsing — the parser reports cryptic errors like "string is missing the terminator" on lines that look fine. ALWAYS stick to ASCII (`-`, `--`, straight quotes) in comments and string literals in `install.ps1`. To verify after editing: `[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path .\install.ps1).Path, [ref]$null, [ref]$errs)`. Same caveat applies to any unsigned `.ps1` file the project distributes. +- **SLSA reusable workflow MUST be tag-pinned, not SHA-pinned** — `slsa-framework/slsa-github-generator/.github/workflows/*` looks like it should follow CI-001 (SHA pin), but the SLSA generator's `generate-builder.sh` reads the workflow ref to download the prebuilt builder from a GitHub release and rejects non-tag refs (`Invalid ref: ... Expected ref of the form refs/tags/vX.Y.Z`, exit 2). Pinning by SHA broke the v0.31.0 release ([run 25107195589](https://github.com/archgate/cli/actions/runs/25107195589)). The CI-001 rule allowlists this path so it does NOT block a SHA repin — meaning a future agent could "fix" the tag pin and the rule would be silent until the next release fails. ALWAYS keep `@v2.x.y` for `release-binaries.yml:165` and read the inline comment + CI-001 "Carved-out exceptions" before changing. Upstream issue: [slsa-framework/slsa-github-generator#150](https://github.com/slsa-framework/slsa-github-generator/issues/150). ## Validation Pipeline diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 15452788..e8052ad8 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -162,7 +162,11 @@ jobs: actions: read id-token: write contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 + # SLSA reusable workflow MUST be referenced by tag, not SHA — the generator's + # generate-builder.sh extracts the version from the ref to download the builder + # binary from a GitHub release and rejects non-tag refs. See CI-001 exception + # and slsa-framework/slsa-github-generator#150. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: "${{ needs.combine-hashes.outputs.digests }}" upload-assets: true