burn-down Part B: 14 small rows fixed and closed across four repos (306 -> 292); no :latest in the hub build; gate list pinned; closed-id duplicates refused; R-262 subset pinned
gates / gates (push) Failing after 1m40s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 17:10:38 +02:00
parent f5a0aeb0b8
commit b26d292a64
19 changed files with 220 additions and 35 deletions
@@ -0,0 +1,6 @@
### R262-a: drop skipped from knownUnmodelled
--- FAIL: TestR262_RestoreTestFieldsAreAKnownSubset (0.00s)
FAIL
### R262-b: the hub stops decoding source_tier
--- FAIL: TestR262_RestoreTestFieldsAreAKnownSubset (0.00s)
FAIL
@@ -0,0 +1,9 @@
### R263-a: ClearBackupTarget writes true
1700: s.StoragePaths[i].BackupTarget = true
--- FAIL: TestR263_OnlySetBackupTargetGrantsTheRole (0.24s)
r263_backup_target_writers_test.go:73: BackupTarget may be granted outside SetBackupTarget at: [../settings/settings.go:1700:3]
FAIL
### R263-b: a composite literal BackupTarget: true in another package
--- FAIL: TestR263_OnlySetBackupTargetGrantsTheRole (0.28s)
r263_backup_target_writers_test.go:73: BackupTarget may be granted outside SetBackupTarget at: [../web/zz_r263_decoy.go:5:50]
FAIL
+14
View File
@@ -62,6 +62,20 @@ The full text of every row below: `git show ab2b3049:documentation/backlog/OPEN-
| **R-818** | **Two changelogs cite register ids for other findings.** (P4) | CLOSED 2026-10-05 — CORRECTED | Dated correction notes under hub v0.109.0 (`hub/CHANGELOG.md`) and controller v0.224.0 + v0.225.0 (`felhom-controller/CHANGELOG.md`): those two findings never had register rows of their own — the triage's „the real ids are in CLOSED-ITEMS" was itself wrong (no closed row names hub v0.109.0 or controller v0.224.0/v0.225.0). Nothing renumbered. |
| **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-762 (its unique fact moved there) | Still true: templates/wger/docker-compose.yml has no WGER_USE_GUNICORN (grep empty). R-762 (open, read) states 'Owner decides together with R-755 (same server question)' and its fix names 'the gunicorn switch of R-755'. |
| **R-446** | **[P2-MEDIUM] „Naprakész" can be FALSE, and the badge that says it cannot tell.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-440 (its unique fact moved there) | felhom-controller/controller/internal/stacks/updateorder.go:96: `if len(s.CatalogDigests) == 0 // s.CatalogTestedAt.IsZero() { return false }` — blind only for apps with no ladder entry, i.e. the same 15 templates R-440 lists (app-catalog has no update_ladder for them). Both rows close by the same act: each app's first proven ladder step (R-462). |
| **R-799** | **[P3-LOW] The MeTube fixture's `POST /add` leaves out `download_type`, which upstream's validator lists as required.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: `MeTube.add_body()` sends `download_type: video`; `scripts/test_upgrade_fixtures_metube.py` (red-proof: the field removed → FAIL). Not exercised on a box (the next MeTube step will). |
| **R-761** | **[P3-LOW] The canonical example template tells a new app's author the logo is `<slug>-logo.webp`; the controller loads `<slug>-logo.svg`, then `.png`.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: the canonical template comment, `REUSE.md` and `NEW-APP-CHECKLIST.md` name `{slug}-logo.svg` then `.png` (controller `config.go` AppLogoURL/AppLogoPNGURL). Comment-only. |
| **R-391** | **Gate 11 (observations) is registered in three of the four runners; `app-catalog-felhom.eu` is the exception.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: `CLAUDE.md` states its `REPORT.md` carries no observations section by convention (the row's second option); the shared gate was not copied. |
| **R-291** | **CI's installability assertion is now BOUNDED by a retention number, and the narrowing is recorded here so it can be widened deliberately rather than discovered.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-agent `d833163`: `scripts/retention-policy.json` names the source of its 10 — the R-267 newest-10 prune, established 2026-08-10 (R-287) — and drops the non-existent `registry-retention.md` reader; `check-published-versions.py` still reads 10 (checked). The min_agent-floor bound stays recorded in the file as the better bound. |
| **R-348** | **Every agent restart blanks the reported backup list for up to ~18 hours, and the comment that covers it says "unaffected".** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-agent `d833163`: `internal/backup/store.go` says a restart blanks the reported backup list until the next run; only the hub's verdict (7-day look-back) is unaffected. Comment-only. |
| **R-263** | **C7 — „This is the ONLY writer of `StoragePath.BackupTarget`" is false, and nothing pins it.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-controller `114ff27`: comment „the only writer that GRANTS"; `internal/settings/r263_backup_target_writers_test.go` scans every non-test file under internal/ and cmd/ (assignments and composite-literal keys). Red-proofs: ClearBackupTarget writing true; a `BackupTarget: true` literal in internal/web — both convict (`audits/burndown-2026-10-05/r263-red-proof.txt`). |
| **R-368** | **The storage default DOES apply at deploy time — the earlier claim that it never does was wrong, and the residual defect is smaller and different.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-controller `114ff27`: the `IsDefault` comment says the deploy FORM pre-selects it and the deploy API applies no default (the row's second option; behaviour unchanged on purpose). |
| **R-418** | **`repo_gates.py`'s docstring listed ELEVEN gates while THIRTEEN were registered** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `repo_gates.py` docstring lists all 17 gates; `scripts/test_repo_gates_docstring.py` asserts list == GATES in order (red-proof: one line removed → FAIL), run on every push by the script-tests gate. |
| **R-345** | **`hub/Makefile` tags and pushes `:latest`, which the project's own rules forbid in two places.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `hub/Makefile` AND the real release script `scripts/build-hub.sh` (3 lines; the build dir links to it) no longer tag or push `felhom-hub:latest` — nothing pulls it (grep of all repos + homelab-manifests). `scripts/test_no_latest_push.py` (walk of hub/ + scripts/; red-proofs: the old Makefile and the old build-hub.sh each FAIL). Whether a stale `:latest` sits on the registry was not checked. |
| **R-416** | **`closed_register_gate.py` still has no within-register duplicate-id rule.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `closed_register_gate.py` RULE 4 refuses an id twice in CLOSED-ITEMS.md (OPEN duplicates were already `register_shape_gate.py` RULE 3); 0 duplicates existed, so it registered green. Decoy `closed-register/duplicate-closed-id` (red-proof: RULE 4 off → LIVE HOLE). |
| **R-261** | **C6 — `CountSelfBindTokens` exists so that callers can assert an invariant, and no production caller asserts it.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `hub/internal/store/selfbind.go` names `CountSelfBindTokens` a test accessor and the two tests that pin the auto-mint invariant. Comment-only; no hub release needed. |
| **R-262** | **C7 — a comment claims a cross-repo contract is mirrored „field-for-field" and „the key-set tests guard drift"; it is two fields short, AND THE FIXTURE THE TEST READS OMITS THE SAME TWO FIELDS.** (P3) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: the hub comment says hostRestoreTest is a deliberate SUBSET; `hub/internal/api/r262_restoretest_subset_test.go` pins the hub fields and the known-unmodelled agent fields and cross-checks the agent source beside it — which found a THIRD unmodelled field, `skipped` (agent v0.133.0, R-672; by design a skipped test reads as failed with its reason). Red-proofs: drop `skipped` from the list / stop decoding source_tier → FAIL. |
| **R-286** | **A control drawn from the same channel as the measurement cannot detect a defect in that channel — and this one passed while the measurement was wrong.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: workspace standing rule 3 (both `CLAUDE.md` copies, identical) adds: a control must come from a DIFFERENT channel than the measurement; a hub-state check copies `hub.db-wal` or asks the running pod. |
| **R-588** | **[P3-LOW] ISO release records live in two different places, so "was the gate run for this image?" cannot be answered by looking.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `runbooks/iso-release-gate.md` names `documentation/tests/iso-release-<ver>-<date>/` as the one home; `tests/iso-release-1.28.0-2026-09-16/README.md` points at the 1.28.0 record inside the 2026-09-16 audit. |
---
File diff suppressed because one or more lines are too long
@@ -320,3 +320,8 @@ record.
Record each criterion as PASS/FAIL **with the observed value and what was scanned for**, in the
release report. A criterion with no recorded observation is a criterion that was not run.
**The ONE home for a release's record is `documentation/tests/iso-release-<version>-<date>/`** (R-588,
2026-10-05) — one directory per published ISO, so „was the gate run for this image?" is answered by looking,
not by a full-text search for a checksum. A record made elsewhere (inside an audit) gets a directory here
whose `README.md` points at it. Today: 1.27.0, 1.27.1, 1.28.0 (pointer), 1.29.0.
+7 -1
View File
@@ -54,7 +54,13 @@ roles. **A file being open in the editor is NOT an instruction. If no task is st
attempts.
3. **An absent log line is not evidence of correct behaviour.** Verify with a POSITIVE observable —
something that MUST appear when the system is healthy. An empty log is equally consistent with
"working" and "stopped entirely".
"working" and "stopped entirely". **And the control must come from a DIFFERENT channel than the measurement** (R-286):
same query, same snapshot, same API or same clock all share the defect they are meant to catch — a
stale hub snapshot once "confirmed" itself (2 events all day) while the operator's mailbox held eight
alarms. A hub-state check copies `hub.db-wal` too, or asks the running pod. **And the control must come from a DIFFERENT channel than the measurement** (R-286):
same query, same snapshot, same API or same clock all share the defect they are meant to catch — a
stale hub snapshot once "confirmed" itself (2 events all day) while the operator's mailbox held eight
alarms. A hub-state check copies `hub.db-wal` too, or asks the running pod.
4. **A recommendation that is not followed gets one line saying why.** Silence reads as agreement and
the disagreement is lost.
5. **Evidence is copied off the machine at the end of the phase that produced it — before any revert,
@@ -0,0 +1,9 @@
# ISO release 1.28.0 — 2026-09-16 (pointer)
The gate record for this image was made inside an audit about something else, before the one-home rule
(`runbooks/iso-release-gate.md` „Result recording", R-588):
- `documentation/audits/evidence-backup-promise-2026-09-16/phaseD-iso-gate.txt` — run 2026-09-16T15:07:32Z against
`felhom-installer-1.28.0-pve9.2-1.iso`, sha256 `a4cd9b6ddcb55bae3700ab307084d2b699330cc20710688d5816318f04f6d635`.
Nothing was re-run for this pointer; it only makes the record findable by looking.
+11
View File
@@ -1,3 +1,14 @@
## unreleased — comments, a pinned subset and the build without `:latest`; no image change (burn-down 2026-10-05: R-261, R-262, R-345)
The next hub release carries these lines into its own entry. Nothing here changes the running hub.
- **R-262:** `hostRestoreTest` is a deliberate SUBSET of the agent's `RestoreTest` — `mount_parity`, `mount_inventory`
and `skipped` are not modelled (a skipped test reads as failed with its reason, by the agent's design).
`internal/api/r262_restoretest_subset_test.go` pins both lists and cross-checks the agent source when it sits beside
this repo; it found `skipped`, which the comment had not named.
- **R-261:** `CountSelfBindTokens` is documented as the test accessor it is.
- **R-345:** `Makefile` `docker-push` no longer tags or pushes `:latest` (nor does `scripts/build-hub.sh`).
## v0.136.0 — the hub writes a nightly, checked copy of its own database (R-173, decision A) (2026-10-05)
**Operator action on deploy: none.** The hub's volume grows from 1 GiB to 2 GiB and joins Longhorn's nightly backup
+1 -2
View File
@@ -18,8 +18,7 @@ docker:
docker-push: docker
docker push $(IMAGE):$(VERSION)
docker tag $(IMAGE):$(VERSION) $(IMAGE):latest
docker push $(IMAGE):latest
# never :latest (R-345) — the manifest pins a version; a moving tag is how a restart upgrades by surprise
clean:
rm -rf bin/
+3 -2
View File
@@ -724,8 +724,9 @@ type hostPBSSnapshot struct {
VerifyUPID string `json:"verify_upid,omitempty"`
}
// hostBackup / hostRestoreTest mirror the agent's hub.Backup / hub.RestoreTest wire
// contract field-for-field (slice 6, doc 03 §8). DUPLICATED contract — the golden stays
// hostBackup mirrors the agent's hub.Backup wire contract field-for-field; hostRestoreTest is a deliberate
// SUBSET of hub.RestoreTest — the agent's mount_parity, mount_inventory and skipped are NOT modelled here (R-262,
// pinned by TestR262_RestoreTestFieldsAreAKnownSubset) (slice 6, doc 03 §8). DUPLICATED contract — the golden stays
// byte-identical with felhom-agent's copy and the key-set tests guard drift. The hub
// persists these via report_json (no new columns this slice) and surfaces a FAILED
// restore-test prominently (the loudest DR signal). The rich backup policy is slice 10.
@@ -0,0 +1,58 @@
package api
import (
"os"
"reflect"
"regexp"
"sort"
"strings"
"testing"
)
// R-262: hostRestoreTest is a deliberate SUBSET of the agent's hub.RestoreTest. This pins WHICH subset: the hub decodes
// exactly `hubRestoreTestFields`, and the agent fields it leaves out are exactly `knownUnmodelled`. When the agent's
// source is beside this repo (the workspace), its RestoreTest json tags must equal the union — so an agent field added
// later fails here until someone decides to model it or to list it as unmodelled. RED-PROOF: drop a field from the
// struct, or delete an entry from knownUnmodelled → this test fails.
func TestR262_RestoreTestFieldsAreAKnownSubset(t *testing.T) {
// skipped (agent v0.133.0, R-672): a skipped test arrives as Pass=false with Error "skipped: …", which the hub
// reads as a failed test with that reason — the agent's own comment calls that the honest reading.
knownUnmodelled := []string{"mount_inventory", "mount_parity", "skipped"}
var got []string
rt := reflect.TypeOf(hostRestoreTest{})
for i := 0; i < rt.NumField(); i++ {
got = append(got, strings.Split(rt.Field(i).Tag.Get("json"), ",")[0])
}
sort.Strings(got)
want := []string{"duration_seconds", "error", "pass", "scratch_vmid", "source_archive", "source_tier",
"tested_at", "verified", "warnings", "warnings_recognized"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("hostRestoreTest json fields = %v, want %v", got, want)
}
for _, f := range knownUnmodelled {
for _, g := range got {
if g == f {
t.Fatalf("%s is listed as unmodelled but hostRestoreTest decodes it — update knownUnmodelled", f)
}
}
}
src, err := os.ReadFile("../../../../felhom-agent/internal/hub/report.go")
if err != nil {
t.Logf("agent source not beside this repo (%v) — the cross-repo half is not checked here", err)
return
}
body := regexp.MustCompile(`(?s)type RestoreTest struct \{(.*?)\n\}`).FindSubmatch(src)
if body == nil {
t.Fatal("type RestoreTest struct not found in felhom-agent/internal/hub/report.go")
}
var agent []string
for _, m := range regexp.MustCompile("json:\"([a-z_]+)").FindAllSubmatch(body[1], -1) {
agent = append(agent, string(m[1]))
}
sort.Strings(agent)
union := append(append([]string{}, want...), knownUnmodelled...)
sort.Strings(union)
if !reflect.DeepEqual(agent, union) {
t.Fatalf("agent RestoreTest fields %v != hub fields + knownUnmodelled %v — model the new field or list it", agent, union)
}
}
+5 -4
View File
@@ -104,10 +104,11 @@ func (s *Store) DeleteSelfBindTokens(customerID string) error {
}
// CountSelfBindTokens reports how many capability tokens exist for a customer (v0.67.0). Minting is
// single-active (delete-then-insert), so this is 0 or 1 in practice; it exists so callers can assert
// the "after this runs, the only live link is one we just issued — or none" invariant that the
// auto-mint at customer-create / RESET-completion depends on. Read-only, no oracle risk: it is keyed
// by customer id, which the operator already knows.
// single-active (delete-then-insert), so this is 0 or 1 in practice. It is a TEST ACCESSOR (R-261): no
// production code calls it. The "after this runs, the only live link is one we just issued — or none"
// invariant the auto-mint at customer-create / RESET-completion depends on is pinned by
// web/selfbind_automint_test.go and web/customer_delete_test.go, which call it. Read-only, no oracle risk:
// it is keyed by customer id, which the operator already knows.
func (s *Store) CountSelfBindTokens(customerID string) (int, error) {
var n int
err := s.db.QueryRow(`SELECT COUNT(*) FROM selfbind_tokens WHERE customer_id = ?`, customerID).Scan(&n)
+8
View File
@@ -1,3 +1,11 @@
## build + gates — no `:latest`; the gate list pinned; duplicate closed ids refused (2026-10-05, burn-down)
- **R-345:** `build-hub.sh` no longer tags or pushes `felhom-hub:latest` (`--push`, `--multiarch`, local); nothing
pulls it. `test_no_latest_push.py` walks `hub/` and `scripts/` build files (red-proofs: the old Makefile, the old
build script).
- **R-418:** `repo_gates.py`'s docstring lists all 17 gates; `test_repo_gates_docstring.py` keeps list == `GATES`.
- **R-416:** `closed_register_gate.py` RULE 4 — an id twice in `CLOSED-ITEMS.md`; decoy in `test_gate_decoys.py`.
## gates — `script-tests`: every Python test suite under scripts/ runs on every push (R-885) (2026-10-05)
- `scripts/script_tests_gate.py` (registered in `repo_gates.py`, fast): walks `scripts/` for `test_*.py` and runs each;
+1 -4
View File
@@ -147,12 +147,11 @@ case "${ACTION}" in
info "Building for current platform + pushing..."
docker build "${BUILD_ARGS[@]}" \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
.
info "Pushing..."
docker push "${IMAGE}:${VERSION}"
docker push "${IMAGE}:latest"
# never :latest (R-345): the manifest pins a version, and nothing pulls hub:latest (checked 2026-10-05)
;;
--multiarch)
@@ -169,7 +168,6 @@ case "${ACTION}" in
docker buildx build "${BUILD_ARGS[@]}" \
--platform linux/amd64,linux/arm64 \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
--push \
.
;;
@@ -178,7 +176,6 @@ case "${ACTION}" in
info "Building for current platform (local only)..."
docker build "${BUILD_ARGS[@]}" \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
.
;;
esac
+13 -3
View File
@@ -50,9 +50,9 @@ WHAT THIS GATE CANNOT SEE — the residual holes, named rather than implied:
printed as a WARNING.
3. **A closed-sounding verdict that is not true escapes.** `PARTLY CLOSED` leads with no open word.
This gate checks where a row FILED, never whether the verdict is honest.
4. **A duplicate id WITHIN one register escapes.** `OPEN-ITEMS.md` carries two unrelated findings
both numbered R-133 (filed as R-406). Adding that rule would fail the gate on a pre-existing
defect, and a registered-but-failing gate refuses every push, so it was deliberately left out.
4. ~~A duplicate id WITHIN one register escapes.~~ **Closed 2026-10-05 (R-416):** in `OPEN-ITEMS.md`
`register_shape_gate.py` RULE 3 refuses it; in `CLOSED-ITEMS.md` this gate's RULE 4 does (no duplicates
existed when it was added, so it was registered green).
5. Nothing here reads audits, spikes or inventories. A finding that never reaches either register
is invisible to this gate, as it is to `one_register_gate.py`.
@@ -153,6 +153,16 @@ def main():
for rid in sorted(closed_ids & set(open_ids), key=lambda r: (int(re.sub(r"\D", "", r)), r)):
convicted.append((open_ids[rid], rid, "", "has a row in BOTH registers"))
# RULE 4 — a duplicate id WITHIN CLOSED-ITEMS.md (2026-10-05, R-416). The open register's duplicates are
# register_shape_gate.py's RULE 3; this file had no such rule, so two closed rows under one id (the R-133/R-406
# shape) would make `git show` of "the row that closed R-n" ambiguous. Suffixed ids (R-88a, R-88b) are distinct.
first_seen = {}
for n, rid, _, _ in rows(CLOSED):
if rid in first_seen:
convicted.append((n, rid, "", "CLOSED-ITEMS.md has this id twice (first at line %d)" % first_seen[rid]))
else:
first_seen[rid] = n
# RULE 3 — a finished row left in the OPEN register (2026-10-03)
finished_in_open = []
for n, rid, columns, cells, _ in register_table.rows(OPEN):
+2
View File
@@ -24,6 +24,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
13. observations a REPORT.md observation with no register row behind it (R-389)
14. register-shape a register row whose state cell was eaten, a duplicated id, or a blank line
splitting the table — the register mis-stating how many findings exist (R-627)
15. script-tests every Python test suite under scripts/ (found by a walk), by exit code (R-885)
16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421)
**THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already —
it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so
+15
View File
@@ -25,6 +25,7 @@ Run from the repo root: python3 scripts/test_gate_decoys.py
Exit 0 all decoys rejected · 1 a decoy passed (a live hole).
"""
import io
import re
import json
import os
import shutil
@@ -217,6 +218,20 @@ decoy("closed-register/unreadable-row", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **R-905** | A row with no state cell at all. |\n"))
# --- closed-register RULE 4 (2026-10-05, R-416): the same id twice in CLOSED-ITEMS.md ---------------
# The id is read from the file's FIRST closed row at run time, so the decoy duplicates a real id rather than an
# invented one that could never collide.
def _first_closed_id():
for line in io.open(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"), encoding="utf-8"):
m = re.match(r"^\| \*\*(R-\d+[a-z]?)\*\* \|", line)
if m:
return m.group(1)
decoy("closed-register/duplicate-closed-id", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **%s** | The same id again. | CLOSED 2026-10-05 | none |\n" % _first_closed_id()))
# --- closed-register RULE 3 (2026-10-03): a FINISHED row left in the OPEN register ---------------
# On 2026-10-03 the open register held 113 rows whose leading verdict was finished — a quarter of the
# file. The decoy is that exact shape: a row a session closed in place and never moved. The genuine
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""R-345: nothing in this repo's build tooling tags or pushes an image as `:latest`.
Scans hub/Makefile, scripts/build-hub.sh and every Dockerfile/Makefile/*.sh under hub/ and scripts/ (a walk).
Run: python3 scripts/test_no_latest_push.py"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BAD = re.compile(r"^(?![ \t]*#)[^\n]*?(\bdocker\s+(tag|push)\b[^#\n]*:latest\b|-t\s+\S*:latest\b)", re.M)
hits, scanned = [], 0
for top in ("hub", "scripts"):
for dp, dns, fns in os.walk(os.path.join(ROOT, top)):
dns[:] = [d for d in dns if d not in (".git", "__pycache__", "node_modules")]
for f in fns:
if f in ("Makefile", "Dockerfile") or f.endswith(".sh"):
p = os.path.join(dp, f)
scanned += 1
for m in BAD.finditer(open(p, encoding="utf-8", errors="replace").read()):
hits.append("%s: %s" % (os.path.relpath(p, ROOT), m.group(0).strip()))
if scanned < 5:
print("FAIL: scanned only %d build files — the scope is wrong" % scanned)
sys.exit(1)
if hits:
print("FAIL: a :latest tag/push in build tooling (R-345):\n " + "\n ".join(hits))
sys.exit(1)
print("OK: %d build files, no docker tag/push of :latest" % scanned)
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""R-418: repo_gates.py's docstring list of gates names exactly the gates in GATES, in the same order.
It drifted twice (eleven listed while thirteen ran, 2026-08-24..09-01; then fifteen while seventeen ran). Run:
python3 scripts/test_repo_gates_docstring.py"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import repo_gates # noqa: E402
listed = re.findall(r"^ {1,3}\d+b?\. +([a-z][a-z0-9-]+) ", repo_gates.__doc__, re.M)
registered = [g[0] for g in repo_gates.GATES]
if listed != registered:
print("FAIL: the docstring lists %d gate(s), GATES registers %d" % (len(listed), len(registered)))
print(" only listed: %s" % sorted(set(listed) - set(registered)))
print(" only registered: %s" % sorted(set(registered) - set(listed)))
if set(listed) == set(registered):
print(" (same set, different order)")
sys.exit(1)
print("OK: the docstring lists the %d registered gates, in order" % len(registered))