@codedependant/semantic-release-docker 4.5.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Semantic Release Docker
2
2
 
3
+ # [5.0.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.5.1...v5.0.0) (2024-03-22)
4
+
5
+
6
+ ### Chores
7
+
8
+ * **compose**: update test registry certs [fa8d771](https://github.com/esatterwhite/semantic-release-docker/commit/fa8d771f8f3381e3b2327a29ab10bb27b1257d1e) - Eric Satterwhite
9
+
10
+
11
+ ### Features
12
+
13
+ * **docker**: include support for buildx [ccf9954](https://github.com/esatterwhite/semantic-release-docker/commit/ccf9954fdbad0c25c4ce7b8afcf7730b9652fe76) - Eric Satterwhite, closes: [#44](https://github.com/esatterwhite/semantic-release-docker/issues/44) [#39](https://github.com/esatterwhite/semantic-release-docker/issues/39)
14
+
15
+
16
+ ### Miscellaneous
17
+
18
+ * **README.md**: fix typo in README.md [700e2f0](https://github.com/esatterwhite/semantic-release-docker/commit/700e2f0543a1fbd7d59b6ead84870bc8c5cfc3b3) - Eric Satterwhite
19
+
20
+
21
+ ### **BREAKING CHANGES**
22
+
23
+ * **docker:** images built with buildx will not be stored locally
24
+ * **docker:** dockerVerifyCmd will only take effect during dry runs (--dry-run)
25
+
26
+ ## [4.5.1](https://github.com/esatterwhite/semantic-release-docker/compare/v4.5.0...v4.5.1) (2024-02-04)
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * **image**: account for stderr when looking for an image sha [ba9eec2](https://github.com/esatterwhite/semantic-release-docker/commit/ba9eec2888fb4478a3a90f741de41a155b6c525e) - Eric Satterwhite, closes: [#46](https://github.com/esatterwhite/semantic-release-docker/issues/46)
32
+
3
33
  # [4.5.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.4.0...v4.5.0) (2023-10-20)
4
34
 
5
35
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ Run `npm i --save-dev @codedependant/semantic-release-docker` to install this se
30
30
 
31
31
  ### Docker registry authentication
32
32
 
33
- The `docker registry` authentication set via environment variables. It is not required, and if
33
+ Authentication to a `docker registry` is set via environment variables. It is not required, and if
34
34
  omitted, it is assumed the docker daemon is already authenticated with the target registry.
35
35
 
36
36
  ### Environment variables
@@ -48,6 +48,7 @@ omitted, it is assumed the docker daemon is already authenticated with the targe
48
48
  | `dockerImage` | _Optional_. The name of the image to release. | [String][] | Parsed from package.json `name` property |
49
49
  | `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) |
50
50
  | `dockerProject` | _Optional_. The project or repository name to publish the image to | [String][] | For scoped packages, the scope will be used, otherwise `null` |
51
+ | `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) |
51
52
  | `dockerFile` | _Optional_. The path, relative to `$PWD` to a Docker file to build the target image with | [String][] | `Dockerfile` |
52
53
  | `dockerContext` | _Optional_. A path, relative to `$PWD` to use as the build context A | [String][] | `.` |
53
54
  | `dockerLogin` | _Optional_. Set to false it by pass docker login if the docker daemon is already authorized | [String][] | `true` |
@@ -59,6 +60,19 @@ omitted, it is assumed the docker daemon is already authenticated with the targe
59
60
  | `dockerBuildFlags` | _Optional_. An object containing additional flags to the `docker build` command. Values can be strings or an array of strings | [Object][] | `{}` |
60
61
  | `dockerBuildCacheFrom` | _Optional_. A list of external cache sources. See [--cache-from][] | [String][] | [Array][]<[String][]> | |
61
62
 
63
+
64
+ ### BuildX Support
65
+
66
+ Version 5.X includes initial and experimental support for multi-platform images via the [buildx][] plugin.
67
+ This plugin assumes that the docker daemon and buildx have already been setup correctly.
68
+ Platform specific builder must be setup and selected for this plugin to utilize [buildx][]
69
+
70
+ > [!WARNING]
71
+ >
72
+ > When using buildx via the dockerPlatform option, images are not kept locally
73
+ > and normal docker commands targeting those images will not work.
74
+ > The `dockerVerifyCmd` behavior will only trigger a build and is unable to execute local command
75
+
62
76
  ### Build Arguments
63
77
 
64
78
  By default several build arguments will be included when the docker images is being built.
@@ -87,7 +101,7 @@ String template will be passed these
87
101
  | `git_sha` | The commit SHA of the current release | [String][] |
88
102
  | `git_tag` | The git tag of the current release | [String][] |
89
103
  | `release_type` | The severity type of the current build (`major`, `minor`, `patch`) | [String][] |
90
- | `relase_notes` | The release notes blob associated with the release | [String][] |
104
+ | `release_notes`| The release notes blob associated with the release | [String][] |
91
105
  | `next` | Semver object representing the next release | [Object][] |
92
106
  | `previous` | Semver object representing the previous release | [Object][] |
93
107
  | `major` | The major version of the next release | [Number][] |
@@ -180,6 +194,7 @@ module.exports = {
180
194
  dockerFile: 'Dockerfile',
181
195
  dockerRegistry: 'quay.io',
182
196
  dockerProject: 'codedependant',
197
+ dockerPlatform: ['linux/amd64', 'linux/arm64']
183
198
  dockerBuildFlags: {
184
199
  pull: null
185
200
  , target: 'release'
@@ -209,7 +224,7 @@ Alternatively, using global options w/ root configuration
209
224
  "dockerFile": "Dockerfile",
210
225
  "dockerRegistry": "quay.io",
211
226
  "dockerArgs": {
212
- "GITHUB_TOKEN": true
227
+ "GITHUB_TOKEN": null
213
228
  , "SOME_VALUE": '{{git_sha}}'
214
229
  }
215
230
  }
@@ -283,3 +298,4 @@ $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -o
283
298
  [Object]: https://mdn.io/object
284
299
  [Number]: https://mdn.io/number
285
300
  [--cache-from]: https://docs.docker.com/engine/reference/commandline/build/#cache-from
301
+ [buildx]: https://docs.docker.com/reference/cli/docker/buildx/build
@@ -21,6 +21,7 @@ async function buildConfig(build_id, config, context) {
21
21
  , dockerRegistry: registry = null
22
22
  , dockerLogin: login = true
23
23
  , dockerImage: image
24
+ , dockerPlatform: platform = null
24
25
  , dockerPublish: publish = true
25
26
  , dockerContext = '.'
26
27
  , dockerVerifyCmd: verifycmd = null
@@ -75,5 +76,6 @@ async function buildConfig(build_id, config, context) {
75
76
  , network: network
76
77
  , quiet: typeCast(quiet) === true
77
78
  , clean: typeCast(clean) === true
79
+ , platform: array.toArray(platform)
78
80
  }
79
81
  }
@@ -1,9 +1,22 @@
1
1
  'use strict'
2
2
 
3
+ const os = require('os')
3
4
  const path = require('path')
4
5
  const crypto = require('crypto')
5
6
  const execa = require('execa')
7
+ const buildTemplateVars = require('../build-template-vars.js')
6
8
  const array = require('../lang/array/index.js')
9
+ const string = require('../lang/string/index.js')
10
+ const SHA_REGEX = /(?:writing image\s)?[^@](?:sha\d{3}):(?<sha>\w+)/i
11
+
12
+ function render(item, vars) {
13
+ if (Array.isArray(item)) {
14
+ return item.map((element) => {
15
+ return string.template(element)(vars)
16
+ })
17
+ }
18
+ return string.template(item)(vars)
19
+ }
7
20
 
8
21
  class Image {
9
22
  constructor(opts) {
@@ -17,7 +30,11 @@ class Image {
17
30
  , cwd = process.cwd()
18
31
  , context = '.'
19
32
  , network = 'default'
33
+ , publish = true
20
34
  , quiet = true
35
+ , dry_run = false
36
+ , tags = []
37
+ , platform = []
21
38
  } = opts || {}
22
39
 
23
40
  if (!name || typeof name !== 'string') {
@@ -34,12 +51,53 @@ class Image {
34
51
  , dockerfile: dockerfile
35
52
  , flags: new Map()
36
53
  , name: name
54
+ , tags: tags
37
55
  , network: network
38
56
  , project: project
39
57
  , registry: registry
58
+ , dry_run: dry_run
59
+ , publish: publish
60
+ , platform: array.toArray(platform)
40
61
  }
41
62
 
42
63
  if (quiet) this.flag('quiet', null)
64
+
65
+
66
+ for (const tag of this.tags) {
67
+ this.flag('tag', tag)
68
+ }
69
+ }
70
+
71
+ static from(opts, context) {
72
+ const vars = buildTemplateVars(opts, context)
73
+ const tags = opts.tags.map((template) => {
74
+ return string.template(template)(vars)
75
+ }).filter(Boolean)
76
+
77
+ const image = new(this)({
78
+ registry: opts.registry
79
+ , project: opts.project
80
+ , name: opts.name
81
+ , dockerfile: opts.dockerfile
82
+ , build_id: opts.build
83
+ , cwd: context.cwd
84
+ , tags: tags
85
+ , context: opts.context
86
+ , network: opts.network
87
+ , quiet: opts.quiet
88
+ , publish: opts.publish
89
+ , platform: opts.platform
90
+ , dry_run: !!opts.dryRun
91
+ })
92
+ for (const [key, value] of Object.entries(opts.args)) {
93
+ image.arg(key, string.template(value)(vars))
94
+ }
95
+
96
+ for (const [key, value] of Object.entries(opts.build_flags)) {
97
+ image.flag(key, render(value, vars))
98
+ }
99
+
100
+ return image
43
101
  }
44
102
 
45
103
  get id() {
@@ -55,6 +113,10 @@ class Image {
55
113
  return parts.join('/')
56
114
  }
57
115
 
116
+ get is_buildx() {
117
+ return !!this.opts.platform?.length
118
+ }
119
+
58
120
  get name() {
59
121
  return `${this.repo}:${this.opts.build_id}`
60
122
  }
@@ -100,6 +162,61 @@ class Image {
100
162
  return output
101
163
  }
102
164
 
165
+ get tags() {
166
+ const output = []
167
+ if (this.opts.dry_run) return output
168
+ for (const tag of this.opts.tags) {
169
+ output.push(
170
+ `${this.repo}:${tag}`
171
+ )
172
+ }
173
+
174
+ return output
175
+ }
176
+
177
+ get build_cmd() {
178
+ return this.opts.platform.length
179
+ ? this.buildx_cmd
180
+ : this.docker_cmd
181
+ }
182
+ get docker_cmd() {
183
+ return [
184
+ 'build'
185
+ , `--network=${this.network}`
186
+ , '--tag'
187
+ , this.name
188
+ , ...this.flags
189
+ , '-f'
190
+ , this.dockerfile
191
+ , this.context
192
+ ].filter(Boolean)
193
+ }
194
+
195
+ get buildx_cmd() {
196
+ if (!this.is_buildx) return
197
+ this.opts.flags.delete('provenance') // incompatible with load/push
198
+ this.opts.flags.delete('output') // alias of load/push
199
+ this.opts.flags.delete('load')
200
+ this.opts.flags.set('platform', [this.opts.platform.join(',')])
201
+
202
+ this.flag('pull', null)
203
+ if (this.opts.dry_run || !this.opts.publish) {
204
+ this.opts.flags.delete('push')
205
+ } else {
206
+ this.flag('push', null)
207
+ }
208
+
209
+ const cmd = this.docker_cmd
210
+ cmd.unshift('buildx')
211
+
212
+ this.opts.flags.delete('platform')
213
+ this.opts.flags.delete('push')
214
+ this.opts.flags.delete('pull')
215
+ // remove the build id tag
216
+ cmd.splice(cmd.indexOf(this.name) - 1, 2)
217
+ return cmd
218
+ }
219
+
103
220
  arg(key, val = null) {
104
221
  if (val === true || val == null) { // eslint-disable-line no-eq-null
105
222
  this.flag('build-arg', key)
@@ -128,20 +245,8 @@ class Image {
128
245
  return this
129
246
  }
130
247
 
131
- get build_cmd() {
132
- return [
133
- 'build'
134
- , `--network=${this.network}`
135
- , '--tag'
136
- , this.name
137
- , ...this.flags
138
- , '-f'
139
- , this.dockerfile
140
- , this.context
141
- ].filter(Boolean)
142
- }
143
-
144
248
  async run(cmd) {
249
+ if (this.is_buildx) return
145
250
  const stream = execa('docker', [
146
251
  'run'
147
252
  , '--rm'
@@ -160,9 +265,19 @@ class Image {
160
265
  const stream = execa('docker', this.build_cmd)
161
266
  stream.stdout.pipe(process.stdout)
162
267
  stream.stderr.pipe(process.stderr)
163
- const {stdout} = await stream
164
- const [_, sha] = stdout.split(':')
165
- this.sha = sha.substring(0, 12)
268
+ const {stdout, stderr} = await stream
269
+ const lines = (stdout || stderr).split(os.EOL)
270
+ const len = lines.length - 1
271
+
272
+ for (let x = len; x >= 0; x--) {
273
+ const line = lines[x]
274
+ const match = SHA_REGEX.exec(line)
275
+ if (match) {
276
+ this.sha = match.groups.sha.substring(0, 12)
277
+ return this.sha
278
+ }
279
+ }
280
+ this.sha = this.build_id
166
281
  return this.sha
167
282
  }
168
283
 
@@ -177,14 +292,21 @@ class Image {
177
292
  }
178
293
 
179
294
  async push() {
180
- await execa('docker', ['push', this.repo])
295
+ // push is a part of the buildx build operation
296
+ // At this point the tags have already been pushed.
297
+ // re-pushing manually is considered destructive
298
+ if (this.is_buildx) return
299
+ if (!this.opts.publish) return
300
+
301
+ for (const tag of this.opts.tags) {
302
+ await this.tag(tag)
303
+ }
181
304
  }
182
305
 
183
306
  async clean() {
184
- const images = execa('docker', ['images', this.repo, '-q'])
185
- const rm = execa('xargs', ['docker', 'rmi', '-f'])
186
- images.stdout.pipe(rm.stdin)
187
- return rm
307
+ const {stdout: images} = await execa('docker', ['images', this.repo, '-q'])
308
+ if (!images) return
309
+ await execa('docker', ['rmi', '-f', ...images.split(os.EOL)])
188
310
  }
189
311
  }
190
312
 
@@ -5,17 +5,8 @@ const docker = require('./docker/index.js')
5
5
  module.exports = postPublish
6
6
 
7
7
  async function postPublish(opts, context) {
8
- const {cwd, logger} = context
9
- const image = new docker.Image({
10
- registry: opts.registry
11
- , project: opts.project
12
- , name: opts.name
13
- , dockerfile: opts.dockerfile
14
- , build_id: opts.build
15
- , cwd: cwd
16
- , context: opts.context
17
- })
18
-
8
+ const {logger} = context
9
+ const image = docker.Image.from(opts, context)
19
10
  if (!opts.clean) return
20
11
 
21
12
  logger.info(`removing images for ${image.repo}`)
package/lib/prepare.js CHANGED
@@ -1,47 +1,15 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
4
3
  const docker = require('./docker/index.js')
5
- const buildTemplateVars = require('./build-template-vars.js')
6
- const string = require('./lang/string/index.js')
7
4
 
8
5
  module.exports = dockerPrepare
9
6
 
10
7
  async function dockerPrepare(opts, context) {
11
- const {cwd} = context
12
- const image = new docker.Image({
13
- registry: opts.registry
14
- , project: opts.project
15
- , name: opts.name
16
- , dockerfile: opts.dockerfile
17
- , build_id: opts.build
18
- , cwd: cwd
19
- , context: opts.context
20
- , network: opts.network
21
- , quiet: opts.quiet
22
- })
23
-
24
- const vars = buildTemplateVars(opts, context)
25
-
26
- function render(item, vars) {
27
- if (Array.isArray(item)) {
28
- return item.map((element) => {
29
- return string.template(element)(vars)
30
- })
31
- }
32
- return string.template(item)(vars)
33
- }
34
-
35
- for (const [key, value] of Object.entries(opts.args)) {
36
- image.arg(key, string.template(value)(vars))
37
- }
38
-
39
- for (const [key, value] of Object.entries(opts.build_flags)) {
40
- image.flag(key, render(value, vars))
41
- }
42
-
8
+ const image = docker.Image.from(opts, context)
43
9
  context.logger.info('building image', image.name)
44
- context.logger.debug('build command: docker %s', image.build_cmd.join(' '))
45
- await image.build(path.join(cwd, opts.context))
10
+ context.logger.info('build command: docker %s', image.build_cmd.join(' '))
11
+
12
+ await image.build()
46
13
  return image
47
14
  }
15
+
package/lib/publish.js CHANGED
@@ -1,32 +1,10 @@
1
1
  'use strict'
2
2
 
3
- const buildTemplateVars = require('./build-template-vars.js')
4
3
  const docker = require('./docker/index.js')
5
- const string = require('./lang/string/index.js')
6
4
 
7
5
  module.exports = publish
8
6
 
9
7
  async function publish(opts, context) {
10
- const {cwd, logger} = context
11
- const image = new docker.Image({
12
- registry: opts.registry
13
- , project: opts.project
14
- , name: opts.name
15
- , dockerfile: opts.dockerfile
16
- , build_id: opts.build
17
- , cwd: cwd
18
- , context: opts.context
19
- , quiet: opts.quiet
20
- })
21
-
22
- const vars = buildTemplateVars(opts, context)
23
- const tags = opts.tags.map((template) => {
24
- return string.template(template)(vars)
25
- }).filter(Boolean)
26
-
27
- logger.info('tagging docker image', image.id)
28
- for (const tag of tags) {
29
- logger.info(`pushing image: ${image.repo} tag: ${tag}`)
30
- await image.tag(tag, opts.publish)
31
- }
8
+ const image = docker.Image.from(opts, context)
9
+ await image.push()
32
10
  }
package/lib/verify.js CHANGED
@@ -10,7 +10,7 @@ const docker = require('./docker/index.js')
10
10
  module.exports = verify
11
11
 
12
12
  async function verify(opts, context) {
13
- const {env, cwd} = context
13
+ const {env} = context
14
14
  const PASSWORD = env.DOCKER_REGISTRY_PASSWORD || env.GITHUB_TOKEN
15
15
  const USERNAME = env.DOCKER_REGISTRY_USER
16
16
 
@@ -24,16 +24,7 @@ async function verify(opts, context) {
24
24
  throw error
25
25
  }
26
26
 
27
- const image = new docker.Image({
28
- registry: opts.registry
29
- , project: opts.project
30
- , name: opts.name
31
- , dockerfile: opts.dockerfile
32
- , build_id: opts.build
33
- , cwd: cwd
34
- , context: opts.context
35
- , network: opts.network
36
- })
27
+ const image = docker.Image.from(opts, context)
37
28
 
38
29
  debug('docker options', opts)
39
30
 
@@ -57,11 +48,11 @@ async function verify(opts, context) {
57
48
  await doLogin({...opts, USERNAME, PASSWORD}, context)
58
49
  }
59
50
 
60
- if (!opts.verifycmd) return true
51
+ if (!opts.verifycmd && !opts.dry_run) return true
61
52
 
62
53
  const img = await prepare(opts, context)
63
54
  const output = await img.run(opts.verifycmd)
64
- if (opts.dryRun) await img.clean()
55
+ await img.clean()
65
56
  return output
66
57
  }
67
58
 
@@ -78,7 +69,7 @@ async function doLogin(opts, context) {
78
69
 
79
70
  if (set !== 2) {
80
71
  const error = new SemanticError(
81
- 'Docker authentication failed'
72
+ `Docker authentication failed ${USERNAME} ${PASSWORD}`
82
73
  , 'EAUTH'
83
74
  , 'Both ENV vars DOCKER_REGISTRY_USER and DOCKER_REGISTRY_PASSWORD must be set'
84
75
  )
@@ -100,7 +91,7 @@ async function doLogin(opts, context) {
100
91
  } catch (err) {
101
92
  logger.fatal(err)
102
93
  const error = new SemanticError(
103
- 'Docker authentication failed'
94
+ `Docker authentication failed ${USERNAME} ${PASSWORD}`
104
95
  , 'EAUTH'
105
96
  , `Authentication to ${opts.registry || 'dockerhub'} failed`
106
97
  )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@codedependant/semantic-release-docker",
3
3
  "private": false,
4
- "version": "4.5.0",
4
+ "version": "5.0.0",
5
5
  "description": "docker package",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -21,7 +21,8 @@
21
21
  "lint:fix": "npm run lint -- --fix",
22
22
  "local": "env $(cat env/local.env)",
23
23
  "pretap": "npm run lint",
24
- "release": "semantic-release"
24
+ "release": "semantic-release",
25
+ "release:dry": "semantic-release --no-ci --dry-run --branches=${BRANCH_NAME:-main}"
25
26
  },
26
27
  "keywords": [
27
28
  "semantic-release"