bulk-release 2.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +540 -0
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/package.json +44 -0
- package/src/main/js/analyze.js +70 -0
- package/src/main/js/changelog.js +42 -0
- package/src/main/js/cli.js +7 -0
- package/src/main/js/config.js +52 -0
- package/src/main/js/deps.js +90 -0
- package/src/main/js/gh.js +49 -0
- package/src/main/js/git.js +101 -0
- package/src/main/js/index.js +1 -0
- package/src/main/js/log.js +63 -0
- package/src/main/js/meta.js +171 -0
- package/src/main/js/npm.js +65 -0
- package/src/main/js/processor.js +155 -0
- package/src/main/js/util.js +46 -0
- package/src/test/js/test-utils.js +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# zx-bulk-release
|
|
2
|
+
> [zx](https://github.com/google/zx)-based alternative for [multi-semantic-release](https://github.com/dhoulb/multi-semantic-release)
|
|
3
|
+
|
|
4
|
+
[](https://github.com/semrel-extra/zx-bulk-release/actions)
|
|
5
|
+
[](https://codeclimate.com/github/semrel-extra/zx-bulk-release/maintainability)
|
|
6
|
+
[](https://codeclimate.com/github/semrel-extra/zx-bulk-release/test_coverage)
|
|
7
|
+
[](https://www.npmjs.com/package/zx-bulk-release)
|
|
8
|
+
|
|
9
|
+
🚧 Work in progress. Early access preview
|
|
10
|
+
|
|
11
|
+
## Key features
|
|
12
|
+
* [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) trigger semantic releases.
|
|
13
|
+
* Automated cross-pkg version bumping.
|
|
14
|
+
* Predictable [toposort](https://githib.com/semrel-extra/topo)-driven flow.
|
|
15
|
+
* No default branch blocking (no release commits).
|
|
16
|
+
* Pkg changelogs go to `changelog` branch (configurable).
|
|
17
|
+
* Docs are published to `gh-pages` branch (configurable).
|
|
18
|
+
* No extra builds. The required deps are fetched from the pkg registry (`npmFetch` config opt).
|
|
19
|
+
|
|
20
|
+
## Roadmap
|
|
21
|
+
* [x] Store release metrics to `meta`.
|
|
22
|
+
* [ ] ~~Self-repair. Restore broken/missing metadata from external registries (npm, pypi, m2)~~. Tags should be the only source of truth
|
|
23
|
+
* [ ] Multistack. Add support for java/kt/py.
|
|
24
|
+
* [ ] Semaphore. Let several release agents to serve the monorepo at the same time.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
* macOS / linux
|
|
28
|
+
* Node.js >= 16.0.0
|
|
29
|
+
* npm >=7 / yarn >= 3
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
### Install
|
|
33
|
+
```shell
|
|
34
|
+
yarn add zx-bulk-release
|
|
35
|
+
```
|
|
36
|
+
### CLI
|
|
37
|
+
```shell
|
|
38
|
+
GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
|
|
39
|
+
```
|
|
40
|
+
| Flag | Description | Default |
|
|
41
|
+
|------------------------------|----------------------------------------------------------------|------------------|
|
|
42
|
+
| `--ignore` | Packages to ignore: `a, b` | |
|
|
43
|
+
| `--include-private` | Include `private` packages | `false` |
|
|
44
|
+
| `--concurrency` | `build/publish` threads limit | `os.cpus.length` |
|
|
45
|
+
| `--no-build` | Skip `buildCmd` invoke | |
|
|
46
|
+
| `--no-npm-fetch` | Disable npm artifacts fetching | |
|
|
47
|
+
| `--dry-run` / `--no-publish` | Disable any publish logic | |
|
|
48
|
+
| `--report` | Persist release state to file | |
|
|
49
|
+
| `--debug` | Enable [zx](https://github.com/google/zx#verbose) verbose mode | |
|
|
50
|
+
|
|
51
|
+
### JS API
|
|
52
|
+
```js
|
|
53
|
+
import { run } from 'zx-bulk-release'
|
|
54
|
+
|
|
55
|
+
const cwd = '/foo/bar'
|
|
56
|
+
const env = {GH_TOKEN: 'foo', NPM_TOKEN: 'bar'}
|
|
57
|
+
const flags = {dryRun: true}
|
|
58
|
+
|
|
59
|
+
await run({
|
|
60
|
+
cwd, // Defaults to process.cwd()
|
|
61
|
+
flags, // Defaults to process.env
|
|
62
|
+
env // Defaults to minimist-parsed `process.argv.slice(2)`
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Config
|
|
67
|
+
Any [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) compliant format: `.releaserc`, `.release.json`, `.release.yaml`, etc.
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"cmd": "yarn && yarn build && yarn test",
|
|
71
|
+
"npmFetch": true,
|
|
72
|
+
"changelog": "changelog",
|
|
73
|
+
"ghPages": "gh-pages"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Demo
|
|
78
|
+
* [demo-zx-bulk-release](https://github.com/semrel-extra/demo-zx-bulk-release)
|
|
79
|
+
* [qiwi/pijma](https://github.com/qiwi/pijma)
|
|
80
|
+
|
|
81
|
+
### Output
|
|
82
|
+
|
|
83
|
+
[Compact and clear logs](https://github.com/semrel-extra/demo-zx-bulk-release/runs/7090161341?check_suite_focus=true#step:6:1)
|
|
84
|
+
|
|
85
|
+
```shell
|
|
86
|
+
Run npm_config_yes=true npx zx-bulk-release
|
|
87
|
+
zx-bulk-release
|
|
88
|
+
[@semrel-extra/zxbr-test-a] semantic changes [
|
|
89
|
+
{
|
|
90
|
+
group: 'Fixes & improvements',
|
|
91
|
+
releaseType: 'patch',
|
|
92
|
+
change: 'fix(a): random',
|
|
93
|
+
subj: 'fix(a): random',
|
|
94
|
+
body: '',
|
|
95
|
+
short: '6ff25bd',
|
|
96
|
+
hash: '6ff25bd421755b929ef2b58f35c727670fd93849'
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
[@semrel-extra/zxbr-test-a] run cmd 'yarn && yarn build && yarn test'
|
|
100
|
+
[@semrel-extra/zxbr-test-a] push release tag 2022.6.27-semrel-extra.zxbr-test-a.1.8.1-f0
|
|
101
|
+
[@semrel-extra/zxbr-test-a] push artifact to branch 'meta'
|
|
102
|
+
[@semrel-extra/zxbr-test-a] push changelog
|
|
103
|
+
[@semrel-extra/zxbr-test-a] publish npm package @semrel-extra/zxbr-test-a 1.8.1 to https://registry.npmjs.org
|
|
104
|
+
[@semrel-extra/zxbr-test-a] create gh release
|
|
105
|
+
[@semrel-extra/zxbr-test-b] semantic changes [
|
|
106
|
+
{
|
|
107
|
+
group: 'Dependencies',
|
|
108
|
+
releaseType: 'patch',
|
|
109
|
+
change: 'perf',
|
|
110
|
+
subj: 'perf: @semrel-extra/zxbr-test-a updated to 1.8.1'
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
[@semrel-extra/zxbr-test-b] run cmd 'yarn && yarn build && yarn test'
|
|
114
|
+
[@semrel-extra/zxbr-test-b] push release tag 2022.6.27-semrel-extra.zxbr-test-b.1.3.5-f0
|
|
115
|
+
[@semrel-extra/zxbr-test-b] push artifact to branch 'meta'
|
|
116
|
+
[@semrel-extra/zxbr-test-b] push changelog
|
|
117
|
+
[@semrel-extra/zxbr-test-b] publish npm package @semrel-extra/zxbr-test-b 1.3.5 to https://registry.npmjs.org
|
|
118
|
+
[@semrel-extra/zxbr-test-b] create gh release
|
|
119
|
+
[@semrel-extra/zxbr-test-d] semantic changes [
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Implementation notes
|
|
123
|
+
### Flow
|
|
124
|
+
```js
|
|
125
|
+
try {
|
|
126
|
+
const {packages, queue, root} = await topo({cwd, flags})
|
|
127
|
+
console.log('queue:', queue)
|
|
128
|
+
|
|
129
|
+
for (let name of queue) {
|
|
130
|
+
const pkg = packages[name]
|
|
131
|
+
|
|
132
|
+
await analyze(pkg, packages, root)
|
|
133
|
+
|
|
134
|
+
if (pkg.changes.length === 0) continue
|
|
135
|
+
|
|
136
|
+
await build(pkg, packages)
|
|
137
|
+
|
|
138
|
+
if (flags.dryRun) continue
|
|
139
|
+
|
|
140
|
+
await publish(pkg)
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error(e)
|
|
144
|
+
throw e
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `topo`
|
|
149
|
+
[Toposort](https://github.com/semrel-extra/topo) is used to resolve the pkg release queue.
|
|
150
|
+
By default, it omits the packages marked as `private`. You can override this by setting the `--include-private` flag.
|
|
151
|
+
|
|
152
|
+
### `analyze`
|
|
153
|
+
Determines pkg changes, release type, next version etc.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
export const analyze = async (pkg, packages, root) => {
|
|
157
|
+
pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
|
|
158
|
+
pkg.latest = await getLatest(pkg)
|
|
159
|
+
|
|
160
|
+
const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
|
|
161
|
+
const depsChanges = await updateDeps(pkg, packages)
|
|
162
|
+
const changes = [...semanticChanges, ...depsChanges]
|
|
163
|
+
|
|
164
|
+
pkg.changes = changes
|
|
165
|
+
pkg.version = resolvePkgVersion(changes, pkg.latest.tag?.version || pkg.manifest.version)
|
|
166
|
+
pkg.manifest.version = pkg.version
|
|
167
|
+
|
|
168
|
+
console.log(`[${pkg.name}] semantic changes`, changes)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `build`
|
|
173
|
+
Applies `config.cmd` to build pkg assets: bundles, docs, etc.
|
|
174
|
+
```js
|
|
175
|
+
export const build = async (pkg, packages) => {
|
|
176
|
+
// ...
|
|
177
|
+
if (!pkg.fetched && config.cmd) {
|
|
178
|
+
console.log(`[${pkg.name}] run cmd '${config.cmd}'`)
|
|
179
|
+
await $.o({cwd: pkg.absPath, quote: v => v})`${config.cmd}`
|
|
180
|
+
}
|
|
181
|
+
// ...
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `publish`
|
|
186
|
+
Publish the pkg to git, npm, gh-pages, gh-release, etc.
|
|
187
|
+
```js
|
|
188
|
+
export const publish = async (pkg) => {
|
|
189
|
+
await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
|
|
190
|
+
await pushTag(pkg)
|
|
191
|
+
await pushMeta(pkg)
|
|
192
|
+
await pushChangelog(pkg)
|
|
193
|
+
await npmPublish(pkg)
|
|
194
|
+
await ghRelease(pkg)
|
|
195
|
+
await ghPages(pkg)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Tags
|
|
200
|
+
[Lerna](https://github.com/lerna/lerna) tags (like `@pkg/name@v1.0.0-beta.0`) are suitable for monorepos, but they don’t follow [semver spec](https://semver.org/). Therefore, we propose another contract:
|
|
201
|
+
```js
|
|
202
|
+
'2022.6.13-optional-org.pkg-name.v1.0.0-beta.1+sha.1-f0'
|
|
203
|
+
// date name version format
|
|
204
|
+
```
|
|
205
|
+
Note, [npm-package-name charset](https://www.npmjs.com/package/validate-npm-package-name) is wider than [semver](https://semver.org/spec/v2.0.0.html#spec-item-4), so we need a pinch of [base64url magic](https://stackoverflow.com/questions/55389211/string-based-data-encoding-base64-vs-base64url) for some cases.
|
|
206
|
+
```js
|
|
207
|
+
'2022.6.13-examplecom.v1.0.0.ZXhhbXBsZS5jb20-f1'
|
|
208
|
+
// date name ver b64 format
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### env vars
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
export const parseEnv = (env = process.env) => {
|
|
215
|
+
const {GH_USER, GH_USERNAME, GITHUB_USER, GITHUB_USERNAME, GH_TOKEN, GITHUB_TOKEN, NPM_TOKEN, NPM_REGISTRY, NPMRC, NPM_USERCONFIG, NPM_CONFIG_USERCONFIG, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL} = env
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
ghUser: GH_USER || GH_USERNAME || GITHUB_USER || GITHUB_USERNAME,
|
|
219
|
+
ghToken: GH_TOKEN || GITHUB_TOKEN,
|
|
220
|
+
npmToken: NPM_TOKEN,
|
|
221
|
+
// npmConfig suppresses npmToken
|
|
222
|
+
npmConfig: NPMRC || NPM_USERCONFIG || NPM_CONFIG_USERCONFIG,
|
|
223
|
+
npmRegistry: NPM_REGISTRY || 'https://registry.npmjs.org',
|
|
224
|
+
gitCommitterName: GIT_COMMITTER_NAME || 'Semrel Extra Bot',
|
|
225
|
+
gitCommitterEmail: GIT_COMMITTER_EMAIL || 'semrel-extra-bot@hotmail.com',
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Meta
|
|
231
|
+
|
|
232
|
+
Each release stores its result into the `meta` branch.
|
|
233
|
+
`2022-6-26-semrel-extra-zxbr-test-c-1-3-1-f0.json`
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"META_VERSION": "1",
|
|
237
|
+
"name": "@semrel-extra/zxbr-test-c",
|
|
238
|
+
"hash": "07b7df33f0159f674c940bd7bbb2652cdaef5207",
|
|
239
|
+
"version": "1.3.1",
|
|
240
|
+
"dependencies": {
|
|
241
|
+
"@semrel-extra/zxbr-test-a": "^1.4.0",
|
|
242
|
+
"@semrel-extra/zxbr-test-d": "~1.2.0"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Report
|
|
248
|
+
|
|
249
|
+
Release process state is reported to the console and to a file if `--report` flag is set to `/some/path/release-report.json`, for example.
|
|
250
|
+
```js
|
|
251
|
+
{
|
|
252
|
+
status: 'success', // 'sucess' | 'failure' | 'pending'
|
|
253
|
+
error: null, // null or Error
|
|
254
|
+
queue: ['a', 'b', 'c', 'd'] // release queue
|
|
255
|
+
packages: [{
|
|
256
|
+
name: 'a',
|
|
257
|
+
version: '1.1.0',
|
|
258
|
+
path: '/pkg/abs/path',
|
|
259
|
+
relPath: 'pkg/rel/path',
|
|
260
|
+
config: { // pkg config
|
|
261
|
+
changelog: 'changelog',
|
|
262
|
+
npmFetch: true
|
|
263
|
+
},
|
|
264
|
+
changes: [{ // semantic changes
|
|
265
|
+
group: 'Features',
|
|
266
|
+
releaseType: 'minor',
|
|
267
|
+
change: 'feat: add feat',
|
|
268
|
+
subj: 'feat: add feat',
|
|
269
|
+
body: '',
|
|
270
|
+
short: '792512c',
|
|
271
|
+
hash: '792512cccd69c6345d9d32d3d73e2591ea1776b5'
|
|
272
|
+
}],
|
|
273
|
+
tag: {
|
|
274
|
+
version: 'v1.1.0',
|
|
275
|
+
name: 'a',
|
|
276
|
+
ref: '2022.6.22-a.v1.1.0-f0'
|
|
277
|
+
},
|
|
278
|
+
releaseType: 'minor', // 'major' | 'minor' | 'patch'
|
|
279
|
+
prevVersion: '1.0.0' // previous version or null
|
|
280
|
+
}, {
|
|
281
|
+
name: 'b',
|
|
282
|
+
// ...
|
|
283
|
+
}],
|
|
284
|
+
events: [
|
|
285
|
+
{msg: ['zx-bulk-release'], scope:'~', date: 1665839585488, level: 'info'},
|
|
286
|
+
{msg: ['queue:',['a','b']], scope:'~', date: 1665839585493, level: 'info'},
|
|
287
|
+
{msg: ["run buildCmd 'yarn && yarn build && yarn test'"], scope: 'a', date: 1665839585719, level:'info'},
|
|
288
|
+
// ...
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## References
|
|
294
|
+
* [semrel-extra/zx-semrel](https://github.com/semrel-extra/zx-semrel)
|
|
295
|
+
* [dhoulb/multi-semantic-release](https://github.com/dhoulb/multi-semantic-release)
|
|
296
|
+
* [semantic-release/semantic-release](https://github.com/semantic-release/semantic-release)
|
|
297
|
+
* [conventional-changelog/releaser-tools](https://github.com/conventional-changelog/releaser-tools)
|
|
298
|
+
* [pmowrer/semantic-release-monorepo](https://github.com/pmowrer/semantic-release-monorepo)
|
|
299
|
+
* [bubkoo/semantic-release-monorepo](https://github.com/bubkoo/semantic-release-monorepo)
|
|
300
|
+
* [ext/semantic-release-lerna](https://github.com/ext/semantic-release-lerna)
|
|
301
|
+
* [jscutlery/semver](https://github.com/jscutlery/semver)
|
|
302
|
+
* [microsoft/rushstack](https://github.com/microsoft/rushstack) / [rushjs.io](https://rushjs.io/)
|
|
303
|
+
* [tophat/monodeploy](https://github.com/tophat/monodeploy)
|
|
304
|
+
* [intuit/auto](https://github.com/intuit/auto)
|
|
305
|
+
* [vercel/turborepo](https://github.com/vercel/turborepo)
|
|
306
|
+
* [lerna/lerna](https://github.com/lerna/lerna)
|
|
307
|
+
* [nrwl/nx](https://github.com/nrwl/nx)
|
|
308
|
+
* [moonrepo/moon](https://github.com/moonrepo/moon)
|
|
309
|
+
* [ojkelly/yarn.build](https://github.com/ojkelly/yarn.build)
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bulk-release",
|
|
3
|
+
"alias": "bulk-release",
|
|
4
|
+
"version": "2.2.13",
|
|
5
|
+
"description": "zx-based alternative for multi-semantic-release",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/main/js/index.js",
|
|
9
|
+
"./test-utils": "./src/test/js/test-utils.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": "./src/main/js/cli.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"src/main/js",
|
|
14
|
+
"src/test/js/test-utils.js",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "NPM_REGISTRY='http://localhost:4873' NPM_TOKEN='mRv6eIuiaggXGb9ZDFCtBA==' c8 uvu ./src/test -i fixtures -i utils && c8 report -r lcov",
|
|
21
|
+
"test:it": "NPM_REGISTRY='http://localhost:4873' NPM_TOKEN='mRv6eIuiaggXGb9ZDFCtBA==' node ./src/test/js/integration.test.js",
|
|
22
|
+
"docs": "mkdir -p docs && cp ./README.md ./docs/README.md"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@semrel-extra/topo": "^1.6.0",
|
|
26
|
+
"cosmiconfig": "^8.1.3",
|
|
27
|
+
"queuefy": "^1.2.1",
|
|
28
|
+
"zx-extra": "^2.5.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"c8": "^7.13.0",
|
|
32
|
+
"uvu": "^0.5.6",
|
|
33
|
+
"verdaccio": "^5.22.1"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/semrel-extra/zx-bulk-release.git"
|
|
41
|
+
},
|
|
42
|
+
"author": "Anton Golub <antongolub@antongolub.com>",
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {semver} from 'zx-extra'
|
|
2
|
+
import {updateDeps} from './deps.js'
|
|
3
|
+
import {formatTag} from './meta.js';
|
|
4
|
+
import {log} from './log.js'
|
|
5
|
+
import {getCommits} from './git.js'
|
|
6
|
+
|
|
7
|
+
export const analyze = async (pkg) => {
|
|
8
|
+
const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
|
|
9
|
+
const depsChanges = await updateDeps(pkg, pkg.context.packages)
|
|
10
|
+
const changes = [...semanticChanges, ...depsChanges]
|
|
11
|
+
const releaseType = getNextReleaseType(changes)
|
|
12
|
+
|
|
13
|
+
pkg.changes = changes
|
|
14
|
+
pkg.releaseType = releaseType
|
|
15
|
+
pkg.version = resolvePkgVersion(releaseType, pkg.latest.tag?.version || pkg.manifest.version)
|
|
16
|
+
pkg.manifest.version = pkg.version
|
|
17
|
+
pkg.tag = releaseType ? formatTag({name: pkg.name, version: pkg.version}) : null
|
|
18
|
+
|
|
19
|
+
log({pkg})('semantic changes', changes)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const releaseSeverityOrder = ['major', 'minor', 'patch']
|
|
23
|
+
export const semanticRules = [
|
|
24
|
+
{group: 'Features', releaseType: 'minor', prefixes: ['feat']},
|
|
25
|
+
{group: 'Fixes & improvements', releaseType: 'patch', prefixes: ['fix', 'perf', 'refactor', 'docs', 'patch']},
|
|
26
|
+
{group: 'BREAKING CHANGES', releaseType: 'major', keywords: ['BREAKING CHANGE', 'BREAKING CHANGES']},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export const getSemanticChanges = async (cwd, from, to) => {
|
|
30
|
+
const commits = await getCommits(cwd, from, to)
|
|
31
|
+
|
|
32
|
+
return analyzeCommits(commits)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const analyzeCommits = (commits) =>
|
|
36
|
+
commits.reduce((acc, {subj, body, short, hash}) => {
|
|
37
|
+
semanticRules.forEach(({group, releaseType, prefixes, keywords}) => {
|
|
38
|
+
const prefixMatcher = prefixes && new RegExp(`^(${prefixes.join('|')})(\\([a-z0-9\\-_,]+\\))?:\\s.+$`)
|
|
39
|
+
const keywordsMatcher = keywords && new RegExp(`(${keywords.join('|')}):\\s(.+)`)
|
|
40
|
+
const change = subj.match(prefixMatcher)?.[0] || body.match(keywordsMatcher)?.[2]
|
|
41
|
+
|
|
42
|
+
if (change) {
|
|
43
|
+
acc.push({
|
|
44
|
+
group,
|
|
45
|
+
releaseType,
|
|
46
|
+
change,
|
|
47
|
+
subj,
|
|
48
|
+
body,
|
|
49
|
+
short,
|
|
50
|
+
hash
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return acc
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
export const getNextReleaseType = (changes) => changes.length
|
|
58
|
+
? releaseSeverityOrder.find(type => changes.find(({releaseType}) => type === releaseType))
|
|
59
|
+
: null
|
|
60
|
+
|
|
61
|
+
export const getNextVersion = (releaseType, prevVersion) => {
|
|
62
|
+
if (!prevVersion) return '1.0.0'
|
|
63
|
+
|
|
64
|
+
return semver.inc(prevVersion, releaseType)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const resolvePkgVersion = (releaseType, prevVersion) =>
|
|
68
|
+
releaseType
|
|
69
|
+
? getNextVersion(releaseType, prevVersion)
|
|
70
|
+
: prevVersion || null
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {$} from 'zx-extra'
|
|
2
|
+
import {log} from './log.js'
|
|
3
|
+
import {fetchRepo, getRepo, pushCommit} from './git.js'
|
|
4
|
+
import {msgJoin} from './util.js'
|
|
5
|
+
import {formatTag} from './meta.js'
|
|
6
|
+
|
|
7
|
+
export const pushChangelog = async (pkg) => {
|
|
8
|
+
const {absPath: cwd, config: {changelog: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
|
|
9
|
+
if (!opts) return
|
|
10
|
+
|
|
11
|
+
log({pkg})('push changelog')
|
|
12
|
+
const [branch = 'changelog', file = `${pkg.name.replace(/[^a-z0-9-]/ig, '')}-changelog.md`, ..._msg] = typeof opts === 'string'
|
|
13
|
+
? opts.split(' ')
|
|
14
|
+
: [opts.branch, opts.file, opts.msg]
|
|
15
|
+
const _cwd = await fetchRepo({cwd, branch, basicAuth})
|
|
16
|
+
const msg = msgJoin(_msg, pkg, 'chore: update changelog ${{name}}')
|
|
17
|
+
const releaseNotes = await formatReleaseNotes(pkg)
|
|
18
|
+
|
|
19
|
+
await $.o({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
|
|
20
|
+
await pushCommit({cwd, branch, msg, gitCommitterEmail, gitCommitterName, basicAuth})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const formatReleaseNotes = async (pkg) => {
|
|
24
|
+
const {name, version, absPath: cwd, config: {ghBasicAuth: basicAuth}} = pkg
|
|
25
|
+
const {repoPublicUrl} = await getRepo(cwd, {basicAuth})
|
|
26
|
+
const tag = formatTag({name, version})
|
|
27
|
+
const releaseDiffRef = `## [${name}@${version}](${repoPublicUrl}/compare/${pkg.latest.tag?.ref}...${tag}) (${new Date().toISOString().slice(0, 10)})`
|
|
28
|
+
const releaseDetails = Object.values(pkg.changes
|
|
29
|
+
.reduce((acc, {group, subj, short, hash}) => {
|
|
30
|
+
const {commits} = acc[group] || (acc[group] = {commits: [], group})
|
|
31
|
+
const commitRef = `* ${subj}${short ? ` [${short}](${repoPublicUrl}/commit/${hash})` : ''}`
|
|
32
|
+
|
|
33
|
+
commits.push(commitRef)
|
|
34
|
+
|
|
35
|
+
return acc
|
|
36
|
+
}, {}))
|
|
37
|
+
.map(({group, commits}) => `
|
|
38
|
+
### ${group}
|
|
39
|
+
${commits.join('\n')}`).join('\n')
|
|
40
|
+
|
|
41
|
+
return releaseDiffRef + '\n' + releaseDetails + '\n'
|
|
42
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cosmiconfig } from 'cosmiconfig'
|
|
2
|
+
import { camelize } from './util.js'
|
|
3
|
+
|
|
4
|
+
const CONFIG_NAME = 'release'
|
|
5
|
+
const CONFIG_FILES = [
|
|
6
|
+
'package.json',
|
|
7
|
+
`.${CONFIG_NAME}rc`,
|
|
8
|
+
`.${CONFIG_NAME}rc.json`,
|
|
9
|
+
`.${CONFIG_NAME}rc.yaml`,
|
|
10
|
+
`.${CONFIG_NAME}rc.yml`,
|
|
11
|
+
`.${CONFIG_NAME}rc.js`,
|
|
12
|
+
`.${CONFIG_NAME}rc.cjs`,
|
|
13
|
+
`${CONFIG_NAME}.config.js`,
|
|
14
|
+
`${CONFIG_NAME}.config.cjs`
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const defaultConfig = {
|
|
18
|
+
cmd: 'yarn && yarn build && yarn test',
|
|
19
|
+
changelog: 'changelog',
|
|
20
|
+
npmFetch: true,
|
|
21
|
+
ghRelease: true,
|
|
22
|
+
// npmPublish: true,
|
|
23
|
+
// ghPages: 'gh-pages'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getPkgConfig = async (...cwds) =>
|
|
27
|
+
normalizePkgConfig((await Promise.all(cwds.map(
|
|
28
|
+
cwd => cosmiconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILES }).search(cwd).then(r => r?.config)
|
|
29
|
+
))).find(Boolean) || defaultConfig)
|
|
30
|
+
|
|
31
|
+
export const normalizePkgConfig = (config, env) => ({
|
|
32
|
+
...parseEnv(env),
|
|
33
|
+
...config,
|
|
34
|
+
npmFetch: config.npmFetch || config.fetch || config.fetchPkg,
|
|
35
|
+
buildCmd: config.buildCmd || config.cmd,
|
|
36
|
+
get ghBasicAuth() {
|
|
37
|
+
return this.ghUser && this.ghToken ? `${this.ghUser}:${this.ghToken}` : false
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const parseEnv = ({GH_USER, GH_USERNAME, GITHUB_USER, GITHUB_USERNAME, GH_TOKEN, GITHUB_TOKEN, NPM_TOKEN, NPM_REGISTRY, NPMRC, NPM_USERCONFIG, NPM_CONFIG_USERCONFIG, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL} = process.env) =>
|
|
42
|
+
({
|
|
43
|
+
ghUser: GH_USER || GH_USERNAME || GITHUB_USER || GITHUB_USERNAME,
|
|
44
|
+
ghToken: GH_TOKEN || GITHUB_TOKEN,
|
|
45
|
+
npmConfig: NPMRC || NPM_USERCONFIG || NPM_CONFIG_USERCONFIG,
|
|
46
|
+
npmToken: NPM_TOKEN,
|
|
47
|
+
npmRegistry: NPM_REGISTRY || 'https://registry.npmjs.org',
|
|
48
|
+
gitCommitterName: GIT_COMMITTER_NAME || 'Semrel Extra Bot',
|
|
49
|
+
gitCommitterEmail: GIT_COMMITTER_EMAIL || 'semrel-extra-bot@hotmail.com',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export const normalizeFlags = (flags = {}) => Object.entries(flags).reduce((acc, [k, v]) => ({...acc, [camelize(k)]: v}), {})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {semver} from 'zx-extra'
|
|
2
|
+
import {topo as _topo} from '@semrel-extra/topo'
|
|
3
|
+
|
|
4
|
+
export const depScopes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
5
|
+
|
|
6
|
+
export const traverseDeps = async (pkg, packages, fn) => {
|
|
7
|
+
const {manifest} = pkg
|
|
8
|
+
|
|
9
|
+
for (let scope of depScopes) {
|
|
10
|
+
const deps = manifest[scope]
|
|
11
|
+
if (!deps) continue
|
|
12
|
+
|
|
13
|
+
for (let [name, version] of Object.entries(deps)) {
|
|
14
|
+
if (!packages[name]) continue
|
|
15
|
+
|
|
16
|
+
await fn(pkg, {name, version, deps, scope, pkg: packages[name]})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const updateDeps = async (pkg, packages) => {
|
|
22
|
+
const changes = []
|
|
23
|
+
|
|
24
|
+
await traverseDeps(pkg, packages, async (_, {name, version, deps, scope, pkg: dep}) => {
|
|
25
|
+
const prev = pkg.latest.meta?.[scope]?.[name]
|
|
26
|
+
const actual = dep?.version
|
|
27
|
+
const next = resolveNextVersion(version, actual, prev)
|
|
28
|
+
const _version = next || subsWorkspace(version, actual)
|
|
29
|
+
|
|
30
|
+
pkg[scope] = {...pkg[scope], [name]: _version} // Update pkg context
|
|
31
|
+
deps[name] = _version // Update manifest
|
|
32
|
+
|
|
33
|
+
if (!next) return
|
|
34
|
+
changes.push({
|
|
35
|
+
group: 'Dependencies',
|
|
36
|
+
releaseType: 'patch',
|
|
37
|
+
change: 'perf',
|
|
38
|
+
subj: `perf: ${name} updated to ${next}`,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return changes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const resolveNextVersion = (decl, actual, prev) => {
|
|
46
|
+
if (!decl) return null
|
|
47
|
+
|
|
48
|
+
// https://yarnpkg.com/features/workspaces
|
|
49
|
+
decl = subsWorkspace(decl, actual)
|
|
50
|
+
|
|
51
|
+
if (!semver.satisfies(actual, decl)) return actual === prev ? null : actual
|
|
52
|
+
|
|
53
|
+
return decl === prev ? null : decl
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const subsWorkspace = (decl, actual) => {
|
|
57
|
+
if (decl.startsWith('workspace:')) {
|
|
58
|
+
const [, range, caret] = /^workspace:(([\^~*])?.*)$/.exec(decl)
|
|
59
|
+
|
|
60
|
+
return caret === range
|
|
61
|
+
? caret === '*' ? actual : caret + actual
|
|
62
|
+
: range
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return decl
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const topo = async ({flags = {}, cwd} = {}) => {
|
|
69
|
+
const ignore = typeof flags.ignore === 'string'
|
|
70
|
+
? flags.ignore.split(/\s*,\s*/)
|
|
71
|
+
: Array.isArray(flags.ignore)
|
|
72
|
+
? flags.ignore
|
|
73
|
+
: []
|
|
74
|
+
|
|
75
|
+
const filter = ({manifest: {private: _private, name}}) =>
|
|
76
|
+
!ignore.includes(name) && (flags.includePrivate || !_private)
|
|
77
|
+
|
|
78
|
+
return _topo({cwd, filter})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const traverseQueue = async ({queue, prev, cb}) => {
|
|
82
|
+
const acc = {}
|
|
83
|
+
|
|
84
|
+
return Promise.all(queue.map((name) =>
|
|
85
|
+
(acc[name] = (async () => {
|
|
86
|
+
await Promise.all((prev.get(name) || []).map((p) => acc[p]))
|
|
87
|
+
await cb(name)
|
|
88
|
+
})()))
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {queuefy} from 'queuefy'
|
|
2
|
+
import {$, path} from 'zx-extra'
|
|
3
|
+
import {log} from './log.js'
|
|
4
|
+
import {getRepo, pushCommit} from './git.js'
|
|
5
|
+
import {formatTag} from './meta.js'
|
|
6
|
+
import {formatReleaseNotes} from './changelog.js'
|
|
7
|
+
import {msgJoin} from './util.js'
|
|
8
|
+
|
|
9
|
+
export const ghRelease = async (pkg) => {
|
|
10
|
+
log({pkg})(`create gh release`)
|
|
11
|
+
|
|
12
|
+
const {ghBasicAuth: basicAuth, ghToken} = pkg.config
|
|
13
|
+
if (!ghToken) return null
|
|
14
|
+
|
|
15
|
+
const {name, version, absPath: cwd} = pkg
|
|
16
|
+
const {repoName} = await getRepo(cwd, {basicAuth})
|
|
17
|
+
const tag = formatTag({name, version})
|
|
18
|
+
const releaseNotes = await formatReleaseNotes(pkg)
|
|
19
|
+
const releaseData = JSON.stringify({
|
|
20
|
+
name: tag,
|
|
21
|
+
tag_name: tag,
|
|
22
|
+
body: releaseNotes
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await $.o({cwd})`curl -H 'Authorization: token ${ghToken}' -H 'Accept: application/vnd.github.v3+json' https://api.github.com/repos/${repoName}/releases -d ${releaseData}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const ghPages = queuefy(async (pkg) => {
|
|
29
|
+
const {config: {ghPages: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
|
|
30
|
+
if (!opts) return
|
|
31
|
+
|
|
32
|
+
const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = typeof opts === 'string'
|
|
33
|
+
? opts.split(' ')
|
|
34
|
+
: [opts.branch, opts.from, opts.to, opts.msg]
|
|
35
|
+
const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
|
|
36
|
+
|
|
37
|
+
log({pkg})(`publish docs to ${branch}`)
|
|
38
|
+
|
|
39
|
+
await pushCommit({
|
|
40
|
+
cwd: path.join(pkg.absPath, from),
|
|
41
|
+
from: '.',
|
|
42
|
+
to,
|
|
43
|
+
branch,
|
|
44
|
+
msg,
|
|
45
|
+
gitCommitterEmail,
|
|
46
|
+
gitCommitterName,
|
|
47
|
+
basicAuth
|
|
48
|
+
})
|
|
49
|
+
})
|