chore: Propagate the Docker Hub login to the eleven app repositories #42

Open
opened 2026-09-04 12:41:01 +00:00 by ahmad · 3 comments
Owner

Context

templates/release.yml gained a Docker Hub login (#37, PR #41) so base-image pulls stop
being anonymous. The template is copied into each application rather than called, so
that change reaches a repository only when someone edits its workflow. Eleven repositories
carry a copy that builds images, and none of them authenticates:

ahmad/expiro        ahmad/imamah        ahmad/launchpad
ahmad/portfolio     ahmad/trip
bayan/bayan-web     bayan/kidpoints     bayan/stem
suhba/khitta        suhba/suhba-docs    suhba/suhba-web

Until each is updated, the fleet keeps sharing one anonymous per-IP pull limit — which is
the condition that produces toomanyrequests on a build unrelated to whatever consumed the
quota.

Scope

  • In: adding the same guarded docker login docker.io step to the publish job of each
    repository listed above.
  • Out: the template itself (#37); registering the secrets, which is the platform admin's;
    the runner's own container: image pulls, which are host-side
    (amtronics/infrastructure-vps#395).

Acceptance criteria

  • Given each repository above, when its release.yml is read, then the publish job logs in to docker.io before building.
  • Given a repository without the secrets registered, when its release runs, then it warns and builds anonymously rather than failing.
  • Given the step is added, when the repository's own CI runs, then it passes unchanged.

Notes

One pull request per repository, not one sweeping change: these are eleven independent
repositories with their own review and release cycles, and a single change touching all of
them could not be reviewed meaningfully or reverted selectively.

Copy the step verbatim from the template rather than retyping it. The guard matters — a
missing secret must warn rather than fail — and docker/login-action is the wrong tool here
precisely because it fails on an empty password.

Worth checking per repository rather than assuming: a couple of these build from a
Dockerfile whose base image is already mirrored, in which case the step is harmless but
pointless, and worth skipping with a note.

### Context `templates/release.yml` gained a Docker Hub login (#37, PR #41) so base-image pulls stop being anonymous. The template is **copied** into each application rather than called, so that change reaches a repository only when someone edits its workflow. Eleven repositories carry a copy that builds images, and none of them authenticates: ``` ahmad/expiro ahmad/imamah ahmad/launchpad ahmad/portfolio ahmad/trip bayan/bayan-web bayan/kidpoints bayan/stem suhba/khitta suhba/suhba-docs suhba/suhba-web ``` Until each is updated, the fleet keeps sharing one anonymous per-IP pull limit — which is the condition that produces `toomanyrequests` on a build unrelated to whatever consumed the quota. ### Scope - In: adding the same guarded `docker login docker.io` step to the `publish` job of each repository listed above. - Out: the template itself (#37); registering the secrets, which is the platform admin's; the runner's own `container:` image pulls, which are host-side (`amtronics/infrastructure-vps#395`). ### Acceptance criteria - [ ] Given each repository above, when its `release.yml` is read, then the `publish` job logs in to `docker.io` before building. - [ ] Given a repository without the secrets registered, when its release runs, then it warns and builds anonymously rather than failing. - [ ] Given the step is added, when the repository's own CI runs, then it passes unchanged. ### Notes One pull request per repository, not one sweeping change: these are eleven independent repositories with their own review and release cycles, and a single change touching all of them could not be reviewed meaningfully or reverted selectively. Copy the step verbatim from the template rather than retyping it. The guard matters — a missing secret must warn rather than fail — and `docker/login-action` is the wrong tool here precisely because it fails on an empty password. Worth checking per repository rather than assuming: a couple of these build from a `Dockerfile` whose base image is already mirrored, in which case the step is harmless but pointless, and worth skipping with a note.
ahmad self-assigned this 2026-09-05 17:05:32 +00:00
Author
Owner

Starting this. First repository done, and two findings that change what the remaining ten involve.

ahmad/portfolio → PR ahmad/portfolio#76. The step is byte-identical to the template (compared as parsed YAML, not by eye), and I drove its run: body against a stub docker on an empty PATH: neither secret set → warns, exits 0, docker never invoked; only the user set → same; both set → docker login docker.io -u … --password-stdin. Neutering the if makes the unset case call docker login with empty credentials, so the guard is load-bearing rather than decoration.

Finding 1 — none of the eleven is already mirrored, so the "harmless but pointless" exemption in the issue body does not apply to any of them. I surveyed all eleven. Four parameterise their bases, which looks like the mirrored case:

ahmad/portfolio    ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55…
ahmad/launchpad    ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55…  ARG NGINX_IMAGE=nginx:1.29-alpine@sha256:5616878…
suhba/khitta       ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55…

Those defaults are digest-pinned Docker Hub tags, not fleet-registry references. Parameterised is not mirrored. All eleven genuinely need the step, and none should be skipped.

Finding 2, for amtronics/infrastructure-vps#395 rather than for this issue. Those same three repositories already have the seam that mirroring needs: because the base is an ARG, pointing a build at a mirrored image is a build-args: line in the workflow, with no Dockerfile change and nothing to review in the image definition. The other eight hardcode their FROM and would need editing. That is worth knowing before #395 picks an approach, since it makes three repositories nearly free and eight not.

Survey of the remaining ten, so the rest of this issue is a known quantity rather than a discovery each time. None currently authenticates:

Repository Bases
ahmad/expiro node:22.23.2-alpine, nginx:1.27-alpine
ahmad/imamah node:22-alpine, nginx:alpine
ahmad/launchpad ${NODE_IMAGE}, ${NGINX_IMAGE}
ahmad/trip node:22-alpine, nginx:alpine
bayan/bayan-web node:24-bookworm-slim@sha256:…, nginxinc/nginx-unprivileged:1.29-alpine@sha256:…
bayan/kidpoints oven/bun:1.3.14-alpine, nginx:1.27-alpine
bayan/stem node:24-alpine, nginx:1.29-alpine
suhba/khitta ${NODE_IMAGE}
suhba/suhba-docs python:3.12-slim, nginx:1.27-alpine
suhba/suhba-web node:24-bookworm-slim@sha256:…, nginxinc/nginx-unprivileged:1.29-alpine@sha256:…

Note bayan/kidpoints pulls from oven/bun and suhba/suhba-docs from python, so this is not only a library/node concern — the per-IP cap covers all of them.

One caveat on urgency, since this issue reads like the answer to what the fleet is currently suffering. It is not. Today's failures across ahmad/expiro are auth.docker.io … TLS handshake timeout, a connectivity fault, and docker login talks to that same host — so authenticating would fail identically. This issue is real and worth finishing, but it is insurance against toomanyrequests, a condition the fleet has not hit today. The thing actually blocking merges is #395.

Starting this. First repository done, and two findings that change what the remaining ten involve. **`ahmad/portfolio` → PR ahmad/portfolio#76.** The step is byte-identical to the template (compared as parsed YAML, not by eye), and I drove its `run:` body against a stub `docker` on an empty `PATH`: neither secret set → warns, exits 0, `docker` never invoked; only the user set → same; both set → `docker login docker.io -u … --password-stdin`. Neutering the `if` makes the unset case call `docker login` with empty credentials, so the guard is load-bearing rather than decoration. **Finding 1 — none of the eleven is already mirrored, so the "harmless but pointless" exemption in the issue body does not apply to any of them.** I surveyed all eleven. Four parameterise their bases, which looks like the mirrored case: ``` ahmad/portfolio ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55… ahmad/launchpad ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55… ARG NGINX_IMAGE=nginx:1.29-alpine@sha256:5616878… suhba/khitta ARG NODE_IMAGE=node:22-alpine@sha256:16e22a55… ``` Those defaults are digest-pinned **Docker Hub** tags, not fleet-registry references. Parameterised is not mirrored. All eleven genuinely need the step, and none should be skipped. **Finding 2, for `amtronics/infrastructure-vps#395` rather than for this issue.** Those same three repositories already have the seam that mirroring needs: because the base is an `ARG`, pointing a build at a mirrored image is a `build-args:` line in the workflow, with no Dockerfile change and nothing to review in the image definition. The other eight hardcode their `FROM` and would need editing. That is worth knowing before #395 picks an approach, since it makes three repositories nearly free and eight not. **Survey of the remaining ten**, so the rest of this issue is a known quantity rather than a discovery each time. None currently authenticates: | Repository | Bases | | --- | --- | | `ahmad/expiro` | `node:22.23.2-alpine`, `nginx:1.27-alpine` | | `ahmad/imamah` | `node:22-alpine`, `nginx:alpine` | | `ahmad/launchpad` | `${NODE_IMAGE}`, `${NGINX_IMAGE}` | | `ahmad/trip` | `node:22-alpine`, `nginx:alpine` | | `bayan/bayan-web` | `node:24-bookworm-slim@sha256:…`, `nginxinc/nginx-unprivileged:1.29-alpine@sha256:…` | | `bayan/kidpoints` | `oven/bun:1.3.14-alpine`, `nginx:1.27-alpine` | | `bayan/stem` | `node:24-alpine`, `nginx:1.29-alpine` | | `suhba/khitta` | `${NODE_IMAGE}` | | `suhba/suhba-docs` | `python:3.12-slim`, `nginx:1.27-alpine` | | `suhba/suhba-web` | `node:24-bookworm-slim@sha256:…`, `nginxinc/nginx-unprivileged:1.29-alpine@sha256:…` | Note `bayan/kidpoints` pulls from `oven/bun` and `suhba/suhba-docs` from `python`, so this is not only a `library/node` concern — the per-IP cap covers all of them. **One caveat on urgency, since this issue reads like the answer to what the fleet is currently suffering.** It is not. Today's failures across `ahmad/expiro` are `auth.docker.io … TLS handshake timeout`, a connectivity fault, and `docker login` talks to that same host — so authenticating would fail identically. This issue is real and worth finishing, but it is insurance against `toomanyrequests`, a condition the fleet has not hit today. The thing actually blocking merges is #395.
Author
Owner

Ten of eleven done. Recording where this stands and why the eleventh is deliberately not open yet.

Merged (9): ahmad/portfolio#76, ahmad/launchpad#87, ahmad/trip#129, suhba/khitta#53, Bayan/stem#10, Bayan/kidpoints#208, suhba/suhba-docs#9, Bayan/bayan-web#29, suhba/suhba-web#42.
In review: ahmad/imamah#153 (green, ready).
Not opened: ahmad/expiro.

Why expiro is held, and it is not only the red image check. ahmad/expiro#30 — open, awaiting a human reviewer and a published-port decision — edits .forgejo/workflows/release.yml, which is exactly the file this change touches:

expiro #30: 8 files, including .forgejo/workflows/release.yml
expiro #39: 1 file  (pr.yml)
expiro #41: 2 files (vitest configs)
expiro #42: 1 file  (pr.yml)

So opening the eleventh PR now guarantees a conflict with a pull request that cannot merge without a human. Sequencing it after #30 costs nothing and avoids a rebase that someone would otherwise have to resolve. The image check being red for unrelated runner reasons (infrastructure-vps#395, #390) is a second reason, not the main one.

Three things this rollout established that are worth keeping past it:

The eleven repositories do not share one workflow shape. Bayan/stem's registry login is an unnamed - uses: docker/login-action@v3 where every other repo names the step, so an anchor on that name matches nothing there — silently, since a zero-match replace still succeeds. Asserting the anchor occurs exactly once before writing is what caught it.

They do not share one toolchain either: pnpm (portfolio, launchpad, bayan-web, suhba-web, imamah), npm (trip, khitta, stem), bun (kidpoints, whose validate runs bun test --coverage --coverage-threshold=0.7) and uv/Python (suhba-docs, whose validate cannot be reproduced locally at all — it fetches a submodule SSH key from OpenBao before mkdocs build --strict). Reading the lockfile first is not optional; using the wrong package manager wrote a stray lockfile in ahmad/trip that only a git status check caught.

And the per-IP cap is not a library/node concern: kidpoints pulls oven/bun, suhba-docs pulls python, and bayan-web/suhba-web pin their bases by digest — which fixes which image is fetched, not how, so those pulls count against the shared limit like any other.

Correction, since it appeared in one of these PRs. On suhba/suhba-web#42 I claimed that asserting the login index against every build-push step was a strengthening over checking the first, because that repository builds two images. It is not: the index list comes from enumerate, so it is ascending and builds[0] is min(builds). The forms are equivalent and the weaker one was never broken. Corrected on that PR.

Ten of eleven done. Recording where this stands and why the eleventh is deliberately not open yet. **Merged (9):** `ahmad/portfolio#76`, `ahmad/launchpad#87`, `ahmad/trip#129`, `suhba/khitta#53`, `Bayan/stem#10`, `Bayan/kidpoints#208`, `suhba/suhba-docs#9`, `Bayan/bayan-web#29`, `suhba/suhba-web#42`. **In review:** `ahmad/imamah#153` (green, ready). **Not opened:** `ahmad/expiro`. **Why expiro is held, and it is not only the red `image` check.** `ahmad/expiro#30` — open, awaiting a human reviewer and a published-port decision — edits `.forgejo/workflows/release.yml`, which is exactly the file this change touches: ``` expiro #30: 8 files, including .forgejo/workflows/release.yml expiro #39: 1 file (pr.yml) expiro #41: 2 files (vitest configs) expiro #42: 1 file (pr.yml) ``` So opening the eleventh PR now guarantees a conflict with a pull request that cannot merge without a human. Sequencing it after #30 costs nothing and avoids a rebase that someone would otherwise have to resolve. The `image` check being red for unrelated runner reasons (`infrastructure-vps#395`, `#390`) is a second reason, not the main one. **Three things this rollout established that are worth keeping past it:** The eleven repositories do **not** share one workflow shape. `Bayan/stem`'s registry login is an unnamed `- uses: docker/login-action@v3` where every other repo names the step, so an anchor on that name matches nothing there — silently, since a zero-match replace still succeeds. Asserting the anchor occurs exactly once before writing is what caught it. They do not share one toolchain either: pnpm (`portfolio`, `launchpad`, `bayan-web`, `suhba-web`, `imamah`), npm (`trip`, `khitta`, `stem`), bun (`kidpoints`, whose `validate` runs `bun test --coverage --coverage-threshold=0.7`) and uv/Python (`suhba-docs`, whose `validate` cannot be reproduced locally at all — it fetches a submodule SSH key from OpenBao before `mkdocs build --strict`). Reading the lockfile first is not optional; using the wrong package manager wrote a stray lockfile in `ahmad/trip` that only a `git status` check caught. And the per-IP cap is not a `library/node` concern: `kidpoints` pulls `oven/bun`, `suhba-docs` pulls `python`, and `bayan-web`/`suhba-web` pin their bases by digest — which fixes *which* image is fetched, not *how*, so those pulls count against the shared limit like any other. **Correction, since it appeared in one of these PRs.** On `suhba/suhba-web#42` I claimed that asserting the login index against every `build-push` step was a strengthening over checking the first, because that repository builds two images. It is not: the index list comes from `enumerate`, so it is ascending and `builds[0]` is `min(builds)`. The forms are equivalent and the weaker one was never broken. Corrected on that PR.
Author
Owner

Fleet-wide verification, run against every repository's main rather than against the pull requests — the property this issue actually asserts, and the one a per-PR review cannot establish.

repository              step     builds  identical  login<all builds
ahmad/portfolio            5        [7]  True       True
ahmad/launchpad            5        [7]  True       True
ahmad/trip                 5        [7]  True       True
suhba/khitta               5        [7]  True       True
bayan/stem                 5        [7]  True       True
bayan/kidpoints            5        [7]  True       True
suhba/suhba-docs           7        [9]  True       True
bayan/bayan-web            5        [7]  True       True
suhba/suhba-web            5     [7, 8]  True       True
ahmad/imamah               5        [7]  True       True
ahmad/expiro               -          -  ABSENT

identical compares the parsed Log in to Docker Hub step object against templates/release.yml on platform-actions/main; builds lists every build-push index in the same job. Ten of eleven carry the corrected step — the one including platform-actions#50's fix, so no repository inherited the version that failed a release on a rejected token.

The two rows that differ from the pattern are both explained and both correct: suhba/suhba-docs sits at 7 because its publish fetches a submodule key and initialises submodules first, and suhba/suhba-web has two builds because it also publishes a mail relay.

ahmad/expiro is the only one outstanding, and it is waiting on a person, not on this issue. ahmad/expiro#30 is open, needs a human reviewer and a published-port decision, and edits the same .forgejo/workflows/release.yml this change touches. Opening the eleventh PR before it lands would put a conflict on a branch nobody can merge. Its image check is also red for unrelated runner reasons (amtronics/infrastructure-vps#395, #390), but the file collision is the binding constraint.

So this issue is complete except for one repository, and that repository is blocked behind a decision only the maintainer can make. I have not marked it done.

Fleet-wide verification, run against every repository's `main` rather than against the pull requests — the property this issue actually asserts, and the one a per-PR review cannot establish. ``` repository step builds identical login<all builds ahmad/portfolio 5 [7] True True ahmad/launchpad 5 [7] True True ahmad/trip 5 [7] True True suhba/khitta 5 [7] True True bayan/stem 5 [7] True True bayan/kidpoints 5 [7] True True suhba/suhba-docs 7 [9] True True bayan/bayan-web 5 [7] True True suhba/suhba-web 5 [7, 8] True True ahmad/imamah 5 [7] True True ahmad/expiro - - ABSENT ``` `identical` compares the parsed `Log in to Docker Hub` step object against `templates/release.yml` on `platform-actions/main`; `builds` lists every `build-push` index in the same job. Ten of eleven carry the corrected step — the one including platform-actions#50's fix, so no repository inherited the version that failed a release on a rejected token. The two rows that differ from the pattern are both explained and both correct: `suhba/suhba-docs` sits at 7 because its `publish` fetches a submodule key and initialises submodules first, and `suhba/suhba-web` has two builds because it also publishes a mail relay. **`ahmad/expiro` is the only one outstanding, and it is waiting on a person, not on this issue.** `ahmad/expiro#30` is open, needs a human reviewer and a published-port decision, and edits the same `.forgejo/workflows/release.yml` this change touches. Opening the eleventh PR before it lands would put a conflict on a branch nobody can merge. Its `image` check is also red for unrelated runner reasons (`amtronics/infrastructure-vps#395`, `#390`), but the file collision is the binding constraint. So this issue is complete except for one repository, and that repository is blocked behind a decision only the maintainer can make. I have not marked it done.
Sign in to join this conversation.
No description provided.