Skip to content

Commit 2910e18

Browse files
committed
ci: reduce workflow configuration drift
1 parent 6b5a508 commit 2910e18

3 files changed

Lines changed: 87 additions & 26 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: CI
33
on:
44
push:
55
branches: [main]
6-
paths:
6+
paths: &ci_paths
77
- ".github/workflows/ci.yml"
88
- ".github/scripts/**"
99
- ".gitignore"
@@ -15,23 +15,17 @@ on:
1515
- "skills/**"
1616
pull_request:
1717
branches: [main]
18-
paths:
19-
- ".github/workflows/ci.yml"
20-
- ".github/scripts/**"
21-
- ".gitignore"
22-
- "README.md"
23-
- "CITATION.cff"
24-
- "SECURITY.md"
25-
- "references/**"
26-
- "scripts/**"
27-
- "skills/**"
18+
paths: *ci_paths
2819
schedule:
2920
- cron: "0 9 * * 1"
3021
workflow_dispatch:
3122

3223
permissions:
3324
contents: read
3425

26+
env:
27+
RUFF_VERSION: "0.12.4"
28+
3529
jobs:
3630
validate:
3731
name: Validate skill source
@@ -50,9 +44,7 @@ jobs:
5044
python-version: ${{ matrix.python-version }}
5145

5246
- name: Install validation dependencies
53-
run: |
54-
python -m pip install --upgrade pip
55-
python -m pip install ruff==0.12.4
47+
run: python -m pip install "ruff==$RUFF_VERSION"
5648

5749
- name: Check cross-agent portability
5850
run: python3 .github/scripts/check-portability.py

scripts/test-validate-ci.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import importlib.util
7+
import re
78
from pathlib import Path
89
from types import ModuleType
910

@@ -44,7 +45,41 @@ def main() -> int:
4445
assert_rejected(
4546
module,
4647
workflow.replace('- ".gitignore"', '# - ".gitignore"'),
47-
"commented .gitignore path filters",
48+
"commented .gitignore shared path filter",
49+
)
50+
assert_rejected(
51+
module,
52+
workflow.replace("paths: &ci_paths", "paths:", 1),
53+
"missing shared path anchor",
54+
)
55+
assert_rejected(
56+
module,
57+
workflow.replace("paths: *ci_paths", "paths:", 1),
58+
"pull request not reusing shared path anchor",
59+
)
60+
assert_rejected(
61+
module,
62+
re.sub(
63+
r'RUFF_VERSION: "[^"]+"',
64+
'RUFF_VERSION: "latest"',
65+
workflow,
66+
count=1,
67+
),
68+
"unpinned Ruff version variable",
69+
)
70+
assert_rejected(
71+
module,
72+
workflow.replace('"ruff==$RUFF_VERSION"', "ruff==9.9.9", 1),
73+
"literal Ruff version in install command",
74+
)
75+
assert_rejected(
76+
module,
77+
workflow.replace(
78+
f"run: {module.RUFF_INSTALL_COMMAND}",
79+
"run: python -m pip install --upgrade pip",
80+
1,
81+
),
82+
"unpinned latest pip installation",
4883
)
4984
assert_rejected(
5085
module,
@@ -61,7 +96,7 @@ def main() -> int:
6196
"live URL check in validation matrix",
6297
)
6398

64-
print("PASS: CI policy validator rejects missing gates and mutable action pins")
99+
print("PASS: CI policy validator rejects missing gates, drift, and mutable pins")
65100
return 0
66101

67102

scripts/validate-ci.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
ROOT = Path(__file__).resolve().parents[1]
1212
WORKFLOW = ROOT / ".github/workflows/ci.yml"
1313
SHA_PIN_RE = re.compile(r"^[^@\s]+@[0-9a-f]{40}$")
14+
EXACT_VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+){2}$")
15+
RUFF_INSTALL_COMMAND = 'python -m pip install "ruff==$RUFF_VERSION"'
1416
REQUIRED_VALIDATE_COMMANDS = (
1517
"python3 .github/scripts/check-portability.py",
1618
"python3 scripts/check-version-consistency.py",
@@ -32,6 +34,14 @@ def section_body(workflow: str, name: str) -> str | None:
3234
return match.group("body") if match else None
3335

3436

37+
def top_level_section_body(workflow: str, name: str) -> str | None:
38+
match = re.search(
39+
rf"(?ms)^{re.escape(name)}:\n(?P<body>.*?)(?=^[A-Za-z0-9_-]+:\n|\Z)",
40+
workflow,
41+
)
42+
return match.group("body") if match else None
43+
44+
3545
def active_workflow_lines(workflow: str) -> str:
3646
"""Remove comment-only lines before applying policy checks."""
3747
return "\n".join(
@@ -52,14 +62,32 @@ def validate_workflow(workflow: str) -> list[str]:
5262
active = active_workflow_lines(workflow)
5363
errors: list[str] = []
5464

55-
for event_name in ("push", "pull_request"):
56-
event = section_body(active, event_name)
57-
if event is None:
58-
errors.append(f"ci.yml: missing {event_name} event")
59-
elif not re.search(r'(?m)^\s+-\s+["\']?\.gitignore["\']?\s*$', event):
60-
errors.append(
61-
f"ci.yml: {event_name} workflow must trigger on .gitignore changes"
62-
)
65+
push = section_body(active, "push")
66+
pull_request = section_body(active, "pull_request")
67+
if push is None:
68+
errors.append("ci.yml: missing push event")
69+
else:
70+
if not re.search(r"(?m)^\s*paths:\s*&ci_paths\s*$", push):
71+
errors.append("ci.yml: push paths must define the shared ci_paths anchor")
72+
if not re.search(r'(?m)^\s+-\s+["\']?\.gitignore["\']?\s*$', push):
73+
errors.append("ci.yml: shared workflow paths must include .gitignore")
74+
if pull_request is None:
75+
errors.append("ci.yml: missing pull_request event")
76+
elif not re.search(r"(?m)^\s*paths:\s*\*ci_paths\s*$", pull_request):
77+
errors.append("ci.yml: pull_request paths must reuse the ci_paths anchor")
78+
79+
environment = top_level_section_body(active, "env")
80+
ruff_version = None
81+
if environment is not None:
82+
match = re.search(
83+
r'(?m)^\s*RUFF_VERSION:\s*["\']?([^"\'\s]+)["\']?\s*$', environment
84+
)
85+
if match:
86+
ruff_version = match.group(1)
87+
if ruff_version is None or not EXACT_VERSION_RE.fullmatch(ruff_version):
88+
errors.append(
89+
"ci.yml: workflow-level RUFF_VERSION must be an exact three-part version"
90+
)
6391

6492
validate = section_body(active, "validate")
6593
external = section_body(active, "verify-urls")
@@ -75,8 +103,14 @@ def validate_workflow(workflow: str) -> list[str]:
75103
errors.append(
76104
f"ci.yml: validation matrix missing run command {command!r}"
77105
)
78-
if "python -m pip install ruff==0.12.4" not in validate:
79-
errors.append("ci.yml: validation matrix must install the pinned Ruff version")
106+
if not has_run_command(validate, RUFF_INSTALL_COMMAND):
107+
errors.append(
108+
"ci.yml: validation matrix must install Ruff from RUFF_VERSION"
109+
)
110+
if re.search(r"\bpip\s+install\s+--upgrade\s+pip\b", validate):
111+
errors.append(
112+
"ci.yml: validation matrix must not install an unpinned latest pip"
113+
)
80114
if 'python-version: ["3.10", "3.14"]' not in validate:
81115
errors.append(
82116
"ci.yml: Python matrix must test the advertised 3.10 lower bound and 3.14 stable boundary"

0 commit comments

Comments
 (0)