v0.291.0: decision 78 (R-726) — a returning household's first night sets the orphaned copy aside; R-838 — traefik v3.7.13, cloudflared 2026.9.3, filebrowser 1.5.6-stable, a release now moves a running file browser, check-infra-pins.py
gates / gates (push) Successful in 31s
gates / gates (push) Successful in 31s
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:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check-infra-pins.py — the MONTHLY re-test's infrastructure half (R-838, 2026-10-04).
|
||||
|
||||
The box's three built-in containers (traefik, cloudflared, filebrowser) are pinned in internal/infra/infra.go and are
|
||||
NOT catalog templates, so retest-floating.py never sees them. Before R-838 cloudflared sat four months behind and
|
||||
nothing noticed. This script prints, for each pin, the newest upstream release in the SAME channel (traefik v3.x,
|
||||
cloudflared YYYY.M.P, filebrowser N.N.N-stable — never a beta) and says BEHIND when the pin is older.
|
||||
|
||||
It REPORTS; it changes nothing. A raise is a controller release (edit the constant, read the release notes between the
|
||||
two versions for breaking changes, prove it on 9202 then both demo boxes: the runbook's "Infrastructure pins" section).
|
||||
Network: Docker Hub's public tag API only. Exit 0 = all current, 1 = at least one BEHIND, 2 = could not check.
|
||||
|
||||
Run: python3 scripts/check-infra-pins.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
INFRA = os.path.join(HERE, "..", "internal", "infra", "infra.go")
|
||||
|
||||
CHANNELS = {
|
||||
"TraefikImage": ("library/traefik", re.compile(r"^v3\.(\d+)\.(\d+)$")),
|
||||
"CloudflaredImage": ("cloudflare/cloudflared", re.compile(r"^(\d{4})\.(\d+)\.(\d+)$")),
|
||||
"FileBrowserImage": ("gtstef/filebrowser", re.compile(r"^(\d+)\.(\d+)\.(\d+)-stable$")),
|
||||
}
|
||||
|
||||
|
||||
def pins():
|
||||
src = open(INFRA).read()
|
||||
out = {}
|
||||
for const in CHANNELS:
|
||||
m = re.search(r'\b%s\s*=\s*"([^"]+)"' % const, src)
|
||||
if not m:
|
||||
raise SystemExit(f"check-infra-pins: {const} not found in {INFRA}")
|
||||
out[const] = m.group(1)
|
||||
return out
|
||||
|
||||
|
||||
def tags(repo):
|
||||
url = f"https://hub.docker.com/v2/repositories/{repo}/tags?page_size=100&ordering=last_updated"
|
||||
names = []
|
||||
for _ in range(3): # three pages are plenty for "the newest in a channel"
|
||||
with urllib.request.urlopen(url, timeout=30) as r:
|
||||
d = json.load(r)
|
||||
names += [t["name"] for t in d.get("results", [])]
|
||||
url = d.get("next")
|
||||
if not url:
|
||||
break
|
||||
return names
|
||||
|
||||
|
||||
def key(m):
|
||||
return tuple(int(x) for x in m.groups())
|
||||
|
||||
|
||||
def main():
|
||||
rc = 0
|
||||
for const, image in pins().items():
|
||||
repo, rx = CHANNELS[const]
|
||||
tag = image.split(":", 1)[1]
|
||||
cur = rx.match(tag)
|
||||
try:
|
||||
cands = [m for m in (rx.match(t) for t in tags(repo)) if m]
|
||||
except Exception as e: # noqa: BLE001 — a report, not a gate
|
||||
print(f"{const:17} {image:40} could not check: {e}")
|
||||
rc = max(rc, 2)
|
||||
continue
|
||||
if not cands or not cur:
|
||||
print(f"{const:17} {image:40} no comparable tag found")
|
||||
rc = max(rc, 2)
|
||||
continue
|
||||
newest = max(cands, key=key)
|
||||
state = "current" if key(newest) <= key(cur) else "BEHIND"
|
||||
if state == "BEHIND":
|
||||
rc = max(rc, 1)
|
||||
print(f"{const:17} {image:40} newest {newest.string:16} {state}")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user