The runner came up on 2026-09-29 and the first CI jobs this repo has ever run went green, which falsified a claim merged in #2. MEASURED: gate / gate (pull_request) success 1m58s (PR 1), 1m34s (PR 2) gate / gate (push) success 1m32s (PR 2 only) gate / conformance (pull_request) success "Has been skipped" #2 said listing the conformance context as a required check would deadlock every PR, because the job is `if: false` and never runs. That is wrong. Gitea reports a skipped job as SUCCESS. Requiring it would not deadlock anything -- it would pass VACUOUSLY, forever, while nothing ran. The conclusion is unchanged (never require it) but the reason inverts, and the corrected reason matters more: under HARD RULE 4 a permanently-green check that verifies nothing is worse than a deadlock. A deadlock is loud. This is the exact fail-open shape the 2026-09-18 audit and this one both keep finding. Also records that the (push) contexts only appear for branches matching gate.yml's push trigger -- feature/**, fix/**, docs/**, chore/** -- and NOT feat/**, which is why PR 1's feat/ branch produced only pull_request contexts. Require the pull_request context alone, or PRs from non-matching prefixes never satisfy the rule. CLAUDE.md's enforcement table is updated: the runner half is genuinely fixed and observed green, the branch-protection half is not. Adds the warning that a green tick is not by itself evidence that anything ran. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
118 lines
5.6 KiB
Markdown
118 lines
5.6 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 is currently WEAKER than this rule. Follow the rule anyway.
|
||
|
||
Verified against the live Gitea on 2026-09-29:
|
||
|
||
| Expected | Actual |
|
||
|---|---|
|
||
| `main` protected, direct push rejected | **No branch protection.** `protected: false`, `user_can_push: true` |
|
||
| PR blocked until the gate passes | **`enable_status_check: false`**, `status_check_contexts: []` |
|
||
| Gate runs on every PR | **Fixed 2026-09-29.** Runner `incluster-dind` is online; `gate / gate (pull_request)` runs and passes in ~1m30s–2m |
|
||
| Approval required before merge | **`required_approvals: 0`** |
|
||
|
||
So nothing mechanically stops you from pushing to `main` right now. **That is a gap
|
||
being closed, not permission.** `DECISIONS-LOG.md` (2026-08-31) claimed the gate was a
|
||
"required status check" with a runner online; neither was true, which is how this went
|
||
unnoticed for a month. The runner half is now genuinely fixed and observed green. The
|
||
protection half is not — see `infra/gitea/`.
|
||
|
||
**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.
|
||
|
||
## 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.
|