feat(recipe-report): restructure page — priority-sorted wire table w/ CVE column, addendum, per-recipe changes
New page order: short lead -> the full wire table (sorted by priority-to-address, CVE recipes first, new CVEs count column) -> Addendum (bullets of real special issues, omitted if clean) -> Security Bulletin -> per-recipe "What changed". - recipe-report.py: _table() gains a CVEs column + recipe-name linking; new _changes() helper; render() reordered; docstring SPEC SHAPE updated (cve/addendum/changes added, needs_attention/routine removed). - recipe-report/SKILL.md + example-spec.json: new procedure, spec shape, and gold-standard template (2026-06-05, new format). - launch-report.py: kickoff text reflects the new priority-ordered structure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: recipe-report
|
||||
description: Generate the weekly public "Recipe Report" after the cc-ci /upgrade-all run. Reviews how the upgrade went and the state of every recipe + open PR, classifies what needs attention vs routine, and publishes a self-contained HTML page to report.ci.commoninternet.net (one page per week + a home index). Read-only — reports, never merges or edits PRs. Runs as its own agent (model configured separately from the upgrader; default opus). Invoke as /recipe-report [YYYY-MM-DD].
|
||||
description: Generate the weekly public "Recipe Report" after the cc-ci /upgrade-all run. Reviews how the upgrade went and the state of every recipe + open PR, and publishes a self-contained HTML page to report.ci.commoninternet.net (one page per week + a home index). Page order: short lead → the full wire table (priority-sorted, CVE-count column, CVE recipes first) → Addendum of special issues → Security Bulletin → per-recipe "what changed". Read-only — reports, never merges or edits PRs. Runs as its own agent (model configured separately from the upgrader; default opus). Invoke as /recipe-report [YYYY-MM-DD].
|
||||
---
|
||||
|
||||
# recipe-report
|
||||
@ -24,34 +24,45 @@ Helper: `python3 /srv/cc-ci/cc-ci-plan/recipe-report.py {survey|render|publish}`
|
||||
verdict; and each recipe's **per-recipe upgrade notes** (`upgrade_notes_md` — the breaking-change /
|
||||
**CVE / security** analysis the upgrade did). Read it all.
|
||||
|
||||
3. **Review & classify — this is a front page, write it like an editor.**
|
||||
- **Security first.** Scan the per-recipe `upgrade_notes_md` + the summary (and use your own knowledge
|
||||
of the version bumps) for upgrades that fix **CVEs / security issues**. Anything **critical/high**
|
||||
leads the page → the `security` bulletin (recipe · CVE id(s) + severity · what it fixes · PR link).
|
||||
This is the most important section; be specific about severity and what's exposed if not merged.
|
||||
- **Lead / editorial.** Write the `lead` in opus's voice — useful, concrete, opinionated. **~3 short
|
||||
paragraphs, ~150–180 words:**
|
||||
1. **Fleet state** in a line or two — how healthy, what moved, any trend.
|
||||
2. **What to focus on** — security/critical merges first, then the key failures.
|
||||
3. **Anything strange worth looking into** — odd or unexpected failures, parser snags, PR-state
|
||||
oddities (e.g. a recipe carrying two open PRs to reconcile), drift from the summary, leftover
|
||||
artifacts. This is the "editor's eye" paragraph; flag what a careful maintainer would want to notice.
|
||||
Lead with the single most important thing; the rest of the page carries the detail.
|
||||
- **Needs attention** — GREEN PRs ready to merge + errors/failures to investigate (RED `!testme`,
|
||||
recipe bugs). Short, specific prose + links. Flag cross-cutting issues (e.g. two open PRs to reconcile).
|
||||
- **Routine** — minor/clean bumps, stale-test PRs (need operator `--with-tests`), up-to-date / skipped.
|
||||
3. **Review & classify.** The page order is: **short lead → the full wire table (priority-sorted) →
|
||||
Addendum → Security Bulletin → per-recipe "What changed".** Same newspaper style; this order.
|
||||
- **Security analysis.** Scan the per-recipe `upgrade_notes_md` + the summary (and use your own
|
||||
knowledge of the version bumps) for upgrades that fix **CVEs / security issues**. For each recipe,
|
||||
**count the CVEs** the PR fixes — this drives both the table's `cve` column and the priority sort.
|
||||
Anything **critical/high** also gets a `security` bulletin entry (recipe · CVE id(s) + severity ·
|
||||
what it fixes · PR link); be specific about severity and what's exposed if not merged.
|
||||
- **Lead — ONE short paragraph.** A tight, concrete opener in opus's voice: fleet state in a sentence
|
||||
(how healthy, what moved) and what to address first. Lead with the single most important thing; the
|
||||
table and sections carry all the detail. Do NOT write multiple paragraphs.
|
||||
- **Priority sort for the table.** Order rows by **recommended priority to address**:
|
||||
1. recipes **with CVEs** first (more/higher-severity CVEs higher),
|
||||
2. then **FAILED** (RED) recipes,
|
||||
3. then **STALE**-test recipes,
|
||||
4. then routine **GREEN** bumps,
|
||||
5. then **UPTODATE / SKIPPED** last.
|
||||
- **Addendum — special issues to look into.** A bullet list of real anomalies worth an operator's
|
||||
eye: a recipe carrying **two open PRs** to reconcile, a **CI-run oddity** (flaky tier, needed
|
||||
re-runs), drift between the summary and reality, a **misleading branch name**, a tooling snag that
|
||||
silently drops a recipe, leftover artifact PRs, or **something in the test run that could be
|
||||
improved**. **Only real issues — if everything looks clean, leave it empty; do NOT invent items.**
|
||||
- **What changed — per recipe with a PR.** One short section per recipe that has a PR this week
|
||||
describing what the upgrade actually changes (versions, notable deps, migrations/gotchas, reconcile
|
||||
notes). Skip recipes with no PR.
|
||||
|
||||
4. **Write the spec** `/tmp/report-spec.json` (shape in the helper header): `date`, `subtitle`
|
||||
("Week of <human date>"), `lead` (the editorial, blank-line-separated paragraphs), `security[]`
|
||||
(critical-CVE upgrades — may be empty), `needs_attention[]`, `routine[]`, and a **comprehensive
|
||||
`table[]`** with EVERY recipe (`recipe`, `change` "a → b", `status`
|
||||
GREEN/STALE/FAILED/SKIPPED/UPTODATE, `ci` as a level/result number+info string e.g. `build 154 ✓`
|
||||
or `RED · restore` with `ci_url`, `pr`/`pr_url`, `notes`). **CI = link + number/info only; no images.**
|
||||
4. **Write the spec** `/tmp/report-spec.json` (shape in the helper header):
|
||||
- `date`, `subtitle` ("Week of <human date>"),
|
||||
- `lead` — **one short paragraph**,
|
||||
- `table[]` — EVERY recipe, **sorted by the priority above**. Fields: `recipe`, `change` "a → b",
|
||||
`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.**
|
||||
- `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.
|
||||
|
||||
**Template — match this.** `.claude/skills/recipe-report/example-spec.json` is the gold-standard
|
||||
example (the 2026-06-02 report; the operator approved its style/format). **Match its editorial voice,
|
||||
structure, field shapes, and level of specificity** — the only deliberate change is the **shorter
|
||||
`lead`** described above. Read it before writing your spec.
|
||||
example (operator-approved style/format/voice). **Match its editorial voice, field shapes, and level
|
||||
of specificity**, following the section order above. Read it before writing your spec.
|
||||
|
||||
5. **Render & publish.**
|
||||
`python3 .../recipe-report.py render /tmp/report-spec.json /tmp/week-<DATE>.html`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -44,8 +44,8 @@ def build_kickoff(date):
|
||||
f"Full spec: {WORKDIR}/.claude/skills/recipe-report/SKILL.md. Creds in {WORKDIR}/.testenv; "
|
||||
f"reach the CI server with `ssh cc-ci`.\n"
|
||||
f"You are READ-ONLY: review the latest /upgrade-all run + every recipe's open PRs + CI verdicts, "
|
||||
f"classify needs-attention vs routine, and publish one HTML page per run to "
|
||||
f"report.ci.commoninternet.net (+ regenerate the index). Public page — NO secrets/tokens/raw logs. "
|
||||
f"order the wire table by priority-to-address (CVE recipes first), and publish one HTML page per "
|
||||
f"run to report.ci.commoninternet.net (+ regenerate the index). Public page — NO secrets/tokens/raw logs. "
|
||||
f"Never merge/edit/comment on PRs. When done, print the report URL + 'RECIPE REPORT COMPLETE' and "
|
||||
f"go idle (do NOT loop)."
|
||||
)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""recipe-report — data + HTML helper for the weekly "Recipe Report" (/recipe-report skill).
|
||||
|
||||
A newspaper-style front page: masthead, an editorial LEAD (overall recipe-fleet state + what to focus
|
||||
on), a SECURITY BULLETIN of critical-CVE upgrades up top, then needs-attention / routine, and the
|
||||
comprehensive table ("the full wire") at the end.
|
||||
A newspaper-style front page: masthead, a SHORT editorial LEAD, then the comprehensive table ("the
|
||||
full wire" — priority-sorted, CVEs column), an ADDENDUM of special issues, a SECURITY BULLETIN, and a
|
||||
per-recipe "what changed" section at the end.
|
||||
|
||||
Subcommands (the /recipe-report agent runs them around its own review/classification):
|
||||
survey [DATE] JSON of the run + every recipe's open PRs + CI verdict + per-recipe upgrade
|
||||
@ -11,14 +11,22 @@ Subcommands (the /recipe-report agent runs them around its own review/classifica
|
||||
render SPEC.json OUT.html render the agent's report spec -> a self-contained newspaper HTML page
|
||||
publish OUT.html DATE copy to cc-ci:/var/lib/cc-ci-reports/week-DATE.html and regen the archive index
|
||||
|
||||
Page order: short lead → the full wire table (priority-sorted, CVEs column) → Addendum → Security
|
||||
Bulletin → per-recipe "What changed".
|
||||
|
||||
SPEC SHAPE (the agent writes this JSON):
|
||||
{"date":"YYYY-MM-DD","subtitle":"Week of <human date>",
|
||||
"lead":"<editorial: overall fleet state + what to focus on; blank-line-separated paragraphs>",
|
||||
"security":[{"title":"recipe — CVE-… (critical)","body":"what it fixes","links":[{"text":"PR #","url":"…"}]}],
|
||||
"needs_attention":[{"title":"…","body":"…","links":[…]}],
|
||||
"routine":[ {same shape} ],
|
||||
"table":[{"recipe":"x","change":"a → b","status":"GREEN","ci":"build 154 ✓","ci_url":"…",
|
||||
"pr":"#4","pr_url":"…","notes":"…"}]}
|
||||
"lead":"<ONE short paragraph>",
|
||||
"table":[{"recipe":"x","change":"a → b","status":"GREEN|FAILED|STALE|SKIPPED|UPTODATE",
|
||||
"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,
|
||||
# then stale-tests, then routine green, then up-to-date/skipped.
|
||||
"addendum":["a special issue to look into (multiple open PRs, a CI-run oddity, a possible
|
||||
improvement) …"], # only real issues — omit/empty if everything's clean
|
||||
"security":[{"title":"recipe — CVE-… (high)","body":"what it fixes","links":[{"text":"PR #","url":"…"}]}],
|
||||
"changes":[{"recipe":"x","body":"what changed in this recipe's PR","links":[{"text":"PR #4","url":"…"}]}]}
|
||||
# one `changes` entry per recipe that has a PR this week.
|
||||
PUBLIC PAGE — include only public-safe data (no secrets/tokens/raw logs).
|
||||
"""
|
||||
import base64, html, json, os, re, subprocess, sys, urllib.request
|
||||
@ -114,6 +122,11 @@ th{border-bottom:2px solid var(--rule);font-size:.72rem;text-transform:uppercase
|
||||
footer{margin-top:2.6rem;border-top:4px double var(--rule);padding-top:.8rem;font-size:.82rem;color:var(--mut);text-align:center}
|
||||
.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}
|
||||
.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}
|
||||
.change .ch{font-weight:700;font-size:1.12rem}.change .cb{margin:.2rem 0 .35rem;color:#2b2b2b}
|
||||
"""
|
||||
|
||||
|
||||
@ -148,25 +161,48 @@ def _stories(items, repo_url=None):
|
||||
f'<div class="b">{lk(it.get("body"))}</div>{_links(it.get("links"))}</div>' for it in items)
|
||||
|
||||
|
||||
def _table(rows):
|
||||
def _table(rows, repo_url=None):
|
||||
if not rows:
|
||||
return ""
|
||||
head = "<tr><th>Recipe</th><th>Change</th><th>Status</th><th>CI</th><th>PR</th><th>Notes</th></tr>"
|
||||
head = ("<tr><th>Recipe</th><th>Change</th><th>Status</th><th>CVEs</th>"
|
||||
"<th>CI</th><th>PR</th><th>Notes</th></tr>")
|
||||
trs = []
|
||||
for r in rows:
|
||||
scls = "s-" + str(r.get("status", "")).upper().replace("-", "").replace(" ", "")
|
||||
name = _esc(r.get("recipe"))
|
||||
if repo_url and r.get("recipe") in repo_url:
|
||||
name = f'<a href="{repo_url[r["recipe"]]}">{name}</a>'
|
||||
cve = r.get("cve")
|
||||
cve_cell = (f'<span class="cve">{int(cve)}</span>' if isinstance(cve, (int, float)) and cve
|
||||
else '<span class="muted">none</span>')
|
||||
ci = _esc(r.get("ci"))
|
||||
if r.get("ci_url"):
|
||||
ci = f'<a href="{_esc(r["ci_url"])}">{ci}</a>'
|
||||
pr = _esc(r.get("pr"))
|
||||
if r.get("pr_url"):
|
||||
pr = f'<a href="{_esc(r["pr_url"])}">{pr}</a>'
|
||||
trs.append(f"<tr><td>{_esc(r.get('recipe'))}</td><td>{_esc(r.get('change'))}</td>"
|
||||
f'<td class="{scls}">{_esc(r.get("status"))}</td><td>{ci}</td><td>{pr}</td>'
|
||||
f"<td>{_esc(r.get('notes'))}</td></tr>")
|
||||
trs.append(f"<tr><td>{name}</td><td>{_esc(r.get('change'))}</td>"
|
||||
f'<td class="{scls}">{_esc(r.get("status"))}</td><td>{cve_cell}</td>'
|
||||
f"<td>{ci}</td><td>{pr}</td><td>{_esc(r.get('notes'))}</td></tr>")
|
||||
return f"<table>{head}{''.join(trs)}</table>"
|
||||
|
||||
|
||||
def _changes(items, repo_url=None):
|
||||
"""Per-recipe 'what changed' sections (one per recipe that has a PR)."""
|
||||
if not items:
|
||||
return ""
|
||||
lk = (lambda x: _linkify_recipes(_esc(x), repo_url)) if repo_url else _esc
|
||||
out = []
|
||||
for c in items:
|
||||
name = c.get("recipe")
|
||||
hdr = _esc(name)
|
||||
if repo_url and name in repo_url:
|
||||
hdr = f'<a href="{repo_url[name]}">{hdr}</a>'
|
||||
out.append(f'<div class="change"><div class="ch">{hdr}</div>'
|
||||
f'<div class="cb">{lk(c.get("body"))}</div>{_links(c.get("links"))}</div>')
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _page(title, body):
|
||||
return (f'<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
||||
f'<meta name="viewport" content="width=device-width,initial-scale=1">'
|
||||
@ -195,12 +231,20 @@ def render(spec_path, out_path):
|
||||
f'<div class="dateline"><span>{_esc(sub)}</span>'
|
||||
f'<span>report.ci.commoninternet.net</span><span>{gen}</span></div>'
|
||||
f'<div class="lead">{lead}</div>')
|
||||
# 1) the full wire — every recipe, in the agent's recommended priority order (CVEs first); CVEs column.
|
||||
body += f'<h2>The full wire — every recipe, in priority order</h2>{_table(s.get("table"), repo_url)}'
|
||||
# 2) addendum — special issues to look into (normal-size header); omitted entirely if there are none.
|
||||
add = [a for a in (s.get("addendum") or []) if str(a).strip()]
|
||||
if add:
|
||||
body += ('<p class="addendum-h">Addendum</p><ul class="addendum">'
|
||||
+ "".join(f"<li>{_linkify_recipes(_esc(b), repo_url)}</li>" for b in add) + "</ul>")
|
||||
# 3) security bulletin
|
||||
if s.get("security"):
|
||||
body += ('<div class="bulletin"><div class="kicker">Security Bulletin</div>'
|
||||
'<h2>🔒 Critical CVE upgrades — merge first</h2>' + _stories(s["security"], repo_url) + "</div>")
|
||||
body += f'<h2>⚑ Needs attention</h2>{_stories(s.get("needs_attention"), repo_url)}'
|
||||
body += f'<h2>Routine</h2><div class="cols">{_stories(s.get("routine"), repo_url)}</div>'
|
||||
body += f'<h2>The full wire — every recipe</h2>{_table(s.get("table"))}'
|
||||
'<h2>🔒 Critical CVE upgrades</h2>' + _stories(s["security"], repo_url) + "</div>")
|
||||
# 4) what changed — a short section per recipe that has a PR
|
||||
if s.get("changes"):
|
||||
body += f'<h2>What changed</h2>{_changes(s.get("changes"), repo_url)}'
|
||||
body += (f'<footer>The Recipe Report · generated {gen} · '
|
||||
f'<a href="https://ci.commoninternet.net/">dashboard</a> · <a href="./">archive</a></footer>')
|
||||
open(out_path, "w").write(_page("The Recipe Report — " + s["date"], body))
|
||||
|
||||
Reference in New Issue
Block a user