--- date: 2026-11-08 branch: feat/gate-toolbox-container title: The merge gate runs inside the CI-built toolbox image, pinned by digest — no per-run tool install --- ## What changed `.gitea/workflows/gate.yml`, job `gate` (job name, the gate step, its env and its `|| [ "$?" -eq 3 ]` tolerance are unchanged, so the required context `gate / gate (pull_request)` is unchanged): - **`container:`** `gitea.home.keithjackson.dev/home-network/toolbox@sha256:83a3501e…ef81` — the image `images.yaml` published from `dd94101` (run 789; it is today's `main` and `sha-dd941016478c` tag). The same image `infra/devcontainer/dev.sh` runs, so local and CI gate on the identical toolchain. A `# renovate-digest: image=… tag=main` marker sits on the line above, the repo's existing convention; `infra/gitea/registry-retention.sh` reads the same marker, so this digest is protected from pruning. - **`actions/checkout` replaced by a plain-git step.** The toolbox has no Node (measured: `command -v node` → missing), and `actions/checkout` is a Node action. Chosen over adding Node to the toolbox because that would need two sequential PRs (Containerfile, then a publish, then the pin) and would put a JS runtime into the operator toolbox only to run one action. The step reproduces what checkout@v7 `fetch-depth: 0` actually ran here (read from runs 490 and 495): every branch to `refs/remotes/origin/*`, tags, plus the event SHA; detached for a PR, `-B ` for a push; asserts `HEAD == github.sha`. The job token goes to that one fetch via `GIT_CONFIG_*` env and is **not** persisted in `.git/config` (nothing later needs network git). - **"Install pinned CI toolchain" step removed**, replaced by **"Toolchain is the one ci/versions.env pins"**: every `*_VERSION` in `ci/versions.env` must equal the image's `/etc/dev-toolchain-versions`, else `::error::DRIFT` and the job fails. Without this, dropping the installer would have made `ci/versions.env` an unenforced claim for everything except promtool (which `alert-rules-unit-tested` already asserts). - Header comments rewritten for the new job image (pull auth, cold-pull size, no-Node, why `install-tools.sh` stays). `renovate.json5`: the `renovate-digest` custom manager now also scans `.gitea/workflows/gate.yml`; the `github-actions` manager is disabled for this repo's own images (same reason the `kubernetes` manager already was: it would see a digest-only image and look up `latest`). Comment/message wording only: `ci/gate.sh` (header + the strict-skip error text), `ci/lib.sh`, `ci/README.md`, `.gitea/workflows/README.md` — they said the CI toolchain comes from `install-tools.sh`. `.gitea/workflows/scripts/install-tools.sh` is **kept**: `merge-gate.yaml` (dormant blueprint) calls it, and `conformance/tests/l0-static/alert-rules-unit-tested/check.sh` asserts its promtool wiring (not touched — another agent owns `conformance/`). **Consequence to know:** every toolbox digest bump is now a Renovate PR touching `.gitea/**`, i.e. a sensitive path needing an operator `/approve-override`. That is arguably correct (a new toolchain changes what green means — and `ci/versions.env` was already sensitive), but it is a new recurring operator touch. ## What I verified - Registry pull auth: the Home-Network org is `public`; anonymous `/v2/token` → manifest `200`, `docker-content-digest: sha256:83a3501e…ef81` for both `main` and `sha-dd941016478c`. So no `container: credentials:` is needed. (The Actions token could not have been used anyway — no package scope, per images.yaml's header.) - Image: 466,912,274 bytes compressed across 6 layers (manifest sum), 1,541,728,125 bytes unpacked (`podman image inspect`). Local cold pull 10.4s on the workstation. - Image tool versions equal `ci/versions.env` for all 8 pins (kyverno 1.19.0, kyverno-json 0.0.3, chainsaw 0.2.12, yamllint 1.35.1, kubectl 1.36.4, yq 4.44.6, promtool 3.14.0, check-jsonschema 0.29.4) — both via `/etc/dev-toolchain-versions` and by running each binary's `--version`. `sleep`, `tail`, `git` 2.56.0, `bash`, `python3`, `jq`, `curl` present (act's entrypoint is `/bin/sleep 10800`). - The new checkout script, run in the pinned image against the public repo (auth header disabled for the test only): PR ref `refs/pull/143/head` → detached at `60e3a24`, 1018 commits; push ref `refs/heads/main` → `On branch main` at `e8d2666`; `origin/main` present in both. The toolchain-drift step passes on the real `ci/versions.env` and exits 1 naming both tools when yamllint/promtool pins are altered. - Renovate marker regex (copied from renovate.json5) matches gate.yml exactly once, capturing depName/tag/digest. - LOCAL GATE: see below (filled after the run). ## What I did NOT verify - Renovate actually opening a digest-bump PR for gate.yml — only the regex was tested, not a Renovate run. - The authenticated fetch with the real job token — only CI can do that; the PR's own gate run is that test. - Push-to-main behaviour in CI (only the PR event runs before merge).