fix(ci): make the Mermaid fail-closed guard actually fire

The guard added earlier could never trigger. tools/render_mermaid.py has no
nonzero exit path — its own docstring said "Exits 0 even if some/all fail
(non-fatal)" — so `if ! render_mermaid.py ...; then exit 1; fi` never ran its
body, and a build that rendered zero diagrams still passed. Reproduced by
running the script with no Chrome on PATH: it printed the fallback warning and
exited 0.

Adds a --strict flag that exits 1 when Chrome is missing or any diagram fails
to render, and passes it from every workflow invocation. The default stays
lenient so local mobile-reader builds keep working, which is what the exit-0
behaviour was actually for.

Verified both directions: with no Chrome, --strict exits 1 and the default
exits 0; against the real book with Chrome, --strict renders every diagram and
exits 0.
This commit is contained in:
yeasy
2026-07-23 07:40:28 -07:00
parent 1c8f0dc14d
commit d705dfb631
2 changed files with 13 additions and 3 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ jobs:
run: | run: |
title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])') title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])')
PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH" \ PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH" \
python3 tools/render_mermaid.py --book-dir . --svg-out "$RUNNER_TEMP/mmsvg" python3 tools/render_mermaid.py --book-dir . --svg-out "$RUNNER_TEMP/mmsvg" --strict
python3 tools/build_html_reader.py \ python3 tools/build_html_reader.py \
--book-dir . \ --book-dir . \
--title "$title" \ --title "$title" \
+12 -2
View File
@@ -5,7 +5,8 @@ Extracts ```mermaid blocks in SUMMARY.md order and renders them with mermaid-cli
pointing puppeteer at a system Chrome (CHROME_BIN env or auto-detected). Chunked + pointing puppeteer at a system Chrome (CHROME_BIN env or auto-detected). Chunked +
retried because a single large mmdc pass can crash headless Chrome. Diagrams that retried because a single large mmdc pass can crash headless Chrome. Diagrams that
still fail are simply left out — build_mobile_book.py shows their source as fallback. still fail are simply left out — build_mobile_book.py shows their source as fallback.
Writes d-1.svg .. d-N.svg into --svg-out. Exits 0 even if some/all fail (non-fatal). Writes d-1.svg .. d-N.svg into --svg-out. Exits 0 even if some/all fail, so local
mobile-reader builds still work; pass --strict (CI does) to exit 1 instead.
""" """
import os, re, sys, glob, time, shutil, subprocess, argparse import os, re, sys, glob, time, shutil, subprocess, argparse
@@ -13,6 +14,8 @@ ap = argparse.ArgumentParser()
ap.add_argument("--book-dir", default=".") ap.add_argument("--book-dir", default=".")
ap.add_argument("--svg-out", required=True) ap.add_argument("--svg-out", required=True)
ap.add_argument("--chunk", type=int, default=25) ap.add_argument("--chunk", type=int, default=25)
ap.add_argument("--strict", action="store_true",
help="exit 1 if Chrome is missing or any diagram fails to render")
a = ap.parse_args() a = ap.parse_args()
BOOK, SVG = os.path.abspath(a.book_dir), os.path.abspath(a.svg_out) BOOK, SVG = os.path.abspath(a.book_dir), os.path.abspath(a.svg_out)
shutil.rmtree(SVG, ignore_errors=True); os.makedirs(SVG) shutil.rmtree(SVG, ignore_errors=True); os.makedirs(SVG)
@@ -39,7 +42,10 @@ if N == 0:
chrome = os.environ.get("CHROME_BIN") or next( chrome = os.environ.get("CHROME_BIN") or next(
(shutil.which(n) for n in ["google-chrome-stable","google-chrome","chromium-browser","chromium","chrome"] if shutil.which(n)), None) (shutil.which(n) for n in ["google-chrome-stable","google-chrome","chromium-browser","chromium","chrome"] if shutil.which(n)), None)
if not chrome: if not chrome:
print("WARNING: no Chrome found -> all diagrams will fall back to source"); sys.exit(0) msg = "no Chrome found -> all diagrams would fall back to source"
if a.strict:
print(f"Mermaid rendering failed: {msg}", file=sys.stderr); sys.exit(1)
print(f"WARNING: {msg}"); sys.exit(0)
print(f"using Chrome: {chrome}") print(f"using Chrome: {chrome}")
pptr = os.path.join(SVG, "_pptr.json") pptr = os.path.join(SVG, "_pptr.json")
open(pptr, "w").write('{"executablePath":"%s","args":["--no-sandbox","--disable-gpu","--disable-dev-shm-usage"]}' % chrome) open(pptr, "w").write('{"executablePath":"%s","args":["--no-sandbox","--disable-gpu","--disable-dev-shm-usage"]}' % chrome)
@@ -74,3 +80,7 @@ for att in range(4):
for f in glob.glob(os.path.join(SVG, "*.json")) + glob.glob(os.path.join(SVG, "_chunk.md")): for f in glob.glob(os.path.join(SVG, "*.json")) + glob.glob(os.path.join(SVG, "_chunk.md")):
os.remove(f) os.remove(f)
print(f"RENDERED {done()}/{N} diagrams") print(f"RENDERED {done()}/{N} diagrams")
if a.strict and done() < N:
missing = [i + 1 for i in range(N) if not os.path.isfile(os.path.join(SVG, f"d-{i+1}.svg"))]
print(f"Mermaid rendering failed for diagrams: {missing}", file=sys.stderr)
sys.exit(1)