diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index 8ba7988..11fadd0 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -974,10 +974,19 @@ def main() -> int: # returns None, so this never blocks or fails the run (R7). None → results.json `screenshot` # stays null → the card shows the "no screenshot" placeholder (cosmetics never change verdict). if deploy_ok: - shot = screenshot_mod.capture( - domain, screenshot_mod.screenshot_path(run_artifact_dir), recipe_meta=meta - ) - screenshot_rel = os.path.basename(shot) if shot else None + # capture() already swallows all errors → None; the extra try/except is defense-in-depth + # (U5 R7 hardening) so a screenshot can NEVER fail/crash the run even if that internal + # contract regresses or a recipe SCREENSHOT hook raises. Cosmetics never change the verdict. + try: + shot = screenshot_mod.capture( + domain, screenshot_mod.screenshot_path(run_artifact_dir), recipe_meta=meta + ) + screenshot_rel = os.path.basename(shot) if shot else None + except Exception as e: # noqa: BLE001 — screenshot is cosmetic; never fail a run on it (R7) + print( + f"!! screenshot capture raised (non-fatal, verdict unaffected): {_scrub(str(e))}", + flush=True, + ) # ---- INSTALL tier (always; additive generic + overlay, no op) ---- if "install" in stages: