22"""
33Inspect all notebook outputs under static/ and report failures.
44
5- Behaviour:
5+ Behaviour (always exits 0 — this is a diagnostic step, not a gate) :
66- Writes a Markdown summary table to $GITHUB_STEP_SUMMARY (if set) listing
7- every failed notebook per package/version with its truncated error.
8- - Emits ::warning:: log lines so GitHub annotates each failed notebook
9- visibly on the Actions run page.
10- - Exits with code 1 iff the *latest* version of any package has at least one
11- failed notebook output. Latest is determined from the package manifest's
12- `latestTag`. Historical versions can fail without blocking deployment.
7+ every failed notebook per package/version with the full captured error.
8+ - Emits ::warning:: log lines so GitHub annotates each failed notebook on the
9+ Actions run page (annotation text is truncated for legibility).
10+
11+ Workflow gating: build.py exits non-zero when a build crashes outright;
12+ per-notebook execution failures are deliberately not blocking because a
13+ single broken example shouldn't withhold the other healthy versions from
14+ being deployed.
1315
1416Usage (called from CI):
1517 python scripts/check-notebook-health.py
@@ -53,7 +55,11 @@ def _strip_ansi(s: str) -> str:
5355
5456
5557def collect_failures () -> dict [tuple [str , str ], list [tuple [str , str ]]]:
56- """Return {(package, tag): [(notebook_stem, error_message), ...]} for failed outputs."""
58+ """Return {(package, tag): [(notebook_stem, full_error), ...]} for failed outputs.
59+
60+ The full error string is preserved; annotation formatting decides on its own
61+ how much to surface.
62+ """
5763 failures : dict [tuple [str , str ], list [tuple [str , str ]]] = {}
5864
5965 for pkg_dir in sorted (p for p in STATIC_DIR .iterdir () if p .is_dir ()):
@@ -70,8 +76,6 @@ def collect_failures() -> dict[tuple[str, str], list[tuple[str, str]]]:
7076 if data is None or data .get ("success" ) is not False :
7177 continue
7278 error = _strip_ansi (str (data .get ("error" , "unknown error" ))).strip ()
73- if len (error ) > 200 :
74- error = error [:197 ] + "..."
7579 failures .setdefault ((pkg_dir .name , version_dir .name ), []).append (
7680 (output_file .stem , error )
7781 )
@@ -102,37 +106,43 @@ def main() -> int:
102106 Path (summary_path ).write_text ("### Notebook health\n \n " + msg )
103107 return 0
104108
105- # GitHub annotations — appear inline on the Actions run page .
109+ # GitHub annotations — single-line, so we strip newlines and trim hard .
106110 for (pkg , tag ), items in failures .items ():
107111 for stem , err in items :
108- print (f"::warning title=Notebook failure::{ pkg } /{ tag } /{ stem } : { err } " )
109-
110- # Step summary.
111- lines = ["### Notebook health\n " , f"\n { sum (len (v ) for v in failures .values ())} failed notebook output(s) across { len (failures )} version(s).\n " ]
112- lines .append ("\n | Package | Version | Latest? | Notebook | Error |\n " )
113- lines .append ("|---|---|---|---|---|\n " )
112+ tail = err .replace ("\n " , " ⏎ " )
113+ if len (tail ) > 240 :
114+ tail = tail [- 240 :] # tail of the traceback is where the real exception sits
115+ print (f"::warning title=Notebook failure::{ pkg } /{ tag } /{ stem } : ...{ tail } " )
116+
117+ # Step summary with the FULL error in a collapsible <details> per notebook
118+ # so the run page surfaces every traceback in readable form.
119+ n_failed = sum (len (v ) for v in failures .values ())
120+ lines : list [str ] = [
121+ "### Notebook health\n \n " ,
122+ f"{ n_failed } failed notebook output(s) across { len (failures )} version(s).\n \n " ,
123+ ]
114124 for (pkg , tag ), items in sorted (failures .items ()):
115- is_latest = "**yes**" if latest .get (pkg ) == tag else ""
125+ is_latest = " (latest)" if latest .get (pkg ) == tag else ""
126+ lines .append (f"#### `{ pkg } /{ tag } `{ is_latest } \n \n " )
116127 for stem , err in items :
117- # Escape pipe characters for Markdown tables.
118- err_md = err .replace ("|" , "\\ |" ).replace ("\n " , " " )
119- lines .append (f"| { pkg } | { tag } | { is_latest } | `{ stem } ` | { err_md } |\n " )
128+ lines .append (f"<details><summary><code>{ stem } </code></summary>\n \n " )
129+ lines .append ("```\n " )
130+ lines .append (err .rstrip ())
131+ lines .append ("\n ```\n \n </details>\n \n " )
120132
133+ output = "" .join (lines )
121134 if summary_path :
122- Path (summary_path ).write_text ("" . join ( lines ) )
135+ Path (summary_path ).write_text (output )
123136 else :
124- sys .stdout .write ("" . join ( lines ) )
137+ sys .stdout .write (output )
125138
126- # Hard-fail iff latest version of any package has failures.
127- blocking = sorted ({pkg for (pkg , tag ) in failures if latest .get (pkg ) == tag })
128- if blocking :
139+ latest_failing = sorted ({pkg for (pkg , tag ) in failures if latest .get (pkg ) == tag })
140+ if latest_failing :
129141 print (
130- f"\n Blocking: latest version of { ', ' .join (blocking )} has failed notebooks." ,
142+ f"\n Latest version of { ', ' .join (latest_failing )} has failed notebooks "
143+ "(diagnostic only — does not block deployment)." ,
131144 file = sys .stderr ,
132145 )
133- return 1
134-
135- print ("\n Failures only in historical versions — not blocking deployment." )
136146 return 0
137147
138148
0 commit comments