The gate job now runs inside the CI-built toolbox image, pinned by digest, instead of catthehacker/ubuntu:act-22.04 + a per-run download of yamllint / check-jsonschema / kyverno / promtool / chainsaw (~11s of every run, measured on runs 494/495). Local dev.sh and CI now gate on the identical image.
Unchanged: job name gate (required context gate / gate (pull_request)), the gate step, its env, and || [ "$?" -eq 3 ].
Decisions
Plain-git checkout instead of adding Node to the toolbox.actions/checkout is a Node action and the toolbox has no Node. Adding Node would need two sequential PRs (Containerfile → publish → pin). The git step reproduces what checkout@v7 fetch-depth: 0 ran here (all branches → origin/*, tags, event SHA; detached for PR, -B <branch> for push) and asserts HEAD == github.sha. The job token goes to that one fetch via GIT_CONFIG_* env, and is not persisted.
No pull credentials. Org Home-Network is public and the registry issues anonymous pull tokens (measured: anonymous /v2/token → manifest 200).
Pins stay enforced. The install step is replaced by a check that every *_VERSION in ci/versions.env equals the image's /etc/dev-toolchain-versions (::error::DRIFT otherwise; negative-tested).
Renovate follows the digest via the existing # renovate-digest: convention (renovate.json5 now scans gate.yml; github-actions manager disabled for own images, same as kubernetes). registry-retention.sh reads the same marker.
install-tools.shkept: merge-gate.yaml and alert-rules-unit-tested still reference it.
Image: 466.9 MB compressed / 1.54 GB unpacked. The DinD store is an emptyDir, so the first run after a runner restart or digest bump pays one pull.
Consequence: future toolbox digest bumps touch .gitea/** → sensitive path → need an operator override.
Gate
Local, in the pinned image itself: rc 3, gitea-settings the only stage not run. Not green.
CI: this PR's own run is the test (Gitea runs pull_request workflows from the PR head). approval-policy is expected to fail (.gitea/**, ci/** are sensitive). This needs an operator review/override — I have not posted one and will not merge.
## What
The `gate` job now runs **inside the CI-built toolbox image**, pinned by digest, instead of `catthehacker/ubuntu:act-22.04` + a per-run download of yamllint / check-jsonschema / kyverno / promtool / chainsaw (~11s of every run, measured on runs 494/495). Local `dev.sh` and CI now gate on the **identical** image.
```yaml
container:
# renovate-digest: image=gitea.home.keithjackson.dev/home-network/toolbox tag=main
image: gitea.home.keithjackson.dev/home-network/toolbox@sha256:83a3501efe34fa0b039e3f0d360738686a0f59109866c49779768f5d5ed0ef81
```
Unchanged: job name `gate` (required context `gate / gate (pull_request)`), the gate step, its env, and `|| [ "$?" -eq 3 ]`.
## Decisions
- **Plain-git checkout instead of adding Node to the toolbox.** `actions/checkout` is a Node action and the toolbox has no Node. Adding Node would need two sequential PRs (Containerfile → publish → pin). The git step reproduces what checkout@v7 `fetch-depth: 0` ran here (all branches → `origin/*`, tags, event SHA; detached for PR, `-B <branch>` for push) and asserts `HEAD == github.sha`. The job token goes to that one fetch via `GIT_CONFIG_*` env, and is not persisted.
- **No pull credentials.** Org `Home-Network` is public and the registry issues anonymous pull tokens (measured: anonymous `/v2/token` → manifest 200).
- **Pins stay enforced.** The install step is replaced by a check that every `*_VERSION` in `ci/versions.env` equals the image's `/etc/dev-toolchain-versions` (`::error::DRIFT` otherwise; negative-tested).
- **Renovate** follows the digest via the existing `# renovate-digest:` convention (`renovate.json5` now scans `gate.yml`; `github-actions` manager disabled for own images, same as `kubernetes`). `registry-retention.sh` reads the same marker.
- `install-tools.sh` **kept**: `merge-gate.yaml` and `alert-rules-unit-tested` still reference it.
Image: 466.9 MB compressed / 1.54 GB unpacked. The DinD store is an emptyDir, so the first run after a runner restart or digest bump pays one pull.
**Consequence:** future toolbox digest bumps touch `.gitea/**` → sensitive path → need an operator override.
## Gate
Local, in the pinned image itself: **rc 3**, `gitea-settings` the only stage not run. Not green.
CI: this PR's own run is the test (Gitea runs `pull_request` workflows from the PR head). `approval-policy` is expected to fail (`.gitea/**`, `ci/**` are sensitive). **This needs an operator review/override — I have not posted one and will not merge.**
Handoff: `decisions/2026-11-08-gate-toolbox-container.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The gate job now runs in gitea.home.keithjackson.dev/home-network/toolbox@sha256:83a3501e...
(the image images.yaml publishes from infra/devcontainer/Containerfile) instead of
catthehacker/ubuntu:act-22.04 plus a per-run download of yamllint, check-jsonschema, kyverno,
promtool and chainsaw. Local (dev.sh) and CI now gate on the identical toolchain.
- actions/checkout (Node) replaced by a plain-git step with fetch-depth-0 semantics; the
toolbox has no Node. The job token is not persisted into .git/config.
- The install step is replaced by a check that every ci/versions.env pin equals the image's
/etc/dev-toolchain-versions, so the pins stay enforced.
- Renovate follows the digest through a `# renovate-digest:` marker; github-actions manager
disabled for own images.
- install-tools.sh kept (merge-gate.yaml blueprint + alert-rules-unit-tested check use it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run 500 (this PR's first CI run) failed L0 prod-baremetal-not-dev-surface: act runs steps with
umask 0000, the checkout was world-writable, and Ansible (now present, via the toolbox) refuses
a world-writable directory as an ansible.cfg source. Reproduced and fixed locally in the pinned
image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
The
gatejob now runs inside the CI-built toolbox image, pinned by digest, instead ofcatthehacker/ubuntu:act-22.04+ a per-run download of yamllint / check-jsonschema / kyverno / promtool / chainsaw (~11s of every run, measured on runs 494/495). Localdev.shand CI now gate on the identical image.Unchanged: job name
gate(required contextgate / gate (pull_request)), the gate step, its env, and|| [ "$?" -eq 3 ].Decisions
actions/checkoutis a Node action and the toolbox has no Node. Adding Node would need two sequential PRs (Containerfile → publish → pin). The git step reproduces what checkout@v7fetch-depth: 0ran here (all branches →origin/*, tags, event SHA; detached for PR,-B <branch>for push) and assertsHEAD == github.sha. The job token goes to that one fetch viaGIT_CONFIG_*env, and is not persisted.Home-Networkis public and the registry issues anonymous pull tokens (measured: anonymous/v2/token→ manifest 200).*_VERSIONinci/versions.envequals the image's/etc/dev-toolchain-versions(::error::DRIFTotherwise; negative-tested).# renovate-digest:convention (renovate.json5now scansgate.yml;github-actionsmanager disabled for own images, same askubernetes).registry-retention.shreads the same marker.install-tools.shkept:merge-gate.yamlandalert-rules-unit-testedstill reference it.Image: 466.9 MB compressed / 1.54 GB unpacked. The DinD store is an emptyDir, so the first run after a runner restart or digest bump pays one pull.
Consequence: future toolbox digest bumps touch
.gitea/**→ sensitive path → need an operator override.Gate
Local, in the pinned image itself: rc 3,
gitea-settingsthe only stage not run. Not green.CI: this PR's own run is the test (Gitea runs
pull_requestworkflows from the PR head).approval-policyis expected to fail (.gitea/**,ci/**are sensitive). This needs an operator review/override — I have not posted one and will not merge.Handoff:
decisions/2026-11-08-gate-toolbox-container.md.🤖 Generated with Claude Code