Skip to content
Draft
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
20 changes: 18 additions & 2 deletions .github/workflows/.test-bake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,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: foosec
secrets:
build-secrets: |
fixture_plain: |
alpha-line
beta-line
foosec.fixture_json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }}

bake-set-runner:
uses: ./.github/workflows/bake.yml
permissions:
Expand Down Expand Up @@ -565,11 +582,10 @@ jobs:
contents: read
id-token: write
with:
setup-qemu: true
artifact-upload: false
context: test
output: local
target: go
target: vars
vars: |
XX_VERSION=1.9.0

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 @@ -512,6 +512,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
162 changes: 128 additions & 34 deletions .github/workflows/bake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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, optionally target-scoped, 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 @@ -209,6 +212,7 @@ jobs:
metaImages: ${{ steps.set.outputs.metaImages }}
sign: ${{ steps.set.outputs.sign }}
privateRepo: ${{ steps.set.outputs.privateRepo }}
targets: ${{ steps.set.outputs.targets }}
ghaCacheSign: ${{ steps.set.outputs.ghaCacheSign }}
steps:
-
Expand Down Expand Up @@ -333,7 +337,7 @@ jobs:
const inpArtifactUpload = core.getBooleanInput('artifact-upload');
const inpJobNamePrefix = core.getInput('job-name-prefix');
const inpContext = core.getInput('context');
const inpVars = Util.getInputList('vars');
const inpVars = Util.getInputList('vars', {ignoreComma: true, quote: false});
const inpFiles = Util.getInputList('files');
const inpOutput = core.getInput('output');
const inpPush = core.getBooleanInput('push');
Expand Down Expand Up @@ -467,23 +471,26 @@ jobs:
core.info(bakeSource);
});

const envs = Object.assign({},
inpVars ? inpVars.reduce((acc, curr) => {
const idx = curr.indexOf('=');
if (idx !== -1) {
acc[curr.substring(0, idx)] = curr.substring(idx + 1);
}
return acc;
}, {}) : {},
{
BUILDKIT_MULTI_PLATFORM: '1',
BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
}
);
// Preserve GitHub's default workflow context variables without allowing arbitrary runner env lookup in Bake.
const defaultBakeVars = Object.keys(process.env).filter(key => key.startsWith('GITHUB_') || key.startsWith('RUNNER_')).sort().map(key => `${key}=${process.env[key] || ''}`);
const bakeVars = [...defaultBakeVars, ...inpVars];
const bakeVarKeys = bakeVars.map(value => {
const idx = value.indexOf('=');
return idx === -1 ? '<invalid>' : value.substring(0, idx);
});
await core.group(`Set bake vars`, async () => {
core.info(JSON.stringify(bakeVarKeys.sort(), null, 2));
});

const envs = {
BUILDKIT_MULTI_PLATFORM: '1',
BUILDX_BAKE_DISABLE_VARS_ENV_LOOKUP: '1',
BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
};
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());
await core.group(`Set metaImages output`, async () => {
core.info(JSON.stringify(metaImages, null, 2));
Expand All @@ -500,7 +507,8 @@ jobs:
overrides: inpSet,
sbom: inpSbom ? `generator=${inpSbomImage}` : 'false',
source: bakeSource,
targets: [inpTarget]
targets: [inpTarget],
vars: bakeVars
}, {
env: Object.keys(envs).length > 0 ? envs : undefined
});
Expand Down Expand Up @@ -547,6 +555,7 @@ jobs:
if (unsupportedTargets.length > 0) {
throw new Error(`Only one target can be built at once, found unsupported targets: ${unsupportedTargets.join(', ')}`);
}
core.setOutput('targets', JSON.stringify([...allowedTargets]));
});
} catch (error) {
core.setFailed(error);
Expand Down Expand Up @@ -832,6 +841,8 @@ jobs:
INPUT_CACHE: ${{ inputs.cache }}
INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }}
INPUT_CACHE-MODE: ${{ inputs.cache-mode }}
INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }}
INPUT_TARGETS: ${{ needs.prepare.outputs.targets }}
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_FILES: ${{ inputs.files }}
INPUT_OUTPUT: ${{ inputs.output }}
Expand All @@ -855,7 +866,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 @@ -866,14 +884,16 @@ 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 inpTargets = core.getInput('targets');
const inpContext = core.getInput('context');
const inpFiles = Util.getInputList('files');
const inpOutput = core.getInput('output');
const inpPush = core.getBooleanInput('push');
const inpSbom = core.getBooleanInput('sbom');
const inpSet = Util.getInputList('set', {ignoreComma: true, quote: false});
const inpTarget = core.getInput('target');
const inpVars = Util.getInputList('vars');
const inpVars = Util.getInputList('vars', {ignoreComma: true, quote: false});
const inpMetaImages = core.getMultilineInput('meta-images');
const inpMetaVersion = core.getInput('meta-version');
const inpMetaTags = core.getMultilineInput('meta-tags');
Expand All @@ -889,6 +909,43 @@ jobs:
tags: inpMetaTags
};
const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});

const isInputKeySafe = value => value && !/[\r\n=]/.test(value);
const parseBuildSecretKey = key => {
const separator = key.lastIndexOf('.');
return separator === -1 ? {target: inpTarget, id: key} : {target: key.substring(0, separator), id: key.substring(separator + 1)};
};
const parseBuildSecrets = value => {
const normalized = value.trim();
if (!normalized) {
return [];
}
let parsed;
try {
parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA});
} catch (err) {
const location = err.mark ? ` at line ${err.mark.line + 1}, column ${err.mark.column + 1}` : '';
throw new Error(`Failed to parse build-secrets YAML${location}`);
}
if (!parsed) {
return [];
}
if (Array.isArray(parsed) || typeof parsed !== 'object') {
throw new Error('build-secrets must be a YAML object');
}
return Object.entries(parsed).map(([key, secret]) => {
const {target, id} = parseBuildSecretKey(key);
if (!isInputKeySafe(target) || !isInputKeySafe(id)) {
throw new Error(`Invalid build-secrets key "${key}": use "secret_id" or "target.secret_id" without empty names, line breaks or "="`);
}
if (typeof secret !== 'string') {
throw new Error(`build-secrets value for "${key}" must be a string`);
}
core.setSecret(secret);
return {target, id, secret};
});
};
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 @@ -907,23 +964,59 @@ jobs:
core.info(sbom);
core.setOutput('sbom', sbom);
});

const envs = Object.assign({},
inpVars ? inpVars.reduce((acc, curr) => {
const idx = curr.indexOf('=');
if (idx !== -1) {
acc[curr.substring(0, idx)] = curr.substring(idx + 1);

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

let targets;
try {
targets = JSON.parse(inpTargets || '[]');
const allowedTargets = new Set(targets);
for (const {target} of buildSecrets) {
if (!allowedTargets.has(target)) {
throw new Error(`Build secret target "${target}" is not part of the resolved Bake definition`);
}
return acc;
}, {}) : {},
{
BUILDKIT_MULTI_PLATFORM: '1',
BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
}
);
} catch (err) {
core.setFailed(err.message);
return;
}

// Preserve GitHub's default workflow context variables without allowing arbitrary runner env lookup in Bake.
const defaultBakeVars = Object.keys(process.env).filter(key => key.startsWith('GITHUB_') || key.startsWith('RUNNER_')).sort().map(key => `${key}=${process.env[key] || ''}`);
const bakeVars = [...defaultBakeVars, ...inpVars];
const bakeVarKeys = bakeVars.map(value => {
const idx = value.indexOf('=');
return idx === -1 ? '<invalid>' : value.substring(0, idx);
});
await core.group(`Set bake vars`, async () => {
core.info(JSON.stringify(bakeVarKeys.sort(), null, 2));
core.setOutput('vars', bakeVars.join(os.EOL));
});

const envs = {
BUILDKIT_MULTI_PLATFORM: '1',
BUILDX_BAKE_DISABLE_VARS_ENV_LOOKUP: '1',
BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken
};

const secretOverrides = [];
buildSecrets.forEach(({target, id, secret}, index) => {
const envName = toBuildSecretEnvName(id, index);
envs[envName] = secret;
secretOverrides.push(`${target}.secret.${id}=env=${envName}`);
});

await core.group(`Set envs`, async () => {
core.info(JSON.stringify(envs, null, 2));
core.setOutput('envs', JSON.stringify(envs));
core.info(JSON.stringify(Object.keys(envs).sort(), null, 2));
Object.entries(envs).forEach(([key, value]) => {
core.exportVariable(key, value);
});
});

let bakeFiles = inpFiles;
Expand Down Expand Up @@ -985,6 +1078,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 Expand Up @@ -1049,7 +1143,7 @@ jobs:
targets: ${{ steps.prepare.outputs.target }}
sbom: ${{ steps.prepare.outputs.sbom }}
set: ${{ steps.prepare.outputs.overrides }}
env: ${{ fromJson(steps.prepare.outputs.envs || '{}') }}
vars: ${{ steps.prepare.outputs.vars }}
-
name: Get image digest
id: get-image-digest
Expand Down
Loading
Loading