├── .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 |
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 | [](https://badge.fury.io/js/node-publisher)
14 | 
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 |
--------------------------------------------------------------------------------