A GitHub Actions Workflow for GeoPackage Branch Validation
A GeoPackage that parses in a desktop GIS can still carry invalid geometry, a wrong CRS, or silent container corruption β so a feature branch that touches one should be validated automatically on every push, a practical extension of feature branching for GIS development teams.
Concept & Context
A GeoPackage is deceptively forgiving. Because it is a single SQLite file, it opens without complaint even when a self-intersecting polygon, a layer with a mislabelled coordinate reference system, or a half-written table lurks inside. Those defects surface later β during a spatial join that returns garbage, a merge that fails topology checks, or a tile build that crashes on a null geometry. On a shared feature branch, the cost of catching them late is multiplied across everyone who has already pulled the bad file.
Continuous validation moves the check to the moment of the push. Every time a contributor commits a change to a .gpkg, a workflow enumerates its layers, confirms each declares the CRS the project expects, tests every geometry for validity, and runs an integrity check on the underlying database. If anything fails, the branch goes red before the change spreads. This is the same discipline that best practices for branching GeoPackage projects recommend at the repository level, made mechanical.
Two layers of checking matter here. The spatial layer β driven by ogrinfo and OGR through Python β reasons about features, geometry types, and projections. The container layer β driven by SQLiteβs PRAGMA integrity_check β reasons about the file itself, catching truncation or corruption that the spatial abstraction happily reads past. A robust gate runs both, and slots naturally next to the broader CI/CD validation pipelines for spatial repositories a team already maintains.
Core Validation Pipeline
- Trigger on feature-branch push. Run the workflow on pushes to non-default branches, scoped to paths that include
**/*.gpkg, so it fires only when spatial data actually changes. - Provision the tooling. Install GDAL (
ogrinfo) andsqlite3on the runner so both the spatial and container checks are available. - Inventory layers and confirm CRS. Enumerate every layer, assert each carries the expected
EPSGcode, and record geometry type and feature count for the log. - Test geometry validity. Reject null, empty, or invalid geometries β self-intersections, unclosed rings β before they enter the branch.
- Verify container integrity. Run
PRAGMA integrity_checkon the SQLite database to catch a corrupt or truncated file that still parses as spatial data. - Fail on any error. Aggregate every finding and exit non-zero so a broken
GeoPackageblocks the pull request.
Working Implementation
The Python validator below opens the GeoPackage through OGR, walks every layer and feature, and separately runs the SQLite integrity check. It prints a per-layer summary and exits non-zero on the first category of failure. The YAML wires it to run on feature-branch pushes that touch a .gpkg.
# .github/workflows/gpkg-validate.yml
name: GeoPackage Branch Validation
on:
push:
branches-ignore: ["main", "master"]
paths: ["**/*.gpkg"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install GDAL and SQLite
run: |
sudo apt-get update
sudo apt-get install -y gdal-bin python3-gdal sqlite3
- name: Validate changed GeoPackages
run: |
for f in $(git diff --name-only HEAD~1 HEAD -- '**/*.gpkg'); do
echo "::group::Validating $f"
python tools/validate_gpkg.py "$f" --expect-epsg 4326
echo "::endgroup::"
done
#!/usr/bin/env python3
"""
Validate a GeoPackage for CI: layer inventory, CRS, geometry validity, integrity.
Usage:
python validate_gpkg.py data/roads.gpkg --expect-epsg 4326
Exit codes:
0 all layers valid and container intact
1 one or more checks failed
"""
import argparse
import sqlite3
import sys
from osgeo import ogr, osr
ogr.UseExceptions()
def check_integrity(path: str) -> list[str]:
"""Container-level check: PRAGMA integrity_check on the raw SQLite file."""
errors = []
try:
con = sqlite3.connect(path)
result = con.execute("PRAGMA integrity_check;").fetchone()[0]
if result != "ok":
errors.append(f"integrity_check: {result}")
con.close()
except sqlite3.DatabaseError as exc:
errors.append(f"not a readable SQLite/GeoPackage file: {exc}")
return errors
def check_layers(path: str, expect_epsg: int) -> list[str]:
"""Spatial checks: CRS match plus geometry validity across every feature."""
errors = []
ds = ogr.Open(path)
if ds is None:
return [f"OGR could not open {path}"]
for i in range(ds.GetLayerCount()):
layer = ds.GetLayer(i)
name = layer.GetName()
# CRS check β every spatial layer must declare the expected authority code.
srs = layer.GetSpatialRef()
if srs is None:
errors.append(f"[{name}] no CRS declared")
else:
srs.AutoIdentifyEPSG()
code = srs.GetAuthorityCode(None)
if code is None or int(code) != expect_epsg:
errors.append(f"[{name}] CRS is EPSG:{code}, expected EPSG:{expect_epsg}")
# Geometry validity β reject null, empty, and invalid features.
invalid = 0
for feat in layer:
geom = feat.GetGeometryRef()
if geom is None or geom.IsEmpty():
invalid += 1
elif not geom.IsValid():
invalid += 1
if invalid:
errors.append(f"[{name}] {invalid} null/empty/invalid geometries")
print(f"[{name}] {layer.GetFeatureCount()} features, "
f"type {ogr.GeometryTypeToName(layer.GetGeomType())}")
return errors
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("gpkg")
ap.add_argument("--expect-epsg", type=int, default=4326)
args = ap.parse_args()
errors = check_integrity(args.gpkg) + check_layers(args.gpkg, args.expect_epsg)
if errors:
print("\nVALIDATION FAILED:")
for e in errors:
print(f" - {e}")
return 1
print("\nVALIDATION PASSED")
return 0
if __name__ == "__main__":
sys.exit(main())
Validation & Output Verification
Confirm the workflow behaves before trusting it as a gate. Run the validator locally against a known-good file and expect a clean pass with a per-layer inventory:
python tools/validate_gpkg.py data/roads.gpkg --expect-epsg 4326
echo "exit=$?" # expect exit=0
Deliberately break a copy to prove each check bites. Reproject one layer to a different CRS and confirm the CRS check fails; the exit code must be non-zero:
ogr2ogr -t_srs EPSG:3857 broken.gpkg data/roads.gpkg roads
python tools/validate_gpkg.py broken.gpkg --expect-epsg 4326
echo "exit=$?" # expect exit=1, reporting EPSG:3857
To exercise the container check independently, run the SQLite pragma by hand β it should print exactly ok for a healthy file:
sqlite3 data/roads.gpkg "PRAGMA integrity_check;"
# expected output: ok
On GitHub, push to a feature branch and confirm the run appears in the Actions tab within a minute, then mark it a required status check in branch protection so a broken GeoPackage cannot merge even if a contributor ignores the red run. Pairing fast push feedback with a required check mirrors the guardrails in automated conflict detection in merge requests.
Failure Modes
- Workflow never triggers on a
.gpkgchange β thepathsfilter glob does not match the fileβs location, or the file is tracked by an LFS pointer that changed without the glob noticing. Fix: confirm the**/*.gpkgglob matches the real path and that the diff step compares against the correct base. - CRS check fails on a valid file β the layer stores a custom WKT that
AutoIdentifyEPSGcannot resolve to an authority code, so the code comes back null. Fix: normalize the CRS on write, or compare against the expected WKT directly instead of relying on EPSG resolution. - Geometry validity passes but downstream still breaks β OGRβs
IsValidaccepts geometries that a stricter engine rejects, and a valid but tiny sliver still causes trouble. Fix: add a targeted rule for the specific defect (minimum area, ring orientation) rather than relying onIsValidalone. PRAGMA integrity_checktimes out on a large file β the full page scan is slow on multi-gigabyte GeoPackages. Fix: switch toPRAGMA quick_checkfor routine pushes and reserve the fullintegrity_checkfor the pull-request gate.
Related
- Feature Branching for GIS Development Teams β parent guide on isolating spatial work on branches before it merges
- Best Practices for Branching GeoPackage Projects β repository conventions this workflow enforces automatically
- CI/CD Validation Pipelines for Spatial Repositories β the broader validation program this GeoPackage check belongs to