Orchestrata Ultimate › Gates › validate-ci
Gate: validate-ci
At a glance
| Purpose | Refuse a .gitlab-ci.yml / docker/docker-compose.yml, or source feeding them, that breaks any of 14 hard requirements. |
| Where it runs | The scripts/gates.py battery (registered as validate-ci); scripts/health_check.py. |
| Trigger | A battery run whose fingerprint changed; a health check. |
| Exact command | python scripts/validate_ci.py |
| Blocks | Exit 1 on the first failed requirement (fail-fast, message on stderr); 0 with "OK: All 14 CI validation gates passed". |
| Ruling / ADR | Gate 4: RUL-001 — no xdist. Gates 11–12: RUL-003, RUL-011 — ports from env, no defaults and the user ruling of 2026-08-03. No ADR names this gate. |
| Last recorded verdict | ok: true in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local). wt-MAP-PRECOMMIT has no .cache/gates.json. |
What it refuses
A CI configuration (.gitlab-ci.yml, docker/docker-compose.yml) — or source
feeding it — that violates any of its 14 hard requirements: a test job bypassing the
canonical runner, pytest-xdist, hardcoded production IPs, stale AST artifacts, and
more.
Why it exists
Module docstring: "Deterministic CI configuration validator. Verifies .gitlab-ci.yml
and docker-compose.yml against hard requirements." Individual gates carry their own
recorded incidents — e.g. Gate 11's comment names "the exact shape of the bug a
2026-08 audit found in scripts/e2e_run.py and scripts/refactor.py
(os.environ.setdefault('REDIS_HOST', '10.53.1.10'), the PRODUCTION Redis)". The
gate's own caching has a recorded incident too (gates.py registry comment):
"Fingerprinting only _SRC meant editing .gitlab-ci.yml or Dockerfile.ci left the gate cached as 'unchanged since last PASS' and skipped -- the exact false-green the cache exists to prevent. Caught 2026-08-07 while changing both files."
(An earlier version of this page quoted that comment with docker-compose.yml in
place of Dockerfile.ci; the text above is the comment as it stands in
scripts/gates.py, read through the AST toolkit on 2026-10-23.)
The 14 gates (from the module's own section markers)
| # | Requirement |
|---|---|
| 1 | YAML parseable |
| 2 | Required stages present |
| 3 | No stale coverage path |
| 4 | pytest-xdist is BANNED ("deadlocks this suite ~78%") |
| 5 | Timeout present |
| 6 | Service containers or env vars for Redis/Neo4j |
| 7 | Architecture checker or mypy present |
| 8 | Docker Compose uses env vars, not hardcoded IPs |
| 9 | Test-execution jobs must invoke the canonical runner (scripts/run_tests.py) |
| 10 | AST outputs exist and are not stale |
| 11 | No hardcoded PRODUCTION 10.53.1.x connection literals in scripts/src |
| 12 | No literal fallback for REQUIRED infra-connection env keys (user ruling 2026-08-03) |
| 13 | Every env-profile overlay loader honours ONE ownership set |
| 14 | Env-key ownership claims must have a real declare_owned() |
Gate 9's matcher is deliberately precise: it fires on real test execution (pytest,
the threadpool runner, or the coverage target) "without also calling
scripts/run_tests.py", and deliberately NOT on a bare tests/ path — "lint/static
jobs (ruff/black/mypy over 'src/ tests/') legitimately mention that path without
executing the suite, and must not trip this gate."
Gate 11 is line-scanned with comment lines skipped, "so explanatory comments/docstrings referencing a retired or historical address (there are many, deliberately) do not trip this gate — only a literal actually wired up as a connection default."
How it runs
- Command:
python scripts/validate_ci.py - When it fires: the
scripts/gates.pybattery (registered asvalidate-ci);scripts/health_check.py. - Inputs: fingerprint roots
(*_SRC, ".gitlab-ci.yml", "docker/Dockerfile.ci"), where_SRC = ("src", "scripts", "tests", "pyproject.toml")(theGATESentry inscripts/gates.py, read through the AST toolkit on 2026-10-23). Thedocker/Dockerfile.ciroot dates from4d7533e35(2026-08-07). An earlier version of this page said a 2026-08-28 audit droppedpyproject.tomland replaceddocker/Dockerfile.ciwithdocker/docker-compose.yml; the registry carries neither change, and a pickaxe search fordocker/docker-compose.ymlinscripts/gates.pyhistory finds no commit. - The fingerprinted file and the read file differ.
main()inscripts/validate_ci.pyopensrepo_root / ".gitlab-ci.yml"andrepo_root / "docker" / "docker-compose.yml"; it does not readdocker/Dockerfile.ci. So after a cached PASS, an edit todocker/docker-compose.ymldoes not re-run the gate (derived from the code, not measured by a run). - Caching: the verdict is cached in
.cache/gates.jsonagainst_fingerprint(roots): a sha256 over the relative path and bytes of every file under the directory roots whose suffix is.py,.toml,.pl,.json,.mdor.sh(_interesting()), plus the file roots. There is no per-gate suffix mask: this page previously quoted one, andgit log --all -S GATE_MASKS -- scripts/gates.pyfinds no commit that ever contained aGATE_MASKStable (checked 2026-10-23).
Typical failures and how to fix them
- Exit codes: "Exit 0 = all gates pass. Exit 1 = first failure printed to stderr" — it fails fast on the first broken requirement.
- What a failure looks like (representative): one stderr line naming the gate
number, the file, and the offending content (e.g. the job that runs pytest without
the canonical runner, or the
10.53.1.xliteral with its line). - Un-blocking: fix the CI config or the offending source; for Gate 10, refresh the AST snapshot (ast).
- Gate 8 messages (from
main()): "docker-compose.yml still contains stale 10.222.9.x IPs", "contains hardcoded 10.53.1.x IPs (use ${REDIS_HOST} / ${NEO4J_URI})", "missing ${REDIS_HOST} env var reference", "missing ${NEO4J_URI} env var reference", or "docker-compose.yml not found". Replace the literal with the env reference. - One failure hides the next. The run stops at the first failed requirement, so fix and re-run until the "All 14" line prints.
- A compose edit that the battery reports as unchanged. Run the gate fresh:
python scripts/gates.py --only validate-ci --all, or the command directly.
Wiring
- Invokers:
scripts/gates.py(battery),scripts/health_check.py. - Tests:
tests/scripts/test_validate_ci.py.
Related
- Gates master index
- Development workflow
- CI pipeline — the jobs this gate validates
- RUL-001 — No xdist
- RUL-003 — Env config from the active env profile
- RUL-011 — Ports from env, no defaults
- env-contract and env-subnet-scheme — the neighbouring env-discipline gates (Gates 12–14 here are their CI-side cousins)
- prod-separation — the installed-units side of "never point at prod/dev by accident"