diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfd4136..36e5e29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,12 @@ 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). + # 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 516ed4b..cfc29d5 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/" @@ -104,10 +105,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,22 +116,38 @@ 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 +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( + "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()} + 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 # 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." )