diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21c828c..a59cedc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,13 @@ updates: open-pull-requests-limit: 3 commit-message: prefix: deps(actions) + groups: + github-actions-minor-patch: + patterns: + - "*" + update-types: + - minor + - patch - package-ecosystem: gomod directory: /operator diff --git a/libs/tekton-dag-common/tests/test_m17_operator_ci.py b/libs/tekton-dag-common/tests/test_m17_operator_ci.py index 7ccb88f..84e1c6e 100644 --- a/libs/tekton-dag-common/tests/test_m17_operator_ci.py +++ b/libs/tekton-dag-common/tests/test_m17_operator_ci.py @@ -2,14 +2,23 @@ from pathlib import Path +try: + from .workflow_pins import assert_actions_sha_pinned +except ImportError: + from workflow_pins import assert_actions_sha_pinned + ROOT = Path(__file__).resolve().parents[3] def test_operator_workflow_runs_pinned_quality_and_domain_jobs(): workflow = (ROOT / ".github/workflows/operator.yml").read_text() - assert "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" in workflow - assert "actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e" in workflow + assert_actions_sha_pinned( + workflow, + "actions/checkout", + "actions/setup-go", + "actions/upload-artifact", + ) assert "make lint" in workflow assert "GOTOOLCHAIN: auto" in workflow assert "make test-envtest" in workflow @@ -18,7 +27,6 @@ def test_operator_workflow_runs_pinned_quality_and_domain_jobs(): assert "v0.27.0/kind-linux-amd64" in workflow assert "sha256sum -c -" in workflow assert "--skip-isolation --skip-phase2 --skip-newman" in workflow - assert "actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f" in workflow def test_operator_domain_integration_covers_m17_lifecycle_contracts(): diff --git a/libs/tekton-dag-common/tests/test_m17_static_quality.py b/libs/tekton-dag-common/tests/test_m17_static_quality.py index 416c81e..583689a 100644 --- a/libs/tekton-dag-common/tests/test_m17_static_quality.py +++ b/libs/tekton-dag-common/tests/test_m17_static_quality.py @@ -2,6 +2,11 @@ from pathlib import Path +try: + from .workflow_pins import assert_actions_sha_pinned +except ImportError: + from workflow_pins import assert_actions_sha_pinned + ROOT = Path(__file__).resolve().parents[3] @@ -25,10 +30,13 @@ def test_static_quality_workflow_covers_required_domains(): def test_quality_tool_downloads_and_actions_are_immutable(): workflow = (ROOT / ".github/workflows/static-quality.yml").read_text() - assert "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" in workflow - assert "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" in workflow - assert "actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e" in workflow - assert "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" in workflow + assert_actions_sha_pinned( + workflow, + "actions/checkout", + "actions/setup-python", + "actions/setup-go", + "actions/setup-node", + ) assert "SHELLCHECK_VERSION: v0.11.0" in workflow assert "ACTIONLINT_VERSION: 1.7.12" in workflow assert workflow.count("sha256sum -c -") == 3 diff --git a/libs/tekton-dag-common/tests/test_m17_supply_chain.py b/libs/tekton-dag-common/tests/test_m17_supply_chain.py index 0888cd4..3bb53e1 100644 --- a/libs/tekton-dag-common/tests/test_m17_supply_chain.py +++ b/libs/tekton-dag-common/tests/test_m17_supply_chain.py @@ -41,6 +41,14 @@ def test_dependabot_covers_all_supported_ecosystems_and_directories(): "/libs/baggage-servlet-filter", } + actions = next( + entry for entry in updates if entry["package-ecosystem"] == "github-actions" + ) + assert { + tuple(group.get("update-types") or []) + for group in actions["groups"].values() + } == {("minor", "patch")} + def test_trivy_scans_filesystem_and_production_images(): workflow = (ROOT / ".github/workflows/supply-chain-scan.yml").read_text() diff --git a/libs/tekton-dag-common/tests/test_workflow_pins.py b/libs/tekton-dag-common/tests/test_workflow_pins.py new file mode 100644 index 0000000..5c95cd7 --- /dev/null +++ b/libs/tekton-dag-common/tests/test_workflow_pins.py @@ -0,0 +1,44 @@ +"""SHA-pin helper coverage and Dependabot Action cadence.""" + +from pathlib import Path + +import yaml + +try: + from .workflow_pins import SHA_PINNED_ACTION, assert_actions_sha_pinned +except ImportError: + from workflow_pins import SHA_PINNED_ACTION, assert_actions_sha_pinned + +ROOT = Path(__file__).resolve().parents[3] + + +def test_assert_actions_sha_pinned_accepts_any_digest(): + workflow = ( + "uses: actions/checkout@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + "uses: actions/setup-go@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n" + ) + assert SHA_PINNED_ACTION.search(workflow) + assert_actions_sha_pinned(workflow, "actions/checkout", "actions/setup-go") + + +def test_assert_actions_sha_pinned_rejects_mutable_tags(): + try: + assert_actions_sha_pinned("uses: actions/checkout@v4\n", "actions/checkout") + except AssertionError as exc: + assert "actions/checkout" in str(exc) + else: + raise AssertionError("mutable tag should fail SHA-pin check") + + +def test_github_actions_dependabot_is_minor_patch_only(): + config = yaml.safe_load((ROOT / ".github/dependabot.yml").read_text()) + actions = next( + entry for entry in config["updates"] if entry["package-ecosystem"] == "github-actions" + ) + groups = actions["groups"] + update_types = { + name: set(group.get("update-types") or []) + for name, group in groups.items() + } + assert update_types + assert all(types == {"minor", "patch"} for types in update_types.values()) diff --git a/libs/tekton-dag-common/tests/workflow_pins.py b/libs/tekton-dag-common/tests/workflow_pins.py new file mode 100644 index 0000000..807db92 --- /dev/null +++ b/libs/tekton-dag-common/tests/workflow_pins.py @@ -0,0 +1,13 @@ +"""Helpers for GitHub Actions SHA-pin assertions.""" + +from __future__ import annotations + +import re + +SHA_PINNED_ACTION = re.compile(r"uses:\s+(?P[\w.-]+/[\w.-]+)@(?P[0-9a-f]{40})\b") + + +def assert_actions_sha_pinned(workflow: str, *actions: str) -> None: + found = {match.group("action") for match in SHA_PINNED_ACTION.finditer(workflow)} + missing = [action for action in actions if action not in found] + assert not missing, f"SHA-pinned actions missing: {missing}"