claim(M1): per-recipe history sourced from local /var/lib/cc-ci-runs artifacts (full history, not Drone 100-build slice)
Some checks failed
continuous-integration/drone/push Build is failing

history_for() now enumerates run dirs' results.json, groups by recipe, sorts
newest-first by finished timestamp (mixed numeric+named ids — timestamp is the
only correct key), caps at HISTORY_CAP=30, skips malformed/empty/no-recipe dirs.
Overview + badges + /runs + security guards + stdlib-only unchanged.
Local verify: 13/13 unit tests; full-fixture vs 308 real results.json →
bluesky-pds=8 in exact ts order, plausible capped 30 newest, edge dirs skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 16:25:39 +00:00
parent 2d5211f401
commit 3595e80d08
6 changed files with 277 additions and 8 deletions

View File

@ -25,6 +25,9 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
DRONE_URL = os.environ.get("DRONE_URL", "https://drone.ci.commoninternet.net")
CI_REPO = os.environ.get("CI_REPO", "recipe-maintainers/cc-ci")
CACHE_TTL = int(os.environ.get("CACHE_TTL", "30"))
# Per-recipe history display cap (phase dash): a long-lived recipe (plausible/custom-html have 30+
# runs) stays bounded; newest runs are kept (the list is sorted newest-first before the slice).
HISTORY_CAP = int(os.environ.get("HISTORY_CAP", "30"))
# Phase 3 (R3/R6/U2.3): per-run artifacts (results.json, summary card PNG, app screenshot, level
# badge) written by run_recipe_ci.py under this host dir, bind-mounted read-only into the dashboard
@ -51,9 +54,14 @@ def _read(path):
DRONE_TOKEN = _read(os.environ["DRONE_TOKEN_FILE"])
_CACHE = {"ts": 0.0, "recipes": []}
# Raw custom builds (newest-first), cached so the overview AND the per-recipe history page share one
# Drone fetch within CACHE_TTL (U4 history reads the same list latest_per_recipe groups from).
# Raw custom builds (newest-first), cached within CACHE_TTL. Feeds the OVERVIEW (latest-per-recipe).
# The per-recipe HISTORY page no longer reads this slice — it sources the full history from the local
# run artifacts instead (see _local_history / phase dash), because this Drone slice is capped at the
# latest 100 builds and drops a recipe's older runs out of view.
_BUILDS = {"ts": 0.0, "builds": []}
# Per-recipe history sourced from the LOCAL run artifacts under CCCI_RUNS_DIR (complete: 300+ runs,
# durable, independent of Drone's 100-build window). Whole-dir scan grouped by recipe, cached CACHE_TTL.
_LOCAL = {"ts": 0.0, "by_recipe": {}}
_COLORS = {
"success": "#3fb950",
@ -172,13 +180,80 @@ def latest_per_recipe():
return [_build_row(latest[r]) for r in sorted(latest)]
def _numeric_id(n):
"""run dir name as int for sort tiebreak; -1 for named ids (m2r-*, ab-*) so the PRIMARY sort key
(finished timestamp) decides their position, never int() on a non-numeric id (would crash)."""
try:
return int(n)
except (TypeError, ValueError):
return -1
def _run_status(res):
"""Overall pass/fail for a finished run, derived from its per-stage results map (results.json has
no single top-level status field). Any failed/errored stage → failure; all pass/skip → success;
empty/unknown → unknown. A skip alone is not a failure."""
vals = list((res.get("results") or {}).values())
if any(v in ("fail", "error") for v in vals):
return "failure"
if vals and all(v in ("pass", "skip") for v in vals):
return "success"
return "unknown"
def _local_history_row(run_id, res):
"""Project a local run artifact (results.json) into the same display-row shape _build_row emits,
so render_history is unchanged. `number` is the run dir name (the /runs/<id>/ path + _results_for
key); link to the Drone build when the id is numeric, else to the local summary card."""
ref = res.get("ref") or ""
url = f"{DRONE_URL}/{CI_REPO}/{run_id}" if str(run_id).isdigit() else f"/runs/{run_id}/summary.html"
return {
"recipe": res.get("recipe"),
"status": _run_status(res),
"number": run_id,
"ref": ref[:8],
"version": res.get("version") or ref[:12] or "",
"level": res.get("level"),
"has_screenshot": bool(res.get("screenshot")),
"flags": res.get("flags") or {},
"finished": res.get("finished") or 0,
"url": url,
}
def _local_history():
"""Scan CCCI_RUNS_DIR once (cached CACHE_TTL), group runs by recipe sorted newest-first by the
`finished` timestamp. Run dirs with no/malformed results.json (in-flight / failed-early) are
skipped via _results_for ({} on miss) — never raises, never emits a garbage row. {recipe: [row]}."""
now = time.time()
if now - _LOCAL["ts"] <= CACHE_TTL and _LOCAL["by_recipe"]:
return _LOCAL["by_recipe"]
by_recipe = {}
try:
names = os.listdir(CCCI_RUNS_DIR)
except OSError as e:
log("local runs scan failed", e)
return _LOCAL["by_recipe"]
for name in names:
res = _results_for(name) # traversal-guarded read; {} on miss / malformed / non-dir
recipe = res.get("recipe")
if not recipe:
continue
by_recipe.setdefault(recipe, []).append(_local_history_row(name, res))
# Sort newest-first by finished timestamp (ids are MIXED numeric + named, so a numeric/lexical id
# sort would misorder — timestamp is the only correct key); numeric id is a stable tiebreak only.
for rows in by_recipe.values():
rows.sort(key=lambda r: (r["finished"], _numeric_id(r["number"])), reverse=True)
_LOCAL["by_recipe"] = by_recipe
_LOCAL["ts"] = now
return by_recipe
def history_for(recipe):
"""All runs for one recipe (newest first), augmented from results.json — the per-recipe history
page (R5 'link to history'). [] if none / None on fetch error."""
builds = _custom_recipe_builds()
if builds is None:
return None
return [_build_row(b) for b in builds if (b.get("params") or {}).get("RECIPE") == recipe]
"""All runs for one recipe (newest first, display-capped at HISTORY_CAP), sourced from the LOCAL
run artifacts under CCCI_RUNS_DIR — complete + durable, independent of Drone's 100-build window
(phase dash root cause). [] when the recipe has no local runs."""
return _local_history().get(recipe, [])[:HISTORY_CAP]
def recipes_cached():