fix(lint): provision the mermaid browser in install-lint, not in the parallel lint run

make lint runs install-lint once and then dispatches its fifteen targets
in parallel. scripts/lint/mermaid.sh provisioned chrome-headless-shell
inside that parallel phase with `npx puppeteer browsers install
--install-deps`, whose `apt-get satisfy` carries no lock timeout. In a
fresh stack container another target held the apt lock, the shared
libraries stayed missing, the script only warned, and all 289 Markdown
files then failed on libnspr4.so (#795).

utils/install/lint/mermaid_cli.py now provisions the browser right after
mmdc, inside the serial install-lint step, as root with --install-deps
and as a user without it, because puppeteer refuses to install libraries
without root. A failed install deletes the chrome-headless-shell cache
folder and retries once under a 900 second timeout per attempt; a cut-off
download otherwise leaves a folder without its executable, and puppeteer
refuses every later install of that build. The install-lint stamp counts
as fresh only while the browser executable exists, so a recreated
container provisions it again.

mermaid.sh no longer calls apt-get or npx. It renders one minimal diagram
first and, when that fails, exits with mmdc's output instead of reporting
every Markdown file.

Verified: tests/unit/python/scripts/lint/test_mermaid.py stubs mmdc,
npx and apt-get. With a browser that cannot load libnspr4.so the script
exits 1 with that line, before any Markdown file renders, and mmdc is the
only tool it calls. Against main's mermaid.sh the same test fails: it
calls apt-get and npx and reports the files one by one. The installer and
stamp cases pass, shellcheck is clean.

Not verified: a full make lint in a recreated stack container. The race
reproduced in one of two runs in the issue.

feature/store-i18n changed the provisioning in mermaid.sh with 2f5f52bd1a
and dc67bce5f0 (timeout, deleting a cut-off browser). Both behaviours
moved into mermaid_cli.py here, so its merge takes this version of
mermaid.sh.

Closes #795

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-13 21:31:36 +02:00
1 parent 789c1a1b63
commit c8cb7fda8c
7 files changed
+269 -34

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,62 @@
"""``scripts/lint/mermaid.sh`` renders with the browser install-lint provisioned.
``make lint`` runs its targets in parallel, so the script must not reach for a
package manager, and a browser that cannot start must fail the lint once with
its reason instead of once per Markdown file.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
from utils.cache.files import PROJECT_ROOT
SCRIPT = Path(PROJECT_ROOT) / "scripts" / "lint" / "mermaid.sh"
LIBRARY_ERROR = (
"chrome-headless-shell: error while loading shared libraries: libnspr4.so"
)
RECORDING_STUB = '#!/bin/sh\necho "$(basename "$0")" >> "$STUB_CALLS"\n'
class TestMermaidLint(unittest.TestCase):
def test_a_browser_that_cannot_start_fails_before_any_file_renders(self):
with tempfile.TemporaryDirectory() as tmp:
bin_dir = Path(tmp)
calls = bin_dir / "calls"
calls.touch()
for name in ("npx", "apt-get"):
(bin_dir / name).write_text(RECORDING_STUB)
(bin_dir / "mmdc").write_text(
f"{RECORDING_STUB}echo '{LIBRARY_ERROR}' >&2\nexit 1\n"
)
for stub in bin_dir.iterdir():
stub.chmod(0o755)
env = {
k: v
for k, v in os.environ.items()
if not k.startswith("INFINITO_") and k != "BASH_ENV"
}
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
env["STUB_CALLS"] = str(calls)
proc = subprocess.run(
["bash", str(SCRIPT)],
env=env,
capture_output=True,
text=True,
check=False,
)
recorded = calls.read_text().split()
self.assertEqual(proc.returncode, 1, proc.stderr)
self.assertIn("cannot render a minimal diagram", proc.stderr)
self.assertIn(LIBRARY_ERROR, proc.stderr)
self.assertNotIn("❌", proc.stdout)
self.assertEqual(recorded, ["mmdc"])
if __name__ == "__main__":
unittest.main()