Add Chainguard registry (cgr.dev) login support

Implement native OIDC-based authentication for Chainguard's container
registry, following the same pattern as the existing AWS ECR integration.

When registry is set to cgr.dev, the action automatically exchanges a
GitHub Actions OIDC token with Chainguard's STS endpoint for a short-lived
registry credential, removing the need for chainctl or long-lived pull tokens.

New inputs: chainguard (auto/true/false), chainguard-identity.

Signed-off-by: Augustus Nguyen <theflash28012002@gmail.com>
This commit is contained in:
Augustus Nguyen 2026-04-21 11:11:53 +07:00
parent 4a8376e001
commit 4bcfaae325
9 changed files with 11038 additions and 5741 deletions

41
src/chainguard.ts Normal file
View file

@ -0,0 +1,41 @@
import * as core from '@actions/core';
import * as http from '@actions/http-client';
const chainguardRegistryRegex = /^cgr\.dev$/;
const DEFAULT_ISSUER = 'https://issuer.enforce.dev';
const DEFAULT_AUDIENCE = 'cgr.dev';
export const isChainguard = (registry: string): boolean => {
return chainguardRegistryRegex.test(registry);
};
export interface ChainguardTokenResponse {
token: string;
}
export const getRegistryToken = async (identity: string, issuerURL?: string): Promise<{username: string; password: string}> => {
const issuer = issuerURL || DEFAULT_ISSUER;
core.info('Requesting GitHub Actions OIDC token...');
const oidcToken = await core.getIDToken(DEFAULT_AUDIENCE);
core.info(`Exchanging OIDC token with Chainguard (${issuer})...`);
const client = new http.HttpClient('docker-login-action');
const url = `${issuer}/sts/exchange?aud=${encodeURIComponent(DEFAULT_AUDIENCE)}&identity=${encodeURIComponent(identity)}`;
const response = await client.getJson<ChainguardTokenResponse>(url, {
Authorization: `Bearer ${oidcToken}`
});
if (response.statusCode !== 200 || !response.result?.token) {
throw new Error(`Failed to exchange OIDC token with Chainguard (HTTP ${response.statusCode})`);
}
const token = response.result.token;
core.setSecret(token);
return {
username: 'user',
password: token
};
};

View file

@ -11,6 +11,8 @@ export interface Inputs {
password: string;
scope: string;
ecr: string;
chainguard: string;
chainguardIdentity: string;
logout: boolean;
registryAuth: string;
}
@ -21,6 +23,8 @@ export interface Auth {
password: string;
scope: string;
ecr: string;
chainguard: string;
chainguardIdentity: string;
configDir: string;
}
@ -31,13 +35,15 @@ export function getInputs(): Inputs {
password: core.getInput('password'),
scope: core.getInput('scope'),
ecr: core.getInput('ecr'),
chainguard: core.getInput('chainguard'),
chainguardIdentity: core.getInput('chainguard-identity'),
logout: core.getBooleanInput('logout'),
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)) {
if (inputs.registryAuth && (inputs.registry || inputs.username || inputs.password || inputs.scope || inputs.ecr || inputs.chainguard || inputs.chainguardIdentity)) {
throw new Error('Cannot use registry-auth with other inputs');
}
let auths: Array<Auth> = [];
@ -49,11 +55,15 @@ export function getAuthList(inputs: Inputs): Array<Auth> {
password: inputs.password,
scope: inputs.scope,
ecr: inputs.ecr || 'auto',
chainguard: inputs.chainguard || 'auto',
chainguardIdentity: inputs.chainguardIdentity,
configDir: scopeToConfigDir(registry, inputs.scope)
});
} else {
auths = (yaml.load(inputs.registryAuth) as Array<Auth>).map(auth => {
core.setSecret(auth.password); // redacted in workflow logs
if (auth.password) {
core.setSecret(auth.password);
}
const registry = auth.registry || 'docker.io';
return {
registry,
@ -61,6 +71,8 @@ export function getAuthList(inputs: Inputs): Array<Auth> {
password: auth.password,
scope: auth.scope,
ecr: auth.ecr || 'auto',
chainguard: auth.chainguard || 'auto',
chainguardIdentity: auth.chainguardIdentity,
configDir: scopeToConfigDir(registry, auth.scope)
};
});

View file

@ -3,11 +3,14 @@ import * as core from '@actions/core';
import {Docker} from '@docker/actions-toolkit/lib/docker/docker.js';
import * as aws from './aws.js';
import * as chainguard from './chainguard.js';
import * as context from './context.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 if (/true/i.test(auth.chainguard) || (auth.chainguard == 'auto' && chainguard.isChainguard(auth.registry))) {
await loginChainguard(auth.registry, auth.chainguardIdentity, auth.scope);
} else {
await loginStandard(auth.registry, auth.username, auth.password, auth.scope);
}
@ -54,6 +57,15 @@ export async function loginECR(registry: string, username: string, password: str
}
}
export async function loginChainguard(registry: string, identity: string, scope?: string): Promise<void> {
if (!identity) {
throw new Error('Chainguard identity is required for Chainguard registry login. Set the chainguard-identity input.');
}
core.info(`Retrieving Chainguard registry token via OIDC exchange...`);
const creds = await chainguard.getRegistryToken(identity);
await loginExec(registry, creds.username, creds.password, scope);
}
async function loginExec(registry: string, username: string, password: string, scope?: string): Promise<void> {
let envs: {[key: string]: string} | undefined;
const configDir = context.scopeToConfigDir(registry, scope);