name: build on: workflow_call: inputs: runner: type: string description: "GitHub-hosted Linux runner label or platform mapping to build on" required: false default: | default=ubuntu-24.04 linux/arm=ubuntu-24.04-arm linux/arm64=ubuntu-24.04-arm distribute: type: boolean description: "Whether to distribute the build across multiple runners (one platform per runner)" required: false default: true fail-fast: type: boolean description: "Whether to cancel all in-progress and queued jobs in the matrix if any job fails" required: false default: false setup-qemu: type: boolean description: "Runs the setup-qemu-action step to install QEMU static binaries" required: false default: false artifact-name: type: string description: "Name of the uploaded GitHub artifact (for local output)" required: false default: 'docker-github-builder-assets' artifact-upload: type: boolean description: "Upload build output GitHub artifact (for local output)" required: false default: false annotations: type: string description: "List of annotations to set to the image (for image output)" required: false build-args: type: string description: "List of build-time variables" required: false cache: type: boolean description: "Enable cache to GitHub Actions cache backend" required: false default: false cache-scope: type: string description: "Which scope cache object belongs to if cache enabled (defaults to target name if set)" required: false cache-mode: type: string description: "Cache layers to export if cache enabled (min or max)" required: false default: 'min' context: type: string description: "Context to build from in the Git working tree" required: false default: . file: type: string description: "Path to the Dockerfile" required: false labels: type: string description: "List of labels for an image (for image output)" required: false output: type: string description: "Build output destination (one of image or local). Unlike the build-push-action, it only accepts image or local. The reusable workflow takes care of setting the outputs attribute" required: true platforms: type: string description: "List of target platforms to build" required: false push: type: boolean description: "Push image to the registry (for image output)" required: false default: false sbom: type: boolean description: "Generate SBOM attestation for the build" required: false default: false shm-size: type: string description: "Size of /dev/shm (e.g., 2g)" required: false sign: type: string description: "Sign attestation manifest for image output or artifacts for local output, can be one of auto, true or false. The auto mode will enable signing if push is enabled for pushing the image or if artifact-upload is enabled for uploading the local build output as GitHub Artifact" required: false default: auto target: type: string description: "Sets the target stage to build" required: false ulimit: type: string description: "Ulimit options (e.g., nofile=1024:1024)" required: false # docker/metadata-action set-meta-annotations: type: boolean description: "Append OCI Image Format Specification annotations generated by docker/metadata-action" required: false default: false set-meta-labels: type: boolean description: "Append OCI Image Format Specification labels generated by docker/metadata-action" required: false default: false meta-images: type: string description: "List of images to use as base name for tags (required for image output)" required: false meta-tags: type: string description: "List of tags as key-value pair attributes" required: false meta-flavor: type: string description: "Flavor defines a global behavior for meta-tags" required: false secrets: registry-auths: description: "Raw authentication to registries, defined as YAML objects (for image output)" required: false github-token: description: "GitHub Token used to authenticate against the repository for Git context" required: false outputs: meta-json: description: "Metadata JSON output (for image output)" value: ${{ jobs.finalize.outputs.meta-json }} cosign-version: description: "Cosign version used for verification" value: ${{ jobs.finalize.outputs.cosign-version }} cosign-verify-commands: description: "Cosign verify commands" value: ${{ jobs.finalize.outputs.cosign-verify-commands }} artifact-name: description: "Name of the uploaded artifact (for local output)" value: ${{ jobs.finalize.outputs.artifact-name }} digest: description: "Digest of the image pushed or artifact uploaded" value: ${{ jobs.finalize.outputs.digest }} output-type: description: "Build output type" value: ${{ jobs.finalize.outputs.output-type }} signed: description: "Whether attestations manifests or artifacts were signed" value: ${{ jobs.finalize.outputs.signed }} env: BUILDX_VERSION: "v0.34.1" BUILDKIT_IMAGE: "moby/buildkit:v0.30.0" SBOM_IMAGE: "docker/buildkit-syft-scanner:1.11.0" BINFMT_IMAGE: "tonistiigi/binfmt:qemu-v10.2.1-65" RUNTIME_MODULE: "@docker/github-builder-runtime@0.91.0" RUNTIME_INSTALL_ARGS: | --loglevel=error --no-save --package-lock=false --ignore-scripts --omit=dev --prefer-offline --fund=false --audit=false COSIGN_VERSION: "v3.0.6" LOCAL_EXPORT_DIR: "/tmp/buildx-output" MATRIX_SIZE_LIMIT: "20" BUILDX_METADATA_PROVENANCE: "false" BUILDX_SEND_GIT_QUERY_AS_INPUT: "true" NPM_CONFIG_FETCH_RETRIES: "5" jobs: prepare: runs-on: ubuntu-24.04 outputs: includes: ${{ steps.set.outputs.includes }} metaImages: ${{ steps.set.outputs.metaImages }} sign: ${{ steps.set.outputs.sign }} privateRepo: ${{ steps.set.outputs.privateRepo }} ghaCacheSign: ${{ steps.set.outputs.ghaCacheSign }} steps: - name: Install dependencies uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_RUNTIME-MODULE: ${{ env.RUNTIME_MODULE }} INPUT_RUNTIME-INSTALL-ARGS: ${{ env.RUNTIME_INSTALL_ARGS }} with: script: | const npmArgs = ['install', ...core.getMultilineInput('runtime-install-args'), core.getInput('runtime-module')]; const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const exitCode = await exec.exec('npm', npmArgs, {ignoreReturnCode: true}); if (exitCode === 0) { return; } if (attempt === maxAttempts) { core.setFailed(`npm install failed after ${maxAttempts} attempts`); return; } const retryDelayMs = attempt * 50; core.info(`npm install failed with exit code ${exitCode}; retrying in ${retryDelayMs}ms`); await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } - name: Install Cosign uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_COSIGN-VERSION: ${{ env.COSIGN_VERSION }} with: script: | const { Cosign } = require('@docker/github-builder-runtime/lib/cosign/cosign'); const { Install } = require('@docker/github-builder-runtime/lib/cosign/install'); const inpCosignVersion = core.getInput('cosign-version'); const cosignInstall = new Install(); const cosignBinPath = await cosignInstall.download({ version: core.getInput('cosign-version'), ghaNoCache: true, skipState: true, verifySignature: true }); const cosignPath = await cosignInstall.install(cosignBinPath); const cosign = new Cosign(); await cosign.printVersion(); - name: Check dependencies signatures uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_IMAGES: | ${{ env.BUILDKIT_IMAGE }} ${{ env.SBOM_IMAGE }} ${{ env.BINFMT_IMAGE }} with: script: | const { OCI } = require('@docker/github-builder-runtime/lib/oci/oci'); const { Sigstore } = require('@docker/github-builder-runtime/lib/sigstore/sigstore'); const sigstore = new Sigstore(); for (const image of core.getMultilineInput('images')) { await core.group(`Verifying ${image}`, async () => { try { await sigstore.verifyImageAttestations(image, { certificateIdentityRegexp: `^https://github.com/docker/github-builder(-experimental)?/.github/workflows/bake.yml.*$`, platform: OCI.defaultPlatform() }); } catch (error) { core.setFailed(error); return; } }); } - name: Set outputs id: set uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_MATRIX-SIZE-LIMIT: ${{ env.MATRIX_SIZE_LIMIT }} INPUT_META-IMAGES: ${{ inputs.meta-images }} INPUT_RUNNER: ${{ inputs.runner }} INPUT_DISTRIBUTE: ${{ inputs.distribute }} INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} INPUT_OUTPUT: ${{ inputs.output }} INPUT_PLATFORMS: ${{ inputs.platforms }} INPUT_PUSH: ${{ inputs.push }} INPUT_SIGN: ${{ inputs.sign }} with: script: | const { GitHub } = require('@docker/github-builder-runtime/lib/github/github'); const { Util } = require('@docker/github-builder-runtime/lib/util'); const inpMatrixSizeLimit = parseInt(core.getInput('matrix-size-limit'), 10); const inpMetaImages = core.getMultilineInput('meta-images'); const actionsIdTokenSet = !!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL; const inpRunner = core.getMultilineInput('runner'); const inpDistribute = core.getBooleanInput('distribute'); const inpArtifactUpload = core.getBooleanInput('artifact-upload'); const inpPlatforms = Util.getInputList('platforms'); const inpOutput = core.getInput('output'); const inpPush = core.getBooleanInput('push'); const inpSign = core.getInput('sign'); const parseRunnerConfig = value => { const lines = value.map(line => line.trim()).filter(line => line.length > 0); if (lines.length === 0) { throw new Error('runner input cannot be empty'); } if (lines.length === 1 && !lines[0].includes('=')) { if (lines[0] === 'auto') { core.warning('The runner input value "auto" is deprecated; use a runner mapping with default=ubuntu-24.04, linux/arm=ubuntu-24.04-arm, and linux/arm64=ubuntu-24.04-arm instead'); return { defaultRunner: 'ubuntu-24.04', rules: [ {pattern: 'linux/arm', runner: 'ubuntu-24.04-arm'}, {pattern: 'linux/arm64', runner: 'ubuntu-24.04-arm'} ] }; } if (lines[0] === 'amd64') { core.warning('The runner input value "amd64" is deprecated; use runner=ubuntu-24.04 instead'); return { defaultRunner: 'ubuntu-24.04', rules: [] }; } if (lines[0] === 'arm64') { core.warning('The runner input value "arm64" is deprecated; use runner=ubuntu-24.04-arm instead'); return { defaultRunner: 'ubuntu-24.04-arm', rules: [] }; } return { defaultRunner: lines[0], rules: [] }; } const rules = []; let defaultRunner; for (const line of lines) { const idx = line.indexOf('='); if (idx === -1) { throw new Error(`Invalid runner mapping: ${line}`); } const pattern = line.substring(0, idx).trim(); const runner = line.substring(idx + 1).trim(); if (!pattern) { throw new Error('Runner mapping pattern cannot be empty'); } if (!runner) { throw new Error(`Runner mapping value cannot be empty for ${pattern}`); } if (pattern === 'default') { defaultRunner = runner; continue; } if (pattern.split('/').some(part => part.length === 0)) { throw new Error(`Runner mapping pattern is not a valid platform prefix: ${pattern}`); } rules.push({pattern, runner}); } if (!defaultRunner) { throw new Error('Runner mapping must define a default runner'); } return { defaultRunner, rules }; }; const matchesPlatformPrefix = (pattern, platform) => { const patternParts = pattern.split('/'); const platformParts = platform.split('/'); return patternParts.length <= platformParts.length && patternParts.every((part, index) => part === platformParts[index]); }; const resolveRunner = (runnerConfig, platform) => { if (!platform) { return runnerConfig.defaultRunner; } let match; for (const rule of runnerConfig.rules) { if (!matchesPlatformPrefix(rule.pattern, platform)) { continue; } const specificity = rule.pattern.split('/').length; if (!match || specificity >= match.specificity) { match = {runner: rule.runner, specificity}; } } return match ? match.runner : runnerConfig.defaultRunner; }; let runnerConfig; try { runnerConfig = parseRunnerConfig(inpRunner); } catch (error) { core.setFailed(error.message); return; } await core.group(`Set runner config`, async () => { core.info(JSON.stringify(runnerConfig, null, 2)); }); const sign = inpSign === 'auto' ? (inpOutput === 'image' && inpPush) || (inpOutput === 'local' && inpArtifactUpload) : inpSign === 'true'; if (inpOutput === 'local' && inpPush) { core.warning(`push is ignored when output is local`); } else if (inpOutput === 'image' && inpArtifactUpload) { core.warning(`artifact-upload is ignored when output is image`); } if (inpOutput === 'image' && !inpPush && sign) { core.setFailed(`signing attestation manifests requires push to be enabled`); return; } if (inpDistribute && inpPlatforms.length > inpMatrixSizeLimit) { core.setFailed(`Platforms to build exceed matrix size limit of ${inpMatrixSizeLimit}`); return; } const privateRepo = GitHub.context.payload.repository?.private ?? false; await core.group(`Set privateRepo output`, async () => { core.info(`privateRepo: ${privateRepo}`); core.setOutput('privateRepo', privateRepo); }); const metaImages = inpMetaImages.map(image => image.toLowerCase()); await core.group(`Set metaImages output`, async () => { core.info(JSON.stringify(metaImages, null, 2)); core.setOutput('metaImages', metaImages.join('\n')); }); await core.group(`Set includes output`, async () => { let includes = []; if (!inpDistribute || inpPlatforms.length === 0) { includes.push({ index: 0, runner: resolveRunner(runnerConfig) }); } else { inpPlatforms.forEach((platform, index) => { includes.push({ index: index, platform: platform, runner: resolveRunner(runnerConfig, platform) }); }); } core.info(JSON.stringify(includes, null, 2)); core.setOutput('includes', JSON.stringify(includes)); }); await core.group(`Set sign output`, async () => { core.info(`sign: ${sign}`); core.setOutput('sign', sign); }); await core.group(`Set ghaCacheSign output`, async () => { const ghaCacheSign = actionsIdTokenSet ? 'true' : 'false'; core.info(`ghaCacheSign: ${ghaCacheSign}`); core.setOutput('ghaCacheSign', ghaCacheSign); }); build: runs-on: ${{ matrix.runner }} needs: - prepare strategy: fail-fast: ${{ inputs.fail-fast }} matrix: include: ${{ fromJson(needs.prepare.outputs.includes) }} outputs: # needs predefined outputs as we can't use dynamic ones atm: https://github.com/actions/runner/pull/2477 # 20 is the maximum number of platforms supported by our matrix strategy result_0: ${{ steps.result.outputs.result_0 }} result_1: ${{ steps.result.outputs.result_1 }} result_2: ${{ steps.result.outputs.result_2 }} result_3: ${{ steps.result.outputs.result_3 }} result_4: ${{ steps.result.outputs.result_4 }} result_5: ${{ steps.result.outputs.result_5 }} result_6: ${{ steps.result.outputs.result_6 }} result_7: ${{ steps.result.outputs.result_7 }} result_8: ${{ steps.result.outputs.result_8 }} result_9: ${{ steps.result.outputs.result_9 }} result_10: ${{ steps.result.outputs.result_10 }} result_11: ${{ steps.result.outputs.result_11 }} result_12: ${{ steps.result.outputs.result_12 }} result_13: ${{ steps.result.outputs.result_13 }} result_14: ${{ steps.result.outputs.result_14 }} result_15: ${{ steps.result.outputs.result_15 }} result_16: ${{ steps.result.outputs.result_16 }} result_17: ${{ steps.result.outputs.result_17 }} result_18: ${{ steps.result.outputs.result_18 }} result_19: ${{ steps.result.outputs.result_19 }} steps: - name: Require GitHub-hosted Linux runner uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_RUNNER-ENVIRONMENT: ${{ runner.environment }} INPUT_RUNNER-OS: ${{ runner.os }} with: script: | const runnerEnvironment = core.getInput('runner-environment'); const runnerOS = core.getInput('runner-os'); if (runnerEnvironment !== 'github-hosted' || runnerOS !== 'Linux') { core.setFailed(`This workflow requires a GitHub-hosted Linux runner, got: environment=${runnerEnvironment || 'unknown'}, os=${runnerOS || 'unknown'}`); } - name: Install dependencies uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_RUNTIME-MODULE: ${{ env.RUNTIME_MODULE }} INPUT_RUNTIME-INSTALL-ARGS: ${{ env.RUNTIME_INSTALL_ARGS }} with: script: | const npmArgs = ['install', ...core.getMultilineInput('runtime-install-args'), core.getInput('runtime-module')]; const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const exitCode = await exec.exec('npm', npmArgs, {ignoreReturnCode: true}); if (exitCode === 0) { return; } if (attempt === maxAttempts) { core.setFailed(`npm install failed after ${maxAttempts} attempts`); return; } const retryDelayMs = attempt * 50; core.info(`npm install failed with exit code ${exitCode}; retrying in ${retryDelayMs}ms`); await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } - name: Docker meta id: meta if: ${{ inputs.output == 'image' }} uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ needs.prepare.outputs.metaImages }} tags: ${{ inputs.meta-tags }} flavor: ${{ inputs.meta-flavor }} labels: ${{ inputs.meta-labels }} annotations: ${{ inputs.meta-annotations }} - name: Set up QEMU uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 if: ${{ inputs.setup-qemu }} with: image: ${{ env.BINFMT_IMAGE }} cache-image: false - name: Set GitHub runtime outputs id: github-runtime uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const idTokenRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN || ''; const idTokenRequestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL || ''; if (idTokenRequestToken) { core.setSecret(idTokenRequestToken); } if (idTokenRequestUrl) { core.setSecret(idTokenRequestUrl); } core.setOutput('actions-id-token-request-token', idTokenRequestToken); core.setOutput('actions-id-token-request-url', idTokenRequestUrl); - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: version: ${{ env.BUILDX_VERSION }} cache-binary: false buildkitd-flags: --debug driver-opts: | image=${{ env.BUILDKIT_IMAGE }} env.ACTIONS_ID_TOKEN_REQUEST_TOKEN=${{ steps.github-runtime.outputs.actions-id-token-request-token }} env.ACTIONS_ID_TOKEN_REQUEST_URL=${{ steps.github-runtime.outputs.actions-id-token-request-url }} buildkitd-config-inline: | [cache] [cache.gha] [cache.gha.sign] command = [${{ needs.prepare.outputs.ghaCacheSign == 'true' && '"ghacache-sign-script.sh"' || '' }}] [cache.gha.verify] required = ${{ needs.prepare.outputs.ghaCacheSign }} [cache.gha.verify.policy] timestampThreshold = 1 tlogThreshold = ${{ needs.prepare.outputs.privateRepo == 'true' && '0' || '1' }} subjectAlternativeName = "https://github.com/docker/github-builder/.github/workflows/build.yml*" githubWorkflowRepository = "${{ github.repository }}" issuer = "https://token.actions.githubusercontent.com" runnerEnvironment = "github-hosted" sourceRepositoryURI = "${{ github.server_url }}/${{ github.repository }}" - name: Install Cosign if: ${{ needs.prepare.outputs.sign == 'true' || inputs.cache }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_COSIGN-VERSION: ${{ env.COSIGN_VERSION }} INPUT_BUILDER-NAME: ${{ steps.buildx.outputs.name }} INPUT_GHA-CACHE-SIGN-SCRIPT: | #!/bin/sh set -e # Create temporary files tmp_dir=$(mktemp -d) out_file="$tmp_dir/bundle" in_file="$tmp_dir/blob" signing_config="$tmp_dir/signing-config.json" trap 'rm -rf "$tmp_dir"' EXIT cat > "$in_file" no_default_rekor= if [ "${{ needs.prepare.outputs.privateRepo }}" = "true" ]; then no_default_rekor="--no-default-rekor=true" fi set -x # Create signing config COSIGN_EXPERIMENTAL=1 cosign signing-config create \ --with-default-services=true \ ${no_default_rekor:+$no_default_rekor} \ --out="$signing_config" # Sign with cosign cosign sign-blob \ --yes \ --oidc-provider github-actions \ --new-bundle-format \ --signing-config "$signing_config" \ --bundle "$out_file" \ "$in_file" # Output bundle to stdout cat "$out_file" with: script: | const fs = require('fs'); const os = require('os'); const path = require('path'); const { Buildx } = require('@docker/github-builder-runtime/lib/buildx/buildx'); const { Cosign } = require('@docker/github-builder-runtime/lib/cosign/cosign'); const { Install } = require('@docker/github-builder-runtime/lib/cosign/install'); const inpCosignVersion = core.getInput('cosign-version'); const inpBuilderName = core.getInput('builder-name'); const inpGHACacheSignScript = core.getInput('gha-cache-sign-script'); const cosignInstall = new Install(); const cosignBinPath = await cosignInstall.download({ version: core.getInput('cosign-version'), ghaNoCache: true, skipState: true, verifySignature: true }); const cosignPath = await cosignInstall.install(cosignBinPath); const cosign = new Cosign(); await cosign.printVersion(); const containerName = `${Buildx.containerNamePrefix}${inpBuilderName}0`; const ghaCacheSignScriptPath = path.join(os.tmpdir(), `ghacache-sign-script.sh`); core.info(`Writing GitHub Actions cache sign script to ${ghaCacheSignScriptPath}`); await fs.writeFileSync(ghaCacheSignScriptPath, inpGHACacheSignScript, {mode: 0o700}); core.info(`Copying GitHub Actions cache sign script to BuildKit container ${containerName}`); await exec.exec('docker', [ 'cp', ghaCacheSignScriptPath, `${containerName}:/usr/bin/ghacache-sign-script.sh` ]); core.info(`Copying cosign binary to BuildKit container ${containerName}`); await exec.exec('docker', [ 'cp', cosignPath, `${containerName}:/usr/bin/cosign` ]); - name: Prepare id: prepare uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_PLATFORM: ${{ matrix.platform }} INPUT_SBOM-IMAGE: ${{ env.SBOM_IMAGE }} INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} INPUT_DISTRIBUTE: ${{ inputs.distribute }} INPUT_ANNOTATIONS: ${{ inputs.annotations }} INPUT_BUILD-ARGS: ${{ inputs.build-args }} INPUT_CACHE: ${{ inputs.cache }} INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }} INPUT_CACHE-MODE: ${{ inputs.cache-mode }} INPUT_LABELS: ${{ inputs.labels }} INPUT_CONTEXT: ${{ inputs.context }} INPUT_OUTPUT: ${{ inputs.output }} INPUT_PLATFORMS: ${{ inputs.platforms }} INPUT_PUSH: ${{ inputs.push }} INPUT_SBOM: ${{ inputs.sbom }} INPUT_TARGET: ${{ inputs.target }} INPUT_META-IMAGES: ${{ needs.prepare.outputs.metaImages }} INPUT_META-VERSION: ${{ steps.meta.outputs.version }} INPUT_META-TAGS: ${{ steps.meta.outputs.tags }} INPUT_SET-META-ANNOTATIONS: ${{ inputs.set-meta-annotations }} INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }} INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }} INPUT_META-LABELS: ${{ steps.meta.outputs.labels }} with: script: | 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'); const inpPlatform = core.getInput('platform'); const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; core.setOutput('platform-pair-suffix', platformPairSuffix); const inpSbomImage = core.getInput('sbom-image'); const inpLocalExportDir = core.getInput('local-export-dir'); const inpDistribute = core.getBooleanInput('distribute'); const inpAnnotations = core.getInput('annotations'); const inpBuildArgs = core.getInput('build-args'); const inpCache = core.getBooleanInput('cache'); const inpCacheScope = core.getInput('cache-scope'); const inpCacheMode = core.getInput('cache-mode'); const inpContext = core.getInput('context'); const inpLabels = core.getInput('labels'); const inpOutput = core.getInput('output'); const inpPlatforms = core.getInput('platforms'); const inpPush = core.getBooleanInput('push'); const inpSbom = core.getBooleanInput('sbom'); const inpTarget = core.getInput('target'); const inpMetaImages = core.getMultilineInput('meta-images'); const inpMetaVersion = core.getInput('meta-version'); const inpMetaTags = core.getMultilineInput('meta-tags'); const inpSetMetaAnnotations = core.getBooleanInput('set-meta-annotations'); const inpMetaAnnotations = core.getMultilineInput('meta-annotations'); const inpSetMetaLabels = core.getBooleanInput('set-meta-labels'); const inpMetaLabels = core.getMultilineInput('meta-labels'); const meta = { version: inpMetaVersion, tags: inpMetaTags }; const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta}); const toMultilineInput = value => value.split(/\r?\n/).map(line => line.trim()).filter(Boolean); const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {} : {'fetch-by-commit': 'true'}; const buildContext = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs}); core.setOutput('context', buildContext); switch (inpOutput) { case 'image': if (inpMetaImages.length == 0) { core.setFailed('meta-images is required when output is image'); return; } core.setOutput('output', `type=image,"name=${inpMetaImages.join(',')}",oci-artifact=true,push-by-digest=true,name-canonical=true,push=${inpPush}`); break; case 'local': core.setOutput('output', `type=local,platform-split=true,dest=${inpLocalExportDir}`); break; default: core.setFailed(`Invalid output: ${inpOutput}`); return; } if (inpPlatform) { core.setOutput('platform', inpPlatform); } else if (!inpDistribute && inpPlatforms) { core.setOutput('platform', inpPlatforms); } core.setOutput('sbom', inpSbom ? `generator=${inpSbomImage}` : 'false'); if (inpCache) { core.setOutput('cache-from', `type=gha,scope=${inpCacheScope || inpTarget || 'buildkit'}${platformPairSuffix}`); core.setOutput('cache-to', `type=gha,ignore-error=true,scope=${inpCacheScope || inpTarget || 'buildkit'}${platformPairSuffix},mode=${inpCacheMode}`); } let annotations; let labels; let buildArgs; try { annotations = toMultilineInput(renderTemplate(inpAnnotations)); labels = toMultilineInput(renderTemplate(inpLabels)); buildArgs = renderTemplate(inpBuildArgs); } catch (err) { core.setFailed(`Failed to render Handlebars template: ${err.message}`); return; } if (inpSetMetaAnnotations && inpMetaAnnotations.length > 0) { annotations.push(...inpMetaAnnotations); } core.setOutput('annotations', annotations.join('\n')); if (inpSetMetaLabels && inpMetaLabels.length > 0) { labels.push(...inpMetaLabels); } core.setOutput('labels', labels.join('\n')); core.setOutput('build-args', buildArgs); if (GitHub.context.payload.repository?.private ?? false) { // if this is a private repository, we set min provenance mode core.setOutput('provenance', Build.resolveProvenanceAttrs(`mode=min,version=v1`)); } else { // for a public repository, we set max provenance mode core.setOutput('provenance', Build.resolveProvenanceAttrs(`mode=max,version=v1`)); } - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' }} uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry-auth: ${{ secrets.registry-auths }} - name: Build id: build uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: annotations: ${{ steps.prepare.outputs.annotations }} build-args: ${{ steps.prepare.outputs.build-args }} cache-from: ${{ steps.prepare.outputs.cache-from }} cache-to: ${{ steps.prepare.outputs.cache-to }} context: ${{ steps.prepare.outputs.context }} file: ${{ inputs.file }} labels: ${{ steps.prepare.outputs.labels }} outputs: ${{ steps.prepare.outputs.output }} platforms: ${{ steps.prepare.outputs.platform }} provenance: ${{ steps.prepare.outputs.provenance }} sbom: ${{ steps.prepare.outputs.sbom }} secret-envs: GIT_AUTH_TOKEN=GIT_AUTH_TOKEN shm-size: ${{ inputs.shm-size }} target: ${{ inputs.target }} ulimit: ${{ inputs.ulimit }} env: BUILDKIT_MULTI_PLATFORM: 1 GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }} - name: Login to registry for signing if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }} uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry-auth: ${{ secrets.registry-auths }} env: DOCKER_LOGIN_SCOPE_DISABLED: true # make sure the scope feature is disabled to avoid interfering with cosign OIDC login - name: Signing attestation manifests id: signing-attestation-manifests if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_IMAGE-NAMES: ${{ needs.prepare.outputs.metaImages }} INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | const { Sigstore } = require('@docker/github-builder-runtime/lib/sigstore/sigstore'); const inpImageNames = core.getMultilineInput('image-names'); const inpImageDigest = core.getInput('image-digest'); const sigstore = new Sigstore(); const signResults = await sigstore.signAttestationManifests({ imageNames: inpImageNames, imageDigest: inpImageDigest, retryOnManifestUnknown: true }); const verifyResults = await sigstore.verifySignedManifests(signResults, { certificateIdentityRegexp: `^https://github.com/docker/github-builder/.github/workflows/build.yml.*$`, retryOnManifestUnknown: true }); await core.group(`Verify commands`, async () => { const verifyCommands = []; for (const [attestationRef, verifyResult] of Object.entries(verifyResults)) { const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} ${attestationRef}`; core.info(cmd); verifyCommands.push(cmd); } core.setOutput('verify-commands', verifyCommands.join('\n')); }); - name: Signing local artifacts id: signing-local-artifacts if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'local' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_EXPORT_DIR }} with: script: | const path = require('path'); const { Sigstore } = require('@docker/github-builder-runtime/lib/sigstore/sigstore'); const inplocalExportDir = core.getInput('local-output-dir'); const sigstore = new Sigstore(); const signResults = await sigstore.signProvenanceBlobs({ localExportDir: inplocalExportDir }); const verifyResults = await sigstore.verifySignedArtifacts(signResults, { certificateIdentityRegexp: `^https://github.com/docker/github-builder/.github/workflows/build.yml.*$` }); await core.group(`Verify commands`, async () => { const verifyCommands = []; for (const [artifactPath, verifyResult] of Object.entries(verifyResults)) { const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} --bundle ${path.relative(inplocalExportDir, verifyResult.bundlePath)} ${path.relative(inplocalExportDir, artifactPath)}`; core.info(cmd); verifyCommands.push(cmd); } core.setOutput('verify-commands', verifyCommands.join('\n')); }); - name: List local output if: ${{ inputs.output == 'local' }} run: | tree -nh ${{ env.LOCAL_EXPORT_DIR }} - name: Upload artifact if: ${{ inputs.output == 'local' && inputs.artifact-upload }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix || '0' }} path: ${{ env.LOCAL_EXPORT_DIR }} if-no-files-found: error - name: Set result output id: result uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_INDEX: ${{ matrix.index }} INPUT_VERIFY-COMMANDS: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }} INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} INPUT_ARTIFACT-NAME: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix }} INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} with: script: | const inpIndex = core.getInput('index'); const inpVerifyCommands = core.getInput('verify-commands'); const inpImageDigest = core.getInput('image-digest'); const inpArtifactName = core.getInput('artifact-name'); const inpArtifactUpload = core.getBooleanInput('artifact-upload'); const inpSigned = core.getBooleanInput('signed'); const result = { verifyCommands: inpVerifyCommands, imageDigest: inpImageDigest, artifactName: inpArtifactUpload ? inpArtifactName : '', signed: inpSigned } core.info(JSON.stringify(result, null, 2)); core.setOutput(`result_${inpIndex}`, JSON.stringify(result)); finalize: runs-on: ubuntu-24.04 outputs: meta-json: ${{ steps.meta.outputs.json }} cosign-version: ${{ env.COSIGN_VERSION }} cosign-verify-commands: ${{ steps.set.outputs.cosign-verify-commands }} artifact-name: ${{ inputs.artifact-upload && inputs.artifact-name || '' }} digest: ${{ steps.manifest.outputs.digest || steps.artifact.outputs.artifact-digest }} output-type: ${{ inputs.output }} signed: ${{ needs.prepare.outputs.sign }} needs: - prepare - build steps: - name: Install dependencies uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_RUNTIME-MODULE: ${{ env.RUNTIME_MODULE }} INPUT_RUNTIME-INSTALL-ARGS: ${{ env.RUNTIME_INSTALL_ARGS }} with: script: | const npmArgs = ['install', ...core.getMultilineInput('runtime-install-args'), core.getInput('runtime-module')]; const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const exitCode = await exec.exec('npm', npmArgs, {ignoreReturnCode: true}); if (exitCode === 0) { return; } if (attempt === maxAttempts) { core.setFailed(`npm install failed after ${maxAttempts} attempts`); return; } const retryDelayMs = attempt * 50; core.info(`npm install failed with exit code ${exitCode}; retrying in ${retryDelayMs}ms`); await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } - name: Docker meta id: meta if: ${{ inputs.output == 'image' }} uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ needs.prepare.outputs.metaImages }} tags: ${{ inputs.meta-tags }} flavor: ${{ inputs.meta-flavor }} labels: ${{ inputs.meta-labels }} annotations: ${{ inputs.meta-annotations }} - name: Login to registry if: ${{ inputs.push && inputs.output == 'image' }} uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry-auth: ${{ secrets.registry-auths }} env: DOCKER_LOGIN_SCOPE_DISABLED: true # FIXME: scope feature is not yet supported by Buildx imagetools command - name: Set up Docker Buildx if: ${{ inputs.push && inputs.output == 'image' }} uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: version: ${{ env.BUILDX_VERSION }} buildkitd-flags: --debug driver-opts: image=${{ env.BUILDKIT_IMAGE }} cache-binary: false - name: Create manifest id: manifest if: ${{ inputs.output == 'image' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_PUSH: ${{ inputs.push }} INPUT_IMAGE-NAMES: ${{ needs.prepare.outputs.metaImages }} INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }} INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} INPUT_ANNOTATIONS: ${{ inputs.annotations }} INPUT_SET-META-ANNOTATIONS: ${{ inputs.set-meta-annotations }} INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }} with: script: | const { ImageTools } = require('@docker/github-builder-runtime/lib/buildx/imagetools'); const inpPush = core.getBooleanInput('push'); const inpImageNames = core.getMultilineInput('image-names'); const inpTagNames = core.getMultilineInput('tag-names'); const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); const inpAnnotations = core.getMultilineInput('annotations'); const inpSetMetaAnnotations = core.getBooleanInput('set-meta-annotations'); const inpMetaAnnotations = core.getMultilineInput('meta-annotations'); const toIndexAnnotation = annotation => { const keyEnd = annotation.indexOf('='); const rawKey = keyEnd === -1 ? annotation : annotation.substring(0, keyEnd); const rawValue = keyEnd === -1 ? '' : annotation.substring(keyEnd); const typeSeparator = rawKey.indexOf(':'); if (typeSeparator !== -1) { const typeExpr = rawKey.substring(0, typeSeparator); const key = rawKey.substring(typeSeparator + 1); const hasKnownType = typeExpr.split(',').map(type => type.replace(/\[.*\]$/, '')).some(type => ['manifest', 'index', 'manifest-descriptor', 'index-descriptor'].includes(type)); if (hasKnownType) { return `index:${key}${rawValue}`; } } return `index:${annotation}`; }; if (inpSetMetaAnnotations && inpMetaAnnotations.length > 0) { inpAnnotations.push(...inpMetaAnnotations); } const indexAnnotations = inpAnnotations.filter(annotation => annotation.length > 0).map(toIndexAnnotation); const digests = []; for (const key of Object.keys(inpBuildOutputs)) { const output = JSON.parse(inpBuildOutputs[key]); if (output.imageDigest) { digests.push(output.imageDigest); } } if (digests.length === 0) { core.setFailed('No image digests found from build outputs'); return; } let digest; for (const imageName of inpImageNames) { const tags = []; for (const tag of inpTagNames) { tags.push(`${imageName}:${tag}`); } const result = await new ImageTools().create({ sources: digests, tags: tags, annotations: indexAnnotations, skipExec: !inpPush }); if (inpPush) { if (!result.digest) { core.setFailed('Failed to create manifest, no digest returned'); return; } digest = result.digest; core.info(`Manifest created: ${imageName}@${result.digest}`); } } if (digest) { core.setOutput('digest', digest); } - name: Merge artifacts id: artifact if: ${{ inputs.output == 'local' && inputs.artifact-upload }} uses: actions/upload-artifact/merge@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact-name }} pattern: ${{ inputs.artifact-name }}* delete-merged: true - name: Set outputs id: set uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} with: script: | const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); const inpSigned = core.getBooleanInput('signed'); if (inpSigned) { const verifyCommands = []; for (const key of Object.keys(inpBuildOutputs)) { const output = JSON.parse(inpBuildOutputs[key]); if (output.verifyCommands) { verifyCommands.push(output.verifyCommands); } } core.setOutput('cosign-verify-commands', verifyCommands.join('\n')); }