CI/CD Spatial Quality Gates
CI/CD Spatial Quality Gates is the engineering discipline that turns spatial validation from a manual review step into an automated, blocking part of the deployment pipeline. It takes the predicates, tolerance policies and fixtures defined across geospatial QA fundamentals and architecture and gives them a place to run on every push: a set of gates that a change must pass before it can merge or ship. Where the fundamentals answer what to assert and spatial test pattern design answers how to write the checks, this body of work answers where and when they execute — which stages run pre-merge, which run nightly, how the runtime is pinned so results are reproducible across machines, and how outcomes become observable metrics rather than console noise. A gate is only trustworthy when the same geometry, the same GEOS build and the same PROJ grid produce the same verdict on a laptop and on a runner; everything here exists to guarantee that.
What This Discipline Covers
Continuous integration for spatial data differs from ordinary CI in three ways that make a naïve pytest step unreliable. First, the answer depends on the binary stack — a self-intersection repair or a datum transform can shift by metres between two GEOS or PROJ builds, so an unpinned runtime produces flaky gates. Second, the fixtures are large and slow, so cost-splitting the suite across pre-merge and scheduled tiers is not optional. Third, the tools that install the spatial stack (system GDAL, wheels, conda) each fail differently in a container, so runtime reproducibility is its own engineering problem. This section covers the concrete platforms teams gate on — the GitHub Actions spatial testing workflow, the GitLab CI spatial gates stages, and containerized GIS test runtimes — plus the decision framework for choosing spatial testing tools when more than one library can enforce the same rule.
Gate Architecture
A spatial quality gate is a deployment control point: a stage that reads a candidate change, runs a bounded set of deterministic checks, and returns a pass/fail that the platform enforces on the merge or release. The architecture that keeps gates both fast and thorough splits checks by cost and pins the runtime that evaluates them. Lightweight, exact checks — schema, CRS declaration, geometry validity — run on every push and must finish in well under a minute so they never become the reason a pull request stalls. Expensive, fixture-heavy checks — full topology audits, spatial joins, CRS round-trip drift — run on a schedule against the larger generated collections, where a multi-minute runtime is acceptable.
The container boundary in that diagram is load-bearing. Both lanes run inside the same pinned runtime so that a topology verdict emitted by the nightly lane is reproducible by the pre-merge lane and by an engineer debugging locally. This is why the discipline treats the runtime image as a versioned artifact in its own right, rebuilt deliberately rather than floating on whatever the base image resolves to at build time — the mechanics of which are the subject of the container runtime work below.
Pre-Merge Gates on GitHub Actions and GitLab CI
The first stage that a change hits is the pre-merge gate, and its whole value is speed with exactness: it must catch the cheap, unambiguous failures — a wrong geometry type, a missing CRS, an invalid ring — before a human reviewer spends attention on the diff. On GitHub, this is a required status check wired through GitHub Actions spatial testing, where the practical guide to gating pull requests with pytest-geo in GitHub Actions shows how to fail the merge on a failing spatial assertion. On GitLab, the equivalent is a pipeline stage described under GitLab CI spatial gates, where configuring GitLab CI spatial validation stages covers ordering validate → test → gate so a schema failure short-circuits before any expensive spatial maths runs.
The recurring cost problem on both platforms is installing the spatial stack quickly. GDAL and PROJ wheels are large and their transitive binary dependencies are slow to resolve, so an uncached install can dominate a job that should take seconds — the caching strategy in caching GDAL/PROJ wheels in GitHub Actions is what keeps the fast lane fast.
| Gate tier | Trigger | Typical budget | Checks it runs |
|---|---|---|---|
| Pre-merge | Every push / PR | < 60 s | Schema, CRS declaration, geometry validity, small-fixture topology |
| Merge queue | On merge to main | 1–3 min | Cross-format parity, attribute contracts, index correctness |
| Nightly | Scheduled | 5–30 min | Full topology audit, spatial joins, CRS round-trip drift, raster alignment |
| Release | Tag / deploy | Variable | Service-level fixtures, regression baselines against production sample |
Containerized, Version-Pinned Runtimes
The reason the same test can pass locally and fail in CI almost always traces back to the binary geometry stack. Shapely and GeoPandas call into GEOS; pyproj calls into PROJ; fiona and pyogrio call into GDAL. Each of those C libraries has its own version and, in PROJ’s case, a separately versioned datum-grid database. A make_valid result, a buffer’s vertex count, or a datum shift can differ across builds, so a gate that does not pin the stack is not deterministic. The containerized GIS test runtimes work treats the image as the unit of reproducibility: pinning GDAL/PROJ versions in Docker test images fixes the exact library and grid versions, and reproducible conda environments for spatial CI does the same for teams that resolve the stack through conda-forge rather than system packages.
Version pinning has a numeric consequence worth stating explicitly. If a transform’s ground error is
Recording the engine versions in every gate outcome is what lets a team answer “did an engine change?” in one query instead of a bisect. The version fields belong in the structured log alongside the measured drift.
Choosing the Right Tool for a Gate
More than one library can often enforce the same spatial rule, and the right choice at gate-writing time depends on where the data already lives and how expensive the check is. A topology rule can be evaluated in-process with Shapely or pushed to the database with PostGIS; a schema-and-domain contract can be expressed as a pytest assertion or as a Great Expectations suite; a spatial join in a test can be backed by an in-memory R-tree or a database GiST index. The choosing spatial testing tools decision work compares the realistic options head to head: pytest-geo vs Great Expectations for spatial validation, Shapely vs PostGIS for in-pipeline topology checks, and R-tree vs GiST index performance in test environments.
Fixtures and Determinism in the Pipeline
A gate is reproducible only if its inputs are. Spatial fixtures must be content-addressed — hash the serialized geometry, CRS and attribute table — so a regression test provably runs against identical state across CI runs, and so a change to a fixture is a reviewable event rather than a silent shift. The generated collections that feed the nightly tier come from the test data generation and mocking strategies work; the edge-case set that the fast lane consumes should include at least one fixture per known failure class — anti-meridian crossings, polar features, degenerate and empty geometries, mixed Z/M coordinates. Keeping fixtures small for the pre-merge tier and large for the scheduled tier is what lets the two lanes share assertions while meeting different time budgets, and the async execution for large datasets patterns make the large-fixture tier tractable.
Observability: Drift as a Service-Level Objective
A passing suite is not the same as a trustworthy one. The gate should export its outcomes as metrics — counters for pass/fail by stage, histograms of measured drift against the tolerance budget — so that spatial accuracy becomes a service-level objective rather than a binary build status. Pair the metrics with a structured log schema so any failure is reproducible from a single log line.
| Field | Example | Purpose |
|---|---|---|
gate |
pre-merge |
Which gate tier emitted the event |
check |
topology.no_overlaps |
The specific assertion that ran |
geometry_hash |
sha256:9f2a… |
Content address of the input fixture |
tolerance / measured |
0.01 m / 0.004 m |
Budget enforced and value observed |
gdal / geos / proj |
3.8.4 / 3.12.1 / 9.4.0 |
Pinned engine versions |
duration_ms |
840 |
Runtime, to keep the fast lane honest |
The engine-version fields are not optional: when a nightly gate regresses, they answer the first question — did the stack change — without a re-run.
Budgeting the Pre-Merge Gate
A gate has a time budget whether or not anyone writes it down, and the budget is set by human behaviour rather than by engineering. Below roughly five minutes, engineers wait for the result and act on it. Between five and fifteen they context-switch and come back. Past fifteen they open the next task, and the gate stops being a feedback mechanism and becomes an obstacle discovered later.
Designing to that budget means deciding, explicitly, what does not run pre-merge. The allocation below is a workable starting point for a spatial pipeline, and its shape matters more than its exact numbers: the fast, cheap, high-yield checks get the bulk of the coverage, and the expensive ones are represented by a sample rather than omitted entirely.
| Stage | Budget | What runs | What is deferred to nightly |
|---|---|---|---|
| Container start | ~30 s | Pinned image, warm layer cache | Image rebuild |
| Dependency restore | ~20 s | Cached wheels keyed on a lockfile hash | Fresh resolution |
| Schema and CRS contract | ~15 s | Every feature, every file | — |
| Geometry validity | ~60 s | Every feature | — |
| Topology rules | ~90 s | One coherent spatial subset | Full-coverage dissolve |
| Cross-format parity | ~45 s | Primary output format only | All output formats |
| Service-level checks | — | Nothing | Tiles, routing, joins at volume |
Two decisions inside that budget deserve to be made deliberately. Cache what is deterministic, rebuild what is not — the wheel cache keyed on a lockfile hash is safe because the key changes when the content does, whereas caching a built fixture without hashing its generator is how a stale artefact survives for weeks. And fail fast on the cheap stages: ordering the gate so that a CRS mismatch fails in fifteen seconds rather than after the validity pass turns a large class of red builds from three minutes into fifteen seconds.
Where a Gate Should Stop the Line
Not every failing check should block a merge, and treating them all identically is the fastest way to have the gate routed around. Three responses are available, and the choice should follow from who can act and how quickly.
Block the merge when the failure is caused by the change under review and the author can fix it: a schema contract broken by their migration, a geometry rule violated by their transformation, a parity check failing on the format they altered. This is the only category where blocking is both fair and effective.
Warn on the merge request when the failure is real but not attributable to the author — an upstream dataset drifted, a fixture aged, a threshold is marginal. A comment gives the information without stopping unrelated work, and it accumulates visibly enough that someone eventually fixes it.
Alert a team, off the merge path, when the failure belongs to a producer or to the platform. A merge queue is the wrong place to surface someone else’s data problem, and putting it there trains everyone to ignore red.
The test that decides between them is short: can the person who triggered this run fix it in this change? If yes, block. If no, do not — regardless of how serious the finding is. Severity determines who gets alerted and how loudly; attributability determines whether the merge stops.
Security and Governance at the Gate
The gate is also a trust boundary. A pull request can carry a hostile fixture — a crafted WKT/WKB payload designed to exploit the parser or, when concatenated into SQL, to inject — so the runner must treat fixture geometry as untrusted input. The defensive parsing patterns live under security boundaries in spatial QA, and the gate must never echo raw coordinates into its logs, since a single point can re-identify an individual. Governance also decides which gates a change must pass: a documentation change need not run the full nightly spatial audit, while a change to a transform pipeline must. Matching gate depth to what actually changed keeps CI from becoming the bottleneck teams route around, following the same classification logic as scoping rules for map data validation.
Gating Data Changes, Not Only Code Changes
Most CI thinking assumes the artefact under review is a code change. In a spatial platform a large share of incidents come from data changes that no pull request accompanied — a supplier delivered a new extract, a reference layer was updated, a boundary set was re-issued. A gate wired only to code merges is blind to all of it.
The fix is to treat an incoming dataset as a reviewable artefact with its own gate. The delivery lands in a staging location, the same contract and validity checks run against it, and promotion to the serving location happens only on a pass. The mechanics are identical to a merge gate; what changes is the trigger and who receives the failure.
| Property | Code gate | Data gate |
|---|---|---|
| Trigger | A merge request | A delivery landing in staging |
| Artefact under review | A diff | A dataset version |
| Checks | Contract, validity, topology, parity | The same checks, unchanged |
| Failure goes to | The author | The producing team |
| On pass | Merge | Promote to serving |
| On fail | Block the merge | Keep serving the previous version |
The last row is the one that changes operational life most. A data gate that quarantines a bad delivery and keeps yesterday’s version serving converts what would have been an outage into a delayed update — and the delay is visible, attributable, and fixable by the team that caused it. Without the gate, the same bad delivery reaches consumers and is discovered by them.
Running both gates from the same check definitions is what keeps this affordable. The rules do not care whether they were triggered by a diff or by an arrival, so a single implementation, invoked from two triggers, gives complete coverage of the two ways a spatial platform actually changes.
Failure Attribution: Making Red Mean Something
The most expensive property of a badly-designed gate is not slowness; it is ambiguity. A red build that could mean five different things costs an engineer the same investigation every time, and after enough repetitions the investigation stops happening. Attribution is therefore a design goal, not a reporting nicety.
Four categories cover essentially every red build a spatial gate produces, and each should be distinguishable from the job summary alone, without opening a log.
The change broke a contract. A schema, CRS, or geometry rule failed on data the change produced. The report should name the rule, the feature identifier, the measured value and the threshold — enough to act on without reproducing locally.
The data drifted. The same code now fails against different input. Distinguishing this from the first case requires knowing whether the input changed, which is why recording the input’s content hash in the run is worth the small effort: two runs with the same code and different data hashes make the answer immediate.
The environment moved. An engine version, a grid package, a base image. These should be nearly impossible if the runtime is pinned, and when they do happen the recorded version fields identify them in seconds.
Infrastructure failed. A registry timeout, an unreachable bucket, a runner that ran out of disk. Nothing to do with the data at all, and reporting it in the same colour as a validation failure is what teaches people to re-run first and read second.
| Category | Distinguishing evidence | Right first action |
|---|---|---|
| Contract broken by the change | Rule name, feature id, value versus threshold | Read the diff |
| Data drifted | Input content hash differs, code unchanged | Look upstream |
| Environment moved | Engine version fields differ between runs | Compare the image |
| Infrastructure failed | Distinct exit code, no validation output | Re-run, then escalate |
The practical implementation is unglamorous and effective: emit a structured summary at the end of every run carrying the code revision, the input hash, the engine versions, and a category for the failure. Everything above then falls out of one line, and the habit of re-running a red build to see if it goes away — which is where most wasted CI time actually goes — stops paying off.
Frequently Asked Questions
Should the gate run against the merge commit or the branch head?
The merge commit, always. Testing the branch head answers “did this branch work in isolation”, which is not the question a merge gate exists to answer — two branches that each pass can produce a broken result together, and spatial pipelines are unusually prone to it because a schema change in one and a transformation change in the other interact through data rather than through code. Most CI systems can produce the merge result; using it costs nothing and removes an entire class of post-merge surprise.
How do we stop the gate from being skipped under deadline pressure?
By making the skip visible rather than by trying to prevent it. A documented override that records who used it, when, and why is far healthier than an unbreakable gate, because an unbreakable gate that blocks a genuine emergency will be removed entirely and never come back. Reviewing the override log monthly usually reveals one or two checks that are too slow or too noisy, which is exactly the feedback the gate needs.
What belongs in the container image versus in the job?
Anything version-pinned and slow belongs in the image: GDAL, PROJ, GEOS, the grid packages, the database client. Anything that changes per commit belongs in the job. The dividing line is how often it changes — an image rebuilt weekly and a job that installs a lockfile in twenty seconds is a good arrangement; an image rebuilt on every push is a slow arrangement with none of the benefits.
Is it worth running the gate on the default branch as well as on merge requests?
Yes, on a schedule rather than on every push. The scheduled run is what catches drift that no change caused — a base image that moved, an upstream dataset that changed, a certificate that expired — and it distinguishes “our change broke this” from “this was already broken”. Without it, the first failure after a quiet week is always ambiguous.
How should a gate handle a dependency it cannot reach?
Fail with a distinguishable exit, and do not retry silently. An unreachable object store or a missing grid package is an infrastructure fault, not a data fault, and reporting it as a validation failure sends an engineer to look at their geometry. Separating the two in the report — infrastructure red versus data red — is a small change that saves a great deal of misdirected investigation.
Conclusion
CI/CD spatial quality gates are what make spatial validation continuous: deterministic checks, split by cost across pre-merge and scheduled tiers, executed inside a version-pinned runtime, and instrumented so drift is measured rather than merely detected. Wired this way, a spatial regression is caught at the gate that owns it, traced to an engine or fixture change from a single log line, and prevented from ever reaching the map a user sees.
Related
- GitHub Actions Spatial Testing — required status checks that fail the merge on a failing spatial assertion.
- Spatial Test Observability and Metrics — spans, drift objectives, and flake statistics from the gate itself.
- Performance Benchmarking Spatial Suites — runtime budgets and predicate profiling for the suite the gate runs.
- GitLab CI Spatial Gates — ordered pipeline stages for validate, test and gate.
- Containerized GIS Test Runtimes — pinning GDAL, GEOS and PROJ so gates are reproducible.
- Choosing Spatial Testing Tools — head-to-head decisions between libraries that enforce the same rule.
- Geospatial QA Fundamentals & Architecture — the predicates and tolerance policy these gates enforce.
- Async Execution for Large Datasets — making the nightly, large-fixture tier tractable.