From 5dffd851f948fa3d3e9b0289413986117edb04a7 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 26 Oct 2026 06:13:59 -0400 Subject: [PATCH 1/2] fix(ci): isolation-levels drill removes the probe containers it leaves (D-1481) scenario-01-local-only refuses to cut the network while a container runs; a nself-ci-probe-* container left by the gVisor leg of the drill before it made the nightly red. The drill now removes, on exit, the probe containers (label nself.ci.probe=1) that were not there when it started. --- scripts/drill/isolation-levels/run | 27 +++++++++++++++++++++- scripts/drill/isolation-levels/selftest.sh | 11 +++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/drill/isolation-levels/run b/scripts/drill/isolation-levels/run index 92047fe16..1995cb59c 100755 --- a/scripts/drill/isolation-levels/run +++ b/scripts/drill/isolation-levels/run @@ -33,7 +33,32 @@ for tool in jq go; do command -v "$tool" >/dev/null 2>&1 || { say "$tool is required"; exit 2; } done work=$(mktemp -d "${TMPDIR:-/tmp}/isolation-levels.XXXXXX") || exit 2 -trap 'rm -rf "$work"' EXIT +# probes: "runtime:id" for every engine probe container (label nself.ci.probe=1, see internal/exec/runtime_probe.go) a runtime +# holds, running or not. +probes() { + local c id + for c in docker podman; do + command -v "$c" >/dev/null 2>&1 || continue + for id in $("$c" ps -aq --filter label=nself.ci.probe=1 2>/dev/null || true); do printf '%s:%s\n' "$c" "$id"; done + done +} +# The legs start the engine's gVisor probe container; one that outlives its leg (a killed CLI, a hung start) would keep running +# into the next drill, and scenario-01-local-only refuses to cut the network while any container is up (D-1481). So on exit remove +# the probe containers this run left, and only those: the ones that were there before it started belong to somebody else. +probes_before=$(probes) +# shellcheck disable=SC2329 # runs from the EXIT trap +cleanup() { + local p + for p in $(probes); do + case " +$probes_before +" in *" +$p +"*) ;; *) "${p%%:*}" rm -f "${p#*:}" >/dev/null 2>&1 || true ;; esac + done + rm -rf "$work" +} +trap cleanup EXIT checks=$work/checks.ndjson : >"$checks" started=$(date -u +%Y-%m-%dT%H:%M:%SZ) diff --git a/scripts/drill/isolation-levels/selftest.sh b/scripts/drill/isolation-levels/selftest.sh index a00c48560..0b1764da6 100755 --- a/scripts/drill/isolation-levels/selftest.sh +++ b/scripts/drill/isolation-levels/selftest.sh @@ -24,6 +24,8 @@ cat >"$bin/docker" <<'F' case "$1" in info) if [ "$2" = "--format" ]; then if [ -z "${FAKE_NO_RUNSC:-}" ]; then echo '{"runc":{},"runsc":{}}'; else echo '{"runc":{}}'; fi; fi; exit 0 ;; image | pull) exit 0 ;; +ps) [ -z "${FAKE_PROBES:-}" ] || cat "$FAKE_PROBES" 2>/dev/null; exit 0 ;; +rm) echo "$*" >>"$FAKE_RM_LOG"; exit 0 ;; esac exit 0 F @@ -31,6 +33,7 @@ cat >"$bin/go" <<'F' #!/bin/sh for a in "$@"; do pkg=$a; done echo "=== RUN TestX" +[ -z "${FAKE_GO_LEAK:-}" ] || echo "$FAKE_GO_LEAK" >>"$FAKE_PROBES" case "$pkg" in *"${FAKE_GO_FAIL:-NONE}"*) echo "--- FAIL: TestX (0.00s)"; echo "FAIL"; exit 1 ;; *"${FAKE_GO_SKIP:-NONE}"*) echo "--- SKIP: TestX (0.00s)"; echo "ok"; exit 0 ;; @@ -123,4 +126,12 @@ grep -q "^args: --no-status --pipeline drills --job drill-isolation-levels$" "$w grep -q "v15: 1" "$work/direct.log" || fail "direct env: $(cat "$work/direct.log")" grep -q "repo: $(cd "$here/../../.." && pwd) " "$work/direct.log" || fail "direct repo: $(cat "$work/direct.log")" echo "ok direct" +# 10. A probe container the legs leave behind is removed on exit; one that was there before the drill started is not. +printf 'oldprobe\n' >"$work/probes" +: >"$work/rm.log" +drill leak FAKE_PROBES="$work/probes" FAKE_RM_LOG="$work/rm.log" FAKE_GO_LEAK=newprobe +[ "$(rc leak)" = 0 ] || fail "leak: exit $(rc leak): $(cat "$work/leak.err")" +grep -q '^rm -f newprobe$' "$work/rm.log" || fail "leak: the probe left by the legs was not removed: $(cat "$work/rm.log")" +! grep -q oldprobe "$work/rm.log" || fail "leak: removed a probe container that was there before the drill: $(cat "$work/rm.log")" +echo "ok leak" echo "all ok" From 22d8179a072ca023427a7da549423a2f765d377f Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 26 Oct 2026 06:13:59 -0400 Subject: [PATCH 2/2] fix(ci): run registry-sync on a push to main that changes a served projection (D-1480) The served bundles.members lagged main after a bundle membership change until the daily run. Also trigger on catalog.json, registry.json and counts.json changes on main; the daily schedule and the secret are unchanged. --- .github/workflows/registry-sync.yml | 9 ++++ .../ci/tests/test_registry_sync_triggers.py | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 scripts/ci/tests/test_registry_sync_triggers.py diff --git a/.github/workflows/registry-sync.yml b/.github/workflows/registry-sync.yml index ef17bd441..2cb7ec9be 100644 --- a/.github/workflows/registry-sync.yml +++ b/.github/workflows/registry-sync.yml @@ -3,10 +3,19 @@ name: Registry KV sync # The plugins.nself.org worker serves the catalog from the repo, but its KV copy (stats.lastSync) only moves when # POST /api/sync runs. Nothing called it after 2026-06-23, which kept drill-catalog-freshness red (D-0051). A daily call # keeps lastSync under the drill's 36 hour limit. +# A merge to main that changes a projection the worker serves (catalog.json carries the bundle membership that /bundles.json +# is built from, registry.json the plugin versions) also calls it, so drill-catalog-freshness is not red between that merge +# and the next daily run (D-1480). Same token as the daily run; scripts/ci/tests/test_registry_sync_triggers.py holds the paths. on: schedule: - cron: '17 5 * * *' + push: + branches: [main] + paths: + - 'catalog.json' + - 'registry.json' + - 'counts.json' workflow_dispatch: permissions: diff --git a/scripts/ci/tests/test_registry_sync_triggers.py b/scripts/ci/tests/test_registry_sync_triggers.py new file mode 100644 index 000000000..05745f7f7 --- /dev/null +++ b/scripts/ci/tests/test_registry_sync_triggers.py @@ -0,0 +1,52 @@ +"""Tests for the triggers of .github/workflows/registry-sync.yml (D-1480). + +The worker's served catalog only moves when POST /api/sync runs. The daily schedule stays, and a push to main that +changes a projection the drill compares (catalog.json, registry.json, counts.json) must run it too, or +drill-catalog-freshness stays red from the merge until the next daily run. The workflow is read as text (no YAML +library on the runner), so this checks the `on:` block only. +""" + +from pathlib import Path +import re +import unittest + +WORKFLOW = Path(__file__).resolve().parents[3] / ".github" / "workflows" / "registry-sync.yml" +SERVED = {"catalog.json", "registry.json", "counts.json"} + + +def on_block(text): + m = re.search(r"^on:\n((?:[ \t]+.*\n|[ \t]*\n)+)", text, re.M) + assert m, "no top-level on: block" + return m.group(1) + + +def section(block, key): + """The lines indented under ` key:` inside the on: block.""" + m = re.search(r"^ " + key + r":\n((?: .*\n|[ \t]*\n)+)", block, re.M) + return m.group(1) if m else None + + +class RegistrySyncTriggers(unittest.TestCase): + def setUp(self): + self.text = WORKFLOW.read_text() + self.block = on_block(self.text) + + def test_daily_schedule_stays(self): + sched = section(self.block, "schedule") + self.assertIsNotNone(sched, "the daily schedule is gone") + self.assertRegex(sched, r"cron:\s*'17 5 \* \* \*'") + + def test_push_to_main_on_served_projections(self): + push = section(self.block, "push") + self.assertIsNotNone(push, "no push trigger") + self.assertRegex(push, r"branches:\s*\[main\]") + paths = set(re.findall(r"^\s+-\s+['\"]?([^'\"\s]+)['\"]?\s*$", push.split("paths:")[1], re.M)) + self.assertEqual(paths, SERVED) + + def test_no_new_secret(self): + secrets = set(re.findall(r"secrets\.([A-Z0-9_]+)", self.text)) + self.assertEqual(secrets, {"CLOUDFLARE_WORKER_SYNC_TOKEN"}) + + +if __name__ == "__main__": + unittest.main()