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.
Warnings, and making them fail
Section titled “Warnings, and making them fail”Some things a style does are legal but unsaid. A features, raster or dem
node with no source resolves to the document’s only source of that type — until
someone adds a second one, at which point the node stops building and the style’s
meaning was never written down anywhere. A field that reads a $param gets the
param’s declared default baked in at build time, so render-time overrides of that
param quietly do nothing.
Neither is an error, so ezu check reports them and still exits zero. --strict
flips that: any warning fails the run.
ezu check style.json --strict # warnings are errorsezu tile --style style.json --tile 3/4/5 --strictThe warnings are in the --json report as well, under warnings, so a job can
read them without parsing the log. ezu serve never refuses to start over a
warning — the live editor’s job is to show you the style you have, not to walk
out on it.
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, "warnings": ["node `water_f`: `source` is not set — falling back to the document's only `mvt`/`pmtiles`/`geojson` source, `basemap`. Name it: the style changes meaning the day a second one is declared."]}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 - name: Install ezu # A release binary, so the job needs no Rust toolchain and no # compile. Pin whichever version your styles are checked against. run: | v=v0.9.0; t=x86_64-unknown-linux-gnu curl -fsSL "https://github.com/reearth/ezu/releases/download/$v/ezu-$v-$t.tar.gz" | tar xz echo "$PWD/ezu-$v-$t" >> "$GITHUB_PATH" - 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.