Skip to content

Commit a98d2d3

Browse files
committed
bake: distributed builds
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
1 parent 152188b commit a98d2d3

File tree

2 files changed

+277
-28
lines changed

2 files changed

+277
-28
lines changed

.github/workflows/.test.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,11 @@ jobs:
377377
with:
378378
cosign-release: ${{ needs.bake-local.outputs.cosign-version }}
379379
-
380-
name: Download artifact
380+
name: Download artifacts
381381
uses: actions/download-artifact@v5
382382
with:
383-
name: ${{ needs.bake-local.outputs.artifact-name }}
383+
pattern: ${{ needs.bake-local.outputs.artifact-name }}*
384+
merge-multiple: true
384385
-
385386
name: Verify signatures
386387
uses: actions/github-script@v8

.github/workflows/bake.yml

Lines changed: 274 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ name: bake
33
on:
44
workflow_call:
55
inputs:
6+
runner:
7+
type: string
8+
description: "Runner instance"
9+
required: false
10+
default: 'auto'
611
context:
712
type: string
813
description: "Context to build from, defaults to repository root"
@@ -118,30 +123,169 @@ on:
118123
outputs:
119124
cosign-version:
120125
description: Cosign version used for verification
121-
value: ${{ jobs.build.outputs.cosign-version }}
126+
value: ${{ jobs.post.outputs.cosign-version }}
122127
cosign-verify-commands:
123128
description: Cosign verify commands
124-
value: ${{ jobs.build.outputs.cosign-verify-commands }}
129+
value: ${{ jobs.post.outputs.cosign-verify-commands }}
125130
artifact-name:
126131
description: Name of the uploaded artifact (for local output)
127-
value: ${{ jobs.build.outputs.artifact-name }}
132+
value: ${{ jobs.post.outputs.artifact-name }}
128133

129134
env:
130135
DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.67.0"
131136
COSIGN_VERSION: "v3.0.2"
132137
LOCAL_EXPORT_DIR: "/tmp/buildx-output"
138+
MATRIX_SIZE_LIMIT: 20
133139

134140
jobs:
135-
build:
136-
runs-on: ubuntu-latest
141+
prepare:
142+
runs-on: ${{ inputs.runner == 'auto' && 'ubuntu-latest' || inputs.runner }}
137143
outputs:
138-
cosign-version: ${{ env.COSIGN_VERSION }}
139-
cosign-verify-commands: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }}
140-
artifact-name: ${{ inputs.artifact-name }}
144+
includes: ${{ steps.set.outputs.includes }}
145+
steps:
146+
-
147+
name: Environment variables
148+
uses: actions/github-script@v8
149+
env:
150+
INPUT_ENVS: ${{ inputs.envs }}
151+
with:
152+
script: |
153+
for (const env of core.getMultilineInput('envs')) {
154+
core.info(env);
155+
const [key, value] = env.split('=', 2);
156+
core.exportVariable(key, value);
157+
}
158+
-
159+
name: Install @docker/actions-toolkit
160+
uses: actions/github-script@v8
161+
env:
162+
INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }}
163+
with:
164+
script: |
165+
await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', core.getInput('dat-module')]);
166+
-
167+
name: Set includes
168+
id: set
169+
uses: actions/github-script@v8
170+
env:
171+
INPUT_MATRIX-SIZE-LIMIT: ${{ env.MATRIX_SIZE_LIMIT }}
172+
INPUT_RUNNER: ${{ inputs.runner }}
173+
INPUT_CONTEXT: ${{ inputs.context }}
174+
INPUT_TARGET: ${{ inputs.target }}
175+
INPUT_BAKE-ALLOW: ${{ inputs.bake-allow }}
176+
INPUT_BAKE-FILES: ${{ inputs.bake-files }}
177+
INPUT_BAKE-PULL: ${{ inputs.bake-pull }}
178+
INPUT_BAKE-SBOM: ${{ inputs.bake-sbom }}
179+
INPUT_BAKE-SET: ${{ inputs.bake-set }}
180+
INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }}
181+
with:
182+
script: |
183+
const os = require('os');
184+
const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake');
185+
const { Util } = require('@docker/actions-toolkit/lib/util');
186+
187+
const inpMatrixSizeLimit = parseInt(core.getInput('matrix-size-limit'), 10);
188+
189+
const inpRunner = core.getInput('runner');
190+
const inpContext = core.getInput('context');
191+
const inpTarget = core.getInput('target');
192+
const inpBakeAllow = core.getInput('bake-allow');
193+
const inpBakeFiles = Util.getInputList('bake-files');
194+
const inpBakePull = core.getBooleanInput('bake-pull');
195+
const inpBakeSbom = core.getInput('bake-sbom');
196+
const inpBakeSet = Util.getInputList('bake-set', {ignoreComma: true, quote: false});
197+
const inpGitHubToken = core.getInput('github-token');
198+
199+
const bakeSource = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}.git#${process.env.GITHUB_REF}:${inpContext}`;
200+
await core.group(`Set bake source`, async () => {
201+
core.info(bakeSource);
202+
});
203+
204+
let def;
205+
let target;
206+
await core.group(`Validating definition`, async () => {
207+
const bake = new Bake();
208+
def = await bake.getDefinition({
209+
allow: inpBakeAllow,
210+
files: inpBakeFiles,
211+
overrides: inpBakeSet,
212+
sbom: inpBakeSbom,
213+
source: bakeSource,
214+
targets: [inpTarget],
215+
githubToken: inpGitHubToken
216+
});
217+
if (!def) {
218+
throw new Error('Bake definition not set');
219+
}
220+
const targets = Object.keys(def.target);
221+
if (targets.length > 1) {
222+
throw new Error(`Only one target can be built at once, found: ${targets.join(', ')}`);
223+
}
224+
target = targets[0];
225+
});
226+
227+
await core.group(`Set includes`, async () => {
228+
let includes = [];
229+
const platforms = def.target[target].platforms || [];
230+
if (platforms.length > inpMatrixSizeLimit) {
231+
throw new Error(`Platforms to build exceed matrix size limit of ${inpMatrixSizeLimit}`);
232+
} else if (platforms.length === 0) {
233+
includes.push({
234+
index: 0,
235+
runner: inpRunner === 'auto' ? 'ubuntu-latest' : inpRunner
236+
});
237+
} else {
238+
platforms.forEach((platform, index) => {
239+
let runner = inpRunner;
240+
if (runner === 'auto') {
241+
runner = platform.startsWith('linux/arm') ? 'ubuntu-24.04-arm' : 'ubuntu-latest';
242+
}
243+
includes.push({
244+
index: index,
245+
platform: platform,
246+
runner: runner
247+
});
248+
});
249+
}
250+
core.info(JSON.stringify(includes, null, 2));
251+
core.setOutput('includes', JSON.stringify(includes));
252+
});
253+
254+
build:
255+
runs-on: ${{ matrix.runner }}
256+
needs:
257+
- prepare
141258
permissions:
142259
contents: read
143260
id-token: write # needed for signing the images with GitHub OIDC Token
144261
packages: write # needed to push images to GitHub Container Registry
262+
strategy:
263+
fail-fast: false
264+
matrix:
265+
include: ${{ fromJson(needs.prepare.outputs.includes) }}
266+
outputs:
267+
# needs predefined outputs as we can't use dynamic ones atm: https://github.com/actions/runner/pull/2477
268+
# 20 is the maximum number of platforms supported by our matrix strategy
269+
result_0: ${{ steps.result.outputs.result_0 }}
270+
result_1: ${{ steps.result.outputs.result_1 }}
271+
result_2: ${{ steps.result.outputs.result_2 }}
272+
result_3: ${{ steps.result.outputs.result_3 }}
273+
result_4: ${{ steps.result.outputs.result_4 }}
274+
result_5: ${{ steps.result.outputs.result_5 }}
275+
result_6: ${{ steps.result.outputs.result_6 }}
276+
result_7: ${{ steps.result.outputs.result_7 }}
277+
result_8: ${{ steps.result.outputs.result_8 }}
278+
result_9: ${{ steps.result.outputs.result_9 }}
279+
result_10: ${{ steps.result.outputs.result_10 }}
280+
result_11: ${{ steps.result.outputs.result_11 }}
281+
result_12: ${{ steps.result.outputs.result_12 }}
282+
result_13: ${{ steps.result.outputs.result_13 }}
283+
result_14: ${{ steps.result.outputs.result_14 }}
284+
result_15: ${{ steps.result.outputs.result_15 }}
285+
result_16: ${{ steps.result.outputs.result_16 }}
286+
result_17: ${{ steps.result.outputs.result_17 }}
287+
result_18: ${{ steps.result.outputs.result_18 }}
288+
result_19: ${{ steps.result.outputs.result_19 }}
145289
steps:
146290
-
147291
name: Environment variables
@@ -192,6 +336,7 @@ jobs:
192336
id: prepare
193337
uses: actions/github-script@v8
194338
env:
339+
INPUT_PLATFORM: ${{ matrix.platform }}
195340
INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }}
196341
INPUT_CONTEXT: ${{ inputs.context }}
197342
INPUT_TARGET: ${{ inputs.target }}
@@ -217,7 +362,12 @@ jobs:
217362
const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake');
218363
const { Util } = require('@docker/actions-toolkit/lib/util');
219364
365+
const inpPlatform = core.getInput('platform');
366+
const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : '';
367+
core.setOutput('platform-pair-suffix', platformPairSuffix);
368+
220369
const inpLocalExportDir = core.getInput('local-export-dir');
370+
221371
const inpContext = core.getInput('context');
222372
const inpTarget = core.getInput('target');
223373
const inpOutput = core.getInput('output');
@@ -297,7 +447,7 @@ jobs:
297447
outputOverride = `*.output=type=registry,"name=${inpMetaImages.join(',')}",oci-artifact=true,push-by-digest=true,name-canonical=true`;
298448
break;
299449
case 'local':
300-
outputOverride = `*.output=type=local,dest=${inpLocalExportDir}`;
450+
outputOverride = `*.output=type=local,platform-split=true,dest=${inpLocalExportDir}`;
301451
break;
302452
default:
303453
core.setFailed(`Invalid output: ${inpOutput}`);
@@ -306,9 +456,12 @@ jobs:
306456
let bakeOverrides = [...inpBakeSet, outputOverride];
307457
await core.group(`Set bake overrides`, async () => {
308458
bakeOverrides.push('*.attest=type=provenance,mode=max,version=v1', '*.tags=');
459+
if (inpPlatform) {
460+
bakeOverrides.push(`*.platform=${inpPlatform}`);
461+
}
309462
if (inpCache) {
310-
bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || target}`);
311-
bakeOverrides.push(`*.cache-to=type=gha,scope=${inpCacheScope || target},mode=${inpCacheMode}`);
463+
bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || target}${platformPairSuffix}`);
464+
bakeOverrides.push(`*.cache-to=type=gha,scope=${inpCacheScope || target}${platformPairSuffix},mode=${inpCacheMode}`);
312465
}
313466
core.info(JSON.stringify(bakeOverrides, null, 2));
314467
core.setOutput('overrides', bakeOverrides.join(os.EOL));
@@ -434,22 +587,112 @@ jobs:
434587
}
435588
core.setOutput('verify-commands', verifyCommands.join('\n'));
436589
});
590+
-
591+
name: List local output
592+
if: ${{ inputs.output == 'local' }}
593+
run: |
594+
tree -nh ${{ env.LOCAL_EXPORT_DIR }}
595+
-
596+
name: Upload artifact
597+
if: ${{ inputs.output == 'local' }}
598+
uses: actions/upload-artifact@v5
599+
with:
600+
name: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix }}
601+
path: ${{ env.LOCAL_EXPORT_DIR }}
602+
if-no-files-found: error
603+
-
604+
name: Set result output
605+
id: result
606+
uses: actions/github-script@v8
607+
env:
608+
INPUT_INDEX: ${{ matrix.index }}
609+
INPUT_VERIFY-COMMANDS: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }}
610+
INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }}
611+
INPUT_ARTIFACT-NAME: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix }}
612+
with:
613+
script: |
614+
const inpIndex = core.getInput('index');
615+
const inpVerifyCommands = core.getInput('verify-commands');
616+
const inpImageDigest = core.getInput('image-digest');
617+
const inpArtifactName = core.getInput('artifact-name');
618+
619+
const result = {
620+
verifyCommands: inpVerifyCommands,
621+
imageDigest: inpImageDigest,
622+
artifactName: inpArtifactName
623+
}
624+
core.info(JSON.stringify(result, null, 2));
625+
626+
core.setOutput(`result_${inpIndex}`, JSON.stringify(result));
627+
628+
post:
629+
runs-on: ubuntu-latest
630+
outputs:
631+
cosign-version: ${{ env.COSIGN_VERSION }}
632+
cosign-verify-commands: ${{ steps.set.outputs.cosign-verify-commands }}
633+
artifact-name: ${{ inputs.artifact-name }}
634+
needs:
635+
- build
636+
steps:
637+
-
638+
name: Docker meta
639+
id: meta
640+
if: ${{ inputs.output == 'registry' }}
641+
uses: docker/metadata-action@v5
642+
with:
643+
images: ${{ inputs.meta-images }}
644+
tags: ${{ inputs.meta-tags }}
645+
flavor: ${{ inputs.meta-flavor }}
646+
labels: ${{ inputs.meta-labels }}
647+
annotations: ${{ inputs.meta-annotations }}
648+
bake-target: ${{ inputs.meta-bake-target }}
649+
-
650+
name: Login to registry
651+
if: ${{ inputs.output == 'registry' }}
652+
# TODO: switch to docker/login-action when OIDC is supported
653+
uses: crazy-max/docker-login-action@dockerhub-oidc
654+
with:
655+
registry-auth: ${{ secrets.registry-auths }}
656+
-
657+
name: Set up Docker Buildx
658+
if: ${{ inputs.output == 'registry' }}
659+
uses: docker/setup-buildx-action@v3
660+
with:
661+
version: latest
662+
buildkitd-flags: --debug
437663
-
438664
name: Create manifest
439665
if: ${{ inputs.output == 'registry' }}
440666
uses: actions/github-script@v8
441667
env:
442668
INPUT_IMAGE-NAMES: ${{ inputs.meta-images }}
443669
INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }}
444-
INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }}
670+
INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }}
445671
with:
446672
script: |
447-
for (const imageName of core.getMultilineInput('image-names')) {
673+
const inpImageNames = core.getMultilineInput('image-names');
674+
const inpTagNames = core.getMultilineInput('tag-names');
675+
const inpBuildOutputs = JSON.parse(core.getInput('build-outputs'));
676+
677+
const digests = [];
678+
for (const key of Object.keys(inpBuildOutputs)) {
679+
const output = JSON.parse(inpBuildOutputs[key]);
680+
if (output.imageDigest) {
681+
digests.push(output.imageDigest);
682+
}
683+
}
684+
if (digests.length === 0) {
685+
throw new Error('No image digests found from build outputs');
686+
}
687+
688+
for (const imageName of inpImageNames) {
448689
let createArgs = ['buildx', 'imagetools', 'create'];
449-
for (const tag of core.getMultilineInput('tag-names')) {
690+
for (const tag of inpTagNames) {
450691
createArgs.push('-t', `${imageName}:${tag}`);
451692
}
452-
createArgs.push(core.getInput('image-digest'));
693+
for (const digest of digests) {
694+
createArgs.push(digest);
695+
}
453696
await exec.getExecOutput('docker', createArgs, {
454697
ignoreReturnCode: true
455698
}).then(res => {
@@ -459,15 +702,20 @@ jobs:
459702
});
460703
}
461704
-
462-
name: List local output
463-
if: ${{ inputs.output == 'local' }}
464-
run: |
465-
tree -nh ${{ env.LOCAL_EXPORT_DIR }}
466-
-
467-
name: Upload artifact
468-
if: ${{ inputs.output == 'local' }}
469-
uses: actions/upload-artifact@v5
705+
name: Set outputs
706+
id: set
707+
if: ${{ inputs.output != 'cacheonly' }}
708+
uses: actions/github-script@v8
709+
env:
710+
INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }}
470711
with:
471-
name: ${{ inputs.artifact-name }}
472-
path: ${{ env.LOCAL_EXPORT_DIR }}
473-
if-no-files-found: error
712+
script: |
713+
const inpBuildOutputs = JSON.parse(core.getInput('build-outputs'));
714+
const verifyCommands = [];
715+
for (const key of Object.keys(inpBuildOutputs)) {
716+
const output = JSON.parse(inpBuildOutputs[key]);
717+
if (output.verifyCommands) {
718+
verifyCommands.push(output.verifyCommands);
719+
}
720+
}
721+
core.setOutput('cosign-verify-commands', verifyCommands.join('\n'));

0 commit comments

Comments
 (0)