diff --git a/docs/ci-cd.md b/docs/ci-cd.md index ecf7134d8..5d4a0fc2d 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -210,8 +210,10 @@ lightweight-Python shape, and **`toolchain-preflight`** (ersatztv#772), a checko Those last two are the lane members that live in `docker-build.yml` rather than `pr-checks.yml`, so they are the ones that also run on a **tag push**. `scan` is still the one to think hardest about before changing anything here: its failure does not merely redden a status but **skips `build`** — -an OOM or a wedge there yields no release image at all. `toolchain-preflight` gates nothing and is -a `needs:` of nothing, by design. +an OOM or a wedge there yields no release image at all. `toolchain-preflight` is a `needs:` of nothing, by +design — it does not gate the jobs it diagnoses. It is not consequence-free either: like any red +job it lands in the PR's combined status, which the merge gate reads (see "When the pinned tag +disappears"). Nothing there runs a compiler or a `docker build`, which is why the lane can be capped at 1 GiB per job. The lightweight-Python jobs are the deliberate edge of the "git-only" rule, not an exception to it: `setup-python` + `pip install pytest` + a suite whose @@ -1613,7 +1615,7 @@ package — every `:` older than the 15-slot window gone, every `keep_patte # on the Gitea host: the rule itself sqlite3 /var/lib/gitea/data/gitea.db 'select * from package_cleanup_rule;' # from anywhere: that the cleanup task is scheduled and has been running (schedule/prev/exec_times) -curl -s -u user:pass http://192.168.1.95:3000/api/v1/admin/cron?limit=50 \ +curl -s -u user:pass 'http://192.168.1.95:3000/api/v1/admin/cron?limit=50' \ | jq '.[] | select(.name == "cleanup_packages")' ``` @@ -1640,7 +1642,7 @@ because they send you to different places: | HTTP 200, body is not a manifest | **red** | something is answering for the registry (proxy, login page) | | 401 / 403 | **red** | the credentials were rejected — fix the secrets | | anything else (5xx, unreachable, no `curl`) | **red** after `ETV_CI_ATTEMPTS` tries | `could NOT VERIFY` — check the registry's health, NOT the pin | -| `ETV_REGISTRY_AUTH` unset, or either half empty | **red**, before any query | an absent secret interpolates to `":"`, which is not a credential | +| `ETV_REGISTRY_AUTH` unset, malformed, or either half empty | **red**, before any query | an absent secret interpolates to `":"`, which is not a credential | The last two rows are the ones worth defending, because warning on them and exiting 0 is the natural way to write this check and it is wrong: a missing `curl`, a moved registry and a DNS change all land @@ -1672,7 +1674,7 @@ cd /tmp/etv-toolchain # driver the same inline config ci-image.yml passes it: # [registry."192.168.1.95:3000"] # http = true -prev_builder=$(docker buildx inspect --bootstrap 2>/dev/null | awk '/^Name:/{print $2; exit}') +prev_builder=$(docker buildx inspect 2>/dev/null | awk '/^Name:/{print $2; exit}') docker buildx use default # needs the containerd image store to --push; # both named hosts have it (checked 2026-08-22) docker login 192.168.1.95:3000 -u timothy @@ -1680,7 +1682,7 @@ docker buildx build --platform linux/amd64 --provenance=false \ -f docker/ci/Dockerfile -t "192.168.1.95:3000/timothy/ersatztv-ci:$pin" --push . cd "$repo" && git worktree remove /tmp/etv-toolchain -docker buildx use "$prev_builder" # leave the operator's builder as you found it +[ -n "$prev_builder" ] && docker buildx use "$prev_builder" # leave the builder as you found it ``` Then confirm the tag resolves before re-running anything — the preflight script does exactly this diff --git a/scripts/tests/test_ci_toolchain_image_resolves.py b/scripts/tests/test_ci_toolchain_image_resolves.py index 80bcd55a7..6333e9566 100644 --- a/scripts/tests/test_ci_toolchain_image_resolves.py +++ b/scripts/tests/test_ci_toolchain_image_resolves.py @@ -6,8 +6,10 @@ present, gone, and could-not-tell. Collapsing the third into either of the other becomes decoration, so each is driven here through the real entry point with a stubbed `curl`. `test_MUTATION_a_deleted_tag_is_reported_as_a_failure` is the load-bearing one and is declared in -`scripts/tests/mutation_manifest.py`: disarming the `404` arm leaves a script that still runs, still -prints, still exits 0 — and never reports the outage it exists for. +`scripts/tests/mutation_manifest.py`. Note what it can and cannot turn on: since an unverifiable +answer fails the job too, disarming the `404` arm still exits non-zero, so the EXIT CODE separates +nothing. What the disarm destroys is the DIAGNOSTIC — the outage is reported as "could not verify", +which sends an operator to the registry's health instead of to the rebuild that fixes it. """ from __future__ import annotations @@ -129,8 +131,8 @@ def test_MUTATION_a_deleted_tag_is_reported_as_a_failure(preflight): """The outage of 2026-08-11..13, in one assertion. Declared in `mutation_manifest.py`: replacing the `404` arm sends a deleted tag down the - could-not-tell path, where it warns and exits 0 — a preflight that runs, prints, and misses the - only thing it was built to catch. + could-not-verify path, which fails the job with the wrong story — a preflight that runs, reddens, + and still misses the only thing it was built to name. """ preflight.set_codes({"32747a0": "404"}) result = preflight.run() @@ -296,6 +298,6 @@ def test_the_PRODUCTION_retry_defaults_are_the_ones_that_run(preflight): assert result.returncode != 0 assert len(preflight.calls()) == 3, f"the default attempt count is not 3 — got {len(preflight.calls())} call(s)" assert elapsed >= 8, ( - f"two pauses at the default 5s should take >=10s; took {elapsed:.1f}s, so the default pause " + f"two pauses at the default 5s should clear the 8s floor; took {elapsed:.1f}s, so the pause " "has been shortened out from under the 'a blip does not redden a PR' argument" )