From 10e9d5d585d039409f201834e75872c7f63f8290 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 24 Dec 2020 04:13:41 +0100 Subject: [PATCH] Add bake-file output (#36) Co-authored-by: CrazyMax --- .github/workflows/ci.yml | 35 ++++++ README.md | 69 ++++++----- __tests__/context.test.ts | 11 ++ __tests__/meta.test.ts | 241 ++++++++++++++++++++++++++++++++++++++ action.yml | 2 + dist/index.js | 45 ++++++- src/context.ts | 12 ++ src/main.ts | 10 ++ src/meta.ts | 34 +++++- test/docker-bake.hcl | 38 ++++++ 10 files changed, 460 insertions(+), 37 deletions(-) create mode 100644 test/docker-bake.hcl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c8063a..cd1926a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,3 +177,38 @@ jobs: name: Dump context if: always() uses: crazy-max/ghaction-dump-context@v1 + + bake: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Docker meta + id: docker_meta + uses: ./ + with: + images: | + ${{ env.DOCKER_IMAGE }} + ghcr.io/name/app + tag-sha: true + tag-semver: | + {{version}} + {{major}}.{{minor}} + {{major}} + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Build + uses: crazy-max/ghaction-docker-buildx-bake@v1 + with: + files: | + ./test/docker-bake.hcl + ${{ steps.docker_meta.outputs.bake-file }} + targets: | + release diff --git a/README.md b/README.md index 798cfb6..053b136 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ___ * [Usage](#usage) * [Basic](#basic) * [Semver](#semver) - * [Complete](#complete) + * [Bake definition](#bake-definition) * [Customizing](#customizing) * [inputs](#inputs) * [outputs](#outputs) @@ -85,7 +85,7 @@ jobs: - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v1 + uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -147,7 +147,7 @@ jobs: - name: Login to DockerHub if: github.event_name != 'pull_request' - uses: docker/login-action@v1 + uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -163,23 +163,29 @@ jobs: labels: ${{ steps.docker_meta.outputs.labels }} ``` -### Complete +### Bake definition -| Event | Ref | Commit SHA | Docker Tags | -|-----------------|-------------------------------|------------|-----------------------------------------| -| `schedule` | `refs/heads/master` | `45f132a` | `sha-45f132a`, `nightly` | -| `pull_request` | `refs/pull/2/merge` | `a123b57` | `sha-a123b57`, `pr-2` | -| `push` | `refs/heads/master` | `cf20257` | `sha-cf20257`, `master` | -| `push` | `refs/heads/my/branch` | `a5df687` | `sha-a5df687`, `my-branch` | -| `push tag` | `refs/tags/v1.2.3` | `ad132f5` | `sha-ad132f5`, `1.2.3`, `1.2`, `latest` | -| `push tag` | `refs/tags/v2.0.8-beta.67` | `fc89efd` | `sha-fc89efd`, `2.0.8-beta.67` | +This action also handles a bake definition file that can be used with the +[Docker Buildx Bake action](https://github.com/crazy-max/ghaction-docker-buildx-bake). You just have to declare a +target named `ghaction-docker-meta`. + +```hcl +// docker-bake.hcl + +target "ghaction-docker-meta" {} + +target "build" { + inherits = ["ghaction-docker-meta"] + context = "./" + dockerfile = "Dockerfile" + platforms = ["linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64", "linux/386", "linux/ppc64le"] +} +``` ```yaml name: ci on: - schedule: - - cron: '0 10 * * *' # everyday at 10am push: branches: - '**' @@ -211,22 +217,14 @@ jobs: name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v1 + name: Build + uses: crazy-max/ghaction-docker-buildx-bake@v1 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64,linux/386 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} + files: | + ./docker-bake.hcl + ${{ steps.docker_meta.outputs.bake-file }} + targets: | + build ``` ## Customizing @@ -251,7 +249,7 @@ Following inputs can be used as `step.with` keys | Name | Type | Description | |---------------------|----------|------------------------------------| | `images` | List/CSV | List of Docker images to use as base name for tags | -| `tag-sha` | Bool | Add git short SHA as Docker tag (default `false`) | +| `tag-sha` | Bool | Add git short commit as Docker tag (default `false`) | | `tag-edge` | Bool | Enable edge branch tagging (default `false`) | | `tag-edge-branch` | String | Branch that will be tagged as edge (default `repo.default_branch`) | | `tag-semver` | List/CSV | Handle Git tag as semver [template](#handle-semver-tag) if possible | @@ -273,9 +271,10 @@ Following outputs are available | Name | Type | Description | |---------------|---------|---------------------------------------| -| `version` | String | Generated Docker image version | -| `tags` | String | Generated Docker tags | -| `labels` | String | Generated Docker labels | +| `version` | String | Docker image version | +| `tags` | String | Docker tags | +| `labels` | String | Docker labels | +| `bake-file` | File | [Bake definition file](https://github.com/docker/buildx#file-definition) path | ## Notes @@ -327,7 +326,7 @@ the following expressions: | Expression | Example | Description | |-------------------------|-------------------------------------------|------------------------------------------| -| `{{date 'format'}}` | `{{date 'YYYYMMDD'}}` > `20200110` | Render date by its [moment format](https://momentjs.com/docs/#/displaying/format/) +| `{{date 'format'}}` | `{{date 'YYYYMMDD'}}` > `20200110` | Render date by its [moment format](https://momentjs.com/docs/#/displaying/format/) You can find more examples in the [CI workflow](.github/workflows/ci.yml). @@ -366,7 +365,7 @@ updates: interval: "daily" ``` -# Contributing +## Contributing Want to contribute? Awesome! The most basic way to show your support is to star :star2: the project, or to raise issues :speech_balloon:. If you want to open a pull request, please read the diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 87d95db..70de571 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -1,5 +1,16 @@ +import * as fs from 'fs'; +import * as path from 'path'; + import * as context from '../src/context'; +jest.spyOn(context, 'tmpDir').mockImplementation((): string => { + const tmpDir = path.join('/tmp/.ghaction-docker-meta-jest').split(path.sep).join(path.posix.sep); + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, {recursive: true}); + } + return tmpDir; +}); + describe('getInputList', () => { it('single line correctly', async () => { await setInput('foo', 'bar'); diff --git a/__tests__/meta.test.ts b/__tests__/meta.test.ts index aea3425..844a3f7 100644 --- a/__tests__/meta.test.ts +++ b/__tests__/meta.test.ts @@ -1484,3 +1484,244 @@ describe('custom', () => { ], ])('given %p event ', tagsLabelsTest); }); + +describe('bake-file', () => { + // prettier-ignore + test.each([ + [ + 'event_push.env', + { + images: ['user/app'], + tagCustom: ['my', 'custom', 'tags'] + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "user/app:dev", + "user/app:my", + "user/app:custom", + "user/app:tags" + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "dev", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_push.env', + { + images: ['user/app'], + tagCustom: ['my'] + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "user/app:dev", + "user/app:my", + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "dev", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_tag_release1.env', + { + images: ['user/app'], + tagCustom: ['my', 'custom', 'tags'] + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "user/app:release1", + "user/app:my", + "user/app:custom", + "user/app:tags", + "user/app:latest" + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "release1", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_tag_20200110-RC2.env', + { + images: ['user/app'], + tagMatch: `\\d{8}`, + tagLatest: false, + tagCustom: ['my', 'custom', 'tags'] + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "user/app:20200110", + "user/app:my", + "user/app:custom", + "user/app:tags" + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "20200110", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_tag_v1.1.1.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tagSemver: ['{{version}}', '{{major}}.{{minor}}', '{{major}}'], + tagCustom: ['my', 'custom', 'tags'] + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "org/app:1.1.1", + "org/app:1.1", + "org/app:1", + "org/app:my", + "org/app:custom", + "org/app:tags", + "org/app:latest", + "ghcr.io/user/app:1.1.1", + "ghcr.io/user/app:1.1", + "ghcr.io/user/app:1", + "ghcr.io/user/app:my", + "ghcr.io/user/app:custom", + "ghcr.io/user/app:tags", + "ghcr.io/user/app:latest" + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "1.1.1", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_tag_v1.1.1.env', + { + images: ['org/app', 'ghcr.io/user/app'], + tagSemver: ['{{version}}', '{{major}}.{{minor}}.{{patch}}'], + tagCustom: ['my', 'custom', 'tags'], + tagCustomOnly: true, + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "org/app:my", + "org/app:custom", + "org/app:tags", + "ghcr.io/user/app:my", + "ghcr.io/user/app:custom", + "ghcr.io/user/app:tags" + ], + "labels": { + "org.opencontainers.image.title": "Hello-World", + "org.opencontainers.image.description": "This your first repo!", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.version": "my", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ], + [ + 'event_tag_v1.1.1.env', + { + images: ['org/app'], + labelCustom: [ + "maintainer=CrazyMax", + "org.opencontainers.image.title=MyCustom=Title", + "org.opencontainers.image.description=Another description", + "org.opencontainers.image.vendor=MyCompany", + ], + } as Inputs, + { + "target": { + "ghaction-docker-meta": { + "tags": [ + "org/app:v1.1.1", + "org/app:latest" + ], + "labels": { + "maintainer": "CrazyMax", + "org.opencontainers.image.title": "MyCustom=Title", + "org.opencontainers.image.description": "Another description", + "org.opencontainers.image.url": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.source": "https://github.com/octocat/Hello-World", + "org.opencontainers.image.vendor": "MyCompany", + "org.opencontainers.image.version": "v1.1.1", + "org.opencontainers.image.created": "2020-01-10T00:30:00.000Z", + "org.opencontainers.image.revision": "90dd6032fac8bda1b6c4436a2e65de27961ed071", + "org.opencontainers.image.licenses": "MIT" + } + } + } + } + ] + ])('given %p event ', async (envFile: string, inputs: Inputs, exBakeDefinition: {}) => { + process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures', envFile))); + const context = github.context(); + console.log(process.env, context); + + const repo = await github.repo(process.env.GITHUB_TOKEN || ''); + const meta = new Meta({...getInputs(), ...inputs}, context, repo); + + const bakeFile = meta.bakeFile(); + console.log('bakeFile', bakeFile, fs.readFileSync(bakeFile, 'utf8')); + expect(JSON.parse(fs.readFileSync(bakeFile, 'utf8'))).toEqual(exBakeDefinition); + }); +}); diff --git a/action.yml b/action.yml index 8a4181d..5e8c10b 100644 --- a/action.yml +++ b/action.yml @@ -68,6 +68,8 @@ outputs: description: 'Generated Docker tags' labels: description: 'Generated Docker labels' + bake-file: + description: 'Bake definiton file' runs: using: 'node12' diff --git a/dist/index.js b/dist/index.js index 3a8f66e..02424e5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -39,9 +39,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.asyncForEach = exports.getInputList = exports.getInputs = void 0; +exports.asyncForEach = exports.getInputList = exports.getInputs = exports.tmpDir = void 0; const sync_1 = __importDefault(__webpack_require__(8750)); const core = __importStar(__webpack_require__(2186)); +const fs = __importStar(__webpack_require__(5747)); +const os = __importStar(__webpack_require__(2087)); +const path = __importStar(__webpack_require__(5622)); +let _tmpDir; +function tmpDir() { + if (!_tmpDir) { + _tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghaction-docker-meta-')).split(path.sep).join(path.posix.sep); + } + return _tmpDir; +} +exports.tmpDir = tmpDir; function getInputs() { return { images: getInputList('images'), @@ -184,6 +195,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); +const fs = __importStar(__webpack_require__(5747)); const context_1 = __webpack_require__(3842); const github = __importStar(__webpack_require__(5928)); const meta_1 = __webpack_require__(3714); @@ -213,6 +225,7 @@ function run() { core.info(version.main || ''); core.endGroup(); core.setOutput('version', version.main || ''); + // Docker tags const tags = meta.tags(); core.startGroup(`Docker tags`); for (let tag of tags) { @@ -220,6 +233,7 @@ function run() { } core.endGroup(); core.setOutput('tags', tags.join(inputs.sepTags)); + // Docker labels const labels = meta.labels(); core.startGroup(`Docker labels`); for (let label of labels) { @@ -227,6 +241,12 @@ function run() { } core.endGroup(); core.setOutput('labels', labels.join(inputs.sepLabels)); + // Bake definition file + const bakeFile = meta.bakeFile(); + core.startGroup(`Bake definition file`); + core.info(fs.readFileSync(bakeFile, 'utf8')); + core.endGroup(); + core.setOutput('bake-file', bakeFile); } catch (error) { core.setFailed(error.message); @@ -268,8 +288,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.Meta = void 0; const handlebars = __importStar(__webpack_require__(7492)); +const fs = __importStar(__webpack_require__(5747)); +const path = __importStar(__webpack_require__(5622)); const moment_1 = __importDefault(__webpack_require__(9623)); const semver = __importStar(__webpack_require__(1383)); +const context_1 = __webpack_require__(3842); const core = __importStar(__webpack_require__(2186)); class Meta { constructor(inputs, context, repo) { @@ -397,6 +420,26 @@ class Meta { labels.push(...this.inputs.labelCustom); return labels; } + bakeFile() { + let jsonLabels = {}; + for (let label of this.labels()) { + const matches = label.match(/([^=]*)=(.*)/); + if (!matches) { + continue; + } + jsonLabels[matches[1]] = matches[2]; + } + const bakeFile = path.join(context_1.tmpDir(), 'ghaction-docker-meta-bake.json').split(path.sep).join(path.posix.sep); + fs.writeFileSync(bakeFile, JSON.stringify({ + target: { + 'ghaction-docker-meta': { + tags: this.tags(), + labels: jsonLabels + } + } + }, null, 2)); + return bakeFile; + } } exports.Meta = Meta; //# sourceMappingURL=meta.js.map diff --git a/src/context.ts b/src/context.ts index 68eb5b9..3b55c16 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,10 @@ import csvparse from 'csv-parse/lib/sync'; import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +let _tmpDir: string; export interface Inputs { images: string[]; @@ -19,6 +24,13 @@ export interface Inputs { githubToken: string; } +export function tmpDir(): string { + if (!_tmpDir) { + _tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghaction-docker-meta-')).split(path.sep).join(path.posix.sep); + } + return _tmpDir; +} + export function getInputs(): Inputs { return { images: getInputList('images'), diff --git a/src/main.ts b/src/main.ts index c476072..cb99345 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import {getInputs, Inputs} from './context'; import * as github from './github'; import {Meta, Version} from './meta'; @@ -33,6 +34,7 @@ async function run() { core.endGroup(); core.setOutput('version', version.main || ''); + // Docker tags const tags: Array = meta.tags(); core.startGroup(`Docker tags`); for (let tag of tags) { @@ -41,6 +43,7 @@ async function run() { core.endGroup(); core.setOutput('tags', tags.join(inputs.sepTags)); + // Docker labels const labels: Array = meta.labels(); core.startGroup(`Docker labels`); for (let label of labels) { @@ -48,6 +51,13 @@ async function run() { } core.endGroup(); core.setOutput('labels', labels.join(inputs.sepLabels)); + + // Bake definition file + const bakeFile: string = meta.bakeFile(); + core.startGroup(`Bake definition file`); + core.info(fs.readFileSync(bakeFile, 'utf8')); + core.endGroup(); + core.setOutput('bake-file', bakeFile); } catch (error) { core.setFailed(error.message); } diff --git a/src/meta.ts b/src/meta.ts index b87203e..c019c03 100644 --- a/src/meta.ts +++ b/src/meta.ts @@ -1,7 +1,9 @@ import * as handlebars from 'handlebars'; +import * as fs from 'fs'; +import * as path from 'path'; import moment from 'moment'; import * as semver from 'semver'; -import {Inputs} from './context'; +import {Inputs, tmpDir} from './context'; import * as core from '@actions/core'; import {Context} from '@actions/github/lib/context'; import {ReposGetResponseData} from '@octokit/types'; @@ -143,4 +145,34 @@ export class Meta { labels.push(...this.inputs.labelCustom); return labels; } + + public bakeFile(): string { + let jsonLabels = {}; + for (let label of this.labels()) { + const matches = label.match(/([^=]*)=(.*)/); + if (!matches) { + continue; + } + jsonLabels[matches[1]] = matches[2]; + } + + const bakeFile = path.join(tmpDir(), 'ghaction-docker-meta-bake.json').split(path.sep).join(path.posix.sep); + fs.writeFileSync( + bakeFile, + JSON.stringify( + { + target: { + 'ghaction-docker-meta': { + tags: this.tags(), + labels: jsonLabels + } + } + }, + null, + 2 + ) + ); + + return bakeFile; + } } diff --git a/test/docker-bake.hcl b/test/docker-bake.hcl new file mode 100644 index 0000000..eef851f --- /dev/null +++ b/test/docker-bake.hcl @@ -0,0 +1,38 @@ +target "ghaction-docker-meta" {} + +group "default" { + targets = ["db", "app"] +} + +group "release" { + targets = ["db", "app-plus"] +} + +target "db" { + context = "./test" + tags = ["docker.io/tonistiigi/db"] +} + +target "app" { + inherits = ["ghaction-docker-meta"] + context = "./test" + dockerfile = "Dockerfile" + args = { + name = "foo" + } +} + +target "cross" { + platforms = [ + "linux/amd64", + "linux/arm64", + "linux/386" + ] +} + +target "app-plus" { + inherits = ["app", "cross"] + args = { + IAMPLUS = "true" + } +}