├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── actions.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .release.yml ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ ├── child_process.js └── fs.js ├── assets └── logo.png ├── bin ├── node-publisher ├── node-publisher-eject ├── node-publisher-release └── node-publisher-setup ├── package.json ├── src ├── client │ ├── lerna.js │ ├── lerna.spec.js │ ├── npm.js │ ├── npm.spec.js │ ├── yarn.js │ └── yarn.spec.js ├── index.js ├── setup │ ├── check-build │ │ ├── index.js │ │ └── index.spec.js │ ├── check-ci │ │ ├── index.js │ │ └── index.spec.js │ ├── index.js │ ├── nvmrc │ │ ├── index.js │ │ └── index.spec.js │ ├── release-branch │ │ ├── index.js │ │ └── index.spec.js │ └── utils.js └── utils │ ├── client │ ├── index.js │ └── index.spec.js │ ├── command │ ├── index.js │ └── index.spec.js │ ├── config │ ├── index.js │ └── index.spec.js │ ├── constants.js │ ├── index.js │ ├── index.spec.js │ ├── package │ ├── index.js │ └── index.spec.js │ └── validations │ ├── index.js │ └── index.spec.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | 7 | trim_trailing_whitespace = true 8 | 9 | ; Unix style line endings 10 | end_of_line = lf 11 | 12 | ; Always end file on newline 13 | insert_final_newline = true 14 | 15 | ; Indentation 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "jest/globals": true, 6 | "node": true 7 | }, 8 | "extends": ["standard", "prettier", "prettier/standard"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parser": "babel-eslint", 14 | "plugins": [ 15 | "jest" 16 | ], 17 | "rules": { 18 | "react/no-unused-prop-types": "off", 19 | "react/prop-types": "off", 20 | "react/jsx-no-bind": "off" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: repo-checks 2 | on: [push] 3 | jobs: 4 | main: 5 | name: yarn-simple 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: zendesk/checkout@v2 9 | - name: Read .nvmrc 10 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 11 | id: node-read-nvmrc 12 | - uses: zendesk/setup-node@v2.1.2 13 | with: 14 | node-version: "${{ steps.node-read-nvmrc.outputs.NVMRC }}" 15 | - name: yarn test 16 | run: | 17 | yarn install 18 | yarn travis 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .DS_Store 4 | yarn-error.log 5 | .yarn/install-state.gz 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.21.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .prettierrc 2 | package.json 3 | -------------------------------------------------------------------------------- /.release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rollback: true 3 | 4 | prepare: 5 | - git diff-index --quiet HEAD -- 6 | - git checkout master 7 | - git pull --rebase 8 | - '[[ -f .nvmrc ]] && ./node_modules/.bin/check-node-version --node $(cat .nvmrc)' 9 | - yarn install 10 | 11 | test: 12 | - yarn travis 13 | 14 | after_publish: 15 | - 'git push --follow-tags origin master:master' 16 | 17 | changelog: 18 | - ./node_modules/.bin/offline-github-changelog > CHANGELOG.md 19 | - git add CHANGELOG.md 20 | - git commit --allow-empty -m "Update changelog" 21 | - 'git push origin master:master' 22 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v3.1.0 (2024-01-24) 2 | 3 | #### Pull requests 4 | 5 | - [#55](https://github.com/zendesk/node-publisher/pull/55) Upgrade offline-github-changelog to fix duplicate authors ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 6 | - [#54](https://github.com/zendesk/node-publisher/pull/54) Make local installs work with yarn v4 ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 7 | 8 | #### Commits to master 9 | 10 | - [v3.1.0](https://github.com/zendesk/node-publisher/commit/5a1605e05a331211d8533533aba481aa724263ab) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 11 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/f06d6612e0a3fc350ba06b9925c42852e92c32c8) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 12 | 13 | ### v3.0.0 (2022-12-06) 14 | 15 | #### Pull requests 16 | 17 | - [#47](https://github.com/zendesk/node-publisher/pull/47) Upgrade offline-github-changelog \(Major\) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 18 | - [#46](https://github.com/zendesk/node-publisher/pull/46) Upgrade offline-github-changelog ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 19 | 20 | #### Commits to master 21 | 22 | - [v3.0.0](https://github.com/zendesk/node-publisher/commit/0f45304577e5fed207837410288891468bb75cd5) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 23 | - [Fixed incorrect version in package.json](https://github.com/zendesk/node-publisher/commit/58470f3f6eb3d33794dc9644404c14b78cd08310) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 24 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/44c13daf67e3ddda081c86090817654358500f62) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 25 | - [v1.6.0](https://github.com/zendesk/node-publisher/commit/881d9273d016e37fb742ecb20e5b946f7cf6efd2) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 26 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/bc7bbe4d1c8fcd25942ecf46e300c10eed1bce12) ([Attila Večerek](mailto:avecerek@zendesk.com)) 27 | - [+1 more](https://github.com/zendesk/node-publisher/compare/v2.0.0...v3.0.0) 28 | 29 | ### v2.0.0 (2021-09-17) 30 | 31 | - [v2.0.0](https://github.com/zendesk/node-publisher/commit/0527a926c6af2f080ed13d8b7b9ddd0cb516dd36) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 32 | 33 | ### v1.6.0 (2022-05-31) 34 | 35 | #### Pull requests 36 | 37 | - [#46](https://github.com/zendesk/node-publisher/pull/46) Upgrade offline-github-changelog ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 38 | 39 | #### Commits to master 40 | 41 | - [v1.6.0](https://github.com/zendesk/node-publisher/commit/881d9273d016e37fb742ecb20e5b946f7cf6efd2) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 42 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/bc7bbe4d1c8fcd25942ecf46e300c10eed1bce12) ([Attila Večerek](mailto:avecerek@zendesk.com)) 43 | 44 | ### v1.5.2 (2021-09-20) 45 | 46 | #### Pull requests 47 | 48 | - [#45](https://github.com/zendesk/node-publisher/pull/45) Upgrade offline-github-changelog to the latest version \(Major\) ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 49 | 50 | #### Commits to master 51 | 52 | - [v1.5.2](https://github.com/zendesk/node-publisher/commit/2d6ea7bdbc6de2b69f4a98dba1800ba09a461915) ([Attila Večerek](mailto:avecerek@zendesk.com)) 53 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/ec3f7ffcb8737bfc1d1a6e272f19f5a76f02c467) ([Attila Večerek](mailto:avecerek@zendesk.com)) 54 | 55 | ### v1.5.1 (2020-11-10) 56 | 57 | #### Pull requests 58 | 59 | - [#44](https://github.com/zendesk/node-publisher/pull/44) Upgrade offline-github-changelog to the latest version ([Sune Simonsen](mailto:sune@we-knowhow.dk)) 60 | - [#42](https://github.com/zendesk/node-publisher/pull/42) Add GitHub Actions workflows to migrate away from Travis-CI ([Attila Večerek](mailto:avecerek@zendesk.com), [John Shen](mailto:john@everops.com)) 61 | - [#43](https://github.com/zendesk/node-publisher/pull/43) Use node version from .nvmrc in GHA workflow ([Attila Večerek](mailto:avecerek@zendesk.com)) 62 | 63 | #### Commits to master 64 | 65 | - [v1.5.1](https://github.com/zendesk/node-publisher/commit/a22e5db7fd94b25ffc9d6994374a1739ea220107) ([Attila Večerek](mailto:avecerek@zendesk.com)) 66 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/bb4061d1053e22dd155ab7be13e309ad42abbc9a) ([Attila Večerek](mailto:avecerek@zendesk.com)) 67 | 68 | ### v1.5.0 (2020-06-10) 69 | 70 | #### Pull requests 71 | 72 | - [#41](https://github.com/zendesk/node-publisher/pull/41) Fix reading the contents of .nvmrc ([Attila Večerek](mailto:avecerek@zendesk.com)) 73 | - [#40](https://github.com/zendesk/node-publisher/pull/40) Remove dependency on NVM ([Attila Večerek](mailto:avecerek@zendesk.com)) 74 | 75 | #### Commits to master 76 | 77 | - [v1.5.0](https://github.com/zendesk/node-publisher/commit/af7e542384a5217409c212d3df5d5e8c0ad7a1f3) ([Attila Večerek](mailto:avecerek@zendesk.com)) 78 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/98e4f9ec23b96e240511341d36a62cbd8811e77d) ([Attila Večerek](mailto:avecerek@zendesk.com)) 79 | 80 | ### v1.4.0 (2019-08-24) 81 | 82 | #### Pull requests 83 | 84 | - [#37](https://github.com/zendesk/node-publisher/pull/37) Update jest ([Attila Večerek](mailto:avecerek@zendesk.com)) 85 | - [#36](https://github.com/zendesk/node-publisher/pull/36) Update lint-staged ([Attila Večerek](mailto:avecerek@zendesk.com)) 86 | - [#33](https://github.com/zendesk/node-publisher/pull/33) Update dependencies ([Attila Večerek](mailto:avecerek@zendesk.com)) 87 | - [#35](https://github.com/zendesk/node-publisher/pull/35) Fix the generated config for custom release branches ([Attila Večerek](mailto:avecerek@zendesk.com)) 88 | - [#34](https://github.com/zendesk/node-publisher/pull/34) Order branches during the release branch selection setup process ([Attila Večerek](mailto:avecerek@zendesk.com)) 89 | - [#32](https://github.com/zendesk/node-publisher/pull/32) Implement a setup script, take 2 ([Attila Večerek](mailto:avecerek@zendesk.com)) 90 | - [#31](https://github.com/zendesk/node-publisher/pull/31) Address security alerts ([Attila Večerek](mailto:avecerek@zendesk.com)) 91 | 92 | #### Commits to master 93 | 94 | - [v1.4.0](https://github.com/zendesk/node-publisher/commit/683742b816cdfdd4fe6a40d3d58f0448cf96680e) ([Attila Večerek](mailto:avecerek@zendesk.com)) 95 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/851aff839411ebda209d57603995612d5291abae) ([Attila Večerek](mailto:avecerek@zendesk.com)) 96 | 97 | ### v1.3.1 (2019-02-21) 98 | 99 | #### Pull requests 100 | 101 | - [#30](https://github.com/zendesk/node-publisher/pull/30) Fix the build step not committing the changes ([Attila Večerek](mailto:avecerek@zendesk.com)) 102 | - [#28](https://github.com/zendesk/node-publisher/pull/28) Add --branch release param ([Attila Večerek](mailto:avecerek@zendesk.com)) 103 | 104 | #### Commits to master 105 | 106 | - [v1.3.1](https://github.com/zendesk/node-publisher/commit/0fe80894fc603ac1ecbae5f6c5e3e8b8a7da95a9) ([Attila Večerek](mailto:avecerek@zendesk.com)) 107 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/f659c47d710c7abf4a52c874977a4e050c51011a) ([Attila Večerek](mailto:avecerek@zendesk.com)) 108 | 109 | ### v1.3.0 (2019-01-03) 110 | 111 | #### Pull requests 112 | 113 | - [#26](https://github.com/zendesk/node-publisher/pull/26) Support multiple release configurations ([Attila Večerek](mailto:avecerek@zendesk.com)) 114 | 115 | #### Commits to master 116 | 117 | - [v1.3.0](https://github.com/zendesk/node-publisher/commit/2ec3242870d28a5bf5b15b3c64c4f86731758271) ([Attila Večerek](mailto:avecerek@zendesk.com)) 118 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/d524f6e243aca82b28a9d25537e7c93e110ca7c1) ([Attila Večerek](mailto:avecerek@zendesk.com)) 119 | 120 | ### v1.2.0 (2018-12-26) 121 | 122 | #### Pull requests 123 | 124 | - [#27](https://github.com/zendesk/node-publisher/pull/27) Remove version constraints and add prerelease option ([Attila Večerek](mailto:avecerek@zendesk.com)) 125 | 126 | #### Commits to master 127 | 128 | - [v1.2.0](https://github.com/zendesk/node-publisher/commit/715c5e672f04bf5acb440be63bb6f1e62b5d96be) ([Attila Večerek](mailto:avecerek@zendesk.com)) 129 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/f3182c77bce2d83852540fd109c77a4c9afe07c4) ([Attila Večerek](mailto:avecerek@zendesk.com)) 130 | 131 | ### v1.1.1 (2018-12-11) 132 | 133 | #### Pull requests 134 | 135 | - [#24](https://github.com/zendesk/node-publisher/pull/24) Add details to package.json + minor adjustments ([Attila Večerek](mailto:avecerek@zendesk.com)) 136 | - [#23](https://github.com/zendesk/node-publisher/pull/23) Fix small typo in readme ([Marc Høegh](mailto:Anifacted@users.noreply.github.com)) 137 | 138 | #### Commits to master 139 | 140 | - [v1.1.1](https://github.com/zendesk/node-publisher/commit/266ea03c5e9ff03ff33262ef46498276e3bf95b0) ([Attila Večerek](mailto:avecerek@zendesk.com)) 141 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/f098461adb580224cce9a0f4fdcf12cde22fa02e) ([Attila Večerek](mailto:avecerek@zendesk.com)) 142 | 143 | ### v1.1.0 (2018-12-03) 144 | 145 | #### Pull requests 146 | 147 | - [#22](https://github.com/zendesk/node-publisher/pull/22) Add publish config to be able to release with yarn again ([Attila Večerek](mailto:avecerek@zendesk.com)) 148 | - [#21](https://github.com/zendesk/node-publisher/pull/21) Rename package and adds licenses field to package.json ([Attila Večerek](mailto:avecerek@zendesk.com)) 149 | - [#20](https://github.com/zendesk/node-publisher/pull/20) Remove private repo configuration ([Attila Večerek](mailto:avecerek@zendesk.com)) 150 | - [#18](https://github.com/zendesk/node-publisher/pull/18) Update README ([Attila Večerek](mailto:avecerek@zendesk.com)) 151 | 152 | #### Commits to master 153 | 154 | - [v1.1.0](https://github.com/zendesk/node-publisher/commit/a60e510d9d3c300738e5b382faf1bdf574888305) ([Attila Večerek](mailto:avecerek@zendesk.com)) 155 | - [Removes yarn version check from .release.yml](https://github.com/zendesk/node-publisher/commit/28653d054bf74501c3c9d022f6404c3c652360d1) ([Attila Večerek](mailto:avecerek@zendesk.com)) 156 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/785b3c03faa64e24fc6496ce6c831bd7a07f8616) ([Attila Večerek](mailto:avecerek@zendesk.com)) 157 | 158 | ### v0.5.0 (2018-09-13) 159 | 160 | #### Pull requests 161 | 162 | - [#17](https://github.com/zendesk/node-publisher/pull/17) Commit build files only when there are files staged ([Attila Večerek](mailto:avecerek@zendesk.com)) 163 | 164 | #### Commits to master 165 | 166 | - [v0.5.0](https://github.com/zendesk/node-publisher/commit/865ee4b5cfb5167a72f2d9030686e7a5b7a7fd68) ([Attila Večerek](mailto:avecerek@zendesk.com)) 167 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/4de33c01de2352314e2bf09001b0c6296bfbe9aa) ([Attila Večerek](mailto:avecerek@zendesk.com)) 168 | 169 | ### v0.4.2 (2018-09-06) 170 | 171 | #### Pull requests 172 | 173 | - [#16](https://github.com/zendesk/node-publisher/pull/16) Fix publishing using Lerna ([Attila Večerek](mailto:avecerek@zendesk.com)) 174 | 175 | #### Commits to master 176 | 177 | - [v0.4.2](https://github.com/zendesk/node-publisher/commit/9e1ba73be5b5899bf5f7ea136324e84f1963e170) ([Attila Večerek](mailto:avecerek@zendesk.com)) 178 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/09f0ccad64ce32cdb03f362674fd3032438cbab8) ([Attila Večerek](mailto:avecerek@zendesk.com)) 179 | 180 | ### v0.4.1 (2018-08-31) 181 | 182 | #### Pull requests 183 | 184 | - [#15](https://github.com/zendesk/node-publisher/pull/15) Fix the order of steps in .release.yml when ejected ([Attila Večerek](mailto:avecerek@zendesk.com)) 185 | 186 | #### Commits to master 187 | 188 | - [v0.4.1](https://github.com/zendesk/node-publisher/commit/9c424d93c8a30a574893c523fa647726935b2980) ([Attila Večerek](mailto:avecerek@zendesk.com)) 189 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/4bfa3125231d648256d0919cf77e746cdc0bfce6) ([Attila Večerek](mailto:avecerek@zendesk.com)) 190 | 191 | ### v0.4.0 (2018-08-31) 192 | 193 | #### Pull requests 194 | 195 | - [#14](https://github.com/zendesk/node-publisher/pull/14) Add test runner whitelist ([Attila Večerek](mailto:avecerek@zendesk.com)) 196 | 197 | #### Commits to master 198 | 199 | - [v0.4.0](https://github.com/zendesk/node-publisher/commit/7043e17939f6373175c556e1c16732939489afd5) ([Attila Večerek](mailto:avecerek@zendesk.com)) 200 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/d589da3113e9b96e705c339e68ef86f9f55e842b) ([Attila Večerek](mailto:avecerek@zendesk.com)) 201 | 202 | ### v0.3.0 (2018-08-30) 203 | 204 | #### Pull requests 205 | 206 | - [#12](https://github.com/zendesk/node-publisher/pull/12) Autodetect whether package needs to be built during release ([Attila Večerek](mailto:avecerek@zendesk.com)) 207 | - [#13](https://github.com/zendesk/node-publisher/pull/13) Remove the assumption about the output build directory ([Attila Večerek](mailto:avecerek@zendesk.com)) 208 | 209 | #### Commits to master 210 | 211 | - [v0.3.0](https://github.com/zendesk/node-publisher/commit/4c317fe37464b2d1c9c13c922173256bf62b09f7) ([Attila Večerek](mailto:avecerek@zendesk.com)) 212 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/14245e5c9005e9063ce89a3bfea0164213600599) ([Attila Večerek](mailto:avecerek@zendesk.com)) 213 | 214 | ### v0.2.1 (2018-07-24) 215 | 216 | #### Pull requests 217 | 218 | - [#11](https://github.com/zendesk/node-publisher/pull/11) Allow unmodified bundles to be committed ([Attila Večerek](mailto:avecerek@zendesk.com)) 219 | - [#10](https://github.com/zendesk/node-publisher/pull/10) Clarifies getting started and simplifies usage in README ([Attila Večerek](mailto:avecerek@zendesk.com)) 220 | 221 | #### Commits to master 222 | 223 | - [v0.2.1](https://github.com/zendesk/node-publisher/commit/81eb87bffe33124c10999b10753d0cf8bdfd4e29) ([Attila Večerek](mailto:avecerek@zendesk.com)) 224 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/bdd79c3b9b1254f2e6333d40ec1caaf243f7f23c) ([Attila Večerek](mailto:avecerek@zendesk.com)) 225 | 226 | ### v0.2.0 (2018-07-23) 227 | 228 | #### Pull requests 229 | 230 | - [#9](https://github.com/zendesk/node-publisher/pull/9) Add alias to own release command in package.json ([Attila Večerek](mailto:avecerek@zendesk.com)) 231 | - [#8](https://github.com/zendesk/node-publisher/pull/8) Remove build file naming assumption ([Attila Večerek](mailto:avecerek@zendesk.com)) 232 | - [#7](https://github.com/zendesk/node-publisher/pull/7) Relocate offline-github-changelog in package.json ([Attila Večerek](mailto:avecerek@zendesk.com)) 233 | 234 | #### Commits to master 235 | 236 | - [v0.2.0](https://github.com/zendesk/node-publisher/commit/623df15fad32384f57002d5f8cc6af8e0e09e920) ([Attila Večerek](mailto:avecerek@zendesk.com)) 237 | - [Update README - Yarn forwards the arguments since 1.0, no -- needed in that case](https://github.com/zendesk/node-publisher/commit/e5c23e4531caeebccf3d473c7247328fd8dea782) ([Attila Večerek](mailto:avecerek@zendesk.com)) 238 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/aaf4a5d684971103a407b14aa01462a6bc7446fe) ([Attila Večerek](mailto:avecerek@zendesk.com)) 239 | 240 | ### v0.1.4 (2018-07-11) 241 | 242 | #### Pull requests 243 | 244 | - [#6](https://github.com/zendesk/node-publisher/pull/6) Call local package bins directly instead of relying on \`yarn\` or \`npx\` ([Attila Večerek](mailto:avecerek@zendesk.com)) 245 | 246 | #### Commits to master 247 | 248 | - [v0.1.4](https://github.com/zendesk/node-publisher/commit/fada164b5524b488bfe857f9097ca4965c239d89) ([Attila Večerek](mailto:avecerek@zendesk.com)) 249 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/ccf818ed54a1e50a40f7f41b214e30b15e3e2b6c) ([Attila Večerek](mailto:avecerek@zendesk.com)) 250 | 251 | ### v0.1.3 (2018-07-11) 252 | 253 | #### Pull requests 254 | 255 | - [#5](https://github.com/zendesk/node-publisher/pull/5) Run binaries of local packages either using \`yarn\` or \`npx\` ([Attila Večerek](mailto:avecerek@zendesk.com)) 256 | 257 | #### Commits to master 258 | 259 | - [v0.1.3](https://github.com/zendesk/node-publisher/commit/2bd960baf8b475727f7682c710694170a74854a6) ([Attila Večerek](mailto:avecerek@zendesk.com)) 260 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/7e2eeaddf2a3d115d94720db7ac1ed7d0d3cc2bf) ([Attila Večerek](mailto:avecerek@zendesk.com)) 261 | 262 | ### v0.1.2 (2018-07-10) 263 | 264 | #### Pull requests 265 | 266 | - [#4](https://github.com/zendesk/node-publisher/pull/4) Extend \`package.json\` with a binary entry ([Attila Večerek](mailto:avecerek@zendesk.com)) 267 | 268 | #### Commits to master 269 | 270 | - [v0.1.2](https://github.com/zendesk/node-publisher/commit/610d90b74ef923582db4f0f85f8bb01e632cdf07) ([Attila Večerek](mailto:avecerek@zendesk.com)) 271 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/0aa806ba544544fe663b444520a21e13c0981c5d) ([Attila Večerek](mailto:avecerek@zendesk.com)) 272 | 273 | ### v0.1.1 (2018-07-10) 274 | 275 | #### Pull requests 276 | 277 | - [#3](https://github.com/zendesk/node-publisher/pull/3) Fix \`offline-github-changelog\` command not found error ([Attila Večerek](mailto:avecerek@zendesk.com)) 278 | 279 | #### Commits to master 280 | 281 | - [v0.1.1](https://github.com/zendesk/node-publisher/commit/c5f65c88507f395c135e9d18c1407836a5fed5c7) ([Attila Večerek](mailto:avecerek@zendesk.com)) 282 | - [Update changelog](https://github.com/zendesk/node-publisher/commit/af24d89677943a089ae2887f93ac263c128ad415) ([Attila Večerek](mailto:avecerek@zendesk.com)) 283 | 284 | ### v0.1.0 (2018-07-10) 285 | 286 | #### Pull requests 287 | 288 | - [#2](https://github.com/zendesk/node-publisher/pull/2) Fix rollback and own release ([Attila Večerek](mailto:avecerek@zendesk.com)) 289 | - [#1](https://github.com/zendesk/node-publisher/pull/1) Initial PR ([Attila Večerek](mailto:avecerek@zendesk.com)) 290 | 291 | #### Commits to master 292 | 293 | - [v0.1.0](https://github.com/zendesk/node-publisher/commit/c868c7d2fa931e9363c4881ebb5b36e55ee3e968) ([Attila Večerek](mailto:avecerek@zendesk.com)) 294 | - [Fixes syntax error in own release](https://github.com/zendesk/node-publisher/commit/ad3f2c4f7ab31fe5080161609e6094eddf5e14ea) ([Attila Večerek](mailto:avecerek@zendesk.com)) 295 | - [Adds empty README](https://github.com/zendesk/node-publisher/commit/6974f0f83969a4ca73c7314efef196e4f5adc8a0) ([Attila Večerek](mailto:avecerek@zendesk.com)) 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Node Publisher by Zendesk 8 | 9 |

10 | 11 | This is a configurable release automation tool for node packages inspired by [create-react-app](https://github.com/facebook/create-react-app) and [Travis CI](https://travis-ci.org/). It has a default configuration, which can be overriden in case of need. As a convention, this release tool defines a set of hooks that represent the release lifecycle. The default configuration can be overriden by redefining what commands should run under which hook in a `.release.yml` file. The hooks are listed under the [Lifecycle](#lifecycle) section. 12 | 13 | [![NPM version](https://badge.fury.io/js/node-publisher.svg)](https://badge.fury.io/js/node-publisher) 14 | ![repo-checks](https://github.com/zendesk/node-publisher/workflows/repo-checks/badge.svg) 15 | 16 | # Getting started 17 | ## 1. Install the package: 18 | 19 | ```sh 20 | npm install node-publisher --save-dev 21 | ``` 22 | 23 | or 24 | 25 | ```sh 26 | yarn add --dev node-publisher 27 | ``` 28 | 29 | ## 2. Setup 30 | 31 | Run the setup script: 32 | 33 | ```sh 34 | npx node-publisher setup 35 | ``` 36 | 37 | The script searches for unmet requirements in your package and attempts to address them. In general, it performs the following actions: 38 | 39 | - Checks whether the package root is a git directory. 40 | - Generates a release script in you `package.json` with a release branch of your choice. 41 | - Generates a `.nvmrc` file if missing. 42 | - Checks whether a `build` script is defined in `package.json`. 43 | - Checks whether a CI script is defined in `package.json`. 44 | 45 | For more info, read the [Prerequisites section](#prerequisites). 46 | 47 | # Usage 48 | 49 | ```sh 50 | npm run release -- 51 | ``` 52 | 53 | or 54 | 55 | ```sh 56 | yarn release 57 | ``` 58 | 59 | Since `v1.2.0`, node-publisher supports the version options supported by the detected npm client. In earlier versions, only `major`, `minor` and `patch` options were accepted. When using `yarn`, the pre-release identifier (`--preid`) is ignored. 60 | 61 | ```sh 62 | npm run release -- --preid alpha 63 | ``` 64 | 65 | # Customize the release process 66 | 67 | ```sh 68 | npx node-publisher eject 69 | ``` 70 | 71 | After ejecting, a `.release.yml` file will appear in the root directory of your package. You can override the default behaviour by modifying this file. 72 | 73 | ## Custom branch 74 | Using the `--branch` release param, it is possible to specify which branch should be checked out during the `prepare` [lifecycle](#lifecycle) step. When no `branch` is specified, the `master` branch will be checked out by default. 75 | 76 | ## Multiple configuration files 77 | 78 | Using the `--config` release param, it is possible to specify which file to load the release steps from. This way, one can have different release procedures for different purposes. 79 | 80 | Example: 81 | 82 | ```js 83 | // package.json 84 | 85 | { 86 | "scripts": { 87 | "release": "node-publisher release", 88 | "pre-release": "node-publisher release --config path/to/.pre-release.yml" 89 | } 90 | } 91 | ``` 92 | 93 | # Prerequisites 94 | 95 | The default release process assumes the following: 96 | 97 | - The master branch is called `master`. 98 | - A `.nvmrc` file is present in the root of your package. In case it is missing, the release fails in its preparation phase. 99 | - The tool expects the Node version to match the one in `.nvmrc` during the release process. If the expectation is not met, the release fails in its preparation phase. 100 | - The tool expects the build generation script to be called `build`. Otherwise, the build step is skipped. 101 | - The tool expects the test triggering script to be called `travis` or `ci`. The reason is that many times the standard `test` scripts are implemented to watch the files for changes to re-trigger the tests. This tool relies on the test script to return eventually, hence the choice of the commonly used CI-friendly script names. The list of accepted script names may be extended in the future. If both `travis` and `ci` scripts are present, `travis` will be preferred. 102 | 103 | *Notice:* the test triggering script (`travis` or `ci`) has to return a value, eventually. Otherwise, the release would stall and not run correctly. Interrupting a stalling release process would also interrupt the `rollback` feature's execution. 104 | 105 | # Lifecycle 106 | 107 | 1. `prepare`: The process that prepares the workspace for releasing a new version of your package. It might checkout to master, check whether the working tree is clean, check the current node version, etc. Between this step and `test`, a rollback point is created for your git repo. 108 | 109 | 2. `test`: Runs the tests and/or linting. You might want to configure the tool to run the same command as your CI tool does. 110 | 111 | 3. `build`: Runs your build process. By default it runs either `yarn build` or `npm run build` depending on your npm client. This step is only run if `build` is defined unders `sripts` in your `package.json` file. 112 | 113 | 4. `publish`: Publishes a new version of your package. By default, the tool detects your npm/publishing client and calls the publish command. Currently supported clients are: `npm`, `yarn`, `lerna`. 114 | 115 | 5. `after_publish`: Runs the declared commands immediately after publishing. By default, it pushes the changes to the remote along with the tags. In case the publishing fails, this hook will not execute. 116 | 117 | 6. `after_failure`: Runs the specified commands in case the release process failed at any point. Before running the configured commands, a rollback to the state after `prepare` might happen - in case the `rollback` option is set to `true` which is the default behaviour. 118 | 119 | 7. `changelog`: In case the package was successfully published, a changelog will be generated. This tool uses the [offline-github-changelog](https://github.com/sunesimonsen/offline-github-changelog) package for this purpuse. 120 | 121 | 8. `after_success`: Runs the specified commands after generating the changelog, in case the release process was successful. It might be used to clean up any byproduct of the previous hooks. 122 | 123 | ## Configuration 124 | 125 | The lifecycle hooks can be redefined in the form of a configurable YAML file. Additionally to the hooks, the configuration also accepts the following options: 126 | 127 | * `rollback [Boolean]` - rolls back to the latest commit fetched after the `prepare` step. The rollback itself happens in the `after_failure` step and only if this flag is set to `true`. 128 | 129 | ## Default configuration 130 | The exact configuration depends on the npm client being used and the contents of your `package.json` file. In case you use yarn, the default configuration will look like this: 131 | 132 | ```yaml 133 | rollback: true 134 | 135 | prepare: 136 | - git diff-index --quiet HEAD -- 137 | - git checkout master 138 | - git pull --rebase 139 | - '[[ -f .nvmrc ]] && ./node_modules/.bin/check-node-version --node $(cat .nvmrc)' 140 | - yarn install 141 | 142 | test: 143 | - yarn travis 144 | 145 | build: # only if "build" is defined as a script in your `package.json` 146 | - yarn build 147 | - git diff --staged --quiet || git commit -am "Update build file" 148 | 149 | after_publish: 150 | - git push --follow-tags origin master:master 151 | 152 | changelog: 153 | - ./node_modules/.bin/offline-github-changelog > CHANGELOG.md 154 | - git add CHANGELOG.md 155 | - git commit --allow-empty -m "Update changelog" 156 | - git push origin master:master 157 | ``` 158 | 159 | # Supported publishing clients 160 | 161 | `node-publisher` supports the main npm clients and Lerna as an underlying publishing tool. It automatically detects them based on the different `lock files` or `config files` they produce or require. If multiple of these files are detected, the following precedence will take place regarding the publishing tool to be used: 162 | 163 | `lerna` > `yarn` > `npm` 164 | 165 | # Development 166 | 167 | ## Install packages 168 | ```sh 169 | yarn 170 | ``` 171 | 172 | ## Release a new version 173 | ```sh 174 | yarn release 175 | ``` 176 | 177 | # Contributing 178 | 179 | Contributing to `node-publisher` is fairly easy, as long as the following steps are followed: 180 | 181 | 1. Fork the project 182 | 2. Create your feature branch (`git checkout -b my-new-feature`) 183 | 3. Commit your changes (`git commit -am 'Add some feature'`) 184 | 4. Push to the branch (`git push origin my-new-feature`) 185 | 5. Create a new Pull Request 186 | 6. Mention one or more of the maintainers to get the Pull Request approved and merged 187 | 188 | ## Maintainers 189 | - Attila Večerek ([@vecerek](https://github.com/vecerek/)) 190 | - Sune Simonsen ([@sunesimonsen](https://github.com/sunesimonsen/)) 191 | 192 | # Copyright and License 193 | Copyright (c) 2018 Zendesk Inc. 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 196 | 197 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 200 | -------------------------------------------------------------------------------- /__mocks__/child_process.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = jest.genMockFromModule('child_process'); 4 | 5 | let allowedCommands = []; 6 | const __permitCommands = commands => { 7 | allowedCommands = commands; 8 | }; 9 | 10 | let returnValues = {}; 11 | const __setReturnValues = retValues => { 12 | returnValues = retValues; 13 | }; 14 | 15 | const execSync = jest.fn().mockImplementation(execCommand => { 16 | const isAllowed = 17 | allowedCommands.filter(cmd => execCommand.startsWith(cmd)).length > 0; 18 | 19 | if (!isAllowed) { 20 | throw new Error(); 21 | } 22 | 23 | if (returnValues[execCommand]) { 24 | return returnValues[execCommand]; 25 | } 26 | }); 27 | 28 | childProcess.__permitCommands = __permitCommands; 29 | childProcess.__setReturnValues = __setReturnValues; 30 | childProcess.execSync = execSync; 31 | 32 | module.exports = childProcess; 33 | -------------------------------------------------------------------------------- /__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const fs = jest.genMockFromModule('fs'); 6 | 7 | let mockFiles = []; 8 | const __setMockFiles = newMockFiles => { 9 | mockFiles = []; 10 | // eslint-disable-next-line no-unused-vars 11 | for (const file of newMockFiles) { 12 | mockFiles.push(normalizePath(file)); 13 | } 14 | }; 15 | 16 | const readFileSyncRetValue = {}; 17 | const __setReadFileSyncReturnValue = (file, val) => { 18 | readFileSyncRetValue[normalizePath(file)] = val; 19 | }; 20 | 21 | const existsSync = path => mockFiles.includes(path); 22 | 23 | const readFileSync = path => readFileSyncRetValue[path]; 24 | 25 | const normalizePath = p => 26 | path.isAbsolute(p) ? p : path.resolve(process.env.PWD, p); 27 | 28 | fs.__setMockFiles = __setMockFiles; 29 | fs.__setReadFileSyncReturnValue = __setReadFileSyncReturnValue; 30 | fs.existsSync = existsSync; 31 | fs.readFileSync = readFileSync; 32 | 33 | module.exports = fs; 34 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/node-publisher/e19ff455cae89cbd2796c00699ca4debaded6fac/assets/logo.png -------------------------------------------------------------------------------- /bin/node-publisher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const pkg = require('../package.json'); 5 | 6 | program 7 | .version(pkg.version, '-v, --version') 8 | .description('A configurable release automation tool for node packages inspired by Travis CI.') 9 | .command('setup', 'integrates node-publisher by setting up the release script in package.json') 10 | .command('eject', 'makes the release process fully configurable by placing the default configuration as a `.release.yml` file in the root directory of the package') 11 | .command('release [version]', 'releases the specified version of the package', {isDefault: true}) 12 | .parse(process.argv); 13 | -------------------------------------------------------------------------------- /bin/node-publisher-eject: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const program = require('commander'); 6 | const { safeDump } = require('js-yaml'); 7 | const { buildReleaseEnvironment } = require('../src/utils'); 8 | const { buildReleaseConfig } = require('../src/utils/config'); 9 | 10 | program 11 | .description('Places the default configuration into the root directory of your package as `.release.yml`. After this command is run, the release process is governed by the configuration read from that file. If a release file already exists, it will be overwritten by the default configuration.') 12 | .parse(process.argv); 13 | 14 | try { 15 | const env = buildReleaseEnvironment({ quiet: true }); 16 | 17 | const config = safeDump(buildReleaseConfig(env), { 18 | skipInvalid: true 19 | }); 20 | 21 | fs.writeFileSync( 22 | path.resolve(process.env.PWD, '.release.yml'), 23 | config, 24 | 'utf-8' 25 | ); 26 | } catch (e) { 27 | console.error(`ERROR: ${e.message}`); 28 | process.exit(1); 29 | } 30 | -------------------------------------------------------------------------------- /bin/node-publisher-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const release = require('../'); 5 | const { buildReleaseEnvironment } = require('../src/utils'); 6 | 7 | program 8 | .description('Releases your package under the specified version.') 9 | .option('[version]', 'specify the release version; accepts the versions of the npm client in use.') 10 | .option('--preid ', 'specify the prerelease identifier (pre, alpha, beta, etc.).') 11 | .option('-b, --branch ', 'specify the branch to be checked out during the release process') 12 | .option('-c, --config ', 'set config path. Defaults to ./.release.yml') 13 | .parse(process.argv); 14 | 15 | try { 16 | release({ 17 | env: buildReleaseEnvironment({ branch: program.branch, configPath: program.config }), 18 | nextVersion: program.args[0], 19 | preid: program.preid 20 | }); 21 | } catch (e) { 22 | console.error(`ERROR: ${e.message}`); 23 | process.exit(1); 24 | } 25 | -------------------------------------------------------------------------------- /bin/node-publisher-setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const setup = require('../src/setup'); 5 | 6 | program 7 | .description('Integrates node-publisher by setting up the release script in `package.json`.') 8 | .parse(process.argv); 9 | 10 | (async () => { 11 | try { 12 | await setup(); 13 | console.log('\n🤘 All set up.'); 14 | } catch (e) { 15 | console.error(`ERROR: ${e.message}`); 16 | process.exit(1); 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-publisher", 3 | "description": "A configurable release automation tool inspired by create-react-app and Travis CI.", 4 | "author": "Attila Večerek ", 5 | "homepage": "https://github.com/zendesk/node-publisher#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/zendesk/node-publisher.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/zendesk/node-publisher/issues" 12 | }, 13 | "version": "3.1.0", 14 | "main": "src/index.js", 15 | "bin": "./bin/node-publisher", 16 | "scripts": { 17 | "coverage": "jest --coverage", 18 | "lint": "eslint ./**/*.js", 19 | "lint-staged": "lint-staged", 20 | "release": "./bin/node-publisher release", 21 | "test": "jest", 22 | "travis": "yarn lint && yarn test" 23 | }, 24 | "dependencies": { 25 | "check-node-version": "3.2.0", 26 | "commander": "2.15.1", 27 | "inquirer": "6.5.0", 28 | "js-yaml": "3.13.1", 29 | "offline-github-changelog": "3.0.2", 30 | "semver": "6.2.0" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "10.0.2", 34 | "eslint": "^6.0.0", 35 | "eslint-config-prettier": "^6.0.0", 36 | "eslint-config-standard": "^14.0.0", 37 | "eslint-plugin-import": "^2.18.0", 38 | "eslint-plugin-jest": "^22.0.0", 39 | "eslint-plugin-node": "^9.1.0", 40 | "eslint-plugin-promise": "^4.2.0", 41 | "eslint-plugin-standard": "^4.0.0", 42 | "jest": "24.9.0", 43 | "lint-staged": "^9.0.0", 44 | "pre-commit": "1.2.2", 45 | "prettier": "1.18.2", 46 | "prettier-package-json": "2.0.1" 47 | }, 48 | "keywords": [ 49 | "lerna", 50 | "npm", 51 | "release-management", 52 | "yarn" 53 | ], 54 | "engines": { 55 | "node": ">=14" 56 | }, 57 | "publishConfig": { 58 | "access": "public", 59 | "registry": "https://registry.npmjs.org/" 60 | }, 61 | "licenses": [ 62 | { 63 | "type": "Apache 2.0", 64 | "url": "https://github.com/zendesk/node-publisher/blob/master/LICENSE" 65 | } 66 | ], 67 | "lint-staged": { 68 | "*.js": [ 69 | "eslint", 70 | "prettier --single-quote --write", 71 | "git add" 72 | ], 73 | "package.json": [ 74 | "prettier-package-json --write", 75 | "git add" 76 | ] 77 | }, 78 | "packageManager": "yarn@1.22.21", 79 | "pre-commit": "lint-staged" 80 | } 81 | -------------------------------------------------------------------------------- /src/client/lerna.js: -------------------------------------------------------------------------------- 1 | const publish = ({ nextVersion, execCommand, preid }) => { 2 | const publishCommand = preid 3 | ? `lerna publish ${nextVersion} --preid ${preid}` 4 | : `lerna publish ${nextVersion}`; 5 | 6 | execCommand(publishCommand); 7 | }; 8 | 9 | module.exports = { 10 | publish 11 | }; 12 | -------------------------------------------------------------------------------- /src/client/lerna.spec.js: -------------------------------------------------------------------------------- 1 | const { publish } = require('./lerna'); 2 | 3 | describe('publish', () => { 4 | describe('without preid', () => { 5 | const options = { 6 | nextVersion: 'patch', 7 | preid: undefined, 8 | execCommand: jest.fn() 9 | }; 10 | 11 | it('publishes new version without a prerelease tag', () => { 12 | expect(() => publish(options)).not.toThrow(); 13 | expect(options.execCommand.mock.calls[0][0]).toBe('lerna publish patch'); 14 | }); 15 | }); 16 | 17 | describe('with preid', () => { 18 | const options = { 19 | nextVersion: 'major', 20 | preid: 'alpha', 21 | execCommand: jest.fn() 22 | }; 23 | 24 | it('publishes new version with a prerelease tag', () => { 25 | expect(() => publish(options)).not.toThrow(); 26 | expect(options.execCommand.mock.calls[0][0]).toBe( 27 | 'lerna publish major --preid alpha' 28 | ); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/client/npm.js: -------------------------------------------------------------------------------- 1 | const publish = ({ nextVersion, execCommand, preid }) => { 2 | const versionCommand = preid 3 | ? `npm version ${nextVersion} --preid=${preid}` 4 | : `npm version ${nextVersion}`; 5 | 6 | execCommand(versionCommand); 7 | execCommand('npm publish'); 8 | }; 9 | 10 | module.exports = { 11 | publish 12 | }; 13 | -------------------------------------------------------------------------------- /src/client/npm.spec.js: -------------------------------------------------------------------------------- 1 | const { publish } = require('./npm'); 2 | 3 | describe('publish', () => { 4 | describe('without preid', () => { 5 | const options = { 6 | nextVersion: 'patch', 7 | preid: undefined, 8 | execCommand: jest.fn() 9 | }; 10 | 11 | it('publishes new version without a prerelease tag', () => { 12 | expect(() => publish(options)).not.toThrow(); 13 | expect(options.execCommand.mock.calls[0][0]).toBe('npm version patch'); 14 | expect(options.execCommand.mock.calls[1][0]).toBe('npm publish'); 15 | }); 16 | }); 17 | 18 | describe('with preid', () => { 19 | const options = { 20 | nextVersion: 'major', 21 | preid: 'alpha', 22 | execCommand: jest.fn() 23 | }; 24 | 25 | it('publishes new version with a prerelease tag', () => { 26 | expect(() => publish(options)).not.toThrow(); 27 | expect(options.execCommand.mock.calls[0][0]).toBe( 28 | 'npm version major --preid=alpha' 29 | ); 30 | expect(options.execCommand.mock.calls[1][0]).toBe('npm publish'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/client/yarn.js: -------------------------------------------------------------------------------- 1 | const publish = ({ nextVersion, execCommand }) => { 2 | execCommand(`yarn publish --new-version ${nextVersion}`); 3 | }; 4 | 5 | module.exports = { 6 | publish 7 | }; 8 | -------------------------------------------------------------------------------- /src/client/yarn.spec.js: -------------------------------------------------------------------------------- 1 | const { publish } = require('./yarn'); 2 | 3 | describe('publish', () => { 4 | describe('without preid', () => { 5 | const options = { 6 | nextVersion: 'patch', 7 | preid: undefined, 8 | execCommand: jest.fn() 9 | }; 10 | 11 | it('publishes new version without a prerelease id', () => { 12 | expect(() => publish(options)).not.toThrow(); 13 | expect(options.execCommand.mock.calls[0][0]).toBe( 14 | 'yarn publish --new-version patch' 15 | ); 16 | }); 17 | }); 18 | 19 | describe('with preid', () => { 20 | const options = { 21 | nextVersion: 'major', 22 | preid: 'alpha', 23 | execCommand: jest.fn() 24 | }; 25 | 26 | it('publishes new version without a prerelease id', () => { 27 | expect(() => publish(options)).not.toThrow(); 28 | expect(options.execCommand.mock.calls[0][0]).toBe( 29 | 'yarn publish --new-version major' 30 | ); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | loadReleaseConfig, 3 | execCommands, 4 | currentCommitId, 5 | rollbackCommit 6 | } = require('./utils'); 7 | const command = require('./utils/command'); 8 | 9 | const release = (options) => { 10 | const { env } = options; 11 | const config = loadReleaseConfig(env); 12 | const publishClient = require(`./client/${env.publishClient}.js`); 13 | 14 | let failed = false; 15 | let commitId = null; 16 | try { 17 | execCommands(config.prepare); 18 | commitId = currentCommitId(); 19 | execCommands(config.test); 20 | execCommands(config.build); 21 | 22 | if (config.publish) { 23 | execCommands(config.publish); 24 | } else { 25 | publishClient.publish(Object.assign( 26 | {}, options, { execCommand: command.exec } 27 | )); 28 | } 29 | 30 | execCommands(config.after_publish); 31 | } catch (e) { 32 | console.error(`ERROR: ${e.message}`); 33 | failed = true; 34 | } 35 | 36 | if (failed) { 37 | if (commitId && config.rollback) { 38 | rollbackCommit(commitId); 39 | } 40 | 41 | execCommands(config.after_failure); 42 | console.log('🔥 Failed to publish new release.'); 43 | } else { 44 | execCommands(config.changelog); 45 | execCommands(config.after_success); 46 | console.log('🤘 Successfully published.'); 47 | } 48 | }; 49 | 50 | module.exports = release; 51 | -------------------------------------------------------------------------------- /src/setup/check-build/index.js: -------------------------------------------------------------------------------- 1 | const { hasBuildScript } = require('../../utils/validations'); 2 | const { warn } = require('../utils'); 3 | 4 | const checkBuildStep = () => { 5 | if (!hasBuildScript()) { 6 | warn( 7 | `Your package.json does not contain a \`build\` script. \ 8 | Make sure to set up your build process if you need one.` 9 | ); 10 | } 11 | }; 12 | 13 | module.exports = { 14 | checkBuildStep 15 | }; 16 | -------------------------------------------------------------------------------- /src/setup/check-build/index.spec.js: -------------------------------------------------------------------------------- 1 | const { checkBuildStep } = require('./'); 2 | const validations = require('../../utils/validations'); 3 | const utils = require('../utils'); 4 | 5 | jest.mock('../../utils/validations'); 6 | jest.mock('../utils'); 7 | 8 | describe('checkBuildStep', () => { 9 | afterEach(() => utils.warn.mockClear()); 10 | 11 | it('warns the user if a CI script is missing', () => { 12 | validations.hasBuildScript.mockReturnValue(false); 13 | 14 | checkBuildStep(); 15 | 16 | expect(utils.warn).toHaveBeenCalledTimes(1); 17 | }); 18 | 19 | it('does not warn the user if a CI script is defined', () => { 20 | validations.hasBuildScript.mockReturnValue(true); 21 | 22 | checkBuildStep(); 23 | 24 | expect(utils.warn).not.toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/setup/check-ci/index.js: -------------------------------------------------------------------------------- 1 | const { hasCiScript } = require('../../utils/validations'); 2 | const { warn } = require('../utils'); 3 | 4 | const checkCiStep = () => { 5 | if (!hasCiScript()) { 6 | warn( 7 | `Your package.json does not contain a CI script. \ 8 | Make sure to define your CI script under a \`ci\` or \`travis\` key. \ 9 | Also, make sure the script you define exits with a status \ 10 | that can be read from the terminal with $?` 11 | ); 12 | } 13 | }; 14 | 15 | module.exports = { 16 | checkCiStep 17 | }; 18 | -------------------------------------------------------------------------------- /src/setup/check-ci/index.spec.js: -------------------------------------------------------------------------------- 1 | const { checkCiStep } = require('./'); 2 | const validations = require('../../utils/validations'); 3 | const utils = require('../utils'); 4 | 5 | jest.mock('../../utils/validations'); 6 | jest.mock('../utils'); 7 | 8 | describe('checkCiStep', () => { 9 | afterEach(() => utils.warn.mockClear()); 10 | 11 | it('warns the user if a CI script is missing', () => { 12 | validations.hasCiScript.mockReturnValue(false); 13 | 14 | checkCiStep(); 15 | 16 | expect(utils.warn).toHaveBeenCalledTimes(1); 17 | }); 18 | 19 | it('does not warn the user if a CI script is defined', () => { 20 | validations.hasCiScript.mockReturnValue(true); 21 | 22 | checkCiStep(); 23 | 24 | expect(utils.warn).not.toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/setup/index.js: -------------------------------------------------------------------------------- 1 | const { releaseBranchStep } = require('./release-branch'); 2 | const { nvmrcStep } = require('./nvmrc'); 3 | const { checkBuildStep } = require('./check-build'); 4 | const { checkCiStep } = require('./check-ci'); 5 | 6 | async function setup() { 7 | await releaseBranchStep(); 8 | await nvmrcStep(); 9 | checkBuildStep(); 10 | checkCiStep(); 11 | } 12 | 13 | module.exports = setup; 14 | -------------------------------------------------------------------------------- /src/setup/nvmrc/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const childProcess = require('child_process'); 5 | const semver = require('semver'); 6 | const { ask } = require('../utils'); 7 | 8 | const NVM_PATH = path.resolve(os.homedir(), '.nvm'); 9 | const NVM_CONFIG_PATH = path.resolve(process.env.PWD, '.nvmrc'); 10 | 11 | const nvmrcExists = () => fs.existsSync(NVM_CONFIG_PATH); 12 | 13 | const versionTransformer = (version, _answers, flags) => 14 | flags.isFinal && version[0] !== 'v' ? `v${version}` : version; 15 | 16 | const question = () => ({ 17 | type: 'input', 18 | name: 'nvmrc', 19 | message: `Your package does not contain a .nvmrc file. \ 20 | What version of Node would you like your package to depend on?`, 21 | default: childProcess 22 | .execSync('node -v') 23 | .toString() 24 | .trim(), 25 | validate: version => 26 | semver.valid(version) 27 | ? true 28 | : `The specified version is not a valid semver. \ 29 | Examples of valid semver: 1.2.3, 42.6.7.9.3-alpha, etc.`, 30 | transformer: versionTransformer 31 | }); 32 | 33 | const generateNvmrcFile = version => 34 | fs.writeFileSync( 35 | NVM_CONFIG_PATH, 36 | `${versionTransformer(version, {}, { isFinal: true })}\n`, 37 | 'utf-8' 38 | ); 39 | 40 | async function nvmrcStep() { 41 | if (nvmrcExists()) { 42 | return; 43 | } 44 | 45 | const nodeVersion = await ask(question()); 46 | generateNvmrcFile(nodeVersion); 47 | } 48 | 49 | module.exports = { 50 | NVM_PATH, 51 | NVM_CONFIG_PATH, 52 | versionTransformer, 53 | nvmrcStep 54 | }; 55 | -------------------------------------------------------------------------------- /src/setup/nvmrc/index.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { 3 | versionTransformer, 4 | nvmrcStep, 5 | NVM_PATH, 6 | NVM_CONFIG_PATH 7 | } = require('./'); 8 | const utils = require('../utils'); 9 | 10 | jest.mock('fs'); 11 | jest.mock('../utils'); 12 | 13 | describe('versionTransformer', () => { 14 | describe('when isFinal flag is true', () => { 15 | const isFinal = true; 16 | 17 | it('returns the version with the "v" prefix', () => { 18 | expect(versionTransformer('9.11.1', [], { isFinal })).toBe('v9.11.1'); 19 | expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1'); 20 | }); 21 | }); 22 | 23 | describe('when isFinal flag is false', () => { 24 | const isFinal = false; 25 | 26 | it('returns the version as input by the user', () => { 27 | expect(versionTransformer('9.11.1', [], { isFinal })).toBe('9.11.1'); 28 | expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1'); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('nvmrcStep', () => { 34 | let MOCKED_FILES = []; 35 | 36 | beforeEach(() => { 37 | require('fs').__setMockFiles(MOCKED_FILES); 38 | utils.ask.mockReturnValue('v9.11.0'); 39 | }); 40 | afterEach(() => { 41 | utils.ask.mockClear(); 42 | fs.writeFileSync.mockClear(); 43 | }); 44 | afterAll(() => require('fs').__setMockFiles([])); 45 | 46 | describe('when .nvmrc file exists', () => { 47 | beforeAll(() => { 48 | MOCKED_FILES.push('.nvmrc'); 49 | }); 50 | 51 | it('does not ask for the node version', async () => { 52 | try { 53 | await nvmrcStep(); 54 | } catch (_) { 55 | expect(utils.ask).not.toHaveBeenCalled(); 56 | } 57 | }); 58 | 59 | it('does not generate a .nvmrc file', async () => { 60 | try { 61 | await nvmrcStep(); 62 | } catch (_) { 63 | expect(fs.writeFileSync).not.toHaveBeenCalled(); 64 | } 65 | }); 66 | }); 67 | 68 | describe('when .nvmrc file does not exist', () => { 69 | beforeAll(() => { 70 | MOCKED_FILES = [NVM_PATH]; 71 | }); 72 | 73 | it('asks for the node version', async () => { 74 | try { 75 | await nvmrcStep(); 76 | } catch (_) { 77 | expect(utils.ask).not.toHaveBeenCalledTimes(1); 78 | } 79 | }); 80 | 81 | it('generates a .nvmrc file', async () => { 82 | try { 83 | await nvmrcStep(); 84 | } catch (_) { 85 | expect(fs.writeFileSync).not.toHaveBeenCalledTimes(1); 86 | expect(fs.writeFileSync).not.toHaveBeenCalledWith( 87 | NVM_CONFIG_PATH, 88 | 'v9.11.0', 89 | 'utf-8' 90 | ); 91 | } 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/setup/release-branch/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const childProcess = require('child_process'); 4 | const { ask, addScript } = require('../utils'); 5 | const { DEFAULT_BRANCH } = require('../../utils/constants'); 6 | 7 | const GIT_PATH = path.resolve(process.env.PWD, '.git'); 8 | 9 | const isGitRepo = () => fs.existsSync(GIT_PATH); 10 | 11 | const gitBranches = () => { 12 | const rawBranches = childProcess.execSync('git branch').toString(); 13 | const branchesRegex = /^\s*(?:\* )?(.+)/gm; 14 | const branches = []; 15 | 16 | let match = branchesRegex.exec(rawBranches); 17 | while (match !== null) { 18 | branches.push(match[1].trim()); 19 | match = branchesRegex.exec(rawBranches); 20 | } 21 | 22 | return prioritizeDefaultBranch(branches); 23 | }; 24 | 25 | const prioritizeDefaultBranch = branches => { 26 | const index = branches.indexOf(DEFAULT_BRANCH); 27 | 28 | return index === -1 29 | ? branches 30 | : branches.splice(index, 1).concat(branches); 31 | } 32 | 33 | const question = () => ({ 34 | type: 'list', 35 | name: 'release branch', 36 | message: 37 | 'Which of the following branches would you like to use as your release branch?', 38 | choices: gitBranches() 39 | }); 40 | 41 | async function releaseBranchStep() { 42 | if (!isGitRepo()) { 43 | throw new Error( 44 | 'This is not a Git repository. Run `git init` before running this setup.' 45 | ); 46 | } 47 | 48 | const releaseBranch = await ask(question()); 49 | const releaseCommand = 50 | releaseBranch === 'master' 51 | ? 'node-publisher release' 52 | : `node-publisher release --default-branch=${releaseBranch}`; 53 | 54 | addScript('release', releaseCommand); 55 | } 56 | 57 | module.exports = { 58 | isGitRepo, 59 | gitBranches, 60 | releaseBranchStep 61 | }; 62 | -------------------------------------------------------------------------------- /src/setup/release-branch/index.spec.js: -------------------------------------------------------------------------------- 1 | const { isGitRepo, gitBranches, releaseBranchStep } = require('./'); 2 | const utils = require('../utils'); 3 | 4 | jest.mock('fs'); 5 | jest.mock('child_process'); 6 | jest.mock('../utils'); 7 | 8 | require('child_process').__permitCommands(['git']); 9 | 10 | describe('isGitRepo', () => { 11 | afterEach(() => { 12 | require('fs').__setMockFiles([]); 13 | }); 14 | 15 | describe('when .git directory exists', () => { 16 | const MOCKED_FILES = ['.git']; 17 | 18 | it('returns true', () => { 19 | require('fs').__setMockFiles(MOCKED_FILES); 20 | 21 | expect(isGitRepo()).toBe(true); 22 | }); 23 | }); 24 | 25 | describe('when .git directory does not exist', () => { 26 | it('returns false', () => { 27 | expect(isGitRepo()).toBe(false); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('gitBranches', () => { 33 | describe('when the first branch is the current branch', () => { 34 | const rawBranches = `* abc-branch-xyz 35 | master 36 | my-branch-123`; 37 | 38 | it('returns the git branches in the correct order', () => { 39 | require('child_process').__setReturnValues({ 40 | 'git branch': rawBranches 41 | }); 42 | 43 | expect(gitBranches()).toEqual([ 44 | 'master', 45 | 'abc-branch-xyz', 46 | 'my-branch-123' 47 | ]); 48 | }); 49 | }); 50 | 51 | describe('when the second branch is the current branch', () => { 52 | const rawBranches = `abc-branch-xyz 53 | * master 54 | my-branch-123`; 55 | 56 | it('returns the git branches in the correct order', () => { 57 | require('child_process').__setReturnValues({ 58 | 'git branch': rawBranches 59 | }); 60 | 61 | expect(gitBranches()).toEqual([ 62 | 'master', 63 | 'abc-branch-xyz', 64 | 'my-branch-123' 65 | ]); 66 | }); 67 | }); 68 | 69 | describe('when master is not in the list of branches', () => { 70 | const rawBranches = `abc-branch-xyz 71 | * production 72 | my-branch-123`; 73 | 74 | it('returns the git branches in the correct order', () => { 75 | require('child_process').__setReturnValues({ 76 | 'git branch': rawBranches 77 | }); 78 | 79 | expect(gitBranches()).toEqual([ 80 | 'abc-branch-xyz', 81 | 'production', 82 | 'my-branch-123' 83 | ]); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('releaseBranchStep', () => { 89 | let MOCKED_FILES = []; 90 | 91 | beforeEach(() => require('fs').__setMockFiles(MOCKED_FILES)); 92 | afterEach(() => { 93 | utils.ask.mockClear(); 94 | utils.addScript.mockClear(); 95 | }); 96 | afterAll(() => require('fs').__setMockFiles([])); 97 | 98 | describe('when root is not a git directory', () => { 99 | it('throws an error', async () => { 100 | const error = 101 | 'This is not a Git repository. Run `git init` before running this setup.'; 102 | 103 | await expect(releaseBranchStep()).rejects.toThrow(error); 104 | }); 105 | 106 | it('does not ask for the release branch', async () => { 107 | try { 108 | await releaseBranchStep(); 109 | } catch (_) { 110 | expect(utils.ask).not.toHaveBeenCalled(); 111 | } 112 | }); 113 | 114 | it('does not add a release script to package.json', async () => { 115 | try { 116 | await releaseBranchStep(); 117 | } catch (_) { 118 | expect(utils.addScript).not.toHaveBeenCalled(); 119 | } 120 | }); 121 | }); 122 | 123 | describe('when root is a git directory', () => { 124 | beforeAll(() => { 125 | MOCKED_FILES = ['.git']; 126 | }); 127 | 128 | it('does not throw an error', async () => { 129 | await expect(releaseBranchStep()).resolves.not.toThrow(); 130 | }); 131 | 132 | it('asks for the release branch', async () => { 133 | await releaseBranchStep(); 134 | 135 | expect(utils.ask).toHaveBeenCalledTimes(1); 136 | }); 137 | 138 | it('adds a release script to package.json', async () => { 139 | await releaseBranchStep(); 140 | 141 | expect(utils.addScript).toHaveBeenCalledTimes(1); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/setup/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const inquirer = require('inquirer'); 3 | const { packageJson } = require('../utils/package'); 4 | const { PACKAGE_JSON_PATH } = require('../utils/constants'); 5 | 6 | async function ask(question) { 7 | const { name } = question; 8 | const answer = await inquirer.prompt([question]); 9 | 10 | return answer[name]; 11 | } 12 | 13 | const format = obj => JSON.stringify(obj, null, 2) + '\n'; 14 | 15 | const addScript = (key, value) => { 16 | const pkg = packageJson(); 17 | const newScripts = Object.assign({}, pkg.scripts, { [key]: value }); 18 | const newPkg = Object.assign({}, pkg, { scripts: newScripts }); 19 | 20 | fs.writeFileSync(PACKAGE_JSON_PATH, format(newPkg), 'utf-8'); 21 | }; 22 | 23 | const warn = message => console.log(`WARNING: ${message}`); 24 | 25 | module.exports = { 26 | ask, 27 | addScript, 28 | warn 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/client/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { LERNA_JSON_PATH } = require('../constants'); 4 | 5 | const npmClient = () => { 6 | const clientMap = { 7 | 'package-lock.json': 'npm', 8 | 'yarn.lock': 'yarn' 9 | }; 10 | 11 | let client; 12 | for (const file in clientMap) { 13 | if (fs.existsSync(path.resolve(process.env.PWD, file))) { 14 | client = clientMap[file]; 15 | } 16 | } 17 | 18 | if (!client) { 19 | throw new Error( 20 | 'Client could not be detected, make sure you use one of the supported clients.' 21 | ); 22 | } 23 | 24 | return client; 25 | }; 26 | 27 | const publishClient = () => { 28 | const lernaConfigExists = fs.existsSync(LERNA_JSON_PATH); 29 | 30 | return lernaConfigExists ? 'lerna' : npmClient(); 31 | }; 32 | 33 | module.exports = { 34 | npmClient, 35 | publishClient 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/client/index.spec.js: -------------------------------------------------------------------------------- 1 | const { npmClient, publishClient } = require('./'); 2 | 3 | jest.mock('fs'); 4 | 5 | describe('npmClient', () => { 6 | beforeEach(() => { 7 | require('fs').__setMockFiles([]); 8 | }); 9 | 10 | describe('when client cannot be detected', () => { 11 | const MOCKED_FILES = []; 12 | 13 | it('throws an error', () => { 14 | require('fs').__setMockFiles(MOCKED_FILES); 15 | 16 | expect(npmClient).toThrow(); 17 | }); 18 | }); 19 | 20 | describe('when package-lock.json found', () => { 21 | const MOCKED_FILES = ['package-lock.json']; 22 | 23 | it('returns npm', () => { 24 | require('fs').__setMockFiles(MOCKED_FILES); 25 | 26 | expect(npmClient).not.toThrow(); 27 | expect(npmClient()).toBe('npm'); 28 | }); 29 | }); 30 | 31 | describe('when both package-lock.json and yarn.lock are found', () => { 32 | const MOCKED_FILES = ['package-lock.json', 'yarn.lock']; 33 | 34 | it('returns yarn', () => { 35 | require('fs').__setMockFiles(MOCKED_FILES); 36 | 37 | expect(npmClient).not.toThrow(); 38 | expect(npmClient()).toBe('yarn'); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('publishClient', () => { 44 | beforeEach(() => { 45 | require('fs').__setMockFiles([]); 46 | }); 47 | 48 | describe('when lerna.json is found', () => { 49 | const MOCKED_FILES = ['lerna.json']; 50 | 51 | it('returns lerna', () => { 52 | require('fs').__setMockFiles(MOCKED_FILES); 53 | 54 | expect(publishClient).not.toThrow(); 55 | expect(publishClient()).toBe('lerna'); 56 | }); 57 | }); 58 | 59 | describe('when lerna.json is not found', () => { 60 | const MOCKED_FILES = ['yarn.lock']; 61 | 62 | it('returns the npm client', () => { 63 | require('fs').__setMockFiles(MOCKED_FILES); 64 | 65 | expect(publishClient).not.toThrow(); 66 | expect(publishClient()).toBe('yarn'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/utils/command/index.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | const exec = (command, opts = { stdio: [0, 1, 2] }) => { 4 | try { 5 | return execSync(command, opts); 6 | } catch (e) { 7 | throw new Error(`Execution of command \`${command}\` failed.`); 8 | } 9 | }; 10 | 11 | module.exports = { 12 | exec 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/command/index.spec.js: -------------------------------------------------------------------------------- 1 | const command = require('./index'); 2 | 3 | jest.mock('child_process'); 4 | 5 | describe('execCommand', () => { 6 | const PERMIT_COMMANDS = ['echo', 'ls', 'cd']; 7 | 8 | describe('when command fails to be executed', () => { 9 | it('throws an error', () => { 10 | require('child_process').__permitCommands(PERMIT_COMMANDS); 11 | 12 | expect(() => command.exec('myCommand --some-arg')).toThrow( 13 | 'Execution of command `myCommand --some-arg` failed.' 14 | ); 15 | }); 16 | }); 17 | 18 | describe('when command is successfully executed', () => { 19 | it('returns', () => { 20 | require('child_process').__permitCommands(PERMIT_COMMANDS); 21 | 22 | expect(() => command.exec('echo "Something"')).not.toThrow(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils/config/index.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml'); 2 | 3 | const readReleaseConfig = str => yaml.safeLoad(str); 4 | 5 | const buildReleaseConfig = env => { 6 | const client = env.npmClient; 7 | const binPathPrefix = './node_modules/.bin/'; 8 | const scriptRunner = client === 'yarn' ? 'yarn' : 'npm run'; 9 | 10 | const config = { 11 | rollback: true, 12 | prepare: [ 13 | 'git diff-index --quiet HEAD --', 14 | `git checkout ${env.branch}`, 15 | 'git pull --rebase', 16 | `[[ -f .nvmrc ]] && ${binPathPrefix}check-node-version --node $(cat .nvmrc)`, 17 | `${client} install` 18 | ], 19 | test: [`${scriptRunner} ${env.testRunner}`], 20 | build: null, 21 | after_publish: [ 22 | `git push --follow-tags origin ${env.branch}:${env.branch}` 23 | ], 24 | changelog: [ 25 | `${binPathPrefix}offline-github-changelog > CHANGELOG.md`, 26 | 'git add CHANGELOG.md', 27 | 'git commit --allow-empty -m "Update changelog"', 28 | `git push origin ${env.branch}:${env.branch}` 29 | ] 30 | }; 31 | 32 | if (env.withBuildStep) { 33 | config.build = [ 34 | `${scriptRunner} build`, 35 | 'git diff --quiet || git commit -am "Update build file"' 36 | ]; 37 | } else { 38 | delete config.build; 39 | } 40 | 41 | return config; 42 | }; 43 | 44 | module.exports = { 45 | readReleaseConfig, 46 | buildReleaseConfig 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils/config/index.spec.js: -------------------------------------------------------------------------------- 1 | const { buildReleaseConfig } = require('./'); 2 | const baseEnv = { testRunner: 'travis', branch: 'master' }; 3 | 4 | describe('buildReleaseConfig', () => { 5 | describe('when build script is defined in package.json', () => { 6 | const withBuildEnv = Object.assign({}, baseEnv, { withBuildStep: true }); 7 | 8 | describe('and npm is the detected npm client', () => { 9 | const env = Object.assign({}, withBuildEnv, { npmClient: 'npm' }); 10 | 11 | it('returns a configuration with the build step', () => { 12 | const config = buildReleaseConfig(env); 13 | 14 | expect(config.prepare[config.prepare.length - 1]).toBe('npm install'); 15 | expect(config.test[0]).toBe('npm run travis'); 16 | expect(config.build[0]).toBe('npm run build'); 17 | }); 18 | }); 19 | 20 | describe('and yarn is the detected npm client', () => { 21 | const env = Object.assign({}, withBuildEnv, { npmClient: 'yarn' }); 22 | 23 | it('returns a configuration with the build step', () => { 24 | const config = buildReleaseConfig(env); 25 | 26 | expect(config.prepare[config.prepare.length - 1]).toBe('yarn install'); 27 | expect(config.test[0]).toBe('yarn travis'); 28 | expect(config.build[0]).toBe('yarn build'); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('when build script is not defined in package.json', () => { 34 | const withoutBuildEnv = Object.assign({}, baseEnv, { 35 | npmClient: 'yarn', 36 | withBuildStep: false 37 | }); 38 | 39 | it('returns a configuration without the build step', () => { 40 | const config = buildReleaseConfig(withoutBuildEnv); 41 | 42 | expect(config.build).toBeUndefined(); 43 | }); 44 | }); 45 | 46 | describe('when the configured branch is other than master', () => { 47 | const customBranchEnv = Object.assign({}, baseEnv, { 48 | npmClient: 'yarn', 49 | branch: 'my-branch' 50 | }); 51 | 52 | it('returns a configuration with a check ou step to the custom branch', () => { 53 | const config = buildReleaseConfig(customBranchEnv); 54 | 55 | expect(config.prepare[1]).toBe('git checkout my-branch'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | 4 | const DEFAULT_TEST_RUNNER = 'travis'; 5 | const VALID_TEST_RUNNERS = [DEFAULT_TEST_RUNNER, 'ci']; 6 | const DEFAULT_BRANCH = 'master'; 7 | const DEFAULT_CONFIG_PATH = '.release.yml'; 8 | const PACKAGE_JSON_PATH = path.resolve(process.env.PWD, 'package.json'); 9 | const LERNA_JSON_PATH = path.resolve(process.env.PWD, 'lerna.json'); 10 | const DEFAULT_RELEASE_CONFIG_PATH = path.resolve( 11 | process.env.PWD, 12 | '.release.yml' 13 | ); 14 | const GIT_PATH = path.resolve(process.env.PWD, '.git'); 15 | const NVM_PATH = path.resolve(os.homedir(), '.nvm'); 16 | const NVM_CONFIG_PATH = path.resolve(process.env.PWD, '.nvmrc'); 17 | 18 | module.exports = { 19 | DEFAULT_TEST_RUNNER, 20 | VALID_TEST_RUNNERS, 21 | DEFAULT_BRANCH, 22 | DEFAULT_CONFIG_PATH, 23 | PACKAGE_JSON_PATH, 24 | LERNA_JSON_PATH, 25 | DEFAULT_RELEASE_CONFIG_PATH, 26 | GIT_PATH, 27 | NVM_PATH, 28 | NVM_CONFIG_PATH 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const command = require('./command'); 4 | const { 5 | DEFAULT_CONFIG_PATH, 6 | DEFAULT_BRANCH, 7 | VALID_TEST_RUNNERS, 8 | DEFAULT_TEST_RUNNER 9 | } = require('./constants'); 10 | const { npmClient, publishClient } = require('./client'); 11 | const { readReleaseConfig, buildReleaseConfig } = require('./config'); 12 | const { packageJson } = require('./package'); 13 | const { 14 | validateNodeVersion, 15 | validatePkgRoot, 16 | validateTestRunner, 17 | validateLerna, 18 | hasBuildScript 19 | } = require('./validations'); 20 | 21 | const buildReleaseEnvironment = ({ 22 | branch = DEFAULT_BRANCH, 23 | configPath = DEFAULT_CONFIG_PATH, 24 | quiet = false 25 | }) => { 26 | validateNodeVersion(); 27 | validatePkgRoot(); 28 | 29 | const pkg = packageJson(); 30 | const testRunner = 31 | VALID_TEST_RUNNERS.find(script => script in pkg.scripts) || 32 | DEFAULT_TEST_RUNNER; 33 | 34 | try { 35 | validateTestRunner(testRunner); 36 | } catch (e) { 37 | if (!quiet) { 38 | throw e; 39 | } 40 | } 41 | 42 | const client = publishClient(); 43 | if (client === 'lerna') { 44 | validateLerna(); 45 | } 46 | 47 | return { 48 | publishClient: publishClient(), 49 | npmClient: npmClient(), 50 | branch: branch, 51 | configPath: configPath, 52 | testRunner: testRunner, 53 | withBuildStep: hasBuildScript() 54 | }; 55 | }; 56 | 57 | const loadReleaseConfig = env => { 58 | const configPath = path.isAbsolute(env.configPath) 59 | ? env.configPath 60 | : path.resolve(process.env.PWD, env.configPath); 61 | 62 | if (fs.existsSync(configPath)) { 63 | return readReleaseConfig(fs.readFileSync(configPath, 'utf8')); 64 | } else if (env.configPath !== DEFAULT_CONFIG_PATH) { 65 | throw new Error( 66 | `The configuration file \`${env.configPath}\` does not exist.` 67 | ); 68 | } 69 | 70 | return buildReleaseConfig(env); 71 | }; 72 | 73 | const execCommands = configCommands => { 74 | if (configCommands) { 75 | const commands = [].concat(configCommands); 76 | 77 | for (const cmd of commands) { 78 | command.exec(cmd); 79 | } 80 | } 81 | }; 82 | 83 | const currentCommitId = () => 84 | command 85 | .exec('git rev-parse HEAD', {}) 86 | .toString() 87 | .trim(); 88 | 89 | const rollbackCommit = commitId => command.exec(`git reset --hard ${commitId}`); 90 | 91 | const versionTransformer = (version, _answers, flags) => 92 | flags.isFinal && version[0] !== 'v' ? `v${version}` : version; 93 | 94 | module.exports = { 95 | buildReleaseEnvironment, 96 | loadReleaseConfig, 97 | execCommands, 98 | currentCommitId, 99 | rollbackCommit, 100 | versionTransformer 101 | }; 102 | -------------------------------------------------------------------------------- /src/utils/index.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const childProcess = require('child_process'); 3 | const { execSync } = childProcess; 4 | const { 5 | DEFAULT_TEST_RUNNER, 6 | DEFAULT_BRANCH, 7 | DEFAULT_CONFIG_PATH 8 | } = require('./constants'); 9 | const { 10 | buildReleaseEnvironment, 11 | loadReleaseConfig, 12 | execCommands, 13 | currentCommitId, 14 | rollbackCommit, 15 | versionTransformer 16 | } = require('./index'); 17 | const config = require('./config'); 18 | const validations = require('./validations'); 19 | const client = require('./client'); 20 | 21 | jest.mock('fs'); 22 | jest.mock('child_process'); 23 | jest.mock('./config'); 24 | jest.mock('./validations'); 25 | jest.mock('./client'); 26 | 27 | const mockTestRunner = testRunner => 28 | require('fs').__setReadFileSyncReturnValue( 29 | 'package.json', 30 | JSON.stringify({ 31 | scripts: { 32 | [testRunner]: 'jest' 33 | } 34 | }) 35 | ); 36 | 37 | describe('buildReleaseEnvironment', () => { 38 | const baseOptions = {}; 39 | const throwError = () => { 40 | throw new Error(); 41 | }; 42 | 43 | describe('when branch is passed', () => { 44 | beforeAll(() => mockTestRunner('test')); 45 | 46 | it('contains the passed in branch', () => { 47 | const env = buildReleaseEnvironment({ branch: 'my-branch' }); 48 | 49 | expect(env.branch).toBe('my-branch'); 50 | }); 51 | }); 52 | 53 | describe('when branch is not passed', () => { 54 | it('contains the default branch', () => { 55 | const env = buildReleaseEnvironment({}); 56 | 57 | expect(env.branch).toBe(DEFAULT_BRANCH); 58 | }); 59 | }); 60 | 61 | describe('when configPath is passed', () => { 62 | beforeAll(() => mockTestRunner('test')); 63 | 64 | it('contains the passed in config path', () => { 65 | const env = buildReleaseEnvironment({ configPath: '.pre-release.yml' }); 66 | 67 | expect(env.configPath).toBe('.pre-release.yml'); 68 | }); 69 | }); 70 | 71 | describe('when configPath is not passed', () => { 72 | it('contains the default config path', () => { 73 | const env = buildReleaseEnvironment({}); 74 | 75 | expect(env.configPath).toBe(DEFAULT_CONFIG_PATH); 76 | }); 77 | }); 78 | 79 | describe('when errors are silenced', () => { 80 | const options = Object.assign({}, baseOptions, { quiet: true }); 81 | 82 | describe('and PWD is not package root', () => { 83 | it('throws an error', () => { 84 | validations.validatePkgRoot.mockImplementation(throwError); 85 | 86 | expect(() => buildReleaseEnvironment(options)).toThrow(); 87 | 88 | validations.validatePkgRoot.mockRestore(); 89 | }); 90 | }); 91 | 92 | describe('and the detected test runner is invalid', () => { 93 | beforeAll(() => { 94 | mockTestRunner('test'); 95 | validations.validateTestRunner.mockImplementation(throwError); 96 | }); 97 | 98 | afterAll(() => validations.validateTestRunner.mockRestore()); 99 | 100 | it('does not throw an error', () => { 101 | expect(() => buildReleaseEnvironment(options)).not.toThrow(); 102 | }); 103 | 104 | it('returns the default test runner', () => { 105 | const env = buildReleaseEnvironment(options); 106 | 107 | expect(env.testRunner).toBe(DEFAULT_TEST_RUNNER); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('when errors are not silenced', () => { 113 | const options = baseOptions; 114 | 115 | describe('and PWD is not package root', () => { 116 | it('throws an error', () => { 117 | validations.validatePkgRoot.mockImplementation(throwError); 118 | 119 | expect(() => buildReleaseEnvironment(options)).toThrow(); 120 | 121 | validations.validatePkgRoot.mockRestore(); 122 | }); 123 | }); 124 | 125 | describe('and the detected test runner is invalid', () => { 126 | beforeAll(() => mockTestRunner('test')); 127 | 128 | it('throws an error', () => { 129 | validations.validateTestRunner.mockImplementation(throwError); 130 | 131 | expect(() => buildReleaseEnvironment(options)).toThrow(); 132 | 133 | validations.validateTestRunner.mockRestore(); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('when lerna is the detected client', () => { 139 | const options = baseOptions; 140 | beforeAll(() => client.publishClient.mockReturnValue('lerna')); 141 | 142 | describe('and lerna was bootstrapped', () => { 143 | it('does not throw an error', () => { 144 | expect(() => buildReleaseEnvironment(options)).not.toThrow(); 145 | }); 146 | }); 147 | 148 | describe('and lerna was not bootstrapped', () => { 149 | it('throws an error', () => { 150 | validations.validateLerna.mockImplementation(throwError); 151 | 152 | expect(() => buildReleaseEnvironment(options)).toThrow(); 153 | 154 | validations.validateLerna.mockRestore(); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('loadReleaseConfig', () => { 161 | describe('when the default configPath is passed', () => { 162 | describe('and .release.yml is present in the root pkg directory', () => { 163 | it('reads the custom release config from the file', () => { 164 | const MOCKED_FILES = ['.release.yml']; 165 | 166 | require('fs').__setMockFiles(MOCKED_FILES); 167 | require('fs').__setReadFileSyncReturnValue( 168 | DEFAULT_CONFIG_PATH, 169 | 'file contents' 170 | ); 171 | config.readReleaseConfig.mockReturnValue('configuration'); 172 | 173 | loadReleaseConfig({ configPath: DEFAULT_CONFIG_PATH }); 174 | 175 | expect(config.readReleaseConfig).toHaveBeenCalledWith('file contents'); 176 | }); 177 | }); 178 | 179 | describe('and .release.yml is not present in the root pkg directory', () => { 180 | it('loads the default configuration', () => { 181 | require('fs').__setMockFiles([]); 182 | 183 | loadReleaseConfig({ configPath: DEFAULT_CONFIG_PATH }); 184 | 185 | expect(config.buildReleaseConfig).toHaveBeenCalled(); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('when a non-default configPath is passed', () => { 191 | describe('and the config file is present in the file system', () => { 192 | beforeAll(() => { 193 | const MOCKED_FILES = ['.pre-release.yml', '/path/.release.yml']; 194 | 195 | require('fs').__setMockFiles(MOCKED_FILES); 196 | require('fs').__setReadFileSyncReturnValue('.pre-release.yml', 'a'); 197 | require('fs').__setReadFileSyncReturnValue('/path/.release.yml', 'b'); 198 | }); 199 | 200 | it('reads the release config from a relative path', () => { 201 | const fileExistsSpy = jest.spyOn(require('fs'), 'existsSync'); 202 | 203 | loadReleaseConfig({ configPath: '.pre-release.yml' }); 204 | 205 | expect(config.readReleaseConfig).toHaveBeenCalledWith('a'); 206 | expect(fileExistsSpy).toHaveBeenCalledWith( 207 | path.resolve(process.env.PWD, '.pre-release.yml') 208 | ); 209 | }); 210 | 211 | it('reads the release config from an absolute path', () => { 212 | const fileExistsSpy = jest.spyOn(require('fs'), 'existsSync'); 213 | 214 | loadReleaseConfig({ configPath: '/path/.release.yml' }); 215 | 216 | expect(config.readReleaseConfig).toHaveBeenCalledWith('b'); 217 | expect(fileExistsSpy).toHaveBeenCalledWith('/path/.release.yml'); 218 | }); 219 | }); 220 | 221 | describe('and the config file is not present in the file system', () => { 222 | it('throws an error', () => { 223 | require('fs').__setMockFiles([]); 224 | 225 | expect(() => { 226 | loadReleaseConfig({ configPath: '.pre-release.yml' }); 227 | }).toThrow(); 228 | }); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('execCommands', () => { 234 | childProcess.__permitCommands(['mycommand', 'mySecondCommand']); 235 | 236 | afterEach(() => { 237 | execSync.mockClear(); 238 | }); 239 | 240 | describe('when config commands are undefined', () => { 241 | it('execCommand is not called', () => { 242 | execCommands(); 243 | 244 | expect(execSync).not.toHaveBeenCalled(); 245 | }); 246 | }); 247 | 248 | describe('when a single command is being passed', () => { 249 | it('execCommand is called 1 time', () => { 250 | execCommands('mycommand --some-arg'); 251 | 252 | expect(execSync).toHaveBeenCalledTimes(1); 253 | expect(execSync.mock.calls[0][0]).toBe('mycommand --some-arg'); 254 | }); 255 | }); 256 | 257 | describe('when multiple commands are being passed as an array', () => { 258 | it('execCommand is called multiple times', () => { 259 | execCommands(['mycommand --some-arg', 'mySecondCommand --some-arg']); 260 | 261 | expect(execSync).toHaveBeenCalledTimes(2); 262 | expect(execSync.mock.calls[0][0]).toBe('mycommand --some-arg'); 263 | expect(execSync.mock.calls[1][0]).toBe('mySecondCommand --some-arg'); 264 | }); 265 | }); 266 | }); 267 | 268 | describe('currentCommitId', () => { 269 | it("returns the HEAD commit's ID", () => { 270 | childProcess.__permitCommands(['git']); 271 | childProcess.__setReturnValues({ 272 | 'git rev-parse HEAD': Buffer.from( 273 | 'f030084d079ba5e07de6546879a84e23de536db1\n', 274 | 'utf8' 275 | ) 276 | }); 277 | 278 | const commitId = currentCommitId(); 279 | 280 | expect(execSync).toHaveBeenCalled(); 281 | expect(commitId).toBe('f030084d079ba5e07de6546879a84e23de536db1'); 282 | 283 | execSync.mockClear(); 284 | }); 285 | }); 286 | 287 | describe('rollbackCommit', () => { 288 | it('rolls back to the specified commit', () => { 289 | childProcess.__permitCommands(['git']); 290 | 291 | rollbackCommit('f030084d079ba5e07de6546879a84e23de536db1'); 292 | 293 | expect(execSync).toHaveBeenCalled(); 294 | expect(execSync.mock.calls[0][0]).toBe( 295 | 'git reset --hard f030084d079ba5e07de6546879a84e23de536db1' 296 | ); 297 | 298 | execSync.mockClear(); 299 | }); 300 | }); 301 | 302 | describe('versionTransformer', () => { 303 | describe('when isFinal flag is true', () => { 304 | const isFinal = true; 305 | 306 | it('returns the version with the "v" prefix', () => { 307 | expect(versionTransformer('9.11.1', [], { isFinal })).toBe('v9.11.1'); 308 | expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1'); 309 | }); 310 | }); 311 | 312 | describe('when isFinal flag is false', () => { 313 | const isFinal = false; 314 | 315 | it('returns the version as input by the user', () => { 316 | expect(versionTransformer('9.11.1', [], { isFinal })).toBe('9.11.1'); 317 | expect(versionTransformer('v9.11.1', [], { isFinal })).toBe('v9.11.1'); 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /src/utils/package/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { PACKAGE_JSON_PATH } = require('../constants'); 3 | 4 | let pkg; 5 | const packageJson = () => { 6 | if (pkg) return pkg; 7 | 8 | try { 9 | pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')); 10 | } catch (e) { 11 | throw new Error( 12 | 'Your package.json could not be parsed. Make sure the manifest is valid.' 13 | ); 14 | } 15 | 16 | return pkg; 17 | }; 18 | 19 | module.exports = { 20 | packageJson 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/package/index.spec.js: -------------------------------------------------------------------------------- 1 | const { packageJson } = require('./index'); 2 | 3 | jest.mock('fs'); 4 | 5 | describe('packageJson', () => { 6 | it('returns the package.json as a JSON object', () => { 7 | require('fs').__setReadFileSyncReturnValue( 8 | 'package.json', 9 | JSON.stringify({ 10 | scripts: { 11 | test: 'jest' 12 | } 13 | }) 14 | ); 15 | 16 | expect(packageJson()).toEqual({ 17 | scripts: { 18 | test: 'jest' 19 | } 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils/validations/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { execSync } = require('child_process'); 3 | const { packageJson } = require('../package'); 4 | const { 5 | VALID_TEST_RUNNERS, 6 | GIT_PATH, 7 | NVM_CONFIG_PATH, 8 | PACKAGE_JSON_PATH, 9 | LERNA_JSON_PATH 10 | } = require('../constants'); 11 | 12 | const validateNodeVersion = () => { 13 | const expectedNodeVersion = fs.readFileSync(NVM_CONFIG_PATH, { encoding: 'utf-8' }).trim(); 14 | const actualNodeVersion = execSync('node -v', { encoding: 'utf-8' }).trim(); 15 | 16 | if (expectedNodeVersion !== actualNodeVersion) { 17 | throw new Error(`Expected Node version to be ${expectedNodeVersion} but instead it is ${actualNodeVersion}`); 18 | } 19 | }; 20 | 21 | const validatePkgRoot = () => { 22 | if (!fs.existsSync(PACKAGE_JSON_PATH)) { 23 | throw new Error('Run this script from the root of your package.'); 24 | } 25 | }; 26 | 27 | const validateTestRunner = testRunner => { 28 | if (!testRunner) { 29 | throw new Error( 30 | 'Your package.json must define at least one of the two required scripts: "travis", "ci"' 31 | ); 32 | } 33 | }; 34 | 35 | const validateLerna = () => { 36 | if (fs.existsSync(LERNA_JSON_PATH)) return; 37 | 38 | throw new Error( 39 | 'Lerna configuration could not be found. Make sure to bootstrap lerna first.' 40 | ); 41 | }; 42 | 43 | const isGitProject = () => fs.existsSync(GIT_PATH); 44 | 45 | const nvmrcExists = () => fs.existsSync(NVM_CONFIG_PATH); 46 | 47 | const hasBuildScript = () => { 48 | const pkg = packageJson(); 49 | if (!pkg.scripts) { 50 | return false; 51 | } 52 | 53 | return 'build' in pkg.scripts; 54 | }; 55 | 56 | const hasCiScript = () => { 57 | const pkg = packageJson(); 58 | if (!pkg.scripts) { 59 | return false; 60 | } 61 | 62 | return VALID_TEST_RUNNERS.some(testRunner => testRunner in pkg.scripts); 63 | }; 64 | 65 | module.exports = { 66 | validateNodeVersion, 67 | validatePkgRoot, 68 | validateTestRunner, 69 | validateLerna, 70 | isGitProject, 71 | nvmrcExists, 72 | hasBuildScript, 73 | hasCiScript 74 | }; 75 | -------------------------------------------------------------------------------- /src/utils/validations/index.spec.js: -------------------------------------------------------------------------------- 1 | const utils = require('../package'); 2 | const { VALID_TEST_RUNNERS } = require('../constants'); 3 | const { 4 | validateNodeVersion, 5 | validatePkgRoot, 6 | validateTestRunner, 7 | validateLerna, 8 | isGitProject, 9 | nvmrcExists, 10 | hasBuildScript, 11 | hasCiScript 12 | } = require('./'); 13 | 14 | jest.mock('fs'); 15 | jest.mock('child_process'); 16 | jest.mock('../package'); 17 | 18 | describe('validateNodeVersion', () => { 19 | beforeEach(() => { 20 | require('fs').__setMockFiles(['.nvmrc']); 21 | require('fs').__setReadFileSyncReturnValue('.nvmrc', 'v12.18.0'); 22 | require('child_process').__permitCommands(['node']); 23 | }); 24 | 25 | describe('with incorrect Node version', () => { 26 | beforeEach(() => { 27 | require('child_process').__setReturnValues({ 28 | 'node -v': 'v10.16.0', 29 | }); 30 | }); 31 | 32 | it('throws an error', () => { 33 | expect(validateNodeVersion).toThrow(); 34 | }); 35 | }); 36 | 37 | describe('with correct Node version', () => { 38 | beforeEach(() => { 39 | require('child_process').__setReturnValues({ 40 | 'node -v': 'v12.18.0', 41 | }); 42 | }); 43 | 44 | it('does not throw an error', () => { 45 | expect(validateNodeVersion).not.toThrow(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('validatePkgRoot', () => { 51 | beforeEach(() => { 52 | require('fs').__setMockFiles([]); 53 | }); 54 | 55 | describe('when not run from the root of the package', () => { 56 | const MOCKED_FILES = ['not_package.json']; 57 | 58 | it('throws an error', () => { 59 | require('fs').__setMockFiles(MOCKED_FILES); 60 | 61 | expect(validatePkgRoot).toThrow( 62 | 'Run this script from the root of your package.' 63 | ); 64 | }); 65 | }); 66 | 67 | describe('when run from the root of the package', () => { 68 | const MOCKED_FILES = ['package.json']; 69 | 70 | it('returns', () => { 71 | require('fs').__setMockFiles(MOCKED_FILES); 72 | 73 | expect(validatePkgRoot).not.toThrow(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('validateTestRunner', () => { 79 | describe('when test runner is defined', () => { 80 | const testRunner = 'travis'; 81 | 82 | it('does not throw an error', () => { 83 | expect(() => validateTestRunner(testRunner)).not.toThrow(); 84 | }); 85 | }); 86 | 87 | describe('when test runner is undefined', () => { 88 | const testRunner = undefined; 89 | 90 | it('throws an error', () => { 91 | expect(() => validateTestRunner(testRunner)).toThrow(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('validateLerna', () => { 97 | describe('when lerna was bootstrapped', () => { 98 | const MOCKED_FILES = ['lerna.json']; 99 | 100 | it('does not throw an error', () => { 101 | require('fs').__setMockFiles(MOCKED_FILES); 102 | 103 | expect(validateLerna).not.toThrow(); 104 | }); 105 | }); 106 | 107 | describe('when lerna was not bootstrapped', () => { 108 | it('throws an error', () => { 109 | require('fs').__setMockFiles([]); 110 | 111 | expect(validateLerna).toThrow(); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('isGitProject', () => { 117 | afterEach(() => { 118 | require('fs').__setMockFiles([]); 119 | }); 120 | 121 | describe('when .git directory exists', () => { 122 | const MOCKED_FILES = ['.git']; 123 | 124 | it('returns true', () => { 125 | require('fs').__setMockFiles(MOCKED_FILES); 126 | 127 | expect(isGitProject()).toBe(true); 128 | }); 129 | }); 130 | 131 | describe('when .git directory does not exist', () => { 132 | it('returns false', () => { 133 | expect(isGitProject()).toBe(false); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('nvmrcExists', () => { 139 | afterEach(() => { 140 | require('fs').__setMockFiles([]); 141 | }); 142 | 143 | describe('when .nvmrc file exists', () => { 144 | const MOCKED_FILES = ['.nvmrc']; 145 | 146 | it('returns true', () => { 147 | require('fs').__setMockFiles(MOCKED_FILES); 148 | 149 | expect(nvmrcExists()).toBe(true); 150 | }); 151 | }); 152 | 153 | describe('when .nvmrc file does not exist', () => { 154 | it('returns false', () => { 155 | expect(nvmrcExists()).toBe(false); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('hasBuildScript', () => { 161 | describe('when a build script is defined in package.json', () => { 162 | it('returns false', () => { 163 | utils.packageJson.mockReturnValue({ scripts: { build: 'webpack' } }); 164 | 165 | expect(hasBuildScript()).toBe(true); 166 | }); 167 | }); 168 | 169 | describe('when a build script is not defined in package.json', () => { 170 | it('returns false', () => { 171 | utils.packageJson.mockReturnValue({ scripts: {} }); 172 | 173 | expect(hasBuildScript()).toBe(false); 174 | }); 175 | }); 176 | 177 | describe('when scripts are not defined in package.json', () => { 178 | it('returns false', () => { 179 | utils.packageJson.mockReturnValue({}); 180 | 181 | expect(hasBuildScript()).toBe(false); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('hasCiScript', () => { 187 | for (const testRunner of VALID_TEST_RUNNERS) { 188 | describe(`when a ${testRunner} script is defined in package.json`, () => { 189 | it('returns true', () => { 190 | utils.packageJson.mockReturnValue({ 191 | scripts: { ci: 'eslint && jest' } 192 | }); 193 | 194 | expect(hasCiScript()).toBe(true); 195 | }); 196 | }); 197 | } 198 | 199 | describe('when a valid test runner script is not defined in package.json', () => { 200 | it('returns false', () => { 201 | utils.packageJson.mockReturnValue({ scripts: {} }); 202 | 203 | expect(hasCiScript()).toBe(false); 204 | }); 205 | }); 206 | 207 | describe('when scripts are not defined in package.json', () => { 208 | it('returns false', () => { 209 | utils.packageJson.mockReturnValue({}); 210 | 211 | expect(hasCiScript()).toBe(false); 212 | }); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------