From 1a37813b04d4d3f03a88e5a0a2b3529f584992d4 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Thu, 30 Jul 2026 10:36:51 -0700 Subject: [PATCH] fix: pin GitHub Actions to commit SHAs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `uses:` referenced a mutable tag. A tag can be repointed by whoever controls the upstream repo, and these workflows run with a token that has `contents: write` — enough to push tags and publish releases. A moved tag would execute new code with that token and nothing in the repo would change. Pin to the commits `@v4` and `@v5` resolve to today, so behavior is identical and only the mutability is removed: actions/checkout 11d5960 (v4.4.0) actions/setup-go 40f1582 (v5.6.0) Add a CI step that fails on any `uses:` not pinned to a full 40-character SHA, so this cannot regress. It anchors on `uses:` as a YAML step key rather than matching the token anywhere, since an earlier version flagged its own grep pattern; verified it passes when clean and catches tags, @main, and abbreviated SHAs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VSrrciEDBTuFNMtKocScML --- .github/workflows/ci.yml | 21 +++++++++++++++++++-- .github/workflows/release.yml | 6 +++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e524994..c25fc80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,9 +12,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod @@ -28,6 +28,23 @@ jobs: exit 1 fi + # A tag is mutable: whoever controls it can repoint it at new code that + # runs with this workflow's token. Pinning to a commit SHA is the only + # form GitHub will not silently resolve to something else, so fail if an + # unpinned `uses:` reappears. + - name: actions are pinned to SHAs + run: | + # Only match `uses:` as a YAML step key (optional "- " then uses:), + # so this check's own grep pattern and comments are not scanned. + all_uses="$(grep -rhoE '^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*[^[:space:]]+' .github/workflows/ \ + | sed -E 's/.*uses:[[:space:]]*//' | sort -u)" + unpinned="$(printf '%s\n' "$all_uses" | grep -vE '@[0-9a-f]{40}$' || true)" + if [ -n "$unpinned" ]; then + echo "these must be pinned to a full 40-character commit SHA:" + printf '%s\n' "$unpinned" + exit 1 + fi + - name: vet run: go vet ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6eb5f2c..d978820 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: outputs: tag: ${{ steps.pick.outputs.tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 # need all tags to find the latest version @@ -60,14 +60,14 @@ jobs: if: needs.version.outputs.tag != '' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: # Build from the tag itself, not the branch head, so the release is # reproducible even if main moves on while this job runs. ref: ${{ needs.version.outputs.tag }} fetch-depth: 0 # full history so release notes can diff against the previous tag - - uses: actions/setup-go@v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod