Skip to content

Commit

Permalink
feat(docker)!: include support for buildx
Browse files Browse the repository at this point in the history
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: #39
  • Loading branch information
esatterwhite committed Mar 13, 2024
1 parent 1a86e77 commit 952d334
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 31 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ omitted, it is assumed the docker daemon is already authenticated with the targe
| `dockerImage` | _Optional_. The name of the image to release. | [String][] | Parsed from package.json `name` property |
| `dockerRegistry` | _Optional_. The hostname and port used by the the registry in format `hostname[:port]`. Omit the port if the registry uses the default port | [String][] | `null` (dockerhub) |
| `dockerProject` | _Optional_. The project or repository name to publish the image to | [String][] | For scoped packages, the scope will be used, otherwise `null` |
| `dockerPlatform` | _Optional_. A list of target platofmrs to build for. If specified, [buildx][] Will be used to generate the final images | [Array][]<[String][]> | `null` (default docker build behavior) |
| `dockerFile` | _Optional_. The path, relative to `$PWD` to a Docker file to build the target image with | [String][] | `Dockerfile` |
| `dockerContext` | _Optional_. A path, relative to `$PWD` to use as the build context A | [String][] | `.` |
| `dockerLogin` | _Optional_. Set to false it by pass docker login if the docker daemon is already authorized | [String][] | `true` |
Expand All @@ -59,6 +60,12 @@ omitted, it is assumed the docker daemon is already authenticated with the targe
| `dockerBuildFlags` | _Optional_. An object containing additional flags to the `docker build` command. Values can be strings or an array of strings | [Object][] | `{}` |
| `dockerBuildCacheFrom` | _Optional_. A list of external cache sources. See [--cache-from][] | [String][] | [Array][]<[String][]> | |

> [!WARNING]
>
> When using buildx via the dockerPlatform option, images are not kept locally
> and normal docker commands targeting those images will not work.
> The `dockerVerifyCmd` behavior is also not supported when using `buildx`
### Build Arguments

By default several build arguments will be included when the docker images is being built.
Expand Down Expand Up @@ -180,6 +187,7 @@ module.exports = {
dockerFile: 'Dockerfile',
dockerRegistry: 'quay.io',
dockerProject: 'codedependant',
dockerPlatform: ['linux/amd64', 'linux/arm64']
dockerBuildFlags: {
pull: null
, target: 'release'
Expand Down Expand Up @@ -283,3 +291,4 @@ $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -o
[Object]: https://mdn.io/object
[Number]: https://mdn.io/number
[--cache-from]: https://docs.docker.com/engine/reference/commandline/build/#cache-from
[buildx]: https://docs.docker.com/reference/cli/docker/buildx/build
2 changes: 2 additions & 0 deletions lib/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
98 changes: 79 additions & 19 deletions lib/docker/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Image {
, context = '.'
, network = 'default'
, quiet = true
, dry_run = false
, tags = []
, platform = []
} = opts || {}

if (!name || typeof name !== 'string') {
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -174,6 +226,8 @@ class Image {
return this.sha
}
}
this.sha = this.build_id
return this.sha
}

async tag(tag, push = true) {
Expand All @@ -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)])
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 8 additions & 8 deletions lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ 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
, name: opts.name
, 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()
}
3 changes: 3 additions & 0 deletions lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions test/integration/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:../../../'
}
})
Expand Down
6 changes: 6 additions & 0 deletions test/unit/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ test('build-config', async (t) => {
, nocache: false
, publish: true
, tags: ['latest', '{{major}}-latest', '{{version}}']
, platform: []
, args: {
SRC_DIRECTORY: 'one'
, TARGET_PATH: 'workspace/one'
Expand Down Expand Up @@ -89,6 +90,7 @@ test('build-config', async (t) => {
tt.match(config, {
dockerfile: 'Dockerfile'
, nocache: false
, platform: []
, tags: ['latest', '{{major}}-latest', '{version}']
, args: {
SRC_DIRECTORY: 'scoped'
Expand All @@ -115,6 +117,7 @@ test('build-config', async (t) => {
, dockerImage: 'override'
, dockerFile: 'Dockerfile.test'
, dockerPublish: false
, dockerPlatform: 'linux/amd64'
, dockerBuildQuiet: 'false'
}, {
cwd: path.join(t.testdirName, 'scoped')
Expand All @@ -123,6 +126,7 @@ test('build-config', async (t) => {
dockerfile: 'Dockerfile.test'
, publish: false
, nocache: false
, platform: ['linux/amd64']
, tags: ['latest', '{{major}}-latest', '{{version}}']
, args: {
SRC_DIRECTORY: 'scoped'
Expand Down Expand Up @@ -150,13 +154,15 @@ test('build-config', async (t) => {
, dockerFile: 'Dockerfile.test'
, dockerTags: 'latest,{{major}}-latest , fake, {{version}}'
, dockerAutoClean: false
, dockerPlatform: ['linux/amd64', 'linux/arm64']
, dockerBuildQuiet: 'false'
}, {
cwd: path.join(t.testdirName, 'scoped')
})
tt.match(config, {
dockerfile: 'Dockerfile.test'
, nocache: false
, platform: ['linux/amd64', 'linux/arm64']
, tags: ['latest', '{{major}}-latest', 'fake', '{{version}}']
, args: {
SRC_DIRECTORY: 'scoped'
Expand Down

0 comments on commit 952d334

Please sign in to comment.