The branch-protection + PR-gate + sidecar sequence is complete and verified. This records the finished state and fixes what the sequence itself proved wrong. CORRECTS A FALSE CLAIM I SHIPPED. PR #7 said the sidecar conversion would remove the one-per-start dockerd line "http: TLS handshake error from 127.0.0.1: EOF". It does NOT. Exactly one still appears, immediately after "API listen on [::]:2376" -- it is now the startupProbe's FIRST attempt, landing microseconds after dockerd begins listening and cut off by timeoutSeconds: 1; the next poll succeeds. Cosmetic, and deliberately not chased: raising the timeout to hide one benign startup line would slow every subsequent poll. The manifest comment now states this instead of the wrong prediction. Lands infra/gitea/require-gate.sh and verify-then-enforce.sh, which drove the rollout from a session temp dir. verify-then-enforce.sh merges nothing -- merging is a human's reviewed act -- but forces the reconcile and ABORTS unless the pod is healthy, a runner is online, and a real workflow goes green on it, before enabling enforcement and probing the push. Re-run it after any future act-runner change. Trims the now-obsolete feat/ rationale from claim-ship.sh: per-branch push triggers were deleted, so no prefix produces a duplicate run any more. CLAUDE.md's enforcement table flips from "weaker than this rule" to machine-enforced, with all four rows marked verified and the push rejection noted as PROVOKED rather than assumed. It also now states what a green PR does NOT prove: CLUSTER_AVAILABLE is unset, so L2/L3 never runs in CI and only the L0/L1 offline subset is covered. infra/gitea/README.md marks the sequence done and names the root cause that remains unfixed: Gitea repo settings live in its own database, invisible to Flux and unreadable by the conformance gate, so NOTHING detects drift from what this directory declares. These scripts assert desired state; they are not a detector. The check that reads the live API and fails on divergence is the highest-value work left, and it is the difference between this being fixed and this being unable to recur. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
148 lines
7.1 KiB
Markdown
148 lines
7.1 KiB
Markdown
# CLAUDE.md — how work lands in this repo
|
|
|
|
Read this before doing anything durable. It is the delivery contract for AI coding
|
|
sessions and agents. `DECISIONS-LOG.md`'s last entry is the current PM handoff and
|
|
takes precedence on *what* to work on; this file governs *how* it lands.
|
|
|
|
## 1. Never push to `main`. Ever.
|
|
|
|
Every durable change reaches `main` exactly one way:
|
|
|
|
branch -> push the branch -> open a PR -> CI gate green -> human approval -> merge
|
|
|
|
There is no other path. Specifically:
|
|
|
|
- **Do not `git push origin main`.**
|
|
- **Do not `git push origin HEAD:main`.** Same thing wearing a hat.
|
|
- **Do not merge your own PR.** A human approves and clicks merge.
|
|
- **Do not `kubectl apply` / `edit` / `patch` a durable change.** Flux reconciles `main`;
|
|
anything applied by hand is drift that the next reconcile reverts or, worse, adopts.
|
|
|
|
> **Many runbooks under `runbooks/` still say `git push origin main` IS the deploy.**
|
|
> That wording is **stale** — it predates the PR flow. It describes what Flux does
|
|
> (main is what the cluster follows), not what you are permitted to do. Honour this
|
|
> file, not that line. Fixing those runbooks is tracked work.
|
|
|
|
## 2. Enforcement (as of 2026-09-29: the rule is now MACHINE-ENFORCED)
|
|
|
|
Verified against the live Gitea on 2026-09-29:
|
|
|
|
| Expected | Actual |
|
|
|---|---|
|
|
| `main` protected, direct push rejected | ✅ `protected: true`, `user_can_push: false` — **verified by a real rejected push**, not by the API's word |
|
|
| PR blocked until the gate passes | ✅ `enable_status_check: true`, context `gate / gate (pull_request)` |
|
|
| Gate runs on every PR | ✅ runner `incluster-dind` online; ~1m35s |
|
|
| Approval required before merge | ✅ `required_approvals: 1` |
|
|
|
|
All four were FALSE as recently as 2026-09-29, while this repo's own log asserted they were
|
|
true. They are true now because each was measured, and the push rejection was provoked rather
|
|
than assumed. **Re-measure before relying on any of them** — see §2b.
|
|
|
|
Still not covered: `vars.CLUSTER_AVAILABLE` is unset, so **L2/L3 conformance does not run in
|
|
CI**. A green PR therefore proves the L0/L1 offline subset, not runtime behaviour.
|
|
|
|
**Consequence for you:** CI now runs, but it does not yet *block* anything. Always run
|
|
the toolbox gate yourself and quote its numbers. Never describe a PR as "green" on the
|
|
strength of a check you have not seen report — and note that a **skipped** Gitea job
|
|
reports as `success`, so a green tick is not by itself evidence that anything ran.
|
|
|
|
## 2b. Never infer platform state from this repository
|
|
|
|
**This repo is not evidence about the live platform.** Neither is `DECISIONS-LOG.md`.
|
|
Both describe what someone believed at the time of writing, and both have been
|
|
confidently wrong for a month at a stretch.
|
|
|
|
On 2026-09-29 the log stated the merge gate was a required status check with a runner
|
|
online. The API said `branch_protections: []` and **zero** runners. Everything downstream
|
|
of that belief — "the gate enforces this", "your push would be rejected", "CI covers it" —
|
|
was wrong, and nothing in the tree could have revealed it, because the tree was the source
|
|
of the error.
|
|
|
|
**So, before asserting that any gate, protection, runner, secret, or workload is in a
|
|
given state: query the thing itself.**
|
|
|
|
| Claim about... | Check |
|
|
|---|---|
|
|
| branch protection / required checks | `GET /api/v1/repos/{owner}/{repo}/branches/main` |
|
|
| a runner exists | `GET /api/v1/admin/actions/runners` |
|
|
| CI actually ran | the commit's `/statuses` — and read the **description**; a *skipped* Gitea job reports `success` with "Has been skipped" |
|
|
| a workload / Secret / env | `kubectl --context k3s-prod ...` (read-only is fine for agents) |
|
|
| Flux applied your commit | compare `.status.lastAppliedRevision` against the GitRepository's artifact revision |
|
|
|
|
Quote the measured field, not the prose. If you cannot measure it, say so explicitly —
|
|
"unverified" is a legitimate report, and a far better one than a confident guess that
|
|
matches the docs.
|
|
|
|
**A green tick is not proof anything ran.** Read what actually executed.
|
|
|
|
## 3. Gate before you push
|
|
|
|
./infra/devcontainer/dev.sh run bash -c './ci/gate.sh'
|
|
|
|
Exit codes are a contract — do not flatten them:
|
|
|
|
| rc | Meaning |
|
|
|---|---|
|
|
| 0 | PASS |
|
|
| 1 | BLOCKED — something failed |
|
|
| 3 | UNDETERMINED — a stage did not run. **Never report this as green.** |
|
|
|
|
`spec-immutability` reaches rc 3 whenever the sibling spec repo is not checked out
|
|
beside this one; that single skip is expected and tolerated. Any *other* skip is a
|
|
real gap in your run.
|
|
|
|
**Never gate on the host.** `promtool` and `kustomize` are not on the host `$PATH`,
|
|
so checks SKIP and you get a false rc 3. Only the pinned toolbox result counts.
|
|
|
|
### Running the gate from a git worktree
|
|
|
|
Two unrelated failures, both with known workarounds — put both in any agent brief:
|
|
|
|
1. **`dev.sh` refuses to start:** `infra/ansible/.cache/prod.kubeconfig` is untracked
|
|
and exists only in the main checkout. Do **not** copy it (it is a credential).
|
|
Use `DEV_KUBECONFIG=/dev/null` — the gate contacts no cluster, so L2/L3 scaffold-skip
|
|
normally.
|
|
2. **Every `git` call inside the container fails:** a worktree's `.git` is a pointer to a
|
|
gitdir outside the mounted root. `dev.sh` takes no extra-mount flag, so inject one via
|
|
the `ENGINE` hook — a shim that intercepts `run` and re-execs
|
|
`podman run -v /path/to/main/.git:/path/to/main/.git:ro "$@"`. A traceback in
|
|
`tracked_files()` is this bug, not a broken tree.
|
|
|
|
## 4. Branch naming
|
|
|
|
Use `feat/`, `fix/`, `docs/`, `chore/`. Note that `.gitea/workflows/gate.yml`'s **push**
|
|
trigger lists `feature/**` — not `feat/**` — so branch pushes get no fast-feedback run.
|
|
The `pull_request` trigger fires regardless of branch name, so the PR gate is unaffected.
|
|
|
|
## 5. Cluster access
|
|
|
|
`kubectl` does not work on the operator workstation; the toolchain lives only in the
|
|
pinned container. Always name the context explicitly:
|
|
|
|
./infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod get nodes'
|
|
|
|
`dev.sh` merges both clusters and picks a current-context **per invocation**; that
|
|
default has already changed once. Never rely on it. Do not run two `dev.sh`
|
|
invocations concurrently.
|
|
|
|
**Mutating `kubectl` and `flux reconcile` are operator actions, not agent actions.**
|
|
|
|
## 6. Secrets
|
|
|
|
- **Never `sops -d` a file into a terminal, a log, or a transcript.**
|
|
- **Never print a Secret's `.data`/`.stringData`** when checking whether it exists —
|
|
list key names or object names only. A password has been leaked into a transcript
|
|
this way before.
|
|
- Encrypted files are `*.sops.yaml` or live under `secrets/`. The root `.sops.yaml` is
|
|
the single creation-rules file; do not add a second one in a subtree.
|
|
- A short-lived credential (e.g. a plex.tv claim token, ~minutes) cannot survive the PR
|
|
flow. Do not design around committing one — read the expiry from the issuing page and
|
|
treat a burnt token as free to retry.
|
|
|
|
## 7. Before you finish
|
|
|
|
Append a handoff entry to `DECISIONS-LOG.md` describing what you changed, what you
|
|
verified, and what you did **not** verify. State gaps plainly — an honest
|
|
"UNVERIFIED" is worth more than an optimistic "green", and this repo has already been
|
|
burned by the difference.
|