Adds test-advisory-scan.py (58 offline tests on fixtures + 6 live regressions against the week-2026-08-07 report) and audit-advisory-scan.py, which re-derives every count with a SEPARATE semver implementation and its own release fetch and diffs against the scanner. Both found real defects: 1. Window membership was compared on ragged tuples, so (18,) < (18,0) — a CVE patched in 18.0 fell OUTSIDE a window ending at 18. Bare major tags are the norm for sidecars (postgres:18, redis:8-alpine). Now zero-padded, which also keeps the upper bound conservative (18.5 stays out of a window ending at 18). 2. Advisories with no knowable fix version were silently counted as 'not fixed'. Twelve redis advisories say patched_versions 'TBD' or '7.4.X' with an open-ended range — six of them high severity. They are now INDETERMINATE: not counted, not dismissed, and surfaced in the output. All twelve turned out to be genuinely fixed: redis names each in the release notes of every branch that got the fix (CVE-2025-32023 -> 6.2.19, 7.2.10, 7.4.5, 8.0.3, 8.2.0). So a third deterministic method resolves them from release notes, with the naming tags recorded as the citation. discourse's redis contribution goes 5 -> 17, and its total 128 -> 140. Pass 2 (--adjudicate) is the model-judged stage for what arithmetic cannot settle: it hands over each open case's full evidence, plus every verdict pass 1 reached, and takes FIXED/NOT-FIXED/STILL-UNKNOWN with a reason citing that evidence. It may only raise a count. Vendor-page-only CVEs — the shape of both gitea CVSS-9.8 RCEs — now reach it instead of being dropped. Tests cover pass 1 only, by design; pass 2's judgement is a model's. What is tested there is deterministic: which cases it selects, and that truncation is announced rather than silent. SPEC.md rewritten around the two passes.
9.9 KiB
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, 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 separately overridable via REPORT_BACKEND/REPORT_MODEL; defaults to the upgrader's backend+model, opencode-go/glm-5.2). Invoke as /recipe-report [YYYY-MM-DD].
recipe-report
Produce the weekly "The Recipe Report" — a public HTML page reviewing the latest /upgrade-all run
and the live state of every recipe + open PR. Served at https://report.ci.commoninternet.net (one
week-<date>.html per run + a home-page index). You are read-only: you report, you never merge,
edit, or comment on PRs. The page is public + unauthenticated — include only public-safe data
(recipe names, version bumps, PR titles/links, CI verdicts, concise error summaries); never secrets,
tokens, internal IPs, or raw logs.
Helper: python3 /srv/cc-ci/cc-ci-plan/recipe-report.py {survey|render|publish} (see its header).
Deterministic formatting — spec in, page out (NON-NEGOTIABLE). ALL layout, CSS, and HTML is owned by
recipe-report.py's render(). Your ONLY output artifact is the spec JSON (data + editorial text).
You never write, edit, post-process, or templatize HTML yourself; you never put markup/inline styles
in spec text fields (they are HTML-escaped on purpose); the page is produced solely by
render <spec> <out>. Same spec shape every week → a structurally identical page. If the layout itself
must change, edit recipe-report.py (and example-spec.json) — never the generated page. This is what
keeps every weekly edition looking the same regardless of which model writes the spec.
Procedure
-
Pick the date.
DATE= the argument, else today (UTC). The/upgrade-allsummary for that run is/srv/cc-ci/.cc-ci-logs/upgrades/upgrade-all-<DATE>.md. -
Survey.
python3 /srv/cc-ci/cc-ci-plan/recipe-report.py survey <DATE> > /tmp/survey.json. This gives you: the raw/upgrade-allsummary; for every recipe mirror its open PRs + thecc-ci/testmeverdict; and each recipe's per-recipe upgrade notes (upgrade_notes_md— the breaking-change / CVE / security analysis the upgrade did). Read it all. -
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'scvecolumn and the priority sort.- ADDITIONALLY, and never instead: each per-recipe log carries an
### Advisory scan (deterministic pre-step)block (fromcc-ci-plan/advisory-scan.py— GitHub Security Advisories + vendor security pages + OSV, with severities and fixed-in versions). Treat its CVE list as a further source and report the UNION of it and what you found by reading. Its entries are machine-derived with advisory IDs, so prefer them for CVE ids / severities / fixed-in versions, and cite the GHSA where present in the Security Bulletin. If the block lists failed sources, the count is not authoritative: render the cve cell as?(unknown), nevernone— a blank that reads as "clean" is exactly how two CVSS-9.8 gitea RCEs were reported as "none" on 2026-08-07. ?must stay RARE — it means "we tried and could not tell", not "we didn't look". Use it ONLY when a scan ran and reported genuinely failed sources, or when the scan block says COUNT UNKNOWN. In that case the scan's0means not determined: publish?and say so in the notes; publishing0would assert a clean bill of health nothing supports. Note a version-scheme change is no longer a reason for?— the scan resolves semver→calver jumps (discourse 3.5.3 → 2026.7.1) by falling back to advisory publish dates and reports a real number.- A count with undetermined advisories is a FLOOR. If the scan block says N advisories "could NOT be judged", report the number but say in the notes that it is a floor — those advisories are neither fixed nor safe, they are unmeasured. Never round them away.
- Counts span every image, each judged by its own window. A scan block lists one line per
image with its version range and classification method; the headline is their union. So a
recipe's count legitimately includes sidecar CVEs (discourse's 128 = 123 app + 5 redis).
When a sidecar contributes a critical/high, name the image in the bulletin — CVE-2025-49844 is
a redis flaw, not a discourse one, and an operator reading "discourse" needs to know that.
(The scan headline itself now says
UNKNOWNrather than a number in that case.) In particular: a recipe with no upgrade this run (up-to-date/skipped) has nothing an upgrade could have fixed — report0, not?. A recipe with a clean scan reports its number (including0). Benign notes in a scan block (no-advisories-published,skipped: template URL) are NOT failures and must not trigger?. If you find yourself rendering?for many recipes, that is a bug to report in the Addendum, not a normal outcome. Anything critical/high also gets asecuritybulletin entry (recipe · CVE id(s) + severity · what it fixes · PR link); be specific about severity and what's exposed if not merged.
- ADDITIONALLY, and never instead: each per-recipe log carries an
- 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:
- recipes with CVEs first (more/higher-severity CVEs higher),
- then FAILED (RED) recipes,
- then STALE-test recipes,
- then routine GREEN bumps,
- 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.
- Security analysis. Scan the per-recipe
-
Write the spec
/tmp/report-spec.json(shape in the helper header):date,subtitle("Week of "),lead— one short paragraph,table[]— EVERY recipe, sorted by the priority above. Fields:recipe,change"a → b",statusGREEN/FAILED/STALE/SKIPPED/UPTODATE,cve(integer count of CVEs this PR fixes;0or omit for none),cias a level/result number+info string e.g.build 154 ✓orRED 200 · installwithci_url,pr/pr_url,notes. CI = link + number/info only; no images.- The table renders two status columns: TESTS (the
statusfield — the CI/test verdict) and STATUS (the PR's LIVE state, fetched client-side). You only supplystatus; the live STATUS column is derived automatically fromrecipe+prand needs no spec field — it showsopenvs a ✓ (any not-open state), refreshing ~30s via the same-origin/pr/<recipe>/<n>proxy (cc-cinix/modules/reports.nix); it degrades to a muted "?" if the proxy/repo is unreachable.
- The table renders two status columns: TESTS (the
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.jsonis the gold-standard 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. -
Render & publish — via the script only; never hand-edit the HTML.
python3 .../recipe-report.py render /tmp/report-spec.json /tmp/week-<DATE>.htmlpython3 .../recipe-report.py publish /tmp/week-<DATE>.html <DATE>(publish copies the page tocc-ci:/var/lib/cc-ci-reports/and regenerates the index.) Ifrendererrors, fix the spec JSON (a malformed field) and re-run render — do NOT work around it by writing HTML by hand. The rendered file is an artifact of the spec, never edited directly. -
Verify & stop.
curl -fsS https://report.ci.commoninternet.net/week-<DATE>.htmlrenders and the index lists it. Print the report URL andRECIPE REPORT COMPLETE, then go idle (one-shot — do not loop).
Guardrails
- Read-only. Never merge/edit/comment on PRs or touch recipes/tests. You only describe + link.
- Public-safe. No secrets/tokens/internal hostnames/raw logs on the page. Summarize errors.
- One page per run; always regenerate the index so all weeks are listed (newest first).