The Helm chart landed in #167 with two gaps written into ADR-0033: make k8s-lint existed
but no CI job ran it, and "a second deployment description to keep in step with compose —
nothing enforces that today; a drift check belongs in CI (follow-up)". Both are closed here.
make k8s-drift (infra/helm/check-drift.py, stdlib only) compares what each stack
actually deploys rather than diffing two files that differ by design: workload names and
resolved container images, taken from docker compose config --format json and a rendered
chart. The six differences that exist today are declared in DEVIATIONS with the reason
each was forced — the four *-init Django services folded into their web pods, and the two
bootstrap Jobs compose runs from the host — so only a new difference fails.
A k8s CI job runs k8s-lint then k8s-drift on every push and PR. No cluster, no
marketplace action: helm is fetched as the pinned static binary the Talos runbook already
gives developers.
Failing test committed before the implementation — the red commit reports all six
real differences; the green commit declares them.
Implementation makes the test pass.
Conventional Commits referencing the issue (refs #168).
CI green — awaiting the run on this PR (make k8s-lint and make k8s-drift pass locally).
docker compose up unaffected — no service, image or compose file is touched.
Docs updated — docs/runbooks/ci.md (job table + the one place local and CI now
differ), docs/runbooks/kubernetes-talos.md §7/§"not ported", and ADR-0033's cost note.
No ADR needed: no new dependency (python stdlib, and helm/docker were already
prerequisites of the k8s-* targets), no boundary moved, no §8 rule bent.
Not user-visible, so no demo note.
Notes for reviewers
Verified by hand that both drift classes fail the check, not just that it passes today:
bumping OPENZAAK_TAG in compose alone → reports openzaak and oz-celery with both
image strings;
adding a workload to values.yaml alone → reports it by name.
Deliberate limits (there is a ponytail: note in the script):
Names and images only, as sets — no per-workload env, ports or volumes. Those differ
by design in four documented places, so comparing them would mean re-encoding every
deviation field by field for very little more signal.
The three observability workloads are rendered with enabled=true by the check, even
though both stacks default them off, so their images can't drift unwatched.
k8s-lint/k8s-drift are not in make ci, to avoid making helm a hard
prerequisite for everyone. That is now the only local/CI difference; it's called out in docs/runbooks/ci.md.
Follow-ups filed while reviewing the chart, not addressed here: #169 (the published docs
omit every ADR after 0010 and all runbooks but ci.md) and #170 (the production-posture
ADR #25 asked for — secrets are still plain text in values.yaml).
## What & why
The Helm chart landed in #167 with two gaps written into ADR-0033: `make k8s-lint` existed
but no CI job ran it, and *"a second deployment description to keep in step with compose —
nothing enforces that today; a drift check belongs in CI (follow-up)"*. Both are closed here.
**`make k8s-drift`** (`infra/helm/check-drift.py`, stdlib only) compares what each stack
actually deploys rather than diffing two files that differ by design: workload names and
resolved container images, taken from `docker compose config --format json` and a rendered
chart. The six differences that exist today are declared in `DEVIATIONS` with the reason
each was forced — the four `*-init` Django services folded into their web pods, and the two
bootstrap Jobs compose runs from the host — so only a *new* difference fails.
**A `k8s` CI job** runs `k8s-lint` then `k8s-drift` on every push and PR. No cluster, no
marketplace action: helm is fetched as the pinned static binary the Talos runbook already
gives developers.
Closes #168
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation — the red commit reports all six
real differences; the green commit declares them.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #168`).
- [x] CI green — awaiting the run on this PR (`make k8s-lint` and `make k8s-drift` pass locally).
- [x] `docker compose up` unaffected — no service, image or compose file is touched.
- [x] Docs updated — `docs/runbooks/ci.md` (job table + the one place local and CI now
differ), `docs/runbooks/kubernetes-talos.md` §7/§"not ported", and ADR-0033's cost note.
- [x] No ADR needed: no new dependency (python stdlib, and helm/docker were already
prerequisites of the `k8s-*` targets), no boundary moved, no §8 rule bent.
- [x] Not user-visible, so no demo note.
## Notes for reviewers
Verified by hand that both drift classes fail the check, not just that it passes today:
- bumping `OPENZAAK_TAG` in compose alone → reports `openzaak` and `oz-celery` with both
image strings;
- adding a workload to `values.yaml` alone → reports it by name.
Deliberate limits (there is a `ponytail:` note in the script):
- **Names and images only**, as sets — no per-workload env, ports or volumes. Those differ
by design in four documented places, so comparing them would mean re-encoding every
deviation field by field for very little more signal.
- **The three observability workloads are rendered with `enabled=true`** by the check, even
though both stacks default them off, so their images can't drift unwatched.
- **`k8s-lint`/`k8s-drift` are not in `make ci`**, to avoid making `helm` a hard
prerequisite for everyone. That is now the only local/CI difference; it's called out in
`docs/runbooks/ci.md`.
Follow-ups filed while reviewing the chart, not addressed here: #169 (the published docs
omit every ADR after 0010 and all runbooks but `ci.md`) and #170 (the production-posture
ADR #25 asked for — secrets are still plain text in `values.yaml`).
not
added this to the Iteration 6 — Production Posture milestone 2026-09-10 09:01:57 +00:00
ADR-0033 shipped the chart with this cost written down: "a second deployment
description to keep in step with compose. Nothing enforces that today; a drift
check belongs in CI (follow-up)." An upstream image bump or a new service applied
to only one of the two files lands unnoticed.
`make k8s-drift` compares what each stack actually deploys — workload names and
resolved container images, from `docker compose config` and a rendered chart —
rather than diffing the two files, which differ by design.
Red: it reports the six differences that exist today, all of them the platform
deviations ADR-0033 forced. Declaring those as intended is the green step.
Six differences exist by design and are now recorded in DEVIATIONS with the reason
each one was forced, so the check passes on today's tree and fails on tomorrow's
accident: the four `*-init` Django services folded into their web pods (one migrator
per database), and the two bootstrap Jobs compose runs from the host instead.
Verified against both drift classes by hand: bumping OPENZAAK_TAG in compose alone
reports openzaak + oz-celery, and adding a workload to values.yaml alone reports it
by name.
`make k8s-lint` existed since the chart landed but nothing ran it, so the chart had
no automated coverage at all. A `k8s` job now runs it plus `make k8s-drift` on every
push and PR: no cluster, ~20s, and it catches the two failure modes the chart is
actually exposed to — a values typo that renders invalid YAML, and a change made to
one stack but not the other.
helm is installed as its pinned static binary (the URL the Talos runbook already
gives developers) rather than via a marketplace action: nothing extra to vet.
The `k8s` targets stay out of `make ci` on purpose — helm is optional for anyone not
deploying to Kubernetes — which is the one place local and CI now differ, noted in
docs/runbooks/ci.md.
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 & why
The Helm chart landed in #167 with two gaps written into ADR-0033:
make k8s-lintexistedbut no CI job ran it, and "a second deployment description to keep in step with compose —
nothing enforces that today; a drift check belongs in CI (follow-up)". Both are closed here.
make k8s-drift(infra/helm/check-drift.py, stdlib only) compares what each stackactually deploys rather than diffing two files that differ by design: workload names and
resolved container images, taken from
docker compose config --format jsonand a renderedchart. The six differences that exist today are declared in
DEVIATIONSwith the reasoneach was forced — the four
*-initDjango services folded into their web pods, and the twobootstrap Jobs compose runs from the host — so only a new difference fails.
A
k8sCI job runsk8s-lintthenk8s-drifton every push and PR. No cluster, nomarketplace action: helm is fetched as the pinned static binary the Talos runbook already
gives developers.
Closes #168
Definition of Done
real differences; the green commit declares them.
refs #168).make k8s-lintandmake k8s-driftpass locally).docker compose upunaffected — no service, image or compose file is touched.docs/runbooks/ci.md(job table + the one place local and CI nowdiffer),
docs/runbooks/kubernetes-talos.md§7/§"not ported", and ADR-0033's cost note.prerequisites of the
k8s-*targets), no boundary moved, no §8 rule bent.Notes for reviewers
Verified by hand that both drift classes fail the check, not just that it passes today:
OPENZAAK_TAGin compose alone → reportsopenzaakandoz-celerywith bothimage strings;
values.yamlalone → reports it by name.Deliberate limits (there is a
ponytail:note in the script):by design in four documented places, so comparing them would mean re-encoding every
deviation field by field for very little more signal.
enabled=trueby the check, eventhough both stacks default them off, so their images can't drift unwatched.
k8s-lint/k8s-driftare not inmake ci, to avoid makinghelma hardprerequisite for everyone. That is now the only local/CI difference; it's called out in
docs/runbooks/ci.md.Follow-ups filed while reviewing the chart, not addressed here: #169 (the published docs
omit every ADR after 0010 and all runbooks but
ci.md) and #170 (the production-postureADR #25 asked for — secrets are still plain text in
values.yaml).