From be9402169b0bcffe2c415f91805f90dbbfcc1373 Mon Sep 17 00:00:00 2001 From: Hayden <154503486+groupthinking@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:47:34 -0500 Subject: [PATCH 1/3] ci: lock dependency-review action graph from verified pilot --- .github/workflows/actions.lock | 25 +++++++++++++++++++++++++ .github/workflows/dependency-review.yml | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/actions.lock diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock new file mode 100644 index 000000000..7f40ca00c --- /dev/null +++ b/.github/workflows/actions.lock @@ -0,0 +1,25 @@ +# This file is machine-generated by `gh actions-lock`. +# Do not edit by hand; run `gh actions-lock` to update. +# Docs: https://gh.io/actions-lockfile +version: 'v0.0.3' +workflows: + '.github/workflows/dependency-review.yml': + - 'actions/checkout@v7.0.1' + - 'actions/dependency-review-action@v5.0.0' + - 'advanced-security/component-detection-dependency-submission-action@v0.1.6' +dependencies: + 'actions/checkout@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 + 'actions/dependency-review-action@v5.0.0': + ref: 'v5.0.0' + commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294' + owner_id: 44036562 + repo_id: 476372928 + 'advanced-security/component-detection-dependency-submission-action@v0.1.6': + ref: 'v0.1.6' + commit: 'sha1-b282c67b1008fde9b60da4bdfcd8849613e293b2' + owner_id: 71494179 + repo_id: 604441937 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b7caff932..569e0f3d6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -35,7 +35,7 @@ jobs: timeout-minutes: 10 # Prevent long-running jobs steps: - name: Checkout code - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} # PR runs compare the exact base and head trees before asking the @@ -120,7 +120,7 @@ jobs: ${{ github.event_name == 'push' || (github.event.pull_request.head.repo.full_name == github.repository && steps.dependency-changes.outputs.changed == 'true') }} - uses: advanced-security/component-detection-dependency-submission-action@b282c67b1008fde9b60da4bdfcd8849613e293b2 # v0.1.6 + uses: advanced-security/component-detection-dependency-submission-action@v0.1.6 with: snapshot-sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} snapshot-ref: ${{ github.event_name == 'pull_request' && format('refs/heads/{0}', github.event.pull_request.head.ref) || github.ref }} @@ -129,7 +129,7 @@ jobs: if: >- ${{ github.event_name == 'pull_request' && steps.dependency-changes.outputs.changed == 'true' }} - uses: actions/dependency-review-action@v5 + uses: actions/dependency-review-action@v5.0.0 with: fail-on-severity: moderate # The dependency graph API can briefly report "no snapshots found for the From 5cd469aac4b1f7ad031d9afe54e4292dc18170f5 Mon Sep 17 00:00:00 2001 From: Hayden <154503486+groupthinking@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:08:56 -0500 Subject: [PATCH 2/3] fix(ci): review action lockfile-only dependency updates Recognize the exact Actions lockfile path in the dependency-input detector. Exercise the workflow detector against real single-file Git diffs, including unrelated actions.lock names and the separate gh-aw lock format. Refs #2448; addresses review discussion_r4354819144. --- .github/workflows/dependency-review.yml | 1 + tests/unit/test_dependency_review_workflow.py | 83 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 569e0f3d6..1e3b418eb 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -95,6 +95,7 @@ jobs: or path.as_posix() in { ".github/dependabot.yaml", ".github/dependabot.yml", + ".github/workflows/actions.lock", ".github/workflows/dependency-review.yml", "vendor/modules.txt", } diff --git a/tests/unit/test_dependency_review_workflow.py b/tests/unit/test_dependency_review_workflow.py index 58b8f9cfb..1f349a76e 100644 --- a/tests/unit/test_dependency_review_workflow.py +++ b/tests/unit/test_dependency_review_workflow.py @@ -1,10 +1,16 @@ from __future__ import annotations +import os +import subprocess +import sys from pathlib import Path +import pytest import yaml -WORKFLOW_PATH = Path(__file__).resolve().parents[2] / ".github/workflows/dependency-review.yml" +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[2] / ".github/workflows/dependency-review.yml" +) def _load_workflow() -> dict: @@ -13,6 +19,77 @@ def _load_workflow() -> dict: return workflow if "on" in workflow else {**workflow, "on": workflow[True]} +@pytest.mark.parametrize( + ("changed_path", "expected_changed"), + [ + (".github/workflows/actions.lock", True), + ("docs/actions.lock", False), + (".github/aw/actions-lock.json", False), + ], +) +def test_dependency_detector_handles_lockfile_only_changes( + tmp_path: Path, changed_path: str, expected_changed: bool +) -> None: + """Run the workflow's detector against an actual one-file Git diff.""" + repo = tmp_path / "repo" + repo.mkdir() + + def git(*args: str) -> str: + return subprocess.run( + [ + "git", + "-c", + "user.name=Workflow test", + "-c", + "user.email=workflow-test@example.invalid", + *args, + ], + cwd=repo, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + git("init", "--quiet") + changed_file = repo / changed_path + changed_file.parent.mkdir(parents=True, exist_ok=True) + changed_file.write_text("before\n") + git("add", ".") + git("commit", "--quiet", "-m", "baseline") + base = git("rev-parse", "HEAD") + changed_file.write_text("after\n") + git("add", ".") + git("commit", "--quiet", "-m", "update one lockfile") + head = git("rev-parse", "HEAD") + + steps = _load_workflow()["jobs"]["dependency-review"]["steps"] + detection = next(step for step in steps if step.get("id") == "dependency-changes") + script = detection["run"].split("python - <<'PY'\n", 1)[1].rsplit("\nPY", 1)[0] + output = tmp_path / "output" + summary = tmp_path / "summary" + subprocess.run( + [sys.executable, "-c", script], + cwd=repo, + env={ + **os.environ, + "BASE_SHA": base, + "HEAD_SHA": head, + "GITHUB_OUTPUT": str(output), + "GITHUB_STEP_SUMMARY": str(summary), + }, + check=True, + capture_output=True, + text=True, + ) + assert output.read_text() == ( + f"changed={str(expected_changed).lower()}\ncount={int(expected_changed)}\n" + ) + if expected_changed: + assert f"- `{changed_path}`" in summary.read_text() + else: + assert "No dependency manifests" in summary.read_text() + + def test_dependency_review_submits_pr_head_snapshot_and_retries_warnings() -> None: workflow = _load_workflow() triggers = workflow["on"] @@ -82,5 +159,7 @@ def test_dependency_review_submits_pr_head_snapshot_and_retries_warnings() -> No assert review["with"]["retry-on-snapshot-warnings"] is True assert review["with"]["retry-on-snapshot-warnings-timeout"] == 300 - no_delta = next(step for step in steps if step["name"] == "Record no dependency delta") + no_delta = next( + step for step in steps if step["name"] == "Record no dependency delta" + ) assert "steps.dependency-changes.outputs.changed == 'false'" in no_delta["if"] From 854c4050ee9189b2665051615cab374416376cfb Mon Sep 17 00:00:00 2001 From: Hayden <154503486+groupthinking@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:54:32 -0500 Subject: [PATCH 3/3] fix(ci): refresh verified lock graph after main reconciliation --- .github/workflows/actions.lock | 14 +++- .github/workflows/dependency-review.yml | 6 +- .github/workflows/workflow-lock-pilot.yml | 75 ------------------- tests/unit/test_dependency_review_workflow.py | 15 +++- 4 files changed, 26 insertions(+), 84 deletions(-) delete mode 100644 .github/workflows/workflow-lock-pilot.yml diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 7f40ca00c..1adc77b3b 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -6,7 +6,8 @@ workflows: '.github/workflows/dependency-review.yml': - 'actions/checkout@v7.0.1' - 'actions/dependency-review-action@v5.0.0' - - 'advanced-security/component-detection-dependency-submission-action@v0.1.6' + - 'actions/upload-artifact@v7.0.1' + - 'advanced-security/component-detection-dependency-submission-action@v0.2.0' dependencies: 'actions/checkout@v7.0.1': ref: 'v7.0.1' @@ -18,8 +19,13 @@ dependencies: commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294' owner_id: 44036562 repo_id: 476372928 - 'advanced-security/component-detection-dependency-submission-action@v0.1.6': - ref: 'v0.1.6' - commit: 'sha1-b282c67b1008fde9b60da4bdfcd8849613e293b2' + 'actions/upload-artifact@v7.0.1': + ref: 'v7.0.1' + commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + owner_id: 44036562 + repo_id: 192625955 + 'advanced-security/component-detection-dependency-submission-action@v0.2.0': + ref: 'v0.2.0' + commit: 'sha1-a02aeda7a175ad4dd38a13c4d3ff2df9da8f2cf0' owner_id: 71494179 repo_id: 604441937 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d0ede61d1..e529e41ec 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -129,7 +129,7 @@ jobs: # bound produced base/head drift in the #2501 exact-lock canary. env: PipReportFileLevelTimeoutSeconds: "240" - uses: advanced-security/component-detection-dependency-submission-action@a02aeda7a175ad4dd38a13c4d3ff2df9da8f2cf0 # v0.2.0 + uses: advanced-security/component-detection-dependency-submission-action@v0.2.0 with: snapshot-sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} snapshot-ref: ${{ github.event_name == 'pull_request' && format('refs/heads/{0}', github.event.pull_request.head.ref) || github.ref }} @@ -150,7 +150,7 @@ jobs: if: >- ${{ github.event_name == 'pull_request' && steps.dependency-changes.outputs.changed == 'true' }} - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + uses: actions/dependency-review-action@v5.0.0 with: base-ref: ${{ github.event.pull_request.base.sha }} head-ref: ${{ github.event.pull_request.head.sha }} @@ -235,7 +235,7 @@ jobs: if: >- ${{ always() && github.event_name == 'pull_request' && steps.dependency-changes.outputs.changed == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7.0.1 with: name: dependency-review-${{ github.event.pull_request.head.sha }}-${{ github.run_attempt }} path: dependency-review-evidence/ diff --git a/.github/workflows/workflow-lock-pilot.yml b/.github/workflows/workflow-lock-pilot.yml deleted file mode 100644 index 6ee211e8e..000000000 --- a/.github/workflows/workflow-lock-pilot.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Workflow lock pilot - -on: - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: read - -jobs: - scoped-lock-validation: - if: github.head_ref == 'test/actions-lock-dependency-review' && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - - name: Download verified official CLI - shell: bash - run: | - set -euo pipefail - curl --fail --silent --show-error --location --max-time 60 \ - https://github.com/github/gh-actions-lock/releases/download/v0.4.0/linux-amd64 \ - --output "$RUNNER_TEMP/gh-actions-lock" - echo "f9da98f2a6e9e0ae77deadf9a2319a8a86bb002b541cc3203b247cc59fb04842 $RUNNER_TEMP/gh-actions-lock" | sha256sum --check - chmod +x "$RUNNER_TEMP/gh-actions-lock" - "$RUNNER_TEMP/gh-actions-lock" --version - - name: Generate and verify only dependency-review - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - tool="$RUNNER_TEMP/gh-actions-lock" - workflow=.github/workflows/dependency-review.yml - set +e - "$tool" --verify-local "$workflow" - baseline_status=$? - set -e - test "$baseline_status" -eq 1 - "$tool" --no-interactive "$workflow" - "$tool" --verify-local "$workflow" - "$tool" --verify "$workflow" - git diff --check - cp "$workflow" "$RUNNER_TEMP/dependency-review.verified.yml" - python - <<'PY' - from pathlib import Path - p = Path('.github/workflows/dependency-review.yml') - text = p.read_text() - import re - changed, count = re.subn(r'uses: actions/checkout@[^\s]+', 'uses: actions/checkout@v0.0.0-pilot-missing', text, count=1) - assert count == 1 - p.write_text(changed) - PY - set +e - "$tool" --verify-local "$workflow" - negative_status=$? - set -e - cp "$RUNNER_TEMP/dependency-review.verified.yml" "$workflow" - test "$negative_status" -eq 1 - "$tool" --verify-local "$workflow" - python - <<'PY' - import base64, hashlib, json, subprocess - from pathlib import Path - paths = ['.github/workflows/actions.lock', '.github/workflows/dependency-review.yml'] - status = subprocess.check_output(['git', 'status', '--porcelain'], text=True) - changed = {line[3:] for line in status.splitlines()} - assert changed == set(paths), status - for path in paths: - raw = Path(path).read_bytes() - print('PILOT_FILE_JSON=' + json.dumps({'path': path, 'sha256': hashlib.sha256(raw).hexdigest(), 'base64': base64.b64encode(raw).decode()})) - print('PILOT_VERIFIED: generation, local, online, negative, restore, scoped diff') - PY diff --git a/tests/unit/test_dependency_review_workflow.py b/tests/unit/test_dependency_review_workflow.py index 52118190e..fcc4a3504 100644 --- a/tests/unit/test_dependency_review_workflow.py +++ b/tests/unit/test_dependency_review_workflow.py @@ -23,6 +23,17 @@ def _load_workflow() -> dict: return workflow if "on" in workflow else {**workflow, "on": workflow[True]} +def _locked_action(uses: str) -> str: + """Resolve the actual runner pin, not the readable tag in the YAML.""" + lock = yaml.safe_load(WORKFLOW_PATH.with_name("actions.lock").read_text()) + assert uses in lock["workflows"][".github/workflows/dependency-review.yml"] + pin = lock["dependencies"][uses] + owner_repo, ref = uses.rsplit("@", 1) + assert pin["ref"] == ref + assert pin["commit"].startswith("sha1-") + return f"{owner_repo}@{pin['commit'].removeprefix('sha1-')}" + + @pytest.mark.parametrize( ("changed_path", "expected_changed"), [ @@ -145,7 +156,7 @@ def test_dependency_review_submits_pr_head_snapshot_and_retries_warnings() -> No assert "steps.dependency-changes.outputs.changed == 'true'" in submission["if"] assert "snapshot_detector" not in submission["if"] assert not any("focused" in step["name"].lower() for step in steps) - assert submission["uses"] == ( + assert _locked_action(submission["uses"]) == ( "advanced-security/component-detection-dependency-submission-action" "@a02aeda7a175ad4dd38a13c4d3ff2df9da8f2cf0" ) @@ -385,7 +396,7 @@ def test_numpy_remains_subject_to_the_unchanged_license_policy() -> None: assert settings.get("license-check", True) is True assert settings.get("warn-only", False) is False assert "continue-on-error" not in review - assert review["uses"] == ( + assert _locked_action(review["uses"]) == ( "actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294" )