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
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:
@ -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():
|
||||
|
||||
Reference in New Issue
Block a user