Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<sha256>.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/<build>`, 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** |
Expand Down
29 changes: 23 additions & 6 deletions tests/test_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down Expand Up @@ -104,33 +105,49 @@ 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:
found.append((f"{path.name}:{name}", image))
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://<image>` 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/<image> for an official image."
)

Expand Down
Loading