Files
felhom.eu/scripts/iso_bootstrap_gate.py
T
admin 970616b72b
gates / gates (push) Failing after 11m41s
New apps 2026-10-10: Grocy and LubeLogger on the website; Monica stopped
The catalogue side is app-catalog-felhom.eu b7f0f7c. Here: the evidence, the
website, the register and the operator's view.

Website
- Two cards in the Otthon & Eletmod / Home & Lifestyle section of BOTH apps
  pages, with assets: grocy-logo.svg is grocy's own icon with its single fill
  made white like the other logos, lubelogger-logo.png is the app's own icon
  with its dark background dropped and the mark made white (the rule
  SparkyFitness's PNG follows), and six screenshots of each app's own UI with a
  household's own data, taken headless on the bench from the published template.
- The app count moved 56 -> 58 in 15 places per language set, both languages,
  and the open-source tile 49 -> 51. Checked by asking the same patterns for the
  new number afterwards. marketing/facebook/COPY.md still says 56 and is NOT
  changed: the post it carries is already scheduled.

Evidence
- documentation/audits/new-apps-2026-10-10/ — FIT.md (checklist group 0 for all
  three, with the Hungarian-UI column), the bench and box transcripts, the
  memory samples, the screenshots and the gate runs.

Register: 137 -> 139 rows, 2 opened, 0 closed.
- R-926 after a remove-keeping-backups and restore, nobody has checked what the
  app page shows for an after_install app's generated password. Measured here:
  LubeLogger is safe (its login is derived from the environment at every start,
  the new password signs in, the data is back); grocy's install password still
  signs in from the restored database but the deployed environment no longer
  carries ADMIN_PASSWORD at all. Seven apps are in the class.
- R-927 Monica, stopped at checklist 0.2 with the measurements, waiting on the
  operator.

Gate scripts: the same console trap in nineteen of them and in repo_gates.py
itself, where it ABORTED THE WHOLE RUNNER at the first gate — printing a
non-ASCII character on this workstation's cp1250 console raised
UnicodeEncodeError before the gate had decided anything, and reuse_refs_check.py
died while printing a NOTE. All now reconfigure their own streams. Red-proof
that the remaining script-test failures are not mine: test_due_checks_gate.py
fails the same 5 of 42 with the change reverted.
2026-10-10 12:34:05 +02:00

170 lines
8.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""iso_bootstrap_gate.py — the ISO first-boot harness, run as a gate (R-502, decision 147).
Usage: python3 scripts/iso_bootstrap_gate.py
Exit 0 harness green AND its planted-broken copy convicted · 1 a harness check failed, or the harness
could not see a planted broken banner · 2 NOT CHECKED (no docker, no image, docker itself failed).
WHAT IT RUNS. scripts/iso/test/bootstrap-modes.sh — felhom-bootstrap.sh's two modes, the network gate,
the console banners against their goldens and the postinst — inside `felhom-iso-assistant:trixie`.
Until this gate it ran only by hand, and it was RED for two days before anyone saw (R-586).
WHERE IT RUNS. Full runs only — `fast=False` in repo_gates.py, so never in --fast, never in the
pre-push hook, never in CI (both call --fast; CI has no docker). Operator ruling 2026-10-06, `09` §3
decision 147. Pinned by scripts/test_iso_bootstrap_gate.py (`test_registered_full_runs_only`).
NOT CHECKED IS NEVER A PASS. No docker binary, no image, or a docker error (exit 125-127, a timeout)
is exit 2 with the words "not checked". The image is built by hand:
docker build -f scripts/iso/Dockerfile.assistant -t felhom-iso-assistant:trixie scripts/iso
THE TREE STAYS CLEAN. The harness writes into /work (fakes, logs, /etc/felhom). It never sees the repo:
the four inputs are copied to a temp dir, mounted READ-ONLY at /src, and copied to /work INSIDE the
container. `--network none`: the harness fakes curl and needs no network.
THE BUILT-IN DECOY (the positive control, every run). After a green run the gate runs the harness a
second time against a copy of felhom-bootstrap.sh whose print_pairing_banner returns at once — the
exact shape that left the household's first screen untested until ISO 1.27.0 (R-496). The harness
MUST fail it, naming the banner. If it passes, the instrument is blind and the green run proved
nothing: exit 1. A green run is also required to show its own pass line AND at least MIN_OK `ok:`
lines — a harness that printed the pass line and checked nothing is not green.
"""
import os
import re
import shutil
import subprocess
import sys
import tempfile
import uuid
# A gate's own console must not decide its verdict. These scripts quote back Hungarian copy, file
# paths and arrow characters; a Windows console here is cp1250, and printing one of them raised
# UnicodeEncodeError *before the gate had decided anything* — reuse_refs_check.py died while printing
# a NOTE, which the runner then reported as a failure (2026-10-10). Same trap class as
# poster_facts_gate.py's, which was found by its own red-proof.
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError, OSError): # pragma: no cover - old Python, or a pipe
pass
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ISO = os.path.join(ROOT, "scripts", "iso")
IMAGE = "felhom-iso-assistant:trixie"
TIMEOUT_S = 300
# The harness carried 76 `ok:` checks on 2026-10-06. Far below that, deliberately: this is the floor
# of "it ran", not a count to keep in step.
MIN_OK = 40
PASS_LINE = "ALL BOOTSTRAP-MODE TESTS PASSED"
# The planted decoy: the banner function returns before painting.
MUTANT_ANCHOR = "print_pairing_banner() {\n"
MUTANT_TEXT = MUTANT_ANCHOR + " return 0 # R-502 planted decoy: the banner never paints\n"
# A FAIL line the mutant must produce (ASCII fragment of the harness's own check name).
MUTANT_MUST_FAIL = re.compile(r"^\s*FAIL: R-496: banner painted", re.M)
INPUTS = [ # (repo path, path under /work) — what the harness reads, per its own header
(os.path.join(ISO, "felhom-bootstrap.sh"), "felhom-bootstrap.sh"),
(os.path.join(ISO, "pkg", "debian", "postinst"), "postinst"),
(os.path.join(ISO, "test", "golden"), "golden"),
(os.path.join(ISO, "test", "bootstrap-modes.sh"), os.path.join("test", "bootstrap-modes.sh")),
]
def not_checked(why):
print("iso-bootstrap: NOT CHECKED — %s" % why)
print("iso-bootstrap: INCONCLUSIVE (exit 2) — an unrun harness is never a pass")
return 2
def stage(dest, mutate=False):
"""Copy the harness inputs into dest. Returns None, or a reason the staging failed."""
for src, rel in INPUTS:
if not os.path.exists(src):
return "harness input missing: %s" % src
out = os.path.join(dest, rel)
os.makedirs(os.path.dirname(out), exist_ok=True)
if os.path.isdir(src):
shutil.copytree(src, out)
else:
shutil.copy2(src, out)
if mutate:
p = os.path.join(dest, "felhom-bootstrap.sh")
with open(p, encoding="utf-8") as f:
text = f.read()
if text.count(MUTANT_ANCHOR) != 1:
return ("the decoy anchor %r occurs %d time(s) in felhom-bootstrap.sh (want 1) — the "
"built-in decoy cannot be planted" % (MUTANT_ANCHOR.strip(), text.count(MUTANT_ANCHOR)))
with open(p, "w", encoding="utf-8") as f:
f.write(text.replace(MUTANT_ANCHOR, MUTANT_TEXT))
return None
def run_harness(docker, mutate):
"""Return (rc, output) of one harness run; rc None means docker itself failed (output says why)."""
tmp = tempfile.mkdtemp(prefix="felhom-iso-gate-")
name = "felhom-iso-gate-%s" % uuid.uuid4().hex[:12]
try:
err = stage(tmp, mutate)
if err:
return None, err
cmd = [docker, "run", "--rm", "--name", name, "--network", "none",
"-v", "%s:/src:ro" % tmp, IMAGE, "bash", "-c",
"mkdir -p /work && cp -a /src/. /work/ && exec bash /work/test/bootstrap-modes.sh"]
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=TIMEOUT_S)
except subprocess.TimeoutExpired:
subprocess.run([docker, "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return None, "the harness did not finish in %d s (container %s removed)" % (TIMEOUT_S, name)
out = p.stdout.decode("utf-8", "replace")
if p.returncode in (125, 126, 127):
return None, "docker run exited %d:\n%s" % (p.returncode, out[-600:])
return p.returncode, out
finally:
shutil.rmtree(tmp, ignore_errors=True)
def main():
docker = shutil.which("docker")
if not docker:
return not_checked("no docker on PATH")
p = subprocess.run([docker, "image", "inspect", IMAGE],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
if p.returncode != 0:
return not_checked("image %s is absent or docker is unreachable (%s). Build it with: docker build "
"-f scripts/iso/Dockerfile.assistant -t %s scripts/iso"
% (IMAGE, p.stderr.decode("utf-8", "replace").strip()[:200], IMAGE))
rc, out = run_harness(docker, mutate=False)
if rc is None:
return not_checked(out)
print(out.rstrip())
oks = len(re.findall(r"^\s*ok: ", out, re.M))
fails = re.findall(r"^\s*FAIL: .*$", out, re.M)
if rc != 0 or fails:
print("\niso-bootstrap: FAILED — the harness exited %d with %d failing check(s):" % (rc, len(fails)))
for f in fails:
print(" " + f.strip())
return 1
if PASS_LINE not in out or oks < MIN_OK:
print("\niso-bootstrap: FAILED — exit 0 but %s (pass line %s, %d ok lines, want >= %d)"
% ("the harness proved nothing", "present" if PASS_LINE in out else "ABSENT", oks, MIN_OK))
return 1
mrc, mout = run_harness(docker, mutate=True)
if mrc is None:
return not_checked("the built-in decoy could not run: %s" % mout)
if mrc == 0 or not MUTANT_MUST_FAIL.search(mout):
print("\niso-bootstrap: FAILED — the harness PASSED a bootstrap whose pairing banner never paints "
"(exit %d). The instrument is blind; the green run above proves nothing." % mrc)
print(mout[-800:])
return 1
print("\niso-bootstrap: built-in decoy convicted (a banner that never paints -> harness exit %d)" % mrc)
print("iso-bootstrap gate OK — %d harness checks green in %s" % (oks, IMAGE))
return 0
if __name__ == "__main__":
sys.exit(main())