GitLab CI Spatial Gates
GitLab CI expresses a spatial quality gate as an ordered pipeline of stages, and that ordering is the whole point: a .gitlab-ci.yml lets you run cheap, fail-fast validation before expensive spatial computation ever starts, so a missing CRS or a malformed schema stops the pipeline in seconds rather than after a multi-minute topology audit. This pattern sits beneath CI/CD spatial quality gates and shows how to structure the stages, pin the GDAL/PROJ runtime through an image, gate merge requests with rules, and report geometry failures as JUnit so they render inline in the merge request. Where the GitHub Actions spatial testing model leans on a matrix and required status checks, GitLab leans on stages and pipeline rules — the underlying discipline is identical, but the primitives differ enough to warrant their own treatment.
The stages map onto the same cost tiers every spatial gate uses: validate is the fast, exact pre-merge tier; test is the moderate geometric tier; gate is where a merge-request rule turns the aggregate result into an enforced decision; report publishes the evidence. The sections below build each stage, and the full worked configuration lives in configuring GitLab CI spatial validation stages.
Stage and Rule Reference
| Stage | Purpose | Key GitLab primitive | Failure behaviour |
|---|---|---|---|
| validate | Schema, CRS declaration, geometry validity | needs: [], cheap image |
Blocks all later stages |
| test | Geometry, topology, parity assertions | rules:changes for spatial paths |
Blocks gate |
| gate | Merge-request decision | rules: if $CI_MERGE_REQUEST_IID |
Fails the pipeline |
| report | JUnit + artifacts | artifacts:reports:junit |
Non-blocking, always runs |
Pinning the Runtime with an Image
Every GitLab job runs in a container, which makes runtime pinning natural: set image: to an exact GDAL/PROJ tag rather than a floating one. This is the reproducibility boundary — the same image that runs the validate stage runs the test stage, so a topology verdict is consistent across the pipeline and reproducible by an engineer who pulls the same tag. Building that image reproducibly is covered under containerized GIS test runtimes.
# .gitlab-ci.yml
default:
image: registry.example.com/gis-test:gdal3.9.2-proj9.4.1 # pinned, never :latest
stages: [validate, test, gate, report]
cache:
key:
files: [pyproject.toml] # cache invalidates when deps change
paths: [.cache/pip]
The validate Stage: Fail Fast and Cheap
The validate stage exists to reject the unambiguous failures before any spatial maths runs. It declares needs: [] so it starts immediately, and it runs only the fast spatial assertion types — schema presence, CRS declaration, geometry validity — that finish in seconds.
validate:crs-and-schema:
stage: validate
needs: []
script:
- pytest -q -m "crs or schema" --maxfail=1
# tests/test_crs.py — the kind of exact check the validate stage owns
import geopandas as gpd, pytest
@pytest.mark.crs
def test_declared_srid_matches_contract():
gdf = gpd.read_file("data/parcels.gpkg")
assert gdf.crs is not None, "layer has no declared CRS"
assert gdf.crs.to_epsg() == 3857, f"unexpected SRID {gdf.crs.to_epsg()}"
The test Stage: Run Only When Spatial Paths Change
Spatial assertions are worth running only when spatial code or data changed, so scope the test stage with rules:changes. A change confined to documentation should not spend runner minutes on a topology audit — the same gate-depth-by-change logic as scoping rules for map data validation.
test:geometry:
stage: test
rules:
- if: $CI_MERGE_REQUEST_IID
changes: ["src/**/*.py", "data/**/*.gpkg", "tests/fixtures/**"]
script:
- pytest -q -m "geometry or topology" --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
The gate Stage: Enforce on the Merge Request
The gate stage turns results into an enforced decision. Restrict it to merge-request pipelines with a rules: if guard, and configure the project’s merge checks to require a successful pipeline so a red gate physically blocks the merge — GitLab’s equivalent of a required status check.
gate:spatial:
stage: gate
rules:
- if: $CI_MERGE_REQUEST_IID
script:
- echo "All spatial validation stages passed — merge permitted."
Reporting Geometry Failures as JUnit
The report stage publishes JUnit XML so failures render inline in the merge request’s Tests tab, and it uploads any serialized failing geometry as an artifact. As with any spatial gate, never write raw coordinates into the job log — attach them as a downloadable artifact, in line with the security boundaries around location data.
report:artifacts:
stage: report
when: always # publish evidence even when earlier stages failed
script: ["ls -la artifacts/ || true"]
artifacts:
when: always
paths: [artifacts/failed_geometry.geojson]
reports:
junit: report.xml
Common Failure Modes and Gotchas
- Floating
image:tags.gis-test:latestmakes the pipeline non-deterministic — a rebuilt base can change GEOS and flip a previously green gate. Pin the exact GDAL/PROJ tag. - Missing
needs: []on validate. Without it, the fast stage waits on the DAG and stops being fast feedback. - Unscoped test stage. A
testjob withoutrules:changesruns a full topology audit on documentation-only merge requests, wasting runner minutes. gaterunning on branch pipelines. Without anif: $CI_MERGE_REQUEST_IIDguard the gate runs where there is no merge request to gate, producing confusing red pipelines.- Cache keyed on the branch. Keying the pip cache on the branch instead of
pyproject.tomlre-installs the heavy GDAL/PROJ wheels on every unrelated change. - JUnit not marked
when: always. If the report artifact only uploads on success, the one run you most need evidence from — the failing one — has none.
Conclusion
A GitLab CI spatial gate is an ordered pipeline: a fast-failing validate stage, a change-scoped test stage, an enforced gate stage, and an always-on report stage — all inside a pinned GDAL/PROJ image. Structured this way, the pipeline gives seconds-fast feedback on the cheap failures, spends expensive spatial computation only when spatial code changed, and blocks the merge on a real regression with inline evidence. For the platform-neutral architecture behind these stages, return to CI/CD spatial quality gates.