diff --git a/README.md b/README.md index ce83958..a1a9612 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,13 @@ The plugin can be configured in the [**semantic-release** configuration file](ht #### Config -| Variable | Description | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `baseImageName` | Name of the previously constructed docker image. Required. | -| `baseImageTag` | Name of the previously constructed docker image tag. Optional. Default `"latest"` | -| `registries` | Array of [Registry](#registry) objects. Required. Example: {"user": "DOCKER_USER", "password": "DOCKER_PASSWORD", "url": "docker.pkg.github.com", "imageName": "docker.pkg.github.com/myuser/myrepo/myapp"} | -| `additionalTags` | Array of additional tags to push. Optional. Example: `["beta", "next"]` | +| Variable | Description | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `baseImageName` | Name of the previously constructed docker image. Required. | +| `baseImageTag` | Name of the previously constructed docker image tag. Optional. Default `"latest"` | +| `releaseImageTag` | Name of the docker image tag for new. Optional. Default `"latest"` or `channel` if use a custom branches or prerelase | +| `registries` | Array of [Registry](#registry) objects. Required. Example: {"user": "DOCKER_USER", "password": "DOCKER_PASSWORD", "url": "docker.pkg.github.com", "imageName": "docker.pkg.github.com/myuser/myrepo/myapp"} | +| `additionalTags` | Array of additional tags to push. Optional. Example: `["beta", "next"]` | #### Registry @@ -65,11 +66,12 @@ The plugin can be configured in the [**semantic-release** configuration file](ht Environment variables are variables. Depends of `registries` option. -| Variable | Description | -| ----------------------- | ---------------------------------------------------------------------------------- | -| `DOCKER_USER` | username for docker registry. | -| `DOCKER_PASSWORD` | password for docker registry. | -| `DOCKER_BASE_IMAGE_TAG` | Name of the previously constructed docker image tag. Optional. Default `"latest"`. | +| Variable | Description | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `DOCKER_USER` | username for docker registry. | +| `DOCKER_PASSWORD` | password for docker registry. | +| `DOCKER_BASE_IMAGE_TAG` | Name of the previously constructed docker image tag. Optional. Default `"latest"`. | +| `DOCKER_RELEASE_IMAGE_TAG` | Name of the docker image tag for new release. Optional. Default `"latest"` or `channel` if use a custom branches or prerelase | ### Examples diff --git a/src/prepare.js b/src/prepare.js index ebd045d..6a24463 100644 --- a/src/prepare.js +++ b/src/prepare.js @@ -19,6 +19,11 @@ module.exports = async (pluginConfig, ctx) => { const docker = new Dockerode() const image = docker.getImage(pluginConfig.baseImageName) const tags = [ctx.nextRelease.version] + const channel = ctx.nextRelease.channel || 'latest' + const releaseImageTag = + ctx.env.DOCKER_RELEASE_IMAGE_TAG || + pluginConfig.releaseImageTag || + channel if (pluginConfig.additionalTags && pluginConfig.additionalTags.length > 0) { tags.push(...pluginConfig.additionalTags) } @@ -31,7 +36,7 @@ module.exports = async (pluginConfig, ctx) => { await image.tag({ repo: pluginConfig.baseImageName, tag }) } for (const { imageName } of pluginConfig.registries) { - for (const tag of [...tags, baseImageTag]) { + for (const tag of [...tags, releaseImageTag]) { ctx.logger.log( `Tagging docker image ${pluginConfig.baseImageName}:${baseImageTag} to ${imageName}:${tag}`, ) diff --git a/src/publish.js b/src/publish.js index 1eea9d4..1b3d44e 100644 --- a/src/publish.js +++ b/src/publish.js @@ -48,9 +48,12 @@ const pushImage = (response) => { module.exports = async (pluginConfig, ctx) => { try { const docker = new Dockerode() - const baseImageTag = - ctx.env.DOCKER_BASE_IMAGE_TAG || pluginConfig.baseImageTag || 'latest' - const tags = [baseImageTag, ctx.nextRelease.version] + const channel = ctx.nextRelease.channel || 'latest' + const releaseImageTag = + ctx.env.DOCKER_RELEASE_IMAGE_TAG || + pluginConfig.releaseImageTag || + channel + const tags = [releaseImageTag, ctx.nextRelease.version] if (pluginConfig.additionalTags && pluginConfig.additionalTags.length > 0) { tags.push(...pluginConfig.additionalTags) } diff --git a/src/types.d.ts b/src/types.d.ts index 2aca5f9..0ce9d2d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -19,6 +19,7 @@ export interface Config extends SemanticReleaseConfig { registries?: Registry[] baseImageName?: string baseImageTag?: string + releaseImageTag?: string } export interface ExecOptions { diff --git a/test/prepare.test.js b/test/prepare.test.js index 164e651..2d8be61 100644 --- a/test/prepare.test.js +++ b/test/prepare.test.js @@ -1,50 +1,25 @@ -/* eslint-disable require-jsdoc */ -const { describe, it, before, after } = require('mocha') +const { describe, it, before, beforeEach, after } = require('mocha') const { expect } = require('chai') const mock = require('mock-require') +const { createContext, createConfig, DockerMock, tags } = require('./shared') describe('Prepare', () => { - const ctx = { - env: { DOCKER_USER: 'user', DOCKER_PASSWORD: 'password' }, - nextRelease: { version: '1.0.0' }, - logger: { log: () => ({}), error: () => ({}) }, - } let prepare - /** @type {import('../src/types').Config} */ - const pluginConfig = { - baseImageName: 'ci.example.com/myapp', - registries: [ - { - user: 'DOCKER_USER', - password: 'DOCKER_PASSWORD', - url: 'registry.example.com', - imageName: 'registry.example.com/error', - }, - ], - } before(() => { - class DockerImage { - tag({ repo, tag }) { - return new Promise((resolve, reject) => { - if (/error/.test(repo)) { - return reject(new Error('invalid image')) - } - resolve() - }) - } - } - class DockerMock { - getImage(imageName) { - return new DockerImage() - } - } + tags.splice(0, tags.length) mock('dockerode', DockerMock) prepare = require('../src/prepare') }) + beforeEach(() => { + tags.splice(0, tags.length) + }) + it('expect a EDOCKERIMAGETAG error', async () => { try { + const pluginConfig = createConfig() + const ctx = createContext() await prepare(pluginConfig, ctx) } catch (errs) { const err = errs._errors[0] @@ -54,13 +29,25 @@ describe('Prepare', () => { }) it('expect success prepare', async () => { + const pluginConfig = createConfig() + const ctx = createContext() pluginConfig.registries[0].imageName = 'registry.example.com/myapp' pluginConfig.additionalTags = ['beta'] expect(await prepare(pluginConfig, ctx)).to.be.a('undefined') + expect(tags).include('beta') + expect(tags).include('latest') + }) + + it('prepare with custom branch', async () => { + const pluginConfig = createConfig() + const ctx = createContext() + pluginConfig.registries[0].imageName = 'registry.example.com/myapp' + ctx.nextRelease.channel = 'staging' + expect(await prepare(pluginConfig, ctx)).to.be.a('undefined') + expect(tags).include('staging') }) after(() => { mock.stopAll() }) }) -/* eslint-enable require-jsdoc */ diff --git a/test/publish.test.js b/test/publish.test.js index c21aa13..49de4ea 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -1,68 +1,25 @@ -/* eslint-disable require-jsdoc */ -const EventEmitter = require('events') -const { describe, it, before, after } = require('mocha') +/* eslint-disable sonarjs/no-duplicate-string */ +const { describe, it, before, beforeEach, after } = require('mocha') const { expect } = require('chai') const mock = require('mock-require') +const { createContext, createConfig, DockerMock, tags } = require('./shared') describe('Publish', () => { - // @ts-ignore - class MyEmitter extends EventEmitter {} - const ctx = { - env: { DOCKER_USER: 'user', DOCKER_PASSWORD: 'password' }, - nextRelease: { version: '1.0.0' }, - logger: { log: () => ({}), error: () => ({}) }, - } let publish - /** @type {import('../src/types').Config} */ - const pluginConfig = { - baseImageName: 'ci.example.com/myapp', - registries: [ - { - user: 'DOCKER_USER', - password: 'DOCKER_PASSWORD', - url: 'registry.error.com', - imageName: 'registry.example.com/myapp', - }, - ], - } - let dockerPushArgs - before(() => { - class DockerImage { - push({ tag, password, serveraddress, username }) { - dockerPushArgs.push({ tag, password, serveraddress, username }) - - return new Promise((resolve, reject) => { - const response = new MyEmitter() - setTimeout(() => { - if (/error/.test(serveraddress)) { - // @ts-ignore - response.emit('error', new Error('remote error')) - } else { - // @ts-ignore - response.emit('end') - } - }, 500) - resolve(response) - }) - } - } - class DockerMock { - getImage(imageName) { - return new DockerImage() - } - } + tags.splice(0, tags.length) mock('dockerode', DockerMock) publish = require('../src/publish') }) - // eslint-disable-next-line no-undef beforeEach(() => { - dockerPushArgs = [] + tags.splice(0, tags.length) }) it('expect a EDOCKERIMAGEPUSH error', async () => { try { + const pluginConfig = createConfig() + const ctx = createContext() await publish(pluginConfig, ctx) } catch (errs) { const err = errs._errors[0] @@ -72,29 +29,35 @@ describe('Publish', () => { }) it('expect success publish', async () => { + const pluginConfig = createConfig() + const ctx = createContext() pluginConfig.registries[0].url = 'registry.example.com' pluginConfig.additionalTags = ['beta'] expect(await publish(pluginConfig, ctx)).to.be.a('undefined') - // eslint-disable-next-line no-unused-expressions - expect(isTagPublished('latest')).to.be.true - // eslint-disable-next-line no-unused-expressions - expect(isTagPublished('beta')).to.be.true + expect(tags).include('latest') + expect(tags).include('beta') }) it('expect skip "latest" publish', async () => { + const pluginConfig = createConfig() + const ctx = createContext() pluginConfig.registries[0].url = 'registry.example.com' pluginConfig.registries[0].skipTags = ['latest'] expect(await publish(pluginConfig, ctx)).to.be.a('undefined') - // eslint-disable-next-line no-unused-expressions - expect(isTagPublished('latest')).to.be.false + expect(tags).not.include('latest') + }) + + it('expect publish branch channel as image tag', async () => { + const pluginConfig = createConfig() + const ctx = createContext() + pluginConfig.registries[0].url = 'registry.example.com' + ctx.nextRelease.channel = 'staging' + expect(await publish(pluginConfig, ctx)).to.be.a('undefined') + expect(tags).include('staging') }) after(() => { mock.stopAll() }) - - const isTagPublished = (tag) => { - return dockerPushArgs.some((arg) => arg.tag === tag) - } }) -/* eslint-enable require-jsdoc */ +/* eslint-enable sonarjs/no-duplicate-string */ diff --git a/test/shared.js b/test/shared.js new file mode 100644 index 0000000..d0ea11b --- /dev/null +++ b/test/shared.js @@ -0,0 +1,110 @@ +/* eslint-disable require-jsdoc, sonarjs/no-duplicate-string */ +const EventEmitter = require('events') + +/** @type {string[]} */ +const tags = [] + +// @ts-ignore +class MyEmitter extends EventEmitter {} + +/** + * @typedef {import('../src/types').Context} Context + * @typedef {import('../src/types').Config} Config + */ + +/** + * @returns {Context} -. + * @example const ctx = createContext() + */ +const createContext = () => ({ + env: { DOCKER_USER: 'user', DOCKER_PASSWORD: 'password' }, + nextRelease: { + version: '1.0.0', + type: 'major', + gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + gitTag: 'v1.0.0', + notes: 'Release notes for version 1.1.0...', + }, + logger: { log: () => ({}), error: () => ({}) }, +}) + +/** + * @param {boolean} [valid=true] -. + * @returns {Config} -. + * @example const pluginConfig = createConfig() + */ +const createConfig = (valid = true) => { + if (!valid) return {} + return { + baseImageName: 'ci.example.com/myapp', + registries: [ + { + user: 'DOCKER_USER', + password: 'DOCKER_PASSWORD', + url: 'registry.error.com', + imageName: 'registry.example.com/myapp', + }, + ], + } +} + +class DockerImage { + tag({ repo, tag }) { + tags.push(tag) + return new Promise((resolve, reject) => { + if (/error/.test(repo)) { + return reject(new Error('invalid image')) + } + resolve() + }) + } + + push({ tag, password, serveraddress, username }) { + tags.push(tag) + + return new Promise((resolve, reject) => { + const response = new MyEmitter() + setTimeout(() => { + if (/error/.test(serveraddress)) { + // @ts-ignore + response.emit('error', new Error('remote error')) + } else { + // @ts-ignore + response.emit('end') + } + }, 500) + resolve(response) + }) + } +} + +class DockerMock { + checkAuth({ password, serveraddress, username }) { + return new Promise((resolve, reject) => { + if (username === 'error') { + return reject(new Error('invalid login')) + } + resolve() + }) + } + + getImage(imageName) { + return new DockerImage() + } +} + +/** + * @param {string} tag -. + * @returns {boolean} -. + * @example existImageTag('latest') + */ +const existImageTag = (tag) => tags.some((_tag) => _tag === tag) + +module.exports = { + createContext, + createConfig, + DockerMock, + tags, + existImageTag, +} +/* eslint-enable require-jsdoc, sonarjs/no-duplicate-string */ diff --git a/test/verify.test.js b/test/verify.test.js index 0e88d42..8b00bd6 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -2,6 +2,7 @@ const { describe, it, before, after } = require('mocha') const { expect } = require('chai') const mock = require('mock-require') +const { DockerMock } = require('./shared') describe('Verify', () => { const env = {} @@ -10,16 +11,6 @@ describe('Verify', () => { const pluginConfig = {} before(() => { - class DockerMock { - checkAuth({ password, serveraddress, username }) { - return new Promise((resolve, reject) => { - if (username === 'error') { - return reject(new Error('invalid login')) - } - resolve() - }) - } - } mock('dockerode', DockerMock) verify = require('../src/verify') })