Skip to content

Commit 81657ed

Browse files
committed
test: check pinned actions in the template's workflows too
The pinned-action rule was a grep inside final-check.yml that only saw this repository's own workflows, so the template could generate tag-pinned ones unnoticed. It is now a test over both directories, and the final check runs that test. CI installs a fixed pytest instead of the newest.
1 parent 6f1e309 commit 81657ed

3 files changed

Lines changed: 61 additions & 9 deletions

File tree

‎.github/workflows/final-check.yml‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
continue-on-error: true
4646
run: |
4747
status=0
48-
python -m pip install --disable-pip-version-check pytest > "$RUNNER_TEMP/install.log" 2>&1 || status=$?
48+
python -m pip install --disable-pip-version-check 'pytest==8.4.2' > "$RUNNER_TEMP/install.log" 2>&1 || status=$?
4949
tail -n 20 "$RUNNER_TEMP/install.log"
5050
exit "$status"
5151
@@ -91,20 +91,18 @@ jobs:
9191
cat "$RUNNER_TEMP/config.log"
9292
exit "$status"
9393
94-
# The repository only allows GitHub-owned actions pinned by full SHA.
94+
# The organisation only runs GitHub-owned actions pinned by full SHA. The
95+
# rule lives in tests/test_pinned_actions.py, which also covers the
96+
# workflows the satellite template generates.
9597
- name: Syntax and pinned actions
9698
id: lint
99+
if: steps.install.outcome == 'success'
97100
continue-on-error: true
98101
run: |
99102
status=0
100103
{
101104
python -m compileall -q machine tests || status=1
102-
unpinned=$(grep -nE '^\s*-?\s*uses:' .github/workflows/*.yml | grep -vE 'uses: actions/[a-z-]+@[0-9a-f]{40}( |$)' || true)
103-
if [ -n "$unpinned" ]; then
104-
echo "Not a GitHub-owned action pinned by SHA:"
105-
echo "$unpinned"
106-
status=1
107-
fi
105+
python -m pytest -q -p no:cacheprovider tests/test_pinned_actions.py || status=1
108106
} > "$RUNNER_TEMP/lint.log" 2>&1
109107
cat "$RUNNER_TEMP/lint.log"
110108
exit "$status"

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ jobs:
1818
with:
1919
python-version: "3.12"
2020
- run: |
21-
pip install pytest
21+
pip install 'pytest==8.4.2'
2222
python -m pytest -q

‎tests/test_pinned_actions.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Every action is GitHub-owned and pinned by full commit SHA.
2+
3+
The organisation only runs such actions, in this repository's workflows and
4+
in the ones the satellite template generates. A tag (`@v7`) can be moved to
5+
other code; a SHA cannot.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import re
11+
from pathlib import Path
12+
13+
import pytest
14+
15+
ROOT = Path(__file__).resolve().parent.parent
16+
WORKFLOW_DIRS = (
17+
ROOT / ".github" / "workflows",
18+
ROOT / "machine" / "satellite_template" / ".github" / "workflows",
19+
)
20+
USES = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)", re.MULTILINE)
21+
PINNED = re.compile(r"actions/[a-z0-9-]+@[0-9a-f]{40}\Z")
22+
23+
24+
def unpinned(workflow_text: str) -> list[str]:
25+
"""The `uses:` references that are not `actions/<name>@<40-hex-sha>`."""
26+
return [ref.strip("'\"") for ref in USES.findall(workflow_text)
27+
if not PINNED.match(ref.strip("'\""))]
28+
29+
30+
def workflow_files() -> list[Path]:
31+
return sorted(path for folder in WORKFLOW_DIRS
32+
for pattern in ("*.yml", "*.yaml") for path in folder.glob(pattern))
33+
34+
35+
def test_both_workflow_directories_are_scanned():
36+
folders = {path.parent for path in workflow_files()}
37+
assert folders == set(WORKFLOW_DIRS)
38+
39+
40+
@pytest.mark.parametrize("path", workflow_files(), ids=lambda p: p.relative_to(ROOT).as_posix())
41+
def test_actions_are_github_owned_and_pinned_by_sha(path: Path):
42+
text = path.read_text(encoding="utf-8")
43+
assert USES.search(text), f"{path} uses no action; is the pattern still right?"
44+
assert unpinned(text) == []
45+
46+
47+
def test_the_rule_rejects_tags_branches_and_other_owners():
48+
sha = "3d3c42e5aac5ba805825da76410c181273ba90b1"
49+
assert unpinned(f" - uses: actions/checkout@{sha} # v7.0.1\n") == []
50+
assert unpinned(f" uses: actions/deploy-pages@{sha}\n") == []
51+
assert unpinned(" - uses: actions/checkout@v7\n") == ["actions/checkout@v7"]
52+
assert unpinned(" - uses: actions/checkout@main\n") == ["actions/checkout@main"]
53+
assert unpinned(f" - uses: someone/checkout@{sha}\n") == [f"someone/checkout@{sha}"]
54+
assert unpinned(f" - uses: actions/checkout@{sha[:12]}\n") == [f"actions/checkout@{sha[:12]}"]

0 commit comments

Comments
 (0)