Skip to content

Commit fde374b

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

2 files changed

Lines changed: 289 additions & 25 deletions

File tree

‎.github/workflows/.test.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,18 @@ jobs:
223223
await exec.exec(cmd);
224224
}
225225
226+
build-local-single:
227+
uses: ./.github/workflows/build.yml
228+
permissions:
229+
contents: read
230+
packages: write
231+
id-token: write
232+
with:
233+
output: ${{ github.event_name != 'pull_request' && 'local' || 'cacheonly' }}
234+
artifact-name: build-output-single
235+
build-file: test/hello.Dockerfile
236+
build-sbom: true
237+
226238
bake-aws:
227239
uses: ./.github/workflows/bake.yml
228240
permissions:
@@ -323,3 +335,17 @@ jobs:
323335
for (const cmd of cosignVerifyCommands) {
324336
await exec.exec(cmd);
325337
}
338+
339+
bake-local-single:
340+
uses: ./.github/workflows/bake.yml
341+
permissions:
342+
contents: read
343+
packages: write
344+
id-token: write
345+
with:
346+
context: test
347+
target: hello
348+
output: ${{ github.event_name != 'pull_request' && 'local' || 'cacheonly' }}
349+
cache: true
350+
artifact-name: bake-output-single
351+
bake-sbom: true

‎.github/workflows/bake.yml‎

Lines changed: 263 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -118,30 +118,167 @@ on:
118118
outputs:
119119
cosign-version:
120120
description: Cosign version used for verification
121-
value: ${{ jobs.build.outputs.cosign-version }}
121+
value: ${{ jobs.post.outputs.cosign-version }}
122122
cosign-verify-commands:
123123
description: Cosign verify commands
124-
value: ${{ jobs.build.outputs.cosign-verify-commands }}
124+
value: ${{ jobs.post.outputs.cosign-verify-commands }}
125125
artifact-name:
126126
description: Name of the uploaded artifact (for local output)
127-
value: ${{ jobs.build.outputs.artifact-name }}
127+
value: ${{ jobs.post.outputs.artifact-name }}
128128

129129
env:
130130
DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.67.0"
131131
COSIGN_VERSION: "v3.0.2"
132132
LOCAL_EXPORT_DIR: "/tmp/buildx-output"
133+
MATRIX_SIZE_LIMIT: 20
133134

134135
jobs:
135-
build:
136-
runs-on: ubuntu-latest
136+
prepare:
137+
runs-on: ${{ inputs.runner == 'auto' && 'ubuntu-latest' || inputs.runner }}
137138
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 }}
139+
includes: ${{ steps.set.outputs.includes }}
140+
steps:
141+
-
142+
name: Environment variables
143+
uses: actions/github-script@v8
144+
env:
145+
INPUT_ENVS: ${{ inputs.envs }}
146+
with:
147+
script: |
148+
for (const env of core.getMultilineInput('envs')) {
149+
core.info(env);
150+
const [key, value] = env.split('=', 2);
151+
core.exportVariable(key, value);
152+
}
153+
-
154+
name: Install @docker/actions-toolkit
155+
uses: actions/github-script@v8
156+
env:
157+
INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }}
158+
with:
159+
script: |
160+
await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', core.getInput('dat-module')]);
161+
-
162+
name: Set includes
163+
id: set
164+
uses: actions/github-script@v8
165+
env:
166+
INPUT_MATRIX-SIZE-LIMIT: ${{ env.MATRIX_SIZE_LIMIT }}
167+
INPUT_CONTEXT: ${{ inputs.context }}
168+
INPUT_TARGET: ${{ inputs.target }}
169+
INPUT_BAKE-ALLOW: ${{ inputs.bake-allow }}
170+
INPUT_BAKE-FILES: ${{ inputs.bake-files }}
171+
INPUT_BAKE-PULL: ${{ inputs.bake-pull }}
172+
INPUT_BAKE-SBOM: ${{ inputs.bake-sbom }}
173+
INPUT_BAKE-SET: ${{ inputs.bake-set }}
174+
INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }}
175+
with:
176+
script: |
177+
const os = require('os');
178+
const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake');
179+
const { Util } = require('@docker/actions-toolkit/lib/util');
180+
181+
const inpMatrixSizeLimit = parseInt(core.getInput('matrix-size-limit'), 10);
182+
183+
const inpContext = core.getInput('context');
184+
const inpTarget = core.getInput('target');
185+
const inpBakeAllow = core.getInput('bake-allow');
186+
const inpBakeFiles = Util.getInputList('bake-files');
187+
const inpBakePull = core.getBooleanInput('bake-pull');
188+
const inpBakeSbom = core.getInput('bake-sbom');
189+
const inpBakeSet = Util.getInputList('bake-set', {ignoreComma: true, quote: false});
190+
const inpGitHubToken = core.getInput('github-token');
191+
192+
const bakeSource = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}.git#${process.env.GITHUB_REF}:${inpContext}`;
193+
await core.group(`Set bake source`, async () => {
194+
core.info(bakeSource);
195+
});
196+
197+
let def;
198+
let target;
199+
await core.group(`Validating definition`, async () => {
200+
const bake = new Bake();
201+
def = await bake.getDefinition({
202+
allow: inpBakeAllow,
203+
files: inpBakeFiles,
204+
overrides: inpBakeSet,
205+
sbom: inpBakeSbom,
206+
source: bakeSource,
207+
targets: [inpTarget],
208+
githubToken: inpGitHubToken
209+
});
210+
if (!def) {
211+
throw new Error('Bake definition not set');
212+
}
213+
const targets = Object.keys(def.target);
214+
if (targets.length > 1) {
215+
throw new Error(`Only one target can be built at once, found: ${targets.join(', ')}`);
216+
}
217+
target = targets[0];
218+
});
219+
220+
await core.group(`Set includes`, async () => {
221+
let includes = [];
222+
const platforms = def.target[target].platforms;
223+
if (platforms.length > inpMatrixSizeLimit) {
224+
throw new Error(`Platforms to build exceed matrix size limit of ${inpMatrixSizeLimit}`);
225+
} else if (platforms.length === 0) {
226+
includes.push({
227+
index: 0,
228+
runner: inpRunner === 'auto' ? 'ubuntu-latest' : inpRunner
229+
});
230+
} else {
231+
platforms.forEach((platform, index) => {
232+
let runner = inpRunner;
233+
if (runner === 'auto') {
234+
runner = platform.startsWith('linux/arm') ? 'ubuntu-24.04-arm' : 'ubuntu-latest';
235+
}
236+
includes.push({
237+
index: index,
238+
platform: platform,
239+
runner: runner
240+
});
241+
});
242+
}
243+
core.info(JSON.stringify(includes, null, 2));
244+
core.setOutput('includes', JSON.stringify(includes));
245+
});
246+
247+
build:
248+
runs-on: ${{ matrix.runner }}
249+
needs:
250+
- prepare
141251
permissions:
142252
contents: read
143253
id-token: write # needed for signing the images with GitHub OIDC Token
144254
packages: write # needed to push images to GitHub Container Registry
255+
strategy:
256+
fail-fast: false
257+
matrix:
258+
include: ${{ fromJson(needs.prepare.outputs.includes) }}
259+
outputs:
260+
# needs predefined outputs as we can't use dynamic ones atm: https://github.com/actions/runner/pull/2477
261+
# 20 is the maximum number of platforms supported by our matrix strategy
262+
result_0: ${{ steps.result.outputs.result_0 }}
263+
result_1: ${{ steps.result.outputs.result_1 }}
264+
result_2: ${{ steps.result.outputs.result_2 }}
265+
result_3: ${{ steps.result.outputs.result_3 }}
266+
result_4: ${{ steps.result.outputs.result_4 }}
267+
result_5: ${{ steps.result.outputs.result_5 }}
268+
result_6: ${{ steps.result.outputs.result_6 }}
269+
result_7: ${{ steps.result.outputs.result_7 }}
270+
result_8: ${{ steps.result.outputs.result_8 }}
271+
result_9: ${{ steps.result.outputs.result_9 }}
272+
result_10: ${{ steps.result.outputs.result_10 }}
273+
result_11: ${{ steps.result.outputs.result_11 }}
274+
result_12: ${{ steps.result.outputs.result_12 }}
275+
result_13: ${{ steps.result.outputs.result_13 }}
276+
result_14: ${{ steps.result.outputs.result_14 }}
277+
result_15: ${{ steps.result.outputs.result_15 }}
278+
result_16: ${{ steps.result.outputs.result_16 }}
279+
result_17: ${{ steps.result.outputs.result_17 }}
280+
result_18: ${{ steps.result.outputs.result_18 }}
281+
result_19: ${{ steps.result.outputs.result_19 }}
145282
steps:
146283
-
147284
name: Environment variables
@@ -192,6 +329,7 @@ jobs:
192329
id: prepare
193330
uses: actions/github-script@v8
194331
env:
332+
INPUT_PLATFORM: ${{ matrix.platform }}
195333
INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }}
196334
INPUT_CONTEXT: ${{ inputs.context }}
197335
INPUT_TARGET: ${{ inputs.target }}
@@ -217,7 +355,12 @@ jobs:
217355
const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake');
218356
const { Util } = require('@docker/actions-toolkit/lib/util');
219357
358+
const inpPlatform = core.getInput('platform');
359+
const cacheScopeSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : '';
360+
core.setOutput('artifact-name-suffix', inpPlatform ? `_${inpPlatform.replace(/\//g, '_')}` : '');
361+
220362
const inpLocalExportDir = core.getInput('local-export-dir');
363+
221364
const inpContext = core.getInput('context');
222365
const inpTarget = core.getInput('target');
223366
const inpOutput = core.getInput('output');
@@ -306,9 +449,12 @@ jobs:
306449
let bakeOverrides = [...inpBakeSet, outputOverride];
307450
await core.group(`Set bake overrides`, async () => {
308451
bakeOverrides.push('*.attest=type=provenance,mode=max,version=v1', '*.tags=');
452+
if (inpPlatform) {
453+
bakeOverrides.push(`*.platform=${inpPlatform}`);
454+
}
309455
if (inpCache) {
310-
bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || target}`);
311-
bakeOverrides.push(`*.cache-to=type=gha,scope=${inpCacheScope || target},mode=${inpCacheMode}`);
456+
bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || target}${cacheScopeSuffix}`);
457+
bakeOverrides.push(`*.cache-to=type=gha,scope=${inpCacheScope || target}${cacheScopeSuffix},mode=${inpCacheMode}`);
312458
}
313459
core.info(JSON.stringify(bakeOverrides, null, 2));
314460
core.setOutput('overrides', bakeOverrides.join(os.EOL));
@@ -434,22 +580,109 @@ jobs:
434580
}
435581
core.setOutput('verify-commands', verifyCommands.join('\n'));
436582
});
583+
-
584+
name: List local output
585+
if: ${{ inputs.output == 'local' }}
586+
run: |
587+
tree -nh ${{ env.LOCAL_EXPORT_DIR }}
588+
-
589+
name: Upload artifact
590+
if: ${{ inputs.output == 'local' }}
591+
uses: actions/upload-artifact@v5
592+
with:
593+
name: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.artifact-name-suffix }}
594+
path: ${{ env.LOCAL_EXPORT_DIR }}
595+
if-no-files-found: error
596+
-
597+
name: Set result output
598+
id: result
599+
uses: actions/github-script@v8
600+
env:
601+
INPUT_INDEX: ${{ matrix.index }}
602+
INPUT_VERIFY-COMMANDS: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }}
603+
INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }}
604+
INPUT_ARTIFACT-NAME: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.artifact-name-suffix }}
605+
with:
606+
script: |
607+
const inpIndex = core.getInput('index');
608+
const inpVerifyCommands = core.getInput('verify-commands');
609+
const inpImageDigest = core.getInput('image-digest');
610+
const inpArtifactName = core.getInput('artifact-name');
611+
612+
const result = {
613+
verifyCommands: inpVerifyCommands,
614+
imageDigest: inpImageDigest,
615+
artifactName: inpArtifactName
616+
}
617+
core.info(JSON.stringify(result, null, 2));
618+
619+
core.setOutput(`result_${inpIndex}`, JSON.stringify(result));
620+
621+
post:
622+
runs-on: ubuntu-latest
623+
outputs:
624+
cosign-version: ${{ env.COSIGN_VERSION }}
625+
cosign-verify-commands: ${{ steps.set.outputs.cosign-verify-commands }}
626+
artifact-name: ${{ inputs.artifact-name }}
627+
needs:
628+
- build
629+
steps:
630+
-
631+
name: Docker meta
632+
id: meta
633+
if: ${{ inputs.output == 'registry' }}
634+
uses: docker/metadata-action@v5
635+
with:
636+
images: ${{ inputs.meta-images }}
637+
tags: ${{ inputs.meta-tags }}
638+
flavor: ${{ inputs.meta-flavor }}
639+
labels: ${{ inputs.meta-labels }}
640+
annotations: ${{ inputs.meta-annotations }}
641+
bake-target: ${{ inputs.meta-bake-target }}
642+
-
643+
name: Login to registry
644+
if: ${{ inputs.output == 'registry' }}
645+
# TODO: switch to docker/login-action when OIDC is supported
646+
uses: crazy-max/docker-login-action@dockerhub-oidc
647+
with:
648+
registry-auth: ${{ secrets.registry-auths }}
649+
-
650+
name: Set up Docker Buildx
651+
if: ${{ inputs.output == 'registry' }}
652+
uses: docker/setup-buildx-action@v3
653+
with:
654+
version: latest
655+
buildkitd-flags: --debug
437656
-
438657
name: Create manifest
439658
if: ${{ inputs.output == 'registry' }}
440659
uses: actions/github-script@v8
441660
env:
442661
INPUT_IMAGE-NAMES: ${{ inputs.meta-images }}
443662
INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }}
444-
INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }}
663+
INPUT_BUILD-OUTPUTS: ${{ needs.build.outputs }}
445664
with:
446665
script: |
447-
for (const imageName of core.getMultilineInput('image-names')) {
666+
const inpImageNames = core.getMultilineInput('image-names');
667+
const inpTagNames = core.getMultilineInput('tag-names');
668+
const inpBuildOutputs = JSON.parse(core.getInput('build-outputs'));
669+
670+
const digests = [];
671+
for (const key of Object.keys(inpBuildOutputs)) {
672+
const output = JSON.parse(inpBuildOutputs[key]);
673+
if (output.imageDigest) {
674+
digests.push(output.imageDigest);
675+
}
676+
}
677+
678+
for (const imageName of inpImageNames) {
448679
let createArgs = ['buildx', 'imagetools', 'create'];
449-
for (const tag of core.getMultilineInput('tag-names')) {
680+
for (const tag of inpTagNames) {
450681
createArgs.push('-t', `${imageName}:${tag}`);
451682
}
452-
createArgs.push(core.getInput('image-digest'));
683+
for (const digest of digests) {
684+
createArgs.push(digest);
685+
}
453686
await exec.getExecOutput('docker', createArgs, {
454687
ignoreReturnCode: true
455688
}).then(res => {
@@ -459,15 +692,20 @@ jobs:
459692
});
460693
}
461694
-
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
695+
name: Set outputs
696+
id: set
697+
if: ${{ inputs.output != 'cacheonly' }}
698+
uses: actions/github-script@v8
699+
env:
700+
INPUT_BUILD-OUTPUTS: ${{ needs.build.outputs }}
470701
with:
471-
name: ${{ inputs.artifact-name }}
472-
path: ${{ env.LOCAL_EXPORT_DIR }}
473-
if-no-files-found: error
702+
script: |
703+
const inpBuildOutputs = JSON.parse(core.getInput('build-outputs'));
704+
const verifyCommands = [];
705+
for (const key of Object.keys(inpBuildOutputs)) {
706+
const output = JSON.parse(inpBuildOutputs[key]);
707+
if (output.verifyCommands) {
708+
verifyCommands.push(output.verifyCommands);
709+
}
710+
}
711+
core.setOutput('cosign-verify-commands', verifyCommands.join('\n'));

0 commit comments

Comments
 (0)