From 9353c30afe16673e2942c83e3d399a24c8357a62 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:47:36 +0200 Subject: [PATCH 01/72] build reusable workflow Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..50c61435 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,78 @@ +name: build + +on: + workflow_call: + inputs: + push: + type: boolean + description: "Push image to registry" + required: false + default: false + meta-image: + type: string + description: "Image to use as base name for tags" + required: true + meta-tags: + type: string + description: "List of tags as key-value pair attributes" + required: false + meta-flavor: + type: string + description: "Flavors to apply" + required: false + login-registry: + type: string + description: "Server address of Docker registry. If not set then will default to Docker Hub" + required: false + login-username: + type: string + description: "Username used to log against the Docker registry" + required: false + login-ecr: + type: string + description: "Specifies whether the given registry is ECR (auto, true or false)" + default: 'auto' + required: false + secrets: + login-password: + description: "Password or personal access token used to log against the Docker registry" + required: false + github-token: + description: "GitHub Token used to authenticate against a repository for Git context" + required: false + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # needed for pushing the images to GitHub Packages + steps: + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to registry + uses: docker/login-action@v3 + if: ${{ inputs.push }} + with: + registry: ${{ inputs.login-registry }} + username: ${{ inputs.login-username }} + password: ${{ secrets.login-password }} + ecr: ${{ inputs.login-ecr }} + - + name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.meta-image }} + tags: ${{ inputs.meta-tags }} + flavor: ${{ inputs.meta-flavor }} + - + name: Build + uses: docker/build-push-action@v6 + with: + tags: ${{ steps.meta.outputs.tags }} + push: ${{ inputs.push }} + sbom: true + github-token: ${{ secrets.github-token || github.token }} From 764760bc7536d6210f9ad02c97956cb98d1f32e6 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:51:41 +0200 Subject: [PATCH 02/72] build: set meta labels and annotations Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50c61435..dd301ed0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,16 @@ on: description: "Push image to registry" required: false default: false + set-meta-annotations: + type: boolean + description: "Set metadata-action annotations" + required: false + default: false + set-meta-labels: + type: boolean + description: "Set metadata-action labels" + required: false + default: false meta-image: type: string description: "Image to use as base name for tags" @@ -20,6 +30,14 @@ on: type: string description: "Flavors to apply" required: false + meta-labels: + type: string + description: "List of custom labels" + required: false + meta-annotations: + type: string + description: "List of custom annotations" + required: false login-registry: type: string description: "Server address of Docker registry. If not set then will default to Docker Hub" @@ -68,6 +86,8 @@ jobs: images: ${{ inputs.meta-image }} tags: ${{ inputs.meta-tags }} flavor: ${{ inputs.meta-flavor }} + labels: ${{ inputs.meta-labels }} + annotations: ${{ inputs.meta-annotations }} - name: Build uses: docker/build-push-action@v6 @@ -75,4 +95,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} push: ${{ inputs.push }} sbom: true + labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} + annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} From 5f1cddf8b1797444981f238cb00caa683a9bead5 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:53:12 +0200 Subject: [PATCH 03/72] build: build args Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd301ed0..82d3185c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,10 @@ on: description: "Specifies whether the given registry is ECR (auto, true or false)" default: 'auto' required: false + build-args: + type: string + description: "List of build-time variables" + required: false secrets: login-password: description: "Password or personal access token used to log against the Docker registry" @@ -92,6 +96,7 @@ jobs: name: Build uses: docker/build-push-action@v6 with: + build-args: ${{ inputs.build-args }} tags: ${{ steps.meta.outputs.tags }} push: ${{ inputs.push }} sbom: true From 717a2c9fc03a0a6274bbd1e5e3364b7ea1874905 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:54:34 +0200 Subject: [PATCH 04/72] build: build platforms Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82d3185c..09d5989f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,10 @@ on: type: string description: "List of build-time variables" required: false + build-platforms: + type: string + description: "List of target platforms to build" + required: false secrets: login-password: description: "Password or personal access token used to log against the Docker registry" @@ -97,6 +101,7 @@ jobs: uses: docker/build-push-action@v6 with: build-args: ${{ inputs.build-args }} + platforms: ${{ inputs.build-platforms }} tags: ${{ steps.meta.outputs.tags }} push: ${{ inputs.push }} sbom: true From 0798ce382cd2b2e37547cbe4e9eff91992bc308c Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:59:40 +0200 Subject: [PATCH 05/72] build: install cosign and sign image This is based on cosign example workflow that signs images with GitHub OIDC token. In our case we don't want to sign images but attestations. Next commit will do this. Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09d5989f..611c580a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,7 @@ jobs: permissions: contents: read packages: write # needed for pushing the images to GitHub Packages + id-token: write # needed for signing the images with GitHub OIDC Token steps: - name: Set up Docker Buildx @@ -98,6 +99,7 @@ jobs: annotations: ${{ inputs.meta-annotations }} - name: Build + id: build uses: docker/build-push-action@v6 with: build-args: ${{ inputs.build-args }} @@ -108,3 +110,19 @@ jobs: labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} + - + name: Install cosign + if: ${{ inputs.push }} + uses: sigstore/cosign-installer@v3 + - + name: Sign with GitHub OIDC Token + if: ${{ inputs.push }} + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + images="" + for tag in ${TAGS}; do + images+="${tag}@${DIGEST} " + done + cosign sign --yes ${images} From 61af3a91410364b23fc9da119f6009f065f42cc3 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:00:28 +0200 Subject: [PATCH 06/72] build: use oci-artifact for attestations Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 611c580a..eb0cd7a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,7 +105,7 @@ jobs: build-args: ${{ inputs.build-args }} platforms: ${{ inputs.build-platforms }} tags: ${{ steps.meta.outputs.tags }} - push: ${{ inputs.push }} + outputs: type=image,oci-artifact=true,push=${{ inputs.push }} sbom: true labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} From eadc5521495f17235f74f13913f5eac3c26e64ec Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:02:51 +0200 Subject: [PATCH 07/72] build: step to get attestation manifest digest for signing Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb0cd7a8..8032a061 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,6 +110,31 @@ jobs: labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} + - + name: Get attestations digest + id: attest + uses: actions/github-script@v7 + env: + INPUT_TAGS: ${{ steps.meta.outputs.tags }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + await core.group(`Get attestations digest`, async () => { + await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', getInputList('tags')[0], '--format', '{{json .Manifest}}'], { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + const dt = JSON.parse(res.stdout.trim()); + const attestationDigests = dt.manifests.filter(m => m.annotations && m.annotations['vnd.docker.reference.type'] === 'attestation-manifest').map(m => m.digest); + core.info(JSON.stringify(attestationDigests, null, 2)); + core.setOutput('digests', attestationDigests.join('\n')); + }); + }); - name: Install cosign if: ${{ inputs.push }} From 37b4f8fb69edc2feca0e63621a643e7ad9140bb0 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:03:42 +0200 Subject: [PATCH 08/72] build: only sign attestation manifests Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8032a061..b77c6bf2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -142,12 +142,27 @@ jobs: - name: Sign with GitHub OIDC Token if: ${{ inputs.push }} + uses: actions/github-script@v7 env: - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build.outputs.digest }} - run: | - images="" - for tag in ${TAGS}; do - images+="${tag}@${DIGEST} " - done - cosign sign --yes ${images} + INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + const tags = getInputList('tags'); + const digests = getInputList('digests'); + const images = []; + for (const tag of tags) { + for (const digest of digests) { + images.push(`${tag}@${digest}`); + } + } + await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', ...images], { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + }); From c7eb418d60f53415f8e77063d2183f1262e9339e Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:25:12 +0200 Subject: [PATCH 09/72] build: verify signatures Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b77c6bf2..55825d51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,3 +166,30 @@ jobs: throw new Error(res.stderr); } }); + - + name: Verify signatures + if: ${{ inputs.push }} + uses: actions/github-script@v7 + env: + INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + const tags = getInputList('tags'); + const digests = getInputList('digests'); + const images = []; + for (const tag of tags) { + for (const digest of digests) { + images.push(`${tag}@${digest}`); + } + } + await exec.getExecOutput('cosign', ['-d', 'verify', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', ...images], { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + core.warning(`Verification failed: ${res.stderr}`); + } + }); From d64047465bc1ebe1182c0bda4cda452cf3c0cdbd Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:37:26 +0200 Subject: [PATCH 10/72] build: verify each signature individually Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55825d51..8c50098d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -183,13 +183,15 @@ jobs: const images = []; for (const tag of tags) { for (const digest of digests) { - images.push(`${tag}@${digest}`); + await core.group(`Verifying ${tag}@${digest}`, async () => { + await exec.getExecOutput('cosign', ['-d', 'verify', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + core.warning(`Verification failed for ${tag}@${digest}: ${res.stderr}`); + } + }); + }); } } - await exec.getExecOutput('cosign', ['-d', 'verify', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', ...images], { - ignoreReturnCode: true - }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - core.warning(`Verification failed: ${res.stderr}`); - } - }); + From 80c78b24aed63cb50f0286c026788ebd6e15e9a1 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:54:24 +0200 Subject: [PATCH 11/72] build: build-sbom input Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c50098d..80936d2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,10 @@ on: type: string description: "List of target platforms to build" required: false + build-sbom: + type: string + description: "Generate SBOM attestation for the build (shorthand for --attest=type=sbom)" + required: false secrets: login-password: description: "Password or personal access token used to log against the Docker registry" @@ -106,7 +110,7 @@ jobs: platforms: ${{ inputs.build-platforms }} tags: ${{ steps.meta.outputs.tags }} outputs: type=image,oci-artifact=true,push=${{ inputs.push }} - sbom: true + sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} @@ -194,4 +198,3 @@ jobs: }); } } - From 82581c85fc097c08c827ca61125bb40d8481bcad Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:47:51 +0200 Subject: [PATCH 12/72] build: push by digest and create manifest in last step Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 42 +++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80936d2b..862e0e38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,6 +79,19 @@ jobs: packages: write # needed for pushing the images to GitHub Packages id-token: write # needed for signing the images with GitHub OIDC Token steps: + - + name: Validate + uses: actions/github-script@v7 + env: + INPUT_META-IMAGE: ${{ inputs.meta-image }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + if (getInputList('meta-image').length > 1) { + throw new Error('Only one meta-image is allowed'); + } - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -108,8 +121,7 @@ jobs: with: build-args: ${{ inputs.build-args }} platforms: ${{ inputs.build-platforms }} - tags: ${{ steps.meta.outputs.tags }} - outputs: type=image,oci-artifact=true,push=${{ inputs.push }} + outputs: type=image,"name=${{ inputs.meta-image }}",oci-artifact=true,push-by-digest=true,name-canonical=true,push=${{ inputs.push }} sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} @@ -198,3 +210,29 @@ jobs: }); } } + - + name: Create manifest + if: ${{ inputs.push }} + uses: actions/github-script@v7 + env: + INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + + let createArgs = ['buildx', 'imagetools', 'create']; + for (const tag of getInputList('tags')) { + createArgs.push('-t', tag); + } + createArgs.push(core.getInput('image-digest')); + + await exec.getExecOutput('docker', createArgs, { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + }); From 2da5a1f2a6730efd0b9e5a01b16a106b0d487a8f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:07:36 +0200 Subject: [PATCH 13/72] build: use getMultilineInput from core toolkit Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 862e0e38..1a0ab2dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,10 +86,7 @@ jobs: INPUT_META-IMAGE: ${{ inputs.meta-image }} with: script: | - function getInputList(name) { - return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; - } - if (getInputList('meta-image').length > 1) { + if (core.getMultilineInput('meta-image').length > 1) { throw new Error('Only one meta-image is allowed'); } - @@ -134,11 +131,8 @@ jobs: INPUT_TAGS: ${{ steps.meta.outputs.tags }} with: script: | - function getInputList(name) { - return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; - } await core.group(`Get attestations digest`, async () => { - await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', getInputList('tags')[0], '--format', '{{json .Manifest}}'], { + await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', core.getMultilineInput('tags')[0], '--format', '{{json .Manifest}}'], { ignoreReturnCode: true, silent: true }).then(res => { @@ -164,11 +158,8 @@ jobs: INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} with: script: | - function getInputList(name) { - return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; - } - const tags = getInputList('tags'); - const digests = getInputList('digests'); + const tags = core.getMultilineInput('tags'); + const digests = core.getMultilineInput('digests'); const images = []; for (const tag of tags) { for (const digest of digests) { @@ -191,12 +182,8 @@ jobs: INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} with: script: | - function getInputList(name) { - return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; - } - const tags = getInputList('tags'); - const digests = getInputList('digests'); - const images = []; + const tags = core.getMultilineInput('tags'); + const digests = core.getMultilineInput('digests'); for (const tag of tags) { for (const digest of digests) { await core.group(`Verifying ${tag}@${digest}`, async () => { @@ -219,12 +206,8 @@ jobs: INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | - function getInputList(name) { - return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; - } - let createArgs = ['buildx', 'imagetools', 'create']; - for (const tag of getInputList('tags')) { + for (const tag of core.getMultilineInput('tags')) { createArgs.push('-t', tag); } createArgs.push(core.getInput('image-digest')); From 2abd569c37956a7ba56f1da7fd1342634f902708 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:15:41 +0200 Subject: [PATCH 14/72] build: sign and verify only with referrers API Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a0ab2dc..9fb67f82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,6 +156,7 @@ jobs: env: INPUT_TAGS: ${{ steps.meta.outputs.tags }} INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} + COSIGN_EXPERIMENTAL: 1 with: script: | const tags = core.getMultilineInput('tags'); @@ -166,7 +167,7 @@ jobs: images.push(`${tag}@${digest}`); } } - await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', ...images], { + await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', '--registry-referrers-mode', 'oci-1-1', ...images], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { @@ -180,6 +181,7 @@ jobs: env: INPUT_TAGS: ${{ steps.meta.outputs.tags }} INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} + COSIGN_EXPERIMENTAL: 1 with: script: | const tags = core.getMultilineInput('tags'); @@ -187,7 +189,7 @@ jobs: for (const tag of tags) { for (const digest of digests) { await core.group(`Verifying ${tag}@${digest}`, async () => { - await exec.getExecOutput('cosign', ['-d', 'verify', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { + await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { From 934c9032f56f2ead218d958f36940098e4c476d4 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:00:42 +0200 Subject: [PATCH 15/72] build: enforce github-actions provider for signing Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9fb67f82..32ab49cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -167,7 +167,7 @@ jobs: images.push(`${tag}@${digest}`); } } - await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', '--registry-referrers-mode', 'oci-1-1', ...images], { + await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', ...images], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { From a6745e60f5e2dd7a36c5afc38d848e2298c077b9 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:20:03 +0200 Subject: [PATCH 16/72] testing dockerhub oidc through login-action Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32ab49cb..cc60f0b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,7 +94,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to registry - uses: docker/login-action@v3 + uses: crazy-max/docker-login-action@dockerhub-oidc if: ${{ inputs.push }} with: registry: ${{ inputs.login-registry }} From 2235615e48bf54e23f141a7d831867f3350f9b07 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:32:50 +0200 Subject: [PATCH 17/72] build: fix since pushing by digest Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc60f0b0..352a8139 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -128,11 +128,12 @@ jobs: id: attest uses: actions/github-script@v7 env: - INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_IMAGE-NAME: ${{ inputs.meta-image }} + INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | await core.group(`Get attestations digest`, async () => { - await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', core.getMultilineInput('tags')[0], '--format', '{{json .Manifest}}'], { + await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', `${core.getInput('image-name')}@${core.getInput('image-digest')}`, '--format', '{{json .Manifest}}'], { ignoreReturnCode: true, silent: true }).then(res => { From e9fcf9a0514719095a5d471289edc43f3836c4fd Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:39:37 +0200 Subject: [PATCH 18/72] build: show image manifest Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 352a8139..24d1ce48 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -124,7 +124,7 @@ jobs: annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} - - name: Get attestations digest + name: Get attestations manifest digest id: attest uses: actions/github-script@v7 env: @@ -132,20 +132,22 @@ jobs: INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | - await core.group(`Get attestations digest`, async () => { + let manifest = {}; + await core.group(`Get manifest`, async () => { await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', `${core.getInput('image-name')}@${core.getInput('image-digest')}`, '--format', '{{json .Manifest}}'], { - ignoreReturnCode: true, - silent: true + ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { throw new Error(res.stderr); } - const dt = JSON.parse(res.stdout.trim()); - const attestationDigests = dt.manifests.filter(m => m.annotations && m.annotations['vnd.docker.reference.type'] === 'attestation-manifest').map(m => m.digest); - core.info(JSON.stringify(attestationDigests, null, 2)); - core.setOutput('digests', attestationDigests.join('\n')); + manifest = JSON.parse(res.stdout.trim()); }); }); + await core.group(`Get attestations manifest digest`, async () => { + const attestationDigests = manifest.manifests.filter(m => m.annotations && m.annotations['vnd.docker.reference.type'] === 'attestation-manifest').map(m => m.digest); + core.info(JSON.stringify(attestationDigests, null, 2)); + core.setOutput('digests', attestationDigests.join('\n')); + }); - name: Install cosign if: ${{ inputs.push }} From 3092e487d25eca23b2b9b9c573ccc75d7e31f6f3 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:16:32 +0200 Subject: [PATCH 19/72] build: wait 3sec before verifying Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d1ce48..c12044ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,6 +192,7 @@ jobs: for (const tag of tags) { for (const digest of digests) { await core.group(`Verifying ${tag}@${digest}`, async () => { + await new Promise(resolve => setTimeout(resolve, 3000)); await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { ignoreReturnCode: true }).then(res => { From 5b3110196f28c69ac6fc03935547e6c2f97a5b82 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:22:27 +0200 Subject: [PATCH 20/72] build: fail if we can't verify the signature Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c12044ca..3159364f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -196,8 +196,8 @@ jobs: await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { ignoreReturnCode: true }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - core.warning(`Verification failed for ${tag}@${digest}: ${res.stderr}`); + if (res.exitCode != 0) { + core.setFailed(`Verification failed for ${tag}@${digest}`); } }); }); From f0f7db89658dcb825be3057885e0f901b10a77dd Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:25:47 +0200 Subject: [PATCH 21/72] build: don't verify each tag, just digest Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3159364f..2c38ee0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -182,26 +182,24 @@ jobs: if: ${{ inputs.push }} uses: actions/github-script@v7 env: - INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_IMAGE-NAME: ${{ inputs.meta-image }} INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} COSIGN_EXPERIMENTAL: 1 with: script: | - const tags = core.getMultilineInput('tags'); const digests = core.getMultilineInput('digests'); - for (const tag of tags) { - for (const digest of digests) { - await core.group(`Verifying ${tag}@${digest}`, async () => { - await new Promise(resolve => setTimeout(resolve, 3000)); - await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', `${tag}@${digest}`], { - ignoreReturnCode: true - }).then(res => { - if (res.exitCode != 0) { - core.setFailed(`Verification failed for ${tag}@${digest}`); - } - }); + for (const digest of digests) { + const image = `${core.getInput('image-name')}@${digest}`; + await core.group(`Verifying ${image}`, async () => { + await new Promise(resolve => setTimeout(resolve, 3000)); + await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', image], { + ignoreReturnCode: true + }).then(res => { + if (res.exitCode != 0) { + core.setFailed(`Verification failed for ${image}`); + } }); - } + }); } - name: Create manifest From 00ecfae2ca184a948fe5ed933f51ec0cf85e28f0 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:48:18 +0200 Subject: [PATCH 22/72] build: switch push input to build-output one Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c38ee0a..3de63d2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,11 +3,6 @@ name: build on: workflow_call: inputs: - push: - type: boolean - description: "Push image to registry" - required: false - default: false set-meta-annotations: type: boolean description: "Set metadata-action annotations" @@ -55,6 +50,11 @@ on: type: string description: "List of build-time variables" required: false + build-output: + type: string + description: "Build outout destination (one of cacheonly, registry)" + default: 'cacheonly' + required: false build-platforms: type: string description: "List of target platforms to build" @@ -80,14 +80,29 @@ jobs: id-token: write # needed for signing the images with GitHub OIDC Token steps: - - name: Validate + name: Prepare + id: prepare uses: actions/github-script@v7 env: INPUT_META-IMAGE: ${{ inputs.meta-image }} + INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | - if (core.getMultilineInput('meta-image').length > 1) { - throw new Error('Only one meta-image is allowed'); + const metaImage = core.getMultilineInput('meta-image'); + const buildOutput = core.getInput('build-output'); + + if (core.getMultilineInput('meta-image').length != 1) { + core.setFailed('Only one meta-image is allowed'); + } + switch (buildOutput) { + case 'cacheonly': + core.setOutput('output', 'type=cacheonly'); + break; + case 'registry': + core.setOutput('output', `type=registry,"name=${metaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); + break; + default: + core.setFailed(`Invalid build-output: ${buildOutput}`); } - name: Set up Docker Buildx @@ -95,7 +110,6 @@ jobs: - name: Login to registry uses: crazy-max/docker-login-action@dockerhub-oidc - if: ${{ inputs.push }} with: registry: ${{ inputs.login-registry }} username: ${{ inputs.login-username }} @@ -118,7 +132,7 @@ jobs: with: build-args: ${{ inputs.build-args }} platforms: ${{ inputs.build-platforms }} - outputs: type=image,"name=${{ inputs.meta-image }}",oci-artifact=true,push-by-digest=true,name-canonical=true,push=${{ inputs.push }} + outputs: ${{ steps.prepare.outputs.output }} sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} @@ -126,6 +140,7 @@ jobs: - name: Get attestations manifest digest id: attest + if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: INPUT_IMAGE-NAME: ${{ inputs.meta-image }} @@ -150,11 +165,10 @@ jobs: }); - name: Install cosign - if: ${{ inputs.push }} uses: sigstore/cosign-installer@v3 - name: Sign with GitHub OIDC Token - if: ${{ inputs.push }} + if: ${{ inputs.build-output != 'cacheonly' }} uses: actions/github-script@v7 env: INPUT_TAGS: ${{ steps.meta.outputs.tags }} @@ -179,7 +193,7 @@ jobs: }); - name: Verify signatures - if: ${{ inputs.push }} + if: ${{ inputs.build-output != 'cacheonly' }} uses: actions/github-script@v7 env: INPUT_IMAGE-NAME: ${{ inputs.meta-image }} @@ -203,7 +217,7 @@ jobs: } - name: Create manifest - if: ${{ inputs.push }} + if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: INPUT_TAGS: ${{ steps.meta.outputs.tags }} From cbb18247b75025df424daf90c7b6d08412876014 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:50:31 +0200 Subject: [PATCH 23/72] build: remove timeout on verification Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3de63d2e..37d658a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -203,16 +203,13 @@ jobs: script: | const digests = core.getMultilineInput('digests'); for (const digest of digests) { - const image = `${core.getInput('image-name')}@${digest}`; - await core.group(`Verifying ${image}`, async () => { - await new Promise(resolve => setTimeout(resolve, 3000)); - await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', image], { - ignoreReturnCode: true - }).then(res => { - if (res.exitCode != 0) { - core.setFailed(`Verification failed for ${image}`); - } - }); + const manifest = `${core.getInput('image-name')}@${digest}`; + await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', manifest], { + ignoreReturnCode: true + }).then(res => { + if (res.exitCode != 0) { + core.warning(`Signature verification failed for ${manifest}`); + } }); } - @@ -229,7 +226,6 @@ jobs: createArgs.push('-t', tag); } createArgs.push(core.getInput('image-digest')); - await exec.getExecOutput('docker', createArgs, { ignoreReturnCode: true }).then(res => { From 85225d96c87234f2ef2ee8fe74cdd75164cc059e Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:00:59 +0200 Subject: [PATCH 24/72] build: format exec Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37d658a1..70f10621 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,7 +149,10 @@ jobs: script: | let manifest = {}; await core.group(`Get manifest`, async () => { - await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', `${core.getInput('image-name')}@${core.getInput('image-digest')}`, '--format', '{{json .Manifest}}'], { + await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', + `${core.getInput('image-name')}@${core.getInput('image-digest')}`, + '--format', '{{json .Manifest}}' + ], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { @@ -184,7 +187,12 @@ jobs: images.push(`${tag}@${digest}`); } } - await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', ...images], { + await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', + '--oidc-provider', 'github-actions', + '--oidc-issuer', 'https://token.actions.githubusercontent.com', + '--registry-referrers-mode', 'oci-1-1', + ...images + ], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { From f078345de0fbca05ab26f0f1a8027f877349f45f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:59:35 +0200 Subject: [PATCH 25/72] build: retry signature verification Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70f10621..a95fc0ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -209,16 +209,33 @@ jobs: COSIGN_EXPERIMENTAL: 1 with: script: | + const maxRetries = 100; const digests = core.getMultilineInput('digests'); for (const digest of digests) { const manifest = `${core.getInput('image-name')}@${digest}`; - await exec.getExecOutput('cosign', ['-d', 'verify', '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', manifest], { - ignoreReturnCode: true - }).then(res => { - if (res.exitCode != 0) { - core.warning(`Signature verification failed for ${manifest}`); - } - }); + let attempt = 1; + let res; + let retry = true; + do { + core.info(`#####`); + core.info(`##### Verifying ${manifest} (attempt ${attempt}/${maxRetries})`); + core.info(`#####`); + await new Promise(resolve => setTimeout(resolve, 5000)); + res = await exec.getExecOutput('cosign', [ + '-d', 'verify', + '--experimental-oci11', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + manifest + ], { + ignoreReturnCode: true + }); + if (res.exitCode != 10) retry = false; + attempt++; + } while (retry && attempt < maxRetries); + if (res.exitCode != 0) { + core.setFailed(`Signature verification failed for ${manifest}`); + } } - name: Create manifest From c69dd2b1f82c29b83b07d6a2ea0e2e4cc18a8661 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:17:48 +0200 Subject: [PATCH 26/72] build: support local output and sign attestations blob Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 110 +++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a95fc0ee..3ea4c642 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ on: meta-image: type: string description: "Image to use as base name for tags" - required: true + required: false meta-tags: type: string description: "List of tags as key-value pair attributes" @@ -52,7 +52,7 @@ on: required: false build-output: type: string - description: "Build outout destination (one of cacheonly, registry)" + description: "Build output destination (one of cacheonly, registry, local)" default: 'cacheonly' required: false build-platforms: @@ -71,6 +71,9 @@ on: description: "GitHub Token used to authenticate against a repository for Git context" required: false +env: + LOCAL_OUTPUT_DIR: /tmp/buildx-output + jobs: build: runs-on: ubuntu-latest @@ -84,23 +87,30 @@ jobs: id: prepare uses: actions/github-script@v7 env: + INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} INPUT_META-IMAGE: ${{ inputs.meta-image }} INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | + const localOutputDir = core.getInput('local-output-dir'); const metaImage = core.getMultilineInput('meta-image'); const buildOutput = core.getInput('build-output'); - if (core.getMultilineInput('meta-image').length != 1) { - core.setFailed('Only one meta-image is allowed'); - } switch (buildOutput) { case 'cacheonly': core.setOutput('output', 'type=cacheonly'); break; case 'registry': + if (metaImage.length == 0) { + core.setFailed('meta-image is required when build-output is registry'); + } else if (metaImage.length > 1) { + core.setFailed('Only one meta-image is supported'); + } core.setOutput('output', `type=registry,"name=${metaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); break; + case 'local': + core.setOutput('output', `type=local,dest=${localOutputDir}`); + break; default: core.setFailed(`Invalid build-output: ${buildOutput}`); } @@ -109,6 +119,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to registry + if: ${{ inputs.build-output == 'registry' }} uses: crazy-max/docker-login-action@dockerhub-oidc with: registry: ${{ inputs.login-registry }} @@ -118,6 +129,7 @@ jobs: - name: Docker meta id: meta + if: ${{ inputs.build-output == 'registry' }} uses: docker/metadata-action@v5 with: images: ${{ inputs.meta-image }} @@ -133,6 +145,7 @@ jobs: build-args: ${{ inputs.build-args }} platforms: ${{ inputs.build-platforms }} outputs: ${{ steps.prepare.outputs.output }} + provenance: mode=max sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} @@ -170,8 +183,8 @@ jobs: name: Install cosign uses: sigstore/cosign-installer@v3 - - name: Sign with GitHub OIDC Token - if: ${{ inputs.build-output != 'cacheonly' }} + name: Sign attestation manifest (registry) + if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: INPUT_TAGS: ${{ steps.meta.outputs.tags }} @@ -200,8 +213,41 @@ jobs: } }); - - name: Verify signatures - if: ${{ inputs.build-output != 'cacheonly' }} + name: Sign attestation manifest (local) + if: ${{ inputs.build-output == 'local' }} + uses: actions/github-script@v7 + env: + INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} + COSIGN_EXPERIMENTAL: 1 + with: + script: | + const localOutputDir = core.getInput('local-output-dir'); + + const globber = await glob.create([`${localOutputDir}/provenance.json`, `${localOutputDir}/sbom*spdx.json`].join('\n')); + const attestations = await globber.glob(); + if (attestations.length == 0) { + core.setFailed('No attestation files found'); + return; + } + + for (const attestation of attestations) { + const res = await exec.getExecOutput('cosign', [ + '-d', 'sign-blob', '--yes', + '--oidc-provider', 'github-actions', + '--oidc-issuer', 'https://token.actions.githubusercontent.com', + '--output-signature', `${attestation}.sig`, + '--output-certificate', `${attestation}.crt`, + attestation + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + } + - + name: Verify signatures (registry) + if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: INPUT_IMAGE-NAME: ${{ inputs.meta-image }} @@ -237,6 +283,39 @@ jobs: core.setFailed(`Signature verification failed for ${manifest}`); } } + - + name: Verify signatures (local) + if: ${{ inputs.build-output == 'local' }} + uses: actions/github-script@v7 + env: + INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} + COSIGN_EXPERIMENTAL: 1 + with: + script: | + const localOutputDir = core.getInput('local-output-dir'); + + const globber = await glob.create([`${localOutputDir}/provenance.json`, `${localOutputDir}/sbom*spdx.json`].join('\n')); + const attestations = await globber.glob(); + if (attestations.length == 0) { + core.setFailed('No attestation files found'); + return; + } + + for (const attestation of attestations) { + const res = await exec.getExecOutput('cosign', [ + '-d', 'verify-blob', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--signature', `${attestation}.sig`, + '--certificate', `${attestation}.crt`, + attestation + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + } - name: Create manifest if: ${{ inputs.build-output == 'registry' }} @@ -258,3 +337,16 @@ jobs: throw new Error(res.stderr); } }); + - + name: List output + if: ${{ inputs.build-output == 'local' }} + run: | + tree -nh ${{ env.LOCAL_OUTPUT_DIR }} + - + name: Upload artifact + if: ${{ inputs.build-output == 'local' }} + uses: actions/upload-artifact@v4 + with: + name: docker-github-builder-output + path: ${{ env.LOCAL_OUTPUT_DIR }} + if-no-files-found: error From b52998e850972e0e0554ce01c63a13df0ddcd328 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:13:59 +0200 Subject: [PATCH 27/72] build: install latest buildx and enable buildkit debug Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ea4c642..46e7deba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,6 +117,9 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + version: latest + buildkitd-flags: --debug - name: Login to registry if: ${{ inputs.build-output == 'registry' }} From cd349f257de533535630a511f7bbddd8ab024644 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:14:46 +0200 Subject: [PATCH 28/72] build: pin cosign installer action Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46e7deba..dee2a4b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,7 +184,7 @@ jobs: }); - name: Install cosign - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - name: Sign attestation manifest (registry) if: ${{ inputs.build-output == 'registry' }} From 8a7c5db2fefb9a6ea44e0dc351d66a7e7466c974 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:36:13 +0200 Subject: [PATCH 29/72] build: multi registry auths Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dee2a4b3..39025a83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,19 +33,6 @@ on: type: string description: "List of custom annotations" required: false - login-registry: - type: string - description: "Server address of Docker registry. If not set then will default to Docker Hub" - required: false - login-username: - type: string - description: "Username used to log against the Docker registry" - required: false - login-ecr: - type: string - description: "Specifies whether the given registry is ECR (auto, true or false)" - default: 'auto' - required: false build-args: type: string description: "List of build-time variables" @@ -64,8 +51,8 @@ on: description: "Generate SBOM attestation for the build (shorthand for --attest=type=sbom)" required: false secrets: - login-password: - description: "Password or personal access token used to log against the Docker registry" + registry-auths: + description: "Registry authentication details as YAML objects" required: false github-token: description: "GitHub Token used to authenticate against a repository for Git context" @@ -123,12 +110,9 @@ jobs: - name: Login to registry if: ${{ inputs.build-output == 'registry' }} - uses: crazy-max/docker-login-action@dockerhub-oidc + uses: crazy-max/docker-login-action@multi-and-dockerhub-oidc with: - registry: ${{ inputs.login-registry }} - username: ${{ inputs.login-username }} - password: ${{ secrets.login-password }} - ecr: ${{ inputs.login-ecr }} + logins: ${{ secrets.registry-auths }} - name: Docker meta id: meta From f24613e43400059c0544f908ca051fc67f92d05b Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:04:22 +0200 Subject: [PATCH 30/72] build: update login-action Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39025a83..2c01a3e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,9 +110,10 @@ jobs: - name: Login to registry if: ${{ inputs.build-output == 'registry' }} - uses: crazy-max/docker-login-action@multi-and-dockerhub-oidc + # TODO: switch to docker/login-action when OIDC is supported + uses: crazy-max/docker-login-action@dockerhub-oidc with: - logins: ${{ secrets.registry-auths }} + registry-auth: ${{ secrets.registry-auths }} - name: Docker meta id: meta From 55bf27e54fcf58acb76d9b390f872bb02c2bd61b Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:13:11 +0200 Subject: [PATCH 31/72] build: list output after build for local build Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c01a3e5..35ab424a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,11 @@ jobs: labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} + - + name: List output + if: ${{ inputs.build-output == 'local' }} + run: | + tree -nh ${{ env.LOCAL_OUTPUT_DIR }} - name: Get attestations manifest digest id: attest From ea18033fd863245b31a2c5d05c201d56a50706c6 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:24:51 +0200 Subject: [PATCH 32/72] build: fix signing for multi platform with local output Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35ab424a..8db3f3a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -216,7 +216,7 @@ jobs: script: | const localOutputDir = core.getInput('local-output-dir'); - const globber = await glob.create([`${localOutputDir}/provenance.json`, `${localOutputDir}/sbom*spdx.json`].join('\n')); + const globber = await glob.create([`${localOutputDir}/**/provenance.json`, `${localOutputDir}/**/sbom*spdx.json`].join('\n')); const attestations = await globber.glob(); if (attestations.length == 0) { core.setFailed('No attestation files found'); @@ -287,7 +287,7 @@ jobs: script: | const localOutputDir = core.getInput('local-output-dir'); - const globber = await glob.create([`${localOutputDir}/provenance.json`, `${localOutputDir}/sbom*spdx.json`].join('\n')); + const globber = await glob.create([`${localOutputDir}/**/provenance.json`, `${localOutputDir}/**/sbom*spdx.json`].join('\n')); const attestations = await globber.glob(); if (attestations.length == 0) { core.setFailed('No attestation files found'); From e4e069b7735e656bf530258a215ec801fd0d06f1 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:00:07 +0200 Subject: [PATCH 33/72] build: install cosign from sources Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8db3f3a9..98611670 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,8 +173,16 @@ jobs: core.setOutput('digests', attestationDigests.join('\n')); }); - - name: Install cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + name: Set up Go for cosign + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 + - + name: Build and install cosign + run: | + cd /tmp + GOBIN=/usr/local/bin go install github.com/sigstore/cosign/v2/cmd/cosign@v2.6.0 + cosign version - name: Sign attestation manifest (registry) if: ${{ inputs.build-output == 'registry' }} From e663908d15d45aaf5df1edba8a1e2ea8aff528d5 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:55:42 +0200 Subject: [PATCH 34/72] build: clone cosign repo Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98611670..ed123f6b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -172,16 +172,24 @@ jobs: core.info(JSON.stringify(attestationDigests, null, 2)); core.setOutput('digests', attestationDigests.join('\n')); }); + - + name: Checkout cosign + uses: actions/checkout@v5 + with: + repository: sigstore/cosign + ref: v2.6.0 + path: cosign-tmp - name: Set up Go for cosign - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 + cache-dependency-path: cosign-tmp/go.sum - name: Build and install cosign + working-directory: cosign-tmp run: | - cd /tmp - GOBIN=/usr/local/bin go install github.com/sigstore/cosign/v2/cmd/cosign@v2.6.0 + make install cosign version - name: Sign attestation manifest (registry) From 8a99b29daf5defa5e797eec2df4d9a5b4c09527c Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:24:21 +0200 Subject: [PATCH 35/72] build: use cosign fork with debug on write Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed123f6b..85f21823 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -176,8 +176,8 @@ jobs: name: Checkout cosign uses: actions/checkout@v5 with: - repository: sigstore/cosign - ref: v2.6.0 + repository: crazy-max/cosign + ref: debug-oci-write path: cosign-tmp - name: Set up Go for cosign From d8d8b7eb61b9c1bae5f4379606619aaaa0bb35c5 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:03:35 +0200 Subject: [PATCH 36/72] build: cosign 2.6.0 ref Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85f21823..052ed86d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -177,7 +177,7 @@ jobs: uses: actions/checkout@v5 with: repository: crazy-max/cosign - ref: debug-oci-write + ref: 2.6.0 path: cosign-tmp - name: Set up Go for cosign From 0860319baee6150c092a81fc393e01246f3d9144 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:18:53 +0200 Subject: [PATCH 37/72] build: use cosign installer Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 052ed86d..62f46560 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,24 +173,27 @@ jobs: core.setOutput('digests', attestationDigests.join('\n')); }); - - name: Checkout cosign - uses: actions/checkout@v5 - with: - repository: crazy-max/cosign - ref: 2.6.0 - path: cosign-tmp - - - name: Set up Go for cosign - uses: actions/setup-go@v6 - with: - go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 - cache-dependency-path: cosign-tmp/go.sum - - - name: Build and install cosign - working-directory: cosign-tmp - run: | - make install - cosign version + name: Install cosign + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 +# - +# name: Checkout cosign +# uses: actions/checkout@v5 +# with: +# repository: crazy-max/cosign +# ref: 2.6.0 +# path: cosign-tmp +# - +# name: Set up Go for cosign +# uses: actions/setup-go@v6 +# with: +# go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 +# cache-dependency-path: cosign-tmp/go.sum +# - +# name: Build and install cosign +# working-directory: cosign-tmp +# run: | +# make install +# cosign version - name: Sign attestation manifest (registry) if: ${{ inputs.build-output == 'registry' }} From 7f3ba6cf0cd1e2f21333187ec2d8ef21c49a9f49 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:23:41 +0200 Subject: [PATCH 38/72] build: use cosign 2.6.0-debug-oci-write branch Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62f46560..257ced9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -172,28 +172,28 @@ jobs: core.info(JSON.stringify(attestationDigests, null, 2)); core.setOutput('digests', attestationDigests.join('\n')); }); - - - name: Install cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 -# - -# name: Checkout cosign -# uses: actions/checkout@v5 -# with: -# repository: crazy-max/cosign -# ref: 2.6.0 -# path: cosign-tmp -# - -# name: Set up Go for cosign -# uses: actions/setup-go@v6 -# with: -# go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 -# cache-dependency-path: cosign-tmp/go.sum # - -# name: Build and install cosign -# working-directory: cosign-tmp -# run: | -# make install -# cosign version +# name: Install cosign +# uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + - + name: Checkout cosign + uses: actions/checkout@v5 + with: + repository: crazy-max/cosign + ref: 2.6.0-debug-oci-write + path: cosign-tmp + - + name: Set up Go for cosign + uses: actions/setup-go@v6 + with: + go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 + cache-dependency-path: cosign-tmp/go.sum + - + name: Build and install cosign + working-directory: cosign-tmp + run: | + make install + cosign version - name: Sign attestation manifest (registry) if: ${{ inputs.build-output == 'registry' }} From dfdfca06fafc697886adaa57bb568ecc835355ef Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:27:50 +0200 Subject: [PATCH 39/72] build: use cosign 2.6.0-fix-oci-manifest branch Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 257ced9b..36126d81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -180,7 +180,7 @@ jobs: uses: actions/checkout@v5 with: repository: crazy-max/cosign - ref: 2.6.0-debug-oci-write + ref: 2.6.0-fix-oci-manifest path: cosign-tmp - name: Set up Go for cosign From 81636c539cc8f119427f3631bc4a2a9097b60eea Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:45:09 +0200 Subject: [PATCH 40/72] build: buildkit latest image Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36126d81..47ca283f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,6 +107,8 @@ jobs: with: version: latest buildkitd-flags: --debug + driver-opts: | + image=moby/buildkit:latest - name: Login to registry if: ${{ inputs.build-output == 'registry' }} From 3309be0ee756cc1d3dbb640321075cddb6e14967 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:53:14 +0200 Subject: [PATCH 41/72] build: install cosign earlier Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 47ca283f..97f9690c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,28 @@ jobs: if: ${{ inputs.build-output == 'local' }} run: | tree -nh ${{ env.LOCAL_OUTPUT_DIR }} +# - +# name: Install cosign +# uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + - + name: Checkout cosign + uses: actions/checkout@v5 + with: + repository: crazy-max/cosign + ref: 2.6.0-fix-oci-manifest + path: cosign-tmp + - + name: Set up Go for cosign + uses: actions/setup-go@v6 + with: + go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 + cache-dependency-path: cosign-tmp/go.sum + - + name: Build and install cosign + working-directory: cosign-tmp + run: | + make install + cosign version - name: Get attestations manifest digest id: attest @@ -174,28 +196,6 @@ jobs: core.info(JSON.stringify(attestationDigests, null, 2)); core.setOutput('digests', attestationDigests.join('\n')); }); -# - -# name: Install cosign -# uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - - - name: Checkout cosign - uses: actions/checkout@v5 - with: - repository: crazy-max/cosign - ref: 2.6.0-fix-oci-manifest - path: cosign-tmp - - - name: Set up Go for cosign - uses: actions/setup-go@v6 - with: - go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 - cache-dependency-path: cosign-tmp/go.sum - - - name: Build and install cosign - working-directory: cosign-tmp - run: | - make install - cosign version - name: Sign attestation manifest (registry) if: ${{ inputs.build-output == 'registry' }} From ee15022483a518311f3a31f4261537155f6651cf Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:12:05 +0200 Subject: [PATCH 42/72] build: set github_event as provenance custom field Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97f9690c..ea9077c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,10 +79,25 @@ jobs: INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | + const { randomUUID } = require('crypto'); + const fs = require('fs'); + const os = require('os'); + const path = require('path'); + const localOutputDir = core.getInput('local-output-dir'); const metaImage = core.getMultilineInput('meta-image'); const buildOutput = core.getInput('build-output'); + if (process.env.GITHUB_EVENT_PATH && fs.existsSync(process.env.GITHUB_EVENT_PATH)) { + const githubEvent = { github_event: JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')) }; + const githubEventFile = path.join(os.tmpdir(), `github_event_${randomUUID()}.json`); + fs.writeFileSync(githubEventFile, JSON.stringify(githubEvent, null, 2)); + core.setOutput('github-event-file', githubEventFile); + } else { + core.setFailed('GitHub event file not found'); + return; + } + switch (buildOutput) { case 'cacheonly': core.setOutput('output', 'type=cacheonly'); @@ -105,10 +120,11 @@ jobs: name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - version: latest + version: https://github.com/docker/buildx.git#refs/pull/3452/merge buildkitd-flags: --debug driver-opts: | image=moby/buildkit:latest + mount.${{ steps.prepare.outputs.github-event-file }}=/etc/buildkit/provenance.d/github_event.json - name: Login to registry if: ${{ inputs.build-output == 'registry' }} From 27f850dde0bd684bfe081f4d960cdee068085190 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:34:53 +0200 Subject: [PATCH 43/72] build: let buildx mount github event for provenance Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea9077c2..b70104c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,25 +79,10 @@ jobs: INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | - const { randomUUID } = require('crypto'); - const fs = require('fs'); - const os = require('os'); - const path = require('path'); - const localOutputDir = core.getInput('local-output-dir'); const metaImage = core.getMultilineInput('meta-image'); const buildOutput = core.getInput('build-output'); - if (process.env.GITHUB_EVENT_PATH && fs.existsSync(process.env.GITHUB_EVENT_PATH)) { - const githubEvent = { github_event: JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')) }; - const githubEventFile = path.join(os.tmpdir(), `github_event_${randomUUID()}.json`); - fs.writeFileSync(githubEventFile, JSON.stringify(githubEvent, null, 2)); - core.setOutput('github-event-file', githubEventFile); - } else { - core.setFailed('GitHub event file not found'); - return; - } - switch (buildOutput) { case 'cacheonly': core.setOutput('output', 'type=cacheonly'); @@ -120,11 +105,10 @@ jobs: name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - version: https://github.com/docker/buildx.git#refs/pull/3452/merge + version: https://github.com/docker/buildx.git#refs/pull/3453/merge buildkitd-flags: --debug driver-opts: | image=moby/buildkit:latest - mount.${{ steps.prepare.outputs.github-event-file }}=/etc/buildkit/provenance.d/github_event.json - name: Login to registry if: ${{ inputs.build-output == 'registry' }} From f4fc0a58c2bb2d3e6dc4f202767b3c51eec787fe Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:28:04 +0200 Subject: [PATCH 44/72] build: less aggressive retry backoff Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b70104c7..62ff339f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,7 +269,7 @@ jobs: COSIGN_EXPERIMENTAL: 1 with: script: | - const maxRetries = 100; + const maxRetries = 20; const digests = core.getMultilineInput('digests'); for (const digest of digests) { const manifest = `${core.getInput('image-name')}@${digest}`; @@ -280,7 +280,7 @@ jobs: core.info(`#####`); core.info(`##### Verifying ${manifest} (attempt ${attempt}/${maxRetries})`); core.info(`#####`); - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise(resolve => setTimeout(resolve, 1000)); res = await exec.getExecOutput('cosign', [ '-d', 'verify', '--experimental-oci11', From 3bb7e150fa2ad452c02317d5e335c347ee9e8953 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:20:29 +0200 Subject: [PATCH 45/72] build: use cosign bundle for local signature Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62ff339f..f9a13653 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,8 +249,7 @@ jobs: '-d', 'sign-blob', '--yes', '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', - '--output-signature', `${attestation}.sig`, - '--output-certificate', `${attestation}.crt`, + '--bundle', `${attestation}.cosign.bundle`, attestation ], { ignoreReturnCode: true @@ -320,8 +319,7 @@ jobs: '-d', 'verify-blob', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--signature', `${attestation}.sig`, - '--certificate', `${attestation}.crt`, + '--bundle', `${attestation}.cosign.bundle`, attestation ], { ignoreReturnCode: true From 939d82d7a1e952ddc240f1d3837906b961cf2424 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:53:50 +0200 Subject: [PATCH 46/72] build: add origin annotation to signing payload Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9a13653..fc4d33eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -218,6 +218,7 @@ jobs: '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', + '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, ...images ], { ignoreReturnCode: true @@ -285,6 +286,7 @@ jobs: '--experimental-oci11', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, manifest ], { ignoreReturnCode: true From b68a5f88a7698dcd4883f85f0d5e14e8665e0d2a Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:18:36 +0200 Subject: [PATCH 47/72] build: use attest-blob to embed attestation in DSSE Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc4d33eb..f5917d07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -246,11 +246,14 @@ jobs: } for (const attestation of attestations) { + const afn = attestation.replace(/\.json$/, ""); const res = await exec.getExecOutput('cosign', [ - '-d', 'sign-blob', '--yes', + '-d', 'attest-blob', '--yes', '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', - '--bundle', `${attestation}.cosign.bundle`, + '--predicate', attestation, + '--bundle', `${afn}.sigstore.json`, + '--output-file', `${afn}.signed.json`, attestation ], { ignoreReturnCode: true @@ -317,11 +320,12 @@ jobs: } for (const attestation of attestations) { + const afn = attestation.replace(/\.json$/, ""); const res = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob', + '-d', 'verify-blob-attestation', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--bundle', `${attestation}.cosign.bundle`, + '--bundle', `${afn}.sigstore.json`, attestation ], { ignoreReturnCode: true From da2335848ef80a4e516a07569c282ff7a3d8d53c Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:22:08 +0200 Subject: [PATCH 48/72] build: use buildkit latest stable Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5917d07..bf8c08db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,8 +107,6 @@ jobs: with: version: https://github.com/docker/buildx.git#refs/pull/3453/merge buildkitd-flags: --debug - driver-opts: | - image=moby/buildkit:latest - name: Login to registry if: ${{ inputs.build-output == 'registry' }} From cd6f83509f343608d4b7e380b08c17bf431a7889 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:18:06 +0200 Subject: [PATCH 49/72] build: new bundle format for local Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf8c08db..9cabc303 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,6 +249,7 @@ jobs: '-d', 'attest-blob', '--yes', '--oidc-provider', 'github-actions', '--oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', '--predicate', attestation, '--bundle', `${afn}.sigstore.json`, '--output-file', `${afn}.signed.json`, @@ -323,6 +324,7 @@ jobs: '-d', 'verify-blob-attestation', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', '--bundle', `${afn}.sigstore.json`, attestation ], { From 6124b6674c8ac38a6a4ec98ab4e098af55b14bff Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:29:54 +0200 Subject: [PATCH 50/72] build: format local cosign bundle Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cabc303..17ee4bff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -234,6 +234,7 @@ jobs: COSIGN_EXPERIMENTAL: 1 with: script: | + const fs = require('fs'); const localOutputDir = core.getInput('local-output-dir'); const globber = await glob.create([`${localOutputDir}/**/provenance.json`, `${localOutputDir}/**/sbom*spdx.json`].join('\n')); @@ -260,6 +261,11 @@ jobs: if (res.stderr.length > 0 && res.exitCode != 0) { throw new Error(res.stderr); } + for (const file of [`${afn}.sigstore.json`, `${afn}.signed.json`]) { + if (fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2)); + } + } } - name: Verify signatures (registry) From 432cfba47552ec327847125dfd948258e4c50d2e Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:54:19 +0200 Subject: [PATCH 51/72] build: verify signatures for local artifacts as well Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 125 ++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17ee4bff..a80f81d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,8 @@ jobs: labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} github-token: ${{ secrets.github-token || github.token }} + env: + BUILDKIT_MULTI_PLATFORM: 1 - name: List output if: ${{ inputs.build-output == 'local' }} @@ -195,7 +197,7 @@ jobs: core.setOutput('digests', attestationDigests.join('\n')); }); - - name: Sign attestation manifest (registry) + name: Signing (registry) if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: @@ -226,7 +228,7 @@ jobs: } }); - - name: Sign attestation manifest (local) + name: Signing (local) if: ${{ inputs.build-output == 'local' }} uses: actions/github-script@v7 env: @@ -237,33 +239,35 @@ jobs: const fs = require('fs'); const localOutputDir = core.getInput('local-output-dir'); - const globber = await glob.create([`${localOutputDir}/**/provenance.json`, `${localOutputDir}/**/sbom*spdx.json`].join('\n')); - const attestations = await globber.glob(); - if (attestations.length == 0) { - core.setFailed('No attestation files found'); - return; - } - - for (const attestation of attestations) { - const afn = attestation.replace(/\.json$/, ""); - const res = await exec.getExecOutput('cosign', [ - '-d', 'attest-blob', '--yes', - '--oidc-provider', 'github-actions', - '--oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--predicate', attestation, - '--bundle', `${afn}.sigstore.json`, - '--output-file', `${afn}.signed.json`, - attestation - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); + const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); + for (const platformFolder of platformFolders) { + const baseDir = `${localOutputDir}/${platformFolder}`; + const attestGlobber = await glob.create([`${baseDir}/provenance.json`, `${baseDir}/sbom*spdx.json`].join('\n')); + const attestations = await attestGlobber.glob(); + if (attestations.length == 0) { + core.setFailed(`No attestation files found for ${platformFolder} platform`); + return; } - for (const file of [`${afn}.sigstore.json`, `${afn}.signed.json`]) { - if (fs.existsSync(file)) { - fs.writeFileSync(file, JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2)); + for (const attestation of attestations) { + const afn = attestation.replace(/\.json$/, ""); + const res = await exec.getExecOutput('cosign', [ + '-d', 'attest-blob', '--yes', + '--oidc-provider', 'github-actions', + '--oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--predicate', attestation, + '--bundle', `${afn}.sigstore.json`, + attestation + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + for (const file of [`${afn}.sigstore.json`]) { + if (fs.existsSync(file)) { + fs.writeFileSync(file, JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2)); + } } } } @@ -315,29 +319,52 @@ jobs: COSIGN_EXPERIMENTAL: 1 with: script: | + const fs = require('fs'); const localOutputDir = core.getInput('local-output-dir'); + const signArtifacts = core.getMultilineInput('sign-artifacts'); - const globber = await glob.create([`${localOutputDir}/**/provenance.json`, `${localOutputDir}/**/sbom*spdx.json`].join('\n')); - const attestations = await globber.glob(); - if (attestations.length == 0) { - core.setFailed('No attestation files found'); - return; - } - - for (const attestation of attestations) { - const afn = attestation.replace(/\.json$/, ""); - const res = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob-attestation', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--bundle', `${afn}.sigstore.json`, - attestation - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); + const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); + for (const platformFolder of platformFolders) { + const baseDir = `${localOutputDir}/${platformFolder}`; + const attestGlobber = await glob.create([`${baseDir}/provenance.json`, `${baseDir}/sbom*spdx.json`].join('\n')); + const attestations = await attestGlobber.glob(); + if (attestations.length == 0) { + core.setFailed(`No attestation files found for ${platformFolder} platform`); + return; + } + for (const attestation of attestations) { + const afn = attestation.replace(/\.json$/, ""); + const res = await exec.getExecOutput('cosign', [ + '-d', 'verify-blob-attestation', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--bundle', `${afn}.sigstore.json`, + attestation + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + } + const artifactsGlobber = await glob.create(`${baseDir}/**/*`); + const artifacts = await artifactsGlobber.glob(); + const filteredArtifacts = allArtifacts.filter(p => fs.statSync(p).isFile() && !/provenance\.json$/.test(p) && !/sbom.*spdx\.json$/.test(p) && !/\.sigstore\.json$/.test(p)); + for (const artifact of filteredArtifacts) { + const res = await exec.getExecOutput('cosign', [ + '-d', 'verify-blob-attestation', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--bundle', `provenance.sigstore.json`, + artifact + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } } } - From a7174e22465caed8f4ca4ca8250ad5a3ca81062e Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:41:04 +0200 Subject: [PATCH 52/72] build: only sign provenance attestation Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 78 +++++++++++++------------------------ 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a80f81d0..269974cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -242,34 +242,21 @@ jobs: const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); for (const platformFolder of platformFolders) { const baseDir = `${localOutputDir}/${platformFolder}`; - const attestGlobber = await glob.create([`${baseDir}/provenance.json`, `${baseDir}/sbom*spdx.json`].join('\n')); - const attestations = await attestGlobber.glob(); - if (attestations.length == 0) { - core.setFailed(`No attestation files found for ${platformFolder} platform`); - return; - } - for (const attestation of attestations) { - const afn = attestation.replace(/\.json$/, ""); - const res = await exec.getExecOutput('cosign', [ - '-d', 'attest-blob', '--yes', - '--oidc-provider', 'github-actions', - '--oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--predicate', attestation, - '--bundle', `${afn}.sigstore.json`, - attestation - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - for (const file of [`${afn}.sigstore.json`]) { - if (fs.existsSync(file)) { - fs.writeFileSync(file, JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8')), null, 2)); - } - } + const res = await exec.getExecOutput('cosign', [ + '-d', 'attest-blob', '--yes', + '--oidc-provider', 'github-actions', + '--oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--predicate', `${baseDir}/provenance.json`, + '--bundle', `${baseDir}/provenance.sigstore.json`, + `${baseDir}/provenance.json` + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); } + fs.writeFileSync(`${baseDir}/provenance.sigstore.json`, JSON.stringify(JSON.parse(fs.readFileSync(`${baseDir}/provenance.sigstore.json`, 'utf8')), null, 2)); } - name: Verify signatures (registry) @@ -326,38 +313,29 @@ jobs: const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); for (const platformFolder of platformFolders) { const baseDir = `${localOutputDir}/${platformFolder}`; - const attestGlobber = await glob.create([`${baseDir}/provenance.json`, `${baseDir}/sbom*spdx.json`].join('\n')); - const attestations = await attestGlobber.glob(); - if (attestations.length == 0) { - core.setFailed(`No attestation files found for ${platformFolder} platform`); - return; - } - for (const attestation of attestations) { - const afn = attestation.replace(/\.json$/, ""); - const res = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob-attestation', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--bundle', `${afn}.sigstore.json`, - attestation - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } + const res = await exec.getExecOutput('cosign', [ + '-d', 'verify-blob-attestation', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--bundle', `${baseDir}/provenance.sigstore.json`, + `${baseDir}/provenance.json` + ], { + ignoreReturnCode: true + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); } const artifactsGlobber = await glob.create(`${baseDir}/**/*`); const artifacts = await artifactsGlobber.glob(); - const filteredArtifacts = allArtifacts.filter(p => fs.statSync(p).isFile() && !/provenance\.json$/.test(p) && !/sbom.*spdx\.json$/.test(p) && !/\.sigstore\.json$/.test(p)); + const filteredArtifacts = artifacts.filter(p => fs.statSync(p).isFile() && !/provenance\.json$/.test(p) && !/sbom.*spdx\.json$/.test(p) && !/\.sigstore\.json$/.test(p)); for (const artifact of filteredArtifacts) { const res = await exec.getExecOutput('cosign', [ '-d', 'verify-blob-attestation', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', '--new-bundle-format', - '--bundle', `provenance.sigstore.json`, + '--bundle', `${baseDir}/provenance.sigstore.json`, artifact ], { ignoreReturnCode: true From 5c24e0d0dd701ce84d4a3ad1b4d45e5ba949b781 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:52:38 +0200 Subject: [PATCH 53/72] build: slsa provenance v1 Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 269974cc..1520e751 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,7 +133,7 @@ jobs: build-args: ${{ inputs.build-args }} platforms: ${{ inputs.build-platforms }} outputs: ${{ steps.prepare.outputs.output }} - provenance: mode=max + provenance: mode=max,version=v1 sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} @@ -248,6 +248,7 @@ jobs: '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--new-bundle-format', '--predicate', `${baseDir}/provenance.json`, + '--type', 'https://slsa.dev/provenance/v1', '--bundle', `${baseDir}/provenance.sigstore.json`, `${baseDir}/provenance.json` ], { From d2a2e670bebc32d3205e57a34b3003cbd14217c7 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:59:34 +0200 Subject: [PATCH 54/72] build: use actions/attest@v2 to sign local artifacts Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 60 ++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1520e751..7dc7f4dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,6 +68,7 @@ jobs: contents: read packages: write # needed for pushing the images to GitHub Packages id-token: write # needed for signing the images with GitHub OIDC Token + attestations: write # needed for signing the local artifacts steps: - name: Prepare @@ -197,7 +198,7 @@ jobs: core.setOutput('digests', attestationDigests.join('\n')); }); - - name: Signing (registry) + name: Signing attestations (registry) if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: @@ -228,37 +229,23 @@ jobs: } }); - - name: Signing (local) + name: Signing artifacts amd64 (local) if: ${{ inputs.build-output == 'local' }} - uses: actions/github-script@v7 - env: - INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} - COSIGN_EXPERIMENTAL: 1 + uses: actions/attest@v2 + id: attest_amd64 with: - script: | - const fs = require('fs'); - const localOutputDir = core.getInput('local-output-dir'); - - const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); - for (const platformFolder of platformFolders) { - const baseDir = `${localOutputDir}/${platformFolder}`; - const res = await exec.getExecOutput('cosign', [ - '-d', 'attest-blob', '--yes', - '--oidc-provider', 'github-actions', - '--oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--predicate', `${baseDir}/provenance.json`, - '--type', 'https://slsa.dev/provenance/v1', - '--bundle', `${baseDir}/provenance.sigstore.json`, - `${baseDir}/provenance.json` - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - fs.writeFileSync(`${baseDir}/provenance.sigstore.json`, JSON.stringify(JSON.parse(fs.readFileSync(`${baseDir}/provenance.sigstore.json`, 'utf8')), null, 2)); - } + subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/**/*" + predicate-type: "https://slsa.dev/provenance/v1" + predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/provenance.json" + - + name: Signing artifacts arm64 (local) + if: ${{ inputs.build-output == 'local' }} + uses: actions/attest@v2 + id: attest_arm64 + with: + subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/**/*" + predicate-type: "https://slsa.dev/provenance/v1" + predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/provenance.json" - name: Verify signatures (registry) if: ${{ inputs.build-output == 'registry' }} @@ -304,16 +291,21 @@ jobs: uses: actions/github-script@v7 env: INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} + INPUT_BUNDLE-PATH-AMD64: ${{ steps.attest_amd64.outputs.bundle-path }} + INPUT_BUNDLE-PATH-ARM64: ${{ steps.attest_arm64.outputs.bundle-path }} COSIGN_EXPERIMENTAL: 1 with: script: | const fs = require('fs'); - const localOutputDir = core.getInput('local-output-dir'); - const signArtifacts = core.getMultilineInput('sign-artifacts'); + const inpLocalOutputDir = core.getInput('local-output-dir'); + const inpBundlePathAmd64 = core.getInput('bundle-path-amd64'); + const inpBundlePathArm64 = core.getInput('bundle-path-arm64'); + fs.copyFileSync(inpBundlePathAmd64, `${inpLocalOutputDir}/linux_amd64/provenance.sigstore.json`); + fs.copyFileSync(inpBundlePathArm64, `${inpLocalOutputDir}/linux_arm64/provenance.sigstore.json`); - const platformFolders = fs.readdirSync(localOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); + const platformFolders = fs.readdirSync(inpLocalOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); for (const platformFolder of platformFolders) { - const baseDir = `${localOutputDir}/${platformFolder}`; + const baseDir = `${inpLocalOutputDir}/${platformFolder}`; const res = await exec.getExecOutput('cosign', [ '-d', 'verify-blob-attestation', '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, From 936569223e82faa76422ccc08fd4d66c1e3ed3d2 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:36:24 +0200 Subject: [PATCH 55/72] build: use fork of actions/attest to skip wrting attestation on GH repo Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7dc7f4dd..db38e5d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -231,21 +231,23 @@ jobs: - name: Signing artifacts amd64 (local) if: ${{ inputs.build-output == 'local' }} - uses: actions/attest@v2 + uses: crazy-max/actions-attest@skip-write id: attest_amd64 with: subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/**/*" predicate-type: "https://slsa.dev/provenance/v1" predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/provenance.json" + skip-write: true - name: Signing artifacts arm64 (local) if: ${{ inputs.build-output == 'local' }} - uses: actions/attest@v2 + uses: crazy-max/actions-attest@skip-write id: attest_arm64 with: subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/**/*" predicate-type: "https://slsa.dev/provenance/v1" predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/provenance.json" + skip-write: true - name: Verify signatures (registry) if: ${{ inputs.build-output == 'registry' }} From 6893c722669c4562ea6f38013bd7282bdd260011 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:52:37 +0200 Subject: [PATCH 56/72] build: disable local signature verification temporarily Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db38e5d5..a007e989 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -304,42 +304,6 @@ jobs: const inpBundlePathArm64 = core.getInput('bundle-path-arm64'); fs.copyFileSync(inpBundlePathAmd64, `${inpLocalOutputDir}/linux_amd64/provenance.sigstore.json`); fs.copyFileSync(inpBundlePathArm64, `${inpLocalOutputDir}/linux_arm64/provenance.sigstore.json`); - - const platformFolders = fs.readdirSync(inpLocalOutputDir, { withFileTypes: true }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name); - for (const platformFolder of platformFolders) { - const baseDir = `${inpLocalOutputDir}/${platformFolder}`; - const res = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob-attestation', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--bundle', `${baseDir}/provenance.sigstore.json`, - `${baseDir}/provenance.json` - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - const artifactsGlobber = await glob.create(`${baseDir}/**/*`); - const artifacts = await artifactsGlobber.glob(); - const filteredArtifacts = artifacts.filter(p => fs.statSync(p).isFile() && !/provenance\.json$/.test(p) && !/sbom.*spdx\.json$/.test(p) && !/\.sigstore\.json$/.test(p)); - for (const artifact of filteredArtifacts) { - const res = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob-attestation', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--bundle', `${baseDir}/provenance.sigstore.json`, - artifact - ], { - ignoreReturnCode: true - }); - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - } - } - name: Create manifest if: ${{ inputs.build-output == 'registry' }} From 9536b5a31fadd6b8c5102b3930cb8a8a962be578 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:47:11 +0200 Subject: [PATCH 57/72] build: test not uploading to transparency log Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a007e989..5e26c417 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -220,6 +220,7 @@ jobs: '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, + '--tlog-upload=false', ...images ], { ignoreReturnCode: true From 67ef79b7e6aa26c3e8995fdb19c0efbc3c930f0c Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:01:08 +0100 Subject: [PATCH 58/72] build: use cosign 3.0.2-fix-oci-manifest branch and new-bundle-format for signature manifest Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e26c417..1cc95bae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -154,7 +154,7 @@ jobs: uses: actions/checkout@v5 with: repository: crazy-max/cosign - ref: 2.6.0-fix-oci-manifest + ref: 3.0.2-fix-oci-manifest path: cosign-tmp - name: Set up Go for cosign @@ -220,7 +220,8 @@ jobs: '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, - '--tlog-upload=false', + '--new-bundle-format', 'true', + '--use-signing-config', 'true', ...images ], { ignoreReturnCode: true From abb5893d1efecbc5af64dcecca2701df98a2418d Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:03:01 +0100 Subject: [PATCH 59/72] build: buildx pr-3453 merged Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cc95bae..8a8922e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,7 @@ jobs: name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - version: https://github.com/docker/buildx.git#refs/pull/3453/merge + version: https://github.com/docker/buildx.git#62857022a08552bee5cad0c3044a9a3b185f0b32 buildkitd-flags: --debug - name: Login to registry From ad2c4784d9560596c68878be058082c5498a7e3e Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:54:52 +0100 Subject: [PATCH 60/72] build: testing actions-toolkit implementation to sign blobs Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 88 ++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a8922e8..88b564f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,6 @@ jobs: contents: read packages: write # needed for pushing the images to GitHub Packages id-token: write # needed for signing the images with GitHub OIDC Token - attestations: write # needed for signing the local artifacts steps: - name: Prepare @@ -168,11 +167,19 @@ jobs: run: | make install cosign version + - + name: Install @docker/actions-toolkit + uses: actions/github-script@v8 + with: + script: | + await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', + 'github:docker/actions-toolkit#signing-blobs' + ]); - name: Get attestations manifest digest id: attest if: ${{ inputs.build-output == 'registry' }} - uses: actions/github-script@v7 + uses: actions/github-script@v8 env: INPUT_IMAGE-NAME: ${{ inputs.meta-image }} INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} @@ -198,7 +205,7 @@ jobs: core.setOutput('digests', attestationDigests.join('\n')); }); - - name: Signing attestations (registry) + name: Signing attestation manifests if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: @@ -217,11 +224,10 @@ jobs: } await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', '--oidc-provider', 'github-actions', - '--oidc-issuer', 'https://token.actions.githubusercontent.com', '--registry-referrers-mode', 'oci-1-1', '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, - '--new-bundle-format', 'true', - '--use-signing-config', 'true', + '--new-bundle-format', + '--use-signing-config', ...images ], { ignoreReturnCode: true @@ -231,25 +237,44 @@ jobs: } }); - - name: Signing artifacts amd64 (local) - if: ${{ inputs.build-output == 'local' }} - uses: crazy-max/actions-attest@skip-write - id: attest_amd64 - with: - subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/**/*" - predicate-type: "https://slsa.dev/provenance/v1" - predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_amd64/provenance.json" - skip-write: true - - - name: Signing artifacts arm64 (local) + name: Signing local artifacts if: ${{ inputs.build-output == 'local' }} - uses: crazy-max/actions-attest@skip-write - id: attest_arm64 + uses: actions/github-script@v8 + env: + INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} with: - subject-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/**/*" - predicate-type: "https://slsa.dev/provenance/v1" - predicate-path: "${{ env.LOCAL_OUTPUT_DIR }}/linux_arm64/provenance.json" - skip-write: true + script: | + const path = require('path'); + const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); + const inpLocalOutputDir = core.getInput('local-output-dir'); + + const sigstore = new Sigstore(); + const results = await sigstore.signProvenanceBlobs({ + localExportDir: inpLocalOutputDir + }); + + // TODO: move verification process to actions-toolkit + for (const [provenancePath, signRes] of Object.entries(results)) { + const baseDir = path.dirname(provenancePath); + for (const subject of signRes.subjects) { + const artifactPath = path.join(baseDir, subject.name); + await core.group(`Verifying signed artifact ${artifactPath}`, async () => { + const execRes = await exec.getExecOutput('cosign', [ + '-d', 'verify-blob-attestation', + '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, + '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', + '--new-bundle-format', + '--bundle', signRes.bundlePath, + artifactPath + ], { + ignoreReturnCode: true + }); + if (execRes.stderr.length > 0 && execRes.exitCode != 0) { + throw new Error(execRes.stderr); + } + }); + } + } - name: Verify signatures (registry) if: ${{ inputs.build-output == 'registry' }} @@ -289,23 +314,6 @@ jobs: core.setFailed(`Signature verification failed for ${manifest}`); } } - - - name: Verify signatures (local) - if: ${{ inputs.build-output == 'local' }} - uses: actions/github-script@v7 - env: - INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} - INPUT_BUNDLE-PATH-AMD64: ${{ steps.attest_amd64.outputs.bundle-path }} - INPUT_BUNDLE-PATH-ARM64: ${{ steps.attest_arm64.outputs.bundle-path }} - COSIGN_EXPERIMENTAL: 1 - with: - script: | - const fs = require('fs'); - const inpLocalOutputDir = core.getInput('local-output-dir'); - const inpBundlePathAmd64 = core.getInput('bundle-path-amd64'); - const inpBundlePathArm64 = core.getInput('bundle-path-arm64'); - fs.copyFileSync(inpBundlePathAmd64, `${inpLocalOutputDir}/linux_amd64/provenance.sigstore.json`); - fs.copyFileSync(inpBundlePathArm64, `${inpLocalOutputDir}/linux_arm64/provenance.sigstore.json`); - name: Create manifest if: ${{ inputs.build-output == 'registry' }} From 300850cf7eca42560276abd3be6545c5e4e0c1df Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:03:14 +0100 Subject: [PATCH 61/72] build: build-file input Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88b564f1..9ec4c6e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,10 @@ on: type: string description: "List of build-time variables" required: false + build-file: + type: string + description: "Path to the Dockerfile" + required: false build-output: type: string description: "Build output destination (one of cacheonly, registry, local)" @@ -131,6 +135,7 @@ jobs: uses: docker/build-push-action@v6 with: build-args: ${{ inputs.build-args }} + file: ${{ inputs.build-file }} platforms: ${{ inputs.build-platforms }} outputs: ${{ steps.prepare.outputs.output }} provenance: mode=max,version=v1 From 18f51b982f56cd26d65735d53614626bb3350a4f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:07:19 +0100 Subject: [PATCH 62/72] build: use actions-toolkit to install cosign and verify blobs signature Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 64 +++++++++++-------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ec4c6e1..0517ea33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,28 +150,6 @@ jobs: if: ${{ inputs.build-output == 'local' }} run: | tree -nh ${{ env.LOCAL_OUTPUT_DIR }} -# - -# name: Install cosign -# uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - - - name: Checkout cosign - uses: actions/checkout@v5 - with: - repository: crazy-max/cosign - ref: 3.0.2-fix-oci-manifest - path: cosign-tmp - - - name: Set up Go for cosign - uses: actions/setup-go@v6 - with: - go-version: "1.25.1" # https://github.com/sigstore/cosign/blob/2ec2734e8407ea90ea36103a882b3be2195567eb/Dockerfile#L18 - cache-dependency-path: cosign-tmp/go.sum - - - name: Build and install cosign - working-directory: cosign-tmp - run: | - make install - cosign version - name: Install @docker/actions-toolkit uses: actions/github-script@v8 @@ -180,6 +158,20 @@ jobs: await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', 'github:docker/actions-toolkit#signing-blobs' ]); + - + name: Install Cosign + uses: actions/github-script@v8 + with: + script: | + const { Cosign } = require('@docker/actions-toolkit/lib/cosign/cosign'); + const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); + + const cosignInstall = new Install(); + const cosignBinPath = await cosignInstall.build('https://github.com/crazy-max/cosign.git#3.0.2-fix-oci-manifest', false, true); + await cosignInstall.install(cosignBinPath); + + const cosign = new Cosign(); + await cosign.printVersion(); - name: Get attestations manifest digest id: attest @@ -254,32 +246,14 @@ jobs: const inpLocalOutputDir = core.getInput('local-output-dir'); const sigstore = new Sigstore(); - const results = await sigstore.signProvenanceBlobs({ + const signResults = await sigstore.signProvenanceBlobs({ localExportDir: inpLocalOutputDir }); - // TODO: move verification process to actions-toolkit - for (const [provenancePath, signRes] of Object.entries(results)) { - const baseDir = path.dirname(provenancePath); - for (const subject of signRes.subjects) { - const artifactPath = path.join(baseDir, subject.name); - await core.group(`Verifying signed artifact ${artifactPath}`, async () => { - const execRes = await exec.getExecOutput('cosign', [ - '-d', 'verify-blob-attestation', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--new-bundle-format', - '--bundle', signRes.bundlePath, - artifactPath - ], { - ignoreReturnCode: true - }); - if (execRes.stderr.length > 0 && execRes.exitCode != 0) { - throw new Error(execRes.stderr); - } - }); - } - } + const verifyResults = await sigstore.verifySignedProvenanceBlobs( + { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$` }, + signResults + ); - name: Verify signatures (registry) if: ${{ inputs.build-output == 'registry' }} From 89c21b6133d5a56bdbbda8fba4f3c184a6cbf2a9 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:35:34 +0100 Subject: [PATCH 63/72] build: use vanilla cosign v3.0.2 Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0517ea33..002790a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -167,7 +167,8 @@ jobs: const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); const cosignInstall = new Install(); - const cosignBinPath = await cosignInstall.build('https://github.com/crazy-max/cosign.git#3.0.2-fix-oci-manifest', false, true); + //const cosignBinPath = await cosignInstall.build('https://github.com/crazy-max/cosign.git#3.0.2-fix-oci-manifest', false, true); + const cosignBinPath = await cosignInstall.download('v3.0.2', false, true); await cosignInstall.install(cosignBinPath); const cosign = new Cosign(); From b291252d7548ec899ca58c6f98c2a43c1d9394c9 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:26:07 +0100 Subject: [PATCH 64/72] build: use actions-toolkit to sign and verify attestation manifests Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 124 +++++++----------------------------- 1 file changed, 24 insertions(+), 100 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 002790a2..42fa8c2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,6 +105,14 @@ jobs: default: core.setFailed(`Invalid build-output: ${buildOutput}`); } + - + name: Install @docker/actions-toolkit + uses: actions/github-script@v8 + with: + script: | + await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', + 'github:crazy-max/docker-actions-toolkit#signing-manifest' + ]); - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -150,14 +158,6 @@ jobs: if: ${{ inputs.build-output == 'local' }} run: | tree -nh ${{ env.LOCAL_OUTPUT_DIR }} - - - name: Install @docker/actions-toolkit - uses: actions/github-script@v8 - with: - script: | - await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', - 'github:docker/actions-toolkit#signing-blobs' - ]); - name: Install Cosign uses: actions/github-script@v8 @@ -174,8 +174,7 @@ jobs: const cosign = new Cosign(); await cosign.printVersion(); - - name: Get attestations manifest digest - id: attest + name: Signing attestation manifests if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v8 env: @@ -183,57 +182,21 @@ jobs: INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | - let manifest = {}; - await core.group(`Get manifest`, async () => { - await exec.getExecOutput('docker', ['buildx', 'imagetools', 'inspect', - `${core.getInput('image-name')}@${core.getInput('image-digest')}`, - '--format', '{{json .Manifest}}' - ], { - ignoreReturnCode: true - }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - manifest = JSON.parse(res.stdout.trim()); - }); - }); - await core.group(`Get attestations manifest digest`, async () => { - const attestationDigests = manifest.manifests.filter(m => m.annotations && m.annotations['vnd.docker.reference.type'] === 'attestation-manifest').map(m => m.digest); - core.info(JSON.stringify(attestationDigests, null, 2)); - core.setOutput('digests', attestationDigests.join('\n')); - }); - - - name: Signing attestation manifests - if: ${{ inputs.build-output == 'registry' }} - uses: actions/github-script@v7 - env: - INPUT_TAGS: ${{ steps.meta.outputs.tags }} - INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} - COSIGN_EXPERIMENTAL: 1 - with: - script: | - const tags = core.getMultilineInput('tags'); - const digests = core.getMultilineInput('digests'); - const images = []; - for (const tag of tags) { - for (const digest of digests) { - images.push(`${tag}@${digest}`); - } - } - await exec.getExecOutput('cosign', ['-d', 'sign', '--yes', - '--oidc-provider', 'github-actions', - '--registry-referrers-mode', 'oci-1-1', - '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, - '--new-bundle-format', - '--use-signing-config', - ...images - ], { - ignoreReturnCode: true - }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } + const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); + + const inpImageName = core.getInput('image-name'); + const inpImageDigest = core.getInput('image-digest'); + + const sigstore = new Sigstore(); + const signResults = await sigstore.signAttestationManifests({ + imageName: inpImageName, + imageDigest: inpImageDigest }); + + const verifyResults = await sigstore.verifySignedManifests( + { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$` }, + signResults + ); - name: Signing local artifacts if: ${{ inputs.build-output == 'local' }} @@ -251,49 +214,10 @@ jobs: localExportDir: inpLocalOutputDir }); - const verifyResults = await sigstore.verifySignedProvenanceBlobs( + const verifyResults = await sigstore.verifySignedArtifacts( { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$` }, signResults ); - - - name: Verify signatures (registry) - if: ${{ inputs.build-output == 'registry' }} - uses: actions/github-script@v7 - env: - INPUT_IMAGE-NAME: ${{ inputs.meta-image }} - INPUT_DIGESTS: ${{ steps.attest.outputs.digests }} - COSIGN_EXPERIMENTAL: 1 - with: - script: | - const maxRetries = 20; - const digests = core.getMultilineInput('digests'); - for (const digest of digests) { - const manifest = `${core.getInput('image-name')}@${digest}`; - let attempt = 1; - let res; - let retry = true; - do { - core.info(`#####`); - core.info(`##### Verifying ${manifest} (attempt ${attempt}/${maxRetries})`); - core.info(`#####`); - await new Promise(resolve => setTimeout(resolve, 1000)); - res = await exec.getExecOutput('cosign', [ - '-d', 'verify', - '--experimental-oci11', - '--certificate-identity-regexp', `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$`, - '--certificate-oidc-issuer', 'https://token.actions.githubusercontent.com', - '--annotations', `vnd.docker.reference.origin=docker.io/github-builder/${context.repo.owner}/${context.repo.repo}`, - manifest - ], { - ignoreReturnCode: true - }); - if (res.exitCode != 10) retry = false; - attempt++; - } while (retry && attempt < maxRetries); - if (res.exitCode != 0) { - core.setFailed(`Signature verification failed for ${manifest}`); - } - } - name: Create manifest if: ${{ inputs.build-output == 'registry' }} From 41db215702a1f7db0a7be94f01a0adba65ec5fea Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:59:03 +0100 Subject: [PATCH 65/72] build: cleanup workflow Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 47 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42fa8c2d..87627d72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,14 +63,15 @@ on: required: false env: - LOCAL_OUTPUT_DIR: /tmp/buildx-output + DOCKER_ACTIONS_TOOLKIT_MODULE: "github:crazy-max/docker-actions-toolkit#signing-manifest-test" + COSIGN_VERSION: "v3.0.2" + LOCAL_EXPORT_DIR: "/tmp/buildx-output" jobs: build: runs-on: ubuntu-latest permissions: contents: read - packages: write # needed for pushing the images to GitHub Packages id-token: write # needed for signing the images with GitHub OIDC Token steps: - @@ -78,12 +79,12 @@ jobs: id: prepare uses: actions/github-script@v7 env: - INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} + INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} INPUT_META-IMAGE: ${{ inputs.meta-image }} INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | - const localOutputDir = core.getInput('local-output-dir'); + const localExportDir = core.getInput('local-export-dir'); const metaImage = core.getMultilineInput('meta-image'); const buildOutput = core.getInput('build-output'); @@ -100,19 +101,11 @@ jobs: core.setOutput('output', `type=registry,"name=${metaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); break; case 'local': - core.setOutput('output', `type=local,dest=${localOutputDir}`); + core.setOutput('output', `type=local,dest=${localExportDir}`); break; default: core.setFailed(`Invalid build-output: ${buildOutput}`); } - - - name: Install @docker/actions-toolkit - uses: actions/github-script@v8 - with: - script: | - await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', - 'github:crazy-max/docker-actions-toolkit#signing-manifest' - ]); - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -154,21 +147,25 @@ jobs: env: BUILDKIT_MULTI_PLATFORM: 1 - - name: List output - if: ${{ inputs.build-output == 'local' }} - run: | - tree -nh ${{ env.LOCAL_OUTPUT_DIR }} + name: Install @docker/actions-toolkit + uses: actions/github-script@v8 + env: + INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} + with: + script: | + await exec.exec('npm', ['install', '--prefer-offline', '--no-audit', core.getInput('dat-module')]); - name: Install Cosign uses: actions/github-script@v8 + env: + INPUT_COSIGN-VERSION: ${{ env.COSIGN_VERSION }} with: script: | const { Cosign } = require('@docker/actions-toolkit/lib/cosign/cosign'); const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); const cosignInstall = new Install(); - //const cosignBinPath = await cosignInstall.build('https://github.com/crazy-max/cosign.git#3.0.2-fix-oci-manifest', false, true); - const cosignBinPath = await cosignInstall.download('v3.0.2', false, true); + const cosignBinPath = await cosignInstall.download(core.getInput('cosign-version'), false, true); await cosignInstall.install(cosignBinPath); const cosign = new Cosign(); @@ -202,16 +199,16 @@ jobs: if: ${{ inputs.build-output == 'local' }} uses: actions/github-script@v8 env: - INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_OUTPUT_DIR }} + INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_EXPORT_DIR }} with: script: | const path = require('path'); const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); - const inpLocalOutputDir = core.getInput('local-output-dir'); + const inplocalExportDir = core.getInput('local-output-dir'); const sigstore = new Sigstore(); const signResults = await sigstore.signProvenanceBlobs({ - localExportDir: inpLocalOutputDir + localExportDir: inplocalExportDir }); const verifyResults = await sigstore.verifySignedArtifacts( @@ -240,15 +237,15 @@ jobs: } }); - - name: List output + name: List local output if: ${{ inputs.build-output == 'local' }} run: | - tree -nh ${{ env.LOCAL_OUTPUT_DIR }} + tree -nh ${{ env.LOCAL_EXPORT_DIR }} - name: Upload artifact if: ${{ inputs.build-output == 'local' }} uses: actions/upload-artifact@v4 with: name: docker-github-builder-output - path: ${{ env.LOCAL_OUTPUT_DIR }} + path: ${{ env.LOCAL_EXPORT_DIR }} if-no-files-found: error From 2f92945d09f2aec44dc5e94c9912259c9f1ec22f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:53:37 +0100 Subject: [PATCH 66/72] build: install actions-toolkit 0.65.0 Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87627d72..b74d6a9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ on: required: false env: - DOCKER_ACTIONS_TOOLKIT_MODULE: "github:crazy-max/docker-actions-toolkit#signing-manifest-test" + DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.65.0" COSIGN_VERSION: "v3.0.2" LOCAL_EXPORT_DIR: "/tmp/buildx-output" From c9034539bcf65b2603647c0cefd0e795a26c7875 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:17:24 +0100 Subject: [PATCH 67/72] build: cache and cache-mode inputs for GitHub Actions cache backend Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 39 ++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b74d6a9e..d6bc9ec1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,16 @@ name: build on: workflow_call: inputs: + cache: + type: boolean + description: "Enable cache to GitHub Actions cache backend" + required: false + default: false + cache-mode: + type: string + description: "Cache layers to export if cache enabled (min or max)" + required: false + default: 'min' set-meta-annotations: type: boolean description: "Set metadata-action annotations" @@ -80,31 +90,40 @@ jobs: uses: actions/github-script@v7 env: INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} + INPUT_CACHE: ${{ inputs.cache }} + INPUT_CACHE-MODE: ${{ inputs.cache-mode }} INPUT_META-IMAGE: ${{ inputs.meta-image }} INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | - const localExportDir = core.getInput('local-export-dir'); - const metaImage = core.getMultilineInput('meta-image'); - const buildOutput = core.getInput('build-output'); + const inpLocalExportDir = core.getInput('local-export-dir'); + const inpCache = core.getBooleanInput('cache'); + const inpCacheMode = core.getInput('cache-mode'); + const inpMetaImage = core.getMultilineInput('meta-image'); + const inpBuildOutput = core.getInput('build-output'); - switch (buildOutput) { + switch (inpBuildOutput) { case 'cacheonly': core.setOutput('output', 'type=cacheonly'); break; case 'registry': - if (metaImage.length == 0) { + if (inpMetaImage.length == 0) { core.setFailed('meta-image is required when build-output is registry'); - } else if (metaImage.length > 1) { + } else if (inpMetaImage.length > 1) { core.setFailed('Only one meta-image is supported'); } - core.setOutput('output', `type=registry,"name=${metaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); + core.setOutput('output', `type=registry,"name=${inpMetaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); break; case 'local': - core.setOutput('output', `type=local,dest=${localExportDir}`); + core.setOutput('output', `type=local,dest=${inpLocalExportDir}`); break; default: - core.setFailed(`Invalid build-output: ${buildOutput}`); + core.setFailed(`Invalid build-output: ${inpBuildOutput}`); + } + + if (inpCache) { + core.setOutput('cache-from', `type=gha,scope=docker-github-builder`); + core.setOutput('cache-to', `type=gha,scope=docker-github-builder,mode=${inpCacheMode}`); } - name: Set up Docker Buildx @@ -139,6 +158,8 @@ jobs: file: ${{ inputs.build-file }} platforms: ${{ inputs.build-platforms }} outputs: ${{ steps.prepare.outputs.output }} + cache-from: ${{ steps.prepare.outputs.cache-from }} + cache-to: ${{ steps.prepare.outputs.cache-to }} provenance: mode=max,version=v1 sbom: ${{ inputs.build-sbom }} labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} From aa5750dfa6cb17573129e888b31a097281b0e062 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:49:51 +0100 Subject: [PATCH 68/72] build: setup qemu opt Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6bc9ec1..4c1bc3d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,12 @@ on: description: "Set metadata-action labels" required: false default: false + setup-qemu: + type: boolean + description: "Install QEMU static binaries" + required: false + default: true + # same as docker/metadata-action inputs (minus sep-tags, sep-labels, sep-annotations, bake-target) meta-image: type: string description: "Image to use as base name for tags" @@ -43,6 +49,11 @@ on: type: string description: "List of custom annotations" required: false + # same as docker/setup-qemu-action inputs (minus platforms, cache-image) + qemu-image: + type: string + description: "QEMU static binaries Docker image (e.g. tonistiigi/binfmt:latest)" + required: false build-args: type: string description: "List of build-time variables" @@ -125,6 +136,12 @@ jobs: core.setOutput('cache-from', `type=gha,scope=docker-github-builder`); core.setOutput('cache-to', `type=gha,scope=docker-github-builder,mode=${inpCacheMode}`); } + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + if: ${{ inputs.setup-qemu }} + with: + image: ${{ inputs.qemu-image }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 23a491f7054f53a65a669f8fab8db9274523b73f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:11:21 +0100 Subject: [PATCH 69/72] build: support multiple image names Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 53 +++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c1bc3d5..c2f3b69b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,9 +29,9 @@ on: required: false default: true # same as docker/metadata-action inputs (minus sep-tags, sep-labels, sep-annotations, bake-target) - meta-image: + meta-images: type: string - description: "Image to use as base name for tags" + description: "List of images to use as base name for tags" required: false meta-tags: type: string @@ -84,7 +84,7 @@ on: required: false env: - DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.65.0" + DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.66.0" COSIGN_VERSION: "v3.0.2" LOCAL_EXPORT_DIR: "/tmp/buildx-output" @@ -103,14 +103,14 @@ jobs: INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} INPUT_CACHE: ${{ inputs.cache }} INPUT_CACHE-MODE: ${{ inputs.cache-mode }} - INPUT_META-IMAGE: ${{ inputs.meta-image }} + INPUT_META-IMAGES: ${{ inputs.meta-images }} INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} with: script: | const inpLocalExportDir = core.getInput('local-export-dir'); const inpCache = core.getBooleanInput('cache'); const inpCacheMode = core.getInput('cache-mode'); - const inpMetaImage = core.getMultilineInput('meta-image'); + const inpMetaImages = core.getMultilineInput('meta-images'); const inpBuildOutput = core.getInput('build-output'); switch (inpBuildOutput) { @@ -118,12 +118,10 @@ jobs: core.setOutput('output', 'type=cacheonly'); break; case 'registry': - if (inpMetaImage.length == 0) { - core.setFailed('meta-image is required when build-output is registry'); - } else if (inpMetaImage.length > 1) { - core.setFailed('Only one meta-image is supported'); + if (inpMetaImages.length == 0) { + core.setFailed('meta-images is required when build-output is registry'); } - core.setOutput('output', `type=registry,"name=${inpMetaImage[0]}",oci-artifact=true,push-by-digest=true,name-canonical=true`); + core.setOutput('output', `type=registry,"name=${inpMetaImages.join(',')}",oci-artifact=true,push-by-digest=true,name-canonical=true`); break; case 'local': core.setOutput('output', `type=local,dest=${inpLocalExportDir}`); @@ -161,7 +159,7 @@ jobs: if: ${{ inputs.build-output == 'registry' }} uses: docker/metadata-action@v5 with: - images: ${{ inputs.meta-image }} + images: ${{ inputs.meta-images }} tags: ${{ inputs.meta-tags }} flavor: ${{ inputs.meta-flavor }} labels: ${{ inputs.meta-labels }} @@ -213,18 +211,18 @@ jobs: if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v8 env: - INPUT_IMAGE-NAME: ${{ inputs.meta-image }} + INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); - const inpImageName = core.getInput('image-name'); + const inpImageNames = core.getMultilineInput('image-names'); const inpImageDigest = core.getInput('image-digest'); const sigstore = new Sigstore(); const signResults = await sigstore.signAttestationManifests({ - imageName: inpImageName, + imageNames: inpImageNames, imageDigest: inpImageDigest }); @@ -258,22 +256,25 @@ jobs: if: ${{ inputs.build-output == 'registry' }} uses: actions/github-script@v7 env: - INPUT_TAGS: ${{ steps.meta.outputs.tags }} + INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} + INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }} INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} with: script: | - let createArgs = ['buildx', 'imagetools', 'create']; - for (const tag of core.getMultilineInput('tags')) { - createArgs.push('-t', tag); - } - createArgs.push(core.getInput('image-digest')); - await exec.getExecOutput('docker', createArgs, { - ignoreReturnCode: true - }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); + for (const imageName of core.getMultilineInput('image-names')) { + let createArgs = ['buildx', 'imagetools', 'create']; + for (const tag of core.getMultilineInput('tag-names')) { + createArgs.push('-t', `${imageName}:${tag}`); } - }); + createArgs.push(core.getInput('image-digest')); + await exec.getExecOutput('docker', createArgs, { + ignoreReturnCode: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + }); + } - name: List local output if: ${{ inputs.build-output == 'local' }} From 46d1973a2f2fa01eb28fccfc80f2ecf203fd0aef Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:57:01 +0100 Subject: [PATCH 70/72] build: add remaining build inputs Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 88 ++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2f3b69b..923b9697 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,11 @@ on: type: string description: "QEMU static binaries Docker image (e.g. tonistiigi/binfmt:latest)" required: false + # same as docker/build-push-action inputs + build-annotations: + type: string + description: "List of annotation to set to the image" + required: false build-args: type: string description: "List of build-time variables" @@ -62,6 +67,10 @@ on: type: string description: "Path to the Dockerfile" required: false + build-labels: + type: string + description: "List of metadata for an image" + required: false build-output: type: string description: "Build output destination (one of cacheonly, registry, local)" @@ -71,10 +80,27 @@ on: type: string description: "List of target platforms to build" required: false + build-pull: + type: boolean + description: "Always attempt to pull all referenced images" + required: false + default: false build-sbom: type: string description: "Generate SBOM attestation for the build (shorthand for --attest=type=sbom)" required: false + build-shm-size: + type: string + description: "Size of /dev/shm (e.g., 2g)" + required: false + build-target: + type: string + description: "Sets the target stage to build" + required: false + build-ulimit: + type: string + description: "Ulimit options (e.g., nofile=1024:1024)" + required: false secrets: registry-auths: description: "Registry authentication details as YAML objects" @@ -95,6 +121,17 @@ jobs: contents: read id-token: write # needed for signing the images with GitHub OIDC Token steps: + - + name: Docker meta + id: meta + if: ${{ inputs.build-output == 'registry' }} + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.meta-images }} + tags: ${{ inputs.meta-tags }} + flavor: ${{ inputs.meta-flavor }} + labels: ${{ inputs.meta-labels }} + annotations: ${{ inputs.meta-annotations }} - name: Prepare id: prepare @@ -105,6 +142,13 @@ jobs: INPUT_CACHE-MODE: ${{ inputs.cache-mode }} INPUT_META-IMAGES: ${{ inputs.meta-images }} INPUT_BUILD-OUTPUT: ${{ inputs.build-output }} + INPUT_BUILD-ANNOTATIONS: ${{ inputs.build-annotations }} + INPUT_SET-META-ANNOTATIONS: ${{ inputs.set-meta-annotations }} + INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }} + INPUT_BUILD-LABELS: ${{ inputs.build-labels }} + INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }} + INPUT_META-LABELS: ${{ steps.meta.outputs.labels }} + INPUT_BUILD-TARGET: ${{ inputs.build-target }} with: script: | const inpLocalExportDir = core.getInput('local-export-dir'); @@ -112,6 +156,13 @@ jobs: const inpCacheMode = core.getInput('cache-mode'); const inpMetaImages = core.getMultilineInput('meta-images'); const inpBuildOutput = core.getInput('build-output'); + const inpSetMetaAnnotations = core.getBooleanInput('set-meta-annotations'); + const inpBuildAnnotations = core.getMultilineInput('build-annotations'); + const inpMetaAnnotations = core.getMultilineInput('meta-annotations'); + const inpSetMetaLabels = core.getBooleanInput('set-meta-labels'); + const inpBuildLabels = core.getMultilineInput('build-labels'); + const inpMetaLabels = core.getMultilineInput('meta-labels'); + const inpBuildTarget = core.getInput('build-target'); switch (inpBuildOutput) { case 'cacheonly': @@ -134,6 +185,16 @@ jobs: core.setOutput('cache-from', `type=gha,scope=docker-github-builder`); core.setOutput('cache-to', `type=gha,scope=docker-github-builder,mode=${inpCacheMode}`); } + + if (inpSetMetaAnnotations && inpMetaAnnotations.length > 0) { + inpBuildAnnotations.push(...inpMetaAnnotations); + } + core.setOutput('annotations', inpBuildAnnotations.join('\n')); + + if (inpSetMetaLabels && inpMetaLabels.length > 0) { + inpBuildLabels.push(...inpMetaLabels); + } + core.setOutput('labels', inpBuildLabels.join('\n')); - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -153,32 +214,25 @@ jobs: uses: crazy-max/docker-login-action@dockerhub-oidc with: registry-auth: ${{ secrets.registry-auths }} - - - name: Docker meta - id: meta - if: ${{ inputs.build-output == 'registry' }} - uses: docker/metadata-action@v5 - with: - images: ${{ inputs.meta-images }} - tags: ${{ inputs.meta-tags }} - flavor: ${{ inputs.meta-flavor }} - labels: ${{ inputs.meta-labels }} - annotations: ${{ inputs.meta-annotations }} - name: Build id: build uses: docker/build-push-action@v6 with: + annotations: ${{ steps.prepare.outputs.annotations }} build-args: ${{ inputs.build-args }} - file: ${{ inputs.build-file }} - platforms: ${{ inputs.build-platforms }} - outputs: ${{ steps.prepare.outputs.output }} cache-from: ${{ steps.prepare.outputs.cache-from }} cache-to: ${{ steps.prepare.outputs.cache-to }} + file: ${{ inputs.build-file }} + labels: ${{ steps.prepare.outputs.labels }} + outputs: ${{ steps.prepare.outputs.output }} + platforms: ${{ inputs.build-platforms }} provenance: mode=max,version=v1 + pull: ${{ inputs.build-pull }} sbom: ${{ inputs.build-sbom }} - labels: ${{ inputs.set-meta-labels && steps.meta.outputs.labels || '' }} - annotations: ${{ inputs.set-meta-annotations && steps.meta.outputs.annotations || '' }} + shm-size: ${{ inputs.build-shm-size }} + target: ${{ inputs.build-target }} + ulimit: ${{ inputs.build-ulimit }} github-token: ${{ secrets.github-token || github.token }} env: BUILDKIT_MULTI_PLATFORM: 1 @@ -285,6 +339,6 @@ jobs: if: ${{ inputs.build-output == 'local' }} uses: actions/upload-artifact@v4 with: - name: docker-github-builder-output + name: docker-github-builder-assets path: ${{ env.LOCAL_EXPORT_DIR }} if-no-files-found: error From 144584b2efaab0b326bf6b2fb8dc8102b531a7ad Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:51:27 +0100 Subject: [PATCH 71/72] build: update actions-toolkit to 0.67.0 Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 923b9697..8ff36415 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,7 +110,7 @@ on: required: false env: - DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.66.0" + DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.67.0" COSIGN_VERSION: "v3.0.2" LOCAL_EXPORT_DIR: "/tmp/buildx-output" @@ -120,6 +120,7 @@ jobs: permissions: contents: read id-token: write # needed for signing the images with GitHub OIDC Token + packages: write # needed to push images to GitHub Container Registry steps: - name: Docker meta From 5fdc350d1cc7d4c5bff09a5874b79c09d97dbbec Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:51:27 +0100 Subject: [PATCH 72/72] update README Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fea83c9f..1b42e405 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,52 @@ This repository provides official Docker-maintained [reusable GitHub Actions workflows](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows) to securely build container images using Docker best practices. The workflows -sign BuildKit-generated SLSA-compliant provenance attestations and help -establish Docker as a trusted authority in secure software supply chains. +sign BuildKit-generated SLSA-compliant provenance attestations and align with +the principles behind [Docker Hardened Images](https://docs.docker.com/dhi/how-to/use/), +enabling open source projects to follow a seamless path toward higher levels of +security and trust. ## :test_tube: Experimental This repository is considered **EXPERIMENTAL** and under active development until further notice. It is subject to non-backward compatible changes or removal in any future version. + +## Build reusable workflow + +```yaml +name: ci + +permissions: + contents: read + +on: + push: + branches: + - 'main' + tags: + - 'v*' + pull_request: + + build: + uses: docker/github-builder-experimental/.github/workflows/build.yml@main + permissions: + contents: read + id-token: write # for signing attestation manifests with GitHub OIDC Token + packages: write # needed to push images to GitHub Container Registry + with: + meta-images: name/app + meta-tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + build-output: ${{ github.event_name != 'pull_request' && 'registry' || 'cacheonly' }} + build-platforms: linux/amd64,linux/arm64 + secrets: + registry-auths: | + - registry: docker.io + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} +``` + +You can find the list of available inputs in [`.github/workflows/build.yml`](.github/workflows/build.yml).