main
py 97 lines 3.02 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Check that auto-labeling rules still match real files.
4
5 Mergify silently ignores a condition that can never match, so a file-path
6 condition keeps passing review long after the file it names has moved or been
7 deleted. This walks every `files=` / `files~=` condition in `.github/mergify.yml`
8 and fails if one matches nothing in the tree.
9
10 Usage:
11 python tools/pre_commit/check_label_rules.py
12 """
13
14 import subprocess
15 import sys
16
17 import regex as re
18 import yaml
19
20 MERGIFY = ".github/mergify.yml"
21
22
23 def tracked_files() -> list[str]:
24 out = subprocess.run(
25 ["git", "ls-files"], capture_output=True, text=True, check=True
26 ).stdout
27 return [line for line in out.split("\n") if line]
28
29
30 def file_conditions(node, found: list[str]) -> list[str]:
31 """Collect every string condition nested under a rule's `conditions`."""
32 if isinstance(node, dict):
33 for value in node.values():
34 file_conditions(value, found)
35 elif isinstance(node, list):
36 for value in node:
37 file_conditions(value, found)
38 elif isinstance(node, str):
39 found.append(node)
40 return found
41
42
43 def check(files: list[str]) -> list[tuple[str, str, str]]:
44 with open(MERGIFY) as f:
45 rules = yaml.safe_load(f)["pull_request_rules"]
46
47 # removed-files is excluded: it names paths the tree no longer has.
48 attrs = ("files", "added-files", "modified-files")
49
50 dead = []
51 for rule in rules:
52 for cond in file_conditions(rule.get("conditions", []), []):
53 cond = cond.strip()
54 # Negated conditions (label-tpu-remove) are expected to match nothing.
55 if cond.startswith("-"):
56 continue
57 attr = next(
58 (
59 a
60 for a in attrs
61 if cond.startswith(f"{a}~=") or cond.startswith(f"{a}=")
62 ),
63 None,
64 )
65 if attr is None:
66 continue
67 if cond.startswith(f"{attr}~="):
68 pattern = cond[len(attr) + 2 :]
69 try:
70 regex = re.compile(pattern)
71 except re.error as exc:
72 dead.append((rule["name"], cond, f"invalid regex: {exc}"))
73 continue
74 matched = any(regex.search(f) for f in files)
75 else:
76 matched = cond[len(attr) + 1 :] in files
77 if not matched:
78 dead.append((rule["name"], cond, "matches no tracked file"))
79 return dead
80
81
82 def main() -> int:
83 dead = check(tracked_files())
84 if not dead:
85 return 0
86 print(f"{len(dead)} label condition(s) match nothing:\n", file=sys.stderr)
87 for name, cond, why in dead:
88 print(f" [{name}] {cond}\n {why}", file=sys.stderr)
89 print(
90 "\nRepoint the condition at the file's new location, or remove it.",
91 file=sys.stderr,
92 )
93 return 1
94
95
96 if __name__ == "__main__":
97 raise SystemExit(main())