Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
24 changes: 12 additions & 12 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.12'

Expand Down Expand Up @@ -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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
Expand Down Expand Up @@ -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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
Expand All @@ -384,7 +384,7 @@ runs:

- name: Checkout analysis target
if: steps.guard.outputs.skip != 'true'
uses: actions/checkout@v4
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
repository: ${{ steps.guard.outputs.checkout_repo }}
ref: ${{ steps.guard.outputs.checkout_ref }}
Expand All @@ -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'
Expand Down Expand Up @@ -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@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
Expand All @@ -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@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
Expand Down Expand Up @@ -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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ steps.state.outputs.warmstart_name }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/warmstart
Expand All @@ -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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ steps.state.outputs.base_name }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
Expand Down Expand Up @@ -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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: codeboarding-review-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.review_artifact.outputs.artifact_dir }}
Expand Down Expand Up @@ -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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
Expand All @@ -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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
Expand Down
2 changes: 1 addition & 1 deletion docs/COMMIT_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions tests/test_action_pinning.py
Original file line number Diff line number Diff line change
@@ -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<ref>[^\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()
Loading