├── .editorconfig ├── .gitattributes ├── .gitignore ├── .releaserc.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── commitlint.config.js ├── package-lock.json ├── package.json ├── src ├── dockerPluginConfig.ts ├── index.ts ├── model │ └── auth.ts ├── prepare │ ├── index.spec.ts │ └── index.ts ├── publish │ └── index.ts └── verifyConditions │ ├── index.spec.ts │ └── index.ts ├── tsconfig.json ├── tslint.json └── typings └── semantic-release └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/launch.json 3 | !.vscode/tasks.json 4 | !.vscode/settings.json 5 | !.vscode/extensions.json 6 | 7 | node_modules 8 | dist 9 | .nyc_output 10 | coverage 11 | 12 | *.log 13 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master", 3 | "pkgRoot": "dist", 4 | "repositoryUrl": "git@github.com:iteratec/semantic-release-docker.git" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "maty.vscode-mocha-sidebar", 4 | "ms-vscode.vscode-typescript-tslint-plugin", 5 | "esbenp.prettier-vscode", 6 | "editorconfig.editorconfig", 7 | "eamodio.gitlens", 8 | "eg2.vscode-npm-script" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "mocha tests", 11 | "cwd": "${workspaceFolder}", 12 | "console": "integratedTerminal", 13 | "internalConsoleOptions": "neverOpen", 14 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 15 | "args": [ 16 | "src/**/*.spec.ts", 17 | "-r", "chai", 18 | "-r", "chai-as-promised", 19 | "-r", "ts-node/register" 20 | ], 21 | "sourceMaps": true 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Launch Program", 27 | "program": "${workspaceFolder}\\index.js", 28 | "outFiles": [ 29 | "${workspaceFolder}/**/*.js" 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha.files.glob": "src/**/*.spec.ts", 3 | "mocha.requires": [ 4 | "chai", 5 | "chai-as-promised", 6 | "ts-node/register" 7 | ], 8 | "mocha.options": { 9 | "compilers": "ts:ts-node/register" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 iteratec GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @iteratec/semantic-release-docker 2 | 3 | [![Build Status](https://dev.azure.com/iteratec-oss-bdd/semantic-release-docker/_apis/build/status/iteratec.semantic-release-docker?branchName=master)](https://dev.azure.com/iteratec-oss-bdd/semantic-release-docker/_build/latest?definitionId=2&branchName=master) 4 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 5 | [![latest npm package version](https://img.shields.io/npm/v/@iteratec/semantic-release-docker/latest.svg)](https://www.npmjs.com/package/@iteratec/semantic-release-docker) 6 | [![MIT license](https://img.shields.io/npm/l/@iteratec/semantic-release-docker.svg)](https://www.npmjs.com/package/@iteratec/semantic-release-docker) 7 | 8 | A [semantic-release](https://github.com/semantic-release/semantic-release) plugin to use semantic versioning for docker images. 9 | 10 | ## Supported Steps 11 | 12 | ### verifyConditions 13 | 14 | verifies that environment variables for authentication via username and password are set. 15 | It uses a registry server provided via config or environment variable (preferred) or defaults to docker hub if none is given. 16 | It also verifies that the credentials are correct by logging in to the given registry. 17 | 18 | ### prepare 19 | 20 | tags the specified image with the version number determined by semantic-release and additional tags provided in the configuration. 21 | In addition it supports specifying a complete image name (CIN) via configuration settings according to the canonical format specified by docker: 22 | 23 | `[registryhostname[:port]/][username/]imagename[:tag]` 24 | 25 | ### publish 26 | 27 | pushes the tagged images to the registry. 28 | 29 | ## Installation 30 | 31 | Run `npm i --save-dev @iteratec/semantic-release-docker` to install this semantic-release plugin. 32 | 33 | ## Configuration 34 | 35 | ### Docker registry authentication 36 | 37 | The `docker registry` authentication is **required** and can be set via environment variables. 38 | 39 | ### Environment variables 40 | 41 | | Variable | Description | 42 | | ------------------------ | ----------------------------------------------------------------------------------------- | 43 | | DOCKER_REGISTRY_URL | The hostname and port used by the desired docker registry. Leave blank to use docker hub. | 44 | | DOCKER_REGISTRY_USER | The user name to authenticate with at the registry. | 45 | | DOCKER_REGISTRY_PASSWORD | The password used for authentication at the registry. | 46 | 47 | ### Options 48 | 49 | | Option | Description | 50 | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | 51 | | additionalTags | _Optional_. An array of strings allowing to specify additional tags to apply to the image. | 52 | | imageName | **_Required_** The name of the image to release. | 53 | | registryUrl | _Optional_. The hostname and port used by the the registry in format `hostname[:port]`. Omit the port if the registry uses the default port | 54 | | repositoryName | _Optional_. The name of the repository in the registry, e.g. username on docker hub | 55 | 56 | ## Usage 57 | 58 | full configuration: 59 | 60 | ```json 61 | { 62 | "verifyConfig": ["@iteratec/semantic-release-docker"], 63 | "prepare": { 64 | "path": "@iteratec/semantic-release-docker", 65 | "additionalTags": ["test", "demo"], 66 | "imageName": "my-image", 67 | "registryUrl": "my-private-registry:5678", 68 | "respositoryName": "my-repository" 69 | }, 70 | "publish": { 71 | "path": "@iteratec/semantic-release-docker" 72 | } 73 | } 74 | ``` 75 | 76 | results in `my-private-registry:5678/my-repository/my-image` with tags `test`, `demo` and the `` determined by `semantic-release`. 77 | 78 | minimum configuration: 79 | 80 | ```json 81 | { 82 | "verifyConfig": ["@iteratec/semantic-release-docker"], 83 | "prepare": { 84 | "path": "@iteratec/semantic-release-docker", 85 | "imageName": "my-image" 86 | }, 87 | "publish": { 88 | "path": "@iteratec/semantic-release-docker" 89 | } 90 | } 91 | ``` 92 | 93 | results in `my-image:` 94 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: Build 3 | pool: 4 | name: Hosted Ubuntu 1604 5 | demands: npm 6 | steps: 7 | - task: NodeTool@0 8 | displayName: "Use Node 10" 9 | inputs: 10 | versionSpec: 10.x 11 | - task: Npm@1 12 | displayName: "Install dependencies" 13 | inputs: 14 | verbose: false 15 | - task: Npm@1 16 | displayName: Build 17 | inputs: 18 | command: custom 19 | customCommand: run build 20 | - task: Npm@1 21 | displayName: Test 22 | inputs: 23 | command: custom 24 | customCommand: run test 25 | - script: | 26 | npm run release 27 | displayName: Publish 28 | env: 29 | NPM_TOKEN: $(NPMTOKEN) 30 | GITHUB_TOKEN: $(GITHUBTOKEN) 31 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iteratec/semantic-release-docker", 3 | "version": "1.0.0-semantically-released", 4 | "description": "semantic-release plugins to use semantic versioning with docker images", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rimraf dist && tsc", 8 | "postbuild": "cpx package.json dist/ && cpx package-lock.json dist/", 9 | "commit": "git-cz", 10 | "test": "mocha -r chai -r chai-as-promised -r ts-node/register src/**/*.spec.ts", 11 | "release": "semantic-release" 12 | }, 13 | "keywords": [ 14 | "semantic-release", 15 | "docker", 16 | "semver" 17 | ], 18 | "author": "Christoph Murczek ", 19 | "repository": { 20 | "type": "git", 21 | "url": "git@github.com:iteratec/semantic-release-docker.git" 22 | }, 23 | "license": "MIT", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "dependencies": { 28 | "dockerode": "^2.5.8" 29 | }, 30 | "devDependencies": { 31 | "@commitlint/cli": "^7.5.2", 32 | "@commitlint/config-conventional": "^7.5.0", 33 | "@types/chai": "^4.1.3", 34 | "@types/chai-as-promised": "^7.1.0", 35 | "@types/dockerode": "^2.5.20", 36 | "@types/mocha": "^5.2.0", 37 | "@types/node": "^11.13.4", 38 | "@types/sinon": "^7.0.11", 39 | "chai": "^4.1.2", 40 | "chai-as-promised": "^7.1.1", 41 | "commitizen": "^3.0.7", 42 | "cpx": "^1.5.0", 43 | "cz-conventional-changelog": "^2.1.0", 44 | "ghooks": "^2.0.4", 45 | "mocha": "^6.1.4", 46 | "rimraf": "^2.6.2", 47 | "semantic-release": "^15.13.18", 48 | "sinon": "^7.3.1", 49 | "ts-node": "^8.0.3", 50 | "tslint": "^5.15.0", 51 | "typescript": "^3.4.3" 52 | }, 53 | "config": { 54 | "commitizen": { 55 | "path": "cz-conventional-changelog" 56 | }, 57 | "ghooks": { 58 | "pre-commit": "npm run test", 59 | "commit-msg": "commitlint -e" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/dockerPluginConfig.ts: -------------------------------------------------------------------------------- 1 | import { SemanticReleasePlugin } from 'semantic-release'; 2 | export interface DockerPluginConfig extends SemanticReleasePlugin { 3 | additionalTags?: string[]; 4 | imageName: string; 5 | registryUrl?: string; 6 | repositoryName?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { verifyConditions } from './verifyConditions'; 2 | export { prepare } from './prepare'; 3 | export { publish } from './publish'; 4 | -------------------------------------------------------------------------------- /src/model/auth.ts: -------------------------------------------------------------------------------- 1 | export interface Auth { 2 | username: string; 3 | password: string; 4 | serveraddress?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/prepare/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import Dockerode from 'dockerode'; 4 | import { 5 | SemanticReleaseConfig, 6 | SemanticReleaseContext, 7 | } from 'semantic-release'; 8 | import { createStubInstance, mock, stub } from 'sinon'; 9 | 10 | import { DockerPluginConfig } from '../dockerPluginConfig'; 11 | import { prepare } from './index'; 12 | 13 | // declare var docker: any; 14 | 15 | describe('@iteratec/semantic-release-docker', function() { 16 | describe('prepare', function() { 17 | const config: SemanticReleaseConfig = { 18 | branch: '', 19 | noCi: true, 20 | repositoryUrl: '', 21 | tagFormat: '', 22 | }; 23 | const context: SemanticReleaseContext = { 24 | // tslint:disable-next-line:no-empty 25 | logger: { log: (message: string) => {} }, 26 | nextRelease: { 27 | gitTag: '', 28 | notes: '', 29 | version: 'next', 30 | }, 31 | options: { 32 | branch: '', 33 | noCi: true, 34 | prepare: [ 35 | { 36 | imageName: '', 37 | path: '@iteratec/semantic-release-docker', 38 | } as DockerPluginConfig, 39 | ], 40 | repositoryUrl: '', 41 | tagFormat: '', 42 | }, 43 | }; 44 | const rs = {} as NodeJS.ReadableStream; 45 | const iii = {} as Dockerode.ImageInspectInfo; 46 | const fakeImage = { 47 | get( 48 | callback?: (error?: any, result?: NodeJS.ReadableStream) => void, 49 | ): any { 50 | if (callback) { 51 | return; 52 | } 53 | return new Promise(() => rs); 54 | }, 55 | history(callback?: (error?: any, result?: any) => void): any { 56 | if (callback) { 57 | return; 58 | } 59 | return new Promise(() => ''); 60 | }, 61 | inspect( 62 | callback?: (error?: any, result?: Dockerode.ImageInspectInfo) => void, 63 | ): any { 64 | if (callback) { 65 | return; 66 | } 67 | return new Promise(() => iii); 68 | }, 69 | modem: '', 70 | push( 71 | options?: {}, 72 | callback?: (error?: any, result?: NodeJS.ReadableStream) => void, 73 | ): any { 74 | if (callback) { 75 | return; 76 | } 77 | return new Promise(() => rs); 78 | }, 79 | remove( 80 | options?: {}, 81 | callback?: (error?: any, result?: Dockerode.ImageRemoveInfo) => void, 82 | ): any { 83 | if (callback) { 84 | return; 85 | } 86 | return new Promise(() => ''); 87 | }, 88 | tag(options?: {}, callback?: () => void): any { 89 | if (callback) { 90 | return; 91 | } 92 | return new Promise((resolve) => { 93 | resolve({}); 94 | }); 95 | }, 96 | } as Dockerode.Image; 97 | let dockerStub: any; 98 | 99 | before(function() { 100 | use(chaiAsPromised); 101 | }); 102 | 103 | before(function() { 104 | dockerStub = createStubInstance(Dockerode); 105 | dockerStub.getImage.returns(fakeImage); 106 | }); 107 | 108 | it('should throw if no imagename is provided', function() { 109 | return expect(prepare(config, context)).to.eventually.be.rejectedWith( 110 | '\'imageName\' is not set in plugin configuration', 111 | ); 112 | }); 113 | 114 | it('should tag an image', async function() { 115 | const imageMock = mock(fakeImage); 116 | const fakeImageName = 'fakeTestImage'; 117 | const expected = { 118 | repo: fakeImageName, 119 | tag: context.nextRelease!.version, 120 | }; 121 | (context.options 122 | .prepare![0] as DockerPluginConfig).imageName = fakeImageName; 123 | // setup the mock with expectations 124 | imageMock 125 | .expects('tag') 126 | .once() 127 | .withExactArgs(expected) 128 | .resolves({ name: fakeImageName }); 129 | await prepare(config, context, dockerStub); 130 | // tslint:disable-next-line: no-unused-expression 131 | expect(imageMock.verify()).to.not.throw; 132 | }); 133 | 134 | it('should add multiple tags to an image', async function() { 135 | const imageStub = stub(fakeImage); 136 | const fakeImageName = 'fakeTestImage'; 137 | const expected = [context.nextRelease!.version!, 'tag1', 'tag2']; 138 | (context.options 139 | .prepare![0] as DockerPluginConfig).imageName = fakeImageName; 140 | (context.options.prepare![0] as DockerPluginConfig).additionalTags = [ 141 | expected[1], 142 | expected[2], 143 | ]; 144 | // setup the mock with expectations 145 | imageStub.tag.resolves({ name: fakeImageName }); 146 | 147 | await prepare(config, context, dockerStub); 148 | // tslint:disable-next-line: no-unused-expression 149 | expect(imageStub.tag.calledThrice).to.be.true; 150 | expect(imageStub.tag.firstCall.args[0]).to.deep.equal({ 151 | repo: fakeImageName, 152 | tag: expected[0], 153 | }); 154 | expect(imageStub.tag.secondCall.args[0]).to.deep.equal({ 155 | repo: fakeImageName, 156 | tag: expected[1], 157 | }); 158 | expect(imageStub.tag.thirdCall.args[0]).to.deep.equal({ 159 | repo: fakeImageName, 160 | tag: expected[2], 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /src/prepare/index.ts: -------------------------------------------------------------------------------- 1 | import Dockerode from 'dockerode'; 2 | 3 | import { 4 | SemanticReleaseConfig, 5 | SemanticReleaseContext, 6 | } from 'semantic-release'; 7 | import { DockerPluginConfig } from '../dockerPluginConfig'; 8 | 9 | export var prepared = false; 10 | 11 | export async function prepare( 12 | pluginConfig: SemanticReleaseConfig, 13 | context: SemanticReleaseContext, 14 | docker?: Dockerode, 15 | ): Promise { 16 | const preparePlugin = context.options.prepare!.find( 17 | (p) => p.path === '@iteratec/semantic-release-docker', 18 | ) as DockerPluginConfig; 19 | if (!preparePlugin.imageName) { 20 | throw new Error('\'imageName\' is not set in plugin configuration'); 21 | } 22 | if (!docker) { 23 | docker = new Dockerode(); 24 | } 25 | const image = docker.getImage(preparePlugin.imageName); 26 | let tags = [context.nextRelease!.version!]; 27 | if (preparePlugin.additionalTags && preparePlugin.additionalTags.length > 0) { 28 | tags = tags.concat(preparePlugin.additionalTags); 29 | } 30 | return Promise.all( 31 | tags.map((imagetag) => { 32 | return image.tag({ 33 | repo: 34 | `${ 35 | preparePlugin.registryUrl ? `${preparePlugin.registryUrl}/` : '' 36 | }` + 37 | `${ 38 | preparePlugin.repositoryName 39 | ? `${preparePlugin.repositoryName}/` 40 | : '' 41 | }` + 42 | `${preparePlugin.imageName}`, 43 | tag: imagetag, 44 | }); 45 | }), 46 | ) 47 | .then((data) => { 48 | if (!prepared) { 49 | prepared = true; 50 | } 51 | return data.map((result) => result.name); 52 | }) 53 | .catch((error) => { 54 | throw new Error(error); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/publish/index.ts: -------------------------------------------------------------------------------- 1 | import Dockerode from 'dockerode'; 2 | 3 | import { SemanticReleaseConfig, SemanticReleaseContext } from 'semantic-release'; 4 | import { DockerPluginConfig } from '../dockerPluginConfig'; 5 | import { Auth } from '../model/auth'; 6 | import { prepare, prepared } from '../prepare'; 7 | 8 | interface PushOptions extends Auth { 9 | tag: string; 10 | } 11 | 12 | export interface PublishedRelease { 13 | completeImageName: string[]; 14 | } 15 | 16 | export async function publish(pluginConfig: SemanticReleaseConfig, context: SemanticReleaseContext) { 17 | if (!prepared) { 18 | prepare(pluginConfig, context); 19 | } 20 | const docker = new Dockerode(); 21 | let tags = [context.nextRelease!.version!]; 22 | const preparePlugin = context.options.prepare! 23 | .find((p) => p.path === '@iteratec/semantic-release-docker')! as DockerPluginConfig; 24 | if (preparePlugin.additionalTags && preparePlugin.additionalTags.length > 0) { 25 | tags = tags.concat(preparePlugin.additionalTags); 26 | } 27 | const imageName = `${preparePlugin.registryUrl ? `${preparePlugin.registryUrl}/` : ''}` + 28 | `${preparePlugin.repositoryName ? `${preparePlugin.repositoryName}/` : ''}` + 29 | `${preparePlugin.imageName}`; 30 | const image = docker.getImage(imageName); 31 | const options: PushOptions = { 32 | password: process.env.DOCKER_REGISTRY_PASSWORD!, 33 | serveraddress: process.env.DOCKER_REGISTRY_URL ? 34 | process.env.DOCKER_REGISTRY_URL : preparePlugin.registryUrl ? preparePlugin.registryUrl : '', 35 | tag: '', 36 | username: process.env.DOCKER_REGISTRY_USER!, 37 | }; 38 | return Promise.all(tags.map((imageTag: string) => { 39 | options.tag = imageTag; 40 | context.logger.log(`pushing image ${imageName}:${imageTag}`); 41 | return image.push(options); 42 | })) 43 | .then((streams) => Promise.all(streams.map((stream) => new Promise((resolve, reject) => { 44 | stream.on('data', (chunk) => context.logger.log(chunk.toString())); 45 | stream.on('end', () => resolve()); 46 | stream.on('error', (error) => reject(error)); 47 | })))) 48 | .then(() => { 49 | return { 50 | completeImageName: tags.map((tag: string) => `${imageName}:${tag}`), 51 | } as PublishedRelease; 52 | }) 53 | .catch((error) => { 54 | throw new Error(error); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/verifyConditions/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import Dockerode from 'dockerode'; 4 | import { createStubInstance } from 'sinon'; 5 | 6 | import { 7 | SemanticReleaseConfig, 8 | SemanticReleaseContext, 9 | } from 'semantic-release'; 10 | import { DockerPluginConfig } from '../dockerPluginConfig'; 11 | import { Auth } from '../model/auth'; 12 | import { verifyConditions } from './index'; 13 | 14 | describe('@iteratec/semantic-release-docker', function() { 15 | describe('verifyConditions', function() { 16 | const config: SemanticReleaseConfig = { 17 | branch: '', 18 | noCi: true, 19 | repositoryUrl: '', 20 | tagFormat: '', 21 | }; 22 | const context: SemanticReleaseContext = { 23 | logger: { 24 | // tslint:disable-next-line:no-empty 25 | log: (message: string) => {}, 26 | }, 27 | options: { 28 | branch: '', 29 | noCi: true, 30 | prepare: [ 31 | { 32 | imageName: '', 33 | path: '@iteratec/semantic-release-docker', 34 | } as DockerPluginConfig, 35 | ], 36 | repositoryUrl: '', 37 | tagFormat: '', 38 | }, 39 | }; 40 | let dockerStub: any; 41 | 42 | before(function() { 43 | use(chaiAsPromised); 44 | dockerStub = createStubInstance(Dockerode); 45 | dockerStub.checkAuth.resolves(true); 46 | }); 47 | 48 | afterEach(function() { 49 | process.env.DOCKER_REGISTRY_USER = ''; 50 | process.env.DOCKER_REGISTRY_PASSWORD = ''; 51 | process.env.DOCKER_REGISTRY_URL = ''; 52 | }); 53 | 54 | it('should throw when the username is not set', function() { 55 | return expect( 56 | verifyConditions(config, context), 57 | ).to.eventually.be.rejectedWith( 58 | 'Environment variable DOCKER_REGISTRY_USER must be set in order to login to the registry.', 59 | ); 60 | }); 61 | 62 | it('should throw when the password is not set', function() { 63 | process.env.DOCKER_REGISTRY_USER = 'username'; 64 | return expect( 65 | verifyConditions(config, context), 66 | ).to.eventually.be.rejectedWith( 67 | 'Environment variable DOCKER_REGISTRY_PASSWORD must be set in order to login to the registry.', 68 | ); 69 | }); 70 | 71 | it('should use the registry from the config', async function() { 72 | const expected = { 73 | password: 'password', 74 | serveraddress: 'my_private_registry', 75 | username: 'username', 76 | } as Auth; 77 | process.env.DOCKER_REGISTRY_USER = expected.username; 78 | process.env.DOCKER_REGISTRY_PASSWORD = expected.password; 79 | (context.options.prepare![0] as DockerPluginConfig).registryUrl = 80 | expected.serveraddress; 81 | await verifyConditions(config, context, dockerStub); 82 | // tslint:disable-next-line: no-unused-expression 83 | expect(dockerStub.checkAuth.calledOnce).to.be.true; 84 | expect(dockerStub.checkAuth.firstCall.args[0]).to.deep.equal(expected); 85 | }); 86 | 87 | it('should prefer the registry from the environment variable over the one from the config', async function() { 88 | const expected = { 89 | password: 'topsecret', 90 | serveraddress: 'registry_from_env', 91 | username: 'me', 92 | } as Auth; 93 | process.env.DOCKER_REGISTRY_USER = expected.username; 94 | process.env.DOCKER_REGISTRY_PASSWORD = expected.password; 95 | process.env.DOCKER_REGISTRY_URL = expected.serveraddress; 96 | (context.options.prepare![0] as DockerPluginConfig).registryUrl = 97 | 'registry_from_config'; 98 | await verifyConditions(config, context, dockerStub); 99 | // tslint:disable-next-line: no-unused-expression 100 | expect(dockerStub.checkAuth.calledOnce).to.be.true; 101 | expect(dockerStub.checkAuth.firstCall.args[0]).to.deep.equal(expected); 102 | }); 103 | 104 | it('should let docker decide which registry to use if none is specified', async function() { 105 | const expected = { 106 | password: 'topsecret', 107 | serveraddress: '', 108 | username: 'me', 109 | } as Auth; 110 | (context.options.prepare![0] as DockerPluginConfig).registryUrl = ''; 111 | (context.options.prepare![0] as DockerPluginConfig).imageName = ''; 112 | process.env.DOCKER_REGISTRY_USER = expected.username; 113 | process.env.DOCKER_REGISTRY_PASSWORD = expected.password; 114 | await verifyConditions(config, context, dockerStub); 115 | // tslint:disable-next-line: no-unused-expression 116 | expect(dockerStub.checkAuth.calledOnce).to.be.true; 117 | expect(dockerStub.checkAuth.firstCall.args[0]).to.deep.equal(expected); 118 | }); 119 | 120 | afterEach(function() { 121 | dockerStub.checkAuth.resetHistory(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/verifyConditions/index.ts: -------------------------------------------------------------------------------- 1 | import Dockerode from 'dockerode'; 2 | import { 3 | SemanticReleaseConfig, 4 | SemanticReleaseContext, 5 | } from 'semantic-release'; 6 | 7 | import { DockerPluginConfig } from '../dockerPluginConfig'; 8 | import { Auth } from '../model/auth'; 9 | 10 | export var verified = false; 11 | 12 | export async function verifyConditions( 13 | pluginConfig: SemanticReleaseConfig, 14 | context: SemanticReleaseContext, 15 | docker?: Dockerode, 16 | ) { 17 | if (!process.env.DOCKER_REGISTRY_USER) { 18 | throw new Error( 19 | 'Environment variable DOCKER_REGISTRY_USER must be set in order to login to the registry.', 20 | ); 21 | } 22 | if (!process.env.DOCKER_REGISTRY_PASSWORD) { 23 | throw new Error( 24 | 'Environment variable DOCKER_REGISTRY_PASSWORD must be set in order to login to the registry.', 25 | ); 26 | } 27 | let preparePlugin: DockerPluginConfig; 28 | if ( 29 | !context.options.prepare || 30 | !context.options.prepare!.find( 31 | (p) => p.path === '@iteratec/semantic-release-docker', 32 | ) 33 | ) { 34 | throw new Error('\'prepare\' is not configured'); 35 | } 36 | preparePlugin = context.options.prepare.find( 37 | (p) => p.path === '@iteratec/semantic-release-docker', 38 | ) as DockerPluginConfig; 39 | let registryUrl: string; 40 | if (process.env.DOCKER_REGISTRY_URL || preparePlugin.registryUrl) { 41 | registryUrl = process.env.DOCKER_REGISTRY_URL 42 | ? process.env.DOCKER_REGISTRY_URL 43 | : preparePlugin.registryUrl!; 44 | } else { 45 | registryUrl = ''; 46 | } 47 | 48 | if (!docker) { 49 | docker = new Dockerode(); 50 | } 51 | const auth = { 52 | password: process.env.DOCKER_REGISTRY_PASSWORD, 53 | serveraddress: registryUrl, 54 | username: process.env.DOCKER_REGISTRY_USER, 55 | } as Auth; 56 | return docker.checkAuth(auth).then((result) => { 57 | if (!verified) { 58 | verified = true; 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./dist", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | "typeRoots": ["./node_modules/@types", "./typings"], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": false, 9 | "only-arrow-functions": false, 10 | "no-var-keyword": false, 11 | "quotemark": [true, "single"], 12 | "max-line-length": [true, 140] 13 | }, 14 | "rulesDirectory": [] 15 | } 16 | -------------------------------------------------------------------------------- /typings/semantic-release/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for semantic-release 2 | // Project: semantic-release-docker 3 | // Definitions by: Christoph Murczek 4 | 5 | declare module 'semantic-release' { 6 | 7 | interface Logger { 8 | log: (message: string) => void; 9 | } 10 | 11 | interface NextRelease { 12 | gitTag: string; 13 | gitHead?: string; 14 | notes: string; 15 | version?: string; 16 | } 17 | 18 | export interface SemanticReleaseConfig { 19 | branch: string; 20 | repositoryUrl: string; 21 | tagFormat: string; 22 | noCi: boolean; 23 | } 24 | 25 | export interface SemanticReleasePlugin { 26 | path: string; 27 | } 28 | 29 | interface SemanticReleasePluginConfig { 30 | analyzeCommits?: SemanticReleasePlugin[]; 31 | fail?: SemanticReleasePlugin[]; 32 | generateNotes?: SemanticReleasePlugin[]; 33 | prepare?: SemanticReleasePlugin[]; 34 | publish?: SemanticReleasePlugin[]; 35 | success?: SemanticReleasePlugin[]; 36 | verifyConditions?: SemanticReleasePlugin[] | string[]; 37 | } 38 | 39 | export interface SemanticReleaseContext { 40 | options: SemanticReleaseConfig & SemanticReleasePluginConfig; 41 | nextRelease?: NextRelease; 42 | logger: Logger; 43 | } 44 | 45 | } 46 | --------------------------------------------------------------------------------