diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 18c5586..2056c0f 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -26,7 +26,7 @@ jobs: permissions: contents: read issues: write - uses: hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@241d646f6b83d17aeee2bb97a8c1e1e788eb6099 + uses: hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v1 with: audit-kind: ${{ inputs.audit-kind || 'auto' }} @@ -35,7 +35,7 @@ jobs: permissions: contents: write pull-requests: write - uses: hydra-ecosystem/github-actions/.github/workflows/node-publish.yml@241d646f6b83d17aeee2bb97a8c1e1e788eb6099 + uses: hydra-ecosystem/github-actions/.github/workflows/node-publish.yml@v1 with: validation-passed: ${{ needs.audit.outputs.validation_passed }} app-client-id: ${{ vars.NODE_AUDIT_APP_CLIENT_ID }} diff --git a/README.md b/README.md index 6100981..203c342 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,17 @@ For focused local runs without `nox`, use the same environment directly, for exa - `.venv/bin/python -m pytest server/tests/unit/test_config.py` - `.venv/bin/python -m pytest server/tests/unit/test_app.py` +### GitHub Actions references + +External actions use full commit SHAs with version comments. Reusable workflows +also use full SHAs, with one explicit exception: the two Node maintenance calls +in `.github/workflows/dependency-audit.yml` follow +`hydra-ecosystem/github-actions@v1` for automatic compatible fixes. This trusts +the release tag maintained by that repository. A different source, workflow, +caller, branch, or major release requires a separate policy update. Full SHA +references remain valid for maintenance too. See the shared +[release policy](https://github.com/hydra-ecosystem/github-actions/blob/main/docs/workflow-releases.md). + The Docusaurus website lives in [website/](website/): - `cd website && npm install` diff --git a/server/tests/unit/test_release_workflow_supply_chain.py b/server/tests/unit/test_release_workflow_supply_chain.py index f50bccf..c80b21a 100644 --- a/server/tests/unit/test_release_workflow_supply_chain.py +++ b/server/tests/unit/test_release_workflow_supply_chain.py @@ -3,13 +3,29 @@ import re from pathlib import Path +import pytest import yaml ROOT = Path(__file__).resolve().parents[3] -def test_external_workflow_actions_have_immutable_pins_and_version_comments() -> None: +def _approved_reusable_workflow(reference: str, filename: str) -> bool: + if filename == "dependency-audit.yml" and reference in { + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v1", + "hydra-ecosystem/github-actions/.github/workflows/node-publish.yml@v1", + }: + return True + return ( + re.fullmatch( + r"[\w-]+/[\w.-]+/\.github/workflows/[\w.-]+\.ya?ml@[0-9a-f]{40}", + reference, + ) + is not None + ) + + +def test_external_workflow_actions_follow_reference_policy() -> None: for path in (ROOT / ".github" / "workflows").glob("*.yml"): for line in path.read_text().splitlines(): if "uses:" not in line: @@ -18,10 +34,7 @@ def test_external_workflow_actions_have_immutable_pins_and_version_comments() -> if reference.startswith("./"): continue if "/.github/workflows/" in reference: - assert re.fullmatch( - r"[\w-]+/[\w.-]+/\.github/workflows/[\w.-]+\.ya?ml@[0-9a-f]{40}", - reference, - ), (path, line) + assert _approved_reusable_workflow(reference, path.name), (path, line) else: assert re.fullmatch( r"[\w-]+/[\w./-]+@[0-9a-f]{40} # v\d+\.\d+\.\d+", reference @@ -56,3 +69,65 @@ def test_actions_dependency_updates_are_configured() -> None: and update["schedule"]["interval"] == "monthly" for update in config["updates"] ) + + +@pytest.mark.parametrize( + "filename,reference,allowed", + [ + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v1", + True, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-publish.yml@v1", + True, + ), + ( + "publish.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v1", + False, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v2", + False, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@main", + False, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@v1.2.3", + False, + ), + ( + "dependency-audit.yml", + "other/actions/.github/workflows/node-audit.yml@v1", + False, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/other.yml@v1", + False, + ), + ( + "publish.yml", + "other/actions/.github/workflows/publish.yml@" + "a" * 40, + True, + ), + ( + "dependency-audit.yml", + "hydra-ecosystem/github-actions/.github/workflows/node-audit.yml@" + + "a" * 40, + True, + ), + ], +) +def test_reusable_workflow_reference_policy_boundary( + filename: str, reference: str, allowed: bool +) -> None: + assert _approved_reusable_workflow(reference, filename) is allowed