Skip to content

Commit 8339c95

Browse files
authored
Merge pull request #912 from docker/scope
Add scope input to set scopes for the authentication token
2 parents 0567fa5 + b268aa5 commit 8339c95

File tree

10 files changed

+332
-91
lines changed

10 files changed

+332
-91
lines changed

.github/workflows/ci.yml

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ jobs:
207207
with:
208208
registry: public.ecr.aws
209209

210-
github-container:
210+
ghcr:
211211
runs-on: ${{ matrix.os }}
212212
strategy:
213213
fail-fast: false
@@ -356,3 +356,125 @@ jobs:
356356
echo "::error::Should have failed"
357357
exit 1
358358
fi
359+
360+
scope-dockerhub:
361+
runs-on: ${{ matrix.os }}
362+
strategy:
363+
fail-fast: false
364+
matrix:
365+
os:
366+
- ubuntu-latest
367+
- windows-latest
368+
steps:
369+
-
370+
name: Checkout
371+
uses: actions/checkout@v6
372+
-
373+
name: Login to Docker Hub
374+
uses: ./
375+
with:
376+
username: ${{ secrets.DOCKERHUB_USERNAME }}
377+
password: ${{ secrets.DOCKERHUB_TOKEN }}
378+
scope: '@push'
379+
-
380+
name: Print config.json files
381+
shell: bash
382+
run: |
383+
shopt -s globstar nullglob
384+
for file in ~/.docker/**/config.json; do
385+
echo "## ${file}"
386+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
387+
echo ""
388+
done
389+
390+
scope-dockerhub-repo:
391+
runs-on: ${{ matrix.os }}
392+
strategy:
393+
fail-fast: false
394+
matrix:
395+
os:
396+
- ubuntu-latest
397+
- windows-latest
398+
steps:
399+
-
400+
name: Checkout
401+
uses: actions/checkout@v6
402+
-
403+
name: Login to Docker Hub
404+
uses: ./
405+
with:
406+
username: ${{ secrets.DOCKERHUB_USERNAME }}
407+
password: ${{ secrets.DOCKERHUB_TOKEN }}
408+
scope: 'docker/buildx-bin@push'
409+
-
410+
name: Print config.json files
411+
shell: bash
412+
run: |
413+
shopt -s globstar nullglob
414+
for file in ~/.docker/**/config.json; do
415+
echo "## ${file}"
416+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
417+
echo ""
418+
done
419+
420+
scope-ghcr:
421+
runs-on: ${{ matrix.os }}
422+
strategy:
423+
fail-fast: false
424+
matrix:
425+
os:
426+
- ubuntu-latest
427+
- windows-latest
428+
steps:
429+
-
430+
name: Checkout
431+
uses: actions/checkout@v6
432+
-
433+
name: Login to GitHub Container Registry
434+
uses: ./
435+
with:
436+
registry: ghcr.io
437+
username: ${{ github.actor }}
438+
password: ${{ secrets.GITHUB_TOKEN }}
439+
scope: '@push'
440+
-
441+
name: Print config.json files
442+
shell: bash
443+
run: |
444+
shopt -s globstar nullglob
445+
for file in ~/.docker/**/config.json; do
446+
echo "## ${file}"
447+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
448+
echo ""
449+
done
450+
451+
scope-ghcr-repo:
452+
runs-on: ${{ matrix.os }}
453+
strategy:
454+
fail-fast: false
455+
matrix:
456+
os:
457+
- ubuntu-latest
458+
- windows-latest
459+
steps:
460+
-
461+
name: Checkout
462+
uses: actions/checkout@v6
463+
-
464+
name: Login to GitHub Container Registry
465+
uses: ./
466+
with:
467+
registry: ghcr.io
468+
username: ${{ github.actor }}
469+
password: ${{ secrets.GITHUB_TOKEN }}
470+
scope: 'docker/login-action@push'
471+
-
472+
name: Print config.json files
473+
shell: bash
474+
run: |
475+
shopt -s globstar nullglob
476+
for file in ~/.docker/**/config.json; do
477+
echo "## ${file}"
478+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
479+
echo ""
480+
done

README.md

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ___
2525
* [Quay.io](#quayio)
2626
* [DigitalOcean](#digitalocean-container-registry)
2727
* [Authenticate to multiple registries](#authenticate-to-multiple-registries)
28+
* [Set scopes for the authentication token](#set-scopes-for-the-authentication-token)
2829
* [Customizing](#customizing)
2930
* [inputs](#inputs)
3031
* [Contributing](#contributing)
@@ -527,8 +528,8 @@ jobs:
527528
```
528529

529530
You can also use the `registry-auth` input for raw authentication to
530-
registries, defined as YAML objects. Each object can contain `registry`,
531-
`username`, `password` and `ecr` keys similar to current inputs:
531+
registries, defined as YAML objects. Each object have the same attributes as
532+
current inputs (except `logout`):
532533

533534
> [!WARNING]
534535
> We don't recommend using this method, it's better to use the action multiple
@@ -557,6 +558,60 @@ jobs:
557558
password: ${{ secrets.GITHUB_TOKEN }}
558559
```
559560

561+
### Set scopes for the authentication token
562+
563+
The `scope` input allows limiting registry credentials to a specific repository
564+
or namespace scope when building images with Buildx.
565+
566+
This is useful in GitHub Actions to avoid overriding the Docker Hub
567+
authentication token embedded in GitHub-hosted runners, which is used for
568+
pulling images without rate limits. By scoping credentials, you can
569+
authenticate only where needed (typically for pushing), while keeping
570+
unauthenticated pulls for base images.
571+
572+
When `scope` is set, credentials are written to the Buildx configuration
573+
instead of the global Docker configuration. This means:
574+
* Authentication applies only to the specified scope
575+
* The default Docker Hub credentials remain available for pulls
576+
* Credentials are used only by Buildx during the build
577+
578+
> [!IMPORTANT]
579+
> Credentials written to the Buildx configuration are only accessible by Buildx.
580+
> They are not available to `docker pull`, `docker push`, or any other Docker
581+
> CLI commands outside Buildx.
582+
583+
> [!NOTE]
584+
> This feature requires Buildx version 0.31.0 or later.
585+
586+
```yaml
587+
name: ci
588+
589+
on:
590+
push:
591+
branches: main
592+
593+
jobs:
594+
login:
595+
runs-on: ubuntu-latest
596+
steps:
597+
-
598+
name: Login to Docker Hub (scoped)
599+
uses: docker/login-action@v3
600+
with:
601+
username: ${{ vars.DOCKERHUB_USERNAME }}
602+
password: ${{ secrets.DOCKERHUB_TOKEN }}
603+
scope: 'myorg/myimage@push'
604+
-
605+
name: Build and push
606+
uses: docker/build-push-action@v6
607+
with:
608+
push: true
609+
tags: myorg/myimage:latest
610+
```
611+
612+
In this example, base images are pulled using the embedded GitHub-hosted runner
613+
credentials, while authenticated access is used only to push `myorg/myimage`.
614+
560615
## Customizing
561616

562617
### inputs
@@ -568,13 +623,13 @@ The following inputs can be used as `step.with` keys:
568623
| `registry` | String | `docker.io` | Server address of Docker registry. If not set then will default to Docker Hub |
569624
| `username` | String | | Username for authenticating to the Docker registry |
570625
| `password` | String | | Password or personal access token for authenticating the Docker registry |
626+
| `scope` | String | | Scope for the authentication token |
571627
| `ecr` | String | `auto` | Specifies whether the given registry is ECR (`auto`, `true` or `false`) |
572628
| `logout` | Bool | `true` | Log out from the Docker registry at the end of a job |
573629
| `registry-auth` | YAML | | Raw authentication to registries, defined as YAML objects |
574630

575631
> [!NOTE]
576-
> The `registry-auth` input is mutually exclusive with `registry`, `username`,
577-
> `password` and `ecr` inputs.
632+
> The `registry-auth` input cannot be used with other inputs except `logout`.
578633

579634
## Contributing
580635

__tests__/docker.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ test('logout calls exec', async () => {
5050

5151
const registry = 'https://ghcr.io';
5252

53-
await logout(registry);
53+
await logout(registry, '');
5454

5555
expect(execSpy).toHaveBeenCalledTimes(1);
5656
const callfunc = execSpy.mock.calls[0];

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ inputs:
1919
ecr:
2020
description: 'Specifies whether the given registry is ECR (auto, true or false)'
2121
required: false
22+
scope:
23+
description: 'Scope for the authentication token'
24+
required: false
2225
logout:
2326
description: 'Log out from the Docker registry at the end of a job'
2427
default: 'true'

dist/index.js

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/context.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,90 @@
1+
import path from 'path';
12
import * as core from '@actions/core';
3+
import * as yaml from 'js-yaml';
4+
5+
import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx';
6+
import {Util} from '@docker/actions-toolkit/lib/util';
27

38
export interface Inputs {
49
registry: string;
510
username: string;
611
password: string;
12+
scope: string;
713
ecr: string;
814
logout: boolean;
915
registryAuth: string;
1016
}
1117

18+
export interface Auth {
19+
registry: string;
20+
username: string;
21+
password: string;
22+
scope: string;
23+
ecr: string;
24+
configDir: string;
25+
}
26+
1227
export function getInputs(): Inputs {
1328
return {
1429
registry: core.getInput('registry'),
1530
username: core.getInput('username'),
1631
password: core.getInput('password'),
32+
scope: core.getInput('scope'),
1733
ecr: core.getInput('ecr'),
1834
logout: core.getBooleanInput('logout'),
1935
registryAuth: core.getInput('registry-auth')
2036
};
2137
}
38+
39+
export function getAuthList(inputs: Inputs): Array<Auth> {
40+
if (inputs.registryAuth && (inputs.registry || inputs.username || inputs.password || inputs.scope || inputs.ecr)) {
41+
throw new Error('Cannot use registry-auth with other inputs');
42+
}
43+
let auths: Array<Auth> = [];
44+
if (!inputs.registryAuth) {
45+
auths.push({
46+
registry: inputs.registry || 'docker.io',
47+
username: inputs.username,
48+
password: inputs.password,
49+
scope: inputs.scope,
50+
ecr: inputs.ecr || 'auto',
51+
configDir: scopeToConfigDir(inputs.registry, inputs.scope)
52+
});
53+
} else {
54+
auths = (yaml.load(inputs.registryAuth) as Array<Auth>).map(auth => {
55+
core.setSecret(auth.password); // redacted in workflow logs
56+
return {
57+
registry: auth.registry || 'docker.io',
58+
username: auth.username,
59+
password: auth.password,
60+
scope: auth.scope,
61+
ecr: auth.ecr || 'auto',
62+
configDir: scopeToConfigDir(auth.registry || 'docker.io', auth.scope)
63+
};
64+
});
65+
}
66+
if (auths.length == 0) {
67+
throw new Error('No registry to login');
68+
}
69+
return auths;
70+
}
71+
72+
export function scopeToConfigDir(registry: string, scope?: string): string {
73+
if (scopeDisabled() || !scope || scope === '') {
74+
return '';
75+
}
76+
let configDir = path.join(Buildx.configDir, 'config', registry === 'docker.io' ? 'registry-1.docker.io' : registry);
77+
if (scope.startsWith('@')) {
78+
configDir += scope;
79+
} else {
80+
configDir = path.join(configDir, scope);
81+
}
82+
return configDir;
83+
}
84+
85+
function scopeDisabled(): boolean {
86+
if (process.env.DOCKER_LOGIN_SCOPE_DISABLED) {
87+
return Util.parseBool(process.env.DOCKER_LOGIN_SCOPE_DISABLED);
88+
}
89+
return false;
90+
}

0 commit comments

Comments
 (0)