Compare commits

..

2 Commits

Author SHA1 Message Date
CrazyMax
271085216c chore: update generated content
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-01-06 14:13:45 +01:00
CrazyMax
c92b895f48 Add scope input to set scopes for the authentication token
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-01-06 14:05:59 +01:00
12 changed files with 61 additions and 175 deletions

View File

@@ -375,7 +375,7 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
scope: '@push' scope: push
- -
name: Print config.json files name: Print config.json files
shell: bash shell: bash
@@ -405,7 +405,7 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
scope: 'docker/buildx-bin@push' scope: docker/buildx-bin@push
- -
name: Print config.json files name: Print config.json files
shell: bash shell: bash
@@ -436,7 +436,7 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
scope: '@push' scope: push
- -
name: Print config.json files name: Print config.json files
shell: bash shell: bash
@@ -467,7 +467,7 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
scope: 'docker/login-action@push' scope: docker/login-action@push
- -
name: Print config.json files name: Print config.json files
shell: bash shell: bash

View File

@@ -25,7 +25,6 @@ ___
* [Quay.io](#quayio) * [Quay.io](#quayio)
* [DigitalOcean](#digitalocean-container-registry) * [DigitalOcean](#digitalocean-container-registry)
* [Authenticate to multiple registries](#authenticate-to-multiple-registries) * [Authenticate to multiple registries](#authenticate-to-multiple-registries)
* [Set scopes for the authentication token](#set-scopes-for-the-authentication-token)
* [Customizing](#customizing) * [Customizing](#customizing)
* [inputs](#inputs) * [inputs](#inputs)
* [Contributing](#contributing) * [Contributing](#contributing)
@@ -528,8 +527,8 @@ jobs:
``` ```
You can also use the `registry-auth` input for raw authentication to You can also use the `registry-auth` input for raw authentication to
registries, defined as YAML objects. Each object have the same attributes as registries, defined as YAML objects. Each object can contain `registry`,
current inputs (except `logout`): `username`, `password` and `ecr` keys similar to current inputs:
> [!WARNING] > [!WARNING]
> We don't recommend using this method, it's better to use the action multiple > We don't recommend using this method, it's better to use the action multiple
@@ -558,60 +557,6 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
``` ```
### Set scopes for the authentication token
The `scope` input allows limiting registry credentials to a specific repository
or namespace scope when building images with Buildx.
This is useful in GitHub Actions to avoid overriding the Docker Hub
authentication token embedded in GitHub-hosted runners, which is used for
pulling images without rate limits. By scoping credentials, you can
authenticate only where needed (typically for pushing), while keeping
unauthenticated pulls for base images.
When `scope` is set, credentials are written to the Buildx configuration
instead of the global Docker configuration. This means:
* Authentication applies only to the specified scope
* The default Docker Hub credentials remain available for pulls
* Credentials are used only by Buildx during the build
> [!IMPORTANT]
> Credentials written to the Buildx configuration are only accessible by Buildx.
> They are not available to `docker pull`, `docker push`, or any other Docker
> CLI commands outside Buildx.
> [!NOTE]
> This feature requires Buildx version 0.31.0 or later.
```yaml
name: ci
on:
push:
branches: main
jobs:
login:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub (scoped)
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
scope: 'myorg/myimage@push'
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: myorg/myimage:latest
```
In this example, base images are pulled using the embedded GitHub-hosted runner
credentials, while authenticated access is used only to push `myorg/myimage`.
## Customizing ## Customizing
### inputs ### inputs

View File

@@ -11,7 +11,6 @@ describe('isECR', () => {
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', true], ['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', true],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', true], ['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', true],
['012345678901.dkr-ecr.eu-north-1.on.aws', true], ['012345678901.dkr-ecr.eu-north-1.on.aws', true],
['012345678901.dkr.ecr.eusc-de-east-1.amazonaws.eu', true],
['public.ecr.aws', true], ['public.ecr.aws', true],
['ecr-public.aws.com', true] ['ecr-public.aws.com', true]
])('given registry %p', async (registry, expected) => { ])('given registry %p', async (registry, expected) => {
@@ -27,7 +26,6 @@ describe('isPubECR', () => {
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', false], ['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', false],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', false], ['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', false],
['012345678901.dkr-ecr.eu-north-1.on.aws', false], ['012345678901.dkr-ecr.eu-north-1.on.aws', false],
['012345678901.dkr.ecr.eusc-de-east-1.amazonaws.eu', false],
['public.ecr.aws', true], ['public.ecr.aws', true],
['ecr-public.aws.com', true] ['ecr-public.aws.com', true]
])('given registry %p', async (registry, expected) => { ])('given registry %p', async (registry, expected) => {
@@ -41,7 +39,6 @@ describe('getRegion', () => {
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', 'cn-north-1'], ['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', 'cn-north-1'],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', 'cn-northwest-1'], ['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', 'cn-northwest-1'],
['012345678901.dkr-ecr.eu-north-1.on.aws', 'eu-north-1'], ['012345678901.dkr-ecr.eu-north-1.on.aws', 'eu-north-1'],
['012345678901.dkr.ecr.eusc-de-east-1.amazonaws.eu', 'eusc-de-east-1'],
['public.ecr.aws', 'us-east-1'] ['public.ecr.aws', 'us-east-1']
])('given registry %p', async (registry, expected) => { ])('given registry %p', async (registry, expected) => {
expect(aws.getRegion(registry)).toEqual(expected); expect(aws.getRegion(registry)).toEqual(expected);
@@ -55,7 +52,6 @@ describe('getAccountIDs', () => {
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', '012345678901,012345678910,023456789012', ['012345678901', '012345678910', '023456789012']], ['012345678901.dkr.ecr.eu-west-3.amazonaws.com', '012345678901,012345678910,023456789012', ['012345678901', '012345678910', '023456789012']],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', '012345678910,023456789012', ['390948362332', '012345678910', '023456789012']], ['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', '012345678910,023456789012', ['390948362332', '012345678910', '023456789012']],
['876820548815.dkr-ecr.eu-north-1.on.aws', '012345678910,023456789012', ['876820548815', '012345678910', '023456789012']], ['876820548815.dkr-ecr.eu-north-1.on.aws', '012345678910,023456789012', ['876820548815', '012345678910', '023456789012']],
['012345678901.dkr.ecr.eusc-de-east-1.amazonaws.eu', '012345678910,023456789012', ['012345678901', '012345678910', '023456789012']],
['public.ecr.aws', undefined, []] ['public.ecr.aws', undefined, []]
])('given registry %p', async (registry, accountIDsEnv, expected) => { ])('given registry %p', async (registry, accountIDsEnv, expected) => {
if (accountIDsEnv) { if (accountIDsEnv) {

View File

@@ -50,7 +50,7 @@ test('logout calls exec', async () => {
const registry = 'https://ghcr.io'; const registry = 'https://ghcr.io';
await logout(registry, ''); await logout(registry);
expect(execSpy).toHaveBeenCalledTimes(1); expect(execSpy).toHaveBeenCalledTimes(1);
const callfunc = execSpy.mock.calls[0]; const callfunc = execSpy.mock.calls[0];

2
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ import {NodeHttpHandler} from '@smithy/node-http-handler';
import {HttpProxyAgent} from 'http-proxy-agent'; import {HttpProxyAgent} from 'http-proxy-agent';
import {HttpsProxyAgent} from 'https-proxy-agent'; import {HttpsProxyAgent} from 'https-proxy-agent';
const ecrRegistryRegex = /^(([0-9]{12})\.(dkr\.ecr|dkr-ecr)\.(.+)\.(on\.aws|amazonaws\.(com(.cn)?|eu)))(\/([^:]+)(:.+)?)?$/; const ecrRegistryRegex = /^(([0-9]{12})\.(dkr\.ecr|dkr-ecr)\.(.+)\.(on\.aws|amazonaws\.com(.cn)?))(\/([^:]+)(:.+)?)?$/;
const ecrPublicRegistryRegex = /public\.ecr\.aws|ecr-public\.aws\.com/; const ecrPublicRegistryRegex = /public\.ecr\.aws|ecr-public\.aws\.com/;
export const isECR = (registry: string): boolean => { export const isECR = (registry: string): boolean => {

View File

@@ -1,9 +1,4 @@
import path from 'path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as yaml from 'js-yaml';
import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx';
import {Util} from '@docker/actions-toolkit/lib/util';
export interface Inputs { export interface Inputs {
registry: string; registry: string;
@@ -15,15 +10,6 @@ export interface Inputs {
registryAuth: string; registryAuth: string;
} }
export interface Auth {
registry: string;
username: string;
password: string;
scope: string;
ecr: string;
configDir: string;
}
export function getInputs(): Inputs { export function getInputs(): Inputs {
return { return {
registry: core.getInput('registry'), registry: core.getInput('registry'),
@@ -35,56 +21,3 @@ export function getInputs(): Inputs {
registryAuth: core.getInput('registry-auth') registryAuth: core.getInput('registry-auth')
}; };
} }
export function getAuthList(inputs: Inputs): Array<Auth> {
if (inputs.registryAuth && (inputs.registry || inputs.username || inputs.password || inputs.scope || inputs.ecr)) {
throw new Error('Cannot use registry-auth with other inputs');
}
let auths: Array<Auth> = [];
if (!inputs.registryAuth) {
auths.push({
registry: inputs.registry || 'docker.io',
username: inputs.username,
password: inputs.password,
scope: inputs.scope,
ecr: inputs.ecr || 'auto',
configDir: scopeToConfigDir(inputs.registry, inputs.scope)
});
} else {
auths = (yaml.load(inputs.registryAuth) as Array<Auth>).map(auth => {
core.setSecret(auth.password); // redacted in workflow logs
return {
registry: auth.registry || 'docker.io',
username: auth.username,
password: auth.password,
scope: auth.scope,
ecr: auth.ecr || 'auto',
configDir: scopeToConfigDir(auth.registry || 'docker.io', auth.scope)
};
});
}
if (auths.length == 0) {
throw new Error('No registry to login');
}
return auths;
}
export function scopeToConfigDir(registry: string, scope?: string): string {
if (scopeDisabled() || !scope || scope === '') {
return '';
}
let configDir = path.join(Buildx.configDir, 'config', registry === 'docker.io' ? 'registry-1.docker.io' : registry);
if (scope.startsWith('@')) {
configDir += scope;
} else {
configDir = path.join(configDir, scope);
}
return configDir;
}
function scopeDisabled(): boolean {
if (process.env.DOCKER_LOGIN_SCOPE_DISABLED) {
return Util.parseBool(process.env.DOCKER_LOGIN_SCOPE_DISABLED);
}
return false;
}

View File

@@ -1,11 +1,19 @@
import path from 'path';
import * as aws from './aws';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as aws from './aws';
import * as context from './context';
import {Docker} from '@docker/actions-toolkit/lib/docker/docker'; import {Docker} from '@docker/actions-toolkit/lib/docker/docker';
import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx';
export async function login(auth: context.Auth): Promise<void> { export interface Auth {
registry: string;
username: string;
password: string;
scope?: string;
ecr: string;
}
export async function login(auth: Auth): Promise<void> {
if (/true/i.test(auth.ecr) || (auth.ecr == 'auto' && aws.isECR(auth.registry))) { if (/true/i.test(auth.ecr) || (auth.ecr == 'auto' && aws.isECR(auth.registry))) {
await loginECR(auth.registry, auth.username, auth.password, auth.scope); await loginECR(auth.registry, auth.username, auth.password, auth.scope);
} else { } else {
@@ -13,19 +21,9 @@ export async function login(auth: context.Auth): Promise<void> {
} }
} }
export async function logout(registry: string, configDir: string): Promise<void> { export async function logout(registry: string): Promise<void> {
let envs: {[key: string]: string} | undefined;
if (configDir !== '') {
envs = Object.assign({}, process.env, {
DOCKER_CONFIG: configDir
}) as {
[key: string]: string;
};
core.info(`Alternative config dir: ${configDir}`);
}
await Docker.getExecOutput(['logout', registry], { await Docker.getExecOutput(['logout', registry], {
ignoreReturnCode: true, ignoreReturnCode: true
env: envs
}).then(res => { }).then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) { if (res.stderr.length > 0 && res.exitCode != 0) {
core.warning(res.stderr.trim()); core.warning(res.stderr.trim());
@@ -56,14 +54,13 @@ export async function loginECR(registry: string, username: string, password: str
async function loginExec(registry: string, username: string, password: string, scope?: string): Promise<void> { async function loginExec(registry: string, username: string, password: string, scope?: string): Promise<void> {
let envs: {[key: string]: string} | undefined; let envs: {[key: string]: string} | undefined;
const configDir = context.scopeToConfigDir(registry, scope); if (scope && scope.length > 0) {
if (configDir !== '') {
envs = Object.assign({}, process.env, { envs = Object.assign({}, process.env, {
DOCKER_CONFIG: configDir DOCKER_CONFIG: path.join(Buildx.configDir, 'config', registry, scope)
}) as { }) as {
[key: string]: string; [key: string]: string;
}; };
core.info(`Logging into ${registry} (scope ${scope})...`); core.info(`Logging into ${registry} with scope ${scope}...`);
} else { } else {
core.info(`Logging into ${registry}...`); core.info(`Logging into ${registry}...`);
} }

View File

@@ -1,3 +1,4 @@
import * as yaml from 'js-yaml';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as actionsToolkit from '@docker/actions-toolkit'; import * as actionsToolkit from '@docker/actions-toolkit';
@@ -9,14 +10,35 @@ export async function main(): Promise<void> {
const inputs: context.Inputs = context.getInputs(); const inputs: context.Inputs = context.getInputs();
stateHelper.setLogout(inputs.logout); stateHelper.setLogout(inputs.logout);
const auths = context.getAuthList(inputs); if (inputs.registryAuth && (inputs.registry || inputs.username || inputs.password || inputs.scope || inputs.ecr)) {
stateHelper.setRegistries(Array.from(new Map(auths.map(auth => [`${auth.registry}|${auth.configDir}`, {registry: auth.registry, configDir: auth.configDir} as stateHelper.RegistryState])).values())); throw new Error('Cannot use registry-auth with other inputs');
}
if (auths.length === 1) { if (!inputs.registryAuth) {
await docker.login(auths[0]); stateHelper.setRegistries([inputs.registry || 'docker.io']);
await docker.login({
registry: inputs.registry || 'docker.io',
username: inputs.username,
password: inputs.password,
scope: inputs.scope,
ecr: inputs.ecr || 'auto'
});
return; return;
} }
const auths = (yaml.load(inputs.registryAuth) as docker.Auth[]).map(auth => ({
registry: auth.registry || 'docker.io',
username: auth.username,
password: auth.password,
scope: auth.scope,
ecr: auth.ecr || 'auto'
}));
if (auths.length == 0) {
throw new Error('No registry to login');
}
stateHelper.setRegistries(auths.map(auth => auth.registry).filter((value, index, self) => self.indexOf(value) === index));
for (const auth of auths) { for (const auth of auths) {
await core.group(`Login to ${auth.registry}`, async () => { await core.group(`Login to ${auth.registry}`, async () => {
await docker.login(auth); await docker.login(auth);
@@ -28,10 +50,8 @@ async function post(): Promise<void> {
if (!stateHelper.logout) { if (!stateHelper.logout) {
return; return;
} }
for (const registryState of stateHelper.registries) { for (const registry of stateHelper.registries.split(',')) {
await core.group(`Logout from ${registryState.registry}`, async () => { await docker.logout(registry);
await docker.logout(registryState.registry, registryState.configDir);
});
} }
} }

View File

@@ -1,15 +1,10 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
export const registries = process.env['STATE_registries'] ? (JSON.parse(process.env['STATE_registries']) as Array<RegistryState>) : []; export const registries = process.env['STATE_registries'] || '';
export const logout = /true/i.test(process.env['STATE_logout'] || ''); export const logout = /true/i.test(process.env['STATE_logout'] || '');
export interface RegistryState { export function setRegistries(registries: string[]) {
registry: string; core.saveState('registries', registries.join(','));
configDir: string;
}
export function setRegistries(registries: Array<RegistryState>) {
core.saveState('registries', JSON.stringify(registries));
} }
export function setLogout(logout: boolean) { export function setLogout(logout: boolean) {

View File

@@ -6357,9 +6357,9 @@ __metadata:
linkType: hard linkType: hard
"lodash@npm:^4.17.15": "lodash@npm:^4.17.15":
version: 4.17.23 version: 4.17.21
resolution: "lodash@npm:4.17.23" resolution: "lodash@npm:4.17.21"
checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532
languageName: node languageName: node
linkType: hard linkType: hard