diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73264f5..0a8a04d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,27 @@ jobs: with: username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + registry-auth-oidc: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - + name: Login to registries + uses: ./ + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + with: + registry-auth: | + - username: ${{ vars.DOCKERHUB_OIDC_USERNAME }} + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + ecr: runs-on: ${{ matrix.os }} strategy: diff --git a/README.md b/README.md index f9c4580..af5c833 100644 --- a/README.md +++ b/README.md @@ -648,6 +648,38 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} ``` +Docker Hub OIDC can also be used with `registry-auth`. Grant `id-token: write`, +set `DOCKERHUB_OIDC_CONNECTIONID`, pass the Docker Hub organization name as +`username`, and omit `password` for the Docker Hub object: + +```yaml +name: ci + +on: + push: + branches: main + +permissions: + contents: read + id-token: write + +jobs: + login: + runs-on: ubuntu-latest + steps: + - + name: Login to registries + uses: docker/login-action@v4 + env: + DOCKERHUB_OIDC_CONNECTIONID: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} + with: + registry-auth: | + - username: ${{ vars.DOCKERHUB_ORGANIZATION }} + - registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} +``` + ### Set scopes for the authentication token The `scope` input allows limiting registry credentials to a specific repository diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 21ac148..25a5f51 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -54,6 +54,25 @@ test('getAuthList skips secret masking when registry-auth password is absent', a }); }); +test('getAuthList supports password-less Docker Hub registry-auth for OIDC', async () => { + const [auth] = getAuthList({ + registry: '', + username: '', + password: '', + scope: '', + ecr: '', + logout: true, + registryAuth: '- username: docker-org\n' + }); + + expect(auth).toMatchObject({ + registry: 'docker.io', + username: 'docker-org', + password: undefined, + ecr: 'auto' + }); +}); + test('getAuthList masks registry-auth password when present', async () => { const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); getAuthList({ diff --git a/__tests__/docker.test.ts b/__tests__/docker.test.ts index e4d65a7..4ad2d90 100644 --- a/__tests__/docker.test.ts +++ b/__tests__/docker.test.ts @@ -1,8 +1,14 @@ -import {expect, test, vi} from 'vitest'; +import {afterEach, expect, test, vi} from 'vitest'; import {Docker} from '@docker/actions-toolkit/lib/docker/docker.js'; -import {loginStandard, logout} from '../src/docker.js'; +import {login, loginStandard, logout} from '../src/docker.js'; +import * as dockerhub from '../src/dockerhub.js'; + +afterEach(() => { + vi.restoreAllMocks(); + delete process.env.DOCKERHUB_OIDC_CONNECTIONID; +}); test('loginStandard calls exec', async () => { const execSpy = vi.spyOn(Docker, 'getExecOutput').mockImplementation(async () => { @@ -32,6 +38,37 @@ test('loginStandard calls exec', async () => { }); }); +test('login exchanges Docker Hub OIDC token for password-less auth', async () => { + process.env.DOCKERHUB_OIDC_CONNECTIONID = '123e4567-e89b-42d3-a456-426614174000'; + const execSpy = vi.spyOn(Docker, 'getExecOutput').mockImplementation(async () => { + return { + exitCode: 0, + stdout: '', + stderr: '' + }; + }); + const oidcSpy = vi.spyOn(dockerhub, 'getOIDCToken').mockResolvedValue({ + username: 'docker-org', + token: 'hub-token' + }); + + await login({ + registry: 'docker.io', + username: 'docker-org', + password: '', + scope: '', + ecr: 'auto', + configDir: '' + }); + + expect(oidcSpy).toHaveBeenCalledWith('docker.io', 'docker-org'); + expect(execSpy).toHaveBeenCalledWith(['login', '--password-stdin', '--username', 'docker-org', 'docker.io'], { + input: Buffer.from('hub-token'), + silent: true, + ignoreReturnCode: true + }); +}); + test('logout calls exec', async () => { const execSpy = vi.spyOn(Docker, 'getExecOutput').mockImplementation(async () => { return {