harness: five new front-door fixtures, gitea's installer, HTTPS backends, per-file change names (R-462, R-624, R-735)
gates / gates (push) Successful in 2s
gates / gates (push) Successful in 2s
- fixtures (upgrade_fixtures_box.py): calibre-web (Upload form -> OPDS readback + the served EPUB), wger (web login -> weight entry API), crafty-controller (API v2 roles), uptime-kuma (socket.io polling: setup, login, addStatusPage -> public /api/status-page/<slug>); gitea posts its own first-run installer form (R-624's fixable case) and keeps the admin CLI for an installed one. calibre-web and wger run the template's own after_install on the bench, which has none. - R-735: the bench's `password:N:special` now has the controller's shape (randomWithSpecial); test seen failing first (length 32, no special), then 45/45. - upgrade_boxport / the memory watch: a backend traefik reaches over https (loadbalancer.server.scheme) is reached over https on the bench too (crafty-controller). - upgrade-test: files-before/after-detail.json and `files_changed_detail` NAME the files behind a files_may_change mark (R-734's method, now in the harness); test ChangedFiles. - test_catalog_gates: the gate count was stale (9, the runner has 10) and red on main; now 10. Evidence: felhom.eu/documentation/audits/more-night-apps-2026-09-30/ 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:
+56
-2
@@ -370,12 +370,14 @@ def memory_watch(app: str, project: str, workdir: Path, seconds: int, say, ev: P
|
||||
paths = LOAD_PATHS.get(app, ["/"])
|
||||
native = fx.FIXTURES.get(app)
|
||||
cname, port = getattr(native, "container", app), getattr(native, "port", 80)
|
||||
scheme = "http"
|
||||
if native is None and boxport is not None:
|
||||
# a box-fixture app: load the container traefik routes `/` to, at its own port
|
||||
rts = boxport.routes((workdir / "docker-compose.yml").read_text())
|
||||
root = [r for r in rts if not r[0]] or rts
|
||||
if root:
|
||||
cname, port = root[0][1], root[0][2]
|
||||
scheme = root[0][3] if len(root[0]) > 3 else "http" # an HTTPS backend (crafty-controller)
|
||||
target = container_ip(cname)
|
||||
stop = threading.Event()
|
||||
hits = {"n": 0, "codes": {}}
|
||||
@@ -383,7 +385,8 @@ def memory_watch(app: str, project: str, workdir: Path, seconds: int, say, ev: P
|
||||
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *a, **k):
|
||||
return None # a 30x is the app ANSWERING; following it to the unreachable domain is not load
|
||||
opener = urllib.request.build_opener(_NoRedirect)
|
||||
import ssl
|
||||
opener = urllib.request.build_opener(_NoRedirect, urllib.request.HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
host = None
|
||||
try:
|
||||
envtxt = (workdir / ".env").read_text()
|
||||
@@ -398,7 +401,7 @@ def memory_watch(app: str, project: str, workdir: Path, seconds: int, say, ev: P
|
||||
def worker(i):
|
||||
k = 0
|
||||
while not stop.is_set():
|
||||
url = f"http://{target}:{port}{paths[(i + k) % len(paths)]}"
|
||||
url = f"{scheme}://{target}:{port}{paths[(i + k) % len(paths)]}"
|
||||
k += 1
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"Host": host, "X-Forwarded-Proto": "https"} if host else {})
|
||||
@@ -565,6 +568,51 @@ def bind_tree_hash(project: str, workdir: Path) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def bind_tree_files(project: str, workdir: Path) -> dict:
|
||||
"""{bind source dir: {relpath: "size:sha256[:16]"}} — the same walk as bind_tree_hash, per FILE, so a
|
||||
`files_may_change` mark can NAME the files it saw change (R-734's method, 2026-09-30: the tree hash alone
|
||||
could not say which file moved). Files above 64 MiB carry size and mtime only, as in bind_tree_hash."""
|
||||
import hashlib
|
||||
r = compose(workdir, project, "ps", "-aq", timeout=120)
|
||||
srcs = set()
|
||||
for cid in (r.stdout or "").split():
|
||||
info = cvp._inspect(cid) or {}
|
||||
for m in info.get("Mounts") or []:
|
||||
if m.get("Type") == "bind" and os.path.isdir(m.get("Source") or "") \
|
||||
and not (m.get("Source") or "").startswith(("/var/run", "/run", "/etc", "/proc", "/sys")):
|
||||
srcs.add(m["Source"])
|
||||
out = {}
|
||||
for src in sorted(srcs):
|
||||
files = {}
|
||||
for dp, _, fn in os.walk(src):
|
||||
for f in fn:
|
||||
fp = os.path.join(dp, f)
|
||||
try:
|
||||
st = os.lstat(fp)
|
||||
if st.st_size <= 64 * 1024 * 1024 and os.path.isfile(fp) and not os.path.islink(fp):
|
||||
with open(fp, "rb") as fh:
|
||||
d = hashlib.sha256(fh.read()).hexdigest()[:16]
|
||||
else:
|
||||
d = "mtime-%d" % int(st.st_mtime)
|
||||
files[os.path.relpath(fp, src)] = "%d:%s" % (st.st_size, d)
|
||||
except OSError:
|
||||
files[os.path.relpath(fp, src)] = "unreadable"
|
||||
out[src] = files
|
||||
return out
|
||||
|
||||
|
||||
def changed_files(before: dict, after: dict) -> list:
|
||||
"""["<src>/<relpath> (added|removed|changed)"] between two bind_tree_files results."""
|
||||
out = []
|
||||
for src in sorted(set(before) | set(after)):
|
||||
b, a = before.get(src) or {}, after.get(src) or {}
|
||||
for rel in sorted(set(b) | set(a)):
|
||||
if b.get(rel) != a.get(rel):
|
||||
kind = "added" if rel not in b else "removed" if rel not in a else "changed"
|
||||
out.append(f"{src}/{rel} ({kind})")
|
||||
return out
|
||||
|
||||
|
||||
# --- the PostgreSQL major conversion on the bench (`09` §6.4 part 10, harness v4) ---------------------
|
||||
#
|
||||
# The postgres image converts nothing and refuses an older major's datadir (R-463); 18 refuses even an
|
||||
@@ -793,6 +841,8 @@ def run_edge(edge_id: str) -> dict:
|
||||
|
||||
files_before = bind_tree_hash(project, workdir)
|
||||
(ev / "files-before.json").write_text(json.dumps(files_before, indent=2))
|
||||
detail_before = bind_tree_files(project, workdir)
|
||||
(ev / "files-before-detail.json").write_text(json.dumps(detail_before, indent=2))
|
||||
|
||||
# --- 4. TO ---
|
||||
swap_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
@@ -846,8 +896,12 @@ def run_edge(edge_id: str) -> dict:
|
||||
(ev / "files-after.json").write_text(json.dumps(files_after, indent=2))
|
||||
rec["files_changed"] = sorted(k for k in set(files_before) | set(files_after)
|
||||
if files_before.get(k) != files_after.get(k))
|
||||
detail_after = bind_tree_files(project, workdir)
|
||||
(ev / "files-after-detail.json").write_text(json.dumps(detail_after, indent=2))
|
||||
rec["files_changed_detail"] = changed_files(detail_before, detail_after)[:200]
|
||||
if rec["files_changed"]:
|
||||
rec["marks"] = sorted(set(rec["marks"]) | {"files_may_change"})
|
||||
say(f"files_may_change — the files: {rec['files_changed_detail'][:20]}")
|
||||
say(f"files_may_change: the bind-mounted tree changed under {rec['files_changed']}")
|
||||
|
||||
# --- 5b. the MEMORY WATCH — only for an edge that just read back; a failed one is decided ---
|
||||
|
||||
Reference in New Issue
Block a user