0045 — Deploy only commits that pass the deploy gate
Flux reconciles a
deploybranch that CI fast-forwards to eachmaincommit that passes the contract suite and a full render.
Context
Flux reconciled main directly, so a push was live within about a minute. Nothing checked a commit on the way. The contract suite ran only when someone remembered to run it locally, and the flux-local render check ran only on pull requests, while most commits are pushed straight to main. Renovate's branch automerges land on main without checks too.
In October 2026 a change reached the cluster while a piped validation command hid a failing exit code. The failure was a flaky test, but nothing would have stopped a real one.
Several writers reach main: the operator, agent sessions, merged pull requests, and Renovate. A gate that depends on every writer following a procedure leaves a gap for each of them.
Decision
Flux reconciles refs/heads/deploy instead of main. The Deploy Gate workflow runs on every push to main: the full contract suite in one job and a flux-local render of every Kustomization and HelmRelease in another. When both pass, it fast-forwards deploy to that commit. Pull requests run the same two checks without promoting.
- Fast-forward only. A late run for an older commit cannot rewind
deploy. Ifdeployever holds commits thatmainlacks, promotion stops until an operator reconciles the branches. - Promotion credential. The default workflow token cannot move a branch past changes to workflow files, which Renovate makes routinely. A dedicated GitHub App, limited to this repository's contents and workflows, mints a short-lived token for each promotion. Its key lives in a
deployenvironment that admits onlymain. - Break glass. When the gate itself is broken, the operator fast-forwards
deployby hand to a commit that is already onmain. The local hook requiresANTON_BREAK_GLASS=1for that push. - Local hook. An optional pre-push hook runs the static validators against each pushed commit in a few seconds. It catches mistakes before they become public history. The gate does not depend on it.
Alternatives considered
- Do nothing: unvalidated pushes keep reaching the cluster within a minute.
- Protect
mainwith required checks and auto-merge: every change, including Renovate's branch automerges, would need a pull request. The operator administers the repository, so an emergency bypass would also bypass the gate for routine pushes, the path that caused the incident. Checks would validate a pull request's merge preview rather than the squashed commit that deploys, and personal repositories have no merge queue to close that gap. - Publish validated OCI artifacts for Flux: the cleanest separation, but every Kustomization's source reference would change and a registry would join the deploy path.
- Pre-push hook alone: easy to skip, and it never sees Renovate's commits.
- SSH deploy key instead of a GitHub App: simpler to create, but the key never expires and rulesets cannot single it out from other deploy keys.
Consequences
- A push takes about two to three minutes to go live instead of one.
maincan hold a failing commit.deploythen stays at the last passing commit until a fix lands, and the commits in between deploy together. GitHub notifies whoever pushed the failing commit. A failure from a Renovate automerge notifies no one, sotask deploy:statusis the place to look.- The GitHub App key is one more credential to rotate.
- A break-glass push skips validation. It shows as
deployahead of the newest passing gate run. - The cutover from
maintodeploymust push the same commit to both branches at once; otherwise Flux alternates between them until the gate promotes the switch. - The authenticated auxiliary source still deploys directly from its own repository. This gate covers only this repository.