3131PR_HEAD_REF = "${{ github.event.pull_request.head.sha }}"
3232PR_ONLY_IF = "${{ github.event_name == 'pull_request' }}"
3333TASK_PREFIX_RE = re .compile (r"`([A-Za-z0-9_-]+)/<task>`" )
34- BLOCK_SCALAR_RE = re .compile (r"[:=-]\s*[|>][ +-]?\s*$" )
34+ BLOCK_SCALAR_RE = re .compile (r"[:=-]\s*[|>](?:[1-9][ +-]?|[+-][1-9]?|) \s*$" )
3535
3636
3737def _display_path (path : Path ) -> str :
3838 try :
39- return str (_display_path ( path ))
39+ return str (path . relative_to ( ROOT ))
4040 except ValueError :
4141 return str (path )
4242
@@ -71,8 +71,21 @@ def _structural_lines(path: Path) -> list[tuple[int, int, str]]:
7171 scalar_indent = indent
7272 return out
7373
74- def _push_branches (path : Path ) -> set [str ]:
74+ def _on_block_lines (path : Path ) -> list [tuple [int , int , str ]]:
75+ """Return only children of the top-level on mapping."""
7576 lines = _structural_lines (path )
77+ for i , (_ , indent , stripped ) in enumerate (lines ):
78+ if indent == 0 and stripped == "on:" :
79+ out : list [tuple [int , int , str ]] = []
80+ for item in lines [i + 1 :]:
81+ if item [1 ] == 0 :
82+ break
83+ out .append (item )
84+ return out
85+ return []
86+
87+ def _push_branches (path : Path ) -> set [str ]:
88+ lines = _on_block_lines (path )
7689 for i , (_ , indent , stripped ) in enumerate (lines ):
7790 if indent == 2 and stripped == "push:" :
7891 for _ , child_indent , child in lines [i + 1 :]:
@@ -90,16 +103,16 @@ def _push_branches(path: Path) -> set[str]:
90103 if item .strip ()
91104 }
92105 raise ValueError (f"{ path } : push trigger has no branches list" )
93- raise ValueError (f"{ path } : missing top-level push trigger" )
106+ raise ValueError (f"{ path } : missing top-level on. push trigger" )
94107
95108def _yaml_code (line : str ) -> str :
96109 """Return the structural part of a simple repository workflow line."""
97110 return line .split ("#" , 1 )[0 ].rstrip ()
98111
99112
100113def _pull_request_is_unfiltered (path : Path ) -> bool :
101- """Require default PR activity coverage with no base/path suppression."""
102- lines = _structural_lines (path )
114+ """Require top-level on.pull_request with no suppression filters ."""
115+ lines = _on_block_lines (path )
103116 forbidden = ("branches:" , "branches-ignore:" , "paths:" , "paths-ignore:" , "types:" )
104117 for i , (_ , indent , stripped ) in enumerate (lines ):
105118 if indent == 2 and stripped == "pull_request:" :
@@ -154,13 +167,16 @@ def _job_specs(path: Path) -> dict[str, dict[str, object]]:
154167 continue
155168 if indent == 2 and stripped .endswith (":" ) and not stripped .startswith ("-" ):
156169 current = stripped [:- 1 ]
157- jobs [current ] = {"if" : None , "uses" : [], "refs" : []}
170+ jobs [current ] = {"if" : None , "needs" : None , " uses" : [], "refs" : []}
158171 continue
159172 if current is None :
160173 continue
161174 if indent == 4 and stripped .startswith ("if:" ):
162175 jobs [current ]["if" ] = stripped .split (":" , 1 )[1 ].strip ()
163176 continue
177+ if indent == 4 and stripped .startswith ("needs:" ):
178+ jobs [current ]["needs" ] = stripped .split (":" , 1 )[1 ].strip ()
179+ continue
164180 if indent == 4 and stripped .startswith ("uses:" ):
165181 uses = jobs [current ]["uses" ]
166182 assert isinstance (uses , list )
@@ -202,6 +218,8 @@ def _evidence_job_errors(path: Path) -> list[str]:
202218 else :
203219 if required ["if" ] is not None :
204220 errors .append (f"{ _display_path (path )} :{ required_id } : required job must not be conditional" )
221+ if required ["needs" ] is not None :
222+ errors .append (f"{ _display_path (path )} :{ required_id } : required job must not depend on another job" )
205223 if required ["refs" ] != [None ]:
206224 errors .append (f"{ _display_path (path )} :{ required_id } : required job must have one default-ref checkout" )
207225 head = jobs .get (head_id )
@@ -210,6 +228,8 @@ def _evidence_job_errors(path: Path) -> list[str]:
210228 else :
211229 if head ["if" ] != PR_ONLY_IF :
212230 errors .append (f"{ _display_path (path )} :{ head_id } : exact-head job must be PR-only" )
231+ if head ["needs" ] is not None :
232+ errors .append (f"{ _display_path (path )} :{ head_id } : exact-head job must not depend on another job" )
213233 if head ["refs" ] != [PR_HEAD_REF ]:
214234 errors .append (f"{ _display_path (path )} :{ head_id } : exact-head checkout is not bound to PR head" )
215235 return errors
0 commit comments