From bff144d30251efb0a66b4380257eae4ffdce8abe Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Sun, 25 Oct 2026 14:47:21 +0100 Subject: [PATCH 1/2] 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() From cc6b35181ef1ee0bc6b7aad2ca302c82969caebd Mon Sep 17 00:00:00 2001 From: Svilen Stefanov Date: Sun, 25 Oct 2026 16:31:31 +0100 Subject: [PATCH 2/2] fix: move nested actions off Node 20 Four of the actions that action.yml uses still run on Node 20, which GitHub is retiring on its runners: checkout v4, setup-python v5, upload-artifact v4 and sticky-pull-request-comment v2. setup-java v5 already runs on Node 24. Bump each to the oldest major that runs on Node 24, keeping the change to the runtime move and nothing else: - actions/checkout v4.4.0 -> v5.1.0 - actions/setup-python v5.6.0 -> v6.3.0 - actions/upload-artifact v4.6.2 -> v6.0.0 (v5 still defaults to Node 20) - marocchino/sticky-pull-request-comment v2.9.4 -> v3.0.5 Every input and output the action uses exists in the new versions with the same defaults. Later majors were skipped on purpose: checkout v6 moves persisted credentials to a separate file, and checkout v7 refuses fork checkouts under pull_request_target and workflow_run. These versions need Actions Runner v2.327.1 or newer, which matters for adopters on self-hosted runners. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- action.yml | 22 +++++++++++----------- docs/COMMIT_STRATEGY.md | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 42e9a75..f2a166a 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ The action requests 14-day retention for it; a repository or organisation policy Each review seeds the head analysis from this pull request's own previous run, so a run only covers the commits pushed since it. That analysis is published as a workflow artifact named for the pull request, and the next run fetches it by name. With nothing to fetch, the run seeds from the merge base's analysis instead, which `sync` publishes for every commit it processes. -Everything here is best-effort. A missing artifact, an expired one, or a token without `actions: read` all mean the run derives from the base instead — which is what every run did before any of this existed. Stored analyses are read only when the run that produced them worked on this repository's own code, so a fork cannot leave one behind for a later run to load. On GitHub Enterprise Server, where `actions/upload-artifact@v4` is unsupported, nothing is stored or reused and every review derives from the committed baseline. +Everything here is best-effort. A missing artifact, an expired one, or a token without `actions: read` all mean the run derives from the base instead — which is what every run did before any of this existed. Stored analyses are read only when the run that produced them worked on this repository's own code, so a fork cannot leave one behind for a later run to load. On GitHub Enterprise Server, where `actions/upload-artifact` v4 and later are unsupported, nothing is stored or reused and every review derives from the committed baseline. Because artifacts are readable and writable from every trigger, all paths behave the same: an automatic run, a `/codeboarding` command and a manual dispatch each reuse the previous analysis and publish their own. A stored analysis is discarded, and the head re-derived from the base, whenever the pinned CodeBoarding version, `.codeboardingignore`, the model selection, the analysis depth or the merge base changes — or when the base graph it grew from is not the one this run compares against. diff --git a/action.yml b/action.yml index ee958d8..2102d10 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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: repository: ${{ steps.guard.outputs.checkout_repo }} ref: ${{ steps.guard.outputs.checkout_ref }} @@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: ${{ steps.guard.outputs.comment_id }} number: ${{ steps.guard.outputs.pr_number }} diff --git a/docs/COMMIT_STRATEGY.md b/docs/COMMIT_STRATEGY.md index 49d5712..352cdde 100644 --- a/docs/COMMIT_STRATEGY.md +++ b/docs/COMMIT_STRATEGY.md @@ -259,7 +259,7 @@ Reuse is best effort throughout. A missing or expired artifact, or a token without `actions: read`, falls back to deriving the base directly — which is what every run did before any of this existed. -**GitHub Enterprise Server.** `actions/upload-artifact@v4` is not supported +**GitHub Enterprise Server.** `actions/upload-artifact` v4+ is not supported there, which is why the review artifact has always been restricted to github.com. The stored analyses are restricted the same way, so a GHES review derives the base from the committed baseline on every run. That is the same work