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.

Spatial CI/CD gate architecture: pinned runtime, two lanes, metrics sink A commit flows into a pinned runtime container that fixes GDAL, GEOS and PROJ versions. Within the container a fast pre-merge lane runs schema, CRS and validity checks and feeds a merge gate; a scheduled lane runs topology audits, spatial joins and CRS round-trip drift against larger fixtures. Both lanes emit structured outcomes to a metrics sink where measured drift becomes a service-level objective. Pinned runtime · GDAL / GEOS / PROJ fixed Commit Pre-merge · every push Schema · CRS validity · fast · exact Merge ✓ Scheduled · nightly Topology · joins large fixtures CRS round-trip drift vs budget Metrics sink Prometheus · OTel drift = SLO

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 εtransform\varepsilon_{\text{transform}} and your gate’s tolerance budget is τ\tau, an engine upgrade that changes the transform can push the observed error past the budget:

εobserved>τ    gate fails after an unpinned upgrade\varepsilon_{\text{observed}} > \tau \iff \text{gate fails after an unpinned upgrade}

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.

Choosing where a spatial check runs, by data location and rule type Starting from where the data lives, a check is placed server-side when the data is already in PostGIS, or in-process when it is held as GeoDataFrames. In-process checks then split: schema-and-domain contracts favour Great Expectations while geometry predicates favour pytest with Shapely. Where is the data? already in PostGIS in-process frames Server-side check PostGIS · GiST · ST_ predicates In-process check split by rule type Great Expectations pytest + Shapely contract predicate

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.

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.

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.