chore: Authenticate Docker Hub pulls in the release template #41

Merged
ahmad merged 1 commit from feature/37-docker-hub-login into main 2026-09-04 13:33:55 +00:00
Owner

Issue

Part of #37 — deliberately not Closes. Two of the four acceptance criteria are met here; one is handed to the platform admin and one is follow-up work in other repositories. Closing the issue on merge would hide both.

Problem

Docker Hub counts anonymous pulls per IP, and every repository on this fleet shares one. A burst of releases therefore fails an unrelated build with toomanyrequests, and the base images in each Dockerfile are pulled with no credentials at all.

Solution

The publish job in templates/release.yml logs in to docker.io with a read-only token before building, and the README documents both secrets by name, type, scope and location.

It is a plain docker login, not docker/login-action. That action fails the job on an empty password, which would turn "no token configured" into "no releases" — the opposite of what is wanted. This warns and continues, so a caller without the secrets still builds, anonymously, exactly as it does today.

Review notes

What is verified, and how. The repository's checker only parses action.yml files, so templates/release.yml gets no CI coverage at all — the shell added here would ship unvalidated. I ran the checker's own technique over the template by hand (${{ }} replaced, bash -n): 7 run steps, 0 invalid. I also exercised the guard against a stub docker that refuses empty credentials the way the real one does:

both secrets empty, guard present   ->  ::warning::…anonymously…      exit 0
only the username set               ->  ::warning::…anonymously…      exit 0
both set                            ->  login docker.io -u someone    exit 0
both secrets empty, guard REMOVED   ->  Error: Cannot perform an interactive login…  exit 1

The last line is the control: with the guard deleted, the case the second criterion describes fails the job. No real credential was used — the token in the test is a literal string and the docker on PATH was a stub.

The token is piped through stdin rather than passed as an argument, so it does not appear in a process listing.

Criterion 3 is not done here, by choice. Registering DOCKER_HUB_USER and DOCKER_HUB_READ_PUBLIC on each org and on the ahmad user's repositories is credential handling; I do not read the token out of the workspace store or POST it anywhere. That step is the platform admin's, and until it happens every build simply keeps warning and pulling anonymously — which is the current behaviour, so nothing regresses in the meantime.

Criterion 1 reaches new copies only. This template is copied, not called, so merging it authenticates a repository the next time it converges. Eleven repositories already carry their own copy and none has the login:

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

Editing eleven repositories from an issue that names none of them is not something to slip into this PR; it is filed separately.

Risks and trade-offs

DOCKER_HUB_USER is new — the workspace only had the token. A Docker Hub login needs both, so the alternative was baking an account name into a public repository.

Both secrets are optional by design. That is the right default here, but it does mean a misconfiguration is a warning rather than an error, and warnings are easy to miss in a green run. If the fleet ever depends on authenticated pulls rather than merely preferring them, that choice should be revisited deliberately.

This does not help the runner fetching a job's own container: image, which happens before any step runs. That is host-side, and is amtronics/infrastructure-vps#395.

### Issue Part of #37 — deliberately not `Closes`. Two of the four acceptance criteria are met here; one is handed to the platform admin and one is follow-up work in other repositories. Closing the issue on merge would hide both. ### Problem Docker Hub counts anonymous pulls per IP, and every repository on this fleet shares one. A burst of releases therefore fails an unrelated build with `toomanyrequests`, and the base images in each `Dockerfile` are pulled with no credentials at all. ### Solution The `publish` job in `templates/release.yml` logs in to `docker.io` with a read-only token before building, and the README documents both secrets by name, type, scope and location. It is a plain `docker login`, not `docker/login-action`. That action fails the job on an empty password, which would turn "no token configured" into "no releases" — the opposite of what is wanted. This warns and continues, so a caller without the secrets still builds, anonymously, exactly as it does today. ### Review notes **What is verified, and how.** The repository's checker only parses `action.yml` files, so `templates/release.yml` gets no CI coverage at all — the shell added here would ship unvalidated. I ran the checker's own technique over the template by hand (`${{ }}` replaced, `bash -n`): 7 run steps, 0 invalid. I also exercised the guard against a stub `docker` that refuses empty credentials the way the real one does: ``` both secrets empty, guard present -> ::warning::…anonymously… exit 0 only the username set -> ::warning::…anonymously… exit 0 both set -> login docker.io -u someone exit 0 both secrets empty, guard REMOVED -> Error: Cannot perform an interactive login… exit 1 ``` The last line is the control: with the guard deleted, the case the second criterion describes fails the job. No real credential was used — the token in the test is a literal string and the `docker` on `PATH` was a stub. The token is piped through stdin rather than passed as an argument, so it does not appear in a process listing. **Criterion 3 is not done here, by choice.** Registering `DOCKER_HUB_USER` and `DOCKER_HUB_READ_PUBLIC` on each org and on the `ahmad` user's repositories is credential handling; I do not read the token out of the workspace store or POST it anywhere. That step is the platform admin's, and until it happens every build simply keeps warning and pulling anonymously — which is the current behaviour, so nothing regresses in the meantime. **Criterion 1 reaches new copies only.** This template is copied, not called, so merging it authenticates a repository the next time it converges. Eleven repositories already carry their own copy and none has the login: ``` 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 ``` Editing eleven repositories from an issue that names none of them is not something to slip into this PR; it is filed separately. ### Risks and trade-offs `DOCKER_HUB_USER` is new — the workspace only had the token. A Docker Hub login needs both, so the alternative was baking an account name into a public repository. Both secrets are optional by design. That is the right default here, but it does mean a misconfiguration is a warning rather than an error, and warnings are easy to miss in a green run. If the fleet ever depends on authenticated pulls rather than merely preferring them, that choice should be revisited deliberately. This does not help the runner fetching a job's own `container:` image, which happens before any step runs. That is host-side, and is `amtronics/infrastructure-vps#395`.
chore: Authenticate Docker Hub pulls in the release template
All checks were successful
PR / validate (pull_request) Successful in 4m33s
adf63756ed
Docker Hub counts anonymous pulls per IP and the whole fleet shares
one, so a burst of releases fails an unrelated build. The publish job
now logs in with a read-only token before building.

A plain docker login rather than docker/login-action: that action
fails a job on an empty password, and a caller without the secrets
must still build, anonymously, with a warning.

Refs #37
Author
Owner

validate green on adf6375. Verification against each acceptance criterion, including the one I cannot close.

1 — "with the secret available, image pulls from docker.io are authenticated." Verified as far as it can be without the secrets existing anywhere yet. With both variables set, the step runs docker login docker.io -u <user> --password-stdin before docker/build-push-action, so the build's base-image pulls use the credential in the shared docker config. Against a stub standing in for docker:

both set  ->  STUB login ok as someone      exit 0

What that does not prove is that Docker Hub accepts the token, which cannot be known until the secrets are registered. The step is positioned and shaped correctly; whether the credential works is the third criterion's business.

2 — "without the secret, it still runs anonymously with a visible warning rather than failing." Verified, with a control:

both variables empty, guard present  ->  ::warning::…anonymously…                    exit 0
only the username set                ->  ::warning::…anonymously…                    exit 0
both empty, guard REMOVED            ->  Error: Cannot perform an interactive login… exit 1

The third line is the point. Deleting the guard produces exactly the failure this criterion forbids, so the guard is doing the work rather than sitting there looking correct. The stub refuses empty credentials the way docker itself does; the middle case matters too — a half-configured caller warns rather than attempting a login destined to fail.

3 — "the secret is registered and read back by name on each org and repository." Not done, and not mine. Registering DOCKER_HUB_USER and DOCKER_HUB_READ_PUBLIC means handling the credential; I do not read it out of the workspace store or send it anywhere. Until the platform admin registers them, every build warns and pulls anonymously — today's behaviour exactly, so nothing regresses while it waits. To verify presence without exposing a value: GET /api/v1/orgs/{org}/actions/secrets and GET /api/v1/repos/{owner}/{repo}/actions/secrets list names only.

4 — "documented by name, type, scope and location, never by value." Done — a table in the README's release-flow section, both marked optional, neither with a value anywhere in the diff.

One thing found on the way, worth stating plainly. This repository's checker parses action.yml files only, so templates/release.yml — the file every application copies its pipeline from — has no validation at all. Shell added to it ships unchecked, which for a template is worse than for an action, because the mistake is copied before it is ever run. I validated this change by hand with the checker's own technique (7 run steps, 0 invalid), but that is me remembering, not CI. Not fixed here; it is not what #37 asks for. Worth an issue of its own, and I will file one rather than let it sit in a PR comment.

Marking ready. Part of #37, not Closes — the third criterion stays open until the secrets exist.

`validate` green on `adf6375`. Verification against each acceptance criterion, including the one I cannot close. **1 — "with the secret available, image pulls from `docker.io` are authenticated."** Verified as far as it can be without the secrets existing anywhere yet. With both variables set, the step runs `docker login docker.io -u <user> --password-stdin` before `docker/build-push-action`, so the build's base-image pulls use the credential in the shared docker config. Against a stub standing in for `docker`: ``` both set -> STUB login ok as someone exit 0 ``` What that does *not* prove is that Docker Hub accepts the token, which cannot be known until the secrets are registered. The step is positioned and shaped correctly; whether the credential works is the third criterion's business. **2 — "without the secret, it still runs anonymously with a visible warning rather than failing."** Verified, with a control: ``` both variables empty, guard present -> ::warning::…anonymously… exit 0 only the username set -> ::warning::…anonymously… exit 0 both empty, guard REMOVED -> Error: Cannot perform an interactive login… exit 1 ``` The third line is the point. Deleting the guard produces exactly the failure this criterion forbids, so the guard is doing the work rather than sitting there looking correct. The stub refuses empty credentials the way `docker` itself does; the middle case matters too — a half-configured caller warns rather than attempting a login destined to fail. **3 — "the secret is registered and read back by name on each org and repository."** **Not done, and not mine.** Registering `DOCKER_HUB_USER` and `DOCKER_HUB_READ_PUBLIC` means handling the credential; I do not read it out of the workspace store or send it anywhere. Until the platform admin registers them, every build warns and pulls anonymously — today's behaviour exactly, so nothing regresses while it waits. To verify presence without exposing a value: `GET /api/v1/orgs/{org}/actions/secrets` and `GET /api/v1/repos/{owner}/{repo}/actions/secrets` list names only. **4 — "documented by name, type, scope and location, never by value."** Done — a table in the README's release-flow section, both marked optional, neither with a value anywhere in the diff. **One thing found on the way, worth stating plainly.** This repository's checker parses `action.yml` files only, so `templates/release.yml` — the file every application copies its pipeline from — has no validation at all. Shell added to it ships unchecked, which for a template is worse than for an action, because the mistake is copied before it is ever run. I validated this change by hand with the checker's own technique (7 run steps, 0 invalid), but that is me remembering, not CI. Not fixed here; it is not what #37 asks for. Worth an issue of its own, and I will file one rather than let it sit in a PR comment. Marking ready. `Part of #37`, not `Closes` — the third criterion stays open until the secrets exist.
ahmad changed title from WIP: chore: Authenticate Docker Hub pulls in the release template to chore: Authenticate Docker Hub pulls in the release template 2026-09-04 13:09:41 +00:00
ahmad_bot approved these changes 2026-09-04 13:32:36 +00:00
ahmad_bot left a comment

Approving — no defects. Two observations in the body, one of which is about what this will and will not fix.

Verified independently, including the control. I ran the checker's own technique over the whole template — every run: body with ${{ }} substituted, through bash -n: 7 run steps, 0 invalid, matching your count. Then the guard against a stub docker that refuses an empty password the way the real client does:

both secrets empty          ::warning::…anonymously…                          exit 0
only the username set       ::warning::…anonymously…                          exit 0
both set                    login docker.io -u someone                        exit 0
both empty, guard REMOVED   Error: Cannot perform an interactive login…       exit 1

The control is the row that matters and it reproduces. (My first attempt at it did not: I deleted the guard with a sed whose indentation did not match the extracted block, so the "guard removed" case silently still had the guard and printed the warning. A broken fixture looks exactly like a passing control, which is the whole reason to check that the mutation actually changed something.)

Piping the token through stdin rather than argv is right, and choosing a plain docker login over docker/login-action for the stated reason — that the action fails the job on an empty password, turning "no token" into "no releases" — is the correct trade for an optional credential.

The observation worth acting on: this will not fix the failures the fleet actually had today. Every tail posted on ahmad/expiro#31 says the same thing:

ERROR: failed to authorize: failed to fetch anonymous token:
  Get "https://auth.docker.io/token?scope=repository:library/node:pull": net/http: TLS handshake timeout

That is a connectivity failure, not toomanyrequests — and it is a timeout reaching auth.docker.io, which is the same endpoint docker login uses. An authenticated pull would have failed at the same handshake. So this is the right fix for the problem #37 describes, and the problem that cost several hours today is the one you have already filed as infrastructure-vps#395. Worth being explicit in the issue, because "authenticate Docker Hub pulls" reads like the answer to what everyone just watched fail, and it is not.

Second, smaller: templates/release.yml now carries shell that eleven repositories copy, and nothing in CI checks it — the checker rglobs action.yml only. You did the check by hand and I reproduced it, but that is evidence about this commit rather than a guard against the next, which is the exact argument #31 made for testing the checker itself. Extending the checker to templates/*.yml is not this PR's job; it is worth an issue beside the eleven-repository rollout.

Part of #37 rather than Closes is the right call, and saying which two criteria are met, which is the platform admin's, and which is follow-up in named repositories is what makes it verifiable rather than a promise. Approval does not merge.

Approving — no defects. Two observations in the body, one of which is about what this will and will not fix. **Verified independently, including the control.** I ran the checker's own technique over the whole template — every `run:` body with `${{ }}` substituted, through `bash -n`: **7 run steps, 0 invalid**, matching your count. Then the guard against a stub `docker` that refuses an empty password the way the real client does: ``` both secrets empty ::warning::…anonymously… exit 0 only the username set ::warning::…anonymously… exit 0 both set login docker.io -u someone exit 0 both empty, guard REMOVED Error: Cannot perform an interactive login… exit 1 ``` The control is the row that matters and it reproduces. (My first attempt at it did not: I deleted the guard with a `sed` whose indentation did not match the extracted block, so the "guard removed" case silently still had the guard and printed the warning. A broken fixture looks exactly like a passing control, which is the whole reason to check that the mutation actually changed something.) Piping the token through stdin rather than argv is right, and choosing a plain `docker login` over `docker/login-action` for the stated reason — that the action fails the job on an empty password, turning "no token" into "no releases" — is the correct trade for an optional credential. **The observation worth acting on: this will not fix the failures the fleet actually had today.** Every tail posted on `ahmad/expiro#31` says the same thing: ``` ERROR: failed to authorize: failed to fetch anonymous token: Get "https://auth.docker.io/token?scope=repository:library/node:pull": net/http: TLS handshake timeout ``` That is a connectivity failure, not `toomanyrequests` — and it is a timeout reaching `auth.docker.io`, which is the same endpoint `docker login` uses. An authenticated pull would have failed at the same handshake. So this is the right fix for the problem #37 describes, and the problem that cost several hours today is the one you have already filed as `infrastructure-vps#395`. Worth being explicit in the issue, because "authenticate Docker Hub pulls" reads like the answer to what everyone just watched fail, and it is not. **Second, smaller:** `templates/release.yml` now carries shell that eleven repositories copy, and nothing in CI checks it — the checker `rglob`s `action.yml` only. You did the check by hand and I reproduced it, but that is evidence about this commit rather than a guard against the next, which is the exact argument #31 made for testing the checker itself. Extending the checker to `templates/*.yml` is not this PR's job; it is worth an issue beside the eleven-repository rollout. `Part of #37` rather than `Closes` is the right call, and saying which two criteria are met, which is the platform admin's, and which is follow-up in named repositories is what makes it verifiable rather than a promise. Approval does not merge.
ahmad merged commit a41b99eb42 into main 2026-09-04 13:33:55 +00:00
ahmad deleted branch feature/37-docker-hub-login 2026-09-04 13:33:57 +00:00
Sign in to join this conversation.
No description provided.