├── .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 | [](https://github.com/semantic-release/semantic-release) 4 | [](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 |
--------------------------------------------------------------------------------