feat(lvl5): P1 — 5-rung ladder (L5=abra recipe lint) + de-capped level semantics
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
level.py: RUNGS += lint; statuses {pass,fail,skip,unver}; compute_level = max passed
rung with all below pass-or-skip (fail/unver block); cap_reason/capped DELETED.
harness/lint.py: lint executor — pristine scratch clone of the per-run tree at the
exact tested ref (mirror-origin + untracked-overlay pollution solved by context, no
rule filtered), PTY via script -qec, 60s hard budget, lint.txt artifact, table-parse
classifier (rc only signals FATA), unver on any non-run (never silent pass).
results.py: derive_rungs classifies every N/A source (structural/declared → skip,
else unver), lint rung + synthetic lint stage + lint block in results.json, schema 2,
cap fields removed. run_recipe_ci.py: lint call before tiers (double-wrapped,
verdict-neutral), badge = level only. card/dashboard: 0-5 ramp, cap line → 'level N
of {4|5}', unverified rows, badge number+colour only, lint.txt servable, old schema-1
artifacts render untouched. Unit suite rewritten: 245 passed on cc-ci venv.
This commit is contained in:
+33
-23
@@ -58,6 +58,9 @@ from harness import ( # noqa: E402
|
||||
from harness import ( # noqa: E402
|
||||
deps as deps_mod,
|
||||
)
|
||||
from harness import ( # noqa: E402
|
||||
lint as lint_mod,
|
||||
)
|
||||
from harness import ( # noqa: E402
|
||||
manifest as manifest_mod,
|
||||
)
|
||||
@@ -928,6 +931,24 @@ def main() -> int:
|
||||
run_artifact_dir = os.path.join(results_mod.runs_dir(), results_mod.run_id())
|
||||
junit_dir = os.path.join(run_artifact_dir, "junit")
|
||||
records: list[dict] = []
|
||||
|
||||
# L5 lint rung (phase lvl5): `abra recipe lint` against the EXACT tested ref, in a pristine
|
||||
# scratch clone (harness.lint — the per-run tree is still at head_ref here, before any
|
||||
# version-pinning checkout). Level rung only — NEVER the verdict: run_lint catches every
|
||||
# failure mode into status "unver" (60s hard budget) and this belt-and-braces wrap makes a
|
||||
# crashed executor identical to "could not verify".
|
||||
lint_result = {"status": "unver", "detail": "lint executor crashed", "rules_failed": []}
|
||||
try:
|
||||
lint_result = lint_mod.run_lint(recipe, head_ref, run_artifact_dir)
|
||||
except Exception as e: # noqa: BLE001 — lint is a rung, not a gate; never touches the verdict
|
||||
print(
|
||||
f"!! lint rung executor crashed (non-fatal, rung=unver): {_scrub(str(e))}", flush=True
|
||||
)
|
||||
print(
|
||||
f"lint rung: {lint_result['status']}"
|
||||
f"{' — ' + lint_result['detail'] if lint_result.get('detail') else ''}",
|
||||
flush=True,
|
||||
)
|
||||
with contextlib.suppress(OSError):
|
||||
os.makedirs(junit_dir, exist_ok=True)
|
||||
|
||||
@@ -1253,6 +1274,8 @@ def main() -> int:
|
||||
records=records,
|
||||
results=results,
|
||||
backup_capable=backup_cap,
|
||||
has_upgrade_target=prev is not None, # structural: a previous published version exists
|
||||
lint=lint_result, # L5 rung (phase lvl5)
|
||||
clean_teardown=clean_teardown,
|
||||
no_secret_leak=True, # narrowed below by an actual scan of the serialised artifact
|
||||
screenshot=screenshot_rel, # Phase 3 U1 (R4): relative PNG name iff capture succeeded
|
||||
@@ -1270,17 +1293,15 @@ def main() -> int:
|
||||
file=sys.stderr,
|
||||
)
|
||||
path = results_mod.write_results(data)
|
||||
print(
|
||||
f"results.json written: {path} (level={data['level']}"
|
||||
f"{' — ' + data['level_cap_reason'] if data['level_cap_reason'] else ''})",
|
||||
flush=True,
|
||||
)
|
||||
# Surface UNINTENTIONAL skips in the CI log (non-blocking, R7): a rung that was skipped (N/A)
|
||||
# but is not in the recipe's intentional list — either add the missing coverage or declare it.
|
||||
print(f"results.json written: {path} (level={data['level']} of 5)", flush=True)
|
||||
# Surface UNVERIFIED rungs in the CI log (non-blocking, R7): a rung that should have run
|
||||
# and wasn't verified blocks the level above it — fill the coverage, or (where a
|
||||
# declared/structural reason genuinely applies) declare it in EXPECTED_NA.
|
||||
for rung in data.get("skips", {}).get("unintentional", []):
|
||||
print(
|
||||
f"⚠ coverage: rung '{rung}' was skipped (N/A) but is not declared intentional — add "
|
||||
f"the missing test/label, or list it in tests/{recipe}/recipe_meta.py "
|
||||
f"⚠ coverage: rung '{rung}' is UNVERIFIED (did not run / could not be checked) — "
|
||||
f"the level cannot rise above it. Add the missing test/coverage, or declare a "
|
||||
f"genuine inapplicability in tests/{recipe}/recipe_meta.py "
|
||||
f"EXPECTED_NA = {{'{rung}': '<why>'}}.",
|
||||
flush=True,
|
||||
)
|
||||
@@ -1302,21 +1323,10 @@ def main() -> int:
|
||||
with open(html_path, "w", encoding="utf-8") as f:
|
||||
f.write(card_mod.render_card_html(data, screenshot_rel=data.get("screenshot")))
|
||||
png = card_mod.render_card_png(html_path, os.path.join(run_artifact_dir, "summary.png"))
|
||||
capped = data.get("level_cap_rung")
|
||||
sk = data.get("skips", {})
|
||||
cap_skip = (
|
||||
"intentional"
|
||||
if capped in (sk.get("intentional") or {})
|
||||
else "unintentional"
|
||||
if capped in (sk.get("unintentional") or [])
|
||||
else ""
|
||||
)
|
||||
# Badge = level only (number + colour) — the per-rung table on the card is the sole
|
||||
# carrier of "why isn't this higher" (operator-specified, phase lvl5).
|
||||
with open(os.path.join(run_artifact_dir, "badge.svg"), "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
card_mod.level_badge_svg(
|
||||
data["level"], data.get("level_cap_reason", ""), cap_skip
|
||||
)
|
||||
)
|
||||
f.write(card_mod.level_badge_svg(data["level"]))
|
||||
print(
|
||||
f"summary card {'rendered ' + png if png else '(PNG render unavailable)'} + "
|
||||
f"badge.svg written into {run_artifact_dir}",
|
||||
|
||||
Reference in New Issue
Block a user