diff --git a/api/tests/test_guards_report.py b/api/tests/test_guards_report.py new file mode 100644 index 0000000..a8f8fa1 --- /dev/null +++ b/api/tests/test_guards_report.py @@ -0,0 +1,61 @@ +"""guards_report: job attribution and the Grant-owned split.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) +_spec = importlib.util.spec_from_file_location("guards_report", ROOT / "scripts" / "guards_report.py") +gr = importlib.util.module_from_spec(_spec) +sys.modules["guards_report"] = gr +_spec.loader.exec_module(gr) + +OWNED = yaml.safe_load((ROOT / "ci" / "grant-owned.yml").read_text())["grant_owned"] +WF = """name: CI +on: + push: +jobs: + reality-check: + runs-on: x + steps: + - run: npm install jsdom + test-backend: + runs-on: x + steps: + - run: pip install pytest +""" + + +def test_job_of_attributes_lines_to_their_job(): + assert gr.job_of(WF, 3) is None # `on:` block, not a job + assert gr.job_of(WF, 8) == "reality-check" + assert gr.job_of(WF, 12) == "test-backend" + + +def test_windy_pro_desktop_jobs_and_paths_are_grant_owned(): + ci = ".github/workflows/ci.yml" + assert gr.grant_owned("windy-pro", ci, "reality-check", OWNED) + assert gr.grant_owned("windy-pro", ci, "build-electron", OWNED) + assert not gr.grant_owned("windy-pro", ci, "test-backend", OWNED) # server side: 8c + assert gr.grant_owned("windy-pro", ".github/workflows/release-mac.yml", None, OWNED) + assert gr.grant_owned("windy-pro", "src/client/desktop/main.js", None, OWNED) + assert not gr.grant_owned("windy-pro", "services/account-server/Dockerfile", None, OWNED) + assert not gr.grant_owned("windy-chat", "src/client/desktop/main.js", None, OWNED) + + +def test_render_splits_lane_and_grant_counts(): + F = gr.cg.Finding + res = {"windy-pro": {"sha": "a" * 40, "compute": [], + "hygiene": [(F("ci.yml", 8, "floating install", "npm install"), "reality-check", True), + (F("ci.yml", 12, "floating install", "pip x"), "test-backend", False)]}, + "windy-git": {"sha": "b" * 40, "compute": [], "hygiene": []}} + md = gr.render(res) + assert "| ci-hygiene (house rule 6) | 1 | 1 | ❌ not yet |" in md + assert "| compute-guard (Mind is the only door) | 0 | 0 | ✅ YES |" in md + assert "| windy-git | bbbbbbb | 0 | 0 | clean ✅ |" in md + assert "(job reality-check)" in md diff --git a/ci/grant-owned.yml b/ci/grant-owned.yml new file mode 100644 index 0000000..4ae0378 --- /dev/null +++ b/ci/grant-owned.yml @@ -0,0 +1,15 @@ +# Code Grant owns directly (orchestrator, 09-23): guard findings here are listed +# SEPARATELY in the guards status page and never hold up "block". Changes to +# these files are proposals for Grant / Windy Word 44, not a lane's fix. +grant_owned: + - repo: windy-pro + reason: "Windy Word desktop (Electron) + its release/installer builds: Grant's, built from the Mac mini." + paths: + - "src/client/desktop/*" + - "installer-v2/*" + - ".github/workflows/build-windows.yml" + - ".github/workflows/release-mac.yml" + - ".github/workflows/build-installer.yml" + - ".github/workflows/build-offline-installers.yml" + jobs: + ".github/workflows/ci.yml": [reality-check, build-desktop, test-installer, build-electron] diff --git a/scripts/guards_report.py b/scripts/guards_report.py new file mode 100644 index 0000000..3431f62 --- /dev/null +++ b/scripts/guards_report.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Live status of the repo guards (compute-guard + ci-hygiene) as one markdown page. + +Scans every bridged repo's DEFAULT branch with both guards and renders what is +left, per repo and owner lane. Findings in code Grant owns (ci/grant-owned.yml: +windy-pro's desktop app and its build jobs) are listed in their OWN section and +do not count against "ready to block": those are proposals for Grant, not a +lane's fix (orchestrator, 09-23). + + sudo python3 scripts/guards_report.py > GUARDS_STATUS.md +""" + +from __future__ import annotations + +import fnmatch +import os +import re +import sys +import time +from pathlib import Path + +import yaml + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import ci_hygiene as hy # noqa: E402 +import compute_guard as cg # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +OWNED = Path(os.environ.get("GRANT_OWNED", ROOT / "ci" / "grant-owned.yml")) +REPOS = os.environ.get("BRIDGE_REPOS", "").split() or [ + "windy-chat", "windy-mail", "windy-calendar", "Windy-Clone", "WindyCloud", "windy-search", + "windy-connect", "windy-drops", "windy-code-web", "windy-code", "windy-traveler", + "windy-registry", "eternitas", "windy-translate", "windytranslate-site", "windytraveler-site", + "windy-hand", "windy-cloud-sites", "windy-cloud-domains", "windy-cloud-vps", "windytalk", + "windy-pro", "windy-mind", "windy-git"] +JOB = re.compile(r"^ ([A-Za-z0-9_-]+):\s*$") + + +def job_of(text: str, line: int) -> str | None: + """The workflow job a line belongs to (2-space keys under `jobs:`).""" + in_jobs, job = False, None + for i, raw in enumerate(text.splitlines(), 1): + if raw.startswith("jobs:"): + in_jobs = True + elif in_jobs and JOB.match(raw): + job = JOB.match(raw).group(1) + elif raw and not raw[0].isspace() and not raw.startswith("jobs:"): + in_jobs = False + if i == line: + return job if in_jobs else None + return None + + +def grant_owned(repo: str, path: str, job: str | None, owned: list[dict]) -> bool: + for e in owned: + if e["repo"] != repo: + continue + if any(fnmatch.fnmatch(path, g) for g in e.get("paths") or []): + return True + if job and job in (e.get("jobs") or {}).get(path, []): + return True + return False + + +def scan(repo: str, owned: list[dict]): + bare = cg.WORK / f"{repo}.git" + if not bare.is_dir(): + return None + head = cg._git(bare, "symbolic-ref", "--short", "HEAD").strip() + sha = cg._git(bare, "rev-parse", head).strip() + out = {"sha": sha, "compute": [], "hygiene": []} + texts: dict[str, str] = {} + for key, fs in (("compute", cg.check(repo, sha, head, True) or []), + ("hygiene", hy.check(repo, sha, head, True) or [])): + for f in fs: + job = None + if "/workflows/" in f.path: + if f.path not in texts: + texts[f.path] = cg._git(bare, "show", f"{sha}:{f.path}") + job = job_of(texts[f.path], f.line) + out[key].append((f, job, grant_owned(repo, f.path, job, owned))) + return out + + +def render(results: dict) -> str: + now = time.strftime("%Y-%m-%d %H:%MZ", time.gmtime()) + lane = {k: 0 for k in ("compute", "hygiene")} + grant = {k: 0 for k in ("compute", "hygiene")} + for r in results.values(): + for k in lane: + lane[k] += sum(1 for _, _, g in r[k] if not g) + grant[k] += sum(1 for _, _, g in r[k] if g) + L = [f"# Repo guards: live status (generated {now}; windy-git scripts/guards_report.py)", + "_Default branches only. WARN-only today; the orchestrator says \"block\" per guard when its LANE column is 0. " + "Grant-owned code (ci/grant-owned.yml) is listed separately and never holds up a block._", "", + "| Guard | Lane-owned findings | Grant-owned (proposals) | Ready to block? |", "|---|---|---|---|", + f"| compute-guard (Mind is the only door) | {lane['compute']} | {grant['compute']} | {'✅ YES' if lane['compute'] == 0 else '❌ not yet'} |", + f"| ci-hygiene (house rule 6) | {lane['hygiene']} | {grant['hygiene']} | {'✅ YES' if lane['hygiene'] == 0 else '❌ not yet'} |", + "", "## By repo (lane-owned)", "| Repo | head | compute | hygiene | first items |", "|---|---|---|---|---|"] + for repo, r in sorted(results.items()): + c = [x for x in r["compute"] if not x[2]] + h = [x for x in r["hygiene"] if not x[2]] + items = "; ".join(f"`{f.path}:{f.line}` {f.match}" for f, _, _ in (c + h)[:3]) or "clean ✅" + L.append(f"| {repo} | {r['sha'][:7]} | {len(c)} | {len(h)} | {items} |") + L += ["", "## Grant-owned (windy-pro desktop app + its build jobs): proposals only, not blocking"] + g = [(repo, f, job) for repo, r in sorted(results.items()) for k in ("compute", "hygiene") + for f, job, own in r[k] if own] + L += [f"- {repo} `{f.path}:{f.line}`{f' (job {job})' if job else ''}: {f.match}" for repo, f, job in g] or ["- none"] + return "\n".join(L) + "\n" + + +def main() -> int: + owned = (yaml.safe_load(OWNED.read_text()) or {}).get("grant_owned") or [] + results = {} + for repo in REPOS: + r = scan(repo, owned) + if r is not None: + results[repo] = r + sys.stdout.write(render(results)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())