@@ -3,6 +3,11 @@ name: bake
33on :
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
129134env :
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
134140jobs :
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