diff --git a/.claude/skills/recipe-report/SKILL.md b/.claude/skills/recipe-report/SKILL.md index addf268..281af0b 100644 --- a/.claude/skills/recipe-report/SKILL.md +++ b/.claude/skills/recipe-report/SKILL.md @@ -56,6 +56,11 @@ Helper: `python3 /srv/cc-ci/cc-ci-plan/recipe-report.py {survey|render|publish}` `status` GREEN/FAILED/STALE/SKIPPED/UPTODATE, **`cve`** (integer count of CVEs this PR fixes; `0` or omit for none), `ci` as a level/result number+info string e.g. `build 154 ✓` or `RED 200 · install` with `ci_url`, `pr`/`pr_url`, `notes`. **CI = link + number/info only; no images.** + - The table renders **two** status columns: **TESTS** (the `status` field — the CI/test verdict) + and **STATUS** (the PR's LIVE state, fetched client-side). You only supply `status`; the live + STATUS column is derived automatically from `recipe` + `pr` and needs no spec field — it shows + `open` vs a ✓ (any not-open state), refreshing ~30s via the same-origin `/pr//` proxy + (cc-ci `nix/modules/reports.nix`); it degrades to a muted "?" if the proxy/repo is unreachable. - `addendum[]` — list of bullet strings (may be empty), - `security[]` — critical-CVE bulletin entries (may be empty), - `changes[]` — `{recipe, body, links[]}`, one per recipe that has a PR. diff --git a/cc-ci-plan/recipe-report.py b/cc-ci-plan/recipe-report.py index ca44fb3..6c42772 100755 --- a/cc-ci-plan/recipe-report.py +++ b/cc-ci-plan/recipe-report.py @@ -14,10 +14,18 @@ Subcommands (the /recipe-report agent runs them around its own review/classifica Page order: short lead → the full wire table (priority-sorted, CVEs column) → Addendum → Security Bulletin → per-recipe "What changed". +The full-wire table has two distinct status columns: + TESTS — the CI/test verdict for the PR (GREEN/FAILED/STALE/SKIPPED/UPTODATE; from the `status` field). + STATUS — the PR's LIVE state, fetched client-side and refreshed every ~30s: `open` vs a ✓ for any + not-open state (merged OR closed). Derived from `recipe` + `pr`; the inline script GETs the + same-origin `/pr//` proxy served by the ccci-reports nginx stack (cc-ci repo, + nix/modules/reports.nix). Needs the recipe mirrors public; degrades to a muted "?" if the + proxy/repo is unreachable. No spec-shape change — STATUS uses existing row fields. + SPEC SHAPE (the agent writes this JSON): {"date":"YYYY-MM-DD","subtitle":"Week of ", "lead":"", - "table":[{"recipe":"x","change":"a → b","status":"GREEN|FAILED|STALE|SKIPPED|UPTODATE", + "table":[{"recipe":"x","change":"a → b","status":"GREEN|FAILED|STALE|SKIPPED|UPTODATE", # -> TESTS col "cve":16, # number of CVEs this PR fixes; 0/omit for none "ci":"build 154 ✓","ci_url":"…","pr":"#4","pr_url":"…","notes":"…"}], # ROWS SORTED by recommended priority to address — recipes WITH CVEs first, then failures, @@ -123,6 +131,9 @@ footer{margin-top:2.6rem;border-top:4px double var(--rule);padding-top:.8rem;fon .idx{list-style:none;padding:0}.idx li{padding:.55rem 0;border-bottom:1px solid #d8d2c2;font-size:1.1rem} .idx .d{color:var(--mut);font-size:.85rem;float:right} td .cve{color:var(--red);font-weight:800}.muted{color:#999} +.pr-status{white-space:nowrap} +.pr-open{display:inline-block;font:700 .66rem/1 Georgia,serif;text-transform:uppercase;letter-spacing:.07em;color:#9b6b00;background:#fff3d6;border:1px solid #e3c98a;border-radius:.2rem;padding:.13rem .42rem} +.pr-done{color:#1a7a3c;font-weight:800;font-size:1.05rem} .addendum-h{font-size:1rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin:1.7rem 0 .3rem;color:var(--ink)} ul.addendum{margin:.2rem 0 1.2rem 1.2rem;padding:0}ul.addendum li{margin:.35rem 0;line-height:1.55} .change{margin:.8rem 0;padding-bottom:.7rem;border-bottom:1px solid #d8d2c2} @@ -130,6 +141,35 @@ ul.addendum{margin:.2rem 0 1.2rem 1.2rem;padding:0}ul.addendum li{margin:.35rem """ +# Inline (CSP-safe, dependency-free) script that lights up the live PR-STATUS column. For each +# `.pr-status[data-pr]` cell it GETs the same-origin /pr// proxy (see reports.nix in the +# cc-ci repo) and renders BINARY state: `state==='open'` -> an "open" badge; any other state +# (closed/merged) -> a green ✓ (the title shows merged vs closed). It refreshes every 30s so an open +# tab tracks state in realtime, and is fully resilient — a network/parse error leaves a muted "?" and +# never overwrites a value already resolved, so the page renders fully even if the proxy is down. +STATUS_JS = """ +(function(){ + function refresh(){ + document.querySelectorAll('.pr-status[data-pr]').forEach(function(cell){ + var repo=cell.getAttribute('data-repo'), pr=cell.getAttribute('data-pr'); + if(!repo||!pr) return; + fetch('/pr/'+encodeURIComponent(repo)+'/'+encodeURIComponent(pr),{headers:{'Accept':'application/json'}}) + .then(function(r){ if(!r.ok) throw 0; return r.json(); }) + .then(function(d){ + var st=d&&d.state; + if(st==='open'){ cell.innerHTML='open'; cell.dataset.ok='1'; } + else if(st){ cell.innerHTML='\\u2713'; cell.dataset.ok='1'; } + else throw 0; + }) + .catch(function(){ if(!cell.dataset.ok){ cell.innerHTML='?'; } }); + }); + } + if(document.readyState!=='loading'){ refresh(); } else { document.addEventListener('DOMContentLoaded', refresh); } + setInterval(refresh, 30000); +})(); +""" + + def _esc(s): return html.escape(str(s or "")) @@ -164,8 +204,10 @@ def _stories(items, repo_url=None): def _table(rows, repo_url=None): if not rows: return "" - head = ("RecipeChangeStatusCVEs" - "CIPRNotes") + # TESTS = the CI/test verdict (GREEN/FAILED/STALE/…); STATUS = the PR's LIVE state, fetched + # client-side (open vs ✓ for any not-open state) by the inline script in _page(). + head = ("RecipeChangeTESTSCVEs" + "CIPRSTATUSNotes") trs = [] for r in rows: scls = "s-" + str(r.get("status", "")).upper().replace("-", "").replace(" ", "") @@ -181,9 +223,18 @@ def _table(rows, repo_url=None): pr = _esc(r.get("pr")) if r.get("pr_url"): pr = f'{pr}' + # Live PR-STATUS cell: a JS hook carrying the repo + PR number derived from the existing + # `recipe` + `pr` fields. The inline script fills it from /pr//. Blank when no PR. + recipe = r.get("recipe") or "" + m = re.search(r"\d+", str(r.get("pr") or "")) + if m and recipe: + status_cell = (f'') + else: + status_cell = "" trs.append(f"{name}{_esc(r.get('change'))}" f'{_esc(r.get("status"))}{cve_cell}' - f"{ci}{pr}{_esc(r.get('notes'))}") + f"{ci}{pr}{status_cell}{_esc(r.get('notes'))}") return f"{head}{''.join(trs)}
" @@ -204,10 +255,12 @@ def _changes(items, repo_url=None): def _page(title, body): + # The live-status script ships on every page (reports + archive index); harmless where there are + # no `.pr-status` cells. Self-contained: inline CSS + inline JS, no external requests. return (f'' f'' f"{_esc(title)}" - f'
{body}
') + f'
{body}
') def _mast():