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

Open
opened 2026-09-04 09:48:48 +00:00 by ahmad · 3 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
Sign in to join this conversation.
No description provided.