diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24847bc..b65f9fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,30 +12,6 @@ on: jobs: main: - runs-on: ubuntu-latest - steps: - - - name: Run local registry - run: | - docker run -d -p 5000:5000 registry:2 - - - name: Checkout - uses: actions/checkout@v2.3.1 - - - name: Build and push - uses: ./ - with: - context: ./test - file: ./test/Dockerfile - push: true - tags: | - localhost:5000/name/app:latest - localhost:5000/name/app:1.0.0 - - - name: Dump context - uses: crazy-max/ghaction-dump-context@v1 - - buildx: runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/README.md b/README.md index 1f84c03..9eb0da5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ ___ * [Usage](#usage) * [Quick start](#quick-start) - * [With Buildx](#with-buildx) * [Customizing](#customizing) * [inputs](#inputs) * [outputs](#outputs) @@ -14,49 +13,16 @@ ___ ## Usage +This action uses our [setup-buildx](https://github.com/docker/setup-buildx-action) action that extends the +`docker build` command named [buildx](https://github.com/docker/buildx) with the full support of the features +provided by [Moby BuildKit](https://github.com/moby/buildkit) builder toolkik. This includes multi-arch build, +build-secrets, remote cache, etc. and different builder deployment/namespacing options. + ### Quick start ```yaml name: ci -on: - pull_request: - branches: master - push: - branches: master - tags: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v2 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push - uses: docker/build-push-action@v2 - with: - tags: | - user/app:latest - user/app:1.0.0 -``` - -### With Buildx - -You can also use our [setup-buildx](https://github.com/docker/setup-buildx-action) action that extends the -`docker build` command with the full support of the features provided by -[Moby BuildKit](https://github.com/moby/buildkit) builder toolkit to build multi-platform images. - -```yaml -name: ci - on: pull_request: branches: master @@ -94,6 +60,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x + push: true tags: | user/app:latest user/app:1.0.0 @@ -107,6 +74,7 @@ Following inputs can be used as `step.with` keys | Name | Type | Default | Description | |---------------------|---------|-----------------------------------|------------------------------------| +| `builder` | String | | Builder instance | | `context` | String | `.` | Build's context is the set of files located in the specified `PATH` or `URL` | | `file` | String | `./Dockerfile` | Path to the Dockerfile. | | `build-args` | String | | Newline-delimited list of build-time variables | @@ -115,16 +83,12 @@ Following inputs can be used as `step.with` keys | `pull` | Bool | `false` | Always attempt to pull a newer version of the image | | `target` | String | | Sets the target stage to build | | `no-cache` | Bool | `false` | Do not use cache when building the image | -| `builder`**¹** | String | | Builder instance | -| `platforms`**¹** | String | | Comma-delimited list of target platforms for build | -| `load`**¹** | Bool | `false` | Shorthand for `--output=type=docker` | -| `push` | Bool | `false` | Whether to push the built image (or shorthand for `--output=type=registry` if buildx used) | -| `outputs`**¹** | String | | Newline-delimited list of output destinations (format: `type=local,dest=path`) | -| `cache-from`**¹** | String | | Newline-delimited list of external cache sources (eg. `user/app:cache`, `type=local,src=path/to/dir`) | -| `cache-to`**¹** | String | | Newline-delimited list of cache export destinations (eg. `user/app:cache`, `type=local,dest=path/to/dir`) | - -> **¹** Only available if [docker buildx](https://github.com/docker/buildx) is enabled. -> See [setup-buildx](https://github.com/docker/setup-buildx-action) action for more info. +| `platforms` | String | | Comma-delimited list of target platforms for build | +| `load` | Bool | `false` | Shorthand for `--output=type=docker` | +| `push` | Bool | `false` | Shorthand for `--output=type=registry` | +| `outputs` | String | | Newline-delimited list of output destinations (format: `type=local,dest=path`) | +| `cache-from` | String | | Newline-delimited list of external cache sources (eg. `user/app:cache`, `type=local,src=path/to/dir`) | +| `cache-to` | String | | Newline-delimited list of cache export destinations (eg. `user/app:cache`, `type=local,dest=path/to/dir`) | ### outputs diff --git a/action.yml b/action.yml index 34c6575..581a99a 100644 --- a/action.yml +++ b/action.yml @@ -6,6 +6,9 @@ branding: color: 'blue' inputs: + builder: + description: "Builder instance" + required: false context: description: "Build's context is the set of files located in the specified PATH or URL" required: false @@ -34,9 +37,6 @@ inputs: description: "Do not use cache when building the image" required: false default: 'false' - builder: - description: "Builder instance" - required: false platforms: description: "Comma-delimited list of target platforms for build" required: false @@ -45,7 +45,7 @@ inputs: required: false default: 'false' push: - description: "Whether to push the built image (shorthand for --output=type=registry if buildx used)" + description: "Shorthand for --output=type=registry" required: false default: 'false' outputs: @@ -65,4 +65,3 @@ outputs: runs: using: 'node12' main: 'dist/index.js' - diff --git a/dist/index.js b/dist/index.js index 8fe7c89..9204c00 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1004,7 +1004,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const os = __importStar(__webpack_require__(87)); const buildx = __importStar(__webpack_require__(982)); const context_helper_1 = __webpack_require__(338); -const docker_1 = __webpack_require__(231); const core = __importStar(__webpack_require__(470)); const exec = __importStar(__webpack_require__(986)); function run() { @@ -1015,23 +1014,15 @@ function run() { return; } const inputs = yield context_helper_1.loadInputs(); - const buildxAvailable = yield buildx.isAvailable(); - const buildxInstalled = buildxAvailable && (yield buildx.isInstalled()); - const buildxEnabled = (yield context_helper_1.mustBuildx(inputs)) || buildxInstalled; - let buildArgs = []; - // Check buildx - if (buildxEnabled) { - if (!buildxAvailable) { - core.setFailed(`Buildx is required but not available`); - return; - } - core.info(`🚀 Buildx will be used to build your image`); - buildArgs.push('buildx', 'build'); + if (!(yield buildx.isAvailable())) { + core.setFailed(`Buildx is required. See https://github.com/docker/setup-buildx-action to set up buildx.`); + return; } - else { - buildArgs.push('build'); + let buildArgs = ['buildx', 'build']; + if (inputs.builder) { + core.info(`📌 Using builder instance ${inputs.builder}`); + yield buildx.use(inputs.builder); } - // Global options if (inputs.file) { buildArgs.push('--file', inputs.file); } @@ -1053,50 +1044,27 @@ function run() { if (inputs.noCache) { buildArgs.push('--no-cache'); } - // Buildx options - if (buildxEnabled) { - if (inputs.builder) { - core.info(`📌 Using builder instance ${inputs.builder}`); - yield buildx.use(inputs.builder); - } - if (inputs.platforms) { - buildArgs.push('--platform', inputs.platforms); - } - if (inputs.load) { - buildArgs.push('--load'); - } - if (inputs.push) { - buildArgs.push('--push'); - } - yield asyncForEach(inputs.outputs, (output) => __awaiter(this, void 0, void 0, function* () { - buildArgs.push('--output', output); - })); - yield asyncForEach(inputs.cacheFrom, (cacheFrom) => __awaiter(this, void 0, void 0, function* () { - buildArgs.push('--cache-from', cacheFrom); - })); - yield asyncForEach(inputs.cacheTo, (cacheTo) => __awaiter(this, void 0, void 0, function* () { - buildArgs.push('--cache-from', cacheTo); - })); + if (inputs.platforms) { + buildArgs.push('--platform', inputs.platforms); } + if (inputs.load) { + buildArgs.push('--load'); + } + if (inputs.push) { + buildArgs.push('--push'); + } + yield asyncForEach(inputs.outputs, (output) => __awaiter(this, void 0, void 0, function* () { + buildArgs.push('--output', output); + })); + yield asyncForEach(inputs.cacheFrom, (cacheFrom) => __awaiter(this, void 0, void 0, function* () { + buildArgs.push('--cache-from', cacheFrom); + })); + yield asyncForEach(inputs.cacheTo, (cacheTo) => __awaiter(this, void 0, void 0, function* () { + buildArgs.push('--cache-from', cacheTo); + })); buildArgs.push(inputs.context); core.info(`🏃 Starting build...`); yield exec.exec('docker', buildArgs); - if (!buildxEnabled && inputs.push) { - let pushRepos = []; - yield asyncForEach(inputs.tags, (tag) => __awaiter(this, void 0, void 0, function* () { - const img = yield docker_1.parseImage(tag); - if (!img) { - core.warning(`Cannot parse image reference ${tag}`); - return; - } - const repo = `${img.registry}${img.namespace}${img.repository}`; - if (!pushRepos.includes(repo)) { - pushRepos.push(repo); - core.info(`⬆️ Pushing ${repo}...`); - yield exec.exec('docker', ['push', repo]); - } - })); - } } catch (error) { core.setFailed(error.message); @@ -1113,63 +1081,6 @@ run(); /***/ }), -/***/ 231: -/***/ (function(__unusedmodule, exports, __webpack_require__) { - -"use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseImage = exports.config = void 0; -const path_1 = __importDefault(__webpack_require__(622)); -const os_1 = __importDefault(__webpack_require__(87)); -const fs_1 = __importDefault(__webpack_require__(747)); -function config() { - return __awaiter(this, void 0, void 0, function* () { - const dockerHome = process.env.DOCKER_CONFIG || path_1.default.join(os_1.default.homedir(), '.docker'); - const file = path_1.default.join(dockerHome, 'config.json'); - if (!fs_1.default.existsSync(file)) { - return; - } - return JSON.parse(fs_1.default.readFileSync(file, { encoding: 'utf-8' })); - }); -} -exports.config = config; -exports.parseImage = (image) => __awaiter(void 0, void 0, void 0, function* () { - const match = image.match(/^(?:([^\/]+)\/)?(?:([^\/]+)\/)?([^@:\/]+)(?:[@:](.+))?$/); - if (!match) { - return; - } - let res = { - registry: match[1], - namespace: match[2], - repository: match[3], - tag: match[4] - }; - if (!res.namespace && res.registry && !/[:.]/.test(res.registry)) { - res.namespace = res.registry; - res.registry = undefined; - } - res.registry = res.registry ? `${res.registry}/` : ''; - res.namespace = res.namespace && res.namespace !== 'library' ? `${res.namespace}/` : ''; - res.tag = res.tag && res.tag !== 'latest' ? `:${res.tag}` : ''; - return res; -}); -//# sourceMappingURL=docker.js.map - -/***/ }), - /***/ 338: /***/ (function(__unusedmodule, exports, __webpack_require__) { @@ -1204,7 +1115,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.mustBuildx = exports.loadInputs = void 0; +exports.loadInputs = void 0; const core = __importStar(__webpack_require__(470)); function loadInputs() { return __awaiter(this, void 0, void 0, function* () { @@ -1228,17 +1139,6 @@ function loadInputs() { }); } exports.loadInputs = loadInputs; -function mustBuildx(inputs) { - return __awaiter(this, void 0, void 0, function* () { - return (inputs.builder.length > 0 || - inputs.platforms.length > 0 || - inputs.load || - inputs.outputs.length > 0 || - inputs.cacheFrom.length > 0 || - inputs.cacheTo.length > 0); - }); -} -exports.mustBuildx = mustBuildx; function getInputList(name) { return __awaiter(this, void 0, void 0, function* () { const items = core.getInput(name); @@ -1913,8 +1813,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.use = exports.isInstalled = exports.isAvailable = void 0; -const docker = __importStar(__webpack_require__(231)); +exports.use = exports.isAvailable = void 0; const exec = __importStar(__webpack_require__(807)); function isAvailable() { return __awaiter(this, void 0, void 0, function* () { @@ -1927,14 +1826,6 @@ function isAvailable() { }); } exports.isAvailable = isAvailable; -function isInstalled() { - var _a; - return __awaiter(this, void 0, void 0, function* () { - const dockerCfg = yield docker.config(); - return ((_a = dockerCfg === null || dockerCfg === void 0 ? void 0 : dockerCfg.aliases) === null || _a === void 0 ? void 0 : _a.builder) == 'buildx'; - }); -} -exports.isInstalled = isInstalled; function use(builder) { return __awaiter(this, void 0, void 0, function* () { return yield exec.exec(`docker`, ['buildx', 'use', '--builder', builder], false).then(res => { diff --git a/src/buildx.ts b/src/buildx.ts index 88629b6..38f38ef 100644 --- a/src/buildx.ts +++ b/src/buildx.ts @@ -1,4 +1,3 @@ -import * as docker from './docker'; import * as exec from './exec'; export async function isAvailable(): Promise { @@ -10,11 +9,6 @@ export async function isAvailable(): Promise { }); } -export async function isInstalled(): Promise { - const dockerCfg = await docker.config(); - return dockerCfg?.aliases?.builder == 'buildx'; -} - export async function use(builder: string): Promise { return await exec.exec(`docker`, ['buildx', 'use', '--builder', builder], false).then(res => { if (res.stderr != '' && !res.success) { diff --git a/src/context-helper.ts b/src/context-helper.ts index 5ae0b8c..d4f5b9f 100644 --- a/src/context-helper.ts +++ b/src/context-helper.ts @@ -38,17 +38,6 @@ export async function loadInputs(): Promise { }; } -export async function mustBuildx(inputs: Inputs): Promise { - return ( - inputs.builder.length > 0 || - inputs.platforms.length > 0 || - inputs.load || - inputs.outputs.length > 0 || - inputs.cacheFrom.length > 0 || - inputs.cacheTo.length > 0 - ); -} - async function getInputList(name: string): Promise { const items = core.getInput(name); if (items == '') { diff --git a/src/docker.ts b/src/docker.ts deleted file mode 100644 index 873a1b7..0000000 --- a/src/docker.ts +++ /dev/null @@ -1,54 +0,0 @@ -import path from 'path'; -import os from 'os'; -import fs from 'fs'; - -export interface Config { - credsStore?: string; - experimental?: string; - stackOrchestrator?: string; - aliases?: { - builder?: string; - }; -} - -export interface Image { - registry?: string; - namespace?: string; - repository: string; - tag?: string; -} - -export async function config(): Promise { - const dockerHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker'); - - const file: string = path.join(dockerHome, 'config.json'); - if (!fs.existsSync(file)) { - return; - } - - return JSON.parse(fs.readFileSync(file, {encoding: 'utf-8'})) as Config; -} - -export const parseImage = async (image: string): Promise => { - const match = image.match(/^(?:([^\/]+)\/)?(?:([^\/]+)\/)?([^@:\/]+)(?:[@:](.+))?$/); - if (!match) { - return; - } - - let res: Image = { - registry: match[1], - namespace: match[2], - repository: match[3], - tag: match[4] - }; - - if (!res.namespace && res.registry && !/[:.]/.test(res.registry)) { - res.namespace = res.registry; - res.registry = undefined; - } - - res.registry = res.registry ? `${res.registry}/` : ''; - res.namespace = res.namespace && res.namespace !== 'library' ? `${res.namespace}/` : ''; - res.tag = res.tag && res.tag !== 'latest' ? `:${res.tag}` : ''; - return res; -}; diff --git a/src/main.ts b/src/main.ts index 6a40f87..458709c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ import * as os from 'os'; import * as buildx from './buildx'; -import {Inputs, loadInputs, mustBuildx} from './context-helper'; -import {Image, parseImage} from './docker'; +import {Inputs, loadInputs} from './context-helper'; import * as core from '@actions/core'; import * as exec from '@actions/exec'; @@ -13,24 +12,18 @@ async function run(): Promise { } const inputs: Inputs = await loadInputs(); - const buildxAvailable = await buildx.isAvailable(); - const buildxInstalled = buildxAvailable && (await buildx.isInstalled()); - const buildxEnabled = (await mustBuildx(inputs)) || buildxInstalled; - let buildArgs: Array = []; - // Check buildx - if (buildxEnabled) { - if (!buildxAvailable) { - core.setFailed(`Buildx is required but not available`); - return; - } - core.info(`🚀 Buildx will be used to build your image`); - buildArgs.push('buildx', 'build'); - } else { - buildArgs.push('build'); + if (!(await buildx.isAvailable())) { + core.setFailed(`Buildx is required. See https://github.com/docker/setup-buildx-action to set up buildx.`); + return; } - // Global options + let buildArgs: Array = ['buildx', 'build']; + + if (inputs.builder) { + core.info(`📌 Using builder instance ${inputs.builder}`); + await buildx.use(inputs.builder); + } if (inputs.file) { buildArgs.push('--file', inputs.file); } @@ -52,54 +45,28 @@ async function run(): Promise { if (inputs.noCache) { buildArgs.push('--no-cache'); } - - // Buildx options - if (buildxEnabled) { - if (inputs.builder) { - core.info(`📌 Using builder instance ${inputs.builder}`); - await buildx.use(inputs.builder); - } - if (inputs.platforms) { - buildArgs.push('--platform', inputs.platforms); - } - if (inputs.load) { - buildArgs.push('--load'); - } - if (inputs.push) { - buildArgs.push('--push'); - } - await asyncForEach(inputs.outputs, async output => { - buildArgs.push('--output', output); - }); - await asyncForEach(inputs.cacheFrom, async cacheFrom => { - buildArgs.push('--cache-from', cacheFrom); - }); - await asyncForEach(inputs.cacheTo, async cacheTo => { - buildArgs.push('--cache-from', cacheTo); - }); + if (inputs.platforms) { + buildArgs.push('--platform', inputs.platforms); } - + if (inputs.load) { + buildArgs.push('--load'); + } + if (inputs.push) { + buildArgs.push('--push'); + } + await asyncForEach(inputs.outputs, async output => { + buildArgs.push('--output', output); + }); + await asyncForEach(inputs.cacheFrom, async cacheFrom => { + buildArgs.push('--cache-from', cacheFrom); + }); + await asyncForEach(inputs.cacheTo, async cacheTo => { + buildArgs.push('--cache-from', cacheTo); + }); buildArgs.push(inputs.context); core.info(`🏃 Starting build...`); await exec.exec('docker', buildArgs); - - if (!buildxEnabled && inputs.push) { - let pushRepos: Array = []; - await asyncForEach(inputs.tags, async tag => { - const img: Image | undefined = await parseImage(tag); - if (!img) { - core.warning(`Cannot parse image reference ${tag}`); - return; - } - const repo: string = `${img.registry}${img.namespace}${img.repository}`; - if (!pushRepos.includes(repo)) { - pushRepos.push(repo); - core.info(`⬆️ Pushing ${repo}...`); - await exec.exec('docker', ['push', repo]); - } - }); - } } catch (error) { core.setFailed(error.message); }