|
20 | 20 | from invoke.exceptions import Exit |
21 | 21 |
|
22 | 22 | REPO_ROOT = Path(__file__).resolve().parent.parent |
23 | | -DEPS_DIR = REPO_ROOT / "deps" |
24 | | -RENOVATE_FILE = REPO_ROOT / "renovate.json" |
25 | | -ALLOWLIST_FILE = REPO_ROOT / "deps" / ".renovate-untracked.json" |
| 23 | + |
| 24 | + |
| 25 | +def main(): |
| 26 | + import sys |
| 27 | + |
| 28 | + # BUILD_WORKSPACE_DIRECTORY is set by `bazel run`; fall back to the location |
| 29 | + # of this file when invoked directly (e.g. python tasks/renovate.py). |
| 30 | + _workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY") |
| 31 | + _root = _workspace if _workspace else str(REPO_ROOT) |
| 32 | + # Translate invoke's Exit to sys.exit so `bazel run` / direct python |
| 33 | + # invocation produce the same stderr report + exit code as `dda inv`, |
| 34 | + # whose runner catches Exit internally. |
| 35 | + try: |
| 36 | + check_bazel_coverage(Context(), _root) |
| 37 | + except Exit as e: |
| 38 | + if e.message: |
| 39 | + print(e.message, file=sys.stderr) |
| 40 | + sys.exit(e.code) |
| 41 | + |
| 42 | + |
| 43 | +@task |
| 44 | +def check_bazel_coverage(_: Context, root: str | None = None) -> None: |
| 45 | + """ |
| 46 | + Fail if any http_archive or http_file in deps/ lacks a Renovate customManager. |
| 47 | +
|
| 48 | + Scans every ``*.MODULE.bazel`` file under ``deps/`` for ``http_archive`` |
| 49 | + and ``http_file`` calls. A dep is considered covered when either: |
| 50 | + * its name appears as ``depNameTemplate`` in one of ``renovate.json``'s customManagers, or |
| 51 | + * it is listed in ``deps/.renovate-untracked.json`` with a non-empty rationale. |
| 52 | +
|
| 53 | + Writes a markdown report to ``$GITHUB_STEP_SUMMARY`` when running in GitHub Actions. |
| 54 | + """ |
| 55 | + root_path = Path(root) if root is not None else Path(REPO_ROOT) |
| 56 | + dep_names = _parse_deps_dir(root_path / "deps") |
| 57 | + tracked_names = _parse_renovate_json(root_path / "renovate.json") |
| 58 | + allowlist = _parse_allowlist(root_path / "deps" / ".renovate-untracked.json") |
| 59 | + |
| 60 | + untracked = dep_names - tracked_names - set(allowlist) |
| 61 | + if untracked: |
| 62 | + report = _emit_failure_report(untracked, allowlist) |
| 63 | + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") |
| 64 | + if summary_path: |
| 65 | + # GITHUB_STEP_SUMMARY is a shared file for the whole step; append |
| 66 | + # rather than overwrite to play nice with any other writes. |
| 67 | + with open(summary_path, "a", encoding="utf-8") as fh: |
| 68 | + fh.write(report + "\n") |
| 69 | + raise Exit(report, code=1) |
| 70 | + |
| 71 | + print( |
| 72 | + f"OK: {len(dep_names)} native deps (http_archive + http_file), " |
| 73 | + f"{len(dep_names) - len(allowlist)} tracked by Renovate, " |
| 74 | + f"{len(allowlist)} intentionally untracked." |
| 75 | + ) |
| 76 | + |
| 77 | + |
| 78 | +def _parse_deps_dir(deps_dir: Path) -> set[str]: |
| 79 | + names: set[str] = set() |
| 80 | + for path in deps_dir.rglob("*.MODULE.bazel"): |
| 81 | + text = path.read_text() |
| 82 | + for call in ("http_archive", "http_file"): |
| 83 | + names |= _extract_call_names(text, call) |
| 84 | + return names |
| 85 | + |
26 | 86 |
|
27 | 87 | def _extract_call_names(text: str, call_name: str) -> set[str]: |
28 | 88 | """Extract name = "..." from all call_name(...) blocks, regardless of arg order or comments.""" |
@@ -50,25 +110,20 @@ def _extract_call_names(text: str, call_name: str) -> set[str]: |
50 | 110 | i += 1 |
51 | 111 | i += 1 |
52 | 112 | block = text[pos + len(marker) : i - 1] |
53 | | - m = name_re.search(block) |
| 113 | + uncommented = "\n".join(line for line in block.splitlines() if not line.lstrip().startswith("#")) |
| 114 | + m = name_re.search(uncommented) |
54 | 115 | if m: |
55 | 116 | names.add(m.group(1)) |
56 | 117 | start = i |
57 | 118 | return names |
58 | 119 |
|
59 | 120 |
|
60 | | -def _parse_deps_dir(deps_dir: Path) -> set[str]: |
61 | | - names: set[str] = set() |
62 | | - for path in deps_dir.rglob("*.MODULE.bazel"): |
63 | | - text = path.read_text() |
64 | | - for call in ("http_archive", "http_file"): |
65 | | - names |= _extract_call_names(text, call) |
66 | | - return names |
67 | | - |
68 | | - |
69 | 121 | def _parse_renovate_json(path: Path) -> set[str]: |
| 122 | + # Assumption: renovate.json is plain JSON plus trailing commas only — no |
| 123 | + # // or /* */ comments, no single-quoted strings. Renovate accepts the full |
| 124 | + # JSON5 grammar but ours stays in this subset; if that changes, swap in a |
| 125 | + # real JSON5 parser (e.g. the `json5` package) instead of extending this regex. |
70 | 126 | raw = path.read_text() |
71 | | - # renovate.json is JSON5 (trailing commas allowed); strip them so json.loads accepts. |
72 | 127 | stripped = re.sub(r",(\s*[}\]])", r"\1", raw) |
73 | 128 | data = json.loads(stripped) |
74 | 129 | return {cm["depNameTemplate"] for cm in data.get("customManagers", []) if "depNameTemplate" in cm} |
@@ -114,32 +169,5 @@ def _emit_failure_report(untracked: set[str], allowlist: dict[str, str]) -> str: |
114 | 169 | return "\n".join(lines) |
115 | 170 |
|
116 | 171 |
|
117 | | -@task |
118 | | -def check_bazel_coverage(_: Context) -> None: |
119 | | - """ |
120 | | - Fail if any http_archive or http_file in deps/ lacks a Renovate customManager. |
121 | | -
|
122 | | - Scans every ``*.MODULE.bazel`` file under ``deps/`` for ``http_archive`` |
123 | | - and ``http_file`` calls. A dep is considered covered when either: |
124 | | - * its name appears as ``depNameTemplate`` in one of ``renovate.json``'s customManagers, or |
125 | | - * it is listed in ``deps/.renovate-untracked.json`` with a non-empty rationale. |
126 | | -
|
127 | | - Writes a markdown report to ``$GITHUB_STEP_SUMMARY`` when running in GitHub Actions. |
128 | | - """ |
129 | | - dep_names = _parse_deps_dir(DEPS_DIR) |
130 | | - tracked_names = _parse_renovate_json(RENOVATE_FILE) |
131 | | - allowlist = _parse_allowlist(ALLOWLIST_FILE) |
132 | | - |
133 | | - untracked = dep_names - tracked_names - set(allowlist) |
134 | | - if untracked: |
135 | | - report = _emit_failure_report(untracked, allowlist) |
136 | | - summary_path = os.environ.get("GITHUB_STEP_SUMMARY") |
137 | | - if summary_path: |
138 | | - Path(summary_path).write_text(report + "\n", encoding="utf-8") |
139 | | - raise Exit(report, code=1) |
140 | | - |
141 | | - print( |
142 | | - f"OK: {len(dep_names)} native deps (http_archive + http_file), " |
143 | | - f"{len(dep_names) - len(allowlist)} tracked by Renovate, " |
144 | | - f"{len(allowlist)} intentionally untracked." |
145 | | - ) |
| 172 | +if __name__ == "__main__": |
| 173 | + main() |
0 commit comments