Skip to content

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.

Terminal window
ezu check style.json # parse + build graph + resolve every asset
ezu check style.json --no-fetch # parse + build graph only — offline, fast

On success it prints a one-line summary — style name, version, node count, source count. On failure it names the offending node.

--json writes the same findings to stdout as a JSON object, with the log stream moved to stderr so a pipe sees JSON alone:

Terminal window
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.

.github/workflows/params.yml
- 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.json

Commit 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.

.github/workflows/styles.yml
name: styles
on: [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"
done

Use --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”.

.pre-commit-config.yaml
- repo: local
hooks:
- id: ezu-check
name: ezu check
entry: ezu check --no-fetch
language: system
files: ^styles/.*\.json$

Validation you get before committing is better still. Point your editor’s JSON language server at the schema:

Terminal window
ezu schema --out ezu-style.schema.json
.vscode/settings.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.

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:

Terminal window
ezu translate https://example.com/style.json | ezu check /dev/stdin --no-fetch

Layers 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.