From 8b6c3b755b77b927e3552a3de9f4289e5a66b8c1 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Sun, 22 Oct 2023 15:31:10 -0500 Subject: [PATCH] feat(docker)!: include support for buildx when the dockerPlatform array option is supplied and has a non zero number of items, buildx will be used to build and push images rather than the standard docker builder. BREAKING CHANGE: images build with buildx will not be stored locally Fixes: #44 Fixes: #33 --- lib/build-config.js | 2 + lib/docker/image.js | 98 ++++++++++++++++++++++++++++++------- lib/prepare.js | 2 + lib/publish.js | 16 +++--- lib/verify.js | 3 ++ test/integration/release.js | 8 +-- 6 files changed, 98 insertions(+), 31 deletions(-) diff --git a/lib/build-config.js b/lib/build-config.js index 8d9422e..0a854ef 100644 --- a/lib/build-config.js +++ b/lib/build-config.js @@ -21,6 +21,7 @@ async function buildConfig(build_id, config, context) { , dockerRegistry: registry = null , dockerLogin: login = true , dockerImage: image + , dockerPlatform: platform = null , dockerPublish: publish = true , dockerContext = '.' , dockerVerifyCmd: verifycmd = null @@ -75,5 +76,6 @@ async function buildConfig(build_id, config, context) { , network: network , quiet: typeCast(quiet) === true , clean: typeCast(clean) === true + , platform: array.toArray(platform) } } diff --git a/lib/docker/image.js b/lib/docker/image.js index 0e35298..5412241 100644 --- a/lib/docker/image.js +++ b/lib/docker/image.js @@ -20,6 +20,9 @@ class Image { , context = '.' , network = 'default' , quiet = true + , dry_run = false + , tags = [] + , platform = [] } = opts || {} if (!name || typeof name !== 'string') { @@ -36,12 +39,20 @@ class Image { , dockerfile: dockerfile , flags: new Map() , name: name + , tags: tags , network: network , project: project , registry: registry + , dry_run: dry_run + , platform: array.toArray(platform) } if (quiet) this.flag('quiet', null) + + + for (const tag of this.tags) { + this.flag('tag', tag) + } } get id() { @@ -102,6 +113,56 @@ class Image { return output } + get tags() { + const output = [] + if (this.opts.dry_run) return output + for (const tag of this.opts.tags) { + output.push( + `${this.repo}:${tag}` + ) + } + + return output + } + + get build_cmd() { + return [ + 'build' + , `--network=${this.network}` + , '--tag' + , this.name + , ...this.flags + , '-f' + , this.dockerfile + , this.context + ].filter(Boolean) + } + + get buildx_cmd() { + if (!this.opts.platform?.length) return + this.opts.flags.delete('provenance') // incompatible with load/push + this.opts.flags.delete('output') // alias of load/push + this.opts.flags.delete('load') + this.opts.flags.set('platform', [this.opts.platform.join(',')]) + + this.flag('pull', null) + if (this.opts.dry_run) { + this.opts.flags.delete('push') + } else { + this.flag('push', null) + } + + const cmd = this.build_cmd + cmd.unshift('buildx') + + this.opts.flags.delete('platform') + this.opts.flags.delete('push') + this.opts.flags.delete('pull') + // remove the build id tag + cmd.splice(cmd.indexOf(this.name) - 1, 2) + return cmd + } + arg(key, val = null) { if (val === true || val == null) { // eslint-disable-line no-eq-null this.flag('build-arg', key) @@ -130,19 +191,6 @@ class Image { return this } - get build_cmd() { - return [ - 'build' - , `--network=${this.network}` - , '--tag' - , this.name - , ...this.flags - , '-f' - , this.dockerfile - , this.context - ].filter(Boolean) - } - async run(cmd) { const stream = execa('docker', [ 'run' @@ -159,7 +207,11 @@ class Image { } async build() { - const stream = execa('docker', this.build_cmd) + const cmd = this.opts.platform.length + ? this.buildx_cmd + : this.build_cmd + + const stream = execa('docker', cmd) stream.stdout.pipe(process.stdout) stream.stderr.pipe(process.stderr) const {stdout, stderr} = await stream @@ -174,6 +226,8 @@ class Image { return this.sha } } + this.sha = this.build_id + return this.sha } async tag(tag, push = true) { @@ -187,14 +241,20 @@ class Image { } async push() { - await execa('docker', ['push', this.repo]) + // push is a part of the buildx build operation + // At this point the tags have already been pushed. + // re-pushing manually is considered destructive + if (this.opts.platform.length) return + + for (const tag of this.opts.tags) { + await this.tag(tag) + } } async clean() { - const images = execa('docker', ['images', this.repo, '-q']) - const rm = execa('xargs', ['docker', 'rmi', '-f']) - images.stdout.pipe(rm.stdin) - return rm + const {stdout: images} = await execa('docker', ['images', this.repo, '-q']) + if (!images) return + await execa('docker', ['rmi', '-f', ...images.split(os.EOL)]) } } diff --git a/lib/prepare.js b/lib/prepare.js index 9a2b6dd..4610881 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -19,6 +19,8 @@ async function dockerPrepare(opts, context) { , context: opts.context , network: opts.network , quiet: opts.quiet + , platform: opts.platform + , dry_run: !!opts.dryRun }) const vars = buildTemplateVars(opts, context) diff --git a/lib/publish.js b/lib/publish.js index 8373cd7..6332eab 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -8,6 +8,11 @@ module.exports = publish async function publish(opts, context) { const {cwd, logger} = context + const vars = buildTemplateVars(opts, context) + const tags = opts.tags.map((template) => { + return string.template(template)(vars) + }).filter(Boolean) + const image = new docker.Image({ registry: opts.registry , project: opts.project @@ -15,18 +20,13 @@ async function publish(opts, context) { , dockerfile: opts.dockerfile , build_id: opts.build , cwd: cwd + , tags: tags , context: opts.context , quiet: opts.quiet }) - const vars = buildTemplateVars(opts, context) - const tags = opts.tags.map((template) => { - return string.template(template)(vars) - }).filter(Boolean) logger.info('tagging docker image', image.id) - for (const tag of tags) { - logger.info(`pushing image: ${image.repo} tag: ${tag}`) - await image.tag(tag, opts.publish) - } + if (!opts.publish) return + await image.push() } diff --git a/lib/verify.js b/lib/verify.js index fdc0b83..b4bd65d 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -33,6 +33,9 @@ async function verify(opts, context) { , cwd: cwd , context: opts.context , network: opts.network + , platform: opts.platform + , quiet: opts.quiet + , dry_run: !!opts.dryRun }) debug('docker options', opts) diff --git a/test/integration/release.js b/test/integration/release.js index 28c179d..922aa59 100644 --- a/test/integration/release.js +++ b/test/integration/release.js @@ -35,10 +35,10 @@ test('docker release', async (t) => { ] } , devDependencies: { - 'semantic-release': '*' - , '@semantic-release/commit-analyzer': '*' - , '@semantic-release/release-notes-generator': '*' - , '@semantic-release/npm': '*' + 'semantic-release': '^19.0.0' + , '@semantic-release/commit-analyzer': '^9' + , '@semantic-release/release-notes-generator': '^10' + , '@semantic-release/npm': '^9' , '@codedependant/semantic-release-docker': 'file:../../../' } })