# CI and supply-chain release gates GeoIntel uses the same release gates in Gitea Actions and GitHub Actions: - `.gitea/workflows/release-gates.yml` - `.github/workflows/release-gates.yml` Gitea is the operational source-control platform. Its release workflow builds the production AI variant and is the only workflow that can automatically deploy. The GitHub mirror builds and scans the same AI variant for external review but is not a production deployment authority. ## Runner requirements The `ubuntu-latest` runner must provide: - outbound HTTPS access to PyPI, npm, Docker Hub and vulnerability databases; - Python 3.11 and Node 20 through the official setup actions; - Bash and Docker with Compose v2; - permission to build images and mount `/var/run/docker.sock`; - sufficient disk for the all-in-one AI image and scanner databases. Both container jobs build the explicit AI image, including the PyTorch/Ultralytics layers used on Tower. AI packages remain outside the standard backend lock and are pinned by Docker build arguments. ## Quality gate The quality job installs: ```bash python -m pip install --require-hashes -r backend/requirements-ci.lock python -m pip install --no-deps -e backend cd frontend && npm ci ``` It then validates the lock policy and runs the complete readiness script. The readiness script covers Ruff, repository-layout validation, backend compile/tests, contract audits, Alembic single-head, frontend typecheck/build and release-script syntax. CI also renders offline migration SQL and resolved Compose configuration as retained evidence. Pull requests run managed validation against the real `backend/` and `frontend/` projects plus the complete release gates. On a `main` push, the Unraid deploy job has explicit `needs` dependencies on quality, dependency and AI-container jobs. There is no separate deployment workflow: manual validation uses `workflow_dispatch` on this same release-gates workflow and cannot skip quality, dependency or AI-container jobs. The deploy host requires a full controller commit SHA, builds the AI variant, records its Docker image/config digest, generates SBOM and Trivy evidence for that exact local image, and starts the immutable image ID. Deployment fails if the running container ID, revision label or AI label differs from the retained attestation. ## Reproducible Python lock `backend/requirements-runtime.lock` and `backend/requirements-ci.lock` are generated in a digest-pinned Linux Python 3.11 container. The runtime lock contains base and GIS packages used by the release image. The CI lock adds developer/test dependencies. Both use package hashes and deliberately exclude the optional AI dependency group. Regenerate after changing relevant `pyproject.toml` dependencies: ```bash bash scripts/generate_python_lock.sh python scripts/verify_python_lock.py ``` The all-in-one frontend build uses `npm ci`; the non-AI runtime installs the hashed runtime lock. The optional AI image additionally uses explicit PyTorch, torchvision and Ultralytics build-argument versions. Do not hand-edit dependency versions or hashes in either generated lock. `verify_python_lock.py` rejects a stale input fingerprint, another Python generation version, unhashed packages, missing direct dependencies and AI packages leaking into the standard CI environment. ## Vulnerability policy The dependency job: - fails on any non-excepted vulnerability reported by `pip-audit` for the complete exact lock without platform-specific re-resolution; - fails when `npm audit --audit-level=high` finds a high or critical frontend dependency vulnerability; - publishes both unfiltered and policy-filtered Python JSON reports plus the npm JSON report, including on failure. There are currently no Python or container vulnerability exceptions. `security/pip-audit-exceptions.json` remains as a strict, machine-readable registry: every future exception must identify one advisory, package, specific reason and expiry date. Readiness and CI fail on malformed or expired entries; new advisories are never auto-ignored. GeoIntel requires Starlette 1.3.1 or newer and therefore no longer suppresses the five 2026 Starlette advisories. The all-in-one image replaces the Go-based base-image `gosu` helper with a small `setpriv` exec wrapper and upgrades packaged setuptools/wheel metadata; the final runtime filesystem no longer exposes the vulnerable Go executable. Because OCI layers are immutable, Trivy still records the shadowed Go binary from the official Postgres base layer in the complete JSON report. The release-policy pass skips only `/usr/local/bin/gosu` after the final image is verified to contain the audited shell wrapper. This is not a vulnerability exception: the raw evidence remains published and the runtime wrapper is exercised during live release validation. The operational container job builds the production AI all-in-one image and uses digest-pinned scanner images: - Syft 1.44.0 generates an SPDX JSON SBOM; - Trivy 0.70.0 generates a complete JSON vulnerability report; - fixed high or critical image vulnerabilities fail the gate; - unfixed findings remain in the full report and require explicit release review, but do not make a rebuild impossible when no patched package exists. Run these controls on a Docker-enabled workstation: ```bash docker build \ -f deploy/unraid/Dockerfile.all-in-one \ --build-arg GEOINTEL_INSTALL_AI=true \ --build-arg GEOINTEL_BUILD_SHA=local \ --build-arg GEOINTEL_BUILD_TIME=local \ -t geointel-ci:local-ai . bash scripts/generate_container_sbom.sh geointel-ci:local-ai bash scripts/scan_container_image.sh geointel-ci:local-ai ``` Outputs are written below ignored `artifacts/`; scanner cache is written below ignored `.cache/trivy/`. ## Published evidence Every workflow run retains: - offline Alembic upgrade SQL; - resolved Docker Compose configuration; - pip-audit and npm-audit JSON; - image inspection metadata; - SPDX JSON SBOM; - complete Trivy JSON report. No secret or plaintext database credential belongs in these artefacts.