@@ -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
129129env :
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
134135jobs :
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