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/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
+ [![CI](https://github.com/semrel-extra/zx-bulk-release/workflows/CI/badge.svg)](https://github.com/semrel-extra/zx-bulk-release/actions)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/bb94e929b1b6430781b5/maintainability)](https://codeclimate.com/github/semrel-extra/zx-bulk-release/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/bb94e929b1b6430781b5/test_coverage)](https://codeclimate.com/github/semrel-extra/zx-bulk-release/test_coverage)
7
+ [![npm (tag)](https://img.shields.io/npm/v/zx-bulk-release)](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,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {argv} from 'zx-extra'
4
+ import {run} from './index.js'
5
+ import {normalizeFlags} from './config.js'
6
+
7
+ run({flags: normalizeFlags(argv)})
@@ -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
+ })