Validate in CI
A style can fail in ways that have nothing to do with pixels: a dangling @ref,
a port type mismatch, a cycle, a blur sigma with no static bound, a brush URL
that 404s. ezu check runs exactly the validation the renderer runs, without
rendering, and exits non-zero on any of it.
It also reports the canvas padding the graph needs, which is worth reading even
when it passes: the renderer takes that or the style’s pad, whichever is
larger, so the line tells you which one you are getting — see
padding and neighbours.
ezu check style.json # parse + build graph + resolve every assetezu check style.json --no-fetch # parse + build graph only — offline, fastOn success it prints a one-line summary — style name, version, node count, source count. On failure it names the offending node.
A machine-readable report
Section titled “A machine-readable report”--json writes the same findings to stdout as a JSON object, with the log stream
moved to stderr so a pipe sees JSON alone:
ezu check style.json --json | jq .pad{ "name": "watercolor", "version": "1", "nodes": 18, "sources": 2, "pad": { "declared": 24, "needed": 12 }, "params": { "$schema": "...", "properties": { "softness": { "type": "number", "minimum": 0, "maximum": 4, "default": 0 } } }, "assets_resolved": true}params is the generated params schema
— byte-for-byte what the tile server serves at /style/params and what the wasm
renderer returns from paramsSchema.
That last field is the one worth a CI job of its own if a host generates its own params panel rather than reading the schema at runtime. Nothing in a style fails when it grows a param the panel has no control for: the style renders, the check passes, and the knob is simply absent from the UI. Diffing the schema is what turns that silence into a red build.
- name: Params schema is in step with the UI run: | ezu check styles/basemap.json --no-fetch --json \ | jq -S .params > /tmp/actual.json diff -u ui/params.schema.json /tmp/actual.jsonCommit the schema next to the panel that consumes it and the diff fails the run
that introduced the drift, naming the param. jq -S sorts keys so the comparison
does not depend on emission order.
In GitHub Actions
Section titled “In GitHub Actions”name: styleson: [push, pull_request]
jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: cargo install ezu-cli - name: Validate every style run: | for style in styles/*.json; do echo "== $style" ezu check "$style" doneUse --no-fetch if your CI runners have no network, or if remote assets are
flaky enough to make the job unreliable — you keep the structural checks and give
up only “does this URL resolve”.
As a pre-commit hook
Section titled “As a pre-commit hook”- repo: local hooks: - id: ezu-check name: ezu check entry: ezu check --no-fetch language: system files: ^styles/.*\.json$Editor-time validation
Section titled “Editor-time validation”Validation you get before committing is better still. Point your editor’s JSON language server at the schema:
ezu schema --out ezu-style.schema.json{ "json.schemas": [ { "fileMatch": ["styles/*.json"], "url": "./ezu-style.schema.json" } ]}The schema is assembled from the op registry, so it covers
custom ops you registered too. A hosted copy of the
built-in schema lives at
ezu-style.schema.json.
Translating in CI
Section titled “Translating in CI”ezu translate writes to stdout, so a converted style can be validated in one
line — useful as a canary when an upstream MapLibre style changes:
ezu translate https://example.com/style.json | ezu check /dev/stdin --no-fetchLayers the frontend skipped or approximated are reported on stderr. Fail the job on unexpected warnings if you want to be told when upstream drifts.
Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.