From e6febd3eb9962c5eefba07e6fbaa6c41f801be18 Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Sat, 10 Oct 2026 16:56:10 +0000 Subject: [PATCH 1/5] =?UTF-8?q?#43=20docs:=20CR=201=20=E2=80=94=20ci.yml?= =?UTF-8?q?=20names=20the=20mirror's=20cache=20miss=20and=20the=20apt=20fa?= =?UTF-8?q?llback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mirror.gcr.io serves only cached tags (redis:2.6.17 and busybox:1.21-ubuntu 404 there today), and Redis 7.0 is past EOL, so 7.0.15 can leave the cache. The comment says what that failure looks like and what replaces the mirror. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfd4136..4c1d35e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,11 @@ jobs: # integration suite runs here in full: db 15, a throwaway ACL user, and briefly # maxmemory, all of which a service container absorbs. Docker Hub's official # image via Google's mirror: GitHub's shared runners exhaust Hub's anonymous - # pull limit, and a red main run blocks deploys (#43, tests/test_ci.py). + # pull limit, and a red main run blocks deploys (#43, tests/test_ci.py). The + # mirror serves only tags it has cached (an old one 404s), and 7.0 is past EOL: + # "manifest unknown" at Initialize containers means 7.0.15 left the cache, not + # a broken commit. The fallback is Ubuntu 24.04's apt redis-server, which is + # 7.0.15: pin runs-on, assert the version, and wait on redis-cli ping (#43). services: redis: image: mirror.gcr.io/library/redis:7.0.15 From 05d68461673507f4cfa9cefb9a27344829e1bc1b Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Sat, 10 Oct 2026 16:56:25 +0000 Subject: [PATCH 2/5] =?UTF-8?q?#43=20test:=20CR=202=20=E2=80=94=20the=20im?= =?UTF-8?q?age=20sweep=20covers=20docker://=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A step with `uses: docker://` pulls a container like a service does, and with no registry that is an anonymous Docker Hub pull: _images() now finds those too, and takes the workflows directory so a synthetic workflow can prove it. Co-Authored-By: Claude Opus 5.5 --- tests/test_ci.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_ci.py b/tests/test_ci.py index 516ed4b..1370df7 100644 --- a/tests/test_ci.py +++ b/tests/test_ci.py @@ -104,10 +104,10 @@ def _image(spec: object) -> str | None: return None -def _images() -> list[tuple[str, str]]: - """``(workflow:job[:service], image)`` for every container image a job pulls.""" +def _images(workflows: Path = WORKFLOW.parent) -> list[tuple[str, str]]: + """``(workflow:job[:service|:step n], image)`` for every container image a job pulls.""" found = [] - for path in sorted(WORKFLOW.parent.glob("*.y*ml")): + for path in sorted(workflows.glob("*.y*ml")): jobs = yaml.safe_load(path.read_text()).get("jobs", {}) for name, job in jobs.items(): if (image := _image(job.get("container"))) is not None: @@ -115,9 +115,21 @@ def _images() -> list[tuple[str, str]]: for service, spec in (job.get("services") or {}).items(): if (image := _image(spec)) is not None: found.append((f"{path.name}:{name}:{service}", image)) + for n, step in enumerate(job.get("steps") or [], start=1): + if (uses := step.get("uses", "")).startswith("docker://"): + found.append((f"{path.name}:{name}:step {n}", uses.removeprefix("docker://"))) return found +def test_the_image_sweep_finds_docker_step_images(tmp_path: Path) -> None: + # `uses: docker://` runs a step in a container: a pull like a service's. + (tmp_path / "x.yml").write_text( + "jobs:\n j:\n steps:\n - uses: actions/checkout@v5\n" + " - uses: docker://redis:7\n" + ) + assert _images(tmp_path) == [("x.yml:j:step 2", "redis:7")] + + def test_the_image_sweep_finds_the_redis_service() -> None: # Guard the guard: a sweep that finds nothing passes vacuously. assert "ci.yml:test:redis" in {site for site, _ in _images()} From 4f96d841e8b26064e7b5ed78f54c6bbf47ca0988 Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Sat, 10 Oct 2026 16:56:31 +0000 Subject: [PATCH 3/5] =?UTF-8?q?#43=20test:=20CR=203=20=E2=80=94=20the=20im?= =?UTF-8?q?age=20sweep=20parses=20the=20workflows=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parametrize's values and ids each called _images(), so collection parsed every workflow twice; one module-level IMAGES feeds both. Co-Authored-By: Claude Opus 5.5 --- tests/test_ci.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_ci.py b/tests/test_ci.py index 1370df7..cf1458c 100644 --- a/tests/test_ci.py +++ b/tests/test_ci.py @@ -121,6 +121,9 @@ def _images(workflows: Path = WORKFLOW.parent) -> list[tuple[str, str]]: return found +IMAGES = _images() + + def test_the_image_sweep_finds_docker_step_images(tmp_path: Path) -> None: # `uses: docker://` runs a step in a container: a pull like a service's. (tmp_path / "x.yml").write_text( @@ -132,10 +135,10 @@ def test_the_image_sweep_finds_docker_step_images(tmp_path: Path) -> None: def test_the_image_sweep_finds_the_redis_service() -> None: # Guard the guard: a sweep that finds nothing passes vacuously. - assert "ci.yml:test:redis" in {site for site, _ in _images()} + assert "ci.yml:test:redis" in {site for site, _ in IMAGES} -@pytest.mark.parametrize(("site", "image"), _images(), ids=[s for s, _ in _images()]) +@pytest.mark.parametrize(("site", "image"), IMAGES, ids=[s for s, _ in IMAGES]) def test_image_comes_from_the_mirror(site: str, image: str) -> None: # An image named without a registry is an anonymous Docker Hub pull, whose limit # GitHub's shared runners exhaust: on 2026-10-09 the redis service failed to pull From 97fc2d9296675565833ff26715e3ce96f4bf4a32 Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Sat, 10 Oct 2026 17:34:11 +0000 Subject: [PATCH 4/5] =?UTF-8?q?#43=20docs:=20CR=205=20=E2=80=94=20the=207.?= =?UTF-8?q?0.15=20pin=20points=20at=20broker#85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redis 7.0 is upstream EOL (2024-07-29), and broker#85 now owns the choice of the broker's next Redis. ci.yml, BROKER_REDIS and AGENTS.md's CI row say so, since all three move with the broker. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 1 + AGENTS.md | 2 +- tests/test_ci.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c1d35e..36e5e29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,7 @@ jobs: # "manifest unknown" at Initialize containers means 7.0.15 left the cache, not # a broken commit. The fallback is Ubuntu 24.04's apt redis-server, which is # 7.0.15: pin runs-on, assert the version, and wait on redis-cli ping (#43). + # broker#85 decides the broker's next Redis; this pin moves with it. services: redis: image: mirror.gcr.io/library/redis:7.0.15 diff --git a/AGENTS.md b/AGENTS.md index 6489bf0..e7ffb1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ find-links locks by filename, not hash, so either source satisfies the same `uv. | Input | `gs://co-gcs-blobs` (Replicator's raw blobs), read-only | | Output | `gs://co-gcs-processor/blobs/.bin`, write-if-absent, **never deleted** | | Node.js | 24 LTS from NodeSource apt, agent tooling only (mayfly, SocratiCode): `deploy/nodesource.sh install\|check`, [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md#nodejs-agent-tooling-only) | -| CI | GitHub Actions [.github/workflows/ci.yml](.github/workflows/ci.yml): lint, then the full suite against `redis:7.0.15` (the broker's, not 7.2: CLIENT SETINFO), pulled from `mirror.gcr.io`, never anonymously from Docker Hub (#43). Keyless WIF to `co-pypi-reader` needs the org variable `GCP_WIF_PROVIDER` shared with this repo and a `roles/iam.workloadIdentityUser` binding for `principalSet://…/attribute.repository/CannObserv/processor` | +| CI | GitHub Actions [.github/workflows/ci.yml](.github/workflows/ci.yml): lint, then the full suite against `redis:7.0.15` (the broker's, not 7.2: CLIENT SETINFO; upstream EOL, the next is broker#85's call), pulled from `mirror.gcr.io`, never anonymously from Docker Hub (#43). Keyless WIF to `co-pypi-reader` needs the org variable `GCP_WIF_PROVIDER` shared with this repo and a `roles/iam.workloadIdentityUser` binding for `principalSet://…/attribute.repository/CannObserv/processor` | | Drift check | `processor-drift.timer`, hourly: does live lag `origin/main` in code that runs (`processor.drift.RUNTIME_*`)? Checks in to Status's `co-processor-drift` (`http://status:9000`, status#24); `alert` past 8 h. [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md#the-drift-check) (#35) | | Liveness | `processor run` checks in `ok` to Status's `co-processor-live` every 5 min, only while the consume loop progressed within 605 s; silence past the 900 s grace pages. Daemon thread, 10 s bound: Status never touches consumption. [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md#liveness) (#39) | | Service | systemd unit `processor` — [deploy/processor.service](deploy/processor.service), runbook [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md); live since 2026-10-02. Runs as user `processor` (no sudo, not in `docker`, no home) from `/srv/processor/live` → `releases/`, a read-only release `scripts/deploy.sh` builds from a commit on `origin/main` (#2; the cohort standard, broker#22 / status#9). **Never from a checkout** | diff --git a/tests/test_ci.py b/tests/test_ci.py index cf1458c..26813d6 100644 --- a/tests/test_ci.py +++ b/tests/test_ci.py @@ -11,6 +11,7 @@ # The broker's exact version (broker deploy/redis-acl.conf: "this broker is 7.0.15"). # Not 7.2: redis-py sends CLIENT SETINFO there, which the broker's ACL cannot grant # until it upgrades, so a 7.2 container reports denials the broker never sees. +# 7.0 is upstream EOL: broker#85 decides the next version, and this pin moves with it. # Docker Hub's official image via Google's mirror (#43): see test_image_comes_from_the_mirror. BROKER_REDIS = "mirror.gcr.io/library/redis:7.0.15" MIRROR = "mirror.gcr.io/" From 2ecb9d49a2a13c979f51c364f8e43fba7e4236ad Mon Sep 17 00:00:00 2001 From: gregoryfoster Date: Sat, 10 Oct 2026 17:34:16 +0000 Subject: [PATCH 5/5] =?UTF-8?q?#43=20test:=20CR=206=20=E2=80=94=20the=20mi?= =?UTF-8?q?rror=20check's=20message=20states=20its=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check rejects any image off mirror.gcr.io, but its message always said "from Docker Hub anonymously", false for a ghcr.io image. It now states the rule, then why an unqualified or docker.io image is the case it guards. Co-Authored-By: Claude Opus 5.5 --- tests/test_ci.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ci.py b/tests/test_ci.py index 26813d6..cfc29d5 100644 --- a/tests/test_ci.py +++ b/tests/test_ci.py @@ -146,7 +146,8 @@ def test_image_comes_from_the_mirror(site: str, image: str) -> None: # twice, and since #34 a red main run blocks scripts/deploy.sh (#43). The mirror # serves the same official images (same index digest) with no limit or credential. assert image.startswith(MIRROR), ( - f"{site} pulls {image!r} from Docker Hub anonymously. " + f"{site} pulls {image!r}, which is not on {MIRROR}: an image with no registry, " + f"or on docker.io, is an anonymous Docker Hub pull. " f"Use {MIRROR}library/ for an official image." )