diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d91515d3..011df687 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -235,3 +235,61 @@ jobs: jr-*.tar.gz jr-*.zip jr-*.sha256 + + attest: + name: Attest build provenance + needs: build + # Fork opt-in gate, matching this repo's release-ops convention + # (SIGNING_ENABLED, HOMEBREW_TAP_REPO, RELEASE_GAP_FILL_ENABLED, + # SYNC_UPSTREAM_REPO — see docs/specs/fork-friendly-release-ops.md). + # Forks carry this file verbatim as a no-op unless they opt in. + # Attestations require `id-token: write` and publish the workflow's + # commit SHA to the public Rekor transparency log; they are + # unavailable on GHES and on private forks without GitHub Enterprise + # Cloud. Gating keeps the file byte-identical downstream while never + # hard-failing a fork's release. + if: vars.ATTESTATIONS_ENABLED == 'true' + # Runs in PARALLEL with `release` (both `needs: build`), not upstream + # of it. Both fan in from the same immutable `build` artifacts, so the + # attested bytes are identical to what `release` uploads verbatim + # (softprops copies them unchanged) — the attestation is created from + # the built bytes, not re-downloaded from the release page, so there is + # no publish-then-attest window (TOCTOU / CWE-362). It is deliberately + # NOT a `needs:` of `release`: with the fork gate above, a skipped + # `attest` would otherwise cascade and skip the fork's entire release. + permissions: + # attest-build-provenance needs exactly these two; neither is + # inherited. contents: read is NOT needed — the artifact download + # goes through the Actions artifacts API, not the repo contents API. + id-token: write + attestations: write + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Fan in the built artifacts (same objects `release` uploads) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + merge-multiple: true + path: release-assets + + # Attest the archives only. The `.sha256` sidecars are integrity + # metadata already covered by the archive attestation; attesting + # them would be circular. + # + # Coverage boundary: this attests the bytes produced by release.yml. + # The fork-optional `sign-and-publish.yml` re-signs the darwin + # binaries and emits `.pkg`/`.dmg` with DIFFERENT digests after + # notarization — those are not covered here and need their own attest + # step in that workflow (after codesign/notarize, before upload), + # gated on the same variable. Out of scope for this PR. + - name: Attest build provenance + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: | + release-assets/*.tar.gz + release-assets/*.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index 831db3f1..047b05f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to jr will be documented here. ## [Unreleased] +### Added + +- **CI: build provenance attestations for release artifacts (opt-in):** `release.yml` gains an `attest` job that produces a GitHub Artifact Attestation with SLSA build provenance for the `.tar.gz`/`.zip` release archives, verifiable with `gh attestation verify` and natively by mise's `github:` backend. Attestation is created from the built artifacts (parallel to `release`, fanning in from the same `build` job outputs), so it covers the exact bytes users download without a publish-then-attest window. Gated on `vars.ATTESTATIONS_ENABLED` so forks carry it as a no-op. See `docs/specs/fork-friendly-release-ops.md`. + ## [0.6.0-dev.10] - 2026-07-15 ### Breaking Changes diff --git a/CLAUDE.md b/CLAUDE.md index 2ce7b290..b0bc67fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -330,6 +330,7 @@ When adding a new feature: - `JR_CONFIG_DIR` env var overrides the config directory in debug builds (cross-platform test isolation seam; see BC-6.2.017). Debug builds only — release binaries ignore this env var. The override is read before the `#[cfg(windows)]` / `#[cfg(not(windows))]` OS split in `src/config.rs::global_config_dir()`, so it works identically on all platforms. Pinned by `tests/config_dir_release_gate.rs`. - `JR_CACHE_DIR` env var overrides the cache root directory in debug builds (cross-platform test isolation seam; see BC-6.2.017). Debug builds only — release binaries ignore this env var. Identical gate pattern to `JR_CONFIG_DIR`, applied in `src/cache.rs::cache_root()`. Pinned by `tests/config_dir_release_gate.rs`. - `JR_SERVICE_NAME` env var overrides the keychain service name in debug builds (test isolation seam — lets keyring integration tests scope their own namespace without touching a developer's real `jr-jira-cli` entries). Debug builds only — release binaries ignore this env var and always use the compiled-in default `"jr-jira-cli"`. Gated via `#[cfg(debug_assertions)]` at `src/api/auth.rs::service_name()` (SEC-JR-SERVICE-NAME-GATE). Pinned by `tests/jr_service_name_release_gate.rs`. In `tests/oauth_refresh_integration.rs` (keyring-gated, `#[ignore]`) `JR_SERVICE_NAME` is set in-process via `set_env` serialized by the `env_lock` mutex, rather than a subprocess `.env()` call — safe because those tests are gated and sequential. +- **Release-ops repo-variable gates** — `SIGNING_ENABLED`, `HOMEBREW_TAP_REPO`, `RELEASE_GAP_FILL_ENABLED`, `SYNC_UPSTREAM_REPO`, `ATTESTATIONS_ENABLED` (all GitHub Actions repository variables, never read by `src/` code) gate the opt-in signing/backfill/gap-fill/fork-sync/attestation workflows. All unset in the canonical repo → those workflows are no-ops; downstream forks opt in. `ATTESTATIONS_ENABLED == 'true'` enables the `attest` job in `release.yml` (SLSA build provenance for release archives). Same fail-safe pattern as `JR_E2E_ENABLED`. See `docs/specs/fork-friendly-release-ops.md`. - `JR_STDIN_IS_TTY` env var forces TTY mode in debug builds (interactive-branch test seam; when set to `"1"`, suppresses the `src/main.rs` auto-`--no-input` flip that fires on non-TTY stdin; release builds ignore this env var). Gated via `#[cfg(debug_assertions)]` @@ -337,7 +338,6 @@ When adding a new feature: Used by interactive confirmation tests in `tests/comment_delete.rs` (VP-577-013, VP-577-030) and `tests/comment_edit.rs` (VP-577-029 — S-577-5 interactive `--public` confirmation gate). -- **Release-ops repo-variable gates** — `SIGNING_ENABLED`, `HOMEBREW_TAP_REPO`, `RELEASE_GAP_FILL_ENABLED`, `SYNC_UPSTREAM_REPO` (all GitHub Actions repository variables, never read by `src/` code) gate the opt-in signing/backfill/gap-fill/fork-sync workflows. All unset in the canonical repo → those workflows are no-ops; downstream forks opt in. Same fail-safe pattern as `JR_E2E_ENABLED`. See `docs/specs/fork-friendly-release-ops.md`. - **When adding a new `JR_*` test-seam env var:** grep `CLAUDE.md` for existing `JR_*` entries and add a parallel line in the SAME commit as the code change. This is the codified doc-fallout pattern from #335/#357; first applied retroactively when `JR_BULK_UNKNOWN_GRACE_SECS` and `JR_BULK_AWAIT_TIMEOUT_SECS` shipped without documentation. - **Citation discipline for external-tracker IDs in user-facing strings:** before citing a JRACLOUD-*/GitHub/community ID in anything a user sees (stderr, errors, JSON, hints) or in literal rustdoc, Perplexity-validate the source actually documents the symptom — issue #361 had three misattributed JRACLOUD tickets survive multiple PRs. Also ensure the string is valid in the user's env (e.g. JQL allows one ORDER BY) and keep paraphrasing rustdoc in lockstep. - **Citation form in spec/CLAUDE.md:** prefer symbol-form (`::` or `… § ""`) over line numbers, which drift on refactor. Fall back to `:~NN` (`~` = approximate); never a bare `:NN-MM` for new citations. (#408) diff --git a/docs/specs/fork-friendly-release-ops.md b/docs/specs/fork-friendly-release-ops.md index 17bed6fd..2bc44a19 100644 --- a/docs/specs/fork-friendly-release-ops.md +++ b/docs/specs/fork-friendly-release-ops.md @@ -42,6 +42,7 @@ in both repos, so syncs are conflict-free. | `HOMEBREW_TAP_REPO` | `owner/homebrew-name` tap repo to publish formulas to; also enables the homebrew jobs | unset | | `RELEASE_GAP_FILL_ENABLED` | `'true'` enables the daily gap-fill schedule | unset | | `SYNC_UPSTREAM_REPO` | `owner/repo` to merge from on a schedule (forks only) | unset | +| `ATTESTATIONS_ENABLED` | `'true'` enables the `attest` job in `release.yml` (GitHub Artifact Attestation / SLSA build provenance for the release archives; publishes the workflow's commit SHA to the public Rekor transparency log). Requires `id-token: write`; unavailable on GHES and on private forks without GitHub Enterprise Cloud | unset | | `GITLEAKS_DISABLED` | `'true'` disables the gitleaks secret-scan job in `ci.yml`; for forks that cannot obtain a gitleaks org/commercial license or prefer an alternative scanner | unset | This is the same fail-safe pattern as `vars.JR_E2E_ENABLED`