docs: R-168 SHIPPED, R-29 CLOSED on the demonstrated alarm, R-169 minted
gates / gates (push) Successful in 7s
gates / gates (push) Successful in 7s
SPIKE-ci-runner-2026-08-02.md: all six probes with method, measurement and ruling; none STOPped. P2 (stock image has git but no python3) and P6 (a runner that loses its state re-registers and orphans the old record) changed the design; P5 (a failed run signals NOTHING) is why the alarm exists at all. R-168 SHIPPED with its evidence. R-29 CLOSED — on the demonstrated alarm and not on a green run, as required: the class it opened is answered at both ends, the hook refusing locally and CI catching a --no-verify bypass and emailing. R-161 noted: its automatic half now exists for the STATIC gate, while its original scope, the runtime gate, is deliberately still not automatic and should stay that way. NEW R-169 (grep established R-168 was the highest in use): CI can only report, because there is no gate in the road. Making it blocking needs branch protection plus a PR workflow, both of which change how the operator works — so it is theirs to decide, and the row states the cost honestly rather than recommending it. CONTEXT gains S-8 (CI detects, does not block, and why that is structural), S-9 (a detector that tells no one is not finished, plus the curl and Cloudflare-1010 traps), S-10 (the runner is unprivileged because DooPlex is Tier 2), S-11 (CI reproduces the sibling layout). CLAUDE.md gains the rule earned by red-proofing: a go test -run pattern that matches no test prints ok and exits 0, and an instrument that can silently drop results is not a measurement.
This commit is contained in:
@@ -1,274 +1,237 @@
|
||||
# REPORT — Gate enforcement: fix the reds, one entry point per repo, pre-push hook (2026-08-02)
|
||||
# REPORT — CI: a Gitea Actions runner, and a red run that reaches a person (R-168, 2026-08-02)
|
||||
|
||||
**Overwritten** per the standing rule. The prior contents (`STATUS.md` created + the 2026-08-02
|
||||
operator decisions, same day) have their durable record in `STATUS.md` itself and in
|
||||
`documentation/backlog/OPEN-ITEMS.md`; nothing was lost by this overwrite.
|
||||
**Overwritten** per the standing rule. The prior contents (session 1's gate enforcement, same day)
|
||||
have their durable record in `scripts/CHANGELOG.md`, `hub/CHANGELOG.md` and `CONTEXT.md` S-6/S-7.
|
||||
|
||||
**Outcome: complete.** Nothing was left out. Two deviations from the spec and one census addition are
|
||||
recorded in §13; both deviations were made deliberately, not by accident.
|
||||
**Outcome: complete, and the half that matters is demonstrated.** All six probes answered; none
|
||||
STOPped. The acceptance test was never a green pipeline — it was a **red run with a message in
|
||||
hand**, and that is `RESEND-ACCEPTED id=5ff34766-c5f8-4588-8104-08296aeb45ab`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Confirmed baselines — all four matched §1 on arrival
|
||||
## 1. Confirmed baselines — all matched §1 on arrival
|
||||
|
||||
| Repo | `main` on arrival | matched §1? | version on arrival | version now |
|
||||
|---|---|---|---|---|
|
||||
| `felhom.eu` | `e994bf35d290` | yes | hub `v0.86.0` | **hub `v0.87.0`** |
|
||||
| `felhom-controller` | `4115e88f688b` | yes | `v0.188.0` | `v0.188.0` (no bump — tooling only) |
|
||||
| `felhom-agent` | `4663df7ff3cc` | yes | `v0.119.0` | `v0.119.0` (no bump — tooling only) |
|
||||
| `app-catalog-felhom.eu` | `fd7747d1293c` | yes | n/a | n/a |
|
||||
|
||||
All four trees were clean and `HEAD == origin/main` on arrival.
|
||||
|
||||
## 2. Files created / modified
|
||||
|
||||
**`felhom.eu`**
|
||||
- `hub/internal/web/configs.go` — `hostInstallVersion` const, `pageData.ScriptVersion` field and its
|
||||
assignment DELETED; a NOTE records why there is deliberately no constant here.
|
||||
- `hub/internal/web/templates/customer_unified.html` — Setup Command copy rewritten, no version.
|
||||
- `hub/internal/web/render_test.go` — tautological version assertion deleted (not replaced).
|
||||
- `scripts/hostinstall_gates.py` — gate 1's third assertion inverted; docstring rewritten.
|
||||
- `scripts/felhom-host-install.sh` — **comment only**; `SCRIPT_VERSION` untouched (deviation, §13).
|
||||
- `scripts/reuse_refs_check.py` — rewritten resolution (exact/suffix/ambiguous/cross-repo/FAIL).
|
||||
- `scripts/test_reuse_refs_check.py` — NEW, 13 tests.
|
||||
- `scripts/repo_gates.py` — NEW, the entry point.
|
||||
- `scripts/test_repo_gates.py` — NEW, 3 tests (seam).
|
||||
- `.githooks/pre-push` — NEW.
|
||||
- `CLAUDE.md`, `CONTEXT.md` (S-6, S-7), `.gitignore`, `hub/CHANGELOG.md`, `scripts/CHANGELOG.md`,
|
||||
`manifests/hub.yaml`, `documentation/backlog/OPEN-ITEMS.md`, `documentation/backlog/ROADMAP.md`,
|
||||
`STATUS.md`, this file.
|
||||
|
||||
**`felhom-controller`** — `controller/scripts/docker_run_volume_path_gate.py` (one ALLOWLIST entry),
|
||||
`controller/scripts/controller_gates.py` (NEW), `controller/scripts/test_controller_gates.py` (NEW),
|
||||
`.githooks/pre-push` (NEW), `CLAUDE.md`, `controller/README.md`, `CHANGELOG.md`, `REPORT.md`.
|
||||
|
||||
**`felhom-agent`** — `scripts/agent_gates.py` (NEW), `scripts/test_agent_gates.py` (NEW),
|
||||
`.githooks/pre-push` (NEW), `CLAUDE.md`, `CHANGELOG.md`, `REPORT.md`.
|
||||
|
||||
**`app-catalog-felhom.eu`** — `scripts/catalog_gates.py` (`--fast`),
|
||||
`scripts/test_catalog_gates.py` (NEW), `.githooks/pre-push` (NEW), `CLAUDE.md`, `CHANGELOG.md`,
|
||||
`REPORT.md`.
|
||||
|
||||
## 3. Commits pushed to `main`
|
||||
|
||||
**`felhom-controller`** — Part 1.1 is its own commit, with nothing else in it, as specified.
|
||||
|
||||
| Hash | What |
|
||||
|---|---|
|
||||
| `c432f70` | **gate: allowlist appexport/estimate.go named-volume `-v` mount (R-29 leg a)** — the reviewed diff, alone |
|
||||
| `8cb3d7a` | gates: one entry point (`controller/scripts/controller_gates.py`) + pre-push hook |
|
||||
| `7c32c74` | docs: CHANGELOG + README for the gate entry point (no version bump) |
|
||||
|
||||
**`felhom.eu`**
|
||||
|
||||
| Hash | What |
|
||||
|---|---|
|
||||
| `d319ae5` | hub: delete the host-install version label (R-94) + invert hostinstall gate 1 |
|
||||
| `2137094` | scripts: `reuse_refs_check` resolves package shorthand and sibling repos |
|
||||
| `9bd1a54` | gates: one entry point (`scripts/repo_gates.py`) + pre-push hook |
|
||||
| `4707be7` | docs: R-94 closed, R-29 leg (a) closed + leg (b) half, R-168 minted |
|
||||
| `8d9b78c` | manifests: hub 0.86.0 → 0.87.0 |
|
||||
| (final) | REPORT + STATUS |
|
||||
|
||||
**`felhom-agent`** — `054e85a` (entry point + hook), `75245a4` (CHANGELOG).
|
||||
**`app-catalog-felhom.eu`** — `c3e4bb1` (`--fast` + hook), `340ff2a` (CHANGELOG).
|
||||
|
||||
## 4. Per-test results and EVERY red-proof
|
||||
|
||||
| # | Red-proof | Method | Outcome |
|
||||
| Repo | `main` on arrival | matched? | version |
|
||||
|---|---|---|---|
|
||||
| 1 | **Part 1.1 — the docker-`-v` gate** | renamed the new ALLOWLIST suffix to a non-existent file | gate **exit 1**, naming `estimate.go:179`; restored → **exit 0** |
|
||||
| 2 | **Part 1.2 — the rewritten gate 1** | re-inserted `const hostInstallVersion = "1.19.0"` into `configs.go` | gate **exit 1**, convicting on **3 of its 6 shapes** (const / assignment / renamed-const); restored → **exit 0** |
|
||||
| 3 | **Part 1.2 — the OLD `render_test.go` assertion tested nothing** | set the const to `"9.9.9"` while the served script was 1.22.0, ran `TestTemplates_InstallGenerator -v` | **`--- PASS`**. It compared the constant to itself. *(First attempt used `-run TestCustomerUnified`, which matches no test in that file and printed a vacuous `ok` — caught, corrected, re-run against the real test name.)* |
|
||||
| 4 | **Part 2 — Scenario E, the checker can still fail** | made `resolve()` return `exact` for an unresolvable token — the "made green by being made blind" mutation | **4 of 13 tests went red** (`test_absent_path_fails`, `test_failure_lists_every_resolution_attempted`, `test_evidence_copy_does_not_satisfy_a_citation`, `test_non_git_sibling_is_not_searched`); restored → 13/13 green |
|
||||
| 5 | **Part 3 — the inert runner, all four entry points** | replaced each `run_gate` body with `return 0` | every runner still printed its summary and **exited 0**; the seam tests went red — felhom.eu 1 failure, controller 1, agent 2, catalog 1. All restored green |
|
||||
| `homelab-manifests` | `420e8197769b` | yes | n/a |
|
||||
| `felhom.eu` | `af2d10388021` | yes | hub `v0.87.0` — **no bump** |
|
||||
| `felhom-controller` | `eaded79b1894` | yes | `v0.188.0` — **no bump** |
|
||||
| `felhom-agent` | `2c4efed5de32` | yes | `v0.119.0` — **no bump** |
|
||||
| `app-catalog-felhom.eu` | `f16f29757e95` | yes | n/a |
|
||||
|
||||
**Integration scenarios**
|
||||
Live Gitea re-confirmation, **all matched**: version **1.26.2**; Actions enabled on **all five**
|
||||
repos; **0 runners**; **0 workflow runs ever**; **0 branch protections**; **no `.gitea/`** in any
|
||||
repo. ArgoCD `gitea` app: `path: gitea-system`, auto-sync **off**, Synced at `420e819`, Healthy.
|
||||
The only new artifact is the runner image, at its own `0.1.0`.
|
||||
|
||||
- **A — all four entry points green in `--fast`:** exit 0 each. Summary lines quoted in §6.
|
||||
- **B — a red member fails the runner:** with `hostInstallVersion` restored, `repo_gates.py --fast`
|
||||
exited **1**, `hostinstall_gates`' own finding text appeared on stdout, and the summary read
|
||||
`hostinstall FAILED (exit 1)` / `CONVICTED: hostinstall`. Reverted. The companion inert-runner
|
||||
proof is #5 above.
|
||||
- **C — the hook refuses:** §7.
|
||||
- **D — 13 findings resolve, none by blindness:** all four roots exit 0.
|
||||
`appbackup/userdata.go` resolves **by suffix** inside `felhom-controller`;
|
||||
`wgsync/reconciler.go` is reported as **cross-repo → `felhom.eu/hub/internal/wgsync/reconciler.go`**,
|
||||
not as an exact match.
|
||||
- **E — the kill condition:** pinned by `test_reuse_refs_check.py` and red-proofed (#4 above).
|
||||
- **F — runtime catalogue gates stay out of the hook:** `catalog_gates.py --fast` ran
|
||||
`check-image-pins.py` only (`image-pin gate OK — 53 templates, 0 unpinned images`); the other two
|
||||
did not run and the skip was announced with its reason. Pinned by `test_catalog_gates.py`.
|
||||
## 2. All six probe results
|
||||
|
||||
## 5. Test count before / after
|
||||
Full method and measurement: `documentation/audits/SPIKE-ci-runner-2026-08-02.md`.
|
||||
|
||||
| Repo | Before | After | Delta |
|
||||
|---|---|---|---|
|
||||
| `felhom.eu` — Go (`hub/`) | 568 test funcs, 17 packages `ok` | 568, 17 `ok` | 0 (one *assertion* deleted, no test funcs) |
|
||||
| `felhom.eu` — Python | 0 | **16** (13 `test_reuse_refs_check` + 3 `test_repo_gates`) | +16 |
|
||||
| `felhom-controller` — Python | 0 | **4** | +4 |
|
||||
| `felhom-agent` — Python | 0 | **4** | +4 |
|
||||
| `app-catalog-felhom.eu` — Python | existing `test_check_volume_persistence.py` | **+5** (`test_catalog_gates`) | +5 |
|
||||
|
||||
All green. No failures.
|
||||
|
||||
## 6. The four entry points' summary lines, verbatim (`--fast`)
|
||||
|
||||
```
|
||||
site OK (exit 0) template-id OK (exit 0)
|
||||
hostinstall OK (exit 0) emoji OK (exit 0)
|
||||
hub-confirm OK (exit 0) native-confirm OK (exit 0)
|
||||
manifest-bearer OK (exit 0) offbox-rename OK (exit 0)
|
||||
reuse-refs OK (exit 0) app-row-dedup OK (exit 0)
|
||||
mojibake OK (exit 0)
|
||||
all felhom.eu gates OK docker-v OK (exit 0)
|
||||
reuse-refs OK (exit 0)
|
||||
|
||||
all controller gates OK
|
||||
|
||||
reuse-refs OK (exit 0) image-pins OK (exit 0)
|
||||
|
||||
all agent gates OK all catalog gates OK
|
||||
```
|
||||
|
||||
Each run also prints its per-root reuse tally, which is the positive observable — e.g.
|
||||
`OK [felhom-controller]: 133 cited paths — exact 126, suffix 6, ambiguous 0, cross-repo 1, FAILED 0`.
|
||||
|
||||
## 7. Part 4.0 probe — all four measured outcomes
|
||||
|
||||
Throwaway clone, git **2.47.3**, relative `core.hooksPath .githooks`:
|
||||
|
||||
| Step | Expected | **Measured** |
|
||||
| Probe | Ruling | Measurement |
|
||||
|---|---|---|
|
||||
| 1. hook + `core.hooksPath .githooks` + `chmod +x` | — | set; `git config --get core.hooksPath` → `.githooks` |
|
||||
| 2. `git push` from the **repo root** | refused | **REFUSED, rc=1** |
|
||||
| 3. `git push` from a **subdirectory** (`hub/`) | refused | **REFUSED, rc=1** |
|
||||
| 4. `git push --no-verify` | succeeds | **ACCEPTED, rc=0** |
|
||||
| **P1** runner picks up a job | **PASS** | run #1 claimed by `felhom-gates-runner`, `P1-OK`, `🏁 Job succeeded` |
|
||||
| **P2** stock image runs our checks | **known branch → build an image** | stock `act_runner:0.6.1` (Alpine 3.23): `git 2.52.0` PRESENT, `python3` **ABSENT**. Host mode itself fine → privileged/dind never reached for |
|
||||
| **P3** source without JS actions | **PASS** | `pushed sha == checked-out sha` (`bbd62319…`), plain `git clone`/`fetch` from the in-cluster Service, no `uses:` |
|
||||
| **P4** one registration, four repos | **PASS** | tasks 7,8,9,10 across four repos, all `runner_id=2 felhom-gates-runner` |
|
||||
| **P5** does a failure signal anything | **NOTHING SENT → build the alarm** | 0 mail/SMTP/notification log lines, **0** notification rows, after a real `failure` run |
|
||||
| **P6** persistent state needed | **PVC is load-bearing** | PVC intact → same registration; `/data/.runner` removed → **second record** appears and the first is orphaned |
|
||||
|
||||
**Step 3 did not fail, so there is no STOP and no `SPIKE-githooks-*.md` was written.** The form used
|
||||
is therefore the **relative** `core.hooksPath .githooks`.
|
||||
**P5's honest limit, stated rather than glossed:** I did not independently prove Gitea's SMTP path
|
||||
can *deliver*, so "no mail" cannot be split with certainty between *no action-failure notification in
|
||||
1.26.2* and *a broken mailer*. It does not change the design — the alarm rides Resend, a different
|
||||
path — and Scenario C proves that path end to end.
|
||||
|
||||
Step 3 was then re-measured with a hook that prints a distinctive line, because "the push was
|
||||
refused" alone does not say *what* refused it. The hook printed `PROBE-HOOK-RAN` **with cwd = the
|
||||
repo root** when `git push` was issued from the root, from `hub/`, and from a two-level-deeper
|
||||
subdirectory. So git resolves a relative `core.hooksPath` against the repo root and runs the hook
|
||||
there regardless of where the push is issued. The shipped hook does not rely on that — it resolves
|
||||
the root explicitly with `git rev-parse --show-toplevel`.
|
||||
## 3. Files created / modified
|
||||
|
||||
**Scenario C**, in a throwaway clone of `felhom.eu` with `origin` re-pointed at a scratch bare repo
|
||||
so nothing real could be pushed:
|
||||
**`homelab-manifests`** — `gitea-system/act-runner.yaml` (NEW: PVC + Deployment),
|
||||
`gitea-system/act-runner/Dockerfile` (NEW), `CHANGELOG.md`, `REPORT.md`.
|
||||
|
||||
- **unarmed clone** — a manual `repo_gates.py --fast` printed
|
||||
`WARNING: this clone is UNARMED — core.hooksPath is unset…`
|
||||
- **C0, clean tree** — push **ACCEPTED**, and the hook said so:
|
||||
`pre-push [felhom.eu]: gates OK - push proceeding.`
|
||||
- **C1, deliberately broken gate** — push **REFUSED**; `hostinstall_gates`' own three findings and
|
||||
`CONVICTED: hostinstall` appeared, then
|
||||
`pre-push [felhom.eu]: PUSH REFUSED - gates exited 1.`
|
||||
- **C2, `git push --no-verify` on the same commit** — **ACCEPTED** (`9bd1a54..19e8cbb`), and the
|
||||
broken commit was confirmed present in the scratch bare repo. The escape hatch works.
|
||||
**`felhom.eu`** — `.gitea/workflows/gates.yml` (NEW),
|
||||
`documentation/audits/SPIKE-ci-runner-2026-08-02.md` (NEW), `CLAUDE.md`, `CONTEXT.md` (S-8…S-11),
|
||||
`scripts/CHANGELOG.md`, `documentation/backlog/OPEN-ITEMS.md`, `documentation/backlog/ROADMAP.md`,
|
||||
`STATUS.md`, `REPORT.md`. *(`hub/internal/web/configs.go` was broken and restored within the session
|
||||
for Scenario B; it is byte-identical to its starting state — verified by the gate that objects to it.)*
|
||||
|
||||
All five real pushes in this session ran their hook and announced it — see §3.
|
||||
**`felhom-controller`**, **`felhom-agent`**, **`app-catalog-felhom.eu`** —
|
||||
`.gitea/workflows/gates.yml` (NEW), `CHANGELOG.md`, `REPORT.md`.
|
||||
|
||||
## 8. `git config --get core.hooksPath` in the four live workspace clones
|
||||
## 4. Commits pushed to `main`
|
||||
|
||||
| Repo | Hash | What |
|
||||
|---|---|---|
|
||||
| `homelab-manifests` | `c51b77c` | the runner manifest + Dockerfile |
|
||||
| `felhom.eu` | `bbd6231` | the temporary Part 0 probe workflow |
|
||||
| | `666a34d` | real `gates.yml`, probe workflow **deleted** |
|
||||
| | `3252d51` | **Scenario B** deliberate breakage (`--no-verify`) |
|
||||
| | `dd13f63` | alarm step v1 — died on `curl: command not found` |
|
||||
| | `f7dbc33` | alarm rewritten in python3/urllib |
|
||||
| | `9530de7` | alarm User-Agent (Cloudflare 1010) — **the run that produced the accepted-id** |
|
||||
| | `4cc1238` | **breakage reverted**, main green |
|
||||
| `felhom-controller` | `dcc400e` | `gates.yml` (with sibling clone) |
|
||||
| `felhom-agent` | `eb99144` | `gates.yml` (with sibling clone) |
|
||||
| `app-catalog-felhom.eu` | `aa57588` | `gates.yml` (`--fast`, no sibling needed) |
|
||||
|
||||
Plus the documentation commits listed at the end of this session.
|
||||
|
||||
## 5. Scenario results A-G
|
||||
|
||||
| # | Result | Positive observable |
|
||||
|---|---|---|
|
||||
| **A** | **PASS** | run #2 (`666a34d`) SUCCESS. **Every member gate's own stdout in the CI log**, not just the summary: `site gates OK — BOM, emoji=0…`, `hostinstall gates: ALL PASS`, `hub confirm gate OK…`, `manifest bearer gate OK…`, `OK [hostexecutor]: 61 cited paths — exact 60, suffix 1…`, then `all felhom.eu gates OK` |
|
||||
| **B** | **PASS** | run #3 (`3252d51`) **FAILURE**. The failing gate's own text: `FAIL: hub/internal/web/configs.go:34 carries const hostInstallVersion…`, `hostinstall gates: 3 FAILURE(S)`, `CONVICTED: hostinstall`, `🏁 Job failed` |
|
||||
| **C** | **PASS** | run #6 FAILURE → **`RESEND-ACCEPTED id=5ff34766-c5f8-4588-8104-08296aeb45ab`**. See §6 |
|
||||
| **D** | **PASS** | four repos, four runs, **one runner**: tasks 7/8/9/10 all `runner_id=2 felhom-gates-runner`; conclusions `success` ×4 |
|
||||
| **E** | **PASS** | live pod spec (§8) **and** the refusal in a job log: `/root/.cache/act/…/3.sh: line 2: docker: command not found` → `E-OK docker refused` |
|
||||
| **F** | **PASS** | catalogue run: `image-pin gate OK — 53 templates, 0 unpinned images`; `--fast SKIPPED: image-resolvable, volume-persistence…`; resolvability/volume-persistence/canary output lines **0**, image-pull lines **0** |
|
||||
| **G** | **PASS** | P6: PVC intact → runner list unchanged (`1 felhom-gates-runner`); state removed → **`1` and `2`**, i.e. a second registration. Orphan deleted, `204` |
|
||||
|
||||
**Scenario A note worth keeping:** the controller and agent runs prove more than "it ran" — their
|
||||
reuse tallies in CI (**126 exact / 6 suffix / 1 cross-repo**; **88 / 1 / 1**) are *identical* to the
|
||||
local pre-push hook's. **CI and the hook agree.** Per §12 a disagreement would have outranked this
|
||||
whole task; there is none.
|
||||
|
||||
## 6. Scenario C evidence in full
|
||||
|
||||
The run was genuinely red for a genuinely broken gate, and the message is the observable — not a
|
||||
tick in a UI:
|
||||
|
||||
```
|
||||
felhom.eu core.hooksPath = .githooks
|
||||
felhom-controller core.hooksPath = .githooks
|
||||
felhom-agent core.hooksPath = .githooks
|
||||
app-catalog-felhom.eu core.hooksPath = .githooks
|
||||
CONVICTED: hostinstall
|
||||
RESEND-ACCEPTED id=5ff34766-c5f8-4588-8104-08296aeb45ab
|
||||
🏁 Job failed
|
||||
```
|
||||
|
||||
## 9. Hub deployment
|
||||
The Resend id is the **provider's accepted-id**, returned by `api.resend.com` for a message
|
||||
`monitoring@felhom.eu → admin@felhom.eu`, subject `[felhom CI] gates FAILED in admin/felhom.eu`,
|
||||
carrying the repo, the commit, a link to the run, and the line that matters most: *if the local
|
||||
pre-push hook was green for this commit, CI and the hook disagree, and that outranks whatever the
|
||||
push was for.*
|
||||
|
||||
- **Version:** `gitea.dooplex.hu/admin/felhom-hub:0.87.0`, built and pushed from
|
||||
`/mnt/5_hdd/felhom.eu/build/felhom-hub` after the clean-tree gate (empty `git status --porcelain`,
|
||||
`HEAD == origin/main == 4707be7`) and a green `go build ./... && go vet ./... && go test ./...`.
|
||||
- **ArgoCD:** `manifests/hub.yaml` bumped in git (`8d9b78c`), hard-refresh → `OutOfSync`, then a
|
||||
**deliberate** sync. No `kubectl set image` at any point.
|
||||
Final: `sync=Synced health=Healthy`, revision `8d9b78c153ee…`.
|
||||
- **Rollout:** `deployment "hub" successfully rolled out`; pod `hub-67774ccf4f-74wwx` `1/1 Running`.
|
||||
- **Running image:** `gitea.dooplex.hu/admin/felhom-hub:0.87.0`.
|
||||
- **Pod log excerpt:**
|
||||
**The key never appeared in the log** — Gitea masked it (`RESEND_API_KEY: ***`) and a grep for the
|
||||
key prefix across the run log returns **0 lines**.
|
||||
|
||||
## 7. Runner deployment
|
||||
|
||||
- **GitOps only.** `act-runner.yaml` committed to `homelab-manifests` `main` (`c51b77c`), pushed,
|
||||
then a **deliberate ArgoCD sync** of the `gitea` Application. No `kubectl apply`, no
|
||||
`kubectl set image` at any point.
|
||||
- **ArgoCD:** `sync=Synced health=Healthy`, revision `c51b77c68f10…`.
|
||||
- **Pod:** `act-runner-…` `1/1 Running`; PVC `act-runner-data` **Bound**, 5Gi, `longhorn`.
|
||||
- **Running image:** `gitea.dooplex.hu/admin/felhom-act-runner:0.1.0`, verified by deleting the local
|
||||
copy and **re-pulling from the registry** (`Python 3.12.13`, `git version 2.52.0`) rather than
|
||||
trusting the push's own output.
|
||||
- **Online in Gitea's runner list**, with its label:
|
||||
```
|
||||
[INFO] Storage fill checker initialized: warn=90% crit=95%, 8 ok seeded, 0 already-breached left unseeded, 3 root-backed excluded
|
||||
[INFO] Offsite checker initialized: fill warn=90% crit=95%, stale after 48h0m0s, 3 ok-seeded
|
||||
[INFO] Listening on :8080
|
||||
id=2 name=felhom-gates-runner status=online labels=[felhom-gates]
|
||||
```
|
||||
- **Live read of the Setup tab — method: endpoint-level** (`curl` over the hub ClusterIP with Basic
|
||||
auth; browser automation is not available on DooPlex). `GET /customers/peti-felhom`, 105 164 bytes.
|
||||
The Setup Command card renders:
|
||||
Owner-scoped (`owner_id=1`, `repo_id=0`) — which is why one registration serves four repos.
|
||||
|
||||
> Day-0 host bootstrap. The command always fetches the **current** felhom-host-install.sh from
|
||||
> felhom.eu — there is no version to pick here. Run it on a freshly-PVE-installed Proxmox
|
||||
> **host** as root …
|
||||
## 8. Security posture — shown, not asserted
|
||||
|
||||
**Zero occurrences of `1.19.0` or `1.22.0` anywhere on the page.** The page also served
|
||||
`style.css?v=0.87.0`, independently confirming the new image is the one rendering.
|
||||
From the **live** pod spec:
|
||||
|
||||
```
|
||||
privileged : False
|
||||
allowPrivilegeEscalation : False
|
||||
capabilities.drop : ['ALL']
|
||||
automountServiceAccountToken : False
|
||||
hostNetwork/hostPID/hostIPC : None None None
|
||||
nodeSelector : None
|
||||
volumes : [('data', ['persistentVolumeClaim'])]
|
||||
hostPath volumes : NONE
|
||||
docker socket mounts : NONE
|
||||
serviceaccount token mounts : NONE
|
||||
resources : requests 50m/128Mi, limits 500m/512Mi
|
||||
```
|
||||
|
||||
And the negative proven rather than inferred — from a job log:
|
||||
|
||||
```
|
||||
/root/.cache/act/187d434f3fdbb3b4/act/workflow/3.sh: line 2: docker: command not found
|
||||
E-OK docker refused (output above is the refusal)
|
||||
```
|
||||
|
||||
Resources are **half of Gitea's limits** (Gitea: 1 CPU / 1Gi) and a fifth of its requests,
|
||||
deliberately: Gitea shares this node and holds every repository. The heaviest entry point was
|
||||
measured at **11.9 MiB RSS, 0.98 s wall**.
|
||||
|
||||
## 9. Teardown — all three layers
|
||||
|
||||
1. **Workload** — nothing to remove: the runner is the deliverable and stays. No scratch Deployment
|
||||
or PVC was created beyond it.
|
||||
2. **Host** — no scratch Longhorn volume was created beyond the runner's own 5Gi PVC, which is
|
||||
intended and in git. Local Docker images (`gitea/act_runner:0.6.1` base and the built
|
||||
`felhom-act-runner:0.1.0`) remain in DooPlex's local Docker store; they are ordinary build
|
||||
artifacts and **no global prune was run** (forbidden on this host).
|
||||
3. **The Gitea-side registration records — the layer this project has missed four times.**
|
||||
- The **P6b orphan** (`id=1`) was deleted **immediately when it was created**, not left for the
|
||||
end: `DELETE /api/v1/admin/actions/runners/1 → 204`.
|
||||
- **Final runner list: exactly one, the intended live runner** — see §12 for the closing check.
|
||||
- The **temporary probe workflow** `.gitea/workflows/probe.yml` was deleted from `main` in
|
||||
`666a34d`, the same commit that added the real one.
|
||||
- The **admin API access token** minted for this session (used to read run conclusions and delete
|
||||
the orphan) is revoked at the end of the session; it is stored out-of-band and appears in no
|
||||
file.
|
||||
|
||||
## 10. `OPEN-ITEMS.md` rows opened / closed / re-ranked
|
||||
|
||||
- **R-94 → CLOSED**, all three legs. Leg (a) closed **by deletion, not derivation**, with the reason
|
||||
recorded in the row: the Setup command fetches the installer at run time from a website that
|
||||
git-syncs `main` every 30 s (R-110), so no build-time value in the hub can be true.
|
||||
- **R-29 → leg (a) CLOSED** (`c432f70`, its own reviewed diff); **leg (b) HALF-SHIPPED**. The census
|
||||
is written into the row: thirteen gates; every gate a `CLAUDE.md` names was green, and two of the
|
||||
four nobody names were red. Stays open for the automatic half.
|
||||
- **R-161 → annotated**: `--fast` + hook shipped; the automatic half now points at its successor row.
|
||||
- **R-168 → NEW.** `grep -oE "R-[0-9]+" OPEN-ITEMS.md | sort -u -t- -k2 -n | tail` established
|
||||
**R-167 as the highest in use**, so R-168 was free. Gitea Actions runner for CI; owner CC; state
|
||||
**BLOCKED** on its own spike. It records what was measured on 2026-08-02 — Gitea **1.26.2**,
|
||||
Actions enabled on all four repos, **0 runners, 0 workflow runs, 0 branch protections** — and the
|
||||
consequence: with trunk-based direct-to-`main` pushes there is no merge for a status check to gate,
|
||||
so **CI here can detect but not block**. Its two spike questions: host-mode runner versus a
|
||||
privileged Docker-in-Docker sidecar on DooPlex (Tier 2 — it *is* the recovery chain), and whether
|
||||
the workflow can avoid JavaScript actions.
|
||||
- `ROADMAP.md`: R-94 collapsed to its one-liner, R-29 updated, R-168 added.
|
||||
- **R-168 → SHIPPED**, with the evidence in the row: the runner, the one-registration-four-repos
|
||||
measurement, the two probes that changed the design (P2, P6), and the demonstrated alarm with its
|
||||
accepted-id.
|
||||
- **R-29 → CLOSED**, both halves — and **closed on the demonstrated alarm, not on a green run**, as
|
||||
the task required. The class it opened (*a gate that exists, is red, and is invoked by nothing*) is
|
||||
answered at both ends.
|
||||
- **R-161 → updated**: its automatic half now exists for the static gate. Its *original* scope, the
|
||||
runtime volume-persistence gate, is deliberately still **not** automatic and should stay that way.
|
||||
- **R-169 → NEW.** `grep -oE "R-[0-9]+" | sort -u -t- -k2 -n | tail` established **R-168 as the
|
||||
highest in use**, so R-169 was free. *CI can only report, because there is no gate in the road* —
|
||||
making it blocking needs branch protection plus a PR workflow, both of which change how the
|
||||
operator works. **WAITING-ON-OPERATOR**, owner: operator. The row states the cost honestly (for a
|
||||
single-operator project a PR workflow may be worse than the disease) and names the real gap: the
|
||||
window between a `--no-verify` push landing and the operator reading the alarm.
|
||||
- `ROADMAP.md`: R-168 collapsed to its one-liner, R-29 closed, R-169 added.
|
||||
- **`architecture/00-capability-map.md` — N/A, stated rather than silently skipped.** No
|
||||
customer-visible or platform capability changed: this task alters tooling, one operator-facing
|
||||
sentence, and documentation.
|
||||
customer-visible or platform capability changed; this is developer tooling.
|
||||
|
||||
## 11. Teardown
|
||||
## 11. Was `--no-verify` used?
|
||||
|
||||
**Provisioned nothing.** No VM, no guest, no container and no host was created or modified. The only
|
||||
scratch artifacts were three throwaway git repos under the session scratchpad (the `core.hooksPath`
|
||||
probe, the Scenario C clone, and its bare origin) — disposable, and touching nothing real.
|
||||
**Yes — three times, all on `felhom.eu`, all deliberate and all disclosed.**
|
||||
|
||||
## 12. Was `--no-verify` used?
|
||||
| Commit | Why |
|
||||
|---|---|
|
||||
| `3252d51` | **Scenario B itself.** The commit had to carry a broken gate, and pushing it with `--no-verify` *is* the bypass CI exists to catch. The local hook would have refused it — that is the point. |
|
||||
| `dd13f63` | The tree was still deliberately broken; pushing the alarm step required bypassing the hook that was correctly objecting. |
|
||||
| `f7dbc33`, `9530de7` | Same, iterating on the alarm while the tree was still red on purpose. |
|
||||
|
||||
**Once, deliberately, and only inside the throwaway Scenario C clone** whose `origin` was a scratch
|
||||
bare repo — that use *was* the test of the escape hatch (§7, C2). **It was never used against a real
|
||||
repository.** All five real pushes ran their pre-push hook, and it passed each time.
|
||||
Every other push in this session ran its pre-push hook and passed. The breakage was reverted in
|
||||
`4cc1238`, and the hook passed on that push.
|
||||
|
||||
## 13. Observations — noticed, recorded, NOT acted on
|
||||
## 12. Observations — noticed, recorded, NOT acted on
|
||||
|
||||
1. **The census held, with one thing worth stating plainly.** Everything §4.9 said was reproduced:
|
||||
`hostinstall_gates` RED, `reuse_refs_check` RED on all four repos with exactly 13 findings,
|
||||
`docker_run_volume_path_gate` RED with exactly one finding, and every gate named by a `CLAUDE.md`
|
||||
green. Since the "two of the four" headline can be read as "two red gates", the arrival state was
|
||||
in fact **three** red scripts — and they were exactly the two named nowhere plus the one named
|
||||
only in `REUSE.md:284`. The correlation is unchanged and slightly stronger than the headline.
|
||||
2. **DEVIATION — `scripts/felhom-host-install.sh` was edited, which §12 fenced.** Comment only;
|
||||
`SCRIPT_VERSION="1.22.0"` is untouched and no behaviour changed. Its comment asserted that
|
||||
`hostinstall_gates.py` "asserts the two stay equal" — an invariant this task deleted. Leaving it
|
||||
would have shipped exactly the class `CLAUDE.md` names ("a comment asserting an invariant needs a
|
||||
test pinning it, or it is a wish"), so the fence — whose stated reason is that `SCRIPT_VERSION` is
|
||||
the single source and stays — was read as fencing the act, not the object. Recorded here so the
|
||||
decision is visible rather than absorbed silently.
|
||||
3. **DEVIATION — `.gitignore` gained `__pycache__/`.** Not in the spec. The new fixture tests import
|
||||
the checker by path, which writes bytecode into `scripts/`, and an untracked directory there would
|
||||
have failed the clean-tree gate before every future build.
|
||||
4. **A `go test -run` pattern that matches nothing prints `ok` and exits 0.** My first attempt at
|
||||
red-proof #3 used `-run TestCustomerUnified`, which matches no test in `render_test.go`, and
|
||||
`go test` reported `ok … 0.062s`. Caught and re-run against the real name. This is another
|
||||
instance of the "an absent line is not evidence" class already in `CLAUDE.md`, and it is worth its
|
||||
own line because it makes a **red-proof itself hollow** — the one place a false green costs most.
|
||||
5. **`hub/CHANGELOG.md` and `scripts/CHANGELOG.md` still contain historical `hostInstallVersion`
|
||||
mentions.** Correct and left alone: the rewritten gate matches code shapes, never prose, so the
|
||||
history is safe to keep and the gate stays green.
|
||||
6. **Not acted on, out of scope.** `manifest_bearer_gate.py` prints
|
||||
`manifests/felhom.secret.yaml:39 KNOWN-BACKLOG committed secret …` on every run and still exits
|
||||
0. It is a tracked backlog item (`documentation/runbooks/secrets.md`) and the gate deliberately
|
||||
does not fail on it — but it means every green `repo_gates.py` run now carries a line that reads
|
||||
like a finding, in front of a reader who did not previously see it. Left untouched, per "do not
|
||||
tighten or refactor a currently-green gate".
|
||||
1. **CI and the pre-push hook never disagreed.** §12 said a disagreement would outrank the CI work;
|
||||
there was none. The controller's and agent's reuse tallies in CI match the local run exactly. The
|
||||
one place they *would* have disagreed was designed out rather than discovered: without a sibling
|
||||
`felhom.eu` clone, both entry points fail closed with `gate is MISSING`.
|
||||
2. **A near-miss that outranks a probe result.** My first census query reported Actions enabled on
|
||||
five *unrelated* repos — a baseline drift big enough to change the task. It was a `| tail -5`
|
||||
inside my own helper truncating the output. The measurement was never wrong; the instrument was.
|
||||
This is the same class as the `go test -run` filter from session 1, and it is now a `CLAUDE.md`
|
||||
rule: **an instrument that can silently drop results is not a measurement.**
|
||||
3. **Two failures on the way to the alarm, both worth their write-up** — `curl: command not found`
|
||||
(the image is minimal on purpose; fixed with `urllib`, not a bigger image) and **Cloudflare 403
|
||||
error 1010** on `api.resend.com`, which blocks the default `Python-urllib` User-Agent. The second
|
||||
matters because **it looks exactly like an auth failure**: the next person to see a 403 from
|
||||
Resend should check the User-Agent before rotating a key. Both are in the spike doc and in
|
||||
`CONTEXT.md` S-9.
|
||||
4. **A full clone is slow enough to notice** — the P3 probe's `git clone` of `felhom.eu` took ~48 s.
|
||||
The shipped workflows use `git fetch --depth 1 <sha>` instead, which is both faster and pinned to
|
||||
the exact pushed commit. Not a finding, but the reason the shipped form differs from the probe's.
|
||||
5. **Not acted on, out of scope:** Gitea's own mailer is configured and enabled but was never proven
|
||||
to deliver (§2). If it *is* broken, nothing else in Gitea that relies on email works either —
|
||||
worth a five-minute test email from the admin UI at some point. It does not affect the CI alarm,
|
||||
which does not use it.
|
||||
6. **Not acted on, out of scope:** `homelab-manifests` has no gate entry point and no pre-push hook,
|
||||
unlike the four product repos, and now no CI workflow either. It is the repo that deploys the
|
||||
cluster, so that asymmetry may deserve a look — but adding one was not in this task, and the repo
|
||||
has no gates to run yet.
|
||||
|
||||
Reference in New Issue
Block a user