Skip to content

Audit: mermaid diagrams linted in CI

What we check

A repository whose markdown contains any mermaid fence carries tools/mermaid-lint.sh and runs it from a CI workflow.

Mermaid fails at render time, not at commit time. A diagram with a syntax error commits cleanly, passes actionlint, shellcheck, flake8 and skillsaw alike, and then shows an error box on GitHub and nothing at all on the mkdocs sites. Nothing else in CI reads a diagram, so until this check existed a broken diagram was found by whoever next looked at the page.

Both halves are required. The script alone is a thing nobody runs, and a workflow step alone would mean each project inventing its own invocation of a container -- the point of shipping the wrapper is that the docker arguments, the entrypoint override and the exit-status handling are written once.

A diagram is recognised by a ```mermaid fence: backticks, with no space before the language. That is deliberately narrower than markdown allows, because it is what mmdc recognises -- a ~~~mermaid block renders nothing and exits zero, so counting one would mark a repository covered for a diagram its linter never sees. tools/mermaid-lint.sh greps for the same shape, and a test asserts the two definitions stay together.

Repositories with no mermaid diagrams are N/A. This is a check on diagrams that exist, not a requirement that every project have some; diagram-format is what moves a repository from hand-drawn diagrams into this check's scope.

Why a container

mmdc renders through puppeteer and so needs a browser. Running it from the upstream image keeps chromium and a node toolchain off the runners, and renders exactly what the sites will render.

There is no lighter path worth taking. mermaid's own parse() under plain node throws DOMPurify.addHook is not a function for flowchart and stateDiagram-v2, the two most common types in this fleet, so a DOM-free checker reports false failures on exactly the diagrams that matter; supplying a DOM with jsdom pulls in an undici newer than the runners' node.

The cost is smaller than it looks. The image is cached after its first pull, and rendering is about 1.4 seconds per file amortised inside a single container -- ryll's seven diagram-bearing files in ten seconds, ryll and kerbside together in twenty-two. Nearly all of the real cost is the virtual machine the job needs, which is why the shipped workflow is path-filtered to markdown.

The runner

[self-hosted, vm, debian-12-docker, s], not static. Static runners have no docker daemon. The label must also appear in .github/actionlint.yaml, or actionlint fails on the workflow.

Template

Template: templates/mermaid-lint/ See: templates/mermaid-lint/README.md

Both files copy directly with no per-project substitution. A project that already has a CI gate job may prefer to add the script as a step there instead of taking the shipped workflow; the template's README covers that, and why a path-filtered workflow must not simply be made a required status check.

Projects

Per-project compliance for this criterion is regenerated every morning by the consistency audit: see the compliance page.

📝 Report an issue with this page