├── .dockerignore ├── .eslintrc.js ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── compose ├── auth │ └── htpasswd ├── base.yml ├── certs │ ├── server.crt │ └── server.key ├── dev.yml └── test.yml ├── env └── local.env ├── index.js ├── lib ├── build-config.js ├── build-template-vars.js ├── docker │ ├── image.js │ └── index.js ├── fail.js ├── handlebars │ ├── helpers │ │ ├── endswith.js │ │ ├── eq.js │ │ ├── gt.js │ │ ├── gte.js │ │ ├── includes.js │ │ ├── index.js │ │ ├── lower.js │ │ ├── lt.js │ │ ├── lte.js │ │ ├── neq.js │ │ ├── pick.js │ │ ├── split.js │ │ ├── startswith.js │ │ └── upper.js │ └── index.js ├── lang │ ├── array │ │ ├── index.js │ │ └── to-array.js │ ├── object │ │ ├── get.js │ │ ├── has.js │ │ └── index.js │ └── string │ │ ├── index.js │ │ ├── template.js │ │ └── typecast.js ├── parse-pkg-name.js ├── post-publish.js ├── prepare.js ├── publish.js ├── read-pkg.js ├── success.js └── verify.js ├── package.json ├── pnpm-lock.yaml ├── release.config.js └── test ├── common └── git │ ├── add.js │ ├── commit.js │ ├── head.js │ ├── index.js │ ├── init-origin.js │ ├── init-remote.js │ ├── init.js │ ├── push.js │ ├── tag.js │ └── tags.js ├── fixture ├── docker │ ├── Dockerfile.post │ ├── Dockerfile.prepare │ ├── Dockerfile.publish │ └── Dockerfile.test ├── package.json └── pkg │ ├── one │ └── package.json │ └── two │ └── package.json ├── integration ├── multi-image-release.js ├── post-publish.js ├── prepare.js ├── publish.js ├── release.js └── verify.js └── unit ├── build-config.js ├── build-template-vars.js ├── docker └── image.js ├── handlebars └── helpers │ ├── endswith.js │ ├── eq.js │ ├── gt.js │ ├── gte.js │ ├── includes.js │ ├── index.js │ ├── lower.js │ ├── lt.js │ ├── lte.js │ ├── neq.js │ ├── pick.js │ ├── split.js │ ├── startswith.js │ └── upper.js ├── lang ├── array.js ├── object.js └── string.js ├── parse-pkg-name.js └── read-pkg.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.tgz 3 | compose/ 4 | node_modules/ 5 | coverage/ 6 | release.config.js 7 | .github/ 8 | .nyc_outpuut/ 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | root: true 5 | , ignorePatterns: [ 6 | 'node_modules/' 7 | , 'test/fixture/' 8 | , 'coverage/' 9 | , '.nyc_output/' 10 | , 'env/' 11 | , 'doc/' 12 | ] 13 | , extends: 'codedependant' 14 | , parserOptions: { 15 | ecmaVersion: 2022 16 | , type: 'script' 17 | } 18 | , rules: { 19 | 'object-shorthand': 0 20 | , 'sensible/check-require': [2, 'always', { 21 | root: __dirname 22 | }] 23 | , 'no-unused-vars': [ 24 | 'error', { 25 | varsIgnorePattern: '_' 26 | }] 27 | , 'quote-props': [ 28 | 2 29 | , 'as-needed' 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Test + Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | push: 9 | branch: 10 | - main 11 | jobs: 12 | test: 13 | name: Test Suite 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Install 21 | run: npm install 22 | 23 | release: 24 | name: release 25 | needs: test 26 | runs-on: ubuntu-24.04 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | - uses: actions/setup-node@v1 31 | with: 32 | node-version: 18 33 | - run: npm install 34 | 35 | - name: Publish 36 | run: npm run release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | GIT_AUTHOR_NAME: 'Dependant Bot' 40 | GIT_AUTHOR_EMAIL: 'release-bot@codedependant.net' 41 | GIT_COMMITTER_NAME: 'Dependant Bot' 42 | GIT_COMMITTER_EMAIL: 'release-bot@codedependant.net' 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | *.vim 107 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | compose/ 3 | node_modules/ 4 | coverage/ 5 | release.config.js 6 | pnpm-lock.yaml 7 | .github/ 8 | .nyc_output/ 9 | .dockerignore 10 | Dockerfile 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | hoist=true 2 | package-lock=false 3 | lock-file=false 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Semantic Release Docker 2 | 3 | ## [5.1.1](https://github.com/esatterwhite/semantic-release-docker/compare/v5.1.0...v5.1.1) (2025-05-24) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **ci**: update actions runner dependencies [1f23b01](https://github.com/esatterwhite/semantic-release-docker/commit/1f23b01b6f8ddec193eada6e9a441bba95c739dc) - Eric Satterwhite 9 | * **plugin**: better support for multiple plugin instances [8b6621c](https://github.com/esatterwhite/semantic-release-docker/commit/8b6621c60a4ba40d16d5ed026cd49c23396c6e98) - Eric Satterwhite, closes: [#60](https://github.com/esatterwhite/semantic-release-docker/issues/60) 10 | 11 | 12 | ### Chores 13 | 14 | * **compose**: update docker registry certs [4513f83](https://github.com/esatterwhite/semantic-release-docker/commit/4513f83a3eb9eb0bdab4e05ef132290cfccc0f8e) - Eric Satterwhite 15 | * **deps**: object-hash@3.0.0 [e04e9d3](https://github.com/esatterwhite/semantic-release-docker/commit/e04e9d373c44c6145ebae3170d2b878dedccafc5) - Eric Satterwhite 16 | * **deps**: stream-buffers@3.0.3 [b937a46](https://github.com/esatterwhite/semantic-release-docker/commit/b937a4693ab054d3496a6f902024cd70ff7c7543) - Eric Satterwhite 17 | 18 | # [5.1.0](https://github.com/esatterwhite/semantic-release-docker/compare/v5.0.4...v5.1.0) (2024-12-30) 19 | 20 | 21 | ### Chores 22 | 23 | * **deps**: actions/core@1.11.1 [dd03ed4](https://github.com/esatterwhite/semantic-release-docker/commit/dd03ed4c4e9624e578b7f9b35e370c32463810f5) - Eric Satterwhite 24 | 25 | 26 | ### Features 27 | 28 | * **plugin**: add basic support for github actions [01085f6](https://github.com/esatterwhite/semantic-release-docker/commit/01085f68ffba854f1a4923879d9dfcfdf589eaf0) - Eric Satterwhite, closes: [#49](https://github.com/esatterwhite/semantic-release-docker/issues/49) 29 | 30 | ## [5.0.4](https://github.com/esatterwhite/semantic-release-docker/compare/v5.0.3...v5.0.4) (2024-12-23) 31 | 32 | 33 | ### Continuous Integration 34 | 35 | * fix test [a4b886c](https://github.com/esatterwhite/semantic-release-docker/commit/a4b886cbfdbe200970210aed6633363de44bab28) - Eric Satterwhite 36 | 37 | ## [5.0.3](https://github.com/esatterwhite/semantic-release-docker/compare/v5.0.2...v5.0.3) (2024-03-25) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **verify**: fix logic around the conditional prepare step [ae8335a](https://github.com/esatterwhite/semantic-release-docker/commit/ae8335a654bf844c3186a7d24ec068f03ced3fe5) - Eric Satterwhite 43 | 44 | ## [5.0.2](https://github.com/esatterwhite/semantic-release-docker/compare/v5.0.1...v5.0.2) (2024-03-22) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **config**: propgate dry run option to docker image [1438ded](https://github.com/esatterwhite/semantic-release-docker/commit/1438dedae17c45eeb88f17b6108d343de283f10f) - Eric Satterwhite 50 | 51 | ## [5.0.1](https://github.com/esatterwhite/semantic-release-docker/compare/v5.0.0...v5.0.1) (2024-03-22) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * **config**: include the dry-run param from input options [2de549f](https://github.com/esatterwhite/semantic-release-docker/commit/2de549f8dc2dcdc9741c06959ce0d120dd8b5fc5) - Eric Satterwhite 57 | 58 | # [5.0.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.5.1...v5.0.0) (2024-03-22) 59 | 60 | 61 | ### Chores 62 | 63 | * **compose**: update test registry certs [fa8d771](https://github.com/esatterwhite/semantic-release-docker/commit/fa8d771f8f3381e3b2327a29ab10bb27b1257d1e) - Eric Satterwhite 64 | 65 | 66 | ### Features 67 | 68 | * **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) 69 | 70 | 71 | ### Miscellaneous 72 | 73 | * **README.md**: fix typo in README.md [700e2f0](https://github.com/esatterwhite/semantic-release-docker/commit/700e2f0543a1fbd7d59b6ead84870bc8c5cfc3b3) - Eric Satterwhite 74 | 75 | 76 | ### **BREAKING CHANGES** 77 | 78 | * **docker:** images built with buildx will not be stored locally 79 | * **docker:** dockerVerifyCmd will only take effect during dry runs (--dry-run) 80 | 81 | ## [4.5.1](https://github.com/esatterwhite/semantic-release-docker/compare/v4.5.0...v4.5.1) (2024-02-04) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **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) 87 | 88 | # [4.5.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.4.0...v4.5.0) (2023-10-20) 89 | 90 | 91 | ### Features 92 | 93 | * **config**: allow --quiet flag to be configurable [7abc74e](https://github.com/esatterwhite/semantic-release-docker/commit/7abc74ecdd96baa8d27ecb177defc137a64b482f) - Eric Satterwhite, closes: [#40](https://github.com/esatterwhite/semantic-release-docker/issues/40) 94 | 95 | # [4.4.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.3.0...v4.4.0) (2023-07-11) 96 | 97 | 98 | ### Features 99 | 100 | * **image**: add support for cache-from build flag [efae32d](https://github.com/esatterwhite/semantic-release-docker/commit/efae32d760bc0ef1978ad97f834b4cb034199ace) - Eric Satterwhite, closes: [#35](https://github.com/esatterwhite/semantic-release-docker/issues/35) 101 | * **image**: support arbitrary build flags [d7e875d](https://github.com/esatterwhite/semantic-release-docker/commit/d7e875d2b0b8de58e57c56e9ba24bf3c2db5df84) - Eric Satterwhite 102 | 103 | # [4.3.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.2.0...v4.3.0) (2022-12-29) 104 | 105 | 106 | ### Features 107 | 108 | * **config**: add option to disable post publish image removal [0c03cbd](https://github.com/esatterwhite/semantic-release-docker/commit/0c03cbd16bf7aa34cb435e3782492ad294e9bdfd) - Eric Satterwhite, closes: [#28](https://github.com/esatterwhite/semantic-release-docker/issues/28) 109 | 110 | # [4.2.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.1.0...v4.2.0) (2022-12-19) 111 | 112 | 113 | ### Chores 114 | 115 | * **ci**: downgrade github actions runner version [c6f1935](https://github.com/esatterwhite/semantic-release-docker/commit/c6f1935aec63e21a85c8e513dd74fed29a84562b) - Eric Satterwhite 116 | * **test**: regenerate the local docker registry certs [4cfa6e2](https://github.com/esatterwhite/semantic-release-docker/commit/4cfa6e2a912b69ff3cfc496b88dae449a982e5f4) - Eric Satterwhite 117 | * **test**: remove ci test temporarily [c28f199](https://github.com/esatterwhite/semantic-release-docker/commit/c28f199ab3d2e9bb85846e562c936c782f9440a5) - Eric Satterwhite 118 | 119 | 120 | ### Documentation 121 | 122 | * fix broken table in readme [e85e6f3](https://github.com/esatterwhite/semantic-release-docker/commit/e85e6f38f48e3c11ef88953e0cfa74276aab09a6) - Eric Satterwhite 123 | 124 | 125 | ### Features 126 | 127 | * added dockerNetwork config option [d6f2def](https://github.com/esatterwhite/semantic-release-docker/commit/d6f2defa0a4cfa3a36b1b63e7cdfe13c700e632a) - Eric Satterwhite, closes: [#29](https://github.com/esatterwhite/semantic-release-docker/issues/29) 128 | 129 | # [4.1.0](https://github.com/esatterwhite/semantic-release-docker/compare/v4.0.0...v4.1.0) (2022-04-10) 130 | 131 | 132 | ### Chores 133 | 134 | * **deps**: semantic-release/error@3.0.0 [d55ff11](https://github.com/esatterwhite/semantic-release-docker/commit/d55ff1108a0130ce10932e409683fb741fb315d3) - Eric Satterwhite 135 | * **deps**: tap@16.0.0 [94d3340](https://github.com/esatterwhite/semantic-release-docker/commit/94d3340dba42ed702b71325ea5afc2f627df1fcc) - Eric Satterwhite 136 | 137 | 138 | ### Features 139 | 140 | * **docker**: allow command to be run via docker in verify step [4646bd6](https://github.com/esatterwhite/semantic-release-docker/commit/4646bd681cb4d7ee4a48a9187fbe7dfe88686b78) - Eric Satterwhite, closes: [#20](https://github.com/esatterwhite/semantic-release-docker/issues/20) 141 | 142 | 143 | ### Miscellaneous 144 | 145 | * add prerelease example [90158fd](https://github.com/esatterwhite/semantic-release-docker/commit/90158fdaa65b35a861ce40ca2c3a660f28446f72) - Eric Satterwhite 146 | * Update Readme [74f9910](https://github.com/esatterwhite/semantic-release-docker/commit/74f99107723223a67009c96a762b200f8d25621d) - GitHub 147 | 148 | # [4.0.0](https://github.com/esatterwhite/semantic-release-docker/compare/v3.1.2...v4.0.0) (2021-12-31) 149 | 150 | 151 | ### Chores 152 | 153 | * **deps**: @codedependant/release-config-npm@1.0.4 [fdb0985](https://github.com/esatterwhite/semantic-release-docker/commit/fdb0985d346b1f74d61cf4d0ef238743e5f1d6f4) - Eric Satterwhite 154 | * **deps**: eslint-config-codedependant@3.0.0 [6807acc](https://github.com/esatterwhite/semantic-release-docker/commit/6807accd81002e7bc86d79f260c19141f40363f6) - Eric Satterwhite 155 | * **deps**: eslint@8.5.0 [f7e0bbe](https://github.com/esatterwhite/semantic-release-docker/commit/f7e0bbeb67156dedb0b3a8a2d0bad0d8828c7dde) - Eric Satterwhite 156 | 157 | 158 | ### Features 159 | 160 | * **template**: replace simple template engine with handlebars [b20c89c](https://github.com/esatterwhite/semantic-release-docker/commit/b20c89ca979de7969028541b6a29ac9b867a06c3) - Eric Satterwhite, closes: [#16](https://github.com/esatterwhite/semantic-release-docker/issues/16) 161 | 162 | 163 | ### Miscellaneous 164 | 165 | * fix typo [3aa33ba](https://github.com/esatterwhite/semantic-release-docker/commit/3aa33ba2fc003a9f88e6b1d4263999152a577eda) - Eric Satterwhite 166 | 167 | 168 | ### **BREAKING CHANGES** 169 | 170 | * **template:** Use handlebars as template engine. Place holders are 171 | now double curlies `{{ }}` 172 | 173 | ## [3.1.2](https://github.com/esatterwhite/semantic-release-docker/compare/v3.1.1...v3.1.2) (2021-09-18) 174 | 175 | 176 | ### Bug Fixes 177 | 178 | * **config**: ensure the docker context is passed image builds [366b8d1](https://github.com/esatterwhite/semantic-release-docker/commit/366b8d1d1f90855a76cfdb78009ec4510ead769e) - Eric Satterwhite, closes: [#13](https://github.com/esatterwhite/semantic-release-docker/issues/13) 179 | 180 | ## [3.1.1](https://github.com/esatterwhite/semantic-release-docker/compare/v3.1.0...v3.1.1) (2021-09-18) 181 | 182 | 183 | ### Documentation 184 | 185 | * corrected the parameter - dockerFile [81e9319](https://github.com/esatterwhite/semantic-release-docker/commit/81e9319e7dae9905cf098a5b2c3a9837d4f5d1d9) - Eric Satterwhite 186 | 187 | # [3.1.0](https://github.com/esatterwhite/semantic-release-docker/compare/v3.0.1...v3.1.0) (2021-04-12) 188 | 189 | 190 | ### Features 191 | 192 | * **lib**: parse tags array from string [27d078b](https://github.com/esatterwhite/semantic-release-docker/commit/27d078b2bbd1f8881f2a4c390b50ac6d384e40f4) - Eric Satterwhite 193 | 194 | ## Changelog 195 | 196 | ## [3.0.1](https://github.com/esatterwhite/semantic-release-docker/compare/v3.0.0...v3.0.1) (2021-04-09) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * **docker**: honor absolute paths to docker file [723257b](https://github.com/esatterwhite/semantic-release-docker/commit/723257b705e83ed8d951673e5ab5f4ef7d75b437) - Eric Satterwhite 202 | 203 | # [3.0.0](https://github.com/esatterwhite/semantic-release-docker/compare/v2.2.0...v3.0.0) (2021-04-08) 204 | 205 | 206 | ### Chores 207 | 208 | * **deps**: eslint-config-codedependant [dbe464e](https://github.com/esatterwhite/semantic-release-docker/commit/dbe464ebf7ca571ffe0430f67085d30a291f828d) - Eric Satterwhite 209 | * **deps**: release-config-npm@1.0.1 [98b1a88](https://github.com/esatterwhite/semantic-release-docker/commit/98b1a880ef213abb49af9f9bd5fcff6d13729c21) - Eric Satterwhite 210 | 211 | 212 | ### Features 213 | 214 | * **config**: allow templated build arguments [7167dcf](https://github.com/esatterwhite/semantic-release-docker/commit/7167dcf9aba8f554e2ccb9d42209fce3ec86d3e3) - Eric Satterwhite 215 | * **config**: unnest config values from docker property [2087683](https://github.com/esatterwhite/semantic-release-docker/commit/2087683edfaf66825544ba2eb95eb8c6be533658) - Eric Satterwhite 216 | 217 | 218 | ### **BREAKING CHANGES** 219 | 220 | * **config:** Flatten the docker config option for semantic relase into the root 221 | config. Semantic release doesn't do a very good job of merging options 222 | that are coming from a sharable configuration. This makes it easier to 223 | utilize overrides when using a sharable config. Options are camel cased 224 | using the docker root word prefix 225 | `docker.args` -> `dockerArgs` 226 | `docker.login` -> `dockerLogin` 227 | 228 | # Semantic Release Docker 229 | 230 | # [2.2.0](https://github.com/esatterwhite/semantic-release-docker/compare/v2.1.0...v2.2.0) (2020-12-21) 231 | 232 | 233 | ### Features 234 | 235 | * **verify:** add option to opt out of docker login ([de17169](https://github.com/esatterwhite/semantic-release-docker/commit/de17169897965d197ed51b0aeff2e06d29157c99)) 236 | 237 | # [2.1.0](https://github.com/esatterwhite/semantic-release-docker/compare/v2.0.2...v2.1.0) (2020-08-06) 238 | 239 | 240 | ### Features 241 | 242 | * **docker:** Load default build args into docker build ([0760d5e](https://github.com/esatterwhite/semantic-release-docker/commit/0760d5e73560a4bddbadb5a849f7574522c503fc)) 243 | 244 | ## [2.0.2](https://github.com/esatterwhite/semantic-release-docker/compare/v2.0.1...v2.0.2) (2020-08-05) 245 | 246 | ## [2.0.1](https://github.com/esatterwhite/semantic-release-docker/compare/v2.0.0...v2.0.1) (2020-07-28) 247 | 248 | # [2.0.0](https://github.com/esatterwhite/semantic-release-docker/compare/v1.0.1...v2.0.0) (2020-07-28) 249 | 250 | 251 | ### Features 252 | 253 | * **pkg:** update to trigger a release ([acefb13](https://github.com/esatterwhite/semantic-release-docker/commit/acefb13697ca64723efd90ea0c7f3b5e5a8a5106)) 254 | 255 | ## [1.0.1](https://github.com/esatterwhite/semantic-release-docker/compare/v1.0.0...v1.0.1) (2020-07-26) 256 | 257 | 258 | ### Bug Fixes 259 | 260 | * **lib:** fixes a bug in build config that would miss project config ([51b60c1](https://github.com/esatterwhite/semantic-release-docker/commit/51b60c12f8954c2cb59bb78a276529acc08fb8ea)) 261 | 262 | # 1.0.0 (2020-07-24) 263 | 264 | 265 | ### Features 266 | 267 | * **pkg:** initial implementation ([4a4dead](https://github.com/esatterwhite/semantic-release-docker/commit/4a4dead685892ecf89e900f3b6f7979c69fc440e)) 268 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 0000-BASE 2 | FROM docker:latest 3 | ARG SRC_DIR='.' 4 | RUN apk update && apk upgrade && apk add nodejs npm git curl 5 | WORKDIR /opt/app 6 | COPY ${SRC_DIR}/package.json /opt/app/ 7 | RUN npm install 8 | COPY ${SRC_DIR} /opt/app 9 | WORkDIR /opt/app 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eric Satterwhite 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 | # @codedependant/semantic-release-docker 2 | 3 | [![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) 4 | [![MIT license](https://img.shields.io/npm/l/@codedependant/semantic-release-docker.svg)](https://www.npmjs.com/package/@codedependant/semantic-release-docker) 5 | 6 | A [semantic-release](https://github.com/semantic-release/semantic-release) plugin to use semantic versioning for docker images. 7 | 8 | ## Supported Steps 9 | 10 | ### verifyConditions 11 | 12 | verifies that environment variables for authentication via username and password are set. 13 | It uses a registry server provided via config or environment variable (preferred) or defaults to docker hub if none is given. 14 | It also verifies that the credentials are correct by logging in to the given registry. 15 | 16 | ### prepare 17 | 18 | builds a an image using the specified docker file and context. This image will be used to create tags in later steps 19 | 20 | ### publish 21 | 22 | pushes the tags Images with specified tags and pushes them to the registry. 23 | Tags support simple templating via [handlebars][]. Values enclosed in braces `{{}}` will be substituted with release context information 24 | 25 | ## Installation 26 | 27 | Run `npm i --save-dev @codedependant/semantic-release-docker` to install this semantic-release plugin. 28 | 29 | ## Configuration 30 | 31 | ### Docker registry authentication 32 | 33 | Authentication to a `docker registry` is set via environment variables. It is not required, and if 34 | omitted, it is assumed the docker daemon is already authenticated with the target registry. 35 | 36 | ### Environment variables 37 | 38 | | Variable | Description | 39 | | ------------------------ | ----------------------------------------------------------------------------------------- | 40 | | DOCKER_REGISTRY_USER | The user name to authenticate with at the registry. | 41 | | DOCKER_REGISTRY_PASSWORD | The password used for authentication at the registry. | 42 | 43 | ### Options 44 | 45 | | Option | Description | Type | Default | 46 | |------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|---------------------------------------------------------------| 47 | | `dockerTags` | _Optional_. An array of strings allowing to specify additional tags to apply to the image. Supports templating | [Array][]<[String][]> | [`latest`, `{{major}}-latest`, `{{version}}`] | 48 | | `dockerImage` | _Optional_. The name of the image to release. | [String][] | Parsed from package.json `name` property | 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 | | `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) | 52 | | `dockerFile` | _Optional_. The path, relative to `$PWD` to a Docker file to build the target image with | [String][] | `Dockerfile` | 53 | | `dockerContext` | _Optional_. A path, relative to `$PWD` to use as the build context A | [String][] | `.` | 54 | | `dockerLogin` | _Optional_. Set to false it by pass docker login if the docker daemon is already authorized | [String][] | `true` | 55 | | `dockerArgs` | _Optional_. Include additional values for docker's `build-arg`. Supports templating | [Object][] | | 56 | | `dockerPublish` | _Optional_. Automatically push image tags during the publish phase. | [Boolean][] | `true` | 57 | | `dockerVerifyCmd` | _Optional_. If specified, during the verify stage, the specified command will execute in a container of the build image. If the command errors, the release will fail. | [String][] | `false` | 58 | | `dockerNetwork` | _Optional_. Specify the Docker network to use while the image is building. | [String][] | `default` | 59 | | `dockerAutoClean` | _Optional_. If set to true | [Boolean][] | `true` | 60 | | `dockerBuildFlags` | _Optional_. An object containing additional flags to the `docker build` command. Values can be strings or an array of strings | [Object][] | `{}` | 61 | | `dockerBuildCacheFrom` | _Optional_. A list of external cache sources. See [--cache-from][] | [String][] | [Array][]<[String][]> | | 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 | 76 | ### Build Arguments 77 | 78 | By default several build arguments will be included when the docker images is being built. 79 | Build arguments can be templated in the same fashion as docker tags. If the value for a 80 | build argument is explicitly `true`, the value will be omitted and the value from 81 | a matching environment variable will be utilized instead. This can be useful when trying to include 82 | secrets and other sensitive information 83 | 84 | | Argument Name | Description | Default | 85 | |---------------------|-------------------------------------------------------------------------------------------|------------------------------| 86 | | `SRC_DIRECTORY` | The of the directory the build was triggered from | The directory name of CWD | 87 | | `TARGET_PATH` | Path relative to the execution root. Useful for Sharing a Single Docker file in monorepos | | 88 | | `NPM_PACKAGE_NAME` | The `name` property extracted from `package.json` - if present | | 89 | | `NPM_PACKAGE_SCOPE` | The parsed scope from the `name` property from `package.json` - sans `@` | | 90 | | `CONFIG_NAME` | The configured name of the docker image. | The parsed package name | 91 | | `CONFIG_PROJECT` | The configured docker repo project name | The package scope if present | 92 | | `GIT_SHA` | The commit SHA of the current release | | 93 | | `GIT_TAG` | The git tag of the current release | | 94 | 95 | ### Template Variables 96 | 97 | String template will be passed these 98 | 99 | | Variable name | Description | Type | 100 | |----------------|--------------------------------------------------------------------|-----------------------------| 101 | | `git_sha` | The commit SHA of the current release | [String][] | 102 | | `git_tag` | The git tag of the current release | [String][] | 103 | | `release_type` | The severity type of the current build (`major`, `minor`, `patch`) | [String][] | 104 | | `release_notes`| The release notes blob associated with the release | [String][] | 105 | | `next` | Semver object representing the next release | [Object][] | 106 | | `previous` | Semver object representing the previous release | [Object][] | 107 | | `major` | The major version of the next release | [Number][] | 108 | | `minor` | The minor version of the next release | [Number][] | 109 | | `patch` | The patch version of the next release | [Number][] | 110 | | `prerelease` | The prerelease versions of the next release | [Array][]<[Number][]> | 111 | | `env` | Environment variables that were set at build time | [Object][] | 112 | | `pkg` | Values parsed from `package.json` | [Object][] | 113 | | `build` | A Random build hash representing the current execution context | [String][] | 114 | | `now` | Current timestamp is ISO 8601 format | [String][] | 115 | 116 | ### Template Helpers 117 | 118 | The following handlebars template helpers are pre installed 119 | 120 | | Helper name | Description | Return Type | Example | 121 | |:------------:|------------------------------------------------------------------------|:-----------:|--------------------------------------------------------------------------------------------| 122 | | `endswith` | returns true if a string ends with another | [Boolean][] |
{{#if (endswith myvar 'ing')}}{{ othervar }}{{/if}}
| 123 | | `eq` | returns true if two values are strictly equal to each other | [Boolean][] |
{{#if (eq var_one var_two)}}{{ var_three }}{{/if}}
| 124 | | `gt` | returns true if the first value is greater than the second | [Boolean][] |
{{#if (gt var_one var_two)}}{{ var_three }}{{/if}}
| 125 | | `gte` | returns true if the first value is greater than or equal to the second | [Boolean][] |
{{#if (gte var_one var_two)}}{{ var_three }}{{/if}}
| 126 | | `includes` | returns true if the input (string \| array) includes the second value | [Boolean][] |
{{#if (includes some_array 'one')}}{{ var_one }}{{/if}}
| 127 | | `lower` | returns the lower cased varient of the input string | [String][] |
{{ lower my_var }}
| 128 | | `lt` | returns true if the first value is less than the second | [Boolean][] |
{{#if (lt var_one var_two)}}{{ var_three }}{{/if}}
| 129 | | `lte` | returns true if the first value is less than or equal to the second | [Boolean][] |
{{#if (lte var_one var_two)}}{{ var_three }}{{/if}}
| 130 | | `neq` | returns true if two values are not equal to each other | [Boolean][] |
{{#if (neq var_one var_two)}}{{ var_three }}{{/if}}
| 131 | | `pick` | returns the first non null-ish value. Will treat `false` as a value | `any` |
{{#with (pick var_one, var_two) as \| value \|}}{{ value }}{{/with}}
| 132 | | `split` | splits csv values into a javascript array | [Array][] |
{{#each (split csv_value)}}{{ this }}{{/each}}                             |
133 | | `startswith` | returns true if a string starts with another                           | [Boolean][] | 
{{#if (starts myvar 'foo')}}{{ othervar }}{{/if}}
| 134 | | `upper` | returns the upper cased varient of the input string | [String][] |
{{upper my_var}}
| 135 | 136 | ### Build Flags 137 | 138 | Using the `dockerBuildFlags` option allows you to pass arbitrary flags to the build command. 139 | If the standardized options are not sufficient, `dockerBuildFlags` is a perfect workaround 140 | until first class support can be added. This is considered and advanced feature, and you should 141 | know what you intend to do before using. There is no validation, and any configuration of the 142 | docker daemon required is expected to be done before hand. 143 | 144 | Keys found in `dockerBuildFlags` are normalized as command line flags in the following manner: 145 | 146 | * If the key does not start with a `-` it will be prepended 147 | * all occurences of `_` will be re-written as `-` 148 | * Single letter keys are considered shorthands e.g. `p` becomes `-p` 149 | * Multi letter keys are considered long form e.g. `foo_bar` becomes `--foo-bar` 150 | * If the value is an array, the flag is repeated for each occurance 151 | * A `null` value may be used to omit the value and only inject the flag itself 152 | 153 | #### Example 154 | 155 | ```javascript 156 | { 157 | plugins: [ 158 | ['@codedependant/semantic-release-docker', { 159 | dockerImage: 'my-image', 160 | dockerRegistry: 'quay.io', 161 | dockerProject: 'codedependant', 162 | dockerCacheFrom: 'myname/myapp' 163 | dockerBuildFlags: { 164 | pull: null 165 | , target: 'release' 166 | }, 167 | dockerArgs: { 168 | GITHUB_TOKEN: null 169 | } 170 | }] 171 | ] 172 | } 173 | ``` 174 | 175 | This configuration, will generate the following build command 176 | 177 | ```bash 178 | > docker build --network=default --quiet --tag quay.io/codedependant/my-image:abc123 --cache-from myname/myapp --build-arg GITHUB_TOKEN --pull --target release -f path/to/repo/Dockerfile /path/to/repo 179 | ``` 180 | 181 | ## Usage 182 | 183 | **full configuration**: 184 | 185 | ```javascript 186 | // release.config.js 187 | 188 | module.exports = { 189 | branches: ['main'] 190 | plugins: [ 191 | ['@codedependant/semantic-release-docker', { 192 | dockerTags: ['latest', '{{version}}', '{{major}}-latest', '{{major}}.{{minor}}'], 193 | dockerImage: 'my-image', 194 | dockerFile: 'Dockerfile', 195 | dockerRegistry: 'quay.io', 196 | dockerProject: 'codedependant', 197 | dockerPlatform: ['linux/amd64', 'linux/arm64'] 198 | dockerBuildFlags: { 199 | pull: null 200 | , target: 'release' 201 | }, 202 | dockerArgs: { 203 | API_TOKEN: null 204 | , RELEASE_DATE: new Date().toISOString() 205 | , RELEASE_VERSION: '{{next.version}}' 206 | } 207 | }] 208 | ] 209 | } 210 | ``` 211 | 212 | results in `quay.io/codedependant/my-image` with tags `latest`, `1.0.0`, `1-latest` and the `1.0` determined by `semantic-release`. 213 | 214 | Alternatively, using global options w/ root configuration 215 | ```json5 216 | // package.json 217 | { 218 | "name": "@codedependant/test-project" 219 | "version": "1.0.0" 220 | "release": { 221 | "extends": "@internal/release-config-docker", 222 | "dockerTags": ["latest", "{{version}}", "{{major}}-latest", "{{major}}.{{minor}}"], 223 | "dockerImage": "my-image", 224 | "dockerFile": "Dockerfile", 225 | "dockerRegistry": "quay.io", 226 | "dockerArgs": { 227 | "GITHUB_TOKEN": null 228 | , "SOME_VALUE": '{{git_sha}}' 229 | } 230 | } 231 | } 232 | ``` 233 | 234 | This would generate the following for a `1.2.0` build 235 | 236 | ```shell 237 | $ docker build -t quay.io/codedependant/my-image --build-arg GITHUB_TOKEN --build-arg SOME_VALUE=6eada70 -f Dockerfile . 238 | $ docker tag latest 239 | $ docker tag 1.2.0 240 | $ docker tag 1.2 241 | $ docker tag 1-latest 242 | $ docker push quay.io/codedependant/my-image 243 | ``` 244 | 245 | **minimum configuration**: 246 | 247 | ```json 248 | { 249 | "release": { 250 | "plugins": [ 251 | "@codedependant/semantic-release-docker" 252 | ] 253 | } 254 | } 255 | ``` 256 | 257 | * A package name `@codedependant/test-project` results in docker project name`codedependant` and image name `test-project` 258 | * A package name `test-project` results in a docker image name `test-project` 259 | 260 | the default docker image tags for the 1.0.0 release would be `1.0.0`, `1-latest`, `latest` 261 | 262 | **example prerelease configuration**: 263 | 264 | ```json 265 | { 266 | "release": { 267 | "dockerTags": [ 268 | "{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", 269 | "{{major}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", 270 | "{{major}}.{{minor}}-{{#if prerelease.[0]}}{{prerelease.[0]}}{{else}}latest{{/if}}", 271 | "{{version}}" 272 | ] 273 | } 274 | } 275 | ``` 276 | 277 | the docker tags for version `1.2.3` will be `1.2.3`, `1.2-latest`, `1-latest` and `latest` 278 | the docker tags for version `2.3.4-beta.6` will be `2.3.4-beta.6`, `2.3-beta`, `2-beta` and `beta` 279 | 280 | ## GitHub Actions 281 | 282 | The plugin has some basic support for github actions by exposing several outputs and environment variables 283 | during the publish stage 284 | 285 | > [!WARNING] 286 | > 287 | > When using buildx image shas are different per platform, and as such using the shas directly 288 | > is not supported and may result in unexpected results. 289 | 290 | ### Outputs 291 | 292 | | name | description | example | 293 | |--------------------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------| 294 | | `docker_image` | The full name of the docker image including the registry, sans any tag information | quay.io/codedependant/my-image | 295 | | `docker_image_build_id` | The unique build id used to initial build the image during a release | | 296 | | `docker_image_sha_short` | A shorted version of the image sha value suitable for referencing the image | `b94d27b9934d3e0` | 297 | | `docker_image_sha_long` | The full sha256 value that points the image that was build during a release | `b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9` | 298 | 299 | ### Environment Variables 300 | 301 | | name | description | example | 302 | |-------------------------------------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------| 303 | | `SEMANTIC_RELEASE_DOCKER_IMAGE` | The full name of the docker image including the registry, sans any tag information | quay.io/codedependant/my-image | 304 | | `SEMANTIC_RELEASE_DOCKER_IMAGE_BUILD_ID` | The unique build id used to initial build the image during a release | | 305 | | `SEMANTIC_RELEASE_DOCKER_IMAGE_SHA_SHORT` | A shorted version of the image sha value suitable for referencing the image | `b94d27b9934d3e0` | 306 | | `SEMANTIC_RELEASE_DOCKER_IMAGE_SHA_LONG` | The full sha256 value that points the image that was build during a release | `b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9` | 307 | 308 | ## Development 309 | 310 | ### Docker Registry 311 | 312 | To be able to push to the local registry with auth credentials, ssl certificates are required. 313 | This project uses self signed certs. To regenerate the certs run the following: 314 | 315 | ```bash 316 | $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout server.key -out server.crt 317 | ``` 318 | 319 | **NOTE**: When prompted for an FQDN it must be `registry:5000` 320 | **NOTE**: The credentials for the local registry are **user:** `iamweasel`, **pass:** `secretsquirrel` 321 | 322 | [handlebars]: https://handlebarsjs.com 323 | [Boolean]: https://mdn.io/boolean 324 | [String]: https://mdn.io/string 325 | [Array]: https://mdn.io/array 326 | [Object]: https://mdn.io/object 327 | [Number]: https://mdn.io/number 328 | [--cache-from]: https://docs.docker.com/engine/reference/commandline/build/#cache-from 329 | [buildx]: https://docs.docker.com/reference/cli/docker/buildx/build 330 | -------------------------------------------------------------------------------- /compose/auth/htpasswd: -------------------------------------------------------------------------------- 1 | iamweasel:$2y$05$.CjNBupd5C./9DS72VePXuw1LQ3m5s8.No6XHXIfSIwImS90oT0sO 2 | 3 | -------------------------------------------------------------------------------- /compose/base.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | registry: 4 | image: registry:2 5 | environment: 6 | REGISTRY_HTTP_TLS_CERTIFICATE: /certs/server.crt 7 | REGISTRY_HTTP_TLS_KEY: /certs/server.key 8 | REGISTRY_AUTH: htpasswd 9 | REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd 10 | REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm 11 | volumes: 12 | - $PWD/compose/certs:/certs 13 | - $PWD/compose/auth:/auth 14 | docker: 15 | privileged: true 16 | image: docker:25-dind 17 | environment: 18 | DOCKER_TLS_CERTDIR: '' 19 | DOCKER_BUILDKIT: 1 20 | command: [ 21 | "--insecure-registry=registry:5000" 22 | ] 23 | 24 | -------------------------------------------------------------------------------- /compose/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmzCCAoOgAwIBAgIUZfKvmAkBBUxqof/qSyRcP/ikVYMwDQYJKoZIhvcNAQEL 3 | BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNcmVnaXN0cnk6NTAw 5 | MDAeFw0yNTA1MjMxNjI1NDJaFw0yNjA1MjMxNjI1NDJaMF0xCzAJBgNVBAYTAkFV 6 | MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz 7 | IFB0eSBMdGQxFjAUBgNVBAMMDXJlZ2lzdHJ5OjUwMDAwggEiMA0GCSqGSIb3DQEB 8 | AQUAA4IBDwAwggEKAoIBAQDKEj7v6zvIo0uBX0d5+j98Ei/0NrtaGAbWMRqz8gG5 9 | PTYn0BLCrJxyUFw2bofymHnU1kJfP3wvvWiv+43RhWbV0B7BU63rwbBr7ktp9Tdm 10 | PiKiB972hn/zeec8yfzazrm/G2ZAYgE3OqKSdoiWHGss84XK2GYa/2SZJTSxz0W0 11 | iGC/Pon9L7Q5rfkycpQymIFO2NZCWx9v6DN4228Hxy+GyNVvLUpZ/Du/6TBLYTXy 12 | 4K7ULQ6mAssjM0D+h2k3uoyMzJ84lY7h3d3DQh/tGq06YA8aeSyvreLVM15j+bUu 13 | QLZfrxPrDPNaCSm958/49ZloH88fYjICpgFaZACH86xvAgMBAAGjUzBRMB0GA1Ud 14 | DgQWBBROY6VqgOXnNedDiIcqZ7sOdcyOQzAfBgNVHSMEGDAWgBROY6VqgOXnNedD 15 | iIcqZ7sOdcyOQzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAS 16 | 44AvqMU/dcq+tolhj7ThOFQmh1lCoE/eorC8I6HntHfLM3fvpnQFd5P02Bdp5Pb3 17 | X0xfXlnL6IsG55MUFD6MEsouTLgHMhZPRGgo6lN/LkPL+9Hv1joHLlXGZOi8maJa 18 | 9700pmShbsmJxxv+LPMgkWkQ/S9CQQnH+/pno0NqJuxYJEP2bBknoOHanNrbkL/G 19 | 35/zZRtg9YSwmo+AADd1FlkoJjf6VZjUOF8G44VnM3bAcdZfvEe502cMeJxiBmnI 20 | 91Ukomrx7zZdiSmW1y6cXyhOcv9pDSONce1lP3V/8EI5Vyk4J8qPvxHYlezhy958 21 | OWq7/tseI1+6YxL8FRyq 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /compose/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDKEj7v6zvIo0uB 3 | X0d5+j98Ei/0NrtaGAbWMRqz8gG5PTYn0BLCrJxyUFw2bofymHnU1kJfP3wvvWiv 4 | +43RhWbV0B7BU63rwbBr7ktp9TdmPiKiB972hn/zeec8yfzazrm/G2ZAYgE3OqKS 5 | doiWHGss84XK2GYa/2SZJTSxz0W0iGC/Pon9L7Q5rfkycpQymIFO2NZCWx9v6DN4 6 | 228Hxy+GyNVvLUpZ/Du/6TBLYTXy4K7ULQ6mAssjM0D+h2k3uoyMzJ84lY7h3d3D 7 | Qh/tGq06YA8aeSyvreLVM15j+bUuQLZfrxPrDPNaCSm958/49ZloH88fYjICpgFa 8 | ZACH86xvAgMBAAECggEAFNd37Vf2VRXemkvg++g/NwVLM+WXQk4bGml7JxctbVUu 9 | akHQRYr3IeU+9ZBF7lQisLyeoONT5Dqlew03jeYz+paaLXd7h0b1ctRjq9ySZ7W9 10 | 7bdhHE04Ej0/B+qPbWQIDXl+fOJ+3JrsHK4kHVN2DG9bm9XhBiTUU+Vd/37w4hNF 11 | 9e1o1BhzigsJPm4sOERkh82QyYIj1lc9Zt4jVv5KUHWRr4T7sMTUiBgSUc9hUSaU 12 | 2TM8T2+KOjZIY0ZXK3IE2Lo73E2hreS1Pep0/cmr6TfZNPVaJRl3avPQwJX+heTO 13 | CjJKBAJYZrJWem/RXc5VJ2/b9k1uoR2uU+nAkYfphQKBgQDoCSsClf1lbZRQNiWg 14 | ojdZ2E9MTx3r5AmxNATTE8axoTQYskCH9yDkcZz9mx3qBATOuIBP8GOjg+sBnM9k 15 | 9mk/LkTquc9p+KYaEmkokzgvCCwRwq2MGYuG+xFddNE3ZLPn2s45CEDAhd8Q/SC4 16 | Qi4H5inFXy5HYLAVE+C755idqwKBgQDe8NcwKdA2bStuMIe0giKYo1FogX3MiJcc 17 | GnntUUb64s1BtJYi/Vt9mRK/nfZ8cltCkPaO7TlxEQ0bD0fIgBC8vkm5IhmY4EPl 18 | wKbdP2L8u5OB2P7iTBuiwLFJdsolgq2M1DWC4KnBvVF1ar0ju92rJ/moo6adyYty 19 | OjN9dmTATQKBgAxn8RTCUDoMEdH4EyrzgWIcXqEF2eOy3ZHL5jYi6Iy2wcJQRYL/ 20 | g5KzQGGO2ZqZfGhRFQsxHyKu+vGrIKuVQStPnf+uz5gq4zahpV22AVsCZNjOP9kt 21 | xHgDFHqatFTx3WyYFk6WUl/4yGRwJD+1yiBB/hm/bQoD8WYvGeTyDQbhAoGBAM4b 22 | AhX40hE/JCOeohbzKGDMu/pNnKt2q5zDrW0E8wYGn5PbC+IVMHwRBBA6TSIH5u7H 23 | ben8zloFVYRqwAZQvyh/E1EggWGgE6VYUevBKhZUo64rmphDnFj+o+gy9fdvtFq5 24 | 5S613LrL938ByxI6IFiXgGuzv9mn9k8IF4op5kMRAoGBANvZQz9L8wA9dvEA36BU 25 | hc0g881gQtLSFylBFTvm7SkiITdeuRy6ubo7t84rh+Ae8hUaeoXhsKJvLPUKewsU 26 | Bv6GIAdRe5eHQtuTdxTFnu2Us4W18Jnn56T4OqCXOmDLMK1CmIw3HInQ0hPIdddY 27 | X+wFVOerplOlmcV5etBDJWhC 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /compose/dev.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | registry: 4 | ports: 5 | - 5000:5000 6 | -------------------------------------------------------------------------------- /compose/test.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | semantic-release: 4 | build: 5 | context: ../ 6 | environment: 7 | DOCKER_HOST: tcp://docker:2375 8 | DOCKER_BUILDKIT: 1 9 | TEST_DOCKER_REGISTRY: registry:5000 10 | command: | 11 | sh -c ' 12 | for i in $(seq 1 10); 13 | do 14 | if nc -z -v docker 2375; then 15 | break 16 | else 17 | echo "docker daemon not availabe waiting for 5 seconds and retring" 18 | sleep 5 19 | fi 20 | done 21 | npm run tap 22 | ' 23 | depends_on: 24 | - docker 25 | - registry 26 | -------------------------------------------------------------------------------- /env/local.env: -------------------------------------------------------------------------------- 1 | TEST_DOCKER_REGISTRY=localhost:5000 2 | DOCKER_REGISTRY_USER=iamweasel 3 | DOCKER_REGISTRY_PASSWORD=secretsquirrel 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const docker = require('./lib/docker/index.js') 4 | const dockerPrepare = require('./lib/prepare.js') 5 | const dockerVerify = require('./lib/verify.js') 6 | const dockerPublish = require('./lib/publish.js') 7 | const buildConfig = require('./lib/build-config.js') 8 | const dockerSuccess = require('./lib/success.js') 9 | const dockerFail = require('./lib/fail.js') 10 | 11 | // map multiple images to a unique sha 12 | const IMAGES = new Map() 13 | module.exports = { 14 | buildConfig 15 | , fail 16 | , prepare 17 | , publish 18 | , success 19 | , verifyConditions 20 | } 21 | 22 | /* istanbul ignore next */ 23 | async function fail(config, context) { 24 | const opts = await buildConfig(null, config, context) 25 | const image = IMAGES.get(opts.build) || docker.Image.from(opts, context) 26 | context.image = image 27 | IMAGES.set(opts.build, image) 28 | return dockerFail(opts, context) 29 | } 30 | 31 | async function prepare(config, context) { 32 | const opts = await buildConfig(null, config, context) 33 | const image = await dockerPrepare(opts, context) 34 | IMAGES.set(opts.build, image) 35 | return image 36 | } 37 | 38 | async function publish(config, context) { 39 | const opts = await buildConfig(null, config, context) 40 | const image = IMAGES.get(opts.build) || docker.Image.from(opts, context) 41 | context.image = image 42 | IMAGES.set(opts.build, image) 43 | return dockerPublish(opts, context) 44 | } 45 | 46 | async function success(config, context) { 47 | const opts = await buildConfig(null, config, context) 48 | const image = IMAGES.get(opts.build) || docker.Image.from(opts, context) 49 | context.image = image 50 | IMAGES.set(opts.build, image) 51 | return dockerSuccess(opts, context) 52 | } 53 | 54 | async function verifyConditions(config, context) { 55 | const opts = await buildConfig(null, config, context) 56 | const image = IMAGES.get(opts.build) || docker.Image.from(opts, context) 57 | context.image = image 58 | IMAGES.set(opts.build, image) 59 | return dockerVerify(opts, context) 60 | } 61 | 62 | -------------------------------------------------------------------------------- /lib/build-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hash = require('object-hash') 4 | const path = require('path') 5 | const readPkg = require('./read-pkg.js') 6 | const object = require('./lang/object/index.js') 7 | const array = require('./lang/array/index.js') 8 | const parsePkgName = require('./parse-pkg-name.js') 9 | const typeCast = require('./lang/string/typecast.js') 10 | const PWD = '.' 11 | const PID = `${process.pid}` 12 | const TIMESTAMP = new Date().toISOString() 13 | 14 | module.exports = buildConfig 15 | 16 | function applyDefaults(config) { 17 | 18 | const { 19 | dockerFile: dockerfile = 'Dockerfile' 20 | , dockerNoCache: nocache = false 21 | , dockerTags: tags = ['latest', '{{major}}-latest', '{{version}}'] 22 | , dockerArgs: args = {} 23 | , dockerBuildFlags: build_flags = {} 24 | , dockerBuildCacheFrom: cache_from 25 | , dockerRegistry: registry = null 26 | , dockerLogin: login = true 27 | , dockerImage: image 28 | , dockerPlatform: platform = null 29 | , dockerPublish: publish = true 30 | , dockerContext = '.' 31 | , dockerVerifyCmd: verifycmd = null 32 | , dockerNetwork: network = 'default' 33 | , dockerAutoClean: clean = true 34 | , dockerBuildQuiet: quiet = true 35 | , dryRun: dry_run = false 36 | } = config 37 | 38 | return { 39 | dockerfile 40 | , nocache 41 | , tags 42 | , args 43 | , build_flags 44 | , cache_from 45 | , registry 46 | , login 47 | , image 48 | , platform 49 | , publish 50 | , dockerContext 51 | , verifycmd 52 | , network 53 | , clean 54 | , quiet 55 | , dry_run 56 | } 57 | } 58 | 59 | async function buildConfig(build_id, config, context) { 60 | let name = null 61 | let scope = null 62 | let pkg = {} 63 | 64 | const normalized = applyDefaults(config) 65 | 66 | const { 67 | dockerfile 68 | , nocache 69 | , tags 70 | , args 71 | , build_flags 72 | , cache_from 73 | , registry 74 | , login 75 | , image 76 | , platform 77 | , publish 78 | , dockerContext 79 | , verifycmd 80 | , network 81 | , clean 82 | , quiet 83 | , dry_run 84 | } = normalized 85 | 86 | try { 87 | pkg = await readPkg({cwd: context.cwd}) 88 | const parsed = parsePkgName(pkg.name) 89 | name = parsed.name 90 | scope = parsed.scope 91 | } catch (_) {} 92 | 93 | const project = object.has(config, 'dockerProject') ? config.dockerProject : scope 94 | const root = object.get(context, 'options.root') 95 | const target = path.relative(root || context.cwd, context.cwd) || PWD 96 | const {nextRelease = {}} = context 97 | 98 | if (cache_from) build_flags.cache_from = array.toArray(cache_from) 99 | 100 | const configuration = { 101 | registry 102 | , dockerfile 103 | , nocache 104 | , build_flags 105 | , pkg 106 | , project 107 | , publish 108 | , tags: array.toArray(tags) 109 | , verifycmd 110 | , dry_run: !!typeCast(dry_run || context.dryRun) 111 | , args: { 112 | SRC_DIRECTORY: path.basename(context.cwd) 113 | , TARGET_PATH: target 114 | , NPM_PACKAGE_NAME: object.get(pkg, 'name') 115 | , NPM_PACKAGE_SCOPE: scope 116 | , CONFIG_NAME: image || name 117 | , CONFIG_PROJECT: project 118 | , GIT_SHA: nextRelease.gitHead || '' 119 | , GIT_TAG: nextRelease.gitTag || '' 120 | , ...(args || {}) 121 | } 122 | , name: image || name 123 | , build: null 124 | , login: login 125 | , env: context.env 126 | , context: dockerContext 127 | , network: network 128 | , quiet: typeCast(quiet) === true 129 | , clean: typeCast(clean) === true 130 | , platform: array.toArray(platform) 131 | } 132 | 133 | configuration.build = build_id || genBuildId(normalized) 134 | return configuration 135 | } 136 | 137 | function genBuildId(configuration) { 138 | return hash([configuration, PID, TIMESTAMP], {algorithm: 'sha256'}) 139 | } 140 | -------------------------------------------------------------------------------- /lib/build-template-vars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | const now = new Date().toISOString() 5 | 6 | module.exports = buildTemplateVars 7 | 8 | function buildTemplateVars(opts, context) { 9 | const {nextRelease = {}, lastRelease = {}} = context 10 | 11 | const versions = { 12 | next: semver.parse(nextRelease.version) || {} 13 | , previous: semver.parse(lastRelease.version) || {} 14 | } 15 | 16 | const {tags: _, ...rest} = opts 17 | return { 18 | ...versions.next 19 | , ...versions 20 | , ...nextRelease 21 | , ...rest 22 | , git_tag: nextRelease.gitTag 23 | , git_sha: nextRelease.gitHead 24 | , release_type: nextRelease.type 25 | , release_notes: nextRelease.notes 26 | , now: now 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/docker/image.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const path = require('path') 5 | const crypto = require('crypto') 6 | const execa = require('execa') 7 | const buildTemplateVars = require('../build-template-vars.js') 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}):(?\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 | } 20 | 21 | class Image { 22 | constructor(opts) { 23 | const { 24 | registry = null 25 | , project = null 26 | , name = null 27 | , sha = null 28 | , build_id = crypto.randomBytes(10).toString('hex') 29 | , dockerfile = 'Dockerfile' 30 | , cwd = process.cwd() 31 | , context = '.' 32 | , network = 'default' 33 | , publish = true 34 | , quiet = true 35 | , dry_run = false 36 | , tags = [] 37 | , platform = [] 38 | } = opts || {} 39 | 40 | if (!name || typeof name !== 'string') { 41 | const error = new TypeError('Docker Image "name" is required and must be a string') 42 | throw error 43 | } 44 | 45 | this.sha = sha 46 | this.sha256 = null 47 | this.opts = { 48 | build_id: build_id 49 | , args: new Map() 50 | , context: context 51 | , cwd: cwd 52 | , dockerfile: dockerfile 53 | , flags: new Map() 54 | , name: name 55 | , tags: tags 56 | , network: network 57 | , project: project 58 | , registry: registry 59 | , dry_run: dry_run 60 | , publish: publish 61 | , platform: array.toArray(platform) 62 | } 63 | 64 | if (quiet) this.flag('quiet', null) 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.dry_run 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 101 | } 102 | 103 | get id() { 104 | if (this.sha) return this.sha 105 | return `${this.opts.name}:${this.opts.build_id}` 106 | } 107 | 108 | get repo() { 109 | const parts = [] 110 | if (this.opts.registry) parts.push(this.opts.registry) 111 | if (this.opts.project) parts.push(this.opts.project) 112 | parts.push(this.opts.name) 113 | return parts.join('/') 114 | } 115 | 116 | get is_buildx() { 117 | return !!this.opts.platform?.length 118 | } 119 | 120 | get name() { 121 | return `${this.repo}:${this.opts.build_id}` 122 | } 123 | 124 | get context() { 125 | return path.resolve(this.opts.cwd, this.opts.context) 126 | } 127 | 128 | set context(ctx) { 129 | this.opts.context = ctx 130 | return this.opts.context 131 | } 132 | 133 | get dockerfile() { 134 | return path.resolve(this.opts.cwd, this.opts.dockerfile) 135 | } 136 | 137 | get network() { 138 | return this.opts.network 139 | } 140 | 141 | get flags() { 142 | const output = [] 143 | 144 | for (const [key, value] of this.opts.flags.entries()) { 145 | let normalized = key 146 | if (!key.startsWith('-')) { 147 | normalized = (key.length === 1 ? `-${key}` : `--${key}`) 148 | .toLowerCase() 149 | .replace(/_/g, '-') 150 | } 151 | 152 | if (value === null) { 153 | output.push(normalized) 154 | continue 155 | } 156 | 157 | for (const item of value) { 158 | output.push(normalized, item) 159 | } 160 | } 161 | 162 | return output 163 | } 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 | 220 | arg(key, val = null) { 221 | if (val === true || val == null) { // eslint-disable-line no-eq-null 222 | this.flag('build-arg', key) 223 | } else { 224 | this.flag('build-arg', `${key}=${val}`) 225 | } 226 | this.opts.args.set(key, val) 227 | return this 228 | } 229 | 230 | flag(key, val) { 231 | if (val === null) { 232 | this.opts.flags.set(key, val) 233 | return this 234 | } 235 | 236 | let value = this.opts.flags.get(key) || [] 237 | 238 | if (Array.isArray(val)) { 239 | value = value.concat(val) 240 | } else { 241 | value.push(val) 242 | } 243 | 244 | this.opts.flags.set(key, value) 245 | return this 246 | } 247 | 248 | async run(cmd) { 249 | if (this.is_buildx) return 250 | const stream = execa('docker', [ 251 | 'run' 252 | , '--rm' 253 | , this.name 254 | , ...array.toArray(cmd) 255 | ], {all: true}) 256 | 257 | stream.stdout.pipe(process.stdout) 258 | stream.stderr.pipe(process.stderr) 259 | 260 | const {all} = await stream 261 | return all 262 | } 263 | 264 | async build() { 265 | const stream = execa('docker', this.build_cmd) 266 | stream.stdout.pipe(process.stdout) 267 | stream.stderr.pipe(process.stderr) 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.sha256 = match.groups.sha 277 | this.sha = this.sha256.substring(0, 12) 278 | return this.sha 279 | } 280 | } 281 | this.sha = this.opts.build_id 282 | return this.sha 283 | } 284 | 285 | async tag(tag, push = true) { 286 | await execa('docker', ['tag', this.name, `${this.repo}:${tag}`]) 287 | if (!push) return 288 | 289 | const stream = execa('docker', ['push', `${this.repo}:${tag}`]) 290 | stream.stdout.pipe(process.stdout) 291 | stream.stderr.pipe(process.stderr) 292 | await stream 293 | } 294 | 295 | async push() { 296 | // push is a part of the buildx build operation 297 | // At this point the tags have already been pushed. 298 | // re-pushing manually is considered destructive 299 | if (this.is_buildx) return 300 | if (!this.opts.publish) return 301 | 302 | for (const tag of this.opts.tags) { 303 | await this.tag(tag) 304 | } 305 | } 306 | 307 | async clean() { 308 | const {stdout: images} = await execa('docker', ['images', this.repo, '-q']) 309 | if (!images) return 310 | await execa('docker', ['rmi', '-f', ...images.split(os.EOL)]) 311 | } 312 | } 313 | 314 | module.exports = Image 315 | -------------------------------------------------------------------------------- /lib/docker/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | Image: require('./image.js') 5 | } 6 | -------------------------------------------------------------------------------- /lib/fail.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const postPublish = require('./post-publish.js') 4 | 5 | module.exports = fail 6 | 7 | function fail(opts, context) { 8 | return postPublish(opts, context) 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/endswith.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = endsWith 4 | 5 | function endsWith(str, ...args) { 6 | if (typeof str !== 'string') return false 7 | return args.some((arg) => { 8 | return str.endsWith(arg) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/eq.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = eq 4 | 5 | function eq(lh, rh) { 6 | return lh === rh 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/gt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = gt 4 | 5 | function gt(lh, rh) { 6 | return lh > rh 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/gte.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = gte 4 | 5 | function gte(lh, rh) { 6 | return lh >= rh 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/includes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = includes 4 | 5 | function includes(input, arg, position) { 6 | if (!Array.isArray(input) && typeof input !== 'string') return false 7 | return input.includes(arg, position) 8 | } 9 | 10 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | endswith: require('./endswith') 5 | , eq: require('./eq') 6 | , gt: require('./gt') 7 | , gte: require('./gte') 8 | , includes: require('./includes') 9 | , lower: require('./lower') 10 | , lt: require('./lt') 11 | , lte: require('./lte') 12 | , neq: require('./neq') 13 | , pick: require('./pick') 14 | , startswith: require('./startswith') 15 | , split: require('./split') 16 | , upper: require('./upper') 17 | } 18 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/lower.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = lower 4 | 5 | function lower(str) { 6 | if (typeof str !== 'string') return '' 7 | return str.toLowerCase() 8 | } 9 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/lt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = lt 4 | 5 | function lt(lh, rh) { 6 | return lh < rh 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/lte.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = lte 4 | 5 | function lte(lh, rh) { 6 | return lh < rh 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/neq.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = neq 4 | 5 | function neq(lh, rh) { 6 | /* eslint-disable eqeqeq */ 7 | return lh != rh 8 | /* eslint-enable eqeqeq */ 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/pick.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = pick 4 | 5 | function pick(...args) { 6 | return args.find((value) => { 7 | /* eslint-disable no-eq-null */ 8 | if (value == null) return false 9 | /* eslint-enable no-eq-null */ 10 | if (value === '') return false 11 | return true 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/split.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('../../lang/array/to-array.js') 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/startswith.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = startsWith 4 | 5 | function startsWith(str, ...args) { 6 | if (typeof str !== 'string') return false 7 | return args.some((arg) => { 8 | return str.startsWith(arg) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/upper.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | module.exports = upper 5 | 6 | function upper(str) { 7 | if (typeof str !== 'string') return '' 8 | return str.toUpperCase() 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlebars/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Handlebars = require('handlebars') 4 | const helpers = require('./helpers') 5 | const handlebars = Handlebars.noConflict() 6 | 7 | handlebars.registerHelper(helpers) 8 | 9 | module.exports = handlebars 10 | 11 | -------------------------------------------------------------------------------- /lib/lang/array/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | toArray: require('./to-array.js') 5 | } 6 | -------------------------------------------------------------------------------- /lib/lang/array/to-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CSV_SEP_EXP = /\s*,\s*/ 4 | module.exports = function toArray(item, sep = CSV_SEP_EXP) { 5 | if (!item) return [] 6 | if (item instanceof Set) return Array.from(item) 7 | if (Array.isArray(item)) return item 8 | return typeof item === 'string' ? item.split(sep) : [item] 9 | } 10 | -------------------------------------------------------------------------------- /lib/lang/object/get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = getProperty 4 | 5 | function getProperty(obj, str, sep = '.') { 6 | if (!obj || !str) return null 7 | const parts = str.split(sep) 8 | let ret = obj 9 | const last = parts.pop() 10 | let prop 11 | 12 | /* eslint-disable no-cond-assign */ 13 | while (prop = parts.shift()) { 14 | ret = ret[prop] 15 | if (ret === null || ret === undefined) return ret 16 | } 17 | return ret[last] 18 | } 19 | -------------------------------------------------------------------------------- /lib/lang/object/has.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = hasProperty 4 | 5 | function hasProperty(obj, prop) { 6 | if (!obj) return false 7 | return Object.prototype.hasOwnProperty.call(obj, prop) 8 | } 9 | -------------------------------------------------------------------------------- /lib/lang/object/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | get: require('./get.js') 5 | , has: require('./has.js') 6 | } 7 | -------------------------------------------------------------------------------- /lib/lang/string/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | template: require('./template.js') 5 | , typecast: require('./typecast.js') 6 | } 7 | -------------------------------------------------------------------------------- /lib/lang/string/template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const handlebars = require('../../handlebars') 4 | 5 | module.exports = template 6 | 7 | function template(str) { 8 | if (typeof str !== 'string') return echo(str) 9 | return handlebars.compile(str) 10 | } 11 | 12 | function echo(input) { 13 | return () => { 14 | return input 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/lang/string/typecast.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @module lib/string/typecast 5 | * @author Eric Satterwhite 6 | **/ 7 | 8 | module.exports = function typecast(value) { 9 | if (value === 'null' || value === null) return null 10 | if (value === 'undefined' || value === undefined) return undefined 11 | if (value === 'true' || value === true) return true 12 | if (value === 'false' || value === false) return false 13 | if (value === '' || isNaN(value)) return value 14 | if (isFinite(value)) return parseFloat(value) 15 | return value 16 | } 17 | 18 | /** 19 | * Best effort to cast a string to its native couter part where possible 20 | * Supported casts are booleans, numbers, null and undefined 21 | * @function module:lib/string/typecast 22 | * @param {String} str The string value to typecast 23 | * @return {*} The coerced value 24 | * @example 25 | * typecast('null') // null 26 | * @example 27 | * typecast('true') // true 28 | * @example 29 | * typecast('10.01') // 10.01 30 | * @example 31 | * typecast({}) // {} 32 | **/ 33 | -------------------------------------------------------------------------------- /lib/parse-pkg-name.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const NAME_EXP = /^(?:@([^/]+?)[/])?([^/]+?)$/ 4 | 5 | module.exports = parsePkgName 6 | 7 | function parsePkgName(pkgname) { 8 | if (!pkgname) return {scope: null, name: null} 9 | const [_, scope = null, name = null] = (NAME_EXP.exec(pkgname) || []) 10 | return {scope, name} 11 | } 12 | 13 | -------------------------------------------------------------------------------- /lib/post-publish.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const docker = require('./docker/index.js') 4 | 5 | module.exports = postPublish 6 | 7 | async function postPublish(opts, context) { 8 | const {logger} = context 9 | const image = docker.Image.from(opts, context) 10 | if (!opts.clean) return 11 | 12 | logger.info(`removing images for ${image.repo}`) 13 | await image.clean() 14 | } 15 | -------------------------------------------------------------------------------- /lib/prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const docker = require('./docker/index.js') 4 | 5 | module.exports = dockerPrepare 6 | 7 | async function dockerPrepare(opts, context) { 8 | const {image = docker.Image.from(opts, context)} = context 9 | context.logger.info('building image', image.name) 10 | context.logger.info('build command: docker %s', image.build_cmd.join(' ')) 11 | await image.build() 12 | return image 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lib/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const actions = require('@actions/core') 4 | const docker = require('./docker/index.js') 5 | 6 | module.exports = publish 7 | 8 | async function publish(opts, context) { 9 | const {image = docker.Image.from(opts, context)} = context 10 | 11 | const sha = image.sha || opts.build_id 12 | const sha256 = image.sha256 || opts.build_id 13 | 14 | actions.setOutput('docker_image', image.repo) 15 | actions.setOutput('docker_image_build_id', image.opts.build_id) 16 | actions.setOutput('docker_image_sha_short', sha) 17 | actions.setOutput('docker_image_sha_long', sha256) 18 | 19 | actions.exportVariable('SEMANTIC_RELEASE_DOCKER_IMAGE', image.repo) 20 | actions.exportVariable('SEMANTIC_RELEASE_DOCKER_IMAGE_BUILD_ID', image.opts.build_id) 21 | actions.exportVariable('SEMANTIC_RELEASE_DOCKER_IMAGE_SHA_SHORT', sha) 22 | actions.exportVariable('SEMANTIC_RELEASE_DOCKER_IMAGE_SHA_LONG', sha256) 23 | await image.push() 24 | } 25 | -------------------------------------------------------------------------------- /lib/read-pkg.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const {promises: fs} = require('fs') 5 | 6 | module.exports = getPkg 7 | 8 | async function getPkg(opts) { 9 | const {cwd = process.cwd()} = opts || {} 10 | const pkg = await fs.readFile(path.join(cwd, 'package.json'), 'utf8') 11 | return JSON.parse(pkg) 12 | } 13 | -------------------------------------------------------------------------------- /lib/success.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const postPublish = require('./post-publish.js') 4 | 5 | module.exports = success 6 | 7 | function success(opts, context) { 8 | return postPublish(opts, context) 9 | } 10 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {promises: fs} = require('fs') 4 | const execa = require('execa') 5 | const debug = require('debug')('semantic-release:semantic-release-docker:verify') 6 | const SemanticError = require('@semantic-release/error') 7 | const prepare = require('./prepare.js') 8 | const docker = require('./docker/index.js') 9 | 10 | module.exports = verify 11 | 12 | async function verify(opts, context) { 13 | const {env} = context 14 | const PASSWORD = env.DOCKER_REGISTRY_PASSWORD || env.GITHUB_TOKEN 15 | const USERNAME = env.DOCKER_REGISTRY_USER 16 | 17 | if (!opts.name) { 18 | const error = new SemanticError( 19 | 'Docker image name not found' 20 | , 'EINVAL' 21 | , 'Image name parsed from package.json name if possible. ' 22 | + 'Or via the "dockerImage" option.' 23 | ) 24 | throw error 25 | } 26 | 27 | const image = docker.Image.from(opts, context) 28 | 29 | debug('docker options', opts) 30 | 31 | try { 32 | await fs.readFile(image.dockerfile) 33 | } catch (err) { 34 | const error = new SemanticError( 35 | `Unable to locate Dockerfile: ${image.dockerfile}` 36 | , err.code 37 | , 'Docker file is read from a local relative to PWD. Make sure the "dockerfile"' 38 | + ' option is set to the desired location' 39 | ) 40 | 41 | throw error 42 | } 43 | 44 | debug('image to build', image.repo) 45 | if (!USERNAME && !PASSWORD) { 46 | debug('No docker credentials found. Skipping login') 47 | } else { 48 | await doLogin({...opts, USERNAME, PASSWORD}, context) 49 | } 50 | 51 | if (!opts.verifycmd) return true 52 | if (!opts.dry_run) return true 53 | 54 | const img = await prepare(opts, context) 55 | const output = await img.run(opts.verifycmd) 56 | await img.clean() 57 | return output 58 | } 59 | 60 | async function doLogin(opts, context) { 61 | const {USERNAME, PASSWORD} = opts 62 | const {logger} = context 63 | 64 | if (opts.login === false) { 65 | debug('docker login === false. Skipping login') 66 | } else { 67 | let set = 0 68 | if (USERNAME) set += 1 69 | if (PASSWORD) set += 1 70 | 71 | if (set !== 2) { 72 | const error = new SemanticError( 73 | `Docker authentication failed ${USERNAME} ${PASSWORD}` 74 | , 'EAUTH' 75 | , 'Both ENV vars DOCKER_REGISTRY_USER and DOCKER_REGISTRY_PASSWORD must be set' 76 | ) 77 | throw error 78 | } 79 | 80 | const passwd = execa('echo', [PASSWORD]) 81 | const login = execa('docker', [ 82 | 'login' 83 | , opts.registry || '' 84 | , '-u', USERNAME 85 | , '--password-stdin' 86 | ]) 87 | 88 | passwd.stdout.pipe(login.stdin) 89 | 90 | try { 91 | await login 92 | } catch (err) { 93 | logger.fatal(err) 94 | const error = new SemanticError( 95 | `Docker authentication failed ${USERNAME} ${PASSWORD}` 96 | , 'EAUTH' 97 | , `Authentication to ${opts.registry || 'dockerhub'} failed` 98 | ) 99 | throw error 100 | } 101 | logger.success('docker login successful') 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codedependant/semantic-release-docker", 3 | "private": false, 4 | "version": "5.1.1", 5 | "description": "docker package", 6 | "main": "index.js", 7 | "files": [ 8 | "lib/", 9 | "README.md", 10 | "CHANGELOG.md", 11 | "LICENSE", 12 | "package.json", 13 | "index.js" 14 | ], 15 | "scripts": { 16 | "test": "docker-compose -f compose/base.yml -f compose/test.yml up --exit-code-from semantic-release --force-recreate --remove-orphans --build", 17 | "start": "docker-compose -f compose/base.yml -f compose/dev.yml up --force-recreate --remove-orphans --build", 18 | "stop": "docker-compose -f compose/base.yml -f compose/dev.yml down", 19 | "tap": "tap", 20 | "lint": "eslint .", 21 | "lint:fix": "npm run lint -- --fix", 22 | "local": "env $(cat env/local.env)", 23 | "pretap": "npm run lint", 24 | "release": "semantic-release", 25 | "release:dry": "semantic-release --no-ci --dry-run --branches=${BRANCH_NAME:-main}" 26 | }, 27 | "keywords": [ 28 | "semantic-release" 29 | ], 30 | "author": "Eric Satterwhite ", 31 | "license": "MIT", 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+ssh://git@github.com/esatterwhite/semantic-release-docker.git" 38 | }, 39 | "tap": { 40 | "ts": false, 41 | "jsx": false, 42 | "timeout": 60, 43 | "browser": false, 44 | "check-coverage": true, 45 | "lines": 95, 46 | "branches": 95, 47 | "statements": 95, 48 | "functions": 95, 49 | "files": [ 50 | "test/unit", 51 | "test/integration" 52 | ], 53 | "coverage-report": [ 54 | "text", 55 | "text-summary", 56 | "json", 57 | "json-summary", 58 | "html" 59 | ], 60 | "nyc-arg": [ 61 | "--exclude=coverage/", 62 | "--exclude=test/", 63 | "--exclude=.eslintrc.js", 64 | "--exclude=release.config.js", 65 | "--all" 66 | ] 67 | }, 68 | "devDependencies": { 69 | "@codedependant/release-config-npm": "^1.0.4", 70 | "@semantic-release/changelog": "^5.0.1", 71 | "@semantic-release/git": "^9.0.0", 72 | "@semantic-release/github": "^7.0.7", 73 | "eslint": "^8.5.0", 74 | "eslint-config-codedependant": "^3.0.0", 75 | "semantic-release": "17", 76 | "sinon": "^9.0.2", 77 | "stream-buffers": "^3.0.3", 78 | "tap": "^16.0.0" 79 | }, 80 | "dependencies": { 81 | "@actions/core": "^1.11.1", 82 | "@semantic-release/error": "^3.0.0", 83 | "debug": "^4.1.1", 84 | "execa": "^4.0.2", 85 | "handlebars": "^4.7.7", 86 | "object-hash": "^3.0.0", 87 | "semver": "^7.3.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* istanbul ignore file */ 4 | module.exports = { 5 | branches: ['main'] 6 | , extends: '@codedependant/release-config-npm' 7 | , changelogFile: 'CHANGELOG.md' 8 | , changelogTitle: '# Semantic Release Docker' 9 | } 10 | -------------------------------------------------------------------------------- /test/common/git/add.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | 5 | module.exports = add 6 | 7 | async function add(cwd, file = '.') { 8 | await execa('git', ['add', file], {cwd: cwd}) 9 | } 10 | -------------------------------------------------------------------------------- /test/common/git/commit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | const head = require('./head.js') 5 | 6 | module.exports = commit 7 | 8 | async function commit(cwd, message) { 9 | await execa('git', ['commit', '-m', message], {cwd: cwd}) 10 | return head(cwd) 11 | } 12 | -------------------------------------------------------------------------------- /test/common/git/head.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | 5 | module.exports = head 6 | 7 | async function head(cwd) { 8 | const {stdout} = await execa('git', ['rev-parse', 'HEAD'], {cwd: cwd}) 9 | return stdout 10 | } 11 | -------------------------------------------------------------------------------- /test/common/git/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | add: require('./add.js') 5 | , commit: require('./commit.js') 6 | , head: require('./head.js') 7 | , initOrigin: require('./init-origin.js') 8 | , initRemote: require('./init-remote.js') 9 | , init: require('./init.js') 10 | , push: require('./push.js') 11 | , tag: require('./tag.js') 12 | , tags: require('./tags.js') 13 | } 14 | -------------------------------------------------------------------------------- /test/common/git/init-origin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | const initRemote = require('./init-remote.js') 5 | 6 | module.exports = initOrigin 7 | 8 | async function initOrigin(cwd) { 9 | const origin = await initRemote() 10 | await execa('git', ['remote', 'add', 'origin', origin], {cwd: cwd}) 11 | await execa('git', ['push', '--all', 'origin'], {cwd: cwd}) 12 | return origin 13 | } 14 | -------------------------------------------------------------------------------- /test/common/git/init-remote.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const os = require('os') 5 | const {promises: fs} = require('fs') 6 | const execa = require('execa') 7 | 8 | module.exports = initRemote 9 | 10 | async function initRemote(branch = 'main') { 11 | const cwd = await fs.mkdtemp(path.join(os.tmpdir(), path.sep)) 12 | await execa('git', [ 13 | 'init', '--bare', `--initial-branch=${branch}` 14 | ], {cwd: cwd}) 15 | return `file://${cwd}` 16 | } 17 | -------------------------------------------------------------------------------- /test/common/git/init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const os = require('os') 5 | const {promises: fs} = require('fs') 6 | const execa = require('execa') 7 | 8 | module.exports = init 9 | 10 | async function init(dir, branch = 'main') { 11 | const cwd = dir || await fs.mkdtemp(path.join(os.tmpdir(), path.sep)) 12 | await execa('git', ['init', cwd]) 13 | await execa('git', ['checkout', '-b', branch], {cwd: cwd}) 14 | await execa('git', ['config', '--add', 'commit.gpgsign', false]) 15 | await execa('git', ['config', '--add', 'pull.default', 'current'], {cwd}) 16 | await execa('git', ['config', '--add', 'push.default', 'current'], {cwd}) 17 | await execa('git', ['config', '--add', 'user.name', 'secretsquirrel'], {cwd}) 18 | await execa('git', ['config', '--add', 'user.email', 'secret@mail.com'], {cwd}) 19 | return cwd 20 | } 21 | -------------------------------------------------------------------------------- /test/common/git/push.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | 5 | module.exports = push 6 | 7 | async function push(cwd, remote = 'origin', branch = 'main') { 8 | await execa('git', ['push', '--tags', remote, `HEAD:${branch}`], {cwd: cwd}) 9 | } 10 | -------------------------------------------------------------------------------- /test/common/git/tag.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | 5 | module.exports = tag 6 | 7 | async function tag(cwd, name, hash) { 8 | const args = hash 9 | ? ['tag', '-f', name, hash] 10 | : ['tag', name] 11 | 12 | await execa('git', args, {cwd: cwd}) 13 | } 14 | -------------------------------------------------------------------------------- /test/common/git/tags.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const execa = require('execa') 5 | 6 | module.exports = tags 7 | 8 | async function tags(cwd, hash) { 9 | const cmd = hash 10 | ? ['describe', '--tags', '--exact-match', hash] 11 | : ['tag', '-l', '--sort', 'v:refname'] 12 | const {stdout} = await execa('git', cmd, {cwd: cwd}) 13 | return stdout.split(os.EOL).filter(Boolean) 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/docker/Dockerfile.post: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | COPY . /opt/app 3 | WORKDIR /opt/app 4 | CMD ["echo", "$PWD"] 5 | -------------------------------------------------------------------------------- /test/fixture/docker/Dockerfile.prepare: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | COPY . /opt/prepare 3 | WORKDIR /opt/prepare 4 | CMD ["echo", "$PWD"] 5 | -------------------------------------------------------------------------------- /test/fixture/docker/Dockerfile.publish: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim 2 | COPY . /opt/whizbang 3 | WORKDIR /opt/whizbang 4 | CMD ["echo", "$PWD"] 5 | -------------------------------------------------------------------------------- /test/fixture/docker/Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | COPY . /opt/app 3 | WORKDIR /opt/app 4 | CMD ["echo", "$PWD"] 5 | -------------------------------------------------------------------------------- /test/fixture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fake-scope/whizbangs" 3 | , "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/pkg/one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fixture/one", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/pkg/two/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esatterwhite/semantic-release-docker/c226324c4e0d0456f377e441d46b7eaaa3e42a84/test/fixture/pkg/two/package.json -------------------------------------------------------------------------------- /test/integration/multi-image-release.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {promisify} = require('util') 4 | const exec = promisify(require('child_process').exec) 5 | const sematicRelease = require('semantic-release') 6 | const execa = require('execa') 7 | const {WritableStreamBuffer} = require('stream-buffers') 8 | const {test, threw} = require('tap') 9 | const git = require('../common/git/index.js') 10 | const initOrigin = require('../common/git/init-origin.js') 11 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 12 | const stringify = JSON.stringify 13 | 14 | 15 | 16 | test('docker multiple image release', async (t) => { 17 | 18 | const stdout = new WritableStreamBuffer() 19 | const stderr = new WritableStreamBuffer() 20 | const cwd = t.testdir({ 21 | 'package.json': stringify({ 22 | name: 'service-meta-package' 23 | , version: '0.0.0-development' 24 | , scripts: { 25 | 'test-release': 'semantic-release' 26 | } 27 | , devDependencies: { 28 | 'semantic-release': '^19.0.0' 29 | , '@semantic-release/commit-analyzer': '^9' 30 | , '@semantic-release/release-notes-generator': '^10' 31 | , '@semantic-release/npm': '^9' 32 | , '@codedependant/semantic-release-docker': 'file:../../../' 33 | } 34 | }) 35 | , Dockerfile: 'FROM debian:buster-slim\n\nCMD ["whoami"]' 36 | , 'Dockerfile.alt': 'FROM debian:bullseye-slim\n\nCMD ["whoami"]' 37 | , '.gitignore': 'node_modules/' 38 | }) 39 | 40 | 41 | await git.init(cwd) 42 | t.comment('git repo initialized') 43 | await git.add(cwd) 44 | await git.commit(cwd, 'feat: initial release') 45 | 46 | const origin = await git.initOrigin(cwd) 47 | t.comment(`repository: ${cwd}`) 48 | t.comment(`origin: ${origin}`) 49 | 50 | await exec('npm install', { 51 | cwd: cwd 52 | }) 53 | 54 | const result = await sematicRelease({ 55 | ci: true 56 | , repositoryUrl: origin 57 | , npmPublish: false 58 | , branches: ['main'] 59 | , plugins: [ 60 | '@semantic-release/commit-analyzer' 61 | , '@semantic-release/release-notes-generator' 62 | , '@semantic-release/npm' 63 | , ['@codedependant/semantic-release-docker', { 64 | dockerRegistry: DOCKER_REGISTRY_HOST 65 | , dockerProject: 'docker-release' 66 | , dockerImage: 'abcd' 67 | , dockerArgs: { 68 | SAMPLE_THING: '{{type}}.{{version}}' 69 | , GIT_REF: '{{git_sha}}-{{git_tag}}' 70 | , BUILD_DATE: '{{now}}' 71 | } 72 | }] 73 | , ['@codedependant/semantic-release-docker', { 74 | dockerRegistry: DOCKER_REGISTRY_HOST 75 | , dockerProject: 'docker-release' 76 | , dockerImage: 'wxyz' 77 | , dockerFile: 'Dockerfile.alt' 78 | , dockerArgs: { 79 | SAMPLE_THING: '{{type}}.{{version}}' 80 | , GIT_REF: '{{git_sha}}-{{git_tag}}' 81 | , BUILD_DATE: '{{now}}' 82 | } 83 | 84 | }] 85 | ] 86 | }, { 87 | cwd: cwd 88 | , stdout: stdout 89 | , stderr: stderr 90 | , env: { 91 | ...process.env 92 | , BRANCH_NAME: 'main' 93 | , CI_BRANCH: 'main' 94 | , CI: 'true' 95 | , GITHUB_REF: 'refs/heads/main' 96 | , DOCKER_REGISTRY_USER: 'iamweasel' 97 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 98 | } 99 | }) 100 | 101 | t.type(result, Object, 'release object returned from release process') 102 | 103 | const tag = result.nextRelease.version 104 | const images = [ 105 | `${DOCKER_REGISTRY_HOST}/docker-release/abcd` 106 | , `${DOCKER_REGISTRY_HOST}/docker-release/wxyz` 107 | ] 108 | 109 | for (const image of images) { 110 | const expected = `${image}:${tag}` 111 | const {stdout} = await execa('docker', ['pull', expected, '-q']) 112 | t.equal(expected, stdout, `${expected} successfully published`) 113 | await execa('docker', ['rmi', expected]) 114 | } 115 | }).catch(threw) 116 | -------------------------------------------------------------------------------- /test/integration/post-publish.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const crypto = require('crypto') 5 | const path = require('path') 6 | const sinon = require('sinon') 7 | const execa = require('execa') 8 | const {test, threw} = require('tap') 9 | const buildConfig = require('../../lib/build-config.js') 10 | const verify = require('../../lib/verify.js') 11 | const prepare = require('../../lib/prepare.js') 12 | const publish = require('../../lib/publish.js') 13 | const success = require('../../lib/success.js') 14 | const fail = require('../../lib/fail.js') 15 | const fixturedir = path.join(__dirname, '..', 'fixture') 16 | 17 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 18 | 19 | 20 | const logger = { 21 | success: sinon.stub() 22 | , info: sinon.stub() 23 | , debug: sinon.stub() 24 | , fatal: sinon.stub() 25 | } 26 | test('post publish', async (t) => { 27 | t.test('success', async (t) => { 28 | const build_id = crypto.randomBytes(5).toString('hex') 29 | const context = { 30 | env: { 31 | ...process.env 32 | , DOCKER_REGISTRY_USER: 'iamweasel' 33 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 34 | } 35 | , cwd: fixturedir 36 | , nextRelease: {version: '2.0.0'} 37 | , lastRelease: {version: '1.5.0'} 38 | , logger: logger 39 | } 40 | 41 | const opts = { 42 | dockerRegistry: DOCKER_REGISTRY_HOST 43 | , dockerProject: `postpublish-${build_id}` 44 | , dockerImage: 'success' 45 | , dockerTags: ['{{major}}', '{{major}}.{{minor}}'] 46 | , dockerFile: 'docker/Dockerfile.post' 47 | , dockerAutoClean: false 48 | } 49 | 50 | const config = await buildConfig(build_id, opts, context) 51 | const auth = await verify(config, context) 52 | t.ok(auth, `authentication to ${DOCKER_REGISTRY_HOST} suceeds`) 53 | 54 | const image = await prepare(config, context) 55 | 56 | await publish(config, context) 57 | 58 | // remove build tag 59 | await execa('docker', [ 60 | 'rmi', image.name 61 | ]) 62 | 63 | { 64 | const {stdout} = await execa('docker', [ 65 | 'images', image.repo 66 | , '-q', '--format={{ .Tag }}' 67 | ]) 68 | const tags = stdout.split(os.EOL) 69 | 70 | t.same(tags, ['2', '2.0'], 'expect tags exists before succes stage') 71 | } 72 | 73 | 74 | await success(config, context) 75 | 76 | { 77 | const {stdout} = await execa('docker', [ 78 | 'images', image.repo 79 | , '-q', '--format={{ .Tag }}' 80 | ]) 81 | 82 | t.same( 83 | stdout.split(os.EOL).filter(Boolean) 84 | , ['2', '2.0'] 85 | , 'autoClean=false does not remove local tags with') 86 | } 87 | 88 | await success( 89 | await buildConfig(build_id, { 90 | ...opts 91 | , dockerAutoClean: true 92 | }, context) 93 | , context 94 | ) 95 | 96 | { 97 | const {stdout} = await execa('docker', [ 98 | 'images', image.repo 99 | , '-q', '--format={{ .Tag }}' 100 | ]) 101 | 102 | t.same( 103 | stdout.split(os.EOL).filter(Boolean) 104 | , [] 105 | , 'autoClean=true removes local tags with') 106 | } 107 | }) 108 | 109 | t.test('fail', async (t) => { 110 | const build_id = crypto.randomBytes(5).toString('hex') 111 | const context = { 112 | env: { 113 | ...process.env 114 | , DOCKER_REGISTRY_USER: 'iamweasel' 115 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 116 | } 117 | , cwd: fixturedir 118 | , nextRelease: {version: '3.0.0'} 119 | , lastRelease: {version: '2.0.0'} 120 | , logger: logger 121 | } 122 | 123 | const opts = { 124 | dockerRegistry: DOCKER_REGISTRY_HOST 125 | , dockerProject: `postpublish-${build_id}` 126 | , dockerImage: 'fail' 127 | , dockerTags: ['{{major}}', '{{major}}.{{minor}}'] 128 | , dockerFile: 'docker/Dockerfile.post' 129 | , dockerAutoClean: false 130 | } 131 | 132 | const config = await buildConfig(build_id, opts, context) 133 | const auth = await verify(config, context) 134 | t.ok(auth, `authentication to ${DOCKER_REGISTRY_HOST} suceeds`) 135 | 136 | const image = await prepare(config, context) 137 | 138 | await publish(config, context) 139 | 140 | // remove build tag 141 | await execa('docker', [ 142 | 'rmi', image.name 143 | ]) 144 | 145 | { 146 | const {stdout} = await execa('docker', [ 147 | 'images', image.repo 148 | , '-q', '--format={{ .Tag }}' 149 | ]) 150 | const tags = stdout.split(os.EOL) 151 | 152 | t.same(tags, ['3', '3.0'], 'expect tags exists before succes stage') 153 | } 154 | 155 | 156 | await fail(config, context) 157 | 158 | { 159 | const {stdout} = await execa('docker', [ 160 | 'images', image.repo 161 | , '-q', '--format={{ .Tag }}' 162 | ]) 163 | 164 | t.same( 165 | stdout.split(os.EOL).filter(Boolean) 166 | , ['3', '3.0'] 167 | , 'autoClean=false does not remove local tags with') 168 | } 169 | 170 | await fail( 171 | await buildConfig(build_id, { 172 | ...opts 173 | , dockerAutoClean: true 174 | }, context) 175 | , context 176 | ) 177 | 178 | { 179 | const {stdout} = await execa('docker', [ 180 | 'images', image.repo 181 | , '-q', '--format={{ .Tag }}' 182 | ]) 183 | 184 | t.same( 185 | stdout.split(os.EOL).filter(Boolean) 186 | , [] 187 | , 'autoClean=true removes local tags with') 188 | } 189 | }) 190 | }).catch(threw) 191 | -------------------------------------------------------------------------------- /test/integration/prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const crypto = require('crypto') 5 | const execa = require('execa') 6 | const {test, threw} = require('tap') 7 | const buildConfig = require('../../lib/build-config.js') 8 | const verify = require('../../lib/verify.js') 9 | const prepare = require('../../lib/prepare.js') 10 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 11 | const fixturedir = path.join(__dirname, '..', 'fixture') 12 | const DATE_REGEX = new RegExp( 13 | '^[\\d]{4}-[\\d]{2}-[\\d]{2}T[\\d]{2}:[\\d]{2}:[\\d]{2}' 14 | + '(\.[\\d]{1,6})?(Z|[\\+\\-][\\d]{2}:[\\d]{2})$' // eslint-disable-line no-useless-escape 15 | ) 16 | 17 | function noop() {} 18 | 19 | const logger = { 20 | success: noop 21 | , info: noop 22 | , debug: noop 23 | , fatal: noop 24 | } 25 | 26 | test('steps::prepare', async (t) => { 27 | t.test('build image created', async (tt) => { 28 | const build_id = crypto.randomBytes(5).toString('hex') 29 | const context = { 30 | env: { 31 | ...process.env 32 | , DOCKER_REGISTRY_USER: 'iamweasel' 33 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 34 | } 35 | , cwd: fixturedir 36 | , nextRelease: { 37 | version: '2.1.2' 38 | , gitTag: 'v2.1.2' 39 | , gitHead: 'abacadaba' 40 | } 41 | , logger: logger 42 | } 43 | 44 | const config = await buildConfig(build_id, { 45 | dockerRegistry: DOCKER_REGISTRY_HOST 46 | , dockerProject: 'docker-prepare' 47 | , dockerImage: 'fake' 48 | , dockerVerifyCmd: ['date', '+\'%x\''] 49 | , dockerBuildCacheFrom: 'test' 50 | , dockerArgs: { 51 | MY_VARIABLE: '1' 52 | , TAG_TEMPLATE: '{{git_tag}}' 53 | , MAJOR_TEMPLATE: '{{major}}' 54 | , GIT_REF: '{{git_sha}}' 55 | , BUILD_DATE: '{{now}}' 56 | } 57 | , dockerFile: 'docker/Dockerfile.prepare' 58 | , dockerContext: 'docker' 59 | }, {...context, dryRun: true}) 60 | 61 | tt.match( 62 | await verify(config, context) 63 | , /\d{2}\/\d{2}\/\d{2}/ 64 | , 'verify command executed' 65 | ) 66 | 67 | const image = await prepare(config, context) 68 | 69 | tt.on('end', () => { 70 | image.clean() 71 | }) 72 | 73 | tt.equal(image.opts.args.get('TAG_TEMPLATE'), 'v2.1.2', 'TAG_TEMPLATE value') 74 | tt.equal(image.opts.args.get('MAJOR_TEMPLATE'), '2', 'MAJOR_TEMPLATE value') 75 | tt.equal(image.opts.args.get('GIT_REF'), 'abacadaba', 'GIT_REF value') 76 | tt.match(image.opts.args.get('BUILD_DATE'), DATE_REGEX, 'BUILD_DATE value') 77 | tt.equal(image.context, path.join(context.cwd, config.context), 'docker context path') 78 | 79 | const {stdout} = await execa('docker', [ 80 | 'images', image.name 81 | , '-q', '--format={{ .Tag }}' 82 | ]) 83 | tt.equal(stdout, build_id, 'build image fully built') 84 | }) 85 | 86 | t.test('build image created - progress plain', async (tt) => { 87 | const build_id = crypto.randomBytes(5).toString('hex') 88 | const context = { 89 | env: { 90 | ...process.env 91 | , DOCKER_REGISTRY_USER: 'iamweasel' 92 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 93 | } 94 | , cwd: fixturedir 95 | , nextRelease: { 96 | version: '2.1.2' 97 | , gitTag: 'v2.1.2' 98 | , gitHead: 'abacadaba' 99 | } 100 | , logger: logger 101 | } 102 | 103 | const config = await buildConfig(build_id, { 104 | dockerRegistry: DOCKER_REGISTRY_HOST 105 | , dockerProject: 'docker-prepare' 106 | , dockerImage: 'alternate' 107 | , dockerBuildQuiet: false 108 | , dockerBuildFlags: { 109 | progress: 'plain' 110 | , 'no-cache': null 111 | } 112 | , dockerArgs: { 113 | MY_VARIABLE: '1' 114 | , TAG_TEMPLATE: '{{git_tag}}' 115 | , MAJOR_TEMPLATE: '{{major}}' 116 | , GIT_REF: '{{git_sha}}' 117 | , BUILD_DATE: '{{now}}' 118 | } 119 | , dockerFile: 'docker/Dockerfile.prepare' 120 | , dockerContext: 'docker' 121 | }, context) 122 | 123 | const image = await prepare(config, context) 124 | 125 | tt.on('end', () => { 126 | image.clean() 127 | }) 128 | 129 | tt.equal(image.opts.args.get('TAG_TEMPLATE'), 'v2.1.2', 'TAG_TEMPLATE value') 130 | tt.equal(image.opts.args.get('MAJOR_TEMPLATE'), '2', 'MAJOR_TEMPLATE value') 131 | tt.equal(image.opts.args.get('GIT_REF'), 'abacadaba', 'GIT_REF value') 132 | tt.match(image.opts.args.get('BUILD_DATE'), DATE_REGEX, 'BUILD_DATE value') 133 | tt.equal(image.context, path.join(context.cwd, config.context), 'docker context path') 134 | 135 | const {stdout} = await execa('docker', [ 136 | 'images', image.name 137 | , '-q', '--format={{ .Tag }}:{{ .ID}}' 138 | ]) 139 | const [tag, id] = stdout.split(':') 140 | tt.equal(image.id, id, 'captured id matches docker image id') 141 | tt.equal(tag, build_id, 'build image fully built') 142 | }) 143 | }).catch(threw) 144 | -------------------------------------------------------------------------------- /test/integration/publish.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const path = require('path') 5 | const execa = require('execa') 6 | const {test, threw} = require('tap') 7 | const buildConfig = require('../../lib/build-config.js') 8 | const verify = require('../../lib/verify.js') 9 | const prepare = require('../../lib/prepare.js') 10 | const publish = require('../../lib/publish.js') 11 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 12 | const fixturedir = path.join(__dirname, '..', 'fixture') 13 | 14 | function noop() {} 15 | 16 | const logger = { 17 | success: noop 18 | , info: noop 19 | , debug: noop 20 | , warn: noop 21 | , fatal: console.error 22 | } 23 | 24 | test('steps::publish', async (t) => { 25 | t.test('publish multi tags', async (tt) => { 26 | const build_id = crypto.randomBytes(5).toString('hex') 27 | const context = { 28 | env: { 29 | ...process.env 30 | , DOCKER_REGISTRY_USER: 'iamweasel' 31 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 32 | } 33 | , cwd: fixturedir 34 | , nextRelease: {version: '2.0.0'} 35 | , lastRelease: {version: '1.5.0'} 36 | , logger: logger 37 | } 38 | 39 | const config = await buildConfig(build_id, { 40 | dockerRegistry: DOCKER_REGISTRY_HOST 41 | , dockerProject: 'docker-publish' 42 | , dockerImage: 'real' 43 | , dockerTags: ['{{previous.major}}-previous', '{{major}}-foobar', '{{version}}'] 44 | , dockerFile: 'docker/Dockerfile.publish' 45 | }, context) 46 | 47 | const auth = await verify(config, context) 48 | tt.ok(auth, `authentication to ${DOCKER_REGISTRY_HOST} suceeds`) 49 | 50 | const image = await prepare(config, context) 51 | 52 | await publish(config, context) 53 | await image.clean() 54 | 55 | const tags = ['1-previous', '2-foobar', '2.0.0'] 56 | for (const tag of tags) { 57 | const expected = `${image.repo}:${tag}` 58 | const {stdout} = await execa('docker', ['pull', expected, '-q']) 59 | tt.equal(expected, stdout, `${expected} successfully published`) 60 | } 61 | }) 62 | 63 | t.test('publish multi tags', async (tt) => { 64 | const build_id = crypto.randomBytes(5).toString('hex') 65 | const context = { 66 | env: { 67 | ...process.env 68 | , DOCKER_REGISTRY_USER: 'iamweasel' 69 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 70 | } 71 | , cwd: fixturedir 72 | , dockerPlatform: ['linux/amd64'] 73 | , nextRelease: {version: '2.0.0'} 74 | , lastRelease: {version: '1.5.0'} 75 | , logger: logger 76 | } 77 | 78 | const config = await buildConfig(build_id, { 79 | dockerRegistry: DOCKER_REGISTRY_HOST 80 | , dockerProject: 'docker-publish' 81 | , dockerImage: 'real' 82 | , dockerTags: ['{{previous.major}}-previous', '{{major}}-foobar', '{{version}}'] 83 | , dockerFile: 'docker/Dockerfile.publish' 84 | }, context) 85 | 86 | const auth = await verify(config, context) 87 | tt.ok(auth, `authentication to ${DOCKER_REGISTRY_HOST} suceeds`) 88 | 89 | const image = await prepare(config, context) 90 | 91 | await publish(config, context) 92 | await image.clean() 93 | 94 | const tags = ['1-previous', '2-foobar', '2.0.0'] 95 | for (const tag of tags) { 96 | const expected = `${image.repo}:${tag}` 97 | const {stdout} = await execa('docker', ['pull', expected, '-q']) 98 | tt.equal(expected, stdout, `${expected} successfully published`) 99 | } 100 | }) 101 | }).catch(threw) 102 | -------------------------------------------------------------------------------- /test/integration/release.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const execa = require('execa') 4 | const {test, threw} = require('tap') 5 | const git = require('../common/git/index.js') 6 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 7 | 8 | const stringify = JSON.stringify 9 | 10 | test('docker release', async (t) => { 11 | const cwd = t.testdir({ 12 | 'package.json': stringify({ 13 | name: 'service-meta-package' 14 | , version: '0.0.0-development' 15 | , scripts: { 16 | 'test-release': 'semantic-release' 17 | } 18 | , release: { 19 | ci: true 20 | , npmPublish: false 21 | , branches: ['main'] 22 | , dockerRegistry: DOCKER_REGISTRY_HOST 23 | , dockerProject: 'docker-release' 24 | , dockerImage: 'fake' 25 | , dockerArgs: { 26 | SAMPLE_THING: '{{type}}.{{version}}' 27 | , GIT_REF: '{{git_sha}}-{{git_tag}}' 28 | , BUILD_DATE: '{{now}}' 29 | } 30 | , plugins: [ 31 | '@semantic-release/commit-analyzer' 32 | , '@semantic-release/release-notes-generator' 33 | , '@semantic-release/npm' 34 | , '@codedependant/semantic-release-docker' 35 | ] 36 | } 37 | , devDependencies: { 38 | 'semantic-release': '^19.0.0' 39 | , '@semantic-release/commit-analyzer': '^9' 40 | , '@semantic-release/release-notes-generator': '^10' 41 | , '@semantic-release/npm': '^9' 42 | , '@codedependant/semantic-release-docker': 'file:../../../' 43 | } 44 | }) 45 | , Dockerfile: 'FROM debian:buster-slim\n\nCMD ["whoami"]' 46 | , '.gitignore': 'node_modules/' 47 | }) 48 | 49 | await git.init(cwd) 50 | t.comment('git repo initialized') 51 | await git.add(cwd) 52 | await git.commit(cwd, 'feat: initial release') 53 | 54 | const origin = await git.initOrigin(cwd) 55 | t.comment(`repository: ${cwd}`) 56 | t.comment(`origin: ${origin}`) 57 | 58 | { 59 | const stream = execa('npm', [ 60 | 'install' 61 | ], { 62 | cwd: cwd 63 | , env: { 64 | BRANCH_NAME: 'main' 65 | , CI_BRANCH: 'main' 66 | , CI: 'true' 67 | , GITHUB_REF: 'refs/heads/main' 68 | } 69 | }) 70 | 71 | stream.stdout.pipe(process.stdout) 72 | await stream 73 | } 74 | 75 | const stream = execa('npm', [ 76 | 'run' 77 | , 'test-release' 78 | , `--repositoryUrl=${origin}`], { 79 | cwd: cwd 80 | , env: { 81 | BRANCH_NAME: 'main' 82 | , CI_BRANCH: 'main' 83 | , CI: 'true' 84 | , GITHUB_REF: 'refs/heads/main' 85 | , DOCKER_REGISTRY_USER: 'iamweasel' 86 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 87 | } 88 | }) 89 | stream.stdout.pipe(process.stdout) 90 | await stream 91 | 92 | }).catch(threw) 93 | 94 | test('buildx release', async (t) => { 95 | const cwd = t.testdir({ 96 | 'package.json': stringify({ 97 | name: 'service-meta-package' 98 | , version: '0.0.0-development' 99 | , scripts: { 100 | 'test-release': 'semantic-release --dry-run' 101 | } 102 | , release: { 103 | ci: true 104 | , npmPublish: false 105 | , branches: ['main'] 106 | , dockerRegistry: DOCKER_REGISTRY_HOST 107 | , dockerProject: 'docker-release' 108 | , dockerImage: 'fake' 109 | , dockerVerifyBuild: true 110 | , dockerArgs: { 111 | SAMPLE_THING: '{{type}}.{{version}}' 112 | , GIT_REF: '{{git_sha}}-{{git_tag}}' 113 | , BUILD_DATE: '{{now}}' 114 | } 115 | , plugins: [ 116 | '@semantic-release/commit-analyzer' 117 | , '@semantic-release/release-notes-generator' 118 | , '@semantic-release/npm' 119 | , '@codedependant/semantic-release-docker' 120 | ] 121 | } 122 | , devDependencies: { 123 | 'semantic-release': '^19.0.0' 124 | , '@semantic-release/commit-analyzer': '^9' 125 | , '@semantic-release/release-notes-generator': '^10' 126 | , '@semantic-release/npm': '^9' 127 | , '@codedependant/semantic-release-docker': 'file:../../../' 128 | } 129 | }) 130 | , Dockerfile: 'FROM debian:buster-slim\n\nCMD ["whoami"]' 131 | , '.gitignore': 'node_modules/' 132 | }) 133 | 134 | await git.init(cwd) 135 | t.comment('git repo initialized') 136 | await git.add(cwd) 137 | await git.commit(cwd, 'feat: initial release') 138 | 139 | const origin = await git.initOrigin(cwd) 140 | t.comment(`repository: ${cwd}`) 141 | t.comment(`origin: ${origin}`) 142 | 143 | { 144 | const stream = execa('npm', [ 145 | 'install' 146 | ], { 147 | cwd: cwd 148 | , env: { 149 | BRANCH_NAME: 'main' 150 | , CI_BRANCH: 'main' 151 | , CI: 'true' 152 | , GITHUB_REF: 'refs/heads/main' 153 | } 154 | }) 155 | 156 | stream.stdout.pipe(process.stdout) 157 | await stream 158 | } 159 | 160 | const stream = execa('npm', [ 161 | 'run' 162 | , 'test-release' 163 | , `--repositoryUrl=${origin}`], { 164 | cwd: cwd 165 | , env: { 166 | BRANCH_NAME: 'main' 167 | , CI_BRANCH: 'main' 168 | , CI: 'true' 169 | , GITHUB_REF: 'refs/heads/main' 170 | , DOCKER_REGISTRY_USER: 'iamweasel' 171 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 172 | } 173 | }) 174 | stream.stdout.pipe(process.stdout) 175 | await stream 176 | 177 | }).catch(threw) 178 | -------------------------------------------------------------------------------- /test/integration/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const sinon = require('sinon') 5 | const {test, threw} = require('tap') 6 | 7 | const buildConfig = require('../../lib/build-config.js') 8 | const verify = require('../../lib/verify.js') 9 | const DOCKER_REGISTRY_HOST = process.env.TEST_DOCKER_REGISTRY || 'localhost:5000' 10 | 11 | const logger = { 12 | success: sinon.stub() 13 | , debug: sinon.stub() 14 | , info: sinon.stub() 15 | , fatal: console.error 16 | } 17 | 18 | test('steps::verify', async (t) => { 19 | const build_id = crypto.randomBytes(5).toString('hex') 20 | t.test('docker password, no username', async (tt) => { 21 | const context = { 22 | env: { 23 | ...process.env 24 | , GITHUB_TOKEN: '' 25 | , DOCKER_REGISTRY_PASSWORD: 'abc123' 26 | } 27 | , cwd: process.cwd() 28 | , logger: logger 29 | } 30 | const config = await buildConfig(build_id, { 31 | dockerRegistry: DOCKER_REGISTRY_HOST 32 | }, context) 33 | 34 | await tt.rejects( 35 | verify(config, context) 36 | , { 37 | message: /docker authentication failed/i 38 | , code: 'EAUTH' 39 | , details: /DOCKER_REGISTRY_USER AND DOCKER_REGISTRY_PASSWORD must be set/ig 40 | } 41 | ) 42 | }) 43 | 44 | t.test('docker username, no password', async (tt) => { 45 | const context = { 46 | env: { 47 | ...process.env 48 | , GITHUB_TOKEN: '' 49 | , DOCKER_REGISTRY_USER: 'abc123' 50 | } 51 | , cwd: process.cwd() 52 | , logger: logger 53 | } 54 | 55 | const config = await buildConfig(build_id, { 56 | dockerRegistry: DOCKER_REGISTRY_HOST 57 | }, context) 58 | 59 | tt.rejects( 60 | verify(config, context) 61 | , { 62 | message: /docker authentication failed/i 63 | , code: 'EAUTH' 64 | , details: /DOCKER_REGISTRY_USER AND DOCKER_REGISTRY_PASSWORD must be set/ig 65 | } 66 | ) 67 | }) 68 | 69 | t.test('docker USER/PASS succeeds', async (tt) => { 70 | const context = { 71 | env: { 72 | ...process.env 73 | , DOCKER_REGISTRY_USER: 'iamweasel' 74 | , DOCKER_REGISTRY_PASSWORD: 'secretsquirrel' 75 | } 76 | , cwd: process.cwd() 77 | , logger: logger 78 | } 79 | const config = await buildConfig(build_id, { 80 | dockerRegistry: DOCKER_REGISTRY_HOST 81 | }, context) 82 | 83 | tt.resolves(verify(config, context)) 84 | }) 85 | 86 | t.test('docker USER / GITHUB_TOKEN succeeds', async (tt) => { 87 | const context = { 88 | env: { 89 | ...process.env 90 | , DOCKER_REGISTRY_USER: 'iamweasel' 91 | , GITHUB_TOKEN: 'secretsquirrel' 92 | } 93 | , cwd: process.cwd() 94 | , logger: logger 95 | } 96 | const config = await buildConfig(build_id, { 97 | dockerRegistry: DOCKER_REGISTRY_HOST 98 | }, context) 99 | tt.resolves(verify(config, context)) 100 | }) 101 | 102 | t.test('docker no login', async (tt) => { 103 | const context = { 104 | env: { 105 | ...process.env 106 | , DOCKER_REGISTRY_USER: 'iamweasel' 107 | } 108 | , cwd: process.cwd() 109 | , logger: logger 110 | } 111 | const config = await buildConfig(build_id, { 112 | dockerRegistry: DOCKER_REGISTRY_HOST 113 | , dockerLogin: false 114 | }, context) 115 | tt.resolves(verify(config, context)) 116 | }) 117 | 118 | t.test('docker password, no username', async (tt) => { 119 | const context = { 120 | env: { 121 | ...process.env 122 | , DOCKER_REGISTRY_PASSWORD: 'abc123' 123 | , DOCKER_REGISTRY_USER: 'abc123' 124 | } 125 | , cwd: process.cwd() 126 | , logger: logger 127 | } 128 | const config = await buildConfig(build_id, {}, context) 129 | tt.rejects( 130 | verify(config, context) 131 | , { 132 | message: /docker authentication failed/i 133 | , code: 'EAUTH' 134 | , details: /authentication to dockerhub failed/ig 135 | } 136 | ) 137 | }) 138 | 139 | t.test('No auth provided succeed', async (tt) => { 140 | const context = { 141 | env: { 142 | ...process.env 143 | , GITHUB_TOKEN: '' 144 | } 145 | , cwd: process.cwd() 146 | , logger: logger 147 | } 148 | 149 | const config = await buildConfig(build_id, {}, context) 150 | tt.strictEqual(await verify(config, context), true, 'auth step skipped') 151 | }) 152 | 153 | t.test('unable to collect image name', async (tt) => { 154 | const context = { 155 | env: { 156 | ...process.env 157 | } 158 | , cwd: __dirname 159 | , logger: logger 160 | } 161 | 162 | const config = await buildConfig(build_id, { 163 | dockerRegistry: DOCKER_REGISTRY_HOST 164 | }, context) 165 | tt.rejects(verify(config, context), { 166 | code: 'EINVAL' 167 | , name: 'SemanticReleaseError' 168 | , details: new RegExp( 169 | 'image name parsed from package.json name if possible. ' 170 | + 'or via the "dockerImage" option' 171 | , 'gi' 172 | ) 173 | }) 174 | }) 175 | 176 | t.test('invalid docker file location', async (tt) => { 177 | const context = { 178 | env: { 179 | ...process.env 180 | } 181 | , cwd: process.cwd() 182 | , logger: logger 183 | } 184 | 185 | const config = await buildConfig(build_id, { 186 | dockerRegistry: DOCKER_REGISTRY_HOST 187 | , dockerFile: 'Notafile' 188 | }, context) 189 | 190 | await tt.rejects(verify(config, context), { 191 | code: 'ENOENT' 192 | , name: 'SemanticReleaseError' 193 | , details: /relative to pwd/gi 194 | }) 195 | }) 196 | 197 | }).catch(threw) 198 | -------------------------------------------------------------------------------- /test/unit/build-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const {test, threw} = require('tap') 5 | const buildConfig = require('../../lib/build-config.js') 6 | 7 | test('build-config', async (t) => { 8 | t.testdir({ 9 | standard: { 10 | 'package.json': JSON.stringify({name: 'this-is-not-scoped'}) 11 | } 12 | , scoped: { 13 | 'package.json': JSON.stringify({name: '@scope/this-is-scoped'}) 14 | } 15 | , workspace: { 16 | one: { 17 | 'package.json': JSON.stringify({name: '@internal/package'}) 18 | } 19 | } 20 | }) 21 | 22 | t.test('standard package', async (tt) => { 23 | const config = await buildConfig('id', { 24 | }, { 25 | cwd: path.join(t.testdirName, 'standard') 26 | }) 27 | tt.match(config, { 28 | dockerfile: 'Dockerfile' 29 | , publish: true 30 | , nocache: false 31 | , tags: ['latest', '{{major}}-latest', '{{version}}'] 32 | , args: { 33 | SRC_DIRECTORY: 'standard' 34 | , TARGET_PATH: '.' 35 | , NPM_PACKAGE_NAME: 'this-is-not-scoped' 36 | , NPM_PACKAGE_SCOPE: null 37 | , CONFIG_NAME: 'this-is-not-scoped' 38 | , CONFIG_PROJECT: null 39 | } 40 | , pkg: Object 41 | , registry: null 42 | , name: 'this-is-not-scoped' 43 | , project: null 44 | , build: 'id' 45 | , context: '.' 46 | , quiet: true 47 | , clean: true 48 | , dry_run: false 49 | }) 50 | }) 51 | 52 | t.test('nested workspace: target resolution', async (tt) => { 53 | const config = await buildConfig('id', { 54 | dryRun: true 55 | }, { 56 | options: { 57 | root: t.testdirName 58 | } 59 | , cwd: path.join(t.testdirName, 'workspace', 'one') 60 | }) 61 | tt.match(config, { 62 | dockerfile: 'Dockerfile' 63 | , nocache: false 64 | , publish: true 65 | , tags: ['latest', '{{major}}-latest', '{{version}}'] 66 | , platform: [] 67 | , args: { 68 | SRC_DIRECTORY: 'one' 69 | , TARGET_PATH: 'workspace/one' 70 | , NPM_PACKAGE_NAME: 'package' 71 | , NPM_PACKAGE_SCOPE: 'internal' 72 | , CONFIG_NAME: 'package' 73 | , CONFIG_PROJECT: 'internal' 74 | } 75 | , pkg: Object 76 | , registry: null 77 | , name: 'package' 78 | , project: 'internal' 79 | , build: 'id' 80 | , context: '.' 81 | , quiet: true 82 | , dry_run: true 83 | , clean: true 84 | }) 85 | }) 86 | 87 | t.test('scoped package', async (tt) => { 88 | { 89 | const config = await buildConfig('id', { 90 | }, { 91 | cwd: path.join(t.testdirName, 'scoped') 92 | }) 93 | tt.match(config, { 94 | dockerfile: 'Dockerfile' 95 | , nocache: false 96 | , platform: [] 97 | , tags: ['latest', '{{major}}-latest', '{version}'] 98 | , args: { 99 | SRC_DIRECTORY: 'scoped' 100 | , TARGET_PATH: '.' 101 | , NPM_PACKAGE_NAME: '@scope/this-is-scoped' 102 | , NPM_PACKAGE_SCOPE: 'scope' 103 | , CONFIG_NAME: 'this-is-scoped' 104 | , CONFIG_PROJECT: 'scope' 105 | } 106 | , pkg: Object 107 | , registry: null 108 | , name: 'this-is-scoped' 109 | , project: 'scope' 110 | , build: 'id' 111 | , context: '.' 112 | , clean: true 113 | , quiet: true 114 | }) 115 | } 116 | 117 | { 118 | const config = await buildConfig('id', { 119 | dockerProject: 'kittens' 120 | , dockerImage: 'override' 121 | , dockerFile: 'Dockerfile.test' 122 | , dockerPublish: false 123 | , dockerPlatform: 'linux/amd64' 124 | , dockerBuildQuiet: 'false' 125 | }, { 126 | cwd: path.join(t.testdirName, 'scoped') 127 | }) 128 | tt.match(config, { 129 | dockerfile: 'Dockerfile.test' 130 | , publish: false 131 | , nocache: false 132 | , platform: ['linux/amd64'] 133 | , tags: ['latest', '{{major}}-latest', '{{version}}'] 134 | , args: { 135 | SRC_DIRECTORY: 'scoped' 136 | , TARGET_PATH: '.' 137 | , NPM_PACKAGE_NAME: '@scope/this-is-scoped' 138 | , NPM_PACKAGE_SCOPE: 'scope' 139 | , CONFIG_NAME: 'override' 140 | , CONFIG_PROJECT: 'kittens' 141 | } 142 | , pkg: Object 143 | , registry: null 144 | , name: 'override' 145 | , project: 'kittens' 146 | , build: 'id' 147 | , context: '.' 148 | , clean: true 149 | , quiet: false 150 | }) 151 | } 152 | 153 | { 154 | const config = await buildConfig('id', { 155 | dockerProject: null 156 | , dockerImage: 'override' 157 | , dockerFile: 'Dockerfile.test' 158 | , dockerTags: 'latest,{{major}}-latest , fake, {{version}}' 159 | , dockerAutoClean: false 160 | , dockerPlatform: ['linux/amd64', 'linux/arm64'] 161 | , dockerBuildQuiet: 'false' 162 | }, { 163 | cwd: path.join(t.testdirName, 'scoped') 164 | }) 165 | tt.match(config, { 166 | dockerfile: 'Dockerfile.test' 167 | , nocache: false 168 | , platform: ['linux/amd64', 'linux/arm64'] 169 | , tags: ['latest', '{{major}}-latest', 'fake', '{{version}}'] 170 | , args: { 171 | SRC_DIRECTORY: 'scoped' 172 | , TARGET_PATH: '.' 173 | , NPM_PACKAGE_NAME: '@scope/this-is-scoped' 174 | , NPM_PACKAGE_SCOPE: 'scope' 175 | , CONFIG_NAME: 'override' 176 | , CONFIG_PROJECT: null 177 | } 178 | , pkg: Object 179 | , registry: null 180 | , name: 'override' 181 | , project: null 182 | , build: 'id' 183 | , context: '.' 184 | , quiet: false 185 | , clean: false 186 | }) 187 | } 188 | }) 189 | }).catch(threw) 190 | -------------------------------------------------------------------------------- /test/unit/build-template-vars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const buildConfig = require('../../lib/build-config.js') 5 | const buildTemplateVars = require('../../lib/build-template-vars.js') 6 | 7 | test('buildTemplateVars', async (t) => { 8 | const cwd = t.testdir({ 9 | 'package.json': JSON.stringify({ 10 | name: 'template-vars' 11 | }) 12 | }) 13 | const context = { 14 | cwd 15 | , nextRelease: { 16 | version: '1.0.0' 17 | , gitTag: 'v1.0.0' 18 | , gitHead: 'abcdefgh' 19 | , type: 'major' 20 | , notes: 'test it' 21 | } 22 | } 23 | const opts = await buildConfig('abacadaba', { 24 | dockerArgs: { 25 | TEMPLATE_VALUE: '{{type}}.{{version}}' 26 | , BOOLEAN_VALUE: true 27 | , NULL_VALUE: null 28 | } 29 | }, context) 30 | 31 | const vars = buildTemplateVars(opts, context) 32 | t.match(vars, { 33 | release_type: 'major' 34 | , release_notes: 'test it' 35 | , version: '1.0.0' 36 | , git_sha: 'abcdefgh' 37 | , git_tag: 'v1.0.0' 38 | , pkg: {name: 'template-vars'} 39 | , major: 1 40 | , minor: 0 41 | , patch: 0 42 | , version: '1.0.0' 43 | , network: 'default' 44 | , next: { 45 | major: 1 46 | , minor: 0 47 | , patch: 0 48 | , version: '1.0.0' 49 | } 50 | , args: { 51 | SRC_DIRECTORY: String 52 | , TARGET_PATH: String 53 | , NPM_PACKAGE_NAME: 'template-vars' 54 | , NPM_PACKAGE_SCOPE: null 55 | , CONFIG_NAME: String 56 | , CONFIG_PROJECT: null 57 | , GIT_SHA: 'abcdefgh' 58 | , GIT_TAG: 'v1.0.0' 59 | , TEMPLATE_VALUE: '{{type}}.{{version}}' 60 | , BOOLEAN_VALUE: true 61 | , NULL_VALUE: null 62 | } 63 | }, 'expected template values') 64 | }).catch(threw) 65 | -------------------------------------------------------------------------------- /test/unit/docker/image.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('os') 4 | const path = require('path') 5 | const crypto = require('crypto') 6 | const execa = require('execa') 7 | const {test, threw} = require('tap') 8 | const docker = require('../../../lib/docker/index.js') 9 | 10 | function random() { 11 | return crypto.randomBytes(5).toString('hex') 12 | } 13 | 14 | const fixturedir = path.join(__dirname, '..', '..', 'fixture') 15 | test('exports', async (tt) => { 16 | tt.match(docker, { 17 | Image: Function 18 | }, 'expected exports') 19 | }) 20 | 21 | test('Image', async (t) => { 22 | const build_id = random() 23 | t.throws(() => { 24 | return new docker.Image() 25 | }, /docker image "name" is required and must be a string/gi) 26 | 27 | t.test('image defaults', async (tt) => { 28 | const img = new docker.Image({name: 'test'}) 29 | tt.notOk(img.sha, 'No default sha generated') 30 | tt.match(img.opts, { 31 | build_id: String 32 | , registry: null 33 | , project: null 34 | , name: 'test' 35 | , dockerfile: 'Dockerfile' 36 | , cwd: process.cwd() 37 | , flags: Map 38 | , args: Map 39 | }, 'default image options') 40 | }) 41 | 42 | test('image overrides', async (tt) => { 43 | const img = new docker.Image({ 44 | name: 'test' 45 | , build_id: 'abc123' 46 | , registry: 'quay.io' 47 | , project: 'esatterwhite' 48 | , name: 'test' 49 | , dockerfile: 'Dockerfile.test' 50 | , cwd: path.join(__dirname, 'build') 51 | , sha: 'hello' 52 | , network: 'custom-network' 53 | }) 54 | tt.equal(img.sha, 'hello') 55 | tt.match(img.opts, { 56 | build_id: 'abc123' 57 | , registry: 'quay.io' 58 | , project: 'esatterwhite' 59 | , name: 'test' 60 | , dockerfile: 'Dockerfile.test' 61 | , cwd: path.join(__dirname, 'build') 62 | , flags: Map 63 | , args: Map 64 | , network: 'custom-network' 65 | }, 'default image options') 66 | 67 | tt.equal(img.context, path.join(__dirname, 'build', '.'), 'default image context') 68 | img.context = __dirname 69 | tt.equal(img.context, __dirname, 'context override') 70 | tt.equal( 71 | img.dockerfile 72 | , path.join(__dirname, 'build', 'Dockerfile.test') 73 | , 'docker file path' 74 | ) 75 | }) 76 | 77 | t.test('dockerfile location resolution', async (t) => { 78 | t.test('relative location', async (t) => { 79 | const img = new docker.Image({ 80 | name: 'test' 81 | , build_id: 'abc123' 82 | , registry: 'quay.io' 83 | , project: 'esatterwhite' 84 | , name: 'test' 85 | , dockerfile: '../../Dockerfile.test' 86 | , cwd: path.join(__dirname, 'build') 87 | , sha: 'hello' 88 | }) 89 | t.equal( 90 | img.dockerfile 91 | , path.join(__dirname, '..', 'Dockerfile.test') 92 | , 'docker file path' 93 | ) 94 | }) 95 | 96 | t.test('absolute path', async (t) => { 97 | const img = new docker.Image({ 98 | name: 'test' 99 | , build_id: 'abc123' 100 | , registry: 'quay.io' 101 | , project: 'esatterwhite' 102 | , name: 'test' 103 | , dockerfile: '/var/opt/Dockerfile.test' 104 | , cwd: path.join(__dirname, 'build') 105 | , sha: 'hello' 106 | }) 107 | t.equal( 108 | img.dockerfile 109 | , '/var/opt/Dockerfile.test' 110 | , 'docker file path' 111 | ) 112 | }) 113 | }) 114 | 115 | t.test('image#id()', async (tt) => { 116 | { 117 | const img = new docker.Image({ 118 | name: 'test' 119 | , build_id: 'abc123' 120 | }) 121 | 122 | tt.equal(img.id, 'test:abc123') 123 | } 124 | 125 | { 126 | const img = new docker.Image({ 127 | name: 'test' 128 | , sha: 'abcdefg123456' 129 | , registry: 'quay.io' 130 | , project: 'esatterwhite' 131 | , build_id: 'abacadaba' 132 | }) 133 | 134 | tt.equal(img.id, 'abcdefg123456', 'sha value used as image id') 135 | } 136 | }) 137 | 138 | t.test('image#repo', async (tt) => { 139 | { 140 | const img = new docker.Image({ 141 | name: 'test' 142 | }) 143 | 144 | tt.equal(img.repo, 'test') 145 | } 146 | 147 | { 148 | const img = new docker.Image({ 149 | name: 'foobar' 150 | , registry: 'us.gcr.io' 151 | }) 152 | 153 | tt.equal(img.repo, 'us.gcr.io/foobar') 154 | } 155 | 156 | { 157 | const img = new docker.Image({ 158 | name: 'test' 159 | , registry: 'quay.io' 160 | , project: 'esatterwhite' 161 | }) 162 | 163 | tt.equal(img.repo, 'quay.io/esatterwhite/test', 'generated repo name') 164 | } 165 | }) 166 | 167 | t.test('image#build_cmd (docker build)', async (tt) => { 168 | { 169 | const img = new docker.Image({ 170 | name: 'test' 171 | , registry: 'quay.io' 172 | , project: 'esatterwhite' 173 | , build_id: 'abacadaba' 174 | }) 175 | 176 | tt.same(img.build_cmd, [ 177 | 'build' 178 | , '--network=default' 179 | , '--tag' 180 | , 'quay.io/esatterwhite/test:abacadaba' 181 | , '--quiet' 182 | , '-f' 183 | , path.join(process.cwd(), 'Dockerfile') 184 | , process.cwd() 185 | ], 'build command') 186 | } 187 | 188 | { 189 | const img = new docker.Image({ 190 | name: 'foobar' 191 | , registry: 'us.gcr.io' 192 | , project: 'esatterwhite' 193 | , build_id: '1010101' 194 | , cwd: __dirname 195 | , context: path.join(__dirname, 'fake') 196 | }) 197 | 198 | tt.same(img.build_cmd, [ 199 | 'build' 200 | , '--network=default' 201 | , '--tag' 202 | , 'us.gcr.io/esatterwhite/foobar:1010101' 203 | , '--quiet' 204 | , '-f' 205 | , path.join(__dirname, 'Dockerfile') 206 | , path.join(__dirname, 'fake') 207 | ], 'build command') 208 | } 209 | 210 | { 211 | const img = new docker.Image({ 212 | name: 'foobar' 213 | , registry: 'us.gcr.io' 214 | , project: 'esatterwhite' 215 | , build_id: '1010101' 216 | , cwd: __dirname 217 | , context: path.join(__dirname, 'fake') 218 | }) 219 | 220 | img.arg('ARG_1', 'yes') 221 | img.arg('VALUE_FROM_ENV', true) 222 | tt.same(img.build_cmd, [ 223 | 'build' 224 | , '--network=default' 225 | , '--tag' 226 | , 'us.gcr.io/esatterwhite/foobar:1010101' 227 | , '--quiet' 228 | , '--build-arg' 229 | , 'ARG_1=yes' 230 | , '--build-arg' 231 | , 'VALUE_FROM_ENV' 232 | , '-f' 233 | , path.join(__dirname, 'Dockerfile') 234 | , path.join(__dirname, 'fake') 235 | ], 'build command') 236 | } 237 | { 238 | const img = new docker.Image({ 239 | name: 'foobar' 240 | , registry: 'us.gcr.io' 241 | , project: 'esatterwhite' 242 | , build_id: '1010101' 243 | , cwd: __dirname 244 | , context: path.join(__dirname, 'fake') 245 | , quiet: false 246 | }) 247 | 248 | img.arg('ARG_2', 'no') 249 | img.arg('VALUE_FROM_ENV', true) 250 | tt.same(img.build_cmd, [ 251 | 'build' 252 | , '--network=default' 253 | , '--tag' 254 | , 'us.gcr.io/esatterwhite/foobar:1010101' 255 | , '--build-arg' 256 | , 'ARG_2=no' 257 | , '--build-arg' 258 | , 'VALUE_FROM_ENV' 259 | , '-f' 260 | , path.join(__dirname, 'Dockerfile') 261 | , path.join(__dirname, 'fake') 262 | ], 'build command - quiet = false') 263 | } 264 | }) 265 | 266 | t.test('image#buildx_cmd', async (t) => { 267 | { 268 | const img = new docker.Image({ 269 | name: 'foobar' 270 | , registry: 'us.gcr.io' 271 | , project: 'esatterwhite' 272 | , build_id: '1010101' 273 | , cwd: __dirname 274 | , tags: ['2.0.0', '2-latest'] 275 | , platform: ['linux/amd64'] 276 | , context: path.join(__dirname, 'fake') 277 | }) 278 | 279 | img.arg('ARG_2', 'no') 280 | img.arg('VALUE_FROM_ENV', true) 281 | t.same(img.build_cmd, [ 282 | 'buildx' 283 | , 'build' 284 | , '--network=default' 285 | , '--quiet' 286 | , '--tag' 287 | , 'us.gcr.io/esatterwhite/foobar:2.0.0' 288 | , '--tag' 289 | , 'us.gcr.io/esatterwhite/foobar:2-latest' 290 | , '--build-arg' 291 | , 'ARG_2=no' 292 | , '--build-arg' 293 | , 'VALUE_FROM_ENV' 294 | , '--platform' 295 | , 'linux/amd64' 296 | , '--pull' 297 | , '--push' 298 | , '-f' 299 | , path.join(__dirname, 'Dockerfile') 300 | , path.join(__dirname, 'fake') 301 | ], 'buildx command') 302 | } 303 | 304 | { 305 | const img = new docker.Image({ 306 | name: 'foobar' 307 | , registry: 'us.gcr.io' 308 | , project: 'esatterwhite' 309 | , build_id: '1010101' 310 | , cwd: __dirname 311 | , tags: ['2.0.0', '2-latest'] 312 | , platform: ['linux/amd64'] 313 | , context: path.join(__dirname, 'fake') 314 | , dry_run: true 315 | }) 316 | 317 | img.arg('ARG_2', 'no') 318 | img.arg('VALUE_FROM_ENV', true) 319 | t.same(img.build_cmd, [ 320 | 'buildx' 321 | , 'build' 322 | , '--network=default' 323 | , '--quiet' 324 | , '--build-arg' 325 | , 'ARG_2=no' 326 | , '--build-arg' 327 | , 'VALUE_FROM_ENV' 328 | , '--platform' 329 | , 'linux/amd64' 330 | , '--pull' 331 | , '-f' 332 | , path.join(__dirname, 'Dockerfile') 333 | , path.join(__dirname, 'fake') 334 | ], 'buildx command') 335 | } 336 | }) 337 | 338 | t.test('image#build()', async (tt) => { 339 | const img = new docker.Image({ 340 | name: 'test' 341 | , registry: 'quay.io' 342 | , project: 'esatterwhite' 343 | , build_id: build_id 344 | , cwd: fixturedir 345 | , dockerfile: path.join('docker', 'Dockerfile.test') 346 | , context: path.join(fixturedir, 'docker') 347 | }) 348 | 349 | tt.notOk(img.sha, 'no show before build') 350 | const sha = await img.build() 351 | tt.ok(sha, 'sha value returned from build function') 352 | tt.equal(sha, img.sha, 'image sha set after build') 353 | const {stdout} = await execa('docker', ['run', '--rm', img.name, 'ls', '-1']) 354 | tt.same( 355 | stdout.split(os.EOL).sort() 356 | , [ 357 | 'Dockerfile.test' 358 | , 'Dockerfile.publish' 359 | , 'Dockerfile.prepare' 360 | , 'Dockerfile.post' 361 | ].sort() 362 | , 'files in image context') 363 | }) 364 | 365 | t.test('image#tag()', async (tt) => { 366 | const img = new docker.Image({ 367 | name: 'test' 368 | , registry: 'quay.io' 369 | , project: 'esatterwhite' 370 | , build_id: build_id 371 | , cwd: __dirname 372 | , dockerfile: path.join(fixturedir, 'docker', 'Dockerfile.test') 373 | , context: path.join(__dirname, 'fixture') 374 | }) 375 | await img.tag('1.0.0', false) 376 | const {stdout} = await execa('docker', [ 377 | 'images', img.repo 378 | , '-q', '--format={{ .Tag }}' 379 | ]) 380 | const tags = stdout.split(os.EOL) 381 | tt.same(tags.sort(), [build_id, '1.0.0'].sort(), 'image tags') 382 | }) 383 | 384 | t.test('image#clean()', async (tt) => { 385 | const img = new docker.Image({ 386 | name: 'test' 387 | , registry: 'quay.io' 388 | , project: 'esatterwhite' 389 | , build_id: build_id 390 | , cwd: __dirname 391 | , dockerfile: path.join('fixture', 'Dockerfile.test') 392 | }) 393 | await img.clean() 394 | const {stdout} = await execa('docker', [ 395 | 'images', img.repo 396 | , '-q', '--format={{ .Tag }}' 397 | ]) 398 | tt.same(stdout, '', 'all tags removed') 399 | 400 | tt.resolves(img.clean(), 'does not throw when no matching images found') 401 | }) 402 | 403 | t.test('image.context', async (tt) => { 404 | const img = new docker.Image({ 405 | name: 'test' 406 | , registry: 'quay.io' 407 | , project: 'esatterwhite' 408 | , build_id: build_id 409 | , cwd: __dirname 410 | , dockerfile: path.join('fixture', 'Dockerfile.test') 411 | , context: 'fixture' 412 | }) 413 | 414 | tt.equal(img.context, path.join(__dirname, 'fixture'), 'expected image context') 415 | }) 416 | 417 | }).catch(threw) 418 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/endswith.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {endswith} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('endswith', async (t) => { 8 | t.equal(endswith('foobar', 'bar'), true, 'foobar endswith bar') 9 | t.equal(endswith('foobar', 'foo'), false, 'foobar not endswith foo') 10 | t.equal(endswith([], 'Array]'), false, 'false with not a string') 11 | }) 12 | }).catch(threw) 13 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/eq.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {eq} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('eq', async (t) => { 8 | t.equal(eq(30, 30), true, '30 is equal to 30)') 9 | t.equal(eq(40, 30), false, '40 is not equal to 30)') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/gt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {gt} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('gt', async (t) => { 8 | t.equal(gt(40, 30), true, '40 is greater than 30)') 9 | t.equal(gt(30, 40), false, '30 is not greater than 40)') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/gte.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {gte} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('gte', async (t) => { 8 | t.equal(gte(40, 30), true, '40 is greater than or equal to 30)') 9 | t.equal(gte(30, 40), false, '30 is not greater than or equal to 40)') 10 | t.equal(gte(50, 40), true, '40 is greater than or equal to 40)') 11 | }) 12 | }).catch(threw) 13 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/includes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const helpers = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('includes', async (t) => { 8 | t.equal( 9 | helpers.includes(undefined, undefined, undefined) 10 | , false 11 | , 'includes returns false for undefined inputs' 12 | ) 13 | 14 | t.equal( 15 | helpers.includes(['ca', 'eu', 'eq', 'us']) 16 | , false 17 | , 'includes returns false when valueToFind and indexFrom are undefined' 18 | ) 19 | 20 | t.equal( 21 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'ca') 22 | , true 23 | , 'ca in ["ca","eu","eq","us"]' 24 | ) 25 | 26 | t.equal( 27 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'ca', 0) 28 | , true 29 | , 'ca in ["ca","eu","eq","us"]' 30 | ) 31 | 32 | t.equal( 33 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'us', 0) 34 | , true 35 | , 'us in ["ca","eu","eq","us"]' 36 | ) 37 | 38 | t.equal( 39 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'CA', 0) 40 | , false 41 | , 'ca not in ["ca","eu","eq","us"]' 42 | ) 43 | 44 | t.equal( 45 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'cA', 0) 46 | , false 47 | , 'ca not in ["ca","eu","eq","us"]' 48 | ) 49 | 50 | t.equal( 51 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'ca', 1) 52 | , false 53 | , 'ca not in ["ca","eu","eq","us"] if when indexFrom = 1' 54 | ) 55 | 56 | t.equal( 57 | helpers.includes(['ca', 'eu', 'eq', 'us'], 'ca', -4) 58 | , true 59 | , 'ca in ["ca","eu","eq","us"] when indexFrom < 0' 60 | ) 61 | 62 | t.equal( 63 | helpers.includes([], 'us', 0) 64 | , false 65 | , 'us not in []' 66 | ) 67 | 68 | t.equal( 69 | helpers.includes([3, 4, 50, 100], undefined, undefined) 70 | , false 71 | , 'includes returns false when valueToFind and indexFrom are undefined' 72 | ) 73 | 74 | t.equal( 75 | helpers.includes([3, 4, 50, 100], 4, undefined) 76 | , true 77 | , '4 in [3,4,50,100]' 78 | ) 79 | 80 | t.equal( 81 | helpers.includes([3, 4, 50, 100], 3, 0) 82 | , true 83 | , '3 in [3,4,50,100]' 84 | ) 85 | 86 | t.equal( 87 | helpers.includes([1000, 0, 235, 65, 5], 235, 0) 88 | , true 89 | , '235 in [1000,0,235,65,5]' 90 | ) 91 | 92 | t.equal( 93 | helpers.includes([1000, 0, 235, 65, 5], 5, 0) 94 | , true 95 | , '5 in [1000,0,235,65,5]' 96 | ) 97 | 98 | t.equal( 99 | helpers.includes([1000, 0, 235, 65, 78], 77, 0) 100 | , false 101 | , '77 not in [1000,0,235,65,78]' 102 | ) 103 | 104 | t.equal( 105 | helpers.includes([3, 4, 50, 100], 4, 2) 106 | , false 107 | , '4 not in [3,4,50,100] when indexFrom = 2' 108 | ) 109 | 110 | t.equal( 111 | helpers.includes([3, 4, 50, 100], 3, -4) 112 | , true 113 | , '3 in [3,4,50,100] when indexFrom < 0' 114 | ) 115 | 116 | t.equal( 117 | helpers.includes([], 250, 0) 118 | , false 119 | , '250 not in []' 120 | ) 121 | 122 | t.equal( 123 | helpers.includes([11, 35, 80, 120], 80, 4) 124 | , false 125 | , '80 in [11,35,80,120] but indexFrom = arr.length' 126 | ) 127 | 128 | t.equal( 129 | helpers.includes([11, 35, 80, 120], 11, 5) 130 | , false 131 | , '11 in [11,35,80,120] but indexFrom > arr.length' 132 | ) 133 | }) 134 | }).catch(threw) 135 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | const {test, threw} = require('tap') 5 | const hbs = require('../../../../lib/handlebars') 6 | const helpers = require('../../../../lib/handlebars/helpers') 7 | 8 | test('handlebars helpers', async (t) => { 9 | for (const [name, helper] of Object.entries(helpers)) { 10 | t.type(hbs.helpers[name], 'function', `handle bars has helper named ${name}`) 11 | t.equal(hbs.helpers[name], helper, `handlers function reference ${name} loaded`) 12 | } 13 | }).catch(threw) 14 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/lower.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {lower} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('lower', async (t) => { 8 | t.equal(lower('Hello World'), 'hello world', 'lower cases input') 9 | t.equal(lower(null), '', 'returns empty string on non string input') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/lt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {lt} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('lt', async (t) => { 8 | t.equal(lt(40, 30), false, '40 is lower than 30)') 9 | t.equal(lt(30, 40), true, '30 is not lower than 40)') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/lte.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {lte} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('gte', async (t) => { 8 | t.equal(lte(30, 40), true, '30 is less than or equal to 40') 9 | t.equal(lte(40, 30), false, '40 is not less than or equal to 30') 10 | t.equal(lte(50, 50), false, '50 is less than or equal to 50)') 11 | }) 12 | }).catch(threw) 13 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/neq.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {neq} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('neq', async (t) => { 8 | t.equal(neq(30, 30), false, '30 is not equal to 30)') 9 | t.equal(neq(40, 30), true, '40 is equal to 30)') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/pick.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {pick} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('pick', async (t) => { 8 | t.deepEqual(pick(null, undefined, []), [], 'Should return empty array') 9 | t.deepEqual(pick(null, undefined, '', [], 100), [], 'Should return empty array') 10 | t.equal(pick(null, 0, undefined, []), 0, 'Should return 0') 11 | t.equal(pick(null, undefined, 'null', []), 'null', 'Should return "null"') 12 | }) 13 | }).catch(threw) 14 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/split.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {split} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | const ret = split('10.10.10.10:400', ':') 8 | t.deepEqual('10.10.10.10', ret[0], 'ip address is 10.10.10.10') 9 | t.deepEqual('400', ret[1], 'port is 400') 10 | 11 | t.deepEqual( 12 | split('10.10.10.10:400', '') 13 | , ['1', '0', '.', '1', '0', '.', '1', '0', '.', '1', '0', ':', '4', '0', '0'] 14 | , 'should split on every character' 15 | ) 16 | 17 | t.deepEqual( 18 | split('10.10.10.10:400', 10) 19 | , ['', '.', '.', '.', ':400'] 20 | , 'split on 10' 21 | ) 22 | 23 | t.deepEqual( 24 | split(1000, '') 25 | , [1000] 26 | , 'there is no split' 27 | ) 28 | 29 | t.deepEqual( 30 | split('10.10.10.10:400', undefined) 31 | , ['10.10.10.10:400'] 32 | , 'there is no split' 33 | ) 34 | 35 | t.deepEqual( 36 | split(undefined, ':') 37 | , [] 38 | , 'there is no split' 39 | ) 40 | }).catch(threw) 41 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/startswith.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {startswith} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('startswith', async (t) => { 8 | t.equal(startswith('foobar', 'bar'), false, 'foobar not startswith bar') 9 | t.equal(startswith('foobar', 'foo'), true, 'foobar startswith foo') 10 | t.equal(startswith([], 'Array]'), false, 'false with not a string') 11 | }) 12 | }).catch(threw) 13 | -------------------------------------------------------------------------------- /test/unit/handlebars/helpers/upper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const {upper} = require('../../../../lib/handlebars/helpers') 5 | 6 | test('handlebars helpers', async (t) => { 7 | t.test('upper', async (t) => { 8 | t.equal(upper('Hello World'), 'HELLO WORLD', 'upper cases input') 9 | t.equal(upper(null), '', 'returns empty string on non string input') 10 | }) 11 | }).catch(threw) 12 | -------------------------------------------------------------------------------- /test/unit/lang/array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const array = require('../../../lib/lang/array/index.js') 5 | 6 | test('array', async (t) => { 7 | t.test('toArray', async (t) => { 8 | const cases = [ 9 | {value: undefined, expected: [], message: 'toArray(undefined) == []'} 10 | , {value: null, expected: [], message: 'toArray(null) == []'} 11 | , {value: 1, expected: [1], message: 'toArray(1) == [1]'} 12 | , {value: '', expected: [], message: 'toArray(\'\') == []'} 13 | , {value: 'test', expected: ['test']} 14 | , {value: '1,2,3', expected: ['1', '2', '3']} 15 | , {value: '1, 2, 3', expected: ['1', '2', '3']} 16 | , {value: '1, 2, 3', expected: ['1', ' 2', ' 3'], sep: ','} 17 | , {value: '1|2|3', expected: ['1', '2', '3'], sep: '|'} 18 | , {value: [1, 2, 3], expected: [1, 2, 3]} 19 | , {value: new Set([1, null, 'test']), expected: [1, null, 'test']} 20 | ] 21 | for (const current of cases) { 22 | const args = [current.value] 23 | if (current.sep) { 24 | args.push(current.sep) 25 | } 26 | 27 | t.deepEqual( 28 | array.toArray(...args) 29 | , current.expected 30 | , current.message || `toArray(${current.value}) == ${current.expected}` 31 | ) 32 | } 33 | }) 34 | }).catch(threw) 35 | -------------------------------------------------------------------------------- /test/unit/lang/object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const object = require('../../../lib/lang/object/index.js') 5 | 6 | test('object', async (t) => { 7 | const obj = { 8 | foo: { 9 | bar: { 10 | baz: { 11 | key: 'value' 12 | , test: null 13 | } 14 | } 15 | , value: 'foobar' 16 | , array: [1, 2, 3] 17 | } 18 | , bar: 0 19 | , baz: false 20 | , biff: null 21 | } 22 | 23 | t.test('get', async (tt) => { 24 | tt.match(object.get(obj, 'foo.bar'), { 25 | baz: { 26 | key: 'value' 27 | , test: null 28 | } 29 | }) 30 | tt.deepEqual(object.get(obj, 'foo.array'), [1, 2, 3], 'foo.array') 31 | tt.equal(object.get(obj, 'foo.bar.baz.key'), 'value', 'foo.bar.baz.key') 32 | tt.equal(object.get(obj, 'foo|bar|baz|key', '|'), 'value', 'foo.bar.baz.key') 33 | tt.equal(object.get(obj, 'does.not.exist'), undefined, 'does.not.exist') 34 | tt.strictEqual(object.get(obj, 'foo.bar.baz.test'), null, 'foo.bar.baz.test') 35 | tt.strictEqual(object.get(null, 'foo.bar.baz'), null, 'null input') 36 | }) 37 | 38 | t.test('has', async (t) => { 39 | t.ok(object.has(obj, 'bar'), 'key found') 40 | t.ok(object.has(obj, 'biff'), 'has key w/ null value') 41 | t.false(object.has(obj, 'whizbang'), 'key not found') 42 | t.false(object.has(undefined, 'whizbang'), 'undefined object') 43 | 44 | }) 45 | }).catch(threw) 46 | -------------------------------------------------------------------------------- /test/unit/lang/string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const string = require('../../../lib/lang/string/index.js') 5 | 6 | test('string', async (t) => { 7 | t.test('template', async (t) => { 8 | const cases = [ 9 | { 10 | input: 'hello {{value}}' 11 | , values: {value: 'world'} 12 | , expected: 'hello world' 13 | , message: 'single value lookup' 14 | }, { 15 | input: 'hello {{value}}' 16 | , values: {} 17 | , expected: 'hello ' 18 | , message: 'missing imput values' 19 | }, { 20 | input: 'hello {{place.name}}' 21 | , values: {place: {name: 'boston'}} 22 | , expected: 'hello boston' 23 | , message: 'nested values' 24 | }, { 25 | input: null 26 | , values: {place: {name: 'boston'}} 27 | , expected: null 28 | , message: 'non string value returns value' 29 | } 30 | ] 31 | 32 | for (const tcase of cases) { 33 | const tpl = string.template(tcase.input) 34 | t.equal(tpl(tcase.values), tcase.expected, tcase.message) 35 | } 36 | }) 37 | 38 | t.test('typecast', async (t) => { 39 | t.type(string.typecast, 'function', 'typecast is a function') 40 | t.same(string.typecast({x: 1}), {x: 1}, 'non string value') 41 | const cases = [{ 42 | value: 'foo' 43 | , expected: 'foo' 44 | }, { 45 | value: 'true' 46 | , expected: true 47 | }, { 48 | value: 'false' 49 | , expected: false 50 | }, { 51 | value: true 52 | , expected: true 53 | }, { 54 | value: false 55 | , expected: false 56 | }, { 57 | }, { 58 | value: '123' 59 | , expected: 123 60 | , message: 'integer value' 61 | }, { 62 | value: '123.45' 63 | , expected: 123.45 64 | , message: 'float value' 65 | }, { 66 | value: 'null' 67 | , expected: null 68 | , message: 'null string value' 69 | }, { 70 | value: null 71 | , expected: null 72 | , message: 'null literal value' 73 | }, { 74 | value: 'undefined' 75 | , expected: undefined 76 | , message: 'undefined string value' 77 | }, { 78 | value: undefined 79 | , expected: undefined 80 | , message: 'undefined literal value' 81 | }, { 82 | value: '' 83 | , expected: '' 84 | , message: 'empty string value' 85 | }, { 86 | value: Infinity 87 | , expected: Infinity 88 | , message: 'Infinity returns input' 89 | }] 90 | 91 | for (const current of cases) { 92 | t.equal( 93 | string.typecast(current.value) 94 | , current.expected 95 | , current.message || `camelCase(${current.value}) == ${current.expected}` 96 | ) 97 | } 98 | }) 99 | }).catch(threw) 100 | -------------------------------------------------------------------------------- /test/unit/parse-pkg-name.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test, threw} = require('tap') 4 | const parsePkgName = require('../../lib/parse-pkg-name.js') 5 | 6 | test('parsePkgName', async (t) => { 7 | t.deepEqual(parsePkgName('test'), { 8 | scope: null 9 | , name: 'test' 10 | }, 'non-scoped package name') 11 | 12 | t.deepEqual(parsePkgName('@namespace/foobar'), { 13 | scope: 'namespace' 14 | , name: 'foobar' 15 | }, 'scoped package name') 16 | 17 | t.deepEqual(parsePkgName('nampace/foobar'), { 18 | name: null 19 | , scope: null 20 | }, 'invalid package name') 21 | 22 | t.deepEqual(parsePkgName(), { 23 | name: null 24 | , scope: null 25 | }, 'no package name') 26 | 27 | }).catch(threw) 28 | -------------------------------------------------------------------------------- /test/unit/read-pkg.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const {test, threw} = require('tap') 5 | const readPkg = require('../../lib/read-pkg.js') 6 | 7 | const fixturedir = path.join(__dirname, '..', 'fixture') 8 | test('read-pkg', async (t) => { 9 | t.test('reads cwd by default', async (tt) => { 10 | const pkg = await readPkg() 11 | tt.match(pkg, { 12 | name: '@codedependant/semantic-release-docker' 13 | , version: String 14 | }) 15 | }) 16 | 17 | t.test('reads specified directories', async (tt) => { 18 | const cwd = path.join(fixturedir, 'pkg', 'one') 19 | const pkg = await readPkg({cwd}) 20 | tt.match(pkg, { 21 | name: '@fixture/one' 22 | , version: '0.0.0' 23 | , private: true 24 | }) 25 | }) 26 | 27 | t.test('throws on invalid json', async (tt) => { 28 | const cwd = path.join(fixturedir, 'pkg', 'two') 29 | tt.rejects(readPkg({cwd}), /unexpected end of json input/ig) 30 | }) 31 | 32 | t.test('throws if no package.json', async (tt) => { 33 | const cwd = path.join(fixturedir, 'pkg', 'three') 34 | tt.rejects(readPkg({cwd}), {code: 'ENOENT'}) 35 | }) 36 | }).catch(threw) 37 | --------------------------------------------------------------------------------