1111ROOT = Path (__file__ ).resolve ().parents [1 ]
1212WORKFLOW = ROOT / ".github/workflows/ci.yml"
1313SHA_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"'
1416REQUIRED_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+
3545def 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