From bff144d30251efb0a66b4380257eae4ffdce8abe Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Sun, 25 Oct 2026 14:47:21 +0100 Subject: [PATCH] fix: pin nested actions by commit SHA Repositories can require actions to be pinned to a full-length commit SHA. GitHub enforces that on the actions used by a composite action as well, so the tag references in `action.yml` made the action fail at startup, with a bare `startup_failure`, for every adopter with that policy enabled. Pin all five actions that `action.yml` uses to the commit their current moving tag or branch resolves to, with the exact release as a comment. This does not change which code runs. Add a test that rejects any non-local `uses:` in `action.yml` that is not pinned to a full SHA. --- action.yml | 24 +++++++++++----------- tests/test_action_pinning.py | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 tests/test_action_pinning.py diff --git a/action.yml b/action.yml index 9b2bf3e..ee958d8 100644 --- a/action.yml +++ b/action.yml @@ -271,7 +271,7 @@ runs: # which is the part worth failing before. - name: Setup Python if: steps.guard.outputs.skip != 'true' - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.12' @@ -318,7 +318,7 @@ runs: - name: Report LLM configuration failure if: steps.guard.outputs.skip != 'true' && steps.llm.outputs.error != '' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.pr_number != '' continue-on-error: true - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ steps.guard.outputs.comment_id }} number: ${{ steps.guard.outputs.pr_number }} @@ -368,7 +368,7 @@ runs: - name: Update review progress if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' continue-on-error: true - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ steps.guard.outputs.comment_id }} number: ${{ steps.guard.outputs.pr_number }} @@ -384,7 +384,7 @@ runs: - name: Checkout analysis target if: steps.guard.outputs.skip != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: repository: ${{ steps.guard.outputs.checkout_repo }} ref: ${{ steps.guard.outputs.checkout_ref }} @@ -395,7 +395,7 @@ runs: - name: Setup Java for CodeBoarding if: steps.guard.outputs.skip != 'true' - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: '21' @@ -518,7 +518,7 @@ runs: - name: Publish baseline analysis if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' && steps.sync_commit.outputs.baseline_sha != '' && steps.state.outputs.cfg_hash != '' && github.server_url == 'https://github.com' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.baseline_sha }} path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base @@ -534,7 +534,7 @@ runs: - name: Publish baseline analysis for the analyzed commit if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' && steps.sync_commit.outputs.analyzed_sha != '' && steps.sync_commit.outputs.analyzed_sha != steps.sync_commit.outputs.baseline_sha && steps.state.outputs.cfg_hash != '' && github.server_url == 'https://github.com' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.analyzed_sha }} path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base @@ -625,7 +625,7 @@ runs: - name: Publish this analysis for the next run if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.state.outputs.warmstart_name != '' && github.server_url == 'https://github.com' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ steps.state.outputs.warmstart_name }} path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/warmstart @@ -639,7 +639,7 @@ runs: id: publish_base if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_baseline.outputs.publish_base == 'true' && steps.guard.outputs.is_fork != 'true' && steps.state.outputs.base_name != '' && github.server_url == 'https://github.com' continue-on-error: true - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ steps.state.outputs.base_name }} path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base @@ -686,7 +686,7 @@ runs: - name: Upload review artifact id: upload_review_artifact_dotcom if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && github.server_url == 'https://github.com' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: codeboarding-review-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ steps.review_artifact.outputs.artifact_dir }} @@ -717,7 +717,7 @@ runs: - name: Post review comment id: review_comment if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ steps.guard.outputs.comment_id }} number: ${{ steps.guard.outputs.pr_number }} @@ -744,7 +744,7 @@ runs: - name: Post review failure if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success' && steps.llm.outputs.error == '' continue-on-error: true - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: header: ${{ steps.guard.outputs.comment_id }} number: ${{ steps.guard.outputs.pr_number }} diff --git a/tests/test_action_pinning.py b/tests/test_action_pinning.py new file mode 100644 index 0000000..64be5a4 --- /dev/null +++ b/tests/test_action_pinning.py @@ -0,0 +1,39 @@ +"""Every action that action.yml uses must be pinned to a full commit SHA. + +Repositories can require actions to be pinned to a full-length commit SHA. GitHub enforces that +on the actions a composite action uses as well, so a single tag reference in action.yml makes +the whole action fail at startup for every adopter with that policy enabled, however they +reference this action themselves. The failure is a bare `startup_failure`: no job, no log, no +message naming the offending reference. +""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +ACTION = (ROOT / "action.yml").read_text(encoding="utf-8") + +USES = re.compile(r"^\s*(?:-\s+)?uses:\s*(?P[^\s#]+)", re.MULTILINE) +LOCAL = re.compile(r"^\./") +PINNED = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_./-]+)?@[0-9a-f]{40}$") + + +class ActionPinningTests(unittest.TestCase): + def test_action_uses_at_least_one_action(self) -> None: + # Guards the pattern itself: were it to match nothing, the next test would pass vacuously. + self.assertTrue(USES.search(ACTION)) + + def test_every_used_action_is_pinned_to_a_full_commit_sha(self) -> None: + unpinned = [ + match.group("ref") + for match in USES.finditer(ACTION) + if not LOCAL.match(match.group("ref")) and not PINNED.match(match.group("ref")) + ] + self.assertEqual([], unpinned, "pin these to a full 40-character commit SHA, with the version as a comment") + + +if __name__ == "__main__": + unittest.main()