Repository navigation
ci: exempt generated API reference pages from codeowner review #13551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -113,6 +113,7 @@ class ResolvedModel: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shared: list[SharedSpec] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filetype_shared: list[FiletypeShared] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| meta: dict = field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unowned: list[str] = field(default_factory=list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def label_to_team(self) -> dict[str, str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return {a.label: a.github_team for a in self.areas} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -138,6 +139,10 @@ def owned_patterns(self) -> list[str]: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # keep them unanchored in the coverage set too. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for fs in self.filetype_shared: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pats.append(fs.glob) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Explicitly unowned globs are a deliberate routing decision (review | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # exemption), not a coverage hole -- the gate must not report them as | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # catch-all-only. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pats.extend(anchor(g) for g in self.unowned) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return pats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def unmatched_paths(self, tree: Iterable[str]) -> list[str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,15 +237,20 @@ def resolve_owners(rules: list[tuple[str, list[str]]], filepath: str) -> list[st | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_codeowners(text: str) -> list[tuple[str, list[str]]]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Parse a CODEOWNERS file body into ordered ``(pattern, [owner, ...])``.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Parse a CODEOWNERS file body into ordered ``(pattern, [owner, ...])``. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| A pattern with no owners is kept as ``(pattern, [])`` -- GitHub treats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| such a line as an explicit review exemption (last-match still applies), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| so dropping it would make ``resolve_owners`` fall back to whatever owner | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| an earlier, broader rule declared. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rules: list[tuple[str, list[str]]] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for line in text.splitlines(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stripped = line.split("#", 1)[0].strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not stripped: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pattern, *owners = stripped.split() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if owners: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rules.append((pattern, owners)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rules.append((pattern, owners)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return rules | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -313,6 +323,11 @@ def compute_resolution(spec: dict, tree: Iterable[str] | None = None) -> Resolve | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ``areas`` -- ``path_globs`` are emitted verbatim (sorted). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ``shared`` -- passed through as declared. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ``unowned`` -- globs emitted LAST with no owner: an explicit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| review exemption (GitHub's ownerless-pattern semantics) for content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| whose integrity is guarded by CI rather than human review, e.g. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deterministic generator outputs. The coverage gate counts these as | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claimed, not as catch-all holes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ``advisory`` -- no longer supported. Every owner in CODEOWNERS blocks, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| so a leftover ``advisory:`` block, an ``advisory`` key on a ``shared`` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry, or one on a filetype rule, is rejected rather than silently | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -373,6 +388,32 @@ def compute_resolution(spec: dict, tree: Iterable[str] | None = None) -> Resolve | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "'advisory' key; shared entries always block -- remove it" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw_unowned = spec.get("unowned", []) or [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(raw_unowned, list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # A scalar string would iterate per-character below and silently | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # emit ownerless rules like ``/d`` ``/o`` ``/c`` ``/s``. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"areas.yaml: unowned must be a LIST of glob strings, got {raw_unowned!r}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for g in raw_unowned: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(g, str) or not g.strip(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"areas.yaml: unowned entry {g!r} must be a non-empty glob string" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Store anchored so the conflict check below compares the canonical | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # spelling emission uses -- ``docs/x/`` and ``/docs/x/`` are the same | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # emitted rule and must collide here (anchor() is idempotent, so the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # emitter and coverage gate re-anchoring is a no-op). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unowned = sorted({anchor(g) for g in raw_unowned}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shared_globs = {anchor(s["glob"]) for s in spec_shared} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conflict = shared_globs & set(unowned) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if conflict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "areas.yaml: glob(s) declared both shared and unowned " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"({sorted(conflict)}); pick one -- unowned is emitted last and " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "would silently strip the shared owners" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+391
to
+415
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Safety check that stops an exemption from silently removing reviewers can be bypassed by writing the path slightly differently The check that rejects a path listed both as shared and as review-exempt compares the two spellings literally ( Anchoring happens after the conflict check, so equivalent globs compare unequalBoth sections are anchored at use time: Relatedly, the validation at
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| areas = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ResolvedArea( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label=a["label"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -404,6 +445,7 @@ def compute_resolution(spec: dict, tree: Iterable[str] | None = None) -> Resolve | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shared=list(spec_shared), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filetype_shared=filetype_shared, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| meta=dict(spec.get("meta", {})), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unowned=unowned, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a future edit uses the natural but invalid scalar spelling
unowned: docs/generated/instead of a YAML list, this loop accepts every character as a valid string and emits ownerless rules such as/d,/o, and/srather than rejecting the policy. Both regeneration and the drift check consume the same malformed model, while dead globs are only warnings, so CI can pass even though the intended exemption is silently ineffective. Validate that the top-level value is a list before validating its entries.AGENTS.md reference: AGENTS.md:L140-L143
Useful? React with 👍 / 👎.