mirror of
https://github.com/docker/login-action.git
synced 2026-07-22 11:27:58 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e3d36ea10 | |||
| 14d6a7934e | |||
| 03c851098f | |||
| ad8a81f098 | |||
| 6d219a4928 | |||
| b3200694f4 | |||
| 08d3680aa8 | |||
| 381f5a4f5e | |||
| 4bc69ce4fd | |||
| ddf94996dc | |||
| d870eb57d3 | |||
| 4d7b1348c8 | |||
| 608836776e | |||
| 3bab31f360 | |||
| dc654b7be1 | |||
| c66a8fcb24 | |||
| 6d7f9d458a | |||
| 13169716da | |||
| c1aa9e5f45 | |||
| 026f3975fd | |||
| 3dbb99fd00 |
@@ -10,6 +10,9 @@ updates:
|
||||
crazy-max-dot-github:
|
||||
patterns:
|
||||
- "crazy-max/.github/*"
|
||||
codeql-actions:
|
||||
patterns:
|
||||
- "github/codeql-action/*"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "bot"
|
||||
|
||||
+50
-27
@@ -25,7 +25,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Stop docker
|
||||
run: |
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to ACR
|
||||
uses: ./
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: ./
|
||||
@@ -150,6 +150,29 @@ jobs:
|
||||
username: ${{ vars.DOCKERPUBLICBOT_USERNAME }}
|
||||
password: ${{ secrets.DOCKERPUBLICBOT_READ_PAT }}
|
||||
|
||||
dockerhub-oidc:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Docker Hub with OIDC
|
||||
uses: ./
|
||||
env:
|
||||
DOCKERHUB_OIDC_CONNECTIONID: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_OIDC_USERNAME }}
|
||||
|
||||
ecr:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -161,7 +184,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to ECR
|
||||
uses: ./
|
||||
@@ -181,10 +204,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -209,10 +232,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::175142243308:role/official_gha_cicd_login_action
|
||||
aws-region: us-east-1
|
||||
@@ -233,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Public ECR
|
||||
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
||||
@@ -256,10 +279,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -285,10 +308,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
||||
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::175142243308:role/official_gha_cicd_login_action
|
||||
aws-region: us-east-1
|
||||
@@ -310,7 +333,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
@@ -330,7 +353,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitLab
|
||||
uses: ./
|
||||
@@ -350,7 +373,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Google Artifact Registry
|
||||
uses: ./
|
||||
@@ -370,7 +393,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Google Container Registry
|
||||
uses: ./
|
||||
@@ -384,7 +407,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to registries
|
||||
uses: ./
|
||||
@@ -407,7 +430,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to registries
|
||||
uses: ./
|
||||
@@ -428,7 +451,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to registries
|
||||
id: login
|
||||
@@ -460,7 +483,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: ./
|
||||
@@ -490,7 +513,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: ./
|
||||
@@ -520,7 +543,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
@@ -551,7 +574,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: ./
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Enable corepack
|
||||
run: |
|
||||
@@ -30,17 +30,17 @@ jobs:
|
||||
yarn --version
|
||||
-
|
||||
name: Set up Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Publish
|
||||
uses: actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978 # v0.0.4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
permission-contents: write
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
-
|
||||
name: Generate matrix
|
||||
id: generate
|
||||
|
||||
@@ -28,6 +28,7 @@ ___
|
||||
* [Set scopes for the authentication token](#set-scopes-for-the-authentication-token)
|
||||
* [Customizing](#customizing)
|
||||
* [inputs](#inputs)
|
||||
* [environment variables](#environment-variables)
|
||||
* [Contributing](#contributing)
|
||||
|
||||
## Usage
|
||||
@@ -57,6 +58,36 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
```
|
||||
|
||||
You can also [authenticate to Docker Hub with OpenID Connect](https://docs.docker.com/enterprise/security/oidc-connections/)
|
||||
when your Docker Hub organization has an OIDC connection configured. The
|
||||
workflow must grant the `id-token: write` permission, pass the Docker Hub
|
||||
organization name as `username`, omit `password`, and set the OIDC connection
|
||||
ID in `DOCKERHUB_OIDC_CONNECTIONID` environment variable.
|
||||
|
||||
```yaml
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
login:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
env:
|
||||
DOCKERHUB_OIDC_CONNECTIONID: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_ORGANIZATION }}
|
||||
```
|
||||
|
||||
### GitHub Container Registry
|
||||
|
||||
To authenticate to the [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry),
|
||||
@@ -690,6 +721,15 @@ The following inputs can be used as `step.with` keys:
|
||||
> [!NOTE]
|
||||
> The `registry-auth` input cannot be used with other inputs except `logout`.
|
||||
|
||||
### environment variables
|
||||
|
||||
The following environment variables can be set as `step.env` keys:
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|-------------------------------|--------|---------|-----------------------------------------------------------------------------|
|
||||
| `DOCKERHUB_OIDC_CONNECTIONID` | String | | Docker Hub OIDC connection ID. Required for Docker Hub OIDC login |
|
||||
| `DOCKERHUB_OIDC_EXPIREIN` | Number | `300` | Docker Hub OIDC token lifetime in seconds. Must be between `300` and `3600` |
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to contribute? Awesome! You can find information about contributing to
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as httpm from '@actions/http-client';
|
||||
import {beforeEach, describe, expect, test, vi} from 'vitest';
|
||||
|
||||
import * as dockerhub from '../src/dockerhub.js';
|
||||
|
||||
vi.mock('@actions/core', () => ({
|
||||
getIDToken: vi.fn(),
|
||||
info: vi.fn(),
|
||||
setSecret: vi.fn()
|
||||
}));
|
||||
|
||||
const validConnectionID = '123e4567-e89b-42d3-a456-426614174000';
|
||||
|
||||
const httpResponse = (statusCode: number, body: string, headers: Record<string, string> = {}): httpm.HttpClientResponse => {
|
||||
return {
|
||||
message: {
|
||||
statusCode,
|
||||
headers
|
||||
},
|
||||
readBody: vi.fn(async () => body)
|
||||
} as unknown as httpm.HttpClientResponse;
|
||||
};
|
||||
|
||||
describe('isDockerHubOIDC', () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.DOCKERHUB_OIDC_CONNECTIONID;
|
||||
});
|
||||
|
||||
test.each(['', 'docker.io', 'registry-1.docker.io', 'registry-1-stage.docker.io'])('detects Docker Hub registry %p with empty password', registry => {
|
||||
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
||||
expect(dockerhub.isDockerHubOIDC(registry, '')).toBe(true);
|
||||
});
|
||||
|
||||
test('requires connection ID env var', () => {
|
||||
expect(dockerhub.isDockerHubOIDC('docker.io', '')).toBe(false);
|
||||
});
|
||||
|
||||
test('requires empty password', () => {
|
||||
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
||||
expect(dockerhub.isDockerHubOIDC('docker.io', 'groundcontrol')).toBe(false);
|
||||
});
|
||||
|
||||
test('ignores non-Docker Hub registries', () => {
|
||||
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
||||
expect(dockerhub.isDockerHubOIDC('ghcr.io', '')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOIDCToken', () => {
|
||||
const getIDTokenMock = vi.mocked(core.getIDToken);
|
||||
const setSecretMock = vi.mocked(core.setSecret);
|
||||
let postSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.DOCKERHUB_OIDC_CONNECTIONID = validConnectionID;
|
||||
delete process.env.DOCKERHUB_OIDC_EXPIREIN;
|
||||
getIDTokenMock.mockResolvedValue('github-id-token');
|
||||
postSpy = vi.spyOn(httpm.HttpClient.prototype, 'post').mockResolvedValue(httpResponse(200, JSON.stringify({access_token: 'hub-token'})));
|
||||
});
|
||||
|
||||
test('exchanges GitHub OIDC token for Docker Hub token', async () => {
|
||||
const credentials = await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
||||
|
||||
expect(credentials).toEqual({
|
||||
username: 'dbowie',
|
||||
token: 'hub-token'
|
||||
});
|
||||
expect(getIDTokenMock).toHaveBeenCalledWith('https://identity.docker.com');
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postSpy.mock.calls[0][0]).toBe('https://identity.docker.com/oauth/token');
|
||||
|
||||
const http = postSpy.mock.contexts[0] as httpm.HttpClient;
|
||||
expect(http.userAgent).toBe('github.com/docker/login-action');
|
||||
expect(http.requestOptions?.headers).toEqual({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
|
||||
const body = new URLSearchParams(postSpy.mock.calls[0][1]);
|
||||
expect(body.get('grant_type')).toBe('urn:ietf:params:oauth:grant-type:token-exchange');
|
||||
expect(body.get('subject_token_type')).toBe('urn:ietf:params:oauth:token-type:id_token');
|
||||
expect(body.get('subject_token')).toBe('github-id-token');
|
||||
expect(body.get('connection_id')).toBe(validConnectionID);
|
||||
expect(body.get('expires_in')).toBe('300');
|
||||
expect(setSecretMock).toHaveBeenCalledWith('hub-token');
|
||||
});
|
||||
|
||||
test('uses custom token expiration', async () => {
|
||||
process.env.DOCKERHUB_OIDC_EXPIREIN = '900';
|
||||
await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
||||
const body = new URLSearchParams(postSpy.mock.calls[0][1]);
|
||||
expect(body.get('expires_in')).toBe('900');
|
||||
});
|
||||
|
||||
test('uses stage identity host for stage registry', async () => {
|
||||
await dockerhub.getOIDCToken('registry-1-stage.docker.io', 'dbowie');
|
||||
expect(getIDTokenMock).toHaveBeenCalledWith('https://identity-stage.docker.com');
|
||||
expect(postSpy.mock.calls[0][0]).toBe('https://identity-stage.docker.com/oauth/token');
|
||||
});
|
||||
|
||||
test('requires connection ID env var', async () => {
|
||||
delete process.env.DOCKERHUB_OIDC_CONNECTIONID;
|
||||
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('DOCKERHUB_OIDC_CONNECTIONID is required for Docker Hub OIDC login');
|
||||
expect(getIDTokenMock).not.toHaveBeenCalled();
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('validates connection ID', async () => {
|
||||
process.env.DOCKERHUB_OIDC_CONNECTIONID = 'not-a-uuid';
|
||||
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('Invalid DOCKERHUB_OIDC_CONNECTIONID. Must be a valid UUID.');
|
||||
expect(getIDTokenMock).not.toHaveBeenCalled();
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each(['not-a-number', '299', '3601'])('validates token expiration %p', async expiresIn => {
|
||||
process.env.DOCKERHUB_OIDC_EXPIREIN = expiresIn;
|
||||
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow(`Invalid DOCKERHUB_OIDC_EXPIREIN: ${expiresIn}. Must be between 300 and 3600`);
|
||||
expect(getIDTokenMock).not.toHaveBeenCalled();
|
||||
expect(postSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('retries rate limited token requests with Retry-After', async () => {
|
||||
postSpy.mockResolvedValueOnce(httpResponse(429, '', {'retry-after': '0'})).mockResolvedValueOnce(httpResponse(200, JSON.stringify({access_token: 'hub-token'})));
|
||||
await dockerhub.getOIDCToken('docker.io', 'dbowie');
|
||||
expect(postSpy).toHaveBeenCalledTimes(2);
|
||||
expect(core.info).toHaveBeenCalledWith('Docker Hub OIDC token request rate limited, retrying in 0ms (attempt 1/5)');
|
||||
});
|
||||
|
||||
test('throws Docker Hub API errors', async () => {
|
||||
postSpy.mockResolvedValue(httpResponse(400, JSON.stringify({description: 'bad connection'})));
|
||||
await expect(dockerhub.getOIDCToken('docker.io', 'dbowie')).rejects.toThrow('Docker Hub API: bad status code 400: bad connection');
|
||||
});
|
||||
});
|
||||
+118
-114
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+22
-4
@@ -399,7 +399,7 @@ Apache License
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- @docker/actions-toolkit@0.92.0
|
||||
- @docker/actions-toolkit@0.93.0
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -2398,7 +2398,7 @@ SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- js-yaml@5.2.0
|
||||
- js-yaml@5.2.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -4590,7 +4590,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
The following npm packages may be included in this product:
|
||||
|
||||
- brace-expansion@1.1.13
|
||||
- brace-expansion@1.1.16
|
||||
- brace-expansion@2.0.3
|
||||
|
||||
These packages each contain the following license:
|
||||
@@ -5562,7 +5562,7 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- csv-parse@7.0.0
|
||||
- csv-parse@7.0.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
@@ -5590,6 +5590,24 @@ SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- uuid@14.0.1
|
||||
|
||||
This package contains the following license:
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
-----------
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- tunnel@0.0.6
|
||||
|
||||
+4
-2
@@ -24,12 +24,14 @@
|
||||
"packageManager": "yarn@4.15.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/http-client": "^4.0.1",
|
||||
"@aws-sdk/client-ecr": "^3.1077.0",
|
||||
"@aws-sdk/client-ecr-public": "^3.1077.0",
|
||||
"@docker/actions-toolkit": "^0.92.0",
|
||||
"@docker/actions-toolkit": "^0.93.0",
|
||||
"http-proxy-agent": "^9.1.0",
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"js-yaml": "^5.2.0"
|
||||
"js-yaml": "^5.2.1",
|
||||
"uuid": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.3",
|
||||
|
||||
+9
-1
@@ -4,12 +4,20 @@ import {Docker} from '@docker/actions-toolkit/lib/docker/docker.js';
|
||||
|
||||
import * as aws from './aws.js';
|
||||
import * as context from './context.js';
|
||||
import * as dockerhub from './dockerhub.js';
|
||||
|
||||
export async function login(auth: context.Auth): Promise<void> {
|
||||
if (/true/i.test(auth.ecr) || (auth.ecr == 'auto' && aws.isECR(auth.registry))) {
|
||||
await loginECR(auth.registry, auth.username, auth.password, auth.scope);
|
||||
} else {
|
||||
await loginStandard(auth.registry, auth.username, auth.password, auth.scope);
|
||||
let username = auth.username;
|
||||
let password = auth.password;
|
||||
if (dockerhub.isDockerHubOIDC(auth.registry, password)) {
|
||||
const credentials = await dockerhub.getOIDCToken(auth.registry, username);
|
||||
username = credentials.username;
|
||||
password = credentials.token;
|
||||
}
|
||||
await loginStandard(auth.registry, username, password, auth.scope);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as httpm from '@actions/http-client';
|
||||
import {HttpCodes} from '@actions/http-client';
|
||||
import {validate as uuidValidate} from 'uuid';
|
||||
|
||||
export interface LoginCredentials {
|
||||
username: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface OIDCTokenResponse {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
const defaultExpiresIn = 300;
|
||||
const minExpiresIn = 300;
|
||||
const maxExpiresIn = 3600;
|
||||
const maxRetries = 5;
|
||||
|
||||
export const isDockerHubOIDC = (registry: string, password: string): boolean => {
|
||||
return process.env.DOCKERHUB_OIDC_CONNECTIONID !== undefined && !password && isDockerHubRegistry(registry);
|
||||
};
|
||||
|
||||
const isDockerHubRegistry = (registry: string): boolean => {
|
||||
return registry === '' || registry === 'docker.io' || registry === 'registry-1.docker.io' || registry === 'registry-1-stage.docker.io';
|
||||
};
|
||||
|
||||
export const getOIDCToken = async (registry: string, username: string): Promise<LoginCredentials> => {
|
||||
const connectionID = process.env.DOCKERHUB_OIDC_CONNECTIONID?.trim();
|
||||
if (!connectionID) {
|
||||
throw new Error('DOCKERHUB_OIDC_CONNECTIONID is required for Docker Hub OIDC login');
|
||||
}
|
||||
|
||||
if (!uuidValidate(connectionID)) {
|
||||
throw new Error('Invalid DOCKERHUB_OIDC_CONNECTIONID. Must be a valid UUID.');
|
||||
}
|
||||
|
||||
const expiresIn = getExpiresIn();
|
||||
const identityHost = registry === 'registry-1-stage.docker.io' ? 'identity-stage.docker.com' : 'identity.docker.com';
|
||||
const audience = `https://${identityHost}`;
|
||||
const idToken = await core.getIDToken(audience);
|
||||
const http: httpm.HttpClient = new httpm.HttpClient('github.com/docker/login-action', [], {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
const data = new URLSearchParams();
|
||||
data.set('grant_type', 'urn:ietf:params:oauth:grant-type:token-exchange');
|
||||
data.set('subject_token_type', 'urn:ietf:params:oauth:token-type:id_token');
|
||||
data.set('subject_token', idToken);
|
||||
data.set('connection_id', connectionID);
|
||||
data.set('expires_in', expiresIn.toString());
|
||||
|
||||
const resp = await postWithRetry(http, `https://${identityHost}/oauth/token`, data.toString());
|
||||
|
||||
const tokenResp = <OIDCTokenResponse>JSON.parse(await handleResponse(resp));
|
||||
core.setSecret(tokenResp.access_token);
|
||||
|
||||
return {
|
||||
username,
|
||||
token: tokenResp.access_token
|
||||
};
|
||||
};
|
||||
|
||||
const getExpiresIn = (): number => {
|
||||
const expiresInInput = process.env.DOCKERHUB_OIDC_EXPIREIN?.trim() || defaultExpiresIn.toString();
|
||||
const expiresIn = Number(expiresInInput);
|
||||
if (isNaN(expiresIn) || expiresIn < minExpiresIn || expiresIn > maxExpiresIn) {
|
||||
throw new Error(`Invalid DOCKERHUB_OIDC_EXPIREIN: ${expiresInInput}. Must be between ${minExpiresIn} and ${maxExpiresIn}`);
|
||||
}
|
||||
return expiresIn;
|
||||
};
|
||||
|
||||
const postWithRetry = async (http: httpm.HttpClient, url: string, data: string): Promise<httpm.HttpClientResponse> => {
|
||||
let resp = await http.post(url, data);
|
||||
for (let attempt = 0; (resp.message.statusCode || HttpCodes.InternalServerError) === HttpCodes.TooManyRequests && attempt < maxRetries; attempt++) {
|
||||
const delay = parseRetryAfter(resp.message.headers['retry-after']);
|
||||
if (delay === null) {
|
||||
break;
|
||||
}
|
||||
await resp.readBody();
|
||||
core.info(`Docker Hub OIDC token request rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
resp = await http.post(url, data);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
const parseRetryAfter = (value: string | string[] | undefined): number | null => {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
const seconds = Number(value);
|
||||
if (isNaN(seconds)) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(0, seconds * 1000);
|
||||
};
|
||||
|
||||
const handleResponse = async (resp: httpm.HttpClientResponse): Promise<string> => {
|
||||
const body = await resp.readBody();
|
||||
const statusCode = resp.message.statusCode || HttpCodes.InternalServerError;
|
||||
if (statusCode < HttpCodes.OK || statusCode >= HttpCodes.MultipleChoices) {
|
||||
throw parseError(statusCode, body);
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const parseError = (statusCode: number, body: string): Error => {
|
||||
if (statusCode === 401) {
|
||||
throw new Error(`Docker Hub API: operation not permitted`);
|
||||
}
|
||||
if (body) {
|
||||
const errResp = parseErrorBody(body);
|
||||
for (const k of ['description', 'message', 'detail', 'error']) {
|
||||
if (errResp[k]) {
|
||||
throw new Error(`Docker Hub API: bad status code ${statusCode}: ${errResp[k]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Docker Hub API: bad status code ${statusCode}`);
|
||||
};
|
||||
|
||||
const parseErrorBody = (body: string): Record<string, string> => {
|
||||
try {
|
||||
return <Record<string, string>>JSON.parse(body);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
@@ -677,9 +677,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@docker/actions-toolkit@npm:^0.92.0":
|
||||
version: 0.92.0
|
||||
resolution: "@docker/actions-toolkit@npm:0.92.0"
|
||||
"@docker/actions-toolkit@npm:^0.93.0":
|
||||
version: 0.93.0
|
||||
resolution: "@docker/actions-toolkit@npm:0.93.0"
|
||||
dependencies:
|
||||
"@actions/artifact": "npm:^6.2.1"
|
||||
"@actions/cache": "npm:^6.1.0"
|
||||
@@ -693,16 +693,16 @@ __metadata:
|
||||
"@sigstore/tuf": "npm:^5.0.0"
|
||||
"@sigstore/verify": "npm:^4.1.0"
|
||||
async-retry: "npm:^1.3.3"
|
||||
csv-parse: "npm:^7.0.0"
|
||||
csv-parse: "npm:^7.0.1"
|
||||
gunzip-maybe: "npm:^1.4.2"
|
||||
handlebars: "npm:^4.7.9"
|
||||
he: "npm:^1.2.0"
|
||||
js-yaml: "npm:^5.2.0"
|
||||
js-yaml: "npm:^5.2.1"
|
||||
jwt-decode: "npm:^4.0.0"
|
||||
semver: "npm:^7.8.5"
|
||||
tar-stream: "npm:^3.2.0"
|
||||
tmp: "npm:^0.2.7"
|
||||
checksum: 10/599cea84b897069c53744a2f05371c6d9ad60da18448b4431273529b92fd2d77ddc718ee205cf0bf1da53083d412da90b1067f1e64a5fdac73fe71d486726064
|
||||
checksum: 10/31643db575faf865666247661007d6e5fc8bc2b737001a522879829fcbeeab7b11b886d2cc26d074a26814361ec8427484d7b94ad9155d52c41398a7dc27000e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2762,12 +2762,12 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.13
|
||||
resolution: "brace-expansion@npm:1.1.13"
|
||||
version: 1.1.16
|
||||
resolution: "brace-expansion@npm:1.1.16"
|
||||
dependencies:
|
||||
balanced-match: "npm:^1.0.0"
|
||||
concat-map: "npm:0.0.1"
|
||||
checksum: 10/b5f4329fdbe9d2e25fa250c8f866ebd054ba946179426e99b86dcccddabdb1d481f0e40ee5430032e62a7d0a6c2837605ace6783d015aa1d65d85ca72154d936
|
||||
checksum: 10/94498bead66c51536df5b7bf1b0a0e581a5b7f86888be10481d06920c4bd9d976e003b13a3d19fddc733f21a09d162ff72f90e18b2c9d062b15121e973d0e13b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3103,10 +3103,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csv-parse@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "csv-parse@npm:7.0.0"
|
||||
checksum: 10/53c96e6b4ff80047713bb4d2967d06495890d4b628284a80271860be089fdb5a74cd97c76fd535a00ad26b11cc6e4fc5a243658e5377c0a6334ddd104620d169
|
||||
"csv-parse@npm:^7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "csv-parse@npm:7.0.1"
|
||||
checksum: 10/5c914f01181dbb381068b98e17b49361d853faa95db3e8e82bc96b6a0de5313ecc8325a77db35ff66644d7747099965ba3167ec21bd9ce4500edf21b5bdd49bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3155,9 +3155,10 @@ __metadata:
|
||||
resolution: "docker-login@workspace:."
|
||||
dependencies:
|
||||
"@actions/core": "npm:^3.0.1"
|
||||
"@actions/http-client": "npm:^4.0.1"
|
||||
"@aws-sdk/client-ecr": "npm:^3.1077.0"
|
||||
"@aws-sdk/client-ecr-public": "npm:^3.1077.0"
|
||||
"@docker/actions-toolkit": "npm:^0.92.0"
|
||||
"@docker/actions-toolkit": "npm:^0.93.0"
|
||||
"@eslint/js": "npm:^9.39.3"
|
||||
"@types/js-yaml": "npm:^4.0.9"
|
||||
"@types/node": "npm:^24.11.0"
|
||||
@@ -3173,9 +3174,10 @@ __metadata:
|
||||
globals: "npm:^17.3.0"
|
||||
http-proxy-agent: "npm:^9.1.0"
|
||||
https-proxy-agent: "npm:^9.1.0"
|
||||
js-yaml: "npm:^5.2.0"
|
||||
js-yaml: "npm:^5.2.1"
|
||||
prettier: "npm:^3.8.1"
|
||||
typescript: "npm:^5.9.3"
|
||||
uuid: "npm:^14.0.1"
|
||||
vitest: "npm:^4.0.18"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -4344,14 +4346,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "js-yaml@npm:5.2.0"
|
||||
"js-yaml@npm:^5.2.1":
|
||||
version: 5.2.1
|
||||
resolution: "js-yaml@npm:5.2.1"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.mjs
|
||||
checksum: 10/8a5e55c5d0fcafae4ac02114a99dc070048b8e5a82a056089ce1f69f8a00fd8eb05b622e76ad50aac1f9d409010636c9616c6b2ed4e58dae138379a60d301220
|
||||
checksum: 10/e1eca2d21c15572585bb236d9fde31d6789eb50b9c63e8753fa7e0777bc480f7521cad517bd7a0c66f27dfc27ddcd7100beeefa51c1a50e10e98f2e009633c3d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6300,6 +6302,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^14.0.1":
|
||||
version: 14.0.1
|
||||
resolution: "uuid@npm:14.0.1"
|
||||
bin:
|
||||
uuid: dist-node/bin/uuid
|
||||
checksum: 10/0f978fd5b0269d7acb615342aeb131f0b8d85eeb8ec34f2915d906baa3befcc14a079a430d0f8116aec6fd20c850cbfab65d1b3d1643fa81ed373c0cf9574f18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"validate-npm-package-name@npm:^7.0.0":
|
||||
version: 7.0.2
|
||||
resolution: "validate-npm-package-name@npm:7.0.2"
|
||||
|
||||
Reference in New Issue
Block a user