R-1303: watch — abandoned templates are GAP, a seed's unread pins stay pending (no storm after a seed); 38 checks
gates / gates (push) Successful in 4m5s
gates / gates (push) Successful in 4m5s
This commit is contained in:
1 parent
1f62d62915
commit
6ee23a3b8f
4 files changed
+54
-4
No files matched your search
@@ -13,6 +13,9 @@
|
||||
item as a recorded answer), one mail per advisory, the 7-day reminder, a source down one night vs two, the weekly, the
|
||||
seed, the inventory gate. Red-proved four ways (dedupe, streak, the 30-day window, a swallowed source error).
|
||||
- `hub/Dockerfile`: a `# upstream:` comment above each `FROM`.
|
||||
- After the first CI dry run: a `lifecycle: abandoned` template (plant-it) is GAP and never looked up; a pin a seed
|
||||
could not read (no-key rotation, a source down) stays PENDING and its first real read is recorded silently — no
|
||||
storm in the nights after a seed, while a pin added later still mails (38 checks; red-proved with an empty pending set).
|
||||
|
||||
## 2026-10-26 (evening) — CI: heavy suites by path, no second full gate run, a nightly run (R-1297 A); a decoy hole closed
|
||||
|
||||
|
||||
@@ -385,6 +385,13 @@ def newest_in_channel(tag, names):
|
||||
|
||||
def check_item(it, src, today):
|
||||
"""-> the item with `class`, `advisories`, `not_checked`, `behind`, `notes` filled in."""
|
||||
if it.get("abandoned"):
|
||||
it.update({"advisories": [], "not_checked": [], "behind": None, "class": "GAP",
|
||||
"notes": ["abandoned upstream (lifecycle: abandoned) — no fix will ever come; not looked up"]})
|
||||
if it.get("problems"):
|
||||
it["not_checked"].append({"source": "inventory", "why": "; ".join(it["problems"])})
|
||||
it["class"] = "NOT CHECKED"
|
||||
return it
|
||||
ours = version_of(it["tag"])
|
||||
floating = is_floating(it["tag"])
|
||||
advs, nc, notes = [], [], []
|
||||
@@ -394,6 +401,7 @@ def check_item(it, src, today):
|
||||
if kind in ("github", "advisories"):
|
||||
if src.rotate and it["group"] == "app" and (zlib.crc32(arg.encode()) + today.toordinal()) % 2:
|
||||
notes.append("advisories of %s: read tomorrow (no-key rotation)" % arg)
|
||||
it["adv_unread"] = True
|
||||
elif floating:
|
||||
notes.append("advisories of %s: not matched (floating tag)" % arg)
|
||||
else:
|
||||
@@ -455,6 +463,7 @@ def check_item(it, src, today):
|
||||
"link": VULNNOTES[arg]["url"]})
|
||||
except SourceError as e:
|
||||
nc.append({"source": source_name(kind, arg), "why": str(e)})
|
||||
it["adv_unread"] = True
|
||||
behind, newer_major = None, None
|
||||
if it.get("no_registry"):
|
||||
gh = next((a for k, a in it.get("sources") or [] if k == "github"), None)
|
||||
@@ -525,6 +534,24 @@ def plan(items, state, today, seed=False):
|
||||
checked_images.add(it["image"])
|
||||
for a in it.get("advisories", []):
|
||||
open_now[adv_key(it, a)] = (it, a)
|
||||
# A seed that could not read some pins (no key: rotation; or a source down) leaves them PENDING: their first real
|
||||
# read is recorded silently too, so the first nights after a seed are no mail storm. A pin added later is never
|
||||
# pending, so its advisories mail normally.
|
||||
pending = set(st.get("seed_pending", []))
|
||||
if seed:
|
||||
pending = {it["image"] for it in items if it.get("adv_unread")}
|
||||
else:
|
||||
for it in items:
|
||||
if it["image"] in pending and not it.get("adv_unread"):
|
||||
for a in it.get("advisories", []):
|
||||
k = adv_key(it, a)
|
||||
if k not in known:
|
||||
known[k] = {"first": iso, "last_mailed": iso, "program": label(it), "summary": a["summary"][:160],
|
||||
"silent": "first read after the seed"}
|
||||
would.append({"kind": "advisory", "subject": "(silent, first read after the seed) %s %s" % (
|
||||
it["image"], a["id"]), "text": ""})
|
||||
pending.discard(it["image"])
|
||||
st["seed_pending"] = sorted(pending)
|
||||
new, remind = [], []
|
||||
for k, (it, a) in sorted(open_now.items()):
|
||||
if k not in known:
|
||||
@@ -627,7 +654,8 @@ def weekly_mail(items, today):
|
||||
lines += block("Open advisories (mailed already):", sorted({"%s %s — %s" % (it["image"], it["tag"], a["id"])
|
||||
for it in items for a in it.get("advisories", [])}))
|
||||
lines += block("No machine-readable source (GAP) — not watched for notices:",
|
||||
sorted({"%s — %s" % (it["image"], next((v for k, v in it.get("sources", []) if k == "gap"), ""))
|
||||
sorted({"%s — %s" % (it["image"], next((v for k, v in it.get("sources", []) if k == "gap"),
|
||||
"; ".join(it.get("notes", []))))
|
||||
for it in items if it.get("class") == "GAP"}))
|
||||
lines += block("Floating tags (no exact version to match a notice against):",
|
||||
sorted({"%s:%s (%s)" % (it["image"], it["tag"], it["owner"]) for it in items if it.get("class") == "FLOATING"}))
|
||||
|
||||
@@ -135,8 +135,10 @@ def catalog_items(root):
|
||||
compose = os.path.join(tdir, app, "docker-compose.yml")
|
||||
if not os.path.isfile(compose):
|
||||
continue
|
||||
ups = felhom_upstream_block(_read(os.path.join(tdir, app, ".felhom.yml"))) \
|
||||
if os.path.isfile(os.path.join(tdir, app, ".felhom.yml")) else {}
|
||||
meta = _read(os.path.join(tdir, app, ".felhom.yml")) if os.path.isfile(os.path.join(tdir, app, ".felhom.yml")) else ""
|
||||
ups = felhom_upstream_block(meta)
|
||||
# `lifecycle: abandoned` — still running on boxes that had it, never updated again (plant-it): listed, not looked up.
|
||||
abandoned = bool(re.search(r"^lifecycle:\s*[\"']?abandoned", meta, re.M))
|
||||
seen = set()
|
||||
for line in _read(compose).splitlines():
|
||||
m = IMAGE_RX.match(line)
|
||||
@@ -146,7 +148,7 @@ def catalog_items(root):
|
||||
if (image, tag) in seen:
|
||||
continue
|
||||
seen.add((image, tag))
|
||||
items.append({"group": "app", "owner": app, "image": image, "tag": tag,
|
||||
items.append({"group": "app", "owner": app, "image": image, "tag": tag, "abandoned": abandoned,
|
||||
"where": "templates/%s/.felhom.yml upstream: %s" % (app, image),
|
||||
"sources_text": ups.get(image)})
|
||||
return items
|
||||
|
||||
@@ -182,6 +182,12 @@ def t_classes():
|
||||
fb["class"] == "BEHIND" and fb["behind"] == "1.5.7-stable", (fb["class"], fb.get("behind"), fb["advisories"]))
|
||||
check("W3 redis 7-alpine -> FLOATING", red["class"] == "FLOATING", red["class"])
|
||||
check("W3 postgres gap -> GAP (BEHIND still computed: no newer 16-…)", pg["class"] == "GAP", pg["class"])
|
||||
ab = item("msdeluise/plant-it", "0.10.0", "github:MDeLuise/plant-it", group="app")
|
||||
ab["abandoned"] = True
|
||||
seen_before = len(FakeNet({}).seen)
|
||||
run_items([ab], {})
|
||||
check("W3 an abandoned template -> GAP, nothing looked up (no route needed)", ab["class"] == "GAP" and
|
||||
not ab["not_checked"] and seen_before == 0, (ab["class"], ab.get("not_checked")))
|
||||
check("W4 a source that fails -> NOT CHECKED, with the reason", down["class"] == "NOT CHECKED" and
|
||||
"timed out" in down["not_checked"][0]["why"], down.get("not_checked"))
|
||||
lines = w.table(items)
|
||||
@@ -259,6 +265,17 @@ def t_memory():
|
||||
sm, sst, would = w.plan([adv_item("traefik", "GHSA-1"), down("b")], {}, d(1), seed=True)
|
||||
check("W7 a seed mails nothing, records the advisory, and says what it would have mailed",
|
||||
sm == [] and "traefik|GHSA-1" in sst["advisories"] and kinds(would) == ["advisory"], (kinds(sm), kinds(would)))
|
||||
unread = clean("immich")
|
||||
unread["adv_unread"] = True
|
||||
_, sst, _ = w.plan([adv_item("traefik", "GHSA-1"), unread], {}, d(1), seed=True)
|
||||
check("W7 a pin the seed could not read is PENDING", sst["seed_pending"] == ["immich"], sst.get("seed_pending"))
|
||||
first = adv_item("immich", "GHSA-im1", group="app")
|
||||
newapp = adv_item("newapp", "GHSA-na1", group="app")
|
||||
pm, sst, _ = w.plan([adv_item("traefik", "GHSA-1"), first, newapp], sst, d(2))
|
||||
check("W7 its first real read is recorded SILENTLY; a pin added later still mails",
|
||||
"immich|GHSA-im1" in sst["advisories"] and sst["seed_pending"] == [] and
|
||||
[m for m in pm if m["kind"] == "advisory" and "GHSA-im1" in m["text"]] == [] and
|
||||
any(m["kind"] == "advisory" and "GHSA-na1" in m["text"] for m in pm), (kinds(pm), sst.get("seed_pending")))
|
||||
|
||||
|
||||
def t_inventory():
|
||||
|
||||
Reference in new issue
Block a user