The docker/login-action Username and password required error is thrown before any network call, by a three branch check that has two other messages available to it. It chose this one because both values were empty, which is a different problem from one misnamed secret.

The three branch credential check in login-action and the message each branch throws
Read in order from src/docker.ts. The first branch requires both values to be falsy, so "Username and password required" is the message that means neither arrived.

What this error means

The step fails in under a second, with no registry named and no connection attempted. Steps before it are green, and the build step after it is skipped. The wording almost never changes, which is why it is easy to miss that it is one of three possible sentences: the action also has Username required and Password required, and the one you got rules the other two out.

```Actions log, the message loginStandard throws when both inputs are empty
Run docker/login-action@v3
Error: Username and password required

[error]Username and password required

 ## Common causes ### The job never had the secrets, so both interpolations became empty The common shape, and the one the wording points at. A pull request from a fork gets no repository secrets, a job in a reusable workflow that was called without `secrets: inherit` gets none either, and both cases empty the whole `with:` block at once rather than one line of it. ### The with: block is misindented and its values are not inputs YAML will accept a `username` key that sits one level too deep or beside `uses` instead of under `with`, and the action then receives nothing for either name. Both values are empty for the same reason, which is what distinguishes this from a spelling mistake. ### Secrets exist at a scope this repository cannot read An organization secret that was never shared with this repository, or an environment secret in a job that does not name the environment, resolves to an empty string with no warning. When one set of credentials is stored together this way, both halves disappear together. ### The login step runs on an event that carries no secrets Less common but persistent: a workflow that is correct on push and empty on `pull_request` from a fork. The step is unchanged between the two runs, which is why the file gets read repeatedly and looks fine each time. ## How to fix it ### Use the sentence to pick the search 1. If the message names both values, stop checking secret names and check whether this job receives secrets at all. 2. Open the job's event: a fork pull request and an uninherited reusable workflow both empty everything. 3. If the message names one value, the other arrived, and a single name or scope is wrong. ### Guard the step rather than letting it fail the job When a job legitimately runs both with and without credentials, test the value through `env` rather than through the `secrets` context, which is not available in an `if` at either job or step level. Copy the secret into `env` first, then branch on that. ```.github/workflows/publish.yml (illustrative) - name: Log in to the registry if: env.REGISTRY_USER != '' env: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} 

Name the registry explicitly

Set registry even when you think the default is right. It costs one line and it removes the most confusing symptom of the both-empty case, which is a failure that talks about Docker Hub in a workflow that has nothing to do with Docker Hub.

For ECR, expect a different failure entirely

If the registry is ECR, or ecr: true is set, the three sentences cannot be produced. An empty credential there surfaces through the AWS SDK instead. Do not spend time looking for this message in a job that takes that path.

How to prevent it

  • Put the login step in a job that also declares which environment or which secrets it needs, so an empty value is a visible configuration change rather than a silent one.
  • Prefer ${{ github.token }} and packages: write over a stored credential when the registry is ghcr.io, since there is then nothing to leave unset.
  • When a workflow must run on fork pull requests, guard the login step rather than letting it take the job red.
  • Set registry on every login step, so the failure names the host you meant.

Three messages, and the one you got

The check lives in loginStandard in src/docker.ts and runs before anything is sent anywhere. It is a ladder, and the order matters, because the first branch that matches wins and the later ones are never reached.

```docker/login-action src/docker.ts, loginStandard
if (!username && !password) {
throw new Error('Username and password required');
}
if (!username) {
throw new Error('Username required');
}
if (!password) {
throw new Error('Password required');
}

 ## What the wording rules out Because the branches are exclusive, the sentence you were given is a measurement rather than a generic complaint. The table is the whole of it. | Sentence thrown | Username value | Password value | Where to look first | | --- | --- | --- | --- | | `Username and password required` | Empty | Empty | The `with:` block itself: indentation, a step that lost its inputs, or a job that never had the secrets | | `Username required` | Empty | Present | The username secret or variable alone | | `Password required` | Present | Empty | The password secret alone, which is the misnamed-secret case | > This is why the common advice to check the spelling of one secret so often changes nothing. A single misnamed password secret produces `Password required`, not the sentence on this page. Getting both empty usually means the values never reached the step at all, rather than that two names are wrong at once. ## The branch only runs for some registries The ladder sits inside `loginStandard`, and `login` decides between that and `loginECR` before any of it happens. The ECR path is taken when the `ecr` input is `true`, or when it is `auto` and the registry host looks like ECR. On that path the three sentences are unreachable, and an empty credential produces an AWS SDK failure instead, naming a region or a profile. The other thing `login` does first is worth knowing for the both-empty case: when the `registry` input is also empty, there is no registry to name, and the action logs in to Docker Hub by default. A job that meant to authenticate to a private registry, and whose whole `with:` block failed to resolve, therefore fails while talking about Docker Hub, which it was never configured to use. ## Why there is no recorded run on this page The throw happens before a socket is opened, from two values that were empty in your repository's secret store rather than on any machine. A run of ours would have to deliberately leave our own secrets unset, and the log it produced would be the same single line, carrying no information about which of your names, scopes or triggers was responsible. The sentence above is the one this branch throws, read in the action's source, and the table is that source's own ordering rather than a reproduction. ## FAQ ### Why does it say Docker Hub when I am pushing to a private registry? Because the `registry` input was empty too. The action defaults to Docker Hub when no registry is given, so a `with:` block that failed to resolve as a whole takes the default along with everything else. The mention of Docker Hub is a symptom of the same emptiness, not a separate misconfiguration. ### I only misnamed the password secret, so why does it name both? It would not. A present username with an empty password reaches the third branch and throws `Password required`. If the message names both, both values were empty when the step started, which points at the block, the event or the secret scope rather than at one name. ### Can I check whether the secret exists before the login step? Not through the `secrets` context in an `if`, because that context is not available in a job-level or step-level `if` condition. Assign the secret to `env` on the step or the job and test `env.NAME` instead. The value is still masked in the log; only its emptiness is observable. ### Does the message differ between login-action versions? The three sentences and their order have been stable in `loginStandard` across the v2 and v3 majors. What has moved around them is the dispatch above: which registries are treated as ECR, and the addition of a Docker Hub OIDC path that replaces the username and password before the ladder sees them. ## References - [docker/login-action: src/docker.ts, the loginStandard credential check](https://github.com/docker/login-action/blob/master/src/docker.ts) - [docker/login-action: action.yml, the username and password inputs](https://github.com/docker/login-action/blob/master/action.yml) - [GitHub Actions: contexts reference, where the secrets context may be used](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability)