Skip to content

Commit 3d52056

Browse files
authored
fix: pin nested actions by commit SHA (#155)
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.
1 parent db4158a commit 3d52056

2 files changed

Lines changed: 51 additions & 12 deletions

File tree

‎action.yml‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ runs:
271271
# which is the part worth failing before.
272272
- name: Setup Python
273273
if: steps.guard.outputs.skip != 'true'
274-
uses: actions/setup-python@v5
274+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
275275
with:
276276
python-version: '3.12'
277277

@@ -318,7 +318,7 @@ runs:
318318
- name: Report LLM configuration failure
319319
if: steps.guard.outputs.skip != 'true' && steps.llm.outputs.error != '' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.pr_number != ''
320320
continue-on-error: true
321-
uses: marocchino/sticky-pull-request-comment@v2
321+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
322322
with:
323323
header: ${{ steps.guard.outputs.comment_id }}
324324
number: ${{ steps.guard.outputs.pr_number }}
@@ -368,7 +368,7 @@ runs:
368368
- name: Update review progress
369369
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
370370
continue-on-error: true
371-
uses: marocchino/sticky-pull-request-comment@v2
371+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
372372
with:
373373
header: ${{ steps.guard.outputs.comment_id }}
374374
number: ${{ steps.guard.outputs.pr_number }}
@@ -384,7 +384,7 @@ runs:
384384
385385
- name: Checkout analysis target
386386
if: steps.guard.outputs.skip != 'true'
387-
uses: actions/checkout@v4
387+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
388388
with:
389389
repository: ${{ steps.guard.outputs.checkout_repo }}
390390
ref: ${{ steps.guard.outputs.checkout_ref }}
@@ -395,7 +395,7 @@ runs:
395395

396396
- name: Setup Java for CodeBoarding
397397
if: steps.guard.outputs.skip != 'true'
398-
uses: actions/setup-java@v5
398+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
399399
with:
400400
distribution: temurin
401401
java-version: '21'
@@ -518,7 +518,7 @@ runs:
518518
- name: Publish baseline analysis
519519
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'
520520
continue-on-error: true
521-
uses: actions/upload-artifact@v4
521+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
522522
with:
523523
name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.baseline_sha }}
524524
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
@@ -534,7 +534,7 @@ runs:
534534
- name: Publish baseline analysis for the analyzed commit
535535
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'
536536
continue-on-error: true
537-
uses: actions/upload-artifact@v4
537+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
538538
with:
539539
name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.analyzed_sha }}
540540
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
@@ -625,7 +625,7 @@ runs:
625625
- name: Publish this analysis for the next run
626626
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.state.outputs.warmstart_name != '' && github.server_url == 'https://github.com'
627627
continue-on-error: true
628-
uses: actions/upload-artifact@v4
628+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
629629
with:
630630
name: ${{ steps.state.outputs.warmstart_name }}
631631
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/warmstart
@@ -639,7 +639,7 @@ runs:
639639
id: publish_base
640640
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'
641641
continue-on-error: true
642-
uses: actions/upload-artifact@v4
642+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
643643
with:
644644
name: ${{ steps.state.outputs.base_name }}
645645
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
@@ -686,7 +686,7 @@ runs:
686686
- name: Upload review artifact
687687
id: upload_review_artifact_dotcom
688688
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && github.server_url == 'https://github.com'
689-
uses: actions/upload-artifact@v4
689+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
690690
with:
691691
name: codeboarding-review-${{ github.run_id }}-${{ github.run_attempt }}
692692
path: ${{ steps.review_artifact.outputs.artifact_dir }}
@@ -717,7 +717,7 @@ runs:
717717
- name: Post review comment
718718
id: review_comment
719719
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
720-
uses: marocchino/sticky-pull-request-comment@v2
720+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
721721
with:
722722
header: ${{ steps.guard.outputs.comment_id }}
723723
number: ${{ steps.guard.outputs.pr_number }}
@@ -744,7 +744,7 @@ runs:
744744
- name: Post review failure
745745
if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success' && steps.llm.outputs.error == ''
746746
continue-on-error: true
747-
uses: marocchino/sticky-pull-request-comment@v2
747+
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
748748
with:
749749
header: ${{ steps.guard.outputs.comment_id }}
750750
number: ${{ steps.guard.outputs.pr_number }}

‎tests/test_action_pinning.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Every action that action.yml uses must be pinned to a full commit SHA.
2+
3+
Repositories can require actions to be pinned to a full-length commit SHA. GitHub enforces that
4+
on the actions a composite action uses as well, so a single tag reference in action.yml makes
5+
the whole action fail at startup for every adopter with that policy enabled, however they
6+
reference this action themselves. The failure is a bare `startup_failure`: no job, no log, no
7+
message naming the offending reference.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import re
13+
import unittest
14+
from pathlib import Path
15+
16+
ROOT = Path(__file__).resolve().parent.parent
17+
ACTION = (ROOT / "action.yml").read_text(encoding="utf-8")
18+
19+
USES = re.compile(r"^\s*(?:-\s+)?uses:\s*(?P<ref>[^\s#]+)", re.MULTILINE)
20+
LOCAL = re.compile(r"^\./")
21+
PINNED = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_./-]+)?@[0-9a-f]{40}$")
22+
23+
24+
class ActionPinningTests(unittest.TestCase):
25+
def test_action_uses_at_least_one_action(self) -> None:
26+
# Guards the pattern itself: were it to match nothing, the next test would pass vacuously.
27+
self.assertTrue(USES.search(ACTION))
28+
29+
def test_every_used_action_is_pinned_to_a_full_commit_sha(self) -> None:
30+
unpinned = [
31+
match.group("ref")
32+
for match in USES.finditer(ACTION)
33+
if not LOCAL.match(match.group("ref")) and not PINNED.match(match.group("ref"))
34+
]
35+
self.assertEqual([], unpinned, "pin these to a full 40-character commit SHA, with the version as a comment")
36+
37+
38+
if __name__ == "__main__":
39+
unittest.main()

0 commit comments

Comments
 (0)