Skip to content

Commit 5544bc4

Browse files
ci(#159): parse workflow triggers and checkout steps structurally
1 parent 1fdd65f commit 5544bc4

1 file changed

Lines changed: 39 additions & 74 deletions

File tree

.github/scripts/check_agent_branch_triggers.py

Lines changed: 39 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -64,33 +64,25 @@ def _structural_lines(path: Path) -> list[tuple[int, int, str]]:
6464
return out
6565

6666
def _push_branches(path: Path) -> set[str]:
67-
lines = path.read_text(encoding="utf-8").splitlines()
68-
push_index = next(
69-
(i for i, line in enumerate(lines) if line.rstrip() == " push:"),
70-
None,
71-
)
72-
if push_index is None:
73-
raise ValueError(f"{path}: missing top-level push trigger")
74-
75-
for line in lines[push_index + 1 :]:
76-
if line and not line.startswith(" "):
77-
break
78-
stripped = line.strip()
79-
if not stripped.startswith("branches:"):
80-
continue
81-
raw = stripped.split(":", 1)[1].strip()
82-
if not (raw.startswith("[") and raw.endswith("]")):
83-
raise ValueError(
84-
f"{path}: agent-trigger contract expects inline branches list, got {raw!r}"
85-
)
86-
items = []
87-
for item in raw[1:-1].split(","):
88-
value = item.strip().strip('"').strip("'")
89-
if value:
90-
items.append(value)
91-
return set(items)
92-
raise ValueError(f"{path}: push trigger has no branches list")
93-
67+
lines = _structural_lines(path)
68+
for i, (_, indent, stripped) in enumerate(lines):
69+
if indent == 2 and stripped == "push:":
70+
for _, child_indent, child in lines[i + 1 :]:
71+
if child_indent <= 2:
72+
break
73+
if child_indent == 4 and child.startswith("branches:"):
74+
raw = child.split(":", 1)[1].strip()
75+
if not (raw.startswith("[") and raw.endswith("]")):
76+
raise ValueError(
77+
f"{path}: contract expects inline push branches list"
78+
)
79+
return {
80+
item.strip().strip(chr(34)).strip(chr(39))
81+
for item in raw[1:-1].split(",")
82+
if item.strip()
83+
}
84+
raise ValueError(f"{path}: push trigger has no branches list")
85+
raise ValueError(f"{path}: missing top-level push trigger")
9486

9587
def _yaml_code(line: str) -> str:
9688
"""Return the structural part of a simple repository workflow line."""
@@ -99,73 +91,46 @@ def _yaml_code(line: str) -> str:
9991

10092
def _pull_request_is_unfiltered(path: Path) -> bool:
10193
"""Require default PR activity coverage with no base/path suppression."""
102-
lines = path.read_text(encoding="utf-8").splitlines()
103-
pr_index = next(
104-
(
105-
i
106-
for i, line in enumerate(lines)
107-
if _yaml_code(line).rstrip() == " pull_request:"
108-
),
109-
None,
110-
)
111-
if pr_index is None:
112-
return False
113-
94+
lines = _structural_lines(path)
11495
forbidden = ("branches:", "branches-ignore:", "paths:", "paths-ignore:", "types:")
115-
for line in lines[pr_index + 1 :]:
116-
code = _yaml_code(line)
117-
if not code.strip():
118-
continue
119-
indent = len(code) - len(code.lstrip(" "))
120-
if indent < 4:
121-
break
122-
stripped = code.strip()
123-
if stripped.startswith(forbidden):
124-
return False
125-
return True
126-
96+
for i, (_, indent, stripped) in enumerate(lines):
97+
if indent == 2 and stripped == "pull_request:":
98+
for _, child_indent, child in lines[i + 1 :]:
99+
if child_indent <= 2:
100+
break
101+
if child_indent >= 4 and child.startswith(forbidden):
102+
return False
103+
return True
104+
return False
127105

128106
def _checkout_ref_values(path: Path) -> list[str | None]:
129-
"""Return with.ref for every actions/checkout step, None if absent."""
130-
lines = path.read_text(encoding="utf-8").splitlines()
107+
"""Return with.ref for every structural actions/checkout step."""
108+
lines = _structural_lines(path)
131109
refs: list[str | None] = []
132-
for i, line in enumerate(lines):
133-
code = _yaml_code(line)
134-
stripped = code.strip()
110+
for i, (_, uses_indent, stripped) in enumerate(lines):
135111
short_form = stripped.startswith("- uses: actions/checkout@")
136112
named_form = stripped.startswith("uses: actions/checkout@")
137113
if not (short_form or named_form):
138114
continue
139-
uses_indent = len(code) - len(code.lstrip(" "))
140-
# ``- uses:`` is itself the list item; ``uses:`` under ``- name:`` is
141-
# a peer property. In both cases this is the indentation where ``with:``
142-
# must appear.
143115
property_indent = uses_indent + 2 if short_form else uses_indent
144116
in_with = False
145117
ref_value: str | None = None
146-
for next_line in lines[i + 1 :]:
147-
next_code = _yaml_code(next_line)
148-
if not next_code.strip():
149-
continue
150-
indent = len(next_code) - len(next_code.lstrip(" "))
151-
next_stripped = next_code.strip()
152-
if indent < property_indent:
118+
for _, next_indent, child in lines[i + 1 :]:
119+
if next_indent < property_indent:
153120
break
154-
if indent == property_indent:
155-
if next_stripped == "with:":
121+
if next_indent == property_indent:
122+
if child == "with:":
156123
in_with = True
157124
continue
158-
# Another peer property or the next list item ends ``with``.
159125
in_with = False
160-
if next_stripped.startswith("- "):
126+
if child.startswith("- "):
161127
break
162128
continue
163-
if in_with and indent > property_indent and next_stripped.startswith("ref:"):
164-
ref_value = next_stripped.split(":", 1)[1].strip().strip(chr(34)).strip(chr(39))
129+
if in_with and next_indent > property_indent and child.startswith("ref:"):
130+
ref_value = child.split(":", 1)[1].strip().strip(chr(34)).strip(chr(39))
165131
refs.append(ref_value)
166132
return refs
167133

168-
169134
def _checks_out_exact_pr_head(path: Path) -> bool:
170135
"""At least one checkout must bind to the submitted PR head SHA."""
171136
return PR_HEAD_REF in _checkout_ref_values(path)

0 commit comments

Comments
 (0)