Skip to content

ci: pin GitHub Actions to commit SHAs (S-1.01) - #295

Merged
Zious11 merged 1 commit into
developfrom
ci/pin-github-actions-shas
May 7, 2026
Merged

ci: pin GitHub Actions to commit SHAs (S-1.01)#295
Zious11 merged 1 commit into
developfrom
ci/pin-github-actions-shas

Conversation

@Zious11

@Zious11 Zious11 commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Pin every uses: line in CI/release workflows to a full 40-hex-character commit SHA with the original tag preserved as a trailing comment (e.g., actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2)
  • 20 uses: sites pinned: 15 in ci.yml, 5 in release.yml; 10 distinct action+tag combinations resolved
  • Closes supply-chain attack surface where mutable tag refs (especially softprops/action-gh-release@v2 in release.yml, which has contents: write and receives JR_BUILD_OAUTH_CLIENT_ID/JR_BUILD_OAUTH_CLIENT_SECRET from repo secrets) could be silently redirected to malicious content by a compromised upstream

Story

S-1.01 — Wave 1, first HIGH-priority NFR/infra story

Traces to:

  • NFR-S-E (supply-chain security — HIGH severity)
  • R-H6 (risk register: mutable action tags in CI)
  • cicd-setup.md GAP-1 (floating action refs)

No story dependencies. This is independent infrastructure work.

Architecture Changes

graph TD
    A[".github/workflows/ci.yml<br/>15 uses: lines"] -->|tag → SHA + comment| B["SHA-pinned<br/>ci.yml"]
    C[".github/workflows/release.yml<br/>5 uses: lines"] -->|tag → SHA + comment| D["SHA-pinned<br/>release.yml"]
    E["Dependabot<br/>github-actions ecosystem<br/>(weekly)"] -->|automated SHA updates| B
    E -->|automated SHA updates| D
Loading

Blast radius: CI configuration only. No source code, no binaries, no API surface changed. Purely mechanical text substitution — identical action code runs via immutable SHA resolution instead of mutable tag resolution.

Performance impact: None. SHA resolution is handled by the GitHub Actions runner identically to tag resolution.

Story Dependencies

graph LR
    S101["S-1.01<br/>Pin Actions SHAs<br/>(this PR)"] --> DEV["develop"]
    W0["Wave 0 (S-0.01…S-0.06)<br/>All merged"] --> DEV
Loading

No depends_on entries. This story is independent of all Wave 0 stories.

Spec Traceability

flowchart LR
    NFR["NFR-S-E<br/>Supply-chain security HIGH"] --> AC001["AC-001<br/>All uses: SHA-pinned"]
    NFR --> AC002["AC-002<br/>No floating refs"]
    NFR --> AC003["AC-003<br/>SHAs match claimed tags"]
    NFR --> AC004["AC-004<br/>Dependabot active"]
    AC001 --> GREP1["grep -E 'uses:.*@[a-f0-9]{40}'<br/>20/20 PASS"]
    AC002 --> GREP2["grep -E 'uses:.*@v[0-9]'<br/>0 matches PASS"]
    AC003 --> SHA_VER["SHA→tag verification<br/>10 combinations PASS"]
    AC004 --> DEP["dependabot.yml<br/>github-actions weekly PASS"]
    GREP1 --> COMMIT["2282874<br/>ci(S-1.01): pin all GitHub Actions<br/>to full commit SHAs"]
    GREP2 --> COMMIT
    SHA_VER --> COMMIT
    DEP --> COMMIT
Loading

Acceptance Criteria Status

AC Description Status
AC-001 Every uses: line ends in a 40-hex-char SHA; tag preserved as comment PASS — 20/20
AC-002 No uses: line retains @v<N>, @stable, @<semver>, or @<tool-name> PASS — 0 floating refs
AC-003 Pinned SHAs correspond to the same action versions currently in use PASS — 10 combos verified
AC-004 dependabot.yml package-ecosystem: github-actions weekly still active PASS — unchanged

Pinned Actions Reference

ci.yml (15 sites)

Action Tag SHA
actions/checkout v6 de0fac2e4500dabe0009e67214ff5f5447ce83dd
Swatinem/rust-cache v2 e18b497796c12c097a38f9edb9d0641fb99eee32
dtolnay/rust-toolchain 1.85.0 c93f4f9c67595668add93d3d6895795ce52d8c2d
dtolnay/rust-toolchain stable 29eef336d9b2848a0b548edc03f92a220660cdb8
taiki-e/install-action cargo-llvm-cov e5de28abeb52d916c5e5875d54b21a9e738b61ec
EmbarkStudios/cargo-deny-action v2 91bf2b620e09e18d6eb78b92e7861937469acedb
codecov/codecov-action v6 57e3a136b779b570ffcdbf80b3bdc90e7fab3de2

release.yml (5 sites)

Action Tag SHA Risk
actions/checkout v6 de0fac2e4500dabe0009e67214ff5f5447ce83dd High
dtolnay/rust-toolchain stable 29eef336d9b2848a0b548edc03f92a220660cdb8 Medium
actions/upload-artifact v7 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a Medium
actions/download-artifact v8 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c Medium
softprops/action-gh-release v2 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 HIGH — OAuth secrets

Test Evidence

tdd_mode: facade — no Rust tests; verification is grep + YAML parse.

Check Command Result
SHA-pinned count grep -E 'uses:.*@[a-f0-9]{40}' .github/workflows/*.yml | wc -l 20
Floating refs grep -E 'uses:.*@v[0-9]' .github/workflows/*.yml 0 matches
Floating @stable grep -E 'uses:.*@stable' .github/workflows/*.yml 0 matches
Floating @main grep -E 'uses:.*@main' .github/workflows/*.yml 0 matches
YAML parse (ci.yml) yq e '.' ci.yml > /dev/null valid
YAML parse (release.yml) yq e '.' release.yml > /dev/null valid
Rust build (no changes) cargo build clean (no recompilation)
Unit tests cargo test --lib 600/600 passed
Clippy cargo clippy --all --all-features --tests -- -D warnings clean
Format cargo fmt --all -- --check clean

Mutation kill rate: N/A — facade story.
Coverage delta: 0% — no Rust source changes.

Demo Evidence

N/A — facade story. The diff is the evidence. No UI or runtime behavior changed.

Verification commands for reviewer:

# Count SHA-pinned uses: lines (must equal 20)
grep -E 'uses:.*@[a-f0-9]{40}' .github/workflows/*.yml | wc -l

# Confirm zero floating refs
grep -E 'uses:.*@(v[0-9]|stable|main|master)' .github/workflows/*.yml || echo "PASS: no floating refs"

# Spot-check a SHA against GitHub API (replace org/action and SHA as needed)
gh api repos/actions/checkout/git/commits/de0fac2e4500dabe0009e67214ff5f5447ce83dd --jq '.sha'
gh api repos/softprops/action-gh-release/git/commits/3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 --jq '.sha'
gh api repos/EmbarkStudios/cargo-deny-action/git/commits/91bf2b620e09e18d6eb78b92e7861937469acedb --jq '.sha'

Validation Paradox Note

This PR's CI run will use the pinned SHAs introduced by this very PR. That is intentional and desirable: if a SHA is wrong (typo or stale), the CI job will fail with an action resolution error, surfacing the problem before merge. CI passing on this PR IS the validation that the SHAs resolve correctly. If CI fails on an action resolution error, that is a real SHA issue to investigate.

Holdout Evaluation

N/A — evaluated at wave gate. No Phase 4 holdout exists for CI infrastructure state.

Adversarial Review

N/A — evaluated at Phase 5. No adversarial review required for pure CI configuration changes.

Security Review

Risk addressed: Supply-chain attack via mutable tag references in GitHub Actions workflows.

Highest-risk mitigation: softprops/action-gh-release@v2 → pinned SHA 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65. This action runs with permissions: contents: write and receives JR_BUILD_OAUTH_CLIENT_ID and JR_BUILD_OAUTH_CLIENT_SECRET from repository secrets. A compromised tag ref in this position had direct read access to the embedded OAuth app credentials.

Residual risk: SHA staleness — SHAs pinned at implementation time will drift from latest. Dependabot (package-ecosystem: github-actions, weekly) mitigates by opening automated SHA update PRs.

OWASP relevance: A06:2021 — Vulnerable and Outdated Components (CI/CD pipeline component integrity).

No CRITICAL or HIGH security findings. This PR closes a HIGH-severity gap rather than introducing one.

Risk Assessment

Dimension Assessment
Blast radius CI configuration only; no source, binary, or API surface
Performance impact None
Breaking change False — identical action code runs; resolution mechanism changes from mutable tag to immutable SHA
Rollback Revert commit 2282874; CI immediately reverts to tag-based resolution
Residual risk SHA staleness (Dependabot mitigates)

AI Pipeline Metadata

Field Value
Pipeline mode BROWNFIELD
Story wave 1
tdd_mode facade
Implementation commit 2282874
Models used claude-sonnet-4-6
Phase phase-3-tdd-implementation

Pre-Merge Checklist

  • Diff touches only .github/workflows/ci.yml and .github/workflows/release.yml
  • All 20 uses: lines pinned to 40-hex-char SHA
  • Zero floating refs (@v<N>, @stable, @main)
  • All SHAs verified against claimed tags
  • YAML parse clean for both files
  • cargo build / cargo test --lib / cargo clippy / cargo fmt all clean
  • Dependabot github-actions ecosystem active
  • No source code changes (scope verification: diff stat shows only 2 files)
  • No demo evidence required (facade story)
  • Security review: closes HIGH gap, no new findings
  • CI passing on this PR (validates SHA resolution works)
  • PR reviewer approved
  • No dependency PRs (none — independent story)

Resolves the supply-chain risk where mutable version tags (@v6, @stable, etc.)
could be force-pushed by an action publisher to point at malicious content.
release.yml is the highest-risk workflow because softprops/action-gh-release
receives OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET from repo secrets — a
compromised action would leak embedded OAuth credentials.

- 15 uses: sites pinned in .github/workflows/ci.yml
- 5 uses: sites pinned in .github/workflows/release.yml
- Each pin: @<40-hex-sha>  # <original-tag> for human readability
- Dependabot github-actions ecosystem remains active for weekly SHA updates
- AC-001: all 20 uses: lines now carry a 40-hex SHA (briefing said 19;
  actual count was 20 — dtolnay/rust-toolchain@stable in coverage job added)
- AC-002: no floating semver, @stable, @main, or bare tool-name refs remain
- AC-004: .github/dependabot.yml github-actions ecosystem unchanged
- YAML syntax: both files validated clean with yq
- No source code changes; 600 lib tests pass; clippy/fmt clean

SHA pin table (10 distinct action+tag combinations):
  actions/checkout@v6                    de0fac2e4500dabe0009e67214ff5f5447ce83dd
  Swatinem/rust-cache@v2                 e18b497796c12c097a38f9edb9d0641fb99eee32
  dtolnay/rust-toolchain@stable         29eef336d9b2848a0b548edc03f92a220660cdb8
  dtolnay/rust-toolchain@1.85.0         c93f4f9c67595668add93d3d6895795ce52d8c2d
  EmbarkStudios/cargo-deny-action@v2     91bf2b620e09e18d6eb78b92e7861937469acedb
  taiki-e/install-action@cargo-llvm-cov  e5de28abeb52d916c5e5875d54b21a9e738b61ec
  codecov/codecov-action@v6             57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
  actions/upload-artifact@v7             043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
  actions/download-artifact@v8           3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
  softprops/action-gh-release@v2         3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
@Zious11

Zious11 commented May 7, 2026

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Spot-checked 3 SHAs via GitHub API — all verified correct:

  • actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd resolves directly to a commit tagged v6
  • softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 resolves directly to a commit tagged v2
  • EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb resolves as the dereferenced commit target of the annotated tag v2 (tag object 5bb39ff → commit 91bf2b6)

Additional checks:

  • All 20 uses: lines are SHA-pinned (15 in ci.yml, 5 in release.yml)
  • Zero floating refs remain
  • Comment format @<sha> # <tag> with two spaces before # is consistent across all 20 sites
  • Diff contains only uses: substitutions — no workflow logic changed
  • Commit prefix ci: matches CLAUDE.md Conventional Commits convention

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@Zious11
Zious11 merged commit adae3c5 into develop May 7, 2026
7 checks passed
@Zious11
Zious11 deleted the ci/pin-github-actions-shas branch May 7, 2026 22:12
Zious11 added a commit that referenced this pull request May 7, 2026
- S-1.01 (pin GitHub Actions SHAs): MERGED at adae3c5 via PR #295
- 20 uses: lines pinned (15 ci.yml + 5 release.yml); 10 action+tag → SHA
- 7/7 CI green; review APPROVE 1 cycle; 0 deferred
- Notable: dispatcher allowed clean merge (no admin bypass needed)
- Lesson 4 captured: dispatcher policy variable per-PR; try non-admin first
- sprint-state.yaml: S-1.01 → completed; S-1.02 → active
- Phase 3 Wave 1 progress: 1/8
Zious11 added a commit that referenced this pull request May 7, 2026
Story: S-1.03 (Wave 1 — third story; first non-facade)
NFR: NFR-O-A

Changes:
- Add tracing 0.1.41 + tracing-subscriber 0.3.19 (env-filter + fmt features)
- src/main.rs: init_tracing() with EnvFilter (WARN/DEBUG/TRACE by --verbose/--verbose-bodies); RUST_LOG override
- src/api/client.rs: rate-limit events migrated to tracing::debug!; method+URL retains [verbose] prefix per SD-003 contract (variable-extraction trick)
- src/api/auth.rs: tracing::info!/debug! at OAuth login + refresh entry points; secrets never appear in tracing fields
- src/observability.rs: docstring updated post-S-1.03 (SHOULD-FIX from review)
- 10 new observability tests; 6 SD-003 verbose_bodies tests preserved; 600 lib baseline unchanged
- cargo deny check exits 0 (no new duplicate-version conflicts from tracing transitives)

Code review: APPROVE (1 SHOULD-FIX applied as commit 06c2252; 1 DEFER for body logging migration to tracing::trace! - tracked for Wave 2 cleanup).
CI: 7/7 green.

Related: follows PR #295 (S-1.01), #296 (S-1.02); preserves SD-003 contract from PR #294 (S-0.06).
Zious11 added a commit that referenced this pull request May 7, 2026
)

Story: S-1.04 (Wave 1 — fourth story; tdd_mode: facade)
Risk: R-L12

Changes:
- 6 timeout-minutes added in .github/workflows/ci.yml: fmt 5m, clippy 15m,
  test 30m, msrv 15m, deny 10m, coverage 30m
- 2 timeout-minutes added in .github/workflows/release.yml: build 60m
  (matrix; LTO + cross-compile), release 30m
- All values 12-17x historical observed runtime headroom
- Resolves R-L12: hung jobs no longer consume full 6-hour GitHub default

Code review: APPROVE (0 blocking, 0 SHOULD-FIX, 3 deferred non-blocking observations).
CI: 7/7 green.

Related: follows S-1.01 (#295), S-1.02 (#296), S-1.03 (#297).
Zious11 added a commit that referenced this pull request May 8, 2026
- Final Wave 1 story (S-1.08): MERGED at ab19783 via PR #302
- Wave 1 sequence: PRs #295-#302 (S-1.01..S-1.08)
- 0 production regressions; ~50 new tests
- 5 deferred items in Drift Items; 1 PENDING_MANUAL (S-1.05-AC-001)
- STATE.md compacted; Wave 1 narrative archived to wave-1-summary.md
- Wave 1 retrospective in lessons.md (Lesson 5 candidate)
- sprint-state: S-1.08 → completed; S-2.01 → active; Wave 2 ACTIVE
- Phase 3 progress: 16/31 (~52%)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant