Skip to content

Commit 4c61420

Browse files
chouetzclaude
andcommitted
feat(renovate): add bazel run //tasks:check_renovate_bazel_coverage target
Adds a py_binary so the coverage check can be invoked via Bazel without needing dda/invoke. BUILD_WORKSPACE_DIRECTORY (set by `bazel run`) is used to locate the repo root; direct `python tasks/renovate.py` invocation falls back to the file's parent path. Updates the validate-renovate-deps.yml workflow to use the new Bazel target via the existing bazel-cache action, removing the dda dependency from that workflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c55d8f5 commit 4c61420

3 files changed

Lines changed: 86 additions & 49 deletions

File tree

.github/workflows/validate-renovate-deps.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- "deps/**"
77
- "renovate.json"
88
- "tasks/renovate.py"
9+
- "tasks/BUILD.bazel"
910
- ".github/workflows/validate-renovate-deps.yml"
1011

1112
permissions: {}
@@ -15,9 +16,6 @@ jobs:
1516
runs-on: ubuntu-latest
1617
steps:
1718
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
18-
- name: Install dda
19-
uses: ./.github/actions/install-dda
20-
with:
21-
features: legacy-tasks
22-
- name: Verify every http_archive in deps/ has Renovate coverage
23-
run: dda inv -- renovate.check-bazel-coverage
19+
- uses: ./.github/actions/bazel-cache
20+
- name: Verify every http_archive/http_file in deps/ has Renovate coverage
21+
run: bazel run //tasks:check_renovate_bazel_coverage

tasks/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
load("@rules_python//python:py_binary.bzl", "py_binary")
12
load("@rules_python//python:py_library.bzl", "py_library")
23

34
package(default_visibility = ["//visibility:private"])
@@ -13,3 +14,13 @@ py_library(
1314
)
1415

1516
exports_files(["core_checks.py"])
17+
18+
# No `data = [...]` for renovate.json / deps/ / .renovate-untracked.json on
19+
# purpose: the script resolves them via $BUILD_WORKSPACE_DIRECTORY (set by
20+
# `bazel run`) so it reads the live workspace, not the bazel sandbox. Adding
21+
# them as data deps would silently shadow the real files.
22+
py_binary(
23+
name = "check_renovate_bazel_coverage",
24+
srcs = ["renovate.py"],
25+
deps = ["@py_dev_requirements//invoke"],
26+
)

tasks/renovate.py

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,69 @@
2020
from invoke.exceptions import Exit
2121

2222
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+
2686

2787
def _extract_call_names(text: str, call_name: str) -> set[str]:
2888
"""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]:
50110
i += 1
51111
i += 1
52112
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)
54115
if m:
55116
names.add(m.group(1))
56117
start = i
57118
return names
58119

59120

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-
69121
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.
70126
raw = path.read_text()
71-
# renovate.json is JSON5 (trailing commas allowed); strip them so json.loads accepts.
72127
stripped = re.sub(r",(\s*[}\]])", r"\1", raw)
73128
data = json.loads(stripped)
74129
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:
114169
return "\n".join(lines)
115170

116171

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

Comments
 (0)