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.

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.

Terminal window
ezu check style.json --strict # warnings are errors
ezu tile --style style.json --tile 3/4/5 --strict

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

--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,
"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.

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