diff --git a/.github/subaction-matrix.png b/.github/subaction-matrix.png new file mode 100644 index 0000000..4a67d9e Binary files /dev/null and b/.github/subaction-matrix.png differ diff --git a/.github/workflows/ci-subaction.yml b/.github/workflows/ci-subaction.yml index ebd7028..6274352 100644 --- a/.github/workflows/ci-subaction.yml +++ b/.github/workflows/ci-subaction.yml @@ -25,8 +25,22 @@ on: - 'test/**' jobs: - list-targets-group: + list-targets: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - + testdir: group + - + testdir: group-matrix + target: validate + - + testdir: multi-files + files: | + docker-bake.json + docker-bake.hcl steps: - name: Checkout @@ -36,20 +50,32 @@ jobs: id: gen uses: ./subaction/list-targets with: - workdir: ./test/group - - - name: Check targets - uses: actions/github-script@v7 - with: - script: | - const targets = `${{ steps.gen.outputs.targets }}`; - if (!targets) { - core.setFailed('No targets generated'); - } - core.info(`targets=${targets}`); + workdir: ./test/${{ matrix.testdir }} + files: ${{ matrix.files }} + target: ${{ matrix.target }} - list-targets-group-matrix: + matrix: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - + testdir: group + - + testdir: group-matrix + target: validate + - + testdir: group-with-platform + target: validate + - + testdir: group-with-platform + target: validate + fields: platforms + - + testdir: group-with-platform + target: validate + fields: platforms,dockerfile steps: - name: Checkout @@ -57,43 +83,8 @@ jobs: - name: Matrix gen id: gen - uses: ./subaction/list-targets + uses: ./subaction/matrix with: - workdir: ./test/group-matrix - target: validate - - - name: Check targets - uses: actions/github-script@v7 - with: - script: | - const targets = `${{ steps.gen.outputs.targets }}`; - if (!targets) { - core.setFailed('No targets generated'); - } - core.info(`targets=${targets}`); - - list-targets-multi-files: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v5 - - - name: Matrix gen - id: gen - uses: ./subaction/list-targets - with: - workdir: ./test/multi-files - files: | - docker-bake.json - docker-bake.hcl - - - name: Check targets - uses: actions/github-script@v7 - with: - script: | - const targets = `${{ steps.gen.outputs.targets }}`; - if (!targets) { - core.setFailed('No targets generated'); - } - core.info(`targets=${targets}`); + workdir: ./test/${{ matrix.testdir }} + target: ${{ matrix.target }} + fields: ${{ matrix.fields }} diff --git a/README.md b/README.md index 10fc84a..cb03aa7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ___ * [outputs](#outputs) * [environment variables](#environment-variables) * [Subactions](#subactions) - * [`list-targets`](subaction/list-targets) + * [`matrix`](subaction/matrix) * [Contributing](#contributing) ## Usage @@ -234,7 +234,7 @@ The following outputs are available ## Subactions -* [`list-targets`](subaction/list-targets) +* [`matrix`](subaction/matrix) ## Contributing diff --git a/subaction/matrix/README.md b/subaction/matrix/README.md new file mode 100644 index 0000000..a84d9a3 --- /dev/null +++ b/subaction/matrix/README.md @@ -0,0 +1,140 @@ +## About + +This subaction generates a multi-dimension matrix that can be used in a [GitHub matrix](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix) +through the [`include` property](https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations) +so you can distribute your builds across multiple runners. + +![Screenshot](../../.github/subaction-matrix.png) + +___ + +* [Usage](#usage) +* [Customizing](#customizing) + * [inputs](#inputs) + * [outputs](#outputs) + +## Usage + +### List targets + +```hcl +# docker-bake.hcl +group "validate" { + targets = ["lint", "doctoc"] +} + +target "lint" { + target = "lint" +} + +target "doctoc" { + target = "doctoc" +} +``` + +```yaml +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate.outputs.matrix }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Generate matrix + id: generate + uses: docker/bake-action/subaction/matrix@v6 + with: + target: validate + + validate: + runs-on: ubuntu-latest + needs: + - prepare + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.prepare.outputs.matrix) }} + steps: + - + name: Validate + uses: docker/bake-action@v6 + with: + targets: ${{ matrix.target }} +``` + +### Platforms split + +```hcl +# docker-bake.hcl +target "lint" { + dockerfile = "./hack/dockerfiles/lint.Dockerfile" + output = ["type=cacheonly"] + platforms = [ + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64", + "linux/s390x", + "linux/ppc64le", + "linux/riscv64", + "windows/amd64", + "windows/arm64" + ] +} +``` + +```yaml +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate.outputs.matrix }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Generate matrix + id: generate + uses: docker/bake-action/subaction/matrix@v6 + with: + target: lint + fields: platforms + + lint: + runs-on: ${{ startsWith(matrix.platforms, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: + - prepare + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.prepare.outputs.matrix) }} + steps: + - + name: Lint + uses: docker/bake-action@v6 + with: + targets: ${{ matrix.target }} + set: | + *.platform=${{ matrix.platforms }} +``` + +## Customizing + +### inputs + +| Name | Type | Description | +|-----------|----------|------------------------------------------------------------------------------------------------| +| `workdir` | String | Working directory to use (defaults to `.`) | +| `files` | List/CSV | List of [bake definition files](https://docs.docker.com/build/customize/bake/file-definition/) | +| `target` | String | The target to use within the bake file | +| `fields` | String | List of extra fields to include in the matrix | + +### outputs + +| Name | Type | Description | +|----------|------|----------------------| +| `matrix` | JSON | Matrix configuration | diff --git a/subaction/matrix/action.yml b/subaction/matrix/action.yml new file mode 100644 index 0000000..32b2000 --- /dev/null +++ b/subaction/matrix/action.yml @@ -0,0 +1,101 @@ +# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions +name: 'Matrix' +description: 'Generate a matrix from a Bake definition to help distributing builds in your workflow' + +inputs: + workdir: + description: Working directory + default: '.' + required: false + files: + description: List of Bake files + required: false + target: + description: Bake target + required: false + fields: + description: List of extra fields to include in the matrix + required: false + +outputs: + matrix: + description: Matrix configuration + value: ${{ steps.generate.outputs.includes }} + +runs: + using: composite + steps: + - + name: Generate + id: generate + uses: actions/github-script@v7 + env: + INPUT_WORKDIR: ${{ inputs.workdir }} + INPUT_FILES: ${{ inputs.files }} + INPUT_TARGET: ${{ inputs.target }} + INPUT_FIELDS: ${{ inputs.fields }} + with: + script: | + function getInputList(name) { + return core.getInput(name) ? core.getInput(name).split(/[\r?\n,]+/).filter(x => x !== '') : []; + } + + const workdir = core.getInput('workdir'); + const files = getInputList('files'); + const target = core.getInput('target'); + const fields = getInputList('fields'); + + let def = {}; + await core.group(`Parsing definition`, async () => { + let args = ['buildx', 'bake']; + for (const file of files) { + args.push('--file', file); + } + if (target) { + args.push(target); + } + args.push('--print'); + const res = await exec.getExecOutput('docker', args, { + ignoreReturnCode: true, + silent: true, + cwd: workdir + }); + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + def = JSON.parse(res.stdout.trim()); + core.info(JSON.stringify(def, null, 2)); + }); + + await core.group(`Generating matrix`, async () => { + const result = []; + for (const targetName of Object.keys(def.target)) { + const target = def.target[targetName]; + const entry = { target: targetName }; + if (fields.length === 0) { + result.push({ ...entry }); + continue; + } + let fieldFound = false; + Object.keys(target).forEach(field => { + if (fields.includes(field)) { + fieldFound = true; + const value = target[field]; + if (Array.isArray(value)) { + value.forEach((v) => { + entry[field] = v; + result.push({ ...entry }); + }); + } else { + entry[field] = value; + result.push({ ...entry }); + } + } + }); + if (!fieldFound) { + result.push({ ...entry }); + } + } + core.info(JSON.stringify(result, null, 2)); + core.setOutput('includes', JSON.stringify(result)); + }); diff --git a/test/group-with-platform/docker-bake.hcl b/test/group-with-platform/docker-bake.hcl new file mode 100644 index 0000000..bfba830 --- /dev/null +++ b/test/group-with-platform/docker-bake.hcl @@ -0,0 +1,36 @@ +group "validate" { + targets = ["lint", "lint-gopls", "validate-vendor", "validate-docs"] +} + +target "lint" { + dockerfile = "./hack/dockerfiles/lint.Dockerfile" + output = ["type=cacheonly"] + platforms = [ + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64", + "linux/s390x", + "linux/ppc64le", + "linux/riscv64", + "windows/amd64", + "windows/arm64" + ] +} + +target "lint-gopls" { + inherits = ["lint"] + target = "gopls-analyze" +} + +target "validate-vendor" { + dockerfile = "./hack/dockerfiles/vendor.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + +target "validate-docs" { + dockerfile = "./hack/dockerfiles/docs.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +}