Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/.test-bake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ jobs:
const builderOutputs = JSON.parse(core.getInput('builder-outputs'));
core.info(JSON.stringify(builderOutputs, null, 2));

bake-secret:
uses: ./.github/workflows/bake.yml
permissions:
contents: read
id-token: write
with:
artifact-upload: false
context: test
output: local
target: secret
secrets:
build-secrets: |
fixture_plain: |
alpha-line
beta-line
fixture_json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }}

bake-set-runner:
uses: ./.github/workflows/bake.yml
permissions:
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/.test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,22 @@ jobs:
const builderOutputs = JSON.parse(core.getInput('builder-outputs'));
core.info(JSON.stringify(builderOutputs, null, 2));

build-secret:
uses: ./.github/workflows/build.yml
permissions:
contents: read
id-token: write
with:
artifact-upload: false
file: test/secret.Dockerfile
output: local
secrets:
build-secrets: |
fixture_plain: |
alpha-line
beta-line
fixture_json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }}

build-set-runner:
uses: ./.github/workflows/build.yml
permissions:
Expand Down
67 changes: 64 additions & 3 deletions .github/workflows/bake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ on:
registry-auths:
description: "Raw authentication to registries, defined as YAML objects (for image output)"
required: false
build-secrets:
description: "YAML object mapping BuildKit secret IDs to secret values"
required: false
github-token:
description: "GitHub Token used to authenticate against the repository for Git context"
required: false
Expand Down Expand Up @@ -455,7 +458,7 @@ jobs:
}
);
await core.group(`Set envs`, async () => {
core.info(JSON.stringify(envs, null, 2));
core.info(JSON.stringify(Object.keys(envs).sort(), null, 2));
});

const metaImages = inpMetaImages.map(image => image.toLowerCase());
Expand Down Expand Up @@ -797,6 +800,7 @@ jobs:
INPUT_CACHE: ${{ inputs.cache }}
INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }}
INPUT_CACHE-MODE: ${{ inputs.cache-mode }}
INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }}
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_FILES: ${{ inputs.files }}
INPUT_OUTPUT: ${{ inputs.output }}
Expand All @@ -820,7 +824,14 @@ jobs:
const { Build } = require('@docker/github-builder-runtime/lib/buildx/build');
const { GitHub } = require('@docker/github-builder-runtime/lib/github/github');
const { Util } = require('@docker/github-builder-runtime/lib/util');


let yaml;
try {
yaml = require('js-yaml');
} catch {
yaml = require('@docker/github-builder-runtime/node_modules/js-yaml');
}

const inpPlatform = core.getInput('platform');
const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : '';
core.setOutput('platform-pair-suffix', platformPairSuffix);
Expand All @@ -831,6 +842,7 @@ jobs:
const inpCache = core.getBooleanInput('cache');
const inpCacheScope = core.getInput('cache-scope');
const inpCacheMode = core.getInput('cache-mode');
const inpBuildSecrets = core.getInput('build-secrets');
const inpContext = core.getInput('context');
const inpFiles = Util.getInputList('files');
const inpOutput = core.getInput('output');
Expand All @@ -854,6 +866,40 @@ jobs:
tags: inpMetaTags
};
const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});
const parseBuildSecrets = value => {
const normalized = value.trim();
if (!normalized) {
return {};
}
let parsed;
try {
parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA});
} catch (err) {
throw new Error(`Failed to parse build-secrets YAML: ${err.message}`);
}
if (!parsed) {
return {};
}
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
throw new Error('build-secrets must be a YAML object');
}
const secrets = {};
for (const [id, secret] of Object.entries(parsed)) {
if (!/^[A-Za-z0-9_.-]+$/.test(id)) {
throw new Error(`Invalid build secret id "${id}": use letters, digits, dots, underscores or dashes`);
}
if (typeof secret !== 'string') {
throw new Error(`build-secrets value for "${id}" must be a string`);
}
if (secret.length === 0) {
throw new Error(`build-secrets value for "${id}" must not be empty`);
}
core.setSecret(secret);
secrets[id] = secret;
}
return secrets;
};
const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`;

const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'};
const bakeSource = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs});
Expand All @@ -872,6 +918,14 @@ jobs:
core.info(sbom);
core.setOutput('sbom', sbom);
});

let buildSecrets;
try {
buildSecrets = parseBuildSecrets(inpBuildSecrets);
} catch (err) {
core.setFailed(err.message);
return;
}

const envs = Object.assign({},
inpVars ? inpVars.reduce((acc, curr) => {
Expand All @@ -886,8 +940,14 @@ jobs:
BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
}
);
const secretOverrides = [];
Object.entries(buildSecrets).forEach(([id, secret], index) => {
const envName = toBuildSecretEnvName(id, index);
envs[envName] = secret;
secretOverrides.push(`*.secrets+=id=${id},env=${envName}`);
});
await core.group(`Set envs`, async () => {
core.info(JSON.stringify(envs, null, 2));
core.info(JSON.stringify(Object.keys(envs).sort(), null, 2));
core.setOutput('envs', JSON.stringify(envs));
});

Expand Down Expand Up @@ -950,6 +1010,7 @@ jobs:
bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || inpTarget}${platformPairSuffix}`);
bakeOverrides.push(`*.cache-to=type=gha,ignore-error=true,scope=${inpCacheScope || inpTarget}${platformPairSuffix},mode=${inpCacheMode}`);
}
bakeOverrides.push(...secretOverrides);
core.info(JSON.stringify(bakeOverrides, null, 2));
core.setOutput('overrides', bakeOverrides.join(os.EOL));
});
Expand Down
76 changes: 71 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ on:
registry-auths:
description: "Raw authentication to registries, defined as YAML objects (for image output)"
required: false
build-secrets:
description: "YAML object mapping BuildKit secret IDs to secret values"
required: false
github-token:
description: "GitHub Token used to authenticate against the repository for Git context"
required: false
Expand Down Expand Up @@ -691,6 +694,7 @@ jobs:
INPUT_CACHE: ${{ inputs.cache }}
INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }}
INPUT_CACHE-MODE: ${{ inputs.cache-mode }}
INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }}
INPUT_LABELS: ${{ inputs.labels }}
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_OUTPUT: ${{ inputs.output }}
Expand All @@ -705,12 +709,20 @@ jobs:
INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }}
INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }}
INPUT_META-LABELS: ${{ steps.meta.outputs.labels }}
INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }}
with:
script: |
const { Build } = require('@docker/github-builder-runtime/lib/buildx/build');
const { GitHub } = require('@docker/github-builder-runtime/lib/github/github');
const { Util } = require('@docker/github-builder-runtime/lib/util');


let yaml;
try {
yaml = require('js-yaml');
} catch {
yaml = require('@docker/github-builder-runtime/node_modules/js-yaml');
}

const inpPlatform = core.getInput('platform');
const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : '';
core.setOutput('platform-pair-suffix', platformPairSuffix);
Expand All @@ -724,6 +736,7 @@ jobs:
const inpCache = core.getBooleanInput('cache');
const inpCacheScope = core.getInput('cache-scope');
const inpCacheMode = core.getInput('cache-mode');
const inpBuildSecrets = core.getInput('build-secrets');
const inpContext = core.getInput('context');
const inpLabels = core.getInput('labels');
const inpOutput = core.getInput('output');
Expand All @@ -739,6 +752,7 @@ jobs:
const inpMetaAnnotations = core.getMultilineInput('meta-annotations');
const inpSetMetaLabels = core.getBooleanInput('set-meta-labels');
const inpMetaLabels = core.getMultilineInput('meta-labels');
const inpGitHubToken = core.getInput('github-token');

const meta = {
version: inpMetaVersion,
Expand All @@ -747,6 +761,40 @@ jobs:

const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});
const toMultilineInput = value => value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
const parseBuildSecrets = value => {
const normalized = value.trim();
if (!normalized) {
return {};
}
let parsed;
try {
parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA});
} catch (err) {
throw new Error(`Failed to parse build-secrets YAML: ${err.message}`);
}
if (!parsed) {
return {};
}
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
throw new Error('build-secrets must be a YAML object');
}
const secrets = {};
for (const [id, secret] of Object.entries(parsed)) {
if (!/^[A-Za-z0-9_.-]+$/.test(id)) {
throw new Error(`Invalid build secret id "${id}": use letters, digits, dots, underscores or dashes`);
}
if (typeof secret !== 'string') {
throw new Error(`build-secrets value for "${id}" must be a string`);
}
if (secret.length === 0) {
throw new Error(`build-secrets value for "${id}" must not be empty`);
}
core.setSecret(secret);
secrets[id] = secret;
}
return secrets;
};
const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`;

const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'};
const buildContext = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs});
Expand Down Expand Up @@ -803,6 +851,26 @@ jobs:
}
core.setOutput('labels', labels.join('\n'));
core.setOutput('build-args', buildArgs);

let buildSecrets;
try {
buildSecrets = parseBuildSecrets(inpBuildSecrets);
} catch (err) {
core.setFailed(err.message);
return;
}
const envs = {
BUILDKIT_MULTI_PLATFORM: '1',
GIT_AUTH_TOKEN: inpGitHubToken
};
const secretEnvs = ['GIT_AUTH_TOKEN=GIT_AUTH_TOKEN'];
Object.entries(buildSecrets).forEach(([id, secret], index) => {
const envName = toBuildSecretEnvName(id, index);
envs[envName] = secret;
secretEnvs.push(`${id}=${envName}`);
});
core.setOutput('envs', JSON.stringify(envs));
core.setOutput('secret-envs', secretEnvs.join('\n'));

if (GitHub.context.payload.repository?.private ?? false) {
// if this is a private repository, we set min provenance mode
Expand Down Expand Up @@ -833,13 +901,11 @@ jobs:
platforms: ${{ steps.prepare.outputs.platform }}
provenance: ${{ steps.prepare.outputs.provenance }}
sbom: ${{ steps.prepare.outputs.sbom }}
secret-envs: GIT_AUTH_TOKEN=GIT_AUTH_TOKEN
secret-envs: ${{ steps.prepare.outputs.secret-envs }}
shm-size: ${{ inputs.shm-size }}
target: ${{ inputs.target }}
ulimit: ${{ inputs.ulimit }}
env:
BUILDKIT_MULTI_PLATFORM: 1
GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }}
env: ${{ fromJson(steps.prepare.outputs.envs || '{}') }}
-
name: Login to registry for signing
if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }}
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ___
* [Secrets](#secrets-1)
* [Outputs](#outputs-1)
* [Notes](#notes)
* [BuildKit secrets](#buildkit-secrets)
* [Signed GitHub Actions cache](#signed-github-actions-cache)
* [Runner mapping](#runner-mapping)
* [Metadata templates](#metadata-templates)
Expand Down Expand Up @@ -249,6 +250,7 @@ jobs:
| Name | Default | Description |
|------------------|-----------------------|--------------------------------------------------------------------------------|
| `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) |
| `build-secrets` | | YAML object mapping BuildKit secret IDs to secret values |
| `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context |

### Outputs
Expand Down Expand Up @@ -358,6 +360,7 @@ jobs:
| Name | Default | Description |
|------------------|-----------------------|--------------------------------------------------------------------------------|
| `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) |
| `build-secrets` | | YAML object mapping BuildKit secret IDs to secret values |
| `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context |

### Outputs
Expand All @@ -378,6 +381,33 @@ with `builder-outputs: ${{ toJSON(needs.<job_id>.outputs) }}`.

## Notes

### BuildKit secrets

The `build-secrets` secret is shared by the build and bake workflows. It must
be a YAML object where each key is the BuildKit secret ID and each value is the
secret payload:

```yaml
secrets:
build-secrets: |
npmrc: ${{ toJSON(secrets.NPMRC) }}
aws_credentials: ${{ toJSON(secrets.AWS_CREDENTIALS) }}
inline_config: |
first line
second line
```

Use `toJSON(...)` when injecting GitHub secrets so values that contain newlines
or YAML syntax are preserved as a single YAML scalar.

Each secret is exposed as an env-backed BuildKit secret. The build workflow
passes these values through `docker/build-push-action` `secret-envs`, and the
bake workflow appends matching `*.secrets+=id=...,env=...` overrides.

File-based secrets are not supported. These workflows build from Git context
and do not checkout the caller repository, so caller workspace paths are not
available to the build job.

### Signed GitHub Actions cache

When the workflow has GitHub OIDC available through `id-token: write`, BuildKit
Expand Down
4 changes: 4 additions & 0 deletions test/docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ target "hello-cross" {
platforms = ["linux/amd64", "linux/arm64"]
}

target "secret" {
dockerfile = "secret.Dockerfile"
}

target "go-cross-with-contexts" {
inherits = ["go-cross"]
contexts = {
Expand Down
11 changes: 11 additions & 0 deletions test/secret.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# syntax=docker/dockerfile:1

FROM alpine
RUN --mount=type=secret,id=fixture_plain,env=fixture_plain \
--mount=type=secret,id=fixture_json,env=fixture_json \
printf 'fixture_plain=%s\n' "$fixture_plain" && \
printf 'fixture_json=%s\n' "$fixture_json" && \
printf 'alpha-line\nbeta-line\n' > /tmp/expected && \
printf '%s' "$fixture_plain" | cmp - /tmp/expected && \
printf 'gamma-line\ndelta-line\n' > /tmp/expected-json && \
printf '%s' "$fixture_json" | cmp - /tmp/expected-json
Loading