fc796dd95e
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
85 lines
3.0 KiB
Python
Executable File
85 lines
3.0 KiB
Python
Executable File
#!/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())
|