diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock new file mode 100644 index 000000000..1adc77b3b --- /dev/null +++ b/.github/workflows/actions.lock @@ -0,0 +1,31 @@ +# 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' + - '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' + 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 + '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 8aed0ac94..e529e41ec 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -35,7 +35,7 @@ jobs: timeout-minutes: 30 # Allow full-tree scan, scoped lock submissions, and base/head snapshot retry 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 inspect changes introduced after the shared ancestor. @@ -90,6 +90,7 @@ jobs: def is_dependency_input(path: PurePosixPath) -> bool: # Workflow and Dependabot configuration changes are covered by # CI tests; they do not change the installed dependency graph. + # The native Actions lockfile is an executable dependency input. name = path.name return ( name in exact_names @@ -99,7 +100,7 @@ jobs: and name.endswith((".in", ".txt")) ) or name.endswith((".csproj", ".fsproj", ".gradle", ".gradle.kts")) - or path.as_posix() == "vendor/modules.txt" + or path.as_posix() in {"vendor/modules.txt", ".github/workflows/actions.lock"} ) changed = sorted(path.as_posix() for path in paths if is_dependency_input(path)) @@ -128,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 }} @@ -149,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 }} @@ -234,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/tests/unit/test_dependency_review_workflow.py b/tests/unit/test_dependency_review_workflow.py index 14af291a7..fcc4a3504 100644 --- a/tests/unit/test_dependency_review_workflow.py +++ b/tests/unit/test_dependency_review_workflow.py @@ -6,6 +6,7 @@ import os import runpy import subprocess +import sys from pathlib import Path import pytest @@ -22,6 +23,88 @@ 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"), + [ + (".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 inputs introduced by this PR" in summary.read_text() + + def test_dependency_review_submits_pr_head_snapshot_and_retries_warnings() -> None: workflow = _load_workflow() triggers = workflow["on"] @@ -73,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" ) @@ -313,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" )