Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ Status: in progress. The design is in `specs/next-ci-reliability` of
`--until 2026-10-14T11:00` ends the period in the past, so a baseline can be reproduced after later
changes; `--limit` raises the number of runs read per repository (default 300).

- `standards/audit.py` (available): audits every repository of the organisation against
`standards/standards.json` and prints each deviation as `error`, `warning` or `info`: branch
deletion after merge, Dependabot, CODEOWNERS, secret scanning and a recognised license on public
repositories, a CI that calls the shared gate pinned by a full SHA with a `# vX.Y.Z` comment, too
many leftover branches, and the organisation's two-factor requirement. An exception names a
repository and a rule, gives a reason and expires; an expired one is itself an error.
`py -3 standards/audit.py [--fail-on error]`, or `report/ci_report.py --audit` to append it to the
weekly report.

## Rules

- No secrets, no business logic and no repository-specific values: everything comes in as an input.
Expand Down
30 changes: 30 additions & 0 deletions report/ci_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import argparse
import datetime as dt
import importlib.util
import json
import statistics
import subprocess
Expand Down Expand Up @@ -305,6 +306,24 @@ def render(
return "\n".join(lines)


def load_audit():
"""The standards audit module, found beside this report (`../standards/audit.py`)."""
path = Path(__file__).resolve().parent.parent / "standards" / "audit.py"
spec = importlib.util.spec_from_file_location("standards_audit", path)
module = importlib.util.module_from_spec(spec)
sys.modules["standards_audit"] = module # dataclasses look the module up by name
spec.loader.exec_module(module)
return module


def audit_section(audit_text: str) -> str:
"""The audit report as a section of this one: its title becomes a second-level heading."""
lines = audit_text.split("\n")
if lines and lines[0] == "# Standards audit":
lines[0] = "## Standards audit"
return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
Expand All @@ -324,6 +343,11 @@ def main(argv: list[str] | None = None) -> int:
default=300,
help="runs read per repository (gh allows up to 1000)",
)
parser.add_argument(
"--audit",
action="store_true",
help="append the standards audit of the organisation",
)
parser.add_argument("--output", help="write the report here instead of printing it")
args = parser.parse_args(argv)

Expand All @@ -343,6 +367,12 @@ def main(argv: list[str] | None = None) -> int:
for repo in repos
]
text = render(stats, args.days, now, until=until)
if args.audit:
audit = load_audit()
standard_path = Path(audit.__file__).with_name("standards.json")
text += "\n\n" + audit_section(
audit.render(audit.run(standard_path, now=now), now)
)
if args.output:
Path(args.output).write_text(text + "\n", encoding="utf-8")
else:
Expand Down
Loading
Loading