├── .gitattributes ├── test ├── go.mod ├── main.go ├── hello.Dockerfile ├── go.Dockerfile └── docker-bake.hcl ├── .github ├── CODE_OF_CONDUCT.md ├── dependabot.yml ├── workflows │ ├── .pr-assign-author.yml │ ├── verify.yml │ ├── .test-bake.yml │ ├── .test-build.yml │ ├── build.yml │ └── bake.yml ├── ISSUE_TEMPLATE │ ├── feature.yml │ ├── config.yml │ └── bug.yml ├── SECURITY.md └── CONTRIBUTING.md ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | /test/** linguist-detectable=false 2 | -------------------------------------------------------------------------------- /test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/github-builder/test 2 | 3 | go 1.25.0 4 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("hello world") 7 | } 8 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | - [Moby community guidelines](https://github.com/moby/moby/blob/master/CONTRIBUTING.md#moby-community-guidelines) 4 | - [Docker Code of Conduct](https://github.com/docker/code-of-conduct) 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | open-pull-requests-limit: 10 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "area/dependencies" 10 | - "bot" 11 | -------------------------------------------------------------------------------- /test/hello.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM alpine AS base 4 | ARG TARGETPLATFORM 5 | RUN echo "Hello, World! This is ${TARGETPLATFORM}" > /hello.txt 6 | ARG BUILDKIT_SBOM_SCAN_STAGE=true 7 | 8 | FROM scratch 9 | COPY --from=base /hello.txt / 10 | -------------------------------------------------------------------------------- /.github/workflows/.pr-assign-author.yml: -------------------------------------------------------------------------------- 1 | name: .pr-assign-author 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request_target: 8 | types: 9 | - opened 10 | - reopened 11 | 12 | jobs: 13 | run: 14 | uses: crazy-max/.github/.github/workflows/pr-assign-author.yml@1b673f36fad86812f538c1df9794904038a23cbf 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema 2 | name: Feature request 3 | description: Missing functionality? Come tell us about it! 4 | labels: 5 | - kind/enhancement 6 | - status/triage 7 | 8 | body: 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: What is the feature you want to see? 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: Questions and Discussions 5 | url: https://github.com/docker/github-builder-experimental/discussions/new 6 | about: Use Github Discussions to ask questions and/or open discussion topics. 7 | - name: Documentation 8 | url: https://docs.docker.com/build/ci/github-actions/ 9 | about: Read the documentation. 10 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues 2 | 3 | The project maintainers take security seriously. If you discover a security 4 | issue, please bring it to their attention right away! 5 | 6 | **Please _DO NOT_ file a public issue**, instead send your report privately to 7 | [security@docker.com](mailto:security@docker.com). 8 | 9 | Security reports are greatly appreciated, and we will publicly thank you for it. 10 | We also like to send gifts—if you'd like Docker swag, make sure to let 11 | us know. We currently do not offer a paid security bounty program, but are not 12 | ruling it out in the future. 13 | -------------------------------------------------------------------------------- /test/go.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION="1.25" 4 | 5 | # xx is a helper for cross-compilation 6 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.7.0 AS xx 7 | 8 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS base 9 | COPY --from=xx / / 10 | RUN apk add --no-cache file git 11 | ENV CGO_ENABLED=0 12 | WORKDIR /src 13 | 14 | FROM base AS build 15 | ARG TARGETPLATFORM 16 | RUN --mount=type=bind,target=. \ 17 | --mount=target=/root/.cache,type=cache \ 18 | xx-go build -trimpath -o /out/myapp . \ 19 | && xx-verify --static /out/myapp 20 | ARG BUILDKIT_SBOM_SCAN_STAGE=true 21 | 22 | FROM scratch 23 | COPY --from=build /out / 24 | -------------------------------------------------------------------------------- /test/docker-bake.hcl: -------------------------------------------------------------------------------- 1 | # Special target: https://github.com/docker/metadata-action#bake-definition 2 | target "docker-metadata-action" { 3 | tags = ["github-builder:local"] 4 | } 5 | 6 | group "default" { 7 | targets = ["hello-cross"] 8 | } 9 | 10 | group "grp" { 11 | targets = ["go", "hello"] 12 | } 13 | 14 | target "go" { 15 | inherits = ["docker-metadata-action"] 16 | dockerfile = "go.Dockerfile" 17 | } 18 | 19 | target "go-cross" { 20 | inherits = ["go"] 21 | platforms = ["linux/amd64", "linux/arm64"] 22 | } 23 | 24 | target "hello" { 25 | inherits = ["docker-metadata-action"] 26 | dockerfile = "hello.Dockerfile" 27 | } 28 | 29 | target "hello-cross" { 30 | inherits = ["hello"] 31 | platforms = ["linux/amd64", "linux/arm64"] 32 | } 33 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 4 | 5 | Contributions to this project are [released](https://docs.github.com/en/github/site-policy/github-terms-of-service#6-contributions-under-repository-license) 6 | to the public under the [project's open source license](../LICENSE). 7 | 8 | ## Submitting a pull request 9 | 10 | 1. [Fork](https://github.com/docker/github-builder-experimental/fork) and clone the repository 11 | 2. Create a new branch: `git checkout -b my-branch-name` 12 | 3. Make your changes 13 | 4. Push to your fork and [submit a pull request](https://github.com/docker/github-builder-experimental/compare) 14 | 5. Pat your self on the back and wait for your pull request to be reviewed and merged. 15 | 16 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 17 | 18 | - Make sure the `README.md` and any other relevant **documentation are kept up-to-date**. 19 | - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 20 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as **separate pull requests**. 21 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 22 | 23 | ## Resources 24 | 25 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 26 | - [Using Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) 27 | - [GitHub Help](https://docs.github.com/en) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema 2 | name: Bug Report 3 | description: Report a bug 4 | labels: 5 | - status/triage 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thank you for taking the time to report a bug! 12 | If this is a security issue please report it to the [Docker Security team](mailto:security@docker.com). 13 | 14 | - type: checkboxes 15 | attributes: 16 | label: Contributing guidelines 17 | description: > 18 | Make sure you've read the contributing guidelines before proceeding. 19 | options: 20 | - label: I've read the [contributing guidelines](https://github.com/docker/github-builder-experimental/blob/master/.github/CONTRIBUTING.md) and wholeheartedly agree 21 | required: true 22 | 23 | - type: checkboxes 24 | attributes: 25 | label: "I've found a bug, and:" 26 | description: | 27 | Make sure that your request fulfills all of the following requirements. 28 | If one requirement cannot be satisfied, explain in detail why. 29 | options: 30 | - label: The documentation does not mention anything about my problem 31 | - label: There are no open or closed issues that are related to my problem 32 | 33 | - type: textarea 34 | attributes: 35 | label: Description 36 | description: > 37 | Provide a brief description of the bug in 1-2 sentences. 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | attributes: 43 | label: Expected behaviour 44 | description: > 45 | Describe precisely what you'd expect to happen. 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | attributes: 51 | label: Actual behaviour 52 | description: > 53 | Describe precisely what is actually happening. 54 | validations: 55 | required: true 56 | 57 | - type: input 58 | attributes: 59 | label: Repository URL 60 | description: > 61 | Enter the URL of the repository where you are experiencing the 62 | issue. If your repository is private, provide a link to a minimal 63 | repository that reproduces the issue. 64 | 65 | - type: input 66 | attributes: 67 | label: Workflow run URL 68 | description: > 69 | Enter the URL of the GitHub Action workflow run, if public. 70 | 71 | - type: textarea 72 | attributes: 73 | label: YAML workflow 74 | description: | 75 | Provide the YAML of the workflow that's causing the issue. 76 | Make sure to remove any sensitive information. 77 | render: yaml 78 | validations: 79 | required: true 80 | 81 | - type: textarea 82 | attributes: 83 | label: Workflow logs 84 | description: > 85 | [Attach](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files) 86 | the [log file of your workflow run](https://docs.github.com/en/actions/managing-workflow-runs/using-workflow-run-logs#downloading-logs) 87 | and make sure to remove any sensitive information. 88 | 89 | - type: textarea 90 | attributes: 91 | label: BuildKit logs 92 | description: > 93 | If applicable, provide the [BuildKit container logs](https://docs.docker.com/build/ci/github-actions/configure-builder/#buildkit-container-logs) by enabling debug mode 94 | and reproducing the issue. 95 | render: text 96 | 97 | - type: textarea 98 | attributes: 99 | label: Additional info 100 | description: | 101 | Provide any additional information that could be useful. 102 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | builder-outputs: 7 | type: string 8 | description: "JSON build outputs from Docker GitHub Builder reusable workflows" 9 | required: true 10 | secrets: 11 | registry-auths: 12 | description: "Registry authentication details as YAML objects" 13 | required: false 14 | 15 | env: 16 | DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.71.0" 17 | 18 | jobs: 19 | verify: 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - 23 | name: Extract builder outputs 24 | id: vars 25 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 26 | env: 27 | INPUT_BUILDER-OUTPUTS: ${{ inputs.builder-outputs }} 28 | with: 29 | script: | 30 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 31 | core.info(JSON.stringify(builderOutputs, null, 2)); 32 | 33 | const cosignVersion = builderOutputs['cosign-version']; 34 | const cosignVerifyCommands = builderOutputs['cosign-verify-commands']; 35 | const artifactName = builderOutputs['artifact-name']; 36 | const outputType = builderOutputs['output-type']; 37 | const signed = builderOutputs['signed'] === 'true'; 38 | if (!signed) { 39 | core.warning('No signatures to verify, skipping verification steps'); 40 | } else if (!cosignVersion || !cosignVerifyCommands || !outputType || (outputType === 'local' && !artifactName)) { 41 | core.setFailed('Missing required builder outputs for signature verification'); 42 | return; 43 | } 44 | 45 | core.setOutput('cosign-version', cosignVersion); 46 | core.setOutput('cosign-verify-commands', cosignVerifyCommands); 47 | core.setOutput('artifact-name', artifactName); 48 | core.setOutput('output-type', outputType); 49 | core.setOutput('signed', signed); 50 | - 51 | name: Install @docker/actions-toolkit 52 | if: ${{ steps.vars.outputs.signed == 'true' }} 53 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 54 | env: 55 | INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} 56 | with: 57 | script: | 58 | await exec.exec('npm', ['install', '--prefer-offline', '--ignore-scripts', core.getInput('dat-module')]); 59 | - 60 | name: Install Cosign 61 | if: ${{ steps.vars.outputs.signed == 'true' }} 62 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 63 | env: 64 | INPUT_COSIGN-VERSION: ${{ steps.vars.outputs.cosign-version }} 65 | with: 66 | script: | 67 | const { Cosign } = require('@docker/actions-toolkit/lib/cosign/cosign'); 68 | const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); 69 | 70 | const cosignInstall = new Install(); 71 | const cosignBinPath = await cosignInstall.download({ 72 | version: core.getInput('cosign-version'), 73 | ghaNoCache: true, 74 | skipState: true, 75 | verifySignature: true 76 | }); 77 | await cosignInstall.install(cosignBinPath); 78 | 79 | const cosign = new Cosign(); 80 | await cosign.printVersion(); 81 | - 82 | name: Login to registry 83 | if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'image' }} 84 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 85 | with: 86 | registry-auth: ${{ secrets.registry-auths }} 87 | - 88 | name: Download artifacts 89 | if: ${{ steps.vars.outputs.signed == 'true' && steps.vars.outputs.output-type == 'local' }} 90 | uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 91 | with: 92 | name: ${{ steps.vars.outputs.artifact-name }} 93 | - 94 | name: Verify signatures 95 | if: ${{ steps.vars.outputs.signed == 'true' }} 96 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 97 | env: 98 | INPUT_COSIGN-VERIFY-COMMANDS: ${{ steps.vars.outputs.cosign-verify-commands }} 99 | with: 100 | script: | 101 | for (const cmd of core.getMultilineInput('cosign-verify-commands')) { 102 | await exec.exec(cmd); 103 | } 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /.github/workflows/.test-bake.yml: -------------------------------------------------------------------------------- 1 | name: .test-bake 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - 'main' 12 | - 'releases/v*' 13 | tags: 14 | - 'v*' 15 | paths: 16 | - '.github/workflows/.test-bake.yml' 17 | - '.github/workflows/bake.yml' 18 | - '.github/workflows/verify.yml' 19 | - 'test/**' 20 | pull_request: 21 | paths: 22 | - '.github/workflows/.test-bake.yml' 23 | - '.github/workflows/bake.yml' 24 | - '.github/workflows/verify.yml' 25 | - 'test/**' 26 | 27 | jobs: 28 | bake-aws-single: 29 | uses: ./.github/workflows/bake.yml 30 | permissions: 31 | contents: read 32 | id-token: write 33 | with: 34 | context: test 35 | output: image 36 | push: ${{ github.event_name != 'pull_request' }} 37 | sbom: true 38 | target: hello 39 | meta-images: | 40 | public.ecr.aws/q3b5f1u4/test-docker-action 41 | meta-tags: | 42 | type=raw,value=bake-ghbuilder-single-${{ github.run_id }} 43 | secrets: 44 | registry-auths: | 45 | - registry: public.ecr.aws 46 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 47 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 48 | 49 | bake-aws-single-verify: 50 | uses: ./.github/workflows/verify.yml 51 | if: ${{ github.event_name != 'pull_request' }} 52 | needs: 53 | - bake-aws-single 54 | with: 55 | builder-outputs: ${{ toJSON(needs.bake-aws-single.outputs) }} 56 | secrets: 57 | registry-auths: | 58 | - registry: public.ecr.aws 59 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 60 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 61 | 62 | bake-aws-single-outputs: 63 | runs-on: ubuntu-24.04 64 | needs: 65 | - bake-aws-single 66 | steps: 67 | - 68 | name: Builder outputs 69 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 70 | env: 71 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-aws-single.outputs) }} 72 | with: 73 | script: | 74 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 75 | core.info(JSON.stringify(builderOutputs, null, 2)); 76 | 77 | bake-aws: 78 | uses: ./.github/workflows/bake.yml 79 | permissions: 80 | contents: read 81 | id-token: write 82 | with: 83 | context: test 84 | output: image 85 | push: ${{ github.event_name != 'pull_request' }} 86 | sbom: true 87 | target: hello-cross 88 | meta-images: | 89 | public.ecr.aws/q3b5f1u4/test-docker-action 90 | meta-tags: | 91 | type=raw,value=bake-ghbuilder-${{ github.run_id }} 92 | secrets: 93 | registry-auths: | 94 | - registry: public.ecr.aws 95 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 96 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 97 | 98 | bake-aws-verify: 99 | uses: ./.github/workflows/verify.yml 100 | if: ${{ github.event_name != 'pull_request' }} 101 | needs: 102 | - bake-aws 103 | with: 104 | builder-outputs: ${{ toJSON(needs.bake-aws.outputs) }} 105 | secrets: 106 | registry-auths: | 107 | - registry: public.ecr.aws 108 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 109 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 110 | 111 | bake-aws-outputs: 112 | runs-on: ubuntu-24.04 113 | needs: 114 | - bake-aws 115 | steps: 116 | - 117 | name: Builder outputs 118 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 119 | env: 120 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-aws.outputs) }} 121 | with: 122 | script: | 123 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 124 | core.info(JSON.stringify(builderOutputs, null, 2)); 125 | 126 | bake-aws-nosign: 127 | uses: ./.github/workflows/bake.yml 128 | permissions: 129 | contents: read 130 | id-token: write 131 | with: 132 | context: test 133 | output: image 134 | push: ${{ github.event_name != 'pull_request' }} 135 | sbom: true 136 | sign: false 137 | target: hello-cross 138 | meta-images: | 139 | public.ecr.aws/q3b5f1u4/test-docker-action 140 | meta-tags: | 141 | type=raw,value=bake-ghbuilder-nosign-${{ github.run_id }} 142 | secrets: 143 | registry-auths: | 144 | - registry: public.ecr.aws 145 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 146 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 147 | 148 | bake-aws-nosign-verify: 149 | uses: ./.github/workflows/verify.yml 150 | if: ${{ github.event_name != 'pull_request' }} 151 | needs: 152 | - bake-aws-nosign 153 | with: 154 | builder-outputs: ${{ toJSON(needs.bake-aws-nosign.outputs) }} 155 | secrets: 156 | registry-auths: | 157 | - registry: public.ecr.aws 158 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 159 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 160 | 161 | bake-aws-nosign-outputs: 162 | runs-on: ubuntu-24.04 163 | needs: 164 | - bake-aws-nosign 165 | steps: 166 | - 167 | name: Builder outputs 168 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 169 | env: 170 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-aws-nosign.outputs) }} 171 | with: 172 | script: | 173 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 174 | core.info(JSON.stringify(builderOutputs, null, 2)); 175 | 176 | bake-ghcr-and-aws: 177 | uses: ./.github/workflows/bake.yml 178 | permissions: 179 | contents: read 180 | id-token: write 181 | packages: write 182 | with: 183 | context: test 184 | output: image 185 | push: ${{ github.event_name != 'pull_request' }} 186 | sbom: true 187 | target: hello-cross 188 | meta-images: | 189 | ghcr.io/docker/github-builder-test 190 | public.ecr.aws/q3b5f1u4/test-docker-action 191 | meta-tags: | 192 | type=raw,value=${{ github.run_id }},prefix=bake-ghcr-and-aws- 193 | secrets: 194 | registry-auths: | 195 | - registry: ghcr.io 196 | username: ${{ github.actor }} 197 | password: ${{ secrets.GITHUB_TOKEN }} 198 | - registry: public.ecr.aws 199 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 200 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 201 | 202 | bake-ghcr-and-aws-verify: 203 | uses: ./.github/workflows/verify.yml 204 | if: ${{ github.event_name != 'pull_request' }} 205 | needs: 206 | - bake-ghcr-and-aws 207 | with: 208 | builder-outputs: ${{ toJSON(needs.bake-ghcr-and-aws.outputs) }} 209 | secrets: 210 | registry-auths: | 211 | - registry: ghcr.io 212 | username: ${{ github.actor }} 213 | password: ${{ secrets.GITHUB_TOKEN }} 214 | - registry: public.ecr.aws 215 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 216 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 217 | 218 | bake-ghcr-and-aws-outputs: 219 | runs-on: ubuntu-24.04 220 | needs: 221 | - bake-ghcr-and-aws 222 | steps: 223 | - 224 | name: Builder outputs 225 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 226 | env: 227 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-ghcr-and-aws.outputs) }} 228 | with: 229 | script: | 230 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 231 | core.info(JSON.stringify(builderOutputs, null, 2)); 232 | 233 | bake-local: 234 | uses: ./.github/workflows/bake.yml 235 | permissions: 236 | contents: read 237 | id-token: write 238 | with: 239 | artifact-name: bake-output 240 | artifact-upload: true 241 | context: test 242 | output: local 243 | sbom: true 244 | sign: ${{ github.event_name != 'pull_request' }} 245 | target: hello-cross 246 | 247 | bake-local-verify: 248 | uses: ./.github/workflows/verify.yml 249 | if: ${{ github.event_name != 'pull_request' }} 250 | needs: 251 | - bake-local 252 | with: 253 | builder-outputs: ${{ toJSON(needs.bake-local.outputs) }} 254 | 255 | bake-local-outputs: 256 | runs-on: ubuntu-24.04 257 | needs: 258 | - bake-local 259 | steps: 260 | - 261 | name: Builder outputs 262 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 263 | env: 264 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-local.outputs) }} 265 | with: 266 | script: | 267 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 268 | core.info(JSON.stringify(builderOutputs, null, 2)); 269 | 270 | bake-local-single: 271 | uses: ./.github/workflows/bake.yml 272 | permissions: 273 | contents: read 274 | id-token: write 275 | with: 276 | artifact-name: bake-single-output 277 | artifact-upload: true 278 | context: test 279 | output: local 280 | sbom: true 281 | sign: ${{ github.event_name != 'pull_request' }} 282 | target: hello 283 | 284 | bake-local-single-verify: 285 | uses: ./.github/workflows/verify.yml 286 | if: ${{ github.event_name != 'pull_request' }} 287 | needs: 288 | - bake-local-single 289 | with: 290 | builder-outputs: ${{ toJSON(needs.bake-local-single.outputs) }} 291 | 292 | bake-local-single-outputs: 293 | runs-on: ubuntu-24.04 294 | needs: 295 | - bake-local-single 296 | steps: 297 | - 298 | name: Builder outputs 299 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 300 | env: 301 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-local-single.outputs) }} 302 | with: 303 | script: | 304 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 305 | core.info(JSON.stringify(builderOutputs, null, 2)); 306 | 307 | bake-local-noupload: 308 | uses: ./.github/workflows/bake.yml 309 | permissions: 310 | contents: read 311 | id-token: write 312 | with: 313 | artifact-upload: false 314 | context: test 315 | output: local 316 | sbom: true 317 | target: hello-cross 318 | 319 | bake-local-noupload-verify: 320 | uses: ./.github/workflows/verify.yml 321 | needs: 322 | - bake-local-noupload 323 | with: 324 | builder-outputs: ${{ toJSON(needs.bake-local-noupload.outputs) }} 325 | 326 | bake-local-noupload-outputs: 327 | runs-on: ubuntu-24.04 328 | needs: 329 | - bake-local-noupload 330 | steps: 331 | - 332 | name: Builder outputs 333 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 334 | env: 335 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.bake-local-noupload.outputs) }} 336 | with: 337 | script: | 338 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 339 | core.info(JSON.stringify(builderOutputs, null, 2)); 340 | 341 | bake-local-nosign: 342 | uses: ./.github/workflows/bake.yml 343 | permissions: 344 | contents: read 345 | id-token: write 346 | with: 347 | artifact-name: bake-nosign-output 348 | artifact-upload: true 349 | context: test 350 | output: local 351 | sbom: true 352 | sign: false 353 | target: hello-cross 354 | 355 | bake-local-nosign-verify: 356 | uses: ./.github/workflows/verify.yml 357 | needs: 358 | - bake-local-nosign 359 | with: 360 | builder-outputs: ${{ toJSON(needs.bake-local-nosign.outputs) }} 361 | 362 | build-local-nosign-outputs: 363 | runs-on: ubuntu-24.04 364 | needs: 365 | - bake-local-nosign 366 | steps: 367 | - 368 | name: Builder outputs 369 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 370 | env: 371 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-local-nosign.outputs) }} 372 | with: 373 | script: | 374 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 375 | core.info(JSON.stringify(builderOutputs, null, 2)); 376 | 377 | bake-set-runner: 378 | uses: ./.github/workflows/bake.yml 379 | permissions: 380 | contents: read 381 | id-token: write 382 | with: 383 | runner: amd64 384 | context: test 385 | output: image 386 | push: false 387 | target: hello-cross 388 | meta-images: | 389 | public.ecr.aws/q3b5f1u4/test-docker-action 390 | meta-tags: | 391 | type=raw,value=bake-ghbuilder-${{ github.run_id }} 392 | -------------------------------------------------------------------------------- /.github/workflows/.test-build.yml: -------------------------------------------------------------------------------- 1 | name: .test-build 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - 'main' 12 | - 'releases/v*' 13 | tags: 14 | - 'v*' 15 | paths: 16 | - '.github/workflows/.test-build.yml' 17 | - '.github/workflows/build.yml' 18 | - '.github/workflows/verify.yml' 19 | - 'test/**' 20 | pull_request: 21 | paths: 22 | - '.github/workflows/.test-build.yml' 23 | - '.github/workflows/build.yml' 24 | - '.github/workflows/verify.yml' 25 | - 'test/**' 26 | 27 | jobs: 28 | build-aws-single: 29 | uses: ./.github/workflows/build.yml 30 | permissions: 31 | contents: read 32 | id-token: write 33 | with: 34 | file: test/hello.Dockerfile 35 | output: image 36 | push: ${{ github.event_name != 'pull_request' }} 37 | sbom: true 38 | meta-images: | 39 | public.ecr.aws/q3b5f1u4/test-docker-action 40 | meta-tags: | 41 | type=raw,value=build-ghbuilder-single-${{ github.run_id }} 42 | 43 | secrets: 44 | registry-auths: | 45 | - registry: public.ecr.aws 46 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 47 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 48 | 49 | build-aws-single-verify: 50 | uses: ./.github/workflows/verify.yml 51 | if: ${{ github.event_name != 'pull_request' }} 52 | needs: 53 | - build-aws-single 54 | with: 55 | builder-outputs: ${{ toJSON(needs.build-aws-single.outputs) }} 56 | secrets: 57 | registry-auths: | 58 | - registry: public.ecr.aws 59 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 60 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 61 | 62 | build-aws-single-outputs: 63 | runs-on: ubuntu-24.04 64 | needs: 65 | - build-aws-single 66 | steps: 67 | - 68 | name: Builder outputs 69 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 70 | env: 71 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-aws-single.outputs) }} 72 | with: 73 | script: | 74 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 75 | core.info(JSON.stringify(builderOutputs, null, 2)); 76 | 77 | build-aws: 78 | uses: ./.github/workflows/build.yml 79 | permissions: 80 | contents: read 81 | id-token: write 82 | with: 83 | file: test/hello.Dockerfile 84 | output: image 85 | platforms: linux/amd64,linux/arm64 86 | push: ${{ github.event_name != 'pull_request' }} 87 | sbom: true 88 | meta-images: | 89 | public.ecr.aws/q3b5f1u4/test-docker-action 90 | meta-tags: | 91 | type=raw,value=build-ghbuilder-${{ github.run_id }} 92 | secrets: 93 | registry-auths: | 94 | - registry: public.ecr.aws 95 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 96 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 97 | 98 | build-aws-verify: 99 | uses: ./.github/workflows/verify.yml 100 | if: ${{ github.event_name != 'pull_request' }} 101 | needs: 102 | - build-aws 103 | with: 104 | builder-outputs: ${{ toJSON(needs.build-aws.outputs) }} 105 | secrets: 106 | registry-auths: | 107 | - registry: public.ecr.aws 108 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 109 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 110 | 111 | build-aws-outputs: 112 | runs-on: ubuntu-24.04 113 | needs: 114 | - build-aws 115 | steps: 116 | - 117 | name: Builder outputs 118 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 119 | env: 120 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-aws.outputs) }} 121 | with: 122 | script: | 123 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 124 | core.info(JSON.stringify(builderOutputs, null, 2)); 125 | 126 | build-aws-nosign: 127 | uses: ./.github/workflows/build.yml 128 | permissions: 129 | contents: read 130 | id-token: write 131 | with: 132 | file: test/hello.Dockerfile 133 | output: image 134 | platforms: linux/amd64,linux/arm64 135 | push: ${{ github.event_name != 'pull_request' }} 136 | sbom: true 137 | sign: false 138 | meta-images: | 139 | public.ecr.aws/q3b5f1u4/test-docker-action 140 | meta-tags: | 141 | type=raw,value=build-ghbuilder-nosign--${{ github.run_id }} 142 | secrets: 143 | registry-auths: | 144 | - registry: public.ecr.aws 145 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 146 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 147 | 148 | build-aws-nosign-verify: 149 | uses: ./.github/workflows/verify.yml 150 | if: ${{ github.event_name != 'pull_request' }} 151 | needs: 152 | - build-aws-nosign 153 | with: 154 | builder-outputs: ${{ toJSON(needs.build-aws-nosign.outputs) }} 155 | secrets: 156 | registry-auths: | 157 | - registry: public.ecr.aws 158 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 159 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 160 | 161 | build-aws-nosign-outputs: 162 | runs-on: ubuntu-24.04 163 | needs: 164 | - build-aws-nosign 165 | steps: 166 | - 167 | name: Builder outputs 168 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 169 | env: 170 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-aws-nosign.outputs) }} 171 | with: 172 | script: | 173 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 174 | core.info(JSON.stringify(builderOutputs, null, 2)); 175 | 176 | build-ghcr: 177 | uses: ./.github/workflows/build.yml 178 | permissions: 179 | contents: read 180 | id-token: write 181 | packages: write 182 | with: 183 | file: test/hello.Dockerfile 184 | output: image 185 | platforms: linux/amd64,linux/arm64 186 | push: ${{ github.event_name != 'pull_request' }} 187 | sbom: true 188 | meta-images: ghcr.io/docker/github-builder-test 189 | meta-tags: | 190 | type=raw,value=build-${{ github.run_id }} 191 | secrets: 192 | registry-auths: | 193 | - registry: ghcr.io 194 | username: ${{ github.actor }} 195 | password: ${{ secrets.GITHUB_TOKEN }} 196 | 197 | build-ghcr-verify: 198 | uses: ./.github/workflows/verify.yml 199 | if: ${{ github.event_name != 'pull_request' }} 200 | needs: 201 | - build-ghcr 202 | with: 203 | builder-outputs: ${{ toJSON(needs.build-ghcr.outputs) }} 204 | secrets: 205 | registry-auths: | 206 | - registry: ghcr.io 207 | username: ${{ github.actor }} 208 | password: ${{ secrets.GITHUB_TOKEN }} 209 | 210 | build-ghcr-outputs: 211 | runs-on: ubuntu-24.04 212 | needs: 213 | - build-ghcr 214 | steps: 215 | - 216 | name: Builder outputs 217 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 218 | env: 219 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-ghcr.outputs) }} 220 | with: 221 | script: | 222 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 223 | core.info(JSON.stringify(builderOutputs, null, 2)); 224 | 225 | build-dockerhub-stage: 226 | uses: ./.github/workflows/build.yml 227 | permissions: 228 | contents: read 229 | id-token: write 230 | with: 231 | file: test/hello.Dockerfile 232 | output: image 233 | platforms: linux/amd64,linux/arm64 234 | push: ${{ github.event_name != 'pull_request' }} 235 | sbom: true 236 | meta-images: registry-1-stage.docker.io/docker/github-builder-test 237 | meta-tags: | 238 | type=raw,value=build-${{ github.run_id }} 239 | secrets: 240 | registry-auths: | 241 | - registry: registry-1-stage.docker.io 242 | username: ${{ vars.DOCKERHUB_STAGE_USERNAME }} 243 | password: ${{ secrets.DOCKERHUB_STAGE_TOKEN }} 244 | 245 | build-dockerhub-stage-verify: 246 | uses: ./.github/workflows/verify.yml 247 | if: ${{ github.event_name != 'pull_request' }} 248 | needs: 249 | - build-dockerhub-stage 250 | with: 251 | builder-outputs: ${{ toJSON(needs.build-dockerhub-stage.outputs) }} 252 | secrets: 253 | registry-auths: | 254 | - registry: registry-1-stage.docker.io 255 | username: ${{ vars.DOCKERHUB_STAGE_USERNAME }} 256 | password: ${{ secrets.DOCKERHUB_STAGE_TOKEN }} 257 | 258 | build-dockerhub-stage-outputs: 259 | runs-on: ubuntu-24.04 260 | needs: 261 | - build-dockerhub-stage 262 | steps: 263 | - 264 | name: Builder outputs 265 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 266 | env: 267 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-dockerhub-stage.outputs) }} 268 | with: 269 | script: | 270 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 271 | core.info(JSON.stringify(builderOutputs, null, 2)); 272 | 273 | build-ghcr-and-aws: 274 | uses: ./.github/workflows/build.yml 275 | permissions: 276 | contents: read 277 | id-token: write 278 | packages: write 279 | with: 280 | file: test/hello.Dockerfile 281 | output: image 282 | platforms: linux/amd64,linux/arm64 283 | push: ${{ github.event_name != 'pull_request' }} 284 | sbom: true 285 | meta-images: | 286 | ghcr.io/docker/github-builder-test 287 | public.ecr.aws/q3b5f1u4/test-docker-action 288 | meta-tags: | 289 | type=raw,value=${{ github.run_id }},prefix=build-ghcr-and-aws- 290 | secrets: 291 | registry-auths: | 292 | - registry: ghcr.io 293 | username: ${{ github.actor }} 294 | password: ${{ secrets.GITHUB_TOKEN }} 295 | - registry: public.ecr.aws 296 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 297 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 298 | 299 | build-ghcr-and-aws-verify: 300 | uses: ./.github/workflows/verify.yml 301 | if: ${{ github.event_name != 'pull_request' }} 302 | needs: 303 | - build-ghcr-and-aws 304 | with: 305 | builder-outputs: ${{ toJSON(needs.build-ghcr-and-aws.outputs) }} 306 | secrets: 307 | registry-auths: | 308 | - registry: ghcr.io 309 | username: ${{ github.actor }} 310 | password: ${{ secrets.GITHUB_TOKEN }} 311 | - registry: public.ecr.aws 312 | username: ${{ secrets.AWS_ACCESS_KEY_ID }} 313 | password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 314 | 315 | build-ghcr-and-aws-outputs: 316 | runs-on: ubuntu-24.04 317 | needs: 318 | - build-ghcr-and-aws 319 | steps: 320 | - 321 | name: Builder outputs 322 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 323 | env: 324 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-ghcr-and-aws.outputs) }} 325 | with: 326 | script: | 327 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 328 | core.info(JSON.stringify(builderOutputs, null, 2)); 329 | 330 | build-local: 331 | uses: ./.github/workflows/build.yml 332 | permissions: 333 | contents: read 334 | id-token: write 335 | with: 336 | artifact-name: build-output 337 | artifact-upload: true 338 | file: test/hello.Dockerfile 339 | output: local 340 | platforms: linux/amd64,linux/arm64 341 | sbom: true 342 | sign: ${{ github.event_name != 'pull_request' }} 343 | 344 | build-local-verify: 345 | uses: ./.github/workflows/verify.yml 346 | if: ${{ github.event_name != 'pull_request' }} 347 | needs: 348 | - build-local 349 | with: 350 | builder-outputs: ${{ toJSON(needs.build-local.outputs) }} 351 | 352 | build-local-outputs: 353 | runs-on: ubuntu-24.04 354 | needs: 355 | - build-local 356 | steps: 357 | - 358 | name: Builder outputs 359 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 360 | env: 361 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.local.outputs) }} 362 | with: 363 | script: | 364 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 365 | core.info(JSON.stringify(builderOutputs, null, 2)); 366 | 367 | build-local-single: 368 | uses: ./.github/workflows/build.yml 369 | permissions: 370 | contents: read 371 | id-token: write 372 | with: 373 | artifact-name: build-single-output 374 | artifact-upload: true 375 | file: test/hello.Dockerfile 376 | output: local 377 | sbom: true 378 | sign: ${{ github.event_name != 'pull_request' }} 379 | 380 | build-local-single-verify: 381 | uses: ./.github/workflows/verify.yml 382 | if: ${{ github.event_name != 'pull_request' }} 383 | needs: 384 | - build-local-single 385 | with: 386 | builder-outputs: ${{ toJSON(needs.build-local-single.outputs) }} 387 | 388 | build-local-single-outputs: 389 | runs-on: ubuntu-24.04 390 | needs: 391 | - build-local-single 392 | steps: 393 | - 394 | name: Builder outputs 395 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 396 | env: 397 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-local-single.outputs) }} 398 | with: 399 | script: | 400 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 401 | core.info(JSON.stringify(builderOutputs, null, 2)); 402 | 403 | build-local-noupload: 404 | uses: ./.github/workflows/build.yml 405 | permissions: 406 | contents: read 407 | id-token: write 408 | with: 409 | artifact-upload: false 410 | file: test/hello.Dockerfile 411 | output: local 412 | platforms: linux/amd64,linux/arm64 413 | sbom: true 414 | 415 | build-local-noupload-verify: 416 | uses: ./.github/workflows/verify.yml 417 | needs: 418 | - build-local-noupload 419 | with: 420 | builder-outputs: ${{ toJSON(needs.build-local-noupload.outputs) }} 421 | 422 | build-local-noupload-outputs: 423 | runs-on: ubuntu-24.04 424 | needs: 425 | - build-local-noupload 426 | steps: 427 | - 428 | name: Builder outputs 429 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 430 | env: 431 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-local-noupload.outputs) }} 432 | with: 433 | script: | 434 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 435 | core.info(JSON.stringify(builderOutputs, null, 2)); 436 | 437 | build-local-nosign: 438 | uses: ./.github/workflows/build.yml 439 | permissions: 440 | contents: read 441 | id-token: write 442 | with: 443 | artifact-name: build-nosign-output 444 | artifact-upload: true 445 | file: test/hello.Dockerfile 446 | output: local 447 | platforms: linux/amd64,linux/arm64 448 | sbom: true 449 | sign: false 450 | 451 | build-local-nosign-verify: 452 | uses: ./.github/workflows/verify.yml 453 | needs: 454 | - build-local-nosign 455 | with: 456 | builder-outputs: ${{ toJSON(needs.build-local-nosign.outputs) }} 457 | 458 | build-local-nosign-outputs: 459 | runs-on: ubuntu-24.04 460 | needs: 461 | - build-local-nosign 462 | steps: 463 | - 464 | name: Builder outputs 465 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 466 | env: 467 | INPUT_BUILDER-OUTPUTS: ${{ toJSON(needs.build-local-nosign.outputs) }} 468 | with: 469 | script: | 470 | const builderOutputs = JSON.parse(core.getInput('builder-outputs')); 471 | core.info(JSON.stringify(builderOutputs, null, 2)); 472 | 473 | build-set-runner: 474 | uses: ./.github/workflows/build.yml 475 | permissions: 476 | contents: read 477 | id-token: write 478 | with: 479 | runner: amd64 480 | file: test/hello.Dockerfile 481 | output: image 482 | platforms: linux/amd64,linux/arm64 483 | push: false 484 | meta-images: ghcr.io/docker/github-builder-test 485 | meta-tags: | 486 | type=raw,value=build-${{ github.run_id }} 487 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test build workflow](https://img.shields.io/github/actions/workflow/status/docker/github-builder-experimental/.test-build.yml?label=test%20build&logo=github&style=flat-square)](https://github.com/docker/github-builder-experimental/actions?workflow=.test-build) 2 | [![Test bake workflow](https://img.shields.io/github/actions/workflow/status/docker/github-builder-experimental/.test-bake.yml?label=test%20bake&logo=github&style=flat-square)](https://github.com/docker/github-builder-experimental/actions?workflow=.test-bake) 3 | 4 | > [!CAUTION] 5 | > Do not use it for your production workflows yet! 6 | 7 | ## :test_tube: Experimental 8 | 9 | This repository is considered **EXPERIMENTAL** and under active development 10 | until further notice. It is subject to non-backward compatible changes or 11 | removal in any future version. 12 | 13 | ___ 14 | 15 | * [Overview](#overview) 16 | * [Key Advantages](#key-advantages) 17 | * [Performance](#performance) 18 | * [Security](#security) 19 | * [Isolation & Reliability](#isolation--reliability) 20 | * [Usage](#usage) 21 | * [Build reusable workflow](#build-reusable-workflow) 22 | * [Inputs](#inputs) 23 | * [Secrets](#secrets) 24 | * [Bake reusable workflow](#bake-reusable-workflow) 25 | * [Inputs](#inputs) 26 | * [Secrets](#secrets) 27 | 28 | ## Overview 29 | 30 | This repository provides official Docker-maintained [reusable GitHub Actions workflows](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows) 31 | to securely build container images and artifacts using Docker best practices. 32 | The reusable workflows incorporate functionality from our GitHub Actions like 33 | [`docker/build-push-action`](https://github.com/docker/build-push-action/), 34 | [`docker/metadata-action`](https://github.com/docker/metadata-action/), etc., 35 | into a single workflow: 36 | 37 | ```yaml 38 | name: ci 39 | 40 | permissions: 41 | contents: read 42 | 43 | on: 44 | push: 45 | branches: 46 | - 'main' 47 | tags: 48 | - 'v*' 49 | pull_request: 50 | 51 | build: 52 | uses: docker/github-builder-experimental/.github/workflows/build.yml@main 53 | permissions: 54 | contents: read # to fetch the repository content 55 | id-token: write # for signing attestation(s) with GitHub OIDC Token 56 | with: 57 | output: image 58 | push: ${{ github.event_name != 'pull_request' }} 59 | meta-images: name/app 60 | secrets: 61 | registry-auths: | 62 | - registry: docker.io 63 | username: ${{ vars.DOCKERHUB_USERNAME }} 64 | password: ${{ secrets.DOCKERHUB_TOKEN }} 65 | ``` 66 | 67 | This workflow provides a trusted BuildKit instance and generates signed 68 | SLSA-compliant provenance attestations, guaranteeing the build happened from 69 | the source commit and all build steps ran in isolated sandboxed environments 70 | from immutable sources. This enables GitHub projects to follow a seamless path 71 | toward higher levels of security and trust. 72 | 73 | ## Key Advantages 74 | 75 | ### Performance 76 | 77 | * **Native parallelization for multi-platform builds.** 78 | Workflows can automatically distribute builds across runners based on target 79 | platform to be built, improving throughput for other architectures without 80 | requiring emulation or [custom CI logic](https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners) 81 | or self-managed runners. 82 | 83 | * **Centralized build configuration.** 84 | Repositories no longer need to configure buildx drivers, tune storage, or 85 | adjust resource limits. The reusable workflows encapsulate the recommended 86 | configuration, providing fast, consistent builds across any project that 87 | opts in. 88 | 89 | ### Security 90 | 91 | * **Trusted workflows in the Docker organization.** 92 | Builds are executed by reusable workflows defined in the [**@docker**](https://github.com/docker) 93 | organization, not by arbitrary user-defined workflow steps. Consumers can 94 | rely on GitHub's trust model and repository protections on the Docker side 95 | (branch protection, code review, signing, etc.) to reason about who controls 96 | the build logic. 97 | 98 | * **Verifiable, immutable sources.** 99 | The workflows use the GitHub OIDC token and the exact commit SHA to obtain 100 | source and to bind it into SLSA provenance. This ensures that the build is 101 | tied to the repository contents as checked in—no additional CI step can 102 | silently swap out what is being built. 103 | 104 | * **Signed SLSA provenance for every build.** 105 | BuildKit generates [SLSA-compliant provenance attestation](https://docs.docker.com/build/metadata/attestations/slsa-provenance/) 106 | artifacts that are signed with an identity bound to the GitHub workflow. 107 | Downstream consumers can verify: 108 | - which builder commit produced the image 109 | - which source code commit produced the image 110 | - which workflow and job executed the build 111 | - what inputs and build parameters were used 112 | 113 | * **Protection from user workflow tampering.** 114 | The build steps are pre-defined and optimized in the reusable workflow, and 115 | cannot be altered by user configuration. This protects against tampering: 116 | preventing untrusted workflow steps from modifying build logic, injecting 117 | unexpected flags, or producing misleading provenance. 118 | 119 | ### Isolation & Reliability 120 | 121 | * **Separation between user CI logic and build logic.** 122 | The user's workflow orchestrates *when* to build but not *how* to build. 123 | The actual build steps live in the Docker-maintained reusable workflows, 124 | which cannot be modified from the consuming repository. 125 | 126 | * **Immutable, reproducible build pipeline.** 127 | Builds are driven by declarative inputs (repository commit, build 128 | configuration, workflow version). This leads to: 129 | - reproducibility (same workflow + same inputs → same outputs) 130 | - auditability (inputs and workflow identity recorded in provenance) 131 | - reliability (less dependence on ad-hoc per-repo CI scripting) 132 | 133 | * **Reduced CI variability and config drift.** 134 | By reusing the same workflows, projects avoid maintaining custom build logic 135 | per repository. Caching, provenance, SBOM generation, and build settings 136 | behave uniformly across all adopters. 137 | 138 | * **Higher assurance for downstream consumers.** 139 | Because artifacts are produced by a workflow in the [**@docker**](https://github.com/docker) 140 | organization, with SLSA provenance attached, consumers can verify both the 141 | *source commit* and the *builder identity* before trusting or promoting an 142 | image, an essential part of supply-chain hardening. 143 | 144 | ## Usage 145 | 146 | ### Build reusable workflow 147 | 148 | The [`build.yml` reusable workflow](.github/workflows/build.yml) lets you build 149 | container images and artifacts from a Dockerfile with a user experience similar 150 | to [`docker/build-push-action`](https://github.com/docker/build-push-action/). 151 | It provides a Docker-maintained, opinionated build pipeline that applies best 152 | practices for security, performance, and reliability by default, including 153 | isolated execution and signed SLSA provenance while keeping per-repository 154 | configuration minimal. 155 | 156 | ```yaml 157 | name: ci 158 | 159 | permissions: 160 | contents: read 161 | 162 | on: 163 | push: 164 | branches: 165 | - 'main' 166 | tags: 167 | - 'v*' 168 | pull_request: 169 | 170 | build: 171 | uses: docker/github-builder-experimental/.github/workflows/build.yml@main 172 | permissions: 173 | contents: read # to fetch the repository content 174 | id-token: write # for signing attestation(s) with GitHub OIDC Token 175 | with: 176 | output: image 177 | push: ${{ github.event_name != 'pull_request' }} 178 | platforms: linux/amd64,linux/arm64 179 | meta-images: name/app 180 | meta-tags: | 181 | type=ref,event=branch 182 | type=ref,event=pr 183 | type=semver,pattern={{version}} 184 | secrets: 185 | registry-auths: | 186 | - registry: docker.io 187 | username: ${{ vars.DOCKERHUB_USERNAME }} 188 | password: ${{ secrets.DOCKERHUB_TOKEN }} 189 | 190 | # Optional job to verify the pushed images' signatures. This is already done 191 | # in the `build` job and can be omitted. It's provided here as an example of 192 | # how to use the `verify.yml` reusable workflow. 193 | build-verify: 194 | uses: docker/github-builder-experimental/.github/workflows/verify.yml@main 195 | if: ${{ github.event_name != 'pull_request' }} 196 | needs: 197 | - build 198 | with: 199 | builder-outputs: ${{ toJSON(needs.build.outputs) }} 200 | secrets: 201 | registry-auths: | 202 | - registry: docker.io 203 | username: ${{ vars.DOCKERHUB_USERNAME }} 204 | password: ${{ secrets.DOCKERHUB_TOKEN }} 205 | ``` 206 | 207 | #### Inputs 208 | 209 | > [!NOTE] 210 | > `List` type is a newline-delimited string 211 | > ```yaml 212 | > cache-from: | 213 | > user/app:cache 214 | > type=local,src=path/to/dir 215 | > ``` 216 | > 217 | > `CSV` type is a comma-delimited string 218 | > ```yaml 219 | > tags: name/app:latest,name/app:1.0.0 220 | > ``` 221 | 222 | | Name | Type | Default | Description | 223 | |------------------------|----------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 224 | | `runner` | String | `auto` | [Ubuntu GitHub Hosted Runner](https://github.com/actions/runner-images?tab=readme-ov-file#available-images) to build on (one of `auto`, `amd64`, `arm64`). The `auto` runner selects the best-matching runner based on target `platforms`. You can set it to `amd64` if your build doesn't require emulation (e.g. cross-compilation) | 225 | | `setup-qemu` | Bool | `false` | Runs the `setup-qemu-action` step to install QEMU static binaries | 226 | | `artifact-name` | String | `docker-github-builder-assets` | Name of the uploaded GitHub artifact (for `local` output) | 227 | | `artifact-upload` | Bool | `false` | Upload build output GitHub artifact (for `local` output) | 228 | | `annotations` | List | | List of annotations to set to the image (for `image` output) | 229 | | `build-args` | List | `auto` | List of [build-time variables](https://docs.docker.com/engine/reference/commandline/buildx_build/#build-arg). If you want to set a build-arg through an environment variable, use the `envs` input | 230 | | `context` | String | `.` | Context to build from in the Git working tree | 231 | | `file` | String | `{context}/Dockerfile` | Path to the Dockerfile | 232 | | `labels` | List | | List of labels for an image (for `image` output) | 233 | | `output` | String | | Build output destination (one of [`image`](https://docs.docker.com/build/exporters/image-registry/) or [`local`](https://docs.docker.com/build/exporters/local-tar/)). Unlike the `build-push-action`, it only accepts `image` or `local`. The reusable workflow takes care of setting the `outputs` attribute | 234 | | `platforms` | List/CSV | | List of [target platforms](https://docs.docker.com/engine/reference/commandline/buildx_build/#platform) to build | 235 | | `push` | Bool | `false` | [Push](https://docs.docker.com/engine/reference/commandline/buildx_build/#push) image to the registry (for `image` output) | 236 | | `sbom` | Bool | `false` | Generate [SBOM](https://docs.docker.com/build/attestations/sbom/) attestation for the build | 237 | | `shm-size` | String | | Size of [`/dev/shm`](https://docs.docker.com/engine/reference/commandline/buildx_build/#shm-size) (e.g., `2g`) | 238 | | `sign` | String | `auto` | Sign attestation manifest for `image` output or artifacts for `local` output, can be one of `auto`, `true` or `false`. The `auto` mode will enable signing if `push` is enabled for pushing the `image` or if `artifact-upload` is enabled for uploading the `local` build output as GitHub Artifact | 239 | | `target` | String | | Sets the target stage to build | 240 | | `ulimit` | List | | [Ulimit](https://docs.docker.com/engine/reference/commandline/buildx_build/#ulimit) options (e.g., `nofile=1024:1024`) | 241 | | `set-meta-annotations` | Bool | `false` | Append OCI Image Format Specification annotations generated by `docker/metadata-action` | 242 | | `set-meta-labels` | Bool | `false` | Append OCI Image Format Specification labels generated by `docker/metadata-action` | 243 | | `meta-images` | List | | [List of images](https://github.com/docker/metadata-action?tab=readme-ov-file#images-input) to use as base name for tags (required for image output) | 244 | | `meta-tags` | List | | [List of tags](https://github.com/docker/metadata-action?tab=readme-ov-file#tags-input) as key-value pair attributes | 245 | | `meta-flavor` | List | | [Flavor](https://github.com/docker/metadata-action?tab=readme-ov-file#flavor-input) defines a global behavior for `meta-tags` | 246 | 247 | #### Secrets 248 | 249 | | Name | Default | Description | 250 | |------------------|-----------------------|--------------------------------------------------------------------------------| 251 | | `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) | 252 | | `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context | 253 | 254 | ### Bake reusable workflow 255 | 256 | The [`bake.yml` reusable workflow](.github/workflows/build.yml) lets you build 257 | container images and artifacts from a [Bake definition](https://docs.docker.com/build/bake/) 258 | with a user experience similar to [`docker/bake-action`](https://github.com/docker/bake-action/). 259 | It provides a Docker-maintained, opinionated build pipeline that applies best 260 | practices for security, performance, and reliability by default, including 261 | isolated execution and signed SLSA provenance while keeping per-repository 262 | configuration minimal. 263 | 264 | ```yaml 265 | name: ci 266 | 267 | permissions: 268 | contents: read 269 | 270 | on: 271 | push: 272 | branches: 273 | - 'main' 274 | tags: 275 | - 'v*' 276 | pull_request: 277 | 278 | bake: 279 | uses: docker/github-builder-experimental/.github/workflows/bake.yml@main 280 | permissions: 281 | contents: read # to fetch the repository content 282 | id-token: write # for signing attestation(s) with GitHub OIDC Token 283 | with: 284 | output: image 285 | push: ${{ github.event_name != 'pull_request' }} 286 | meta-images: name/app 287 | meta-tags: | 288 | type=ref,event=branch 289 | type=ref,event=pr 290 | type=semver,pattern={{version}} 291 | secrets: 292 | registry-auths: | 293 | - registry: docker.io 294 | username: ${{ vars.DOCKERHUB_USERNAME }} 295 | password: ${{ secrets.DOCKERHUB_TOKEN }} 296 | 297 | # Optional job to verify the pushed images' signatures. This is already done 298 | # in the `bake` job and can be omitted. It's provided here as an example of 299 | # how to use the `verify.yml` reusable workflow. 300 | bake-verify: 301 | uses: docker/github-builder-experimental/.github/workflows/verify.yml@main 302 | if: ${{ github.event_name != 'pull_request' }} 303 | needs: 304 | - bake 305 | with: 306 | builder-outputs: ${{ toJSON(needs.bake.outputs) }} 307 | secrets: 308 | registry-auths: | 309 | - registry: docker.io 310 | username: ${{ vars.DOCKERHUB_USERNAME }} 311 | password: ${{ secrets.DOCKERHUB_TOKEN }} 312 | ``` 313 | 314 | #### Inputs 315 | 316 | > `List` type is a newline-delimited string 317 | > ```yaml 318 | > set: target.args.mybuildarg=value 319 | > ``` 320 | > ```yaml 321 | > set: | 322 | > target.args.mybuildarg=value 323 | > foo*.args.mybuildarg=value 324 | > ``` 325 | 326 | | Name | Type | Default | Description | 327 | |------------------------|--------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 328 | | `runner` | String | `auto` | [Ubuntu GitHub Hosted Runner](https://github.com/actions/runner-images?tab=readme-ov-file#available-images) to build on (one of `auto`, `amd64`, `arm64`). The `auto` runner selects the best-matching runner based on target `platforms`. You can set it to `amd64` if your build doesn't require emulation (e.g. cross-compilation) | 329 | | `setup-qemu` | Bool | `false` | Runs the `setup-qemu-action` step to install QEMU static binaries | 330 | | `artifact-name` | String | `docker-github-builder-assets` | Name of the uploaded GitHub artifact (for `local` output) | 331 | | `artifact-upload` | Bool | `false` | Upload build output GitHub artifact (for `local` output) | 332 | | `context` | String | `.` | Context to build from in the Git working tree | 333 | | `files` | List | `{context}/docker-bake.hcl` | List of bake definition files | 334 | | `output` | String | | Build output destination (one of [`image`](https://docs.docker.com/build/exporters/image-registry/) or [`local`](https://docs.docker.com/build/exporters/local-tar/)). | 335 | | `push` | Bool | `false` | Push image to the registry (for `image` output) | 336 | | `sbom` | Bool | `false` | Generate [SBOM](https://docs.docker.com/build/attestations/sbom/) attestation for the build | 337 | | `set` | List | | List of [target values to override](https://docs.docker.com/engine/reference/commandline/buildx_bake/#set) (e.g., `targetpattern.key=value`) | 338 | | `sign` | String | `auto` | Sign attestation manifest for `image` output or artifacts for `local` output, can be one of `auto`, `true` or `false`. The `auto` mode will enable signing if `push` is enabled for pushing the `image` or if `artifact-upload` is enabled for uploading the `local` build output as GitHub Artifact | 339 | | `target` | String | `default` | Bake target to build | 340 | | `set-meta-annotations` | Bool | `false` | Append OCI Image Format Specification annotations generated by `docker/metadata-action` | 341 | | `set-meta-labels` | Bool | `false` | Append OCI Image Format Specification labels generated by `docker/metadata-action` | 342 | | `meta-images` | List | | [List of images](https://github.com/docker/metadata-action?tab=readme-ov-file#images-input) to use as base name for tags (required for image output) | 343 | | `meta-tags` | List | | [List of tags](https://github.com/docker/metadata-action?tab=readme-ov-file#tags-input) as key-value pair attributes | 344 | | `meta-labels` | List | | [List of custom labels](https://github.com/docker/metadata-action?tab=readme-ov-file#overwrite-labels-and-annotations) | 345 | | `meta-annotations` | List | | [List of custom annotations](https://github.com/docker/metadata-action?tab=readme-ov-file#overwrite-labels-and-annotations) | 346 | | `meta-flavor` | List | | [Flavor](https://github.com/docker/metadata-action?tab=readme-ov-file#flavor-input) defines a global behavior for `meta-tags` | 347 | 348 | #### Secrets 349 | 350 | | Name | Default | Description | 351 | |------------------|-----------------------|--------------------------------------------------------------------------------| 352 | | `registry-auths` | | Raw authentication to registries, defined as YAML objects (for `image` output) | 353 | | `github-token` | `${{ github.token }}` | GitHub Token used to authenticate against the repository for Git context | 354 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | runner: 7 | type: string 8 | description: "Ubuntu GitHub Hosted Runner to build on (one of auto, amd64, arm64). The auto runner selects the best-matching runner based on target platforms. You can set it to amd64 if your build doesn't require emulation (e.g. cross-compilation)" 9 | required: false 10 | default: 'auto' 11 | setup-qemu: 12 | type: boolean 13 | description: "Runs the setup-qemu-action step to install QEMU static binaries" 14 | required: false 15 | default: false 16 | artifact-name: 17 | type: string 18 | description: "Name of the uploaded GitHub artifact (for local output)" 19 | required: false 20 | default: 'docker-github-builder-assets' 21 | artifact-upload: 22 | type: boolean 23 | description: "Upload build output GitHub artifact (for local output)" 24 | required: false 25 | default: false 26 | annotations: 27 | type: string 28 | description: "List of annotations to set to the image (for image output)" 29 | required: false 30 | build-args: 31 | type: string 32 | description: "List of build-time variables" 33 | required: false 34 | context: 35 | type: string 36 | description: "Context to build from in the Git working tree" 37 | required: false 38 | default: . 39 | file: 40 | type: string 41 | description: "Path to the Dockerfile" 42 | required: false 43 | labels: 44 | type: string 45 | description: "List of labels for an image (for image output)" 46 | required: false 47 | output: 48 | type: string 49 | description: "Build output destination (one of image or local). Unlike the build-push-action, it only accepts image or local. The reusable workflow takes care of setting the outputs attribute" 50 | required: true 51 | platforms: 52 | type: string 53 | description: "List of target platforms to build" 54 | required: false 55 | push: 56 | type: boolean 57 | description: "Push image to the registry (for image output)" 58 | required: false 59 | default: false 60 | sbom: 61 | type: boolean 62 | description: "Generate SBOM attestation for the build" 63 | required: false 64 | default: false 65 | shm-size: 66 | type: string 67 | description: "Size of /dev/shm (e.g., 2g)" 68 | required: false 69 | sign: 70 | type: string 71 | description: "Sign attestation manifest for image output or artifacts for local output, can be one of auto, true or false. The auto mode will enable signing if push is enabled for pushing the image or if artifact-upload is enabled for uploading the local build output as GitHub Artifact" 72 | required: false 73 | default: auto 74 | target: 75 | type: string 76 | description: "Sets the target stage to build" 77 | required: false 78 | ulimit: 79 | type: string 80 | description: "Ulimit options (e.g., nofile=1024:1024)" 81 | required: false 82 | # docker/metadata-action 83 | set-meta-annotations: 84 | type: boolean 85 | description: "Append OCI Image Format Specification annotations generated by docker/metadata-action" 86 | required: false 87 | default: false 88 | set-meta-labels: 89 | type: boolean 90 | description: "Append OCI Image Format Specification labels generated by docker/metadata-action" 91 | required: false 92 | default: false 93 | meta-images: 94 | type: string 95 | description: "List of images to use as base name for tags (required for image output)" 96 | required: false 97 | meta-tags: 98 | type: string 99 | description: "List of tags as key-value pair attributes" 100 | required: false 101 | meta-flavor: 102 | type: string 103 | description: "Flavor defines a global behavior for meta-tags" 104 | required: false 105 | secrets: 106 | registry-auths: 107 | description: "Raw authentication to registries, defined as YAML objects (for image output)" 108 | required: false 109 | github-token: 110 | description: "GitHub Token used to authenticate against the repository for Git context" 111 | required: false 112 | outputs: 113 | meta-json: 114 | description: "Metadata JSON output (for image output)" 115 | value: ${{ jobs.finalize.outputs.meta-json }} 116 | cosign-version: 117 | description: "Cosign version used for verification" 118 | value: ${{ jobs.finalize.outputs.cosign-version }} 119 | cosign-verify-commands: 120 | description: "Cosign verify commands" 121 | value: ${{ jobs.finalize.outputs.cosign-verify-commands }} 122 | artifact-name: 123 | description: "Name of the uploaded artifact (for local output)" 124 | value: ${{ jobs.finalize.outputs.artifact-name }} 125 | output-type: 126 | description: "Build output type" 127 | value: ${{ jobs.finalize.outputs.output-type }} 128 | signed: 129 | description: "Whether attestations manifests or artifacts were signed" 130 | value: ${{ jobs.finalize.outputs.signed }} 131 | 132 | env: 133 | BUILDX_VERSION: "v0.30.1" 134 | BUILDKIT_IMAGE: "moby/buildkit:v0.26.2" 135 | DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.71.0" 136 | COSIGN_VERSION: "v3.0.2" 137 | LOCAL_EXPORT_DIR: "/tmp/buildx-output" 138 | MATRIX_SIZE_LIMIT: "20" 139 | 140 | jobs: 141 | prepare: 142 | runs-on: ubuntu-24.04 143 | outputs: 144 | includes: ${{ steps.set.outputs.includes }} 145 | sign: ${{ steps.set.outputs.sign }} 146 | steps: 147 | - 148 | name: Install @docker/actions-toolkit 149 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 150 | env: 151 | INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} 152 | with: 153 | script: | 154 | await exec.exec('npm', ['install', '--prefer-offline', '--ignore-scripts', core.getInput('dat-module')]); 155 | - 156 | name: Set outputs 157 | id: set 158 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 159 | env: 160 | INPUT_MATRIX-SIZE-LIMIT: ${{ env.MATRIX_SIZE_LIMIT }} 161 | INPUT_RUNNER: ${{ inputs.runner }} 162 | INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} 163 | INPUT_OUTPUT: ${{ inputs.output }} 164 | INPUT_PLATFORMS: ${{ inputs.platforms }} 165 | INPUT_PUSH: ${{ inputs.push }} 166 | INPUT_SIGN: ${{ inputs.sign }} 167 | with: 168 | script: | 169 | const { GitHub } = require('@docker/actions-toolkit/lib/github'); 170 | const { Util } = require('@docker/actions-toolkit/lib/util'); 171 | 172 | const inpMatrixSizeLimit = parseInt(core.getInput('matrix-size-limit'), 10); 173 | 174 | const inpRunner = core.getInput('runner'); 175 | const inpArtifactUpload = core.getBooleanInput('artifact-upload'); 176 | const inpPlatforms = Util.getInputList('platforms'); 177 | const inpOutput = core.getInput('output'); 178 | const inpPush = core.getBooleanInput('push'); 179 | const inpSign = core.getInput('sign'); 180 | 181 | let runner = inpRunner; 182 | if (inpRunner === 'amd64') { 183 | runner = 'ubuntu-24.04'; 184 | } else if (inpRunner === 'arm64') { 185 | runner = 'ubuntu-24.04-arm'; 186 | } else if (inpRunner !== 'auto') { 187 | core.setFailed(`Invalid runner input: ${inpRunner}`); 188 | return; 189 | } 190 | 191 | const sign = 192 | inpSign === 'auto' 193 | ? (inpOutput === 'image' && inpPush) || (inpOutput === 'local' && inpArtifactUpload) 194 | : inpSign === 'true'; 195 | 196 | if (inpOutput === 'local' && inpPush) { 197 | core.warning(`push is ignored when output is local`); 198 | } else if (inpOutput === 'image' && inpArtifactUpload) { 199 | core.warning(`artifact-upload is ignored when output is image`); 200 | } 201 | if (inpOutput === 'image' && !inpPush && sign) { 202 | core.setFailed(`signing attestation manifests requires push to be enabled`); 203 | return; 204 | } 205 | 206 | if (inpPlatforms.length > inpMatrixSizeLimit) { 207 | core.setFailed(`Platforms to build exceed matrix size limit of ${inpMatrixSizeLimit}`); 208 | return; 209 | } 210 | 211 | const privateRepo = GitHub.context.payload.repository?.private ?? false; 212 | await core.group(`Set includes output`, async () => { 213 | let includes = []; 214 | if (inpPlatforms.length === 0) { 215 | includes.push({ 216 | index: 0, 217 | runner: runner === 'auto' ? 'ubuntu-24.04' : runner 218 | }); 219 | } else { 220 | inpPlatforms.forEach((platform, index) => { 221 | includes.push({ 222 | index: index, 223 | platform: platform, 224 | runner: runner === 'auto' ? ((!privateRepo && platform.startsWith('linux/arm')) ? 'ubuntu-24.04-arm' : 'ubuntu-24.04') : runner 225 | }); 226 | }); 227 | } 228 | core.info(JSON.stringify(includes, null, 2)); 229 | core.setOutput('includes', JSON.stringify(includes)); 230 | }); 231 | await core.group(`Set sign output`, async () => { 232 | core.info(`sign: ${sign}`); 233 | core.setOutput('sign', sign); 234 | }); 235 | 236 | build: 237 | runs-on: ${{ matrix.runner }} 238 | needs: 239 | - prepare 240 | strategy: 241 | fail-fast: false 242 | matrix: 243 | include: ${{ fromJson(needs.prepare.outputs.includes) }} 244 | outputs: 245 | # needs predefined outputs as we can't use dynamic ones atm: https://github.com/actions/runner/pull/2477 246 | # 20 is the maximum number of platforms supported by our matrix strategy 247 | result_0: ${{ steps.result.outputs.result_0 }} 248 | result_1: ${{ steps.result.outputs.result_1 }} 249 | result_2: ${{ steps.result.outputs.result_2 }} 250 | result_3: ${{ steps.result.outputs.result_3 }} 251 | result_4: ${{ steps.result.outputs.result_4 }} 252 | result_5: ${{ steps.result.outputs.result_5 }} 253 | result_6: ${{ steps.result.outputs.result_6 }} 254 | result_7: ${{ steps.result.outputs.result_7 }} 255 | result_8: ${{ steps.result.outputs.result_8 }} 256 | result_9: ${{ steps.result.outputs.result_9 }} 257 | result_10: ${{ steps.result.outputs.result_10 }} 258 | result_11: ${{ steps.result.outputs.result_11 }} 259 | result_12: ${{ steps.result.outputs.result_12 }} 260 | result_13: ${{ steps.result.outputs.result_13 }} 261 | result_14: ${{ steps.result.outputs.result_14 }} 262 | result_15: ${{ steps.result.outputs.result_15 }} 263 | result_16: ${{ steps.result.outputs.result_16 }} 264 | result_17: ${{ steps.result.outputs.result_17 }} 265 | result_18: ${{ steps.result.outputs.result_18 }} 266 | result_19: ${{ steps.result.outputs.result_19 }} 267 | steps: 268 | - 269 | name: Install @docker/actions-toolkit 270 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 271 | env: 272 | INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} 273 | with: 274 | script: | 275 | await exec.exec('npm', ['install', '--prefer-offline', '--ignore-scripts', core.getInput('dat-module')]); 276 | - 277 | name: Docker meta 278 | id: meta 279 | if: ${{ inputs.output == 'image' }} 280 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 281 | with: 282 | images: ${{ inputs.meta-images }} 283 | tags: ${{ inputs.meta-tags }} 284 | flavor: ${{ inputs.meta-flavor }} 285 | labels: ${{ inputs.meta-labels }} 286 | annotations: ${{ inputs.meta-annotations }} 287 | - 288 | name: Set up QEMU 289 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 290 | if: ${{ inputs.setup-qemu }} 291 | with: 292 | cache-image: false 293 | - 294 | name: Set up Docker Buildx 295 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 296 | with: 297 | version: ${{ env.BUILDX_VERSION }} 298 | buildkitd-flags: --debug 299 | driver-opts: image=${{ env.BUILDKIT_IMAGE }} 300 | cache-binary: false 301 | - 302 | name: Prepare 303 | id: prepare 304 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 305 | env: 306 | INPUT_PLATFORM: ${{ matrix.platform }} 307 | INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} 308 | INPUT_ANNOTATIONS: ${{ inputs.annotations }} 309 | INPUT_LABELS: ${{ inputs.labels }} 310 | INPUT_CONTEXT: ${{ inputs.context }} 311 | INPUT_OUTPUT: ${{ inputs.output }} 312 | INPUT_PUSH: ${{ inputs.push }} 313 | INPUT_TARGET: ${{ inputs.target }} 314 | INPUT_META-IMAGES: ${{ inputs.meta-images }} 315 | INPUT_SET-META-ANNOTATIONS: ${{ inputs.set-meta-annotations }} 316 | INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }} 317 | INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }} 318 | INPUT_META-LABELS: ${{ steps.meta.outputs.labels }} 319 | with: 320 | script: | 321 | const { GitHub } = require('@docker/actions-toolkit/lib/github'); 322 | 323 | const inpPlatform = core.getInput('platform'); 324 | const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; 325 | core.setOutput('platform-pair-suffix', platformPairSuffix); 326 | const inpLocalExportDir = core.getInput('local-export-dir'); 327 | 328 | const inpAnnotations = core.getMultilineInput('annotations'); 329 | const inpContext = core.getInput('context'); 330 | const inpLabels = core.getMultilineInput('labels'); 331 | const inpOutput = core.getInput('output'); 332 | const inpPush = core.getBooleanInput('push'); 333 | const inpTarget = core.getInput('target'); 334 | 335 | const inpMetaImages = core.getMultilineInput('meta-images'); 336 | const inpSetMetaAnnotations = core.getBooleanInput('set-meta-annotations'); 337 | const inpMetaAnnotations = core.getMultilineInput('meta-annotations'); 338 | const inpSetMetaLabels = core.getBooleanInput('set-meta-labels'); 339 | const inpMetaLabels = core.getMultilineInput('meta-labels'); 340 | 341 | const buildContext = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}.git#${process.env.GITHUB_REF}:${inpContext}`; 342 | core.setOutput('context', buildContext); 343 | 344 | switch (inpOutput) { 345 | case 'image': 346 | if (inpMetaImages.length == 0) { 347 | core.setFailed('meta-images is required when output is image'); 348 | return; 349 | } 350 | core.setOutput('output', `type=image,"name=${inpMetaImages.join(',')}",oci-artifact=true,push-by-digest=true,name-canonical=true,push=${inpPush}`); 351 | break; 352 | case 'local': 353 | core.setOutput('output', `type=local,platform-split=true,dest=${inpLocalExportDir}`); 354 | break; 355 | default: 356 | core.setFailed(`Invalid output: ${inpOutput}`); 357 | return; 358 | } 359 | 360 | if (inpPlatform) { 361 | core.setOutput('platform', inpPlatform); 362 | } 363 | 364 | if (inpSetMetaAnnotations && inpMetaAnnotations.length > 0) { 365 | inpAnnotations.push(...inpMetaAnnotations); 366 | } 367 | core.setOutput('annotations', inpAnnotations.join('\n')); 368 | 369 | if (inpSetMetaLabels && inpMetaLabels.length > 0) { 370 | inpLabels.push(...inpMetaLabels); 371 | } 372 | core.setOutput('labels', inpLabels.join('\n')); 373 | 374 | if (GitHub.context.payload.repository?.private ?? false) { 375 | // if this is a private repository, we set the default provenance 376 | // attributes being set in buildx: https://github.com/docker/buildx/blob/fb27e3f919dcbf614d7126b10c2bc2d0b1927eb6/build/build.go#L603 377 | core.setOutput('provenance', 'mode=min,inline-only=true,version=v1'); 378 | } else { 379 | // for a public repository, we set max provenance mode 380 | core.setOutput('provenance', 'mode=max,version=v1'); 381 | } 382 | - 383 | name: Login to registry 384 | if: ${{ inputs.push && inputs.output == 'image' }} 385 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 386 | with: 387 | registry-auth: ${{ secrets.registry-auths }} 388 | - 389 | name: Build 390 | id: build 391 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 392 | with: 393 | annotations: ${{ steps.prepare.outputs.annotations }} 394 | build-args: ${{ inputs.build-args }} 395 | context: ${{ steps.prepare.outputs.context }} 396 | file: ${{ inputs.file }} 397 | labels: ${{ steps.prepare.outputs.labels }} 398 | outputs: ${{ steps.prepare.outputs.output }} 399 | platforms: ${{ steps.prepare.outputs.platform }} 400 | provenance: ${{ steps.prepare.outputs.provenance }} 401 | sbom: ${{ inputs.sbom }} 402 | secret-envs: GIT_AUTH_TOKEN=GIT_AUTH_TOKEN 403 | shm-size: ${{ inputs.shm-size }} 404 | target: ${{ inputs.target }} 405 | ulimit: ${{ inputs.ulimit }} 406 | env: 407 | BUILDKIT_MULTI_PLATFORM: 1 408 | GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }} 409 | - 410 | name: Install Cosign 411 | if: ${{ needs.prepare.outputs.sign == 'true' }} 412 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 413 | env: 414 | INPUT_COSIGN-VERSION: ${{ env.COSIGN_VERSION }} 415 | with: 416 | script: | 417 | const { Cosign } = require('@docker/actions-toolkit/lib/cosign/cosign'); 418 | const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); 419 | 420 | const cosignInstall = new Install(); 421 | const cosignBinPath = await cosignInstall.download({ 422 | version: core.getInput('cosign-version'), 423 | ghaNoCache: true, 424 | skipState: true, 425 | verifySignature: true 426 | }); 427 | await cosignInstall.install(cosignBinPath); 428 | 429 | const cosign = new Cosign(); 430 | await cosign.printVersion(); 431 | - 432 | name: Signing attestation manifests 433 | id: signing-attestation-manifests 434 | if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }} 435 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 436 | env: 437 | INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} 438 | INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} 439 | with: 440 | script: | 441 | const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); 442 | 443 | const inpImageNames = core.getMultilineInput('image-names'); 444 | const inpImageDigest = core.getInput('image-digest'); 445 | 446 | // ECR registry regexes: https://github.com/docker/login-action/blob/28fdb31ff34708d19615a74d67103ddc2ea9725c/src/aws.ts#L8-L9 447 | const ecrRegistryRegex = /^(([0-9]{12})\.(dkr\.ecr|dkr-ecr)\.(.+)\.(on\.aws|amazonaws\.com(.cn)?))(\/([^:]+)(:.+)?)?$/; 448 | const ecrPublicRegistryRegex = /public\.ecr\.aws|ecr-public\.aws\.com/; 449 | for (const imageName of inpImageNames) { 450 | if (ecrRegistryRegex.test(imageName) || ecrPublicRegistryRegex.test(imageName)) { 451 | core.info(`Detected ECR image name: ${imageName}, adding delay to mitigate eventual consistency issue`); 452 | // FIXME: remove once https://github.com/docker/github-builder-experimental/issues/30 is resolved 453 | await new Promise(resolve => setTimeout(resolve, 5000)); 454 | break; 455 | } 456 | } 457 | 458 | const sigstore = new Sigstore(); 459 | const signResults = await sigstore.signAttestationManifests({ 460 | imageNames: inpImageNames, 461 | imageDigest: inpImageDigest 462 | }); 463 | 464 | const verifyResults = await sigstore.verifySignedManifests( 465 | { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$` }, 466 | signResults 467 | ); 468 | 469 | await core.group(`Verify commands`, async () => { 470 | const verifyCommands = []; 471 | for (const [attestationRef, verifyResult] of Object.entries(verifyResults)) { 472 | const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} ${attestationRef}`; 473 | core.info(cmd); 474 | verifyCommands.push(cmd); 475 | } 476 | core.setOutput('verify-commands', verifyCommands.join('\n')); 477 | }); 478 | - 479 | name: Signing local artifacts 480 | id: signing-local-artifacts 481 | if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'local' }} 482 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 483 | env: 484 | INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_EXPORT_DIR }} 485 | with: 486 | script: | 487 | const path = require('path'); 488 | const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); 489 | const inplocalExportDir = core.getInput('local-output-dir'); 490 | 491 | const sigstore = new Sigstore(); 492 | const signResults = await sigstore.signProvenanceBlobs({ 493 | localExportDir: inplocalExportDir 494 | }); 495 | 496 | const verifyResults = await sigstore.verifySignedArtifacts( 497 | { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/build.yml.*$` }, 498 | signResults 499 | ); 500 | 501 | await core.group(`Verify commands`, async () => { 502 | const verifyCommands = []; 503 | for (const [artifactPath, verifyResult] of Object.entries(verifyResults)) { 504 | const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} --bundle ${path.relative(inplocalExportDir, verifyResult.bundlePath)} ${path.relative(inplocalExportDir, artifactPath)}`; 505 | core.info(cmd); 506 | verifyCommands.push(cmd); 507 | } 508 | core.setOutput('verify-commands', verifyCommands.join('\n')); 509 | }); 510 | - 511 | name: List local output 512 | if: ${{ inputs.output == 'local' }} 513 | run: | 514 | tree -nh ${{ env.LOCAL_EXPORT_DIR }} 515 | - 516 | name: Upload artifact 517 | if: ${{ inputs.output == 'local' && inputs.artifact-upload }} 518 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 519 | with: 520 | name: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix || '0' }} 521 | path: ${{ env.LOCAL_EXPORT_DIR }} 522 | if-no-files-found: error 523 | - 524 | name: Set result output 525 | id: result 526 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 527 | env: 528 | INPUT_INDEX: ${{ matrix.index }} 529 | INPUT_VERIFY-COMMANDS: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }} 530 | INPUT_IMAGE-DIGEST: ${{ steps.build.outputs.digest }} 531 | INPUT_ARTIFACT-NAME: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix }} 532 | INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} 533 | INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} 534 | with: 535 | script: | 536 | const inpIndex = core.getInput('index'); 537 | const inpVerifyCommands = core.getInput('verify-commands'); 538 | const inpImageDigest = core.getInput('image-digest'); 539 | const inpArtifactName = core.getInput('artifact-name'); 540 | const inpArtifactUpload = core.getBooleanInput('artifact-upload'); 541 | const inpSigned = core.getBooleanInput('signed'); 542 | 543 | const result = { 544 | verifyCommands: inpVerifyCommands, 545 | imageDigest: inpImageDigest, 546 | artifactName: inpArtifactUpload ? inpArtifactName : '', 547 | signed: inpSigned 548 | } 549 | core.info(JSON.stringify(result, null, 2)); 550 | 551 | core.setOutput(`result_${inpIndex}`, JSON.stringify(result)); 552 | 553 | finalize: 554 | runs-on: ubuntu-24.04 555 | outputs: 556 | meta-json: ${{ steps.meta.outputs.json }} 557 | cosign-version: ${{ env.COSIGN_VERSION }} 558 | cosign-verify-commands: ${{ steps.set.outputs.cosign-verify-commands }} 559 | artifact-name: ${{ inputs.artifact-upload && inputs.artifact-name || '' }} 560 | output-type: ${{ inputs.output }} 561 | signed: ${{ needs.prepare.outputs.sign }} 562 | needs: 563 | - prepare 564 | - build 565 | steps: 566 | - 567 | name: Docker meta 568 | id: meta 569 | if: ${{ inputs.output == 'image' }} 570 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 571 | with: 572 | images: ${{ inputs.meta-images }} 573 | tags: ${{ inputs.meta-tags }} 574 | flavor: ${{ inputs.meta-flavor }} 575 | labels: ${{ inputs.meta-labels }} 576 | annotations: ${{ inputs.meta-annotations }} 577 | - 578 | name: Login to registry 579 | if: ${{ inputs.push && inputs.output == 'image' }} 580 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 581 | with: 582 | registry-auth: ${{ secrets.registry-auths }} 583 | - 584 | name: Set up Docker Buildx 585 | if: ${{ inputs.push && inputs.output == 'image' }} 586 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 587 | with: 588 | version: ${{ env.BUILDX_VERSION }} 589 | buildkitd-flags: --debug 590 | driver-opts: image=${{ env.BUILDKIT_IMAGE }} 591 | cache-binary: false 592 | - 593 | name: Create manifest 594 | if: ${{ inputs.output == 'image' }} 595 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 596 | env: 597 | INPUT_PUSH: ${{ inputs.push }} 598 | INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} 599 | INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }} 600 | INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} 601 | with: 602 | script: | 603 | const inpPush = core.getBooleanInput('push'); 604 | const inpImageNames = core.getMultilineInput('image-names'); 605 | const inpTagNames = core.getMultilineInput('tag-names'); 606 | const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); 607 | 608 | const digests = []; 609 | for (const key of Object.keys(inpBuildOutputs)) { 610 | const output = JSON.parse(inpBuildOutputs[key]); 611 | if (output.imageDigest) { 612 | digests.push(output.imageDigest); 613 | } 614 | } 615 | if (digests.length === 0) { 616 | core.setFailed('No image digests found from build outputs'); 617 | return; 618 | } 619 | 620 | for (const imageName of inpImageNames) { 621 | let createArgs = ['buildx', 'imagetools', 'create']; 622 | for (const tag of inpTagNames) { 623 | createArgs.push('-t', `${imageName}:${tag}`); 624 | } 625 | for (const digest of digests) { 626 | createArgs.push(digest); 627 | } 628 | if (inpPush) { 629 | await exec.exec('docker', createArgs); 630 | } else { 631 | await core.group(`Generated imagetools create command for ${imageName}`, async () => { 632 | core.info(`docker ${createArgs.join(' ')}`); 633 | }); 634 | } 635 | } 636 | - 637 | name: Merge artifacts 638 | if: ${{ inputs.output == 'local' && inputs.artifact-upload }} 639 | uses: actions/upload-artifact/merge@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 640 | with: 641 | name: ${{ inputs.artifact-name }} 642 | pattern: ${{ inputs.artifact-name }}* 643 | delete-merged: true 644 | - 645 | name: Set outputs 646 | id: set 647 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 648 | env: 649 | INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} 650 | INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} 651 | with: 652 | script: | 653 | const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); 654 | const inpSigned = core.getBooleanInput('signed'); 655 | 656 | if (inpSigned) { 657 | const verifyCommands = []; 658 | for (const key of Object.keys(inpBuildOutputs)) { 659 | const output = JSON.parse(inpBuildOutputs[key]); 660 | if (output.verifyCommands) { 661 | verifyCommands.push(output.verifyCommands); 662 | } 663 | } 664 | core.setOutput('cosign-verify-commands', verifyCommands.join('\n')); 665 | } 666 | -------------------------------------------------------------------------------- /.github/workflows/bake.yml: -------------------------------------------------------------------------------- 1 | name: bake 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | runner: 7 | type: string 8 | description: "Ubuntu GitHub Hosted Runner to build on (one of auto, amd64, arm64). The auto runner selects the best-matching runner based on target platforms. You can set it to amd64 if your build doesn't require emulation (e.g. cross-compilation)" 9 | required: false 10 | default: 'auto' 11 | setup-qemu: 12 | type: boolean 13 | description: "Runs the setup-qemu-action step to install QEMU static binaries" 14 | required: false 15 | default: false 16 | artifact-name: 17 | type: string 18 | description: "Name of the uploaded GitHub artifact (for local output)" 19 | required: false 20 | default: 'docker-github-builder-assets' 21 | artifact-upload: 22 | type: boolean 23 | description: "Upload build output GitHub artifact (for local output)" 24 | required: false 25 | default: false 26 | context: 27 | type: string 28 | description: "Context to build from in the Git working tree" 29 | required: false 30 | default: . 31 | files: 32 | type: string 33 | description: "List of bake definition files" 34 | required: false 35 | output: 36 | type: string 37 | description: "Build output destination (one of image or local). Unlike the build-push-action, it only accepts image or local. The reusable workflow takes care of setting the outputs attribute" 38 | required: true 39 | push: 40 | type: boolean 41 | description: "Push image to the registry (for image output)" 42 | required: false 43 | default: false 44 | sbom: 45 | type: boolean 46 | description: "Generate SBOM attestation for the build" 47 | required: false 48 | default: false 49 | set: 50 | type: string 51 | description: "List of targets values to override (eg. targetpattern.key=value)" 52 | required: false 53 | sign: 54 | type: string 55 | description: "Sign attestation manifest for image output or artifacts for local output, can be one of auto, true or false. The auto mode will enable signing if push is enabled for pushing the image or if artifact-upload is enabled for uploading the local build output as GitHub Artifact" 56 | required: false 57 | default: auto 58 | target: 59 | type: string 60 | description: "Bake target to build" 61 | required: true 62 | default: default 63 | # docker/metadata-action 64 | set-meta-annotations: 65 | type: boolean 66 | description: "Append OCI Image Format Specification annotations generated by docker/metadata-action" 67 | required: false 68 | default: false 69 | set-meta-labels: 70 | type: boolean 71 | description: "Append OCI Image Format Specification labels generated by docker/metadata-action" 72 | required: false 73 | default: false 74 | meta-images: 75 | type: string 76 | description: "List of images to use as base name for tags (required for image output)" 77 | required: false 78 | meta-tags: 79 | type: string 80 | description: "List of tags as key-value pair attributes" 81 | required: false 82 | meta-flavor: 83 | type: string 84 | description: "Flavor defines a global behavior for meta-tags" 85 | required: false 86 | meta-labels: 87 | type: string 88 | description: "List of custom labels" 89 | required: false 90 | meta-annotations: 91 | type: string 92 | description: "List of custom annotations" 93 | required: false 94 | meta-bake-target: 95 | type: string 96 | description: "Bake target name for metadata (defaults to docker-metadata-action)" 97 | required: false 98 | secrets: 99 | registry-auths: 100 | description: "Raw authentication to registries, defined as YAML objects (for image output)" 101 | required: false 102 | github-token: 103 | description: "GitHub Token used to authenticate against the repository for Git context" 104 | required: false 105 | outputs: 106 | meta-json: 107 | description: "Metadata JSON output (for image output)" 108 | value: ${{ jobs.finalize.outputs.meta-json }} 109 | cosign-version: 110 | description: "Cosign version used for verification" 111 | value: ${{ jobs.finalize.outputs.cosign-version }} 112 | cosign-verify-commands: 113 | description: "Cosign verify commands" 114 | value: ${{ jobs.finalize.outputs.cosign-verify-commands }} 115 | artifact-name: 116 | description: "Name of the uploaded artifact (for local output)" 117 | value: ${{ jobs.finalize.outputs.artifact-name }} 118 | output-type: 119 | description: "Build output type" 120 | value: ${{ jobs.finalize.outputs.output-type }} 121 | signed: 122 | description: "Whether attestations manifests or artifacts were signed" 123 | value: ${{ jobs.finalize.outputs.signed }} 124 | 125 | env: 126 | BUILDX_VERSION: "v0.30.1" 127 | BUILDKIT_IMAGE: "moby/buildkit:v0.26.2" 128 | DOCKER_ACTIONS_TOOLKIT_MODULE: "@docker/actions-toolkit@0.71.0" 129 | COSIGN_VERSION: "v3.0.2" 130 | LOCAL_EXPORT_DIR: "/tmp/buildx-output" 131 | MATRIX_SIZE_LIMIT: "20" 132 | 133 | jobs: 134 | prepare: 135 | runs-on: ubuntu-24.04 136 | outputs: 137 | includes: ${{ steps.set.outputs.includes }} 138 | sign: ${{ steps.set.outputs.sign }} 139 | steps: 140 | - 141 | name: Install @docker/actions-toolkit 142 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 143 | env: 144 | INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} 145 | with: 146 | script: | 147 | await exec.exec('npm', ['install', '--prefer-offline', '--ignore-scripts', core.getInput('dat-module')]); 148 | - 149 | name: Set outputs 150 | id: set 151 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 152 | env: 153 | INPUT_MATRIX-SIZE-LIMIT: ${{ env.MATRIX_SIZE_LIMIT }} 154 | INPUT_RUNNER: ${{ inputs.runner }} 155 | INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} 156 | INPUT_CONTEXT: ${{ inputs.context }} 157 | INPUT_FILES: ${{ inputs.files }} 158 | INPUT_OUTPUT: ${{ inputs.output }} 159 | INPUT_PUSH: ${{ inputs.push }} 160 | INPUT_SBOM: ${{ inputs.sbom }} 161 | INPUT_SET: ${{ inputs.set }} 162 | INPUT_SIGN: ${{ inputs.sign }} 163 | INPUT_TARGET: ${{ inputs.target }} 164 | INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }} 165 | with: 166 | script: | 167 | const os = require('os'); 168 | const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake'); 169 | const { GitHub } = require('@docker/actions-toolkit/lib/github'); 170 | const { Util } = require('@docker/actions-toolkit/lib/util'); 171 | 172 | const inpMatrixSizeLimit = parseInt(core.getInput('matrix-size-limit'), 10); 173 | 174 | const inpRunner = core.getInput('runner'); 175 | const inpArtifactUpload = core.getBooleanInput('artifact-upload'); 176 | const inpContext = core.getInput('context'); 177 | const inpFiles = Util.getInputList('files'); 178 | const inpOutput = core.getInput('output'); 179 | const inpPush = core.getBooleanInput('push'); 180 | const inpSbom = core.getBooleanInput('sbom'); 181 | const inpSet = Util.getInputList('set', {ignoreComma: true, quote: false}); 182 | const inpSign = core.getInput('sign'); 183 | const inpTarget = core.getInput('target'); 184 | const inpGitHubToken = core.getInput('github-token'); 185 | 186 | let runner = inpRunner; 187 | if (inpRunner === 'amd64') { 188 | runner = 'ubuntu-24.04'; 189 | } else if (inpRunner === 'arm64') { 190 | runner = 'ubuntu-24.04-arm'; 191 | } else if (inpRunner !== 'auto') { 192 | core.setFailed(`Invalid runner input: ${inpRunner}`); 193 | return; 194 | } 195 | 196 | const sign = 197 | inpSign === 'auto' 198 | ? (inpOutput === 'image' && inpPush) || (inpOutput === 'local' && inpArtifactUpload) 199 | : inpSign === 'true'; 200 | 201 | if (inpOutput === 'local' && inpPush) { 202 | core.warning(`push is ignored when output is local`); 203 | } else if (inpOutput === 'image' && inpArtifactUpload) { 204 | core.warning(`artifact-upload is ignored when output is image`); 205 | } 206 | if (inpOutput === 'image' && !inpPush && sign) { 207 | core.setFailed(`signing attestation manifests requires push to be enabled`); 208 | return; 209 | } 210 | 211 | const bakeSource = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}.git#${process.env.GITHUB_REF}:${inpContext}`; 212 | await core.group(`Set bake source`, async () => { 213 | core.info(bakeSource); 214 | }); 215 | 216 | let def; 217 | let target; 218 | try { 219 | await core.group(`Validating definition`, async () => { 220 | const bake = new Bake(); 221 | def = await bake.getDefinition({ 222 | files: inpFiles, 223 | overrides: inpSet, 224 | sbom: inpSbom ? 'true' : 'false', 225 | source: bakeSource, 226 | targets: [inpTarget], 227 | githubToken: inpGitHubToken 228 | }); 229 | if (!def) { 230 | throw new Error('Bake definition not set'); 231 | } 232 | const targets = Object.keys(def.target); 233 | if (targets.length > 1) { 234 | throw new Error(`Only one target can be built at once, found: ${targets.join(', ')}`); 235 | } 236 | target = targets[0]; 237 | }); 238 | } catch (error) { 239 | core.setFailed(error); 240 | return; 241 | } 242 | 243 | const platforms = def.target[target].platforms || []; 244 | if (platforms.length > inpMatrixSizeLimit) { 245 | core.setFailed(`Platforms to build exceed matrix size limit of ${inpMatrixSizeLimit}`); 246 | return; 247 | } 248 | 249 | const privateRepo = GitHub.context.payload.repository?.private ?? false; 250 | await core.group(`Set includes output`, async () => { 251 | let includes = []; 252 | if (platforms.length === 0) { 253 | includes.push({ 254 | index: 0, 255 | runner: runner === 'auto' ? 'ubuntu-24.04' : runner 256 | }); 257 | } else { 258 | platforms.forEach((platform, index) => { 259 | includes.push({ 260 | index: index, 261 | platform: platform, 262 | runner: runner === 'auto' ? ((!privateRepo && platform.startsWith('linux/arm')) ? 'ubuntu-24.04-arm' : 'ubuntu-24.04') : runner 263 | }); 264 | }); 265 | } 266 | core.info(JSON.stringify(includes, null, 2)); 267 | core.setOutput('includes', JSON.stringify(includes)); 268 | }); 269 | await core.group(`Set sign output`, async () => { 270 | core.info(`sign: ${sign}`); 271 | core.setOutput('sign', sign); 272 | }); 273 | 274 | build: 275 | runs-on: ${{ matrix.runner }} 276 | needs: 277 | - prepare 278 | strategy: 279 | fail-fast: false 280 | matrix: 281 | include: ${{ fromJson(needs.prepare.outputs.includes) }} 282 | outputs: 283 | # needs predefined outputs as we can't use dynamic ones atm: https://github.com/actions/runner/pull/2477 284 | # 20 is the maximum number of platforms supported by our matrix strategy 285 | result_0: ${{ steps.result.outputs.result_0 }} 286 | result_1: ${{ steps.result.outputs.result_1 }} 287 | result_2: ${{ steps.result.outputs.result_2 }} 288 | result_3: ${{ steps.result.outputs.result_3 }} 289 | result_4: ${{ steps.result.outputs.result_4 }} 290 | result_5: ${{ steps.result.outputs.result_5 }} 291 | result_6: ${{ steps.result.outputs.result_6 }} 292 | result_7: ${{ steps.result.outputs.result_7 }} 293 | result_8: ${{ steps.result.outputs.result_8 }} 294 | result_9: ${{ steps.result.outputs.result_9 }} 295 | result_10: ${{ steps.result.outputs.result_10 }} 296 | result_11: ${{ steps.result.outputs.result_11 }} 297 | result_12: ${{ steps.result.outputs.result_12 }} 298 | result_13: ${{ steps.result.outputs.result_13 }} 299 | result_14: ${{ steps.result.outputs.result_14 }} 300 | result_15: ${{ steps.result.outputs.result_15 }} 301 | result_16: ${{ steps.result.outputs.result_16 }} 302 | result_17: ${{ steps.result.outputs.result_17 }} 303 | result_18: ${{ steps.result.outputs.result_18 }} 304 | result_19: ${{ steps.result.outputs.result_19 }} 305 | steps: 306 | - 307 | name: Install @docker/actions-toolkit 308 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 309 | env: 310 | INPUT_DAT-MODULE: ${{ env.DOCKER_ACTIONS_TOOLKIT_MODULE }} 311 | with: 312 | script: | 313 | await exec.exec('npm', ['install', '--prefer-offline', '--ignore-scripts', core.getInput('dat-module')]); 314 | - 315 | name: Docker meta 316 | id: meta 317 | if: ${{ inputs.output == 'image' }} 318 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 319 | with: 320 | images: ${{ inputs.meta-images }} 321 | tags: ${{ inputs.meta-tags }} 322 | flavor: ${{ inputs.meta-flavor }} 323 | labels: ${{ inputs.meta-labels }} 324 | annotations: ${{ inputs.meta-annotations }} 325 | bake-target: ${{ inputs.meta-bake-target }} 326 | - 327 | name: Set up QEMU 328 | uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 329 | if: ${{ inputs.setup-qemu }} 330 | with: 331 | cache-image: false 332 | - 333 | name: Set up Docker Buildx 334 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 335 | with: 336 | version: ${{ env.BUILDX_VERSION }} 337 | buildkitd-flags: --debug 338 | driver-opts: image=${{ env.BUILDKIT_IMAGE }} 339 | cache-binary: false 340 | - 341 | name: Prepare 342 | id: prepare 343 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 344 | env: 345 | INPUT_PLATFORM: ${{ matrix.platform }} 346 | INPUT_LOCAL-EXPORT-DIR: ${{ env.LOCAL_EXPORT_DIR }} 347 | INPUT_CONTEXT: ${{ inputs.context }} 348 | INPUT_FILES: ${{ inputs.files }} 349 | INPUT_OUTPUT: ${{ inputs.output }} 350 | INPUT_PUSH: ${{ inputs.push }} 351 | INPUT_SBOM: ${{ inputs.sbom }} 352 | INPUT_SET: ${{ inputs.set }} 353 | INPUT_TARGET: ${{ inputs.target }} 354 | INPUT_META-IMAGES: ${{ inputs.meta-images }} 355 | INPUT_SET-META-ANNOTATIONS: ${{ inputs.set-meta-annotations }} 356 | INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }} 357 | INPUT_BAKE-FILE-TAGS: ${{ steps.meta.outputs.bake-file-tags }} 358 | INPUT_BAKE-FILE-ANNOTATIONS: ${{ steps.meta.outputs.bake-file-annotations }} 359 | INPUT_BAKE-FILE-LABELS: ${{ steps.meta.outputs.bake-file-labels }} 360 | INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }} 361 | with: 362 | script: | 363 | const os = require('os'); 364 | const { Bake } = require('@docker/actions-toolkit/lib/buildx/bake'); 365 | const { GitHub } = require('@docker/actions-toolkit/lib/github'); 366 | const { Util } = require('@docker/actions-toolkit/lib/util'); 367 | 368 | const inpPlatform = core.getInput('platform'); 369 | const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; 370 | core.setOutput('platform-pair-suffix', platformPairSuffix); 371 | 372 | const inpLocalExportDir = core.getInput('local-export-dir'); 373 | 374 | const inpContext = core.getInput('context'); 375 | const inpFiles = Util.getInputList('files'); 376 | const inpOutput = core.getInput('output'); 377 | const inpPush = core.getBooleanInput('push'); 378 | const inpSbom = core.getBooleanInput('sbom'); 379 | const inpSet = Util.getInputList('set', {ignoreComma: true, quote: false}); 380 | const inpTarget = core.getInput('target'); 381 | const inpMetaImages = core.getMultilineInput('meta-images'); 382 | const inpSetMetaAnnotations = core.getBooleanInput('set-meta-annotations'); 383 | const inpSetMetaLabels = core.getBooleanInput('set-meta-labels'); 384 | const inpBakeFileTags = core.getInput('bake-file-tags'); 385 | const inpBakeFileAnnotations = core.getInput('bake-file-annotations'); 386 | const inpBakeFileLabels = core.getInput('bake-file-labels'); 387 | const inpGitHubToken = core.getInput('github-token'); 388 | 389 | const bakeSource = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}.git#${process.env.GITHUB_REF}:${inpContext}`; 390 | await core.group(`Set bake source`, async () => { 391 | core.info(bakeSource); 392 | core.setOutput('source', bakeSource); 393 | }); 394 | 395 | let target; 396 | try { 397 | await core.group(`Validating definition`, async () => { 398 | const bake = new Bake(); 399 | const def = await bake.getDefinition({ 400 | files: inpFiles, 401 | overrides: inpSet, 402 | sbom: inpSbom ? 'true' : 'false', 403 | source: bakeSource, 404 | targets: [inpTarget], 405 | githubToken: inpGitHubToken 406 | }); 407 | if (!def) { 408 | throw new Error('Bake definition not set'); 409 | } 410 | const targets = Object.keys(def.target); 411 | if (targets.length > 1) { 412 | throw new Error(`Only one target can be built at once, found: ${targets.join(', ')}`); 413 | } 414 | target = targets[0]; 415 | core.setOutput('target', target); 416 | }); 417 | } catch (error) { 418 | core.setFailed(error); 419 | return; 420 | } 421 | 422 | let bakeFiles = inpFiles; 423 | await core.group(`Set bake files`, async () => { 424 | if (bakeFiles.length === 0) { 425 | bakeFiles = ['docker-bake.hcl']; 426 | } 427 | if (inpBakeFileTags) { 428 | bakeFiles.push(`cwd://${inpBakeFileTags}`); 429 | } 430 | if (inpSetMetaAnnotations && inpBakeFileAnnotations) { 431 | bakeFiles.push(`cwd://${inpBakeFileAnnotations}`); 432 | } 433 | if (inpSetMetaLabels && inpBakeFileLabels) { 434 | bakeFiles.push(`cwd://${inpBakeFileLabels}`); 435 | } 436 | core.info(JSON.stringify(bakeFiles, null, 2)); 437 | core.setOutput('files', bakeFiles.join(os.EOL)); 438 | }); 439 | 440 | let outputOverride = ''; 441 | switch (inpOutput) { 442 | case 'image': 443 | if (inpMetaImages.length == 0) { 444 | core.setFailed('meta-images is required when output is image'); 445 | return; 446 | } 447 | outputOverride = `*.output=type=image,"name=${inpMetaImages.join(',')}",oci-artifact=true,push-by-digest=true,name-canonical=true,push=${inpPush}`; 448 | break; 449 | case 'local': 450 | outputOverride = `*.output=type=local,platform-split=true,dest=${inpLocalExportDir}`; 451 | break; 452 | default: 453 | core.setFailed(`Invalid output: ${inpOutput}`); 454 | return; 455 | } 456 | 457 | let bakeOverrides = [...inpSet, outputOverride]; 458 | await core.group(`Set bake overrides`, async () => { 459 | bakeOverrides.push('*.tags='); 460 | if (GitHub.context.payload.repository?.private ?? false) { 461 | // if this is a private repository, we set the default provenance 462 | // attributes being set in buildx: https://github.com/docker/buildx/blob/fb27e3f919dcbf614d7126b10c2bc2d0b1927eb6/build/build.go#L603 463 | bakeOverrides.push('*.attest=type=provenance,mode=min,inline-only=true,version=v1'); 464 | } else { 465 | // for a public repository, we set max provenance mode 466 | bakeOverrides.push('*.attest=type=provenance,mode=max,version=v1'); 467 | } 468 | if (inpPlatform) { 469 | bakeOverrides.push(`*.platform=${inpPlatform}`); 470 | } 471 | core.info(JSON.stringify(bakeOverrides, null, 2)); 472 | core.setOutput('overrides', bakeOverrides.join(os.EOL)); 473 | }); 474 | - 475 | name: Login to registry 476 | if: ${{ inputs.push && inputs.output == 'image' }} 477 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 478 | with: 479 | registry-auth: ${{ secrets.registry-auths }} 480 | - 481 | name: Build 482 | id: bake 483 | uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0 484 | with: 485 | source: ${{ steps.prepare.outputs.source }} 486 | files: ${{ steps.prepare.outputs.files }} 487 | targets: ${{ steps.prepare.outputs.target }} 488 | sbom: ${{ inputs.sbom }} 489 | set: ${{ steps.prepare.outputs.overrides }} 490 | env: 491 | BUILDKIT_MULTI_PLATFORM: 1 492 | BUILDX_BAKE_GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }} 493 | - 494 | name: Get image digest 495 | id: get-image-digest 496 | if: ${{ inputs.output == 'image' }} 497 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 498 | env: 499 | INPUT_TARGET: ${{ steps.prepare.outputs.target }} 500 | INPUT_METADATA: ${{ steps.bake.outputs.metadata }} 501 | with: 502 | script: | 503 | const inpTarget = core.getInput('target'); 504 | const inpMetadata = JSON.parse(core.getInput('metadata')); 505 | const imageDigest = inpMetadata[inpTarget]['containerimage.digest']; 506 | core.info(imageDigest); 507 | core.setOutput('digest', imageDigest); 508 | - 509 | name: Install Cosign 510 | if: ${{ needs.prepare.outputs.sign == 'true' }} 511 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 512 | env: 513 | INPUT_COSIGN-VERSION: ${{ env.COSIGN_VERSION }} 514 | with: 515 | script: | 516 | const { Cosign } = require('@docker/actions-toolkit/lib/cosign/cosign'); 517 | const { Install } = require('@docker/actions-toolkit/lib/cosign/install'); 518 | 519 | const cosignInstall = new Install(); 520 | const cosignBinPath = await cosignInstall.download({ 521 | version: core.getInput('cosign-version'), 522 | ghaNoCache: true, 523 | skipState: true, 524 | verifySignature: true 525 | }); 526 | await cosignInstall.install(cosignBinPath); 527 | 528 | const cosign = new Cosign(); 529 | await cosign.printVersion(); 530 | - 531 | name: Signing attestation manifests 532 | id: signing-attestation-manifests 533 | if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }} 534 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 535 | env: 536 | INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} 537 | INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }} 538 | with: 539 | script: | 540 | const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); 541 | 542 | const inpImageNames = core.getMultilineInput('image-names'); 543 | const inpImageDigest = core.getInput('image-digest'); 544 | 545 | // ECR registry regexes: https://github.com/docker/login-action/blob/28fdb31ff34708d19615a74d67103ddc2ea9725c/src/aws.ts#L8-L9 546 | const ecrRegistryRegex = /^(([0-9]{12})\.(dkr\.ecr|dkr-ecr)\.(.+)\.(on\.aws|amazonaws\.com(.cn)?))(\/([^:]+)(:.+)?)?$/; 547 | const ecrPublicRegistryRegex = /public\.ecr\.aws|ecr-public\.aws\.com/; 548 | for (const imageName of inpImageNames) { 549 | if (ecrRegistryRegex.test(imageName) || ecrPublicRegistryRegex.test(imageName)) { 550 | core.info(`Detected ECR image name: ${imageName}, adding delay to mitigate eventual consistency issue`); 551 | // FIXME: remove once https://github.com/docker/github-builder-experimental/issues/30 is resolved 552 | await new Promise(resolve => setTimeout(resolve, 5000)); 553 | break; 554 | } 555 | } 556 | 557 | const sigstore = new Sigstore(); 558 | const signResults = await sigstore.signAttestationManifests({ 559 | imageNames: inpImageNames, 560 | imageDigest: inpImageDigest 561 | }); 562 | 563 | const verifyResults = await sigstore.verifySignedManifests( 564 | { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml.*$` }, 565 | signResults 566 | ); 567 | 568 | await core.group(`Verify commands`, async () => { 569 | const verifyCommands = []; 570 | for (const [attestationRef, verifyResult] of Object.entries(verifyResults)) { 571 | const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} ${attestationRef}`; 572 | core.info(cmd); 573 | verifyCommands.push(cmd); 574 | } 575 | core.setOutput('verify-commands', verifyCommands.join('\n')); 576 | }); 577 | - 578 | name: Signing local artifacts 579 | id: signing-local-artifacts 580 | if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'local' }} 581 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 582 | env: 583 | INPUT_LOCAL-OUTPUT-DIR: ${{ env.LOCAL_EXPORT_DIR }} 584 | with: 585 | script: | 586 | const path = require('path'); 587 | const { Sigstore } = require('@docker/actions-toolkit/lib/sigstore/sigstore'); 588 | const inplocalExportDir = core.getInput('local-output-dir'); 589 | 590 | const sigstore = new Sigstore(); 591 | const signResults = await sigstore.signProvenanceBlobs({ 592 | localExportDir: inplocalExportDir 593 | }); 594 | 595 | const verifyResults = await sigstore.verifySignedArtifacts( 596 | { certificateIdentityRegexp: `^https://github.com/docker/github-builder-experimental/.github/workflows/bake.yml.*$` }, 597 | signResults 598 | ); 599 | 600 | await core.group(`Verify commands`, async () => { 601 | const verifyCommands = []; 602 | for (const [artifactPath, verifyResult] of Object.entries(verifyResults)) { 603 | const cmd = `cosign ${verifyResult.cosignArgs.join(' ')} --bundle ${path.relative(inplocalExportDir, verifyResult.bundlePath)} ${path.relative(inplocalExportDir, artifactPath)}`; 604 | core.info(cmd); 605 | verifyCommands.push(cmd); 606 | } 607 | core.setOutput('verify-commands', verifyCommands.join('\n')); 608 | }); 609 | - 610 | name: List local output 611 | if: ${{ inputs.output == 'local' }} 612 | run: | 613 | tree -nh ${{ env.LOCAL_EXPORT_DIR }} 614 | - 615 | name: Upload artifact 616 | if: ${{ inputs.output == 'local' && inputs.artifact-upload }} 617 | uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 618 | with: 619 | name: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix || '0' }} 620 | path: ${{ env.LOCAL_EXPORT_DIR }} 621 | if-no-files-found: error 622 | - 623 | name: Set result output 624 | id: result 625 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 626 | env: 627 | INPUT_INDEX: ${{ matrix.index }} 628 | INPUT_VERIFY-COMMANDS: ${{ steps.signing-attestation-manifests.outputs.verify-commands || steps.signing-local-artifacts.outputs.verify-commands }} 629 | INPUT_IMAGE-DIGEST: ${{ steps.get-image-digest.outputs.digest }} 630 | INPUT_ARTIFACT-NAME: ${{ inputs.artifact-name }}${{ steps.prepare.outputs.platform-pair-suffix }} 631 | INPUT_ARTIFACT-UPLOAD: ${{ inputs.artifact-upload }} 632 | INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} 633 | with: 634 | script: | 635 | const inpIndex = core.getInput('index'); 636 | const inpVerifyCommands = core.getInput('verify-commands'); 637 | const inpImageDigest = core.getInput('image-digest'); 638 | const inpArtifactName = core.getInput('artifact-name'); 639 | const inpArtifactUpload = core.getBooleanInput('artifact-upload'); 640 | const inpSigned = core.getBooleanInput('signed'); 641 | 642 | const result = { 643 | verifyCommands: inpVerifyCommands, 644 | imageDigest: inpImageDigest, 645 | artifactName: inpArtifactUpload ? inpArtifactName : '', 646 | signed: inpSigned 647 | } 648 | core.info(JSON.stringify(result, null, 2)); 649 | 650 | core.setOutput(`result_${inpIndex}`, JSON.stringify(result)); 651 | 652 | finalize: 653 | runs-on: ubuntu-24.04 654 | outputs: 655 | meta-json: ${{ steps.meta.outputs.json }} 656 | cosign-version: ${{ env.COSIGN_VERSION }} 657 | cosign-verify-commands: ${{ steps.set.outputs.cosign-verify-commands }} 658 | artifact-name: ${{ inputs.artifact-upload && inputs.artifact-name || '' }} 659 | output-type: ${{ inputs.output }} 660 | signed: ${{ needs.prepare.outputs.sign }} 661 | needs: 662 | - prepare 663 | - build 664 | steps: 665 | - 666 | name: Docker meta 667 | id: meta 668 | if: ${{ inputs.output == 'image' }} 669 | uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 670 | with: 671 | images: ${{ inputs.meta-images }} 672 | tags: ${{ inputs.meta-tags }} 673 | flavor: ${{ inputs.meta-flavor }} 674 | labels: ${{ inputs.meta-labels }} 675 | annotations: ${{ inputs.meta-annotations }} 676 | bake-target: ${{ inputs.meta-bake-target }} 677 | - 678 | name: Login to registry 679 | if: ${{ inputs.push && inputs.output == 'image' }} 680 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 681 | with: 682 | registry-auth: ${{ secrets.registry-auths }} 683 | - 684 | name: Set up Docker Buildx 685 | if: ${{ inputs.push && inputs.output == 'image' }} 686 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 687 | with: 688 | version: ${{ env.BUILDX_VERSION }} 689 | buildkitd-flags: --debug 690 | driver-opts: image=${{ env.BUILDKIT_IMAGE }} 691 | cache-binary: false 692 | - 693 | name: Create manifest 694 | if: ${{ inputs.output == 'image' }} 695 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 696 | env: 697 | INPUT_PUSH: ${{ inputs.push }} 698 | INPUT_IMAGE-NAMES: ${{ inputs.meta-images }} 699 | INPUT_TAG-NAMES: ${{ steps.meta.outputs.tag-names }} 700 | INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} 701 | with: 702 | script: | 703 | const inpPush = core.getBooleanInput('push'); 704 | const inpImageNames = core.getMultilineInput('image-names'); 705 | const inpTagNames = core.getMultilineInput('tag-names'); 706 | const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); 707 | 708 | const digests = []; 709 | for (const key of Object.keys(inpBuildOutputs)) { 710 | const output = JSON.parse(inpBuildOutputs[key]); 711 | if (output.imageDigest) { 712 | digests.push(output.imageDigest); 713 | } 714 | } 715 | if (digests.length === 0) { 716 | core.setFailed('No image digests found from build outputs'); 717 | return; 718 | } 719 | 720 | for (const imageName of inpImageNames) { 721 | let createArgs = ['buildx', 'imagetools', 'create']; 722 | for (const tag of inpTagNames) { 723 | createArgs.push('-t', `${imageName}:${tag}`); 724 | } 725 | for (const digest of digests) { 726 | createArgs.push(digest); 727 | } 728 | if (inpPush) { 729 | await exec.exec('docker', createArgs); 730 | } else { 731 | await core.group(`Generated imagetools create command for ${imageName}`, async () => { 732 | core.info(`docker ${createArgs.join(' ')}`); 733 | }); 734 | } 735 | } 736 | - 737 | name: Merge artifacts 738 | if: ${{ inputs.output == 'local' && inputs.artifact-upload }} 739 | uses: actions/upload-artifact/merge@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 740 | with: 741 | name: ${{ inputs.artifact-name }} 742 | pattern: ${{ inputs.artifact-name }}* 743 | delete-merged: true 744 | - 745 | name: Set outputs 746 | id: set 747 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 748 | env: 749 | INPUT_BUILD-OUTPUTS: ${{ toJSON(needs.build.outputs) }} 750 | INPUT_SIGNED: ${{ needs.prepare.outputs.sign }} 751 | with: 752 | script: | 753 | const inpBuildOutputs = JSON.parse(core.getInput('build-outputs')); 754 | const inpSigned = core.getBooleanInput('signed'); 755 | 756 | if (inpSigned) { 757 | const verifyCommands = []; 758 | for (const key of Object.keys(inpBuildOutputs)) { 759 | const output = JSON.parse(inpBuildOutputs[key]); 760 | if (output.verifyCommands) { 761 | verifyCommands.push(output.verifyCommands); 762 | } 763 | } 764 | core.setOutput('cosign-verify-commands', verifyCommands.join('\n')); 765 | } 766 | --------------------------------------------------------------------------------