Skip to content

Commit

Permalink
feat(config)!: unnest config values from docker property
Browse files Browse the repository at this point in the history
BREAKING CHANGES:

Flatten the docker config option for semantic relase into the root
config. Semantic release doesn't do a very good job of merging options
that are coming from a sharable configuration. This makes it easier to
utilize overrides when using a sharable config. Options are camel cased
using the docker root word prefix
`docker.args` -> `dockerArgs`
`docker.login` -> `dockerLogin`
  • Loading branch information
esatterwhite committed Apr 8, 2021
1 parent 98b1a88 commit 2087683
Show file tree
Hide file tree
Showing 17 changed files with 199 additions and 130 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v2

- name: Install
run: npx pnpm install --frozen-lockfile
run: npm install

- name: test
run: npm test
Expand All @@ -33,7 +33,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npx pnpm install --frozen-lockfile
- run: npm install

- name: Publish
run: npm run release
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
hoist=true
lock-file=false
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 0000-BASE
FROM docker:latest
ARG SRC_DIR='.'
RUN apk update && apk upgrade && apk add nodejs npm
RUN apk update && apk upgrade && apk add nodejs npm git
WORKDIR /opt/app
COPY ${SRC_DIR}/package.json /opt/app/
RUN npm install
Expand Down
132 changes: 96 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,55 +44,115 @@ omitted, it is assumed the docker daemon is already authenticated with the targe

| Option | Description | Default
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------
| docker.tags | _Optional_. An array of strings allowing to specify additional tags to apply to the image. | [`latest`, `{major}-latest`, `{version}`] |
| docker.image | _Optional_. The name of the image to release. | Parsed from package.json `name` property
| docker.registry | _Optional_. The hostname and port used by the the registry in format `hostname[:port]`. Omit the port if the registry uses the default port | `null` (dockerhub)
| docker.project | _Optional_. The project or repository name to publish the image to | For scoped packages, the scope will be used, otherwise `null`
| docker.dockerfile | _Optional_. The path, relative to `$PWD` to a Docker file to build the target image with | `Dockerfile`
| docker.context | _Optional_. A path, relative to `$PWD` to use as the build context A | `.`
| docker.login | _Optional_. Set to false it by pass docker login if the docker daemon is already authorized | `true`
| `dockerTags` | _Optional_. An array of strings allowing to specify additional tags to apply to the image. Supports templating | [`latest`, `{major}-latest`, `{version}`] |
| `dockerImage` | _Optional_. The name of the image to release. | 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 | `null` (dockerhub) |
| `dockerProject` | _Optional_. The project or repository name to publish the image to | For scoped packages, the scope will be used, otherwise `null` |
| `dockerDockerfile` | _Optional_. The path, relative to `$PWD` to a Docker file to build the target image with | `Dockerfile` |
| `dockerContext` | _Optional_. A path, relative to `$PWD` to use as the build context A | `.` |
| `dockerLogin` | _Optional_. Set to false it by pass docker login if the docker daemon is already authorized | `true` |
| `dockerArgs` | _Optional_. Include additional values for docker's `build-arg`. Supports templating | |
| `dockerPublish` | _Optional_. Automatically push image tags during the publish phase. | `true` |

### Build Arguments

By default several build arguments will be included when the docker images is being built.
Build arguments can be templated in the same fashion as docker tags. If the value for a
build argument is explictly `true`, the value will be omitted and the value from
a matching environment variable will be utilized instead. This can be usefule when trying to include
secrets and other sensitive information

| Argument Name | Description | Default |
|---------------------|--------------------------------------------------------------------------------------------|------------------------------|
| `SRC_DIRECTORY` | The of the directory the build was triggered from | The directory name of CWD |
| `TARGET_PATH` | Path relative to the execution root. Usefule for Sharing a Single Docker file in monorepos | |
| `NPM_PACKAGE_NAME` | The `name` property extracted from `package.json` - if present | |
| `NPM_PACKAGE_SCOPE` | The parsed scope from the `name` property from `package.json` - sans `@` | |
| `CONFIG_NAME` | The configured name of the docker image. | The parsed package name |
| `CONFIG_PROJECT` | The configured docker repo project name | The package scope if present |
| `GIT_SHA` | The commit SHA of the current release | |
| `GIT_TAG` | The git tag of the current release | |

### Template Variables

String template will be passed these

| Variable name | Description | Type |
|----------------|--------------------------------------------------------------------|----------|
| `git_sha` | The commit SHA of the current release | `String` |
| `git_tag` | The git tag of the current release | `String` |
| `release_type` | The severity type of the current build (`major`, `minor`, `patch`) | `String` |
| `relase_notes` | The release notes blob associated with the release | `String` |
| `next` | Semver object representing the next release | `Object` |
| `previous` | Semver object representing the previous release | `Object` |
| `major` | The major version of the next release | `Number` |
| `minor` | The minor version of the next release | `Number` |
| `patch` | The patch version of the next release | `Number` |
| `env` | Environment variables that were set at build time | `Object` |
| `pkg` | Values parsed from `package.json` | `Object` |
| `build` | A Random build hash representing the current execution context | `String` |
| `now` | Current timestamp is ISO 8601 format | `String` |

## Usage

full configuration:

```json
{
"release": {
"plugins": [
["@codedependant/semantic-release-docker", {
"docker": {
"tags": ["latest", "{version}", "{major}-latest", "{major}.{minor}"],
"image": "my-image",
"dockerfile": "Dockerfile",
"registry": "quay.io",
"project": "codedependant"
}
}]
]
}
**full configuration**:

```javascript
// release.config.js

module.exports = {
branches: ['main']
plugins: [
['@codedependant/semantic-release-docker', {
dockerTags: ['latest', '{version}', '{major}-latest', '{major}.{minor}'],
dockerImage: 'my-image',
dockerFile: 'Dockerfile',
dockerRegistry: 'quay.io',
dockerProject: 'codedependant',
dockerArgs: {
API_TOKEN: true
, RELEASE_DATE: new Date().toISOString()
, RELEASE_VERSION: '{next.version}'
}
}]
]
}
```

results in `quay.io/codedependant/my-image` with tags `latest`, `1.0.0`, `1-latest` and the `1.0` determined by `semantic-release`.

Alternatively, using global options w/ root configuration
```json
```json5
// package.json
{
"name": "@codedependant/test-project"
"version": "1.0.0"
"release": {
"extends": "@internal/release-config-example",
"docker": {
"tags": ["latest", "{version}", "{major}-latest", "{major}.{minor}"],
"image": "my-image",
"dockerfile": "Dockerfile",
"registry": "quay.io",
"project": "codedependant"
"extends": "@internal/release-config-docker",
"dockerTags": ["latest", "{version}", "{major}-latest", "{major}.{minor}"],
"dockerImage": "my-image",
"dockerFile": "Dockerfile",
"dockerRegistry": "quay.io",
"dockerArgs": {
"GITHUB_TOKEN": true
, "SOME_VALUE": '{git_sha}'
}
}
}
```

minimum configuration:
This would generate the following for a `1.2.0` build

```shell
$ docker build -t quay.io/codedependant/my-image --build-arg GITHUB_TOKEN --build-arg SOME_VALUE=6eada70 -f Dockerfile .
$ docker tag <SHA1> latest
$ docker tag <SHA1> 1.2.0
$ docker tag <SHA1> 1.2
$ docker tag <SHA1> 1-latest
$ docker push quay.io/codedependant/my-image
```

**minimum configuration**:

```json
{
Expand All @@ -104,14 +164,14 @@ minimum configuration:
}
```

* A package name `@codedependant/test-project` results in `codedependant/test-project`
* A package name `test-project` results in `test-project`
* A package name `@codedependant/test-project` results in docker project name`codedependant` and image name `test-project`
* A package name `test-project` results in a docker image name `test-project`

the default docker image tags for the 1.0.0 release would be `1.0.0`, `1-latest`, `latest`

## Development

#### Docker Registry
### Docker Registry

To be able to push to the local registry with auth credentials, ssl certificates are required.
This project uses self signed certs. To regenerate the certs run the following:
Expand Down
1 change: 0 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ module.exports = {
, buildConfig
}


async function prepare(config, context) {
return dockerPrepare(await buildConfig(build_id, config, context), context)
}
Expand Down
20 changes: 11 additions & 9 deletions lib/build-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ module.exports = buildConfig

async function buildConfig(build_id, config, context) {
const {
dockerfile = 'Dockerfile'
, nocache = false
, tags = ['latest', '{major}-latest', '{version}']
, args = {}
, registry = null
, login = true
, image
} = (object.get(config, 'docker') || {})
dockerFile: dockerfile = 'Dockerfile'
, dockerNoCache: nocache = false
, dockerTags: tags = ['latest', '{major}-latest', '{version}']
, dockerArgs: args = {}
, dockerRegistry: registry = null
, dockerLogin: login = true
, dockerImage: image
, dockerPublish: publish = true
} = config

let name = null
let scope = null
Expand All @@ -29,7 +30,7 @@ async function buildConfig(build_id, config, context) {
scope = parsed.scope
} catch (_) {}

const project = object.has(config.docker, 'project') ? config.docker.project : scope
const project = object.has(config, 'dockerProject') ? config.dockerProject : scope
const root = object.get(context, 'options.root')
const target = path.relative(root || context.cwd, context.cwd) || PWD
const {nextRelease = {}} = context
Expand All @@ -40,6 +41,7 @@ async function buildConfig(build_id, config, context) {
, nocache
, pkg
, project
, publish
, args: {
SRC_DIRECTORY: path.basename(context.cwd)
, TARGET_PATH: target
Expand Down
4 changes: 3 additions & 1 deletion lib/build-template-vars.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict'

const semver = require('semver')
const now = new Date().toISOString()

module.exports = buildTemplateVars

function buildTemplateVars(context, opts) {
function buildTemplateVars(opts, context) {
const {nextRelease = {}, lastRelease = {}} = context

const versions = {
Expand All @@ -22,5 +23,6 @@ function buildTemplateVars(context, opts) {
, git_sha: nextRelease.gitHead
, release_type: nextRelease.type
, release_notes: nextRelease.notes
, now: now
}
}
13 changes: 5 additions & 8 deletions lib/prepare.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use strict'

const path = require('path')
const debug = require('debug')('semantic-release:semantic-release-docker:prepare')
const docker = require('./docker/index.js')
const buildTemplateVars = require('./build-template-vars.js')
const string = require('./lang/string/index.js')

module.exports = dockerPrepare

Expand All @@ -22,16 +22,13 @@ async function dockerPrepare(opts, context) {
...opts.args
}

if (args) {
const vars = buildTemplateVars(context, opts)
debug('template variables', vars)
for (const [key, value] of Object.entries(args)) {
image.arg(key, value)
}
const vars = buildTemplateVars(opts, context)
for (const [key, value] of Object.entries(args)) {
image.arg(key, string.template(value)(vars))
}

context.logger.info('building image', image.name)
debug('build command', image.build_cmd)
context.logger.debug('build command: docker %s', image.build_cmd.join(' '))
await image.build(path.join(cwd, opts.context))
return image
}
4 changes: 2 additions & 2 deletions lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ async function publish(opts, context) {
, cwd: cwd
})

const vars = buildTemplateVars(context, opts)
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)
await image.tag(tag, opts.publish)
}

await image.clean()
Expand Down
8 changes: 5 additions & 3 deletions test/common/git/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ module.exports = init

async function init(dir, branch = 'main') {
const cwd = dir || await fs.mkdtemp(path.join(os.tmpdir(), path.sep))
await execa('git', ['init'], {cwd: cwd})
await execa('git', ['init', cwd])
await execa('git', ['checkout', '-b', branch], {cwd: cwd})
await execa('git', ['config', '--add', 'commit.gpgsign', false])
await execa('git', ['config', '--add', 'pull.default', 'current'])
await execa('git', ['config', '--add', 'push.default', 'current'])
await execa('git', ['config', '--add', 'pull.default', 'current'], {cwd})
await execa('git', ['config', '--add', 'push.default', 'current'], {cwd})
await execa('git', ['config', '--add', 'user.name', 'secretsquirrel'], {cwd})
await execa('git', ['config', '--add', 'user.email', 'secret@mail.com'], {cwd})
return cwd
}
31 changes: 25 additions & 6 deletions test/integration/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const verify = require('../../lib/verify.js')
const prepare = require('../../lib/prepare.js')
const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000'
const fixturedir = path.join(__dirname, '..', 'fixture')
const DATE_REGEX = new RegExp(
'^[\\d]{4}-[\\d]{2}-[\\d]{2}T[\\d]{2}:[\\d]{2}:[\\d]{2}'
+ '(\.[\\d]{1,6})?(Z|[\\+\\-][\\d]{2}:[\\d]{2})$' // eslint-disable-line no-useless-escape
)

test('steps::prepare', async (t) => {
t.test('build image created', async (tt) => {
Expand All @@ -21,20 +25,30 @@ test('steps::prepare', async (t) => {
, DOCKER_REGISTRY_PASSWORD: 'secretsquirrel'
}
, cwd: fixturedir
, nextRelease: {
version: '2.1.2'
, gitTag: 'v2.1.2'
, gitHead: 'abacadaba'
}
, logger: {
success: sinon.stub()
, info: sinon.stub()
, debug: sinon.stub()
}
}

const config = await buildConfig(build_id, {
docker: {
registry: DOCKER_REGISTRY_HOST
, project: 'docker-prepare'
, image: 'fake'
, args: {MY_VARIABLE: '1'}
, dockerfile: 'docker/Dockerfile.prepare'
dockerRegistry: DOCKER_REGISTRY_HOST
, dockerProject: 'docker-prepare'
, dockerImage: 'fake'
, dockerArgs: {
MY_VARIABLE: '1'
, TAG_TEMPLATE: '{git_tag}'
, MAJOR_TEMPLATE: '{major}'
, GIT_REF: '{git_sha}'
, BUILD_DATE: '{now}'
}
, dockerFile: 'docker/Dockerfile.prepare'
}, context)

const auth = await verify(config, context)
Expand All @@ -46,6 +60,11 @@ test('steps::prepare', async (t) => {
image.clean()
})

tt.equal(image.opts.args.get('TAG_TEMPLATE'), 'v2.1.2', 'TAG_TEMPLATE value')
tt.equal(image.opts.args.get('MAJOR_TEMPLATE'), '2', 'MAJOR_TEMPLATE value')
tt.equal(image.opts.args.get('GIT_REF'), 'abacadaba', 'GIT_REF value')
tt.match(image.opts.args.get('BUILD_DATE'), DATE_REGEX, 'BUILD_DATE value')

const {stdout} = await execa('docker', [
'images', image.name
, '-q', '--format={{ .Tag }}'
Expand Down

0 comments on commit 2087683

Please sign in to comment.