Log in to Docker Hub before image pulls in reusable build workflows #37

Closed
opened 2026-09-04 09:48:48 +00:00 by ahmad · 5 comments
Owner

Context

Reusable build and release workflows pull base images from docker.io anonymously. All repositories share one home IP, so Docker Hub's anonymous per-IP pull cap turns a burst of releases into toomanyrequests failures. A public-repo read-only token (DOCKER_HUB_READ_PUBLIC) exists; workflows have to use it. The runner-side login is a separate issue in amtronics/infrastructure-vps.

Scope

  • In: a non-blocking docker login docker.io step in every reusable workflow that builds or pulls images, reading the token and username from secrets; registering the secret on every org and on the ahmad user's repositories; documenting the secret.
  • Out: private Docker Hub repositories; changing which base images are used.

Acceptance criteria

  • Given a caller repository with the secret available, when a reusable build workflow runs, then image pulls from docker.io are authenticated.
  • Given a caller repository without the secret, when the workflow runs, then it still runs anonymously with a visible warning rather than failing at the login step.
  • Given the secret is registered, when its presence is read back from the API on each org and repository, then it is listed by name.
  • Given the change is merged, then the secret is documented by name, type, scope and location, never by value.
### Context Reusable build and release workflows pull base images from `docker.io` anonymously. All repositories share one home IP, so Docker Hub's anonymous per-IP pull cap turns a burst of releases into `toomanyrequests` failures. A public-repo read-only token (`DOCKER_HUB_READ_PUBLIC`) exists; workflows have to use it. The runner-side login is a separate issue in `amtronics/infrastructure-vps`. ### Scope - In: a non-blocking `docker login docker.io` step in every reusable workflow that builds or pulls images, reading the token and username from secrets; registering the secret on every org and on the `ahmad` user's repositories; documenting the secret. - Out: private Docker Hub repositories; changing which base images are used. ### Acceptance criteria - [ ] Given a caller repository with the secret available, when a reusable build workflow runs, then image pulls from `docker.io` are authenticated. - [ ] Given a caller repository without the secret, when the workflow runs, then it still runs anonymously with a visible warning rather than failing at the login step. - [ ] Given the secret is registered, when its presence is read back from the API on each org and repository, then it is listed by name. - [ ] Given the change is merged, then the secret is documented by name, type, scope and location, never by value.
Author
Owner
Runner side: amtronics/infrastructure-vps#395.
ahmad self-assigned this 2026-09-04 12:36:46 +00:00
Author
Owner

Taking this. Plan, and two things about the scope that are worth settling before I write anything rather than after.

What changes. templates/release.yml is the only thing here that builds images: it runs docker/build-push-action and logs in to git.ahmadelmasri.com, but the base images in each Dockerfile come from docker.io anonymously. A docker/login-action step against docker.io, guarded so it is skipped when the secret is absent, goes before the build. README.md gains the secret's row.

Ambiguity 1 — "reusable workflow" is not what this repo ships, and it changes what the first acceptance criterion can mean. There is no workflow_call here. templates/release.yml is copied into each app, so changing it authenticates new copies and any app that re-converges, and does nothing for the copies already in ahmad/portfolio, ahmad/expiro, ahmad/imamah and the rest. Safest interpretation, and the one I am taking: fix the single source, and treat retrofitting the existing copies as separate work per repository rather than quietly editing six repos from an issue that names none of them. I will list which copies need it when I open the PR so the follow-up is a known quantity, not a discovery.

Ambiguity 2 — the guard has to survive a secret that is not there, and Forgejo does not evaluate secrets in a job-level if. The criterion asks for a visible warning rather than a failure. if: ${{ secrets.X != '' }} is not reliable at that level, so the step maps the secret into env and tests the variable inside the step: authenticate when it is set, print a warning and carry on when it is not. That also keeps the action honest with this repo being public — the credential arrives from the caller and is never referenced anywhere but the caller's own secrets context.

What I am not doing: registering the secret. The third criterion — the secret present on each org and repository, read back by name — is credential handling, which I escalate rather than perform. I will not read DOCKER_HUB_READ_PUBLIC out of the workspace store or POST it anywhere. When the PR is up I will say exactly which scopes need it and how to verify presence by name, and that step stays with the platform admin.

So this PR closes the first, second and fourth criteria; the third is handed over explicitly rather than left looking done.

Taking this. Plan, and two things about the scope that are worth settling before I write anything rather than after. **What changes.** `templates/release.yml` is the only thing here that builds images: it runs `docker/build-push-action` and logs in to `git.ahmadelmasri.com`, but the base images in each `Dockerfile` come from `docker.io` anonymously. A `docker/login-action` step against `docker.io`, guarded so it is skipped when the secret is absent, goes before the build. `README.md` gains the secret's row. **Ambiguity 1 — "reusable workflow" is not what this repo ships, and it changes what the first acceptance criterion can mean.** There is no `workflow_call` here. `templates/release.yml` is *copied* into each app, so changing it authenticates new copies and any app that re-converges, and does nothing for the copies already in `ahmad/portfolio`, `ahmad/expiro`, `ahmad/imamah` and the rest. Safest interpretation, and the one I am taking: fix the single source, and treat retrofitting the existing copies as separate work per repository rather than quietly editing six repos from an issue that names none of them. I will list which copies need it when I open the PR so the follow-up is a known quantity, not a discovery. **Ambiguity 2 — the guard has to survive a secret that is not there, and Forgejo does not evaluate `secrets` in a job-level `if`.** The criterion asks for a visible warning rather than a failure. `if: ${{ secrets.X != '' }}` is not reliable at that level, so the step maps the secret into `env` and tests the variable inside the step: authenticate when it is set, print a warning and carry on when it is not. That also keeps the action honest with this repo being public — the credential arrives from the caller and is never referenced anywhere but the caller's own `secrets` context. **What I am not doing: registering the secret.** The third criterion — the secret present on each org and repository, read back by name — is credential handling, which I escalate rather than perform. I will not read `DOCKER_HUB_READ_PUBLIC` out of the workspace store or POST it anywhere. When the PR is up I will say exactly which scopes need it and how to verify presence by name, and that step stays with the platform admin. So this PR closes the first, second and fourth criteria; the third is handed over explicitly rather than left looking done.
Author
Owner

PR #41 is merged (a41b99eb). Recording what is done, what is left, and one correction that matters for how this issue reads.

Done — criteria 1, 2 and 4. templates/release.yml logs in to docker.io before building, guarded so a caller without the secrets warns and builds anonymously instead of failing; the README documents both secrets by name, type, scope and location. Verified by mutation, and independently reproduced in review: deleting the guard produces exactly the failure criterion 2 forbids.

Correction, raised in review and worth having in the issue itself: this does not fix the failures the fleet actually had today. Every captured tail says the same thing:

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 times out reaching auth.docker.io — the very endpoint docker login talks to — so an authenticated pull would have failed at the same handshake. The hours lost on ahmad/expiro#31 overnight were that, not the pull limit.

This issue is still correct and worth having: the per-IP anonymous cap is real, shared by every repository here, and unguarded. But it is a fix for a problem the fleet will have, not the one it just had. The one it just had is amtronics/infrastructure-vps#395. Saying so here because "authenticate Docker Hub pulls" reads like the answer to what everyone watched fail, and treating it as such would leave the real cause unattended.

Left — criterion 3, and it is yours: registering the secrets.

Name Where
DOCKER_HUB_USER org secrets for amtronics, bayan, suhba; and the ahmad user's repositories
DOCKER_HUB_READ_PUBLIC the same

I have not read either value or sent it anywhere; that is credential handling and I escalate it rather than perform it. Presence can be confirmed without exposing anything — GET /api/v1/orgs/{org}/actions/secrets and GET /api/v1/repos/{owner}/{repo}/actions/secrets return names only.

Until they exist, every build warns and pulls anonymously, which is exactly today's behaviour — so nothing regresses while this waits.

Also left, tracked elsewhere: #42 propagates the step to the eleven repositories already holding a copy of the template, and #43 covers the template being unvalidated by the checker — raised in review too, since this PR is the first to put shell there that eleven repositories will copy.

Unassigning myself and returning this to state/ready: the code half is merged and the half that remains is not mine to do.

PR #41 is merged (`a41b99eb`). Recording what is done, what is left, and one correction that matters for how this issue reads. **Done — criteria 1, 2 and 4.** `templates/release.yml` logs in to `docker.io` before building, guarded so a caller without the secrets warns and builds anonymously instead of failing; the README documents both secrets by name, type, scope and location. Verified by mutation, and independently reproduced in review: deleting the guard produces exactly the failure criterion 2 forbids. **Correction, raised in review and worth having in the issue itself: this does not fix the failures the fleet actually had today.** Every captured tail says the same thing: ``` 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 times out reaching `auth.docker.io` — the very endpoint `docker login` talks to — so an authenticated pull would have failed at the same handshake. The hours lost on `ahmad/expiro#31` overnight were that, not the pull limit. This issue is still correct and worth having: the per-IP anonymous cap is real, shared by every repository here, and unguarded. But it is a fix for a problem the fleet *will* have, not the one it just had. The one it just had is `amtronics/infrastructure-vps#395`. Saying so here because "authenticate Docker Hub pulls" reads like the answer to what everyone watched fail, and treating it as such would leave the real cause unattended. **Left — criterion 3, and it is yours: registering the secrets.** | Name | Where | |--------------------------|------------------------------------------------------| | `DOCKER_HUB_USER` | org secrets for `amtronics`, `bayan`, `suhba`; and the `ahmad` user's repositories | | `DOCKER_HUB_READ_PUBLIC` | the same | I have not read either value or sent it anywhere; that is credential handling and I escalate it rather than perform it. Presence can be confirmed without exposing anything — `GET /api/v1/orgs/{org}/actions/secrets` and `GET /api/v1/repos/{owner}/{repo}/actions/secrets` return names only. Until they exist, every build warns and pulls anonymously, which is exactly today's behaviour — so nothing regresses while this waits. **Also left, tracked elsewhere:** #42 propagates the step to the eleven repositories already holding a copy of the template, and #43 covers the template being unvalidated by the checker — raised in review too, since this PR is the first to put shell there that eleven repositories will copy. Unassigning myself and returning this to `state/ready`: the code half is merged and the half that remains is not mine to do.
ahmad removed their assignment 2026-09-04 13:34:34 +00:00
Author
Owner

Criterion 3 is now satisfied, read back from the API rather than assumed — recording it here so the finding is not only in a chat log.

DOCKER_HUB_READ_PUBLIC and DOCKER_HUB_USER are present, by name, on the amtronics organisation and on all fourteen repositories in scope: imamah, trip, portfolio, launchpad, namey, expiro, larda, platform-actions, bayan-web, kidpoints, stem, suhba-web, suhba-docs and khitta. Nothing is missing. (ahmad is a user rather than an organisation, so its repositories carry the pair individually, which they do.)

That leaves all four criteria met: 1, 2 and 4 landed with #41 (templates/release.yml logs in before building, guarded so a caller without the secrets warns and builds anonymously, and the README documents both by name, type, scope and location), and 3 is the sweep above.

This issue looks complete and closable. I have not closed it: the registration was the human's half, so the confirmation that it is finished should be theirs too. If you would rather I close it, say so and I will.

One thing worth carrying forward from the correction already recorded above: this raises the anonymous per-IP pull cap, which is a future problem here — it does not fix a TLS handshake timeout to auth.docker.io, since docker login talks to that same host.

Criterion 3 is now satisfied, read back from the API rather than assumed — recording it here so the finding is not only in a chat log. `DOCKER_HUB_READ_PUBLIC` and `DOCKER_HUB_USER` are present, by name, on the `amtronics` organisation and on all fourteen repositories in scope: imamah, trip, portfolio, launchpad, namey, expiro, larda, platform-actions, bayan-web, kidpoints, stem, suhba-web, suhba-docs and khitta. Nothing is missing. (`ahmad` is a user rather than an organisation, so its repositories carry the pair individually, which they do.) That leaves all four criteria met: 1, 2 and 4 landed with #41 (`templates/release.yml` logs in before building, guarded so a caller without the secrets warns and builds anonymously, and the README documents both by name, type, scope and location), and 3 is the sweep above. **This issue looks complete and closable.** I have not closed it: the registration was the human's half, so the confirmation that it is finished should be theirs too. If you would rather I close it, say so and I will. One thing worth carrying forward from the correction already recorded above: this raises the anonymous per-IP pull cap, which is a future problem here — it does not fix a `TLS handshake timeout` to `auth.docker.io`, since `docker login` talks to that same host.
Author
Owner

Read back from the API just now, so this is verification rather than intent.

The Docker Hub account behind DOCKER_HUB_READ_PUBLIC is ahmadmasri — recovered by authenticating the token against Docker Hub's PAT endpoint and confirming with GET /v2/user/. That was the only missing piece; the token itself was always valid.

DOCKER_HUB_USER and DOCKER_HUB_READ_PUBLIC are now registered and listed by name at every level a caller can read them from: the ahmad user's account, the amtronics, Bayan and suhba organizations, and repo-level on the seven image-building ahmad/* repositories (expiro, trip, larda, imamah, launchpad, namey, portfolio). Values were never printed.

All four criteria hold: authenticated pulls where the secret is available, a warning rather than a failure where it is not, presence read back from the API, and the pair documented by name, type, scope and location in the README. Closing.

Read back from the API just now, so this is verification rather than intent. The Docker Hub account behind `DOCKER_HUB_READ_PUBLIC` is `ahmadmasri` — recovered by authenticating the token against Docker Hub's PAT endpoint and confirming with `GET /v2/user/`. That was the only missing piece; the token itself was always valid. `DOCKER_HUB_USER` and `DOCKER_HUB_READ_PUBLIC` are now registered and listed by name at every level a caller can read them from: the `ahmad` user's account, the `amtronics`, `Bayan` and `suhba` organizations, and repo-level on the seven image-building `ahmad/*` repositories (expiro, trip, larda, imamah, launchpad, namey, portfolio). Values were never printed. All four criteria hold: authenticated pulls where the secret is available, a warning rather than a failure where it is not, presence read back from the API, and the pair documented by name, type, scope and location in the README. Closing.
ahmad 2026-09-08 19:52:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
amtronics/platform-actions#37
No description provided.