Skip to content

Commit 27cf775

Browse files
committed
code review
1 parent efa1a0b commit 27cf775

3 files changed

Lines changed: 160 additions & 31 deletions

File tree

deps/.renovate-untracked.json

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_comment": "Bazel http_archive deps in deps/repos.MODULE.bazel that intentionally have no Renovate customManager. Every entry MUST include a non-empty rationale. The check is enforced by tasks/check_renovate_bazel_coverage.py and the validate-renovate-deps.yml workflow. As deps move from this allowlist to a customManager in renovate.json, remove the entry here in the same PR.",
2+
"_comment": "Bazel http_archive and http_file deps in any deps/**/*.MODULE.bazel file that intentionally have no Renovate customManager. Every entry MUST include a non-empty rationale. The check is enforced by tasks/check_renovate_bazel_coverage.py and the validate-renovate-deps.yml workflow. As deps move from this allowlist to a customManager in renovate.json, remove the entry here in the same PR.",
33
"intentionally_untracked": {
44
"xz": "Pending tracking in Phase 2a (github-releases batch).",
55
"zlib": "Pending tracking in Phase 2a (github-releases batch).",
@@ -29,6 +29,31 @@
2929
"attr": "Hard: hosted on Savannah (savannah.nongnu.org); no native Renovate datasource. Needs a customDatasource. Phase 3.",
3030
"gpg-error": "Hard: hosted on gnupg.org FTP; no native Renovate datasource. Needs a customDatasource. Phase 3.",
3131
"gcrypt": "Hard: hosted on gnupg.org FTP; no native Renovate datasource. Needs a customDatasource. Phase 3.",
32-
"unixodbc": "Hard: hosted on unixodbc.org; no native Renovate datasource. Needs a customDatasource. Phase 3."
32+
"unixodbc": "Hard: hosted on unixodbc.org; no native Renovate datasource. Needs a customDatasource. Phase 3.",
33+
"acl": "Pending tracking. Hosted on Savannah (download.savannah.nongnu.org); needs a customDatasource. Phase 3.",
34+
"curl": "Pending tracking in Phase 2a (github-releases batch). Tarball is on curl.haxx.se but releases are tagged on github.com/curl/curl.",
35+
"cpython": "Pending tracking in Phase 2a (github-releases batch). Released on python.org; corresponding GitHub tags exist at github.com/python/cpython.",
36+
"freetds": "Pending tracking in Phase 2a (github-releases batch). Tarballs on freetds.org; releases tagged at github.com/FreeTDS/freetds.",
37+
"libpcap": "Pending tracking in Phase 2a (github-releases batch). Tarball on tcpdump.org; releases tagged at github.com/the-tcpdump-group/libpcap.",
38+
"nfsiostat": "Pending tracking in Phase 2c. Hosted on kernel.org mirrors (no GitHub mirror with reliable tags). Needs a customDatasource.",
39+
"openscap": "Pending tracking in Phase 2a (github-releases batch). Released at github.com/OpenSCAP/openscap.",
40+
"setuptools": "Pending tracking in Phase 2a (github-releases batch). Released at github.com/pypa/setuptools.",
41+
"compile_policy_x86_64": "Pending tracking in Phase 2a. DataDog/dd-policy-engine GitHub release; will be tracked as one bundle with the arm64 and windows variants since they share VERSION.",
42+
"compile_policy_arm64": "Pending tracking in Phase 2a. Shares VERSION with compile_policy_x86_64; tracked together.",
43+
"compile_policy_windows_x86_64": "Pending tracking in Phase 2a. Shares VERSION with compile_policy_x86_64; tracked together.",
44+
"gstatus_binary": "Pending tracking in Phase 2a (github-releases batch). Released at github.com/gluster/gstatus.",
45+
"msodbcsql18_deb_amd64": "Pending tracking. Microsoft .deb package; no native datasource for packages.microsoft.com. Both arches share VERSION and will be tracked as one bundle when a customDatasource is added. Phase 3.",
46+
"msodbcsql18_deb_arm64": "Pending tracking. Shares VERSION with msodbcsql18_deb_amd64; tracked together. Phase 3.",
47+
"snmp_traps": "Intentionally untracked. Datadog-internal SNMP traps DB published to dd-agent-omnibus S3; no public upstream to watch. Bumped manually when the traps DB is regenerated.",
48+
"cacert_pem": "Intentionally untracked. Bumped via a separate Datadog Synthetics-driven workflow (see deps/cacerts/cacerts.MODULE.bazel) that alerts team-agent-build on header changes; auto-PRs from Renovate would race that flow.",
49+
"cacert_license": "Intentionally untracked. Mozilla MPL-2.0 license text accompanying cacert_pem; updated together with cacert_pem via the same out-of-band flow.",
50+
"sqlite3_license": "Intentionally untracked here — version is pinned via the shared sqlite_ver tuple in deps/repos.MODULE.bazel and will be tracked together with sqlite3 once that refactor lands (see sqlite3 entry).",
51+
"sqlite_win": "Intentionally untracked here. Windows-only SQLite amalgamation; pinned to the same release as sqlite3 (manually kept in sync, see TODO in deps/cpython/cpython.MODULE.bazel). Will be folded in with sqlite3 in Phase 3.",
52+
"bzip2_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_src_deps dict alongside the base bzip2. Will be tracked together with bzip2 (Phase 3 customDatasource).",
53+
"mpdecimal_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_src_deps dict. No upstream `mpdecimal` dep exists today — needs its own customManager.",
54+
"xz_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_src_deps dict alongside the base xz. Will be tracked together with xz (Phase 2a).",
55+
"zlib_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_src_deps dict alongside the base zlib. Will be tracked together with zlib (Phase 2a).",
56+
"libffi_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_bin_deps dict. Note: pinned to 3.4.4 (cpython-bin-deps), independent of the base libffi version — must be tracked separately.",
57+
"tcltk_win": "Pending tracking. Loop-emitted Windows variant in deps/cpython/cpython.MODULE.bazel; version pinned via the python_bin_deps dict. No upstream `tcltk` dep — needs its own customManager (cpython-bin-deps tags)."
3358
}
3459
}

tasks/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ exports_files(["core_checks.py"])
2222
py_binary(
2323
name = "check_renovate_bazel_coverage",
2424
srcs = ["renovate.py"],
25+
main = "renovate.py",
2526
deps = ["@py_dev_requirements//invoke"],
2627
)

tasks/renovate.py

Lines changed: 132 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,18 @@ def main():
4343
@task
4444
def check_bazel_coverage(_: Context, root: str | None = None) -> None:
4545
"""
46-
Fail if any http_archive or http_file in deps/ lacks a Renovate customManager.
46+
Fail if any http_archive or http_file in deps/ lacks a Renovate customManager,
47+
or if the allowlist contains stale entries that no longer match any dep.
4748
4849
Scans every ``*.MODULE.bazel`` file under ``deps/`` for ``http_archive``
4950
and ``http_file`` calls. A dep is considered covered when either:
5051
* its name appears as ``depNameTemplate`` in one of ``renovate.json``'s customManagers, or
5152
* it is listed in ``deps/.renovate-untracked.json`` with a non-empty rationale.
5253
54+
Also fails when ``deps/.renovate-untracked.json`` lists a dep that no longer
55+
exists in ``deps/**/*.MODULE.bazel`` — without this, renamed or removed deps
56+
leave dead entries behind and the file rots.
57+
5358
Writes a markdown report to ``$GITHUB_STEP_SUMMARY`` when running in GitHub Actions.
5459
"""
5560
root_path = Path(root) if root is not None else Path(REPO_ROOT)
@@ -58,8 +63,10 @@ def check_bazel_coverage(_: Context, root: str | None = None) -> None:
5863
allowlist = _parse_allowlist(root_path / "deps" / ".renovate-untracked.json")
5964

6065
untracked = dep_names - tracked_names - set(allowlist)
61-
if untracked:
62-
report = _emit_failure_report(untracked, allowlist)
66+
stale = set(allowlist) - dep_names
67+
68+
if untracked or stale:
69+
report = _emit_failure_report(untracked, stale, allowlist)
6370
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
6471
if summary_path:
6572
# GITHUB_STEP_SUMMARY is a shared file for the whole step; append
@@ -80,12 +87,18 @@ def _parse_deps_dir(deps_dir: Path) -> set[str]:
8087
for path in deps_dir.rglob("*.MODULE.bazel"):
8188
text = path.read_text()
8289
for call in ("http_archive", "http_file"):
83-
names |= _extract_call_names(text, call)
90+
names |= _extract_call_names(text, call, path)
8491
return names
8592

8693

87-
def _extract_call_names(text: str, call_name: str) -> set[str]:
88-
"""Extract name = "..." from all call_name(...) blocks, regardless of arg order or comments."""
94+
def _extract_call_names(text: str, call_name: str, path: Path) -> set[str]:
95+
"""Extract name = "..." from all call_name(...) blocks, regardless of arg order or comments.
96+
97+
When the name is a Starlark template (e.g. `name = "{}_win".format(name)`),
98+
resolve it against the enclosing list comprehension and the dict literal it
99+
iterates. Fails loudly on any template we can't resolve — silent skips
100+
would let loop-emitted deps escape the coverage check.
101+
"""
89102
names: set[str] = set()
90103
name_re = re.compile(r'\bname\s*=\s*"([^"]+)"')
91104
marker = call_name + "("
@@ -113,11 +126,84 @@ def _extract_call_names(text: str, call_name: str) -> set[str]:
113126
uncommented = "\n".join(line for line in block.splitlines() if not line.lstrip().startswith("#"))
114127
m = name_re.search(uncommented)
115128
if m:
116-
names.add(m.group(1))
129+
literal = m.group(1)
130+
if "{" in literal:
131+
names |= _resolve_templated_name(text, pos, literal, path)
132+
else:
133+
names.add(literal)
117134
start = i
118135
return names
119136

120137

138+
_DICT_LITERAL_RE = re.compile(
139+
r'^(\w+)\s*=\s*\{(.*?)\n\}',
140+
re.DOTALL | re.MULTILINE,
141+
)
142+
_DICT_KEY_RE = re.compile(r'^\s*"([^"]+)"\s*:', re.MULTILINE)
143+
# `for <iter_var>, ... in <dict_var>.items()` — first target name, optional
144+
# tuple-destructuring tail (which may contain parentheses for `(a, b)`), then
145+
# the source dict. We anchor on the closing ` in ` token so the destructuring
146+
# tail can be anything.
147+
_COMPREHENSION_RE = re.compile(
148+
r'\bfor\s+(\w+)[^\n]*?\s+in\s+(\w+)\.items\(\)',
149+
)
150+
151+
152+
def _resolve_templated_name(text: str, call_pos: int, template: str, path: Path) -> set[str]:
153+
"""Resolve a templated name like "{}_win" by finding the enclosing
154+
comprehension and the dict it iterates.
155+
156+
Supported pattern (the only one in-tree today):
157+
158+
SOME_DICT = {"keyA": (...), "keyB": (...)}
159+
[
160+
http_archive(
161+
name = "{}_suffix".format(VAR),
162+
...
163+
)
164+
for VAR, (...) in SOME_DICT.items()
165+
]
166+
167+
Raises Exit if the template can't be resolved — a silent skip would defeat
168+
the coverage guarantee.
169+
"""
170+
after = text[call_pos:]
171+
comp = _COMPREHENSION_RE.search(after)
172+
if not comp:
173+
raise Exit(
174+
f"{path}: templated name {template!r} has no enclosing "
175+
"`for VAR, ... in DICT.items()` comprehension. Refactor to literal "
176+
"names, or extend tasks/renovate.py to handle this pattern."
177+
)
178+
iter_var, dict_var = comp.group(1), comp.group(2)
179+
keys = _lookup_dict_keys(text, dict_var)
180+
if keys is None:
181+
raise Exit(
182+
f"{path}: templated name {template!r} iterates {dict_var}.items() "
183+
f"but no top-level `{dict_var} = {{...}}` literal was found. "
184+
"Refactor to literal names, or extend tasks/renovate.py."
185+
)
186+
# Substitute each key for the loop variable. Both `"{}".format(VAR)` and
187+
# `"{name}".format(name = VAR)` reduce to a single `{...}` placeholder
188+
# that we replace with the literal key.
189+
if "{}" in template:
190+
return {template.replace("{}", k) for k in keys}
191+
placeholder = "{" + iter_var + "}"
192+
if placeholder in template:
193+
return {template.replace(placeholder, k) for k in keys}
194+
raise Exit(
195+
f"{path}: templated name {template!r} doesn't reference loop var "
196+
f"{iter_var!r} via `{{}}` or `{{{iter_var}}}`. Refactor or extend the parser."
197+
)
198+
199+
200+
def _lookup_dict_keys(text: str, dict_var: str) -> list[str] | None:
201+
for m in _DICT_LITERAL_RE.finditer(text):
202+
if m.group(1) == dict_var:
203+
return _DICT_KEY_RE.findall(m.group(2))
204+
return None
205+
206+
121207
def _parse_renovate_json(path: Path) -> set[str]:
122208
# Assumption: renovate.json is plain JSON plus trailing commas only — no
123209
# // or /* */ comments, no single-quoted strings. Renovate accepts the full
@@ -143,29 +229,46 @@ def _parse_allowlist(path: Path) -> dict[str, str]:
143229
return entries
144230

145231

146-
def _emit_failure_report(untracked: set[str], allowlist: dict[str, str]) -> str:
147-
lines = [
148-
"## ❌ Renovate coverage check failed",
149-
"",
150-
"The following native deps (http_archive / http_file) in `deps/` have no "
151-
"matching `customManager` in `renovate.json`:",
152-
"",
153-
"| dep | suggested fix |",
154-
"|---|---|",
155-
]
156-
for dep in sorted(untracked):
157-
lines.append(
158-
f"| `{dep}` | Add a `customManagers` entry with "
159-
f'`depNameTemplate: "{dep}"`, or add to '
160-
"`deps/.renovate-untracked.json` with a rationale. |"
161-
)
162-
lines += [
163-
"",
164-
"See `renovate.json` for existing patterns (linux-images, windows-images, ...).",
165-
"",
232+
def _emit_failure_report(untracked: set[str], stale: set[str], allowlist: dict[str, str]) -> str:
233+
lines = ["## ❌ Renovate coverage check failed", ""]
234+
if untracked:
235+
lines += [
236+
"The following native deps (http_archive / http_file) in `deps/` have no "
237+
"matching `customManager` in `renovate.json`:",
238+
"",
239+
"| dep | suggested fix |",
240+
"|---|---|",
241+
]
242+
for dep in sorted(untracked):
243+
lines.append(
244+
f"| `{dep}` | Add a `customManagers` entry with "
245+
f'`depNameTemplate: "{dep}"`, or add to '
246+
"`deps/.renovate-untracked.json` with a rationale. |"
247+
)
248+
lines += [
249+
"",
250+
"See `renovate.json` for existing patterns (linux-images, windows-images, ...).",
251+
"",
252+
]
253+
if stale:
254+
lines += [
255+
"The following entries in `deps/.renovate-untracked.json` no longer match "
256+
"any `http_archive` / `http_file` name in `deps/**/*.MODULE.bazel` and "
257+
"must be removed:",
258+
"",
259+
]
260+
for dep in sorted(stale):
261+
lines.append(f"- `{dep}`")
262+
lines += [
263+
"",
264+
"If the dep was renamed, replace the old entry with the new name. If it "
265+
"was removed, delete the entry. Stale allowlist entries silently weaken the check.",
266+
"",
267+
]
268+
lines.append(
166269
f"Currently allowlisted ({len(allowlist)}): "
167-
+ (", ".join(f"`{k}`" for k in sorted(allowlist)) if allowlist else "_none_"),
168-
]
270+
+ (", ".join(f"`{k}`" for k in sorted(allowlist)) if allowlist else "_none_")
271+
)
169272
return "\n".join(lines)
170273

171274

0 commit comments

Comments
 (0)