zx-bulk-release 1.26.1 → 2.0.0

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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## [2.0.0](https://github.com/semrel-extra/zx-bulk-release/compare/v1.26.2...v2.0.0) (2023-03-21)
2
+
3
+ ### Features
4
+ * feat: replace sequent flow with concurrent ([ec35d7e](https://github.com/semrel-extra/zx-bulk-release/commit/ec35d7e337e64369f0423b71008345177f3ac949))
5
+
6
+ ### BREAKING CHANGES
7
+ * concurrent flow might cause throttling issues ([ec35d7e](https://github.com/semrel-extra/zx-bulk-release/commit/ec35d7e337e64369f0423b71008345177f3ac949))
8
+
9
+ ## [1.26.2](https://github.com/semrel-extra/zx-bulk-release/compare/v1.26.1...v1.26.2) (2023-03-21)
10
+
11
+ ### Fixes & improvements
12
+ * refactor: separate processor layer ([dae6b96](https://github.com/semrel-extra/zx-bulk-release/commit/dae6b9682cd7d283e4f776c69f30c5f50fe9fa41))
13
+
1
14
  ## [1.26.1](https://github.com/semrel-extra/zx-bulk-release/compare/v1.26.0...v1.26.1) (2023-02-08)
2
15
 
3
16
  ### Fixes & improvements
package/README.md CHANGED
@@ -278,7 +278,7 @@ Release process state is reported to the console and to a file if `--report` fla
278
278
  events: [
279
279
  {msg: ['zx-bulk-release'], scope:'~', date: 1665839585488, level: 'info'},
280
280
  {msg: ['queue:',['a','b']], scope:'~', date: 1665839585493, level: 'info'},
281
- {msg: ["run buildCmd 'yarn && yarn build && yarn test'"], scope: 'a', date:1665839585719, level:'info'},
281
+ {msg: ["run buildCmd 'yarn && yarn build && yarn test'"], scope: 'a', date: 1665839585719, level:'info'},
282
282
  // ...
283
283
  ]
284
284
  }
@@ -299,6 +299,8 @@ Release process state is reported to the console and to a file if `--report` fla
299
299
  * [vercel/turborepo](https://github.com/vercel/turborepo)
300
300
  * [lerna/lerna](https://github.com/lerna/lerna)
301
301
  * [nrwl/nx](https://github.com/nrwl/nx)
302
+ * [moonrepo/moon](https://github.com/moonrepo/moon)
303
+ * [ojkelly/yarn.build](https://github.com/ojkelly/yarn.build)
302
304
 
303
305
  ## License
304
306
  [MIT](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
- "version": "1.26.1",
3
+ "version": "2.0.0",
4
4
  "description": "zx-based alternative for multi-semantic-release",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,14 +21,15 @@
21
21
  "docs": "mkdir -p docs && cp ./README.md ./docs/README.md"
22
22
  },
23
23
  "dependencies": {
24
- "@semrel-extra/topo": "^1.4.4",
25
- "cosmiconfig": "^8.0.0",
26
- "zx-extra": "^2.5.3"
24
+ "@semrel-extra/topo": "^1.6.0",
25
+ "cosmiconfig": "^8.1.3",
26
+ "queuefy": "^1.2.1",
27
+ "zx-extra": "^2.5.4"
27
28
  },
28
29
  "devDependencies": {
29
- "c8": "^7.12.0",
30
+ "c8": "^7.13.0",
30
31
  "uvu": "^0.5.6",
31
- "verdaccio": "^5.19.1"
32
+ "verdaccio": "^5.22.1"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  import {ctx, semver} from 'zx-extra'
2
2
  import {updateDeps} from './deps.js'
3
3
  import {formatTag} from './tag.js';
4
- import {log} from './util.js'
4
+ import {log} from './log.js'
5
5
 
6
6
  export const analyze = async (pkg, packages) => {
7
7
  const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
@@ -1,18 +1,20 @@
1
1
  import {traverseDeps} from './deps.js'
2
2
  import {fetchPkg} from './npm.js'
3
- import {runHook} from './util.js'
3
+ import {runCmd} from './processor.js'
4
4
 
5
- export const build = async (pkg, packages) => {
5
+ export const build = async (pkg, packages, run = runCmd) => {
6
6
  if (pkg.built) return true
7
7
 
8
8
  await traverseDeps(pkg, packages, async (_, {pkg}) => build(pkg, packages))
9
9
 
10
10
  const {config} = pkg
11
11
 
12
- if (pkg.changes.length === 0 && config.npmFetch) await fetchPkg(pkg)
12
+ if (pkg.changes.length === 0 && config.npmFetch) {
13
+ await fetchPkg(pkg)
14
+ }
13
15
 
14
16
  if (!pkg.fetched && config.buildCmd) {
15
- await runHook(pkg, 'buildCmd')
17
+ await run(pkg, 'buildCmd')
16
18
  }
17
19
 
18
20
  pkg.built = true
@@ -1,62 +1 @@
1
- import {within, $} from 'zx-extra'
2
- import {analyze} from './analyze.js'
3
- import {publish} from './publish.js'
4
- import {build} from './build.js'
5
- import {contextify} from './contextify.js'
6
- import {topo} from './topo.js'
7
- import {createReporter, log} from './util.js';
8
-
9
- export {getLatestTaggedVersion} from './tag.js'
10
-
11
- export const run = async ({cwd = process.cwd(), env, flags = {}} = {}) => within(async () => {
12
- const reporter = $.r = createReporter(flags.report)
13
- $.env = {...process.env, ...env}
14
- $.verbose = !!(flags.debug || $.env.DEBUG ) || $.verbose
15
- log()('zx-bulk-release')
16
-
17
- try {
18
- const {packages, queue, root} = await topo({cwd, flags})
19
- log()('queue:', queue)
20
-
21
- reporter.setQueue(queue, packages)
22
- reporter.setStatus('pending')
23
-
24
- for (let name of queue) {
25
- const pkg = packages[name]
26
-
27
- reporter.setStatus('analyzing', name)
28
- await contextify(pkg, packages, root)
29
- await analyze(pkg, packages)
30
- reporter.setState('config', pkg.config, name)
31
- reporter.setState('version', pkg.version, name)
32
- reporter.setState('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
33
- reporter.setState('releaseType', pkg.releaseType, name)
34
- reporter.setState('tag', pkg.tag, name)
35
-
36
- if (!pkg.releaseType) {
37
- reporter.setStatus('skipped', name)
38
- continue
39
- }
40
-
41
- reporter.setStatus('building', name)
42
- await build(pkg, packages)
43
-
44
- if (flags.dryRun) {
45
- reporter.setStatus('success', name)
46
- continue
47
- }
48
-
49
- reporter.setStatus('publishing', name)
50
- await publish(pkg)
51
-
52
- reporter.setStatus('success', name)
53
- }
54
- } catch (e) {
55
- log({level: 'error'})(e)
56
- reporter.setState('error', e)
57
- reporter.setStatus('failure')
58
- throw e
59
- }
60
- reporter.setStatus('success')
61
- log()('Great success!')
62
- })
1
+ export {run} from './processor.js'
@@ -0,0 +1,9 @@
1
+ import {$} from 'zx-extra'
2
+
3
+ export const log = (ctx) => {
4
+ if ($.state) {
5
+ return $.state.log(ctx)
6
+ }
7
+
8
+ return console.log
9
+ }
@@ -1,5 +1,5 @@
1
1
  import {parseEnv} from './config.js'
2
- import {log} from './util.js'
2
+ import {log} from './log.js'
3
3
  import {$, ctx, fs, path, tempy, copy, INI, fetch} from 'zx-extra'
4
4
 
5
5
  export const fetchPkg = async (pkg, {env = $.env} = {}) => {
@@ -0,0 +1,81 @@
1
+ import os from 'node:os'
2
+ import {$, fs, within} from 'zx-extra'
3
+ import {log} from './log.js'
4
+ import {topo, traverse} from './topo.js'
5
+ import {contextify} from './contextify.js'
6
+ import {analyze} from './analyze.js'
7
+ import {build} from './build.js'
8
+ import {publish} from './publish.js'
9
+ import {createState} from './state.js'
10
+ import {tpl} from './util.js'
11
+ import {queuefy} from 'queuefy'
12
+
13
+ export const run = async ({cwd = process.cwd(), env, flags = {}, concurrency = os.cpus().length} = {}) => within(async () => {
14
+ const state = createState({file: flags.report})
15
+ const _runCmd = queuefy(runCmd, concurrency)
16
+
17
+ $.state = state
18
+ $.env = {...process.env, ...env}
19
+ $.verbose = !!(flags.debug || $.env.DEBUG ) || $.verbose
20
+
21
+ log()('zx-bulk-release')
22
+
23
+ try {
24
+ const {packages, queue, root, sources, next, prev, nodes} = await topo({cwd, flags})
25
+ log()('queue:', queue)
26
+
27
+ state.setQueue(queue, packages)
28
+
29
+ await traverse({nodes, prev, async cb(name) {
30
+ state.setStatus('analyzing', name)
31
+ const pkg = packages[name]
32
+ await contextify(pkg, packages, root)
33
+ await analyze(pkg, packages)
34
+ state.set('config', pkg.config, name)
35
+ state.set('version', pkg.version, name)
36
+ state.set('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
37
+ state.set('releaseType', pkg.releaseType, name)
38
+ state.set('tag', pkg.tag, name)
39
+ }})
40
+
41
+ state.setStatus('pending')
42
+
43
+ await traverse({nodes, prev, async cb(name) {
44
+ const pkg = packages[name]
45
+
46
+ if (!pkg.releaseType) {
47
+ state.setStatus('skipped', name)
48
+ return
49
+ }
50
+
51
+ state.setStatus('building', name)
52
+ await build(pkg, packages, _runCmd)
53
+
54
+ if (flags.dryRun) {
55
+ state.setStatus('success', name)
56
+ return
57
+ }
58
+
59
+ state.setStatus('publishing', name)
60
+ await publish(pkg, _runCmd)
61
+
62
+ state.setStatus('success', name)
63
+ }})
64
+ } catch (e) {
65
+ log({level: 'error'})(e)
66
+ state.set('error', e)
67
+ state.setStatus('failure')
68
+ throw e
69
+ }
70
+ state.setStatus('success')
71
+ log()('Great success!')
72
+ })
73
+
74
+ export const runCmd = async (pkg, name) => {
75
+ const cmd = tpl(pkg.config[name], {...pkg, ...pkg.context})
76
+
77
+ if (cmd) {
78
+ log({pkg})(`run ${name} '${cmd}'`)
79
+ await $.o({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
80
+ }
81
+ }
@@ -1,11 +1,13 @@
1
- import {formatTag, getLatestTag, pushTag} from './tag.js'
2
1
  import {fs, path, $} from 'zx-extra'
2
+ import {formatTag, getLatestTag, pushTag} from './tag.js'
3
3
  import {push, fetch, parseRepo} from './repo.js'
4
4
  import {parseEnv} from './config.js'
5
5
  import {fetchManifest, npmPublish} from './npm.js'
6
- import {restJoin, runHook, log} from './util.js'
6
+ import {msgJoin} from './util.js'
7
+ import {runCmd} from './processor.js'
8
+ import {log} from './log.js'
7
9
 
8
- export const publish = async (pkg) => {
10
+ export const publish = async (pkg, run = runCmd) => {
9
11
  await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
10
12
  await pushTag(pkg)
11
13
  await pushMeta(pkg)
@@ -13,7 +15,7 @@ export const publish = async (pkg) => {
13
15
  await npmPublish(pkg)
14
16
  await ghRelease(pkg)
15
17
  await ghPages(pkg)
16
- await runHook(pkg, 'publishCmd')
18
+ await run(pkg, 'publishCmd')
17
19
  }
18
20
 
19
21
  export const pushMeta = async (pkg) => {
@@ -68,7 +70,7 @@ const pushChangelog = async (pkg) => {
68
70
  ? opts.split(' ')
69
71
  : [opts.branch, opts.file, opts.msg]
70
72
  const _cwd = await fetch({cwd: pkg.absPath, branch})
71
- const msg = restJoin(_msg, pkg, 'chore: update changelog ${{name}}')
73
+ const msg = msgJoin(_msg, pkg, 'chore: update changelog ${{name}}')
72
74
  const releaseNotes = await formatReleaseNotes(pkg)
73
75
 
74
76
  await $.o({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
@@ -103,7 +105,7 @@ const ghPages = async (pkg) => {
103
105
  const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = typeof opts === 'string'
104
106
  ? opts.split(' ')
105
107
  : [opts.branch, opts.from, opts.to, opts.msg]
106
- const msg = restJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
108
+ const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
107
109
 
108
110
  log({pkg})(`publish docs to ${branch}`)
109
111
 
@@ -1,6 +1,7 @@
1
1
  import {$, ctx, fs, path, tempy, copy} from 'zx-extra'
2
2
  import {parseEnv} from './config.js'
3
- import {log} from './util.js'
3
+ import {log} from './log.js'
4
+ import {keyByValue} from './util.js'
4
5
 
5
6
  const branches = {}
6
7
  export const fetch = async ({cwd: _cwd, branch, origin: _origin}) => ctx(async ($) => {
@@ -25,28 +26,39 @@ export const fetch = async ({cwd: _cwd, branch, origin: _origin}) => ctx(async (
25
26
  })
26
27
 
27
28
  export const push = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = []}) => ctx(async ($) => {
28
- const _cwd = await fetch({cwd, branch, origin})
29
- const {gitCommitterEmail, gitCommitterName} = parseEnv($.env)
30
-
31
- for (let {relpath, contents} of files) {
32
- const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
33
- await fs.outputFile(path.resolve(_cwd, to, relpath), _contents)
34
- }
35
- if (from) await copy({baseFrom: cwd, from, baseTo: _cwd, to, ignoreFiles, cwd})
36
-
37
- $.cwd = _cwd
38
-
39
- await $`git config user.name ${gitCommitterName}`
40
- await $`git config user.email ${gitCommitterEmail}`
41
- await $`git add .`
42
- try {
43
- await $`git commit -m ${msg}`
44
- } catch {
45
- log({level: 'warn'})(`no changes to commit to ${branch}`)
46
- return
29
+ let retries = 3
30
+ let _cwd
31
+
32
+ while (retries > 0) {
33
+ try {
34
+ const {gitCommitterEmail, gitCommitterName} = parseEnv($.env)
35
+ _cwd = await fetch({cwd, branch, origin})
36
+
37
+ for (let {relpath, contents} of files) {
38
+ const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
39
+ await fs.outputFile(path.resolve(_cwd, to, relpath), _contents)
40
+ }
41
+ if (from) await copy({baseFrom: cwd, from, baseTo: _cwd, to, ignoreFiles, cwd})
42
+
43
+ $.cwd = _cwd
44
+
45
+ await $`git config user.name ${gitCommitterName}`
46
+ await $`git config user.email ${gitCommitterEmail}`
47
+ await $`git add .`
48
+ try {
49
+ await $`git commit -m ${msg}`
50
+ } catch {
51
+ log({level: 'warn'})(`no changes to commit to ${branch}`)
52
+ return
53
+ }
54
+
55
+ return await $.raw`git push origin HEAD:refs/heads/${branch}`
56
+ } catch (e) {
57
+ retries -= 1
58
+ branches[keyByValue(branches, _cwd)] = null
59
+ console.warn('git push failed', 'retries left', retries, e)
60
+ }
47
61
  }
48
-
49
- await $.raw`git push origin HEAD:refs/heads/${branch}`
50
62
  })
51
63
 
52
64
  const repos = {}
@@ -58,8 +70,8 @@ export const parseRepo = async (_cwd) => {
58
70
  const originUrl = await getOrigin(cwd)
59
71
  const [, , repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/) || []
60
72
  const repoPublicUrl = `https://${repoHost}/${repoName}`
61
- const repoAuthedUrl = ghToken && ghUser && repoHost && repoName ?
62
- `https://${ghUser}:${ghToken}@${repoHost}/${repoName}.git`
73
+ const repoAuthedUrl = ghToken && ghUser && repoHost && repoName
74
+ ? `https://${ghUser}:${ghToken}@${repoHost}/${repoName}.git`
63
75
  : originUrl
64
76
 
65
77
  repos[cwd] = {
@@ -0,0 +1,56 @@
1
+ import {get, set, tpl} from './util.js'
2
+ import {fs} from 'zx-extra'
3
+
4
+ export const createState = ({logger = console, file = null} = {}) => ({
5
+ logger,
6
+ file,
7
+ status: 'initial',
8
+ queue: [],
9
+ packages: [],
10
+ events: [],
11
+ setQueue(queue, packages) {
12
+ this.queue = queue
13
+ this.packages = queue.map(name => {
14
+ const {manifest: {version}, absPath, relPath} = packages[name]
15
+ return {
16
+ status: 'initial',
17
+ name,
18
+ version,
19
+ path: absPath,
20
+ relPath
21
+ }
22
+ })
23
+ },
24
+ get(key, pkgName) {
25
+ return get(
26
+ pkgName ? this.packages.find(({name}) => name === pkgName) : this,
27
+ key
28
+ )
29
+ },
30
+ set(key, value, pkgName) {
31
+ set(
32
+ pkgName ? this.packages.find(({name}) => name === pkgName) : this,
33
+ key,
34
+ value
35
+ )
36
+ },
37
+ setStatus(status, name) {
38
+ this.set('status', status, name)
39
+ this.save()
40
+ },
41
+ getStatus(status, name) {
42
+ return this.get('status', name)
43
+ },
44
+ log(ctx = {}) {
45
+ return function (...chunks) {
46
+ const {pkg, scope = pkg?.name || '~', level = 'info'} = ctx
47
+ const msg = chunks.map(c => typeof c === 'string' ? tpl(c, ctx) : c)
48
+ const event = {msg, scope, date: Date.now(), level}
49
+ this.events.push(event)
50
+ logger[level](`[${scope}]`, ...msg)
51
+ }.bind(this)
52
+ },
53
+ save() {
54
+ this.file && fs.outputJsonSync(this.file, this)
55
+ }
56
+ })
@@ -3,7 +3,7 @@
3
3
  import {ctx, semver, $} from 'zx-extra'
4
4
  import {Buffer} from 'buffer'
5
5
  import {parseEnv} from './config.js'
6
- import {log} from './util.js'
6
+ import {log} from './log.js'
7
7
 
8
8
  export const pushTag = (pkg) => ctx(async ($) => {
9
9
  const {absPath: cwd, name, version} = pkg
@@ -1,4 +1,5 @@
1
1
  import {topo as _topo} from '@semrel-extra/topo'
2
+ import {getPromise} from './util.js'
2
3
 
3
4
  export const topo = async ({flags = {}, cwd} = {}) => {
4
5
  const ignore = typeof flags.ignore === 'string'
@@ -14,4 +15,17 @@ export const topo = async ({flags = {}, cwd} = {}) => {
14
15
  !ignore.includes(name)
15
16
 
16
17
  return _topo({cwd, filter})
17
- }
18
+ }
19
+
20
+ export const traverse = async ({nodes, prev, cb}) => {
21
+ const waitings = nodes.reduce((acc, node) => {
22
+ acc[node] = getPromise()
23
+ return acc
24
+ }, {})
25
+
26
+ await Promise.all(nodes.map(async (name) => {
27
+ await Promise.all((prev.get(name) || []).map((p) => waitings[p].promise))
28
+ await cb(name)
29
+ waitings[name].resolve(true)
30
+ }))
31
+ }
@@ -1,5 +1,3 @@
1
- import {$, fs} from 'zx-extra'
2
-
3
1
  export const tpl = (str, context) =>
4
2
  str?.replace(/\$\{\{\s*([.a-z0-9]+)\s*}}/gi, (matched, key) => get(context, key) ?? '')
5
3
 
@@ -30,73 +28,19 @@ export const set = (obj, path, value) => {
30
28
  return result
31
29
  }
32
30
 
33
- export const runHook = async (pkg, name) => {
34
- const cmd = tpl(pkg.config[name], {...pkg, ...pkg.context})
35
-
36
- if (cmd) {
37
- log({pkg})(`run ${name} '${cmd}'`)
38
- await $.o({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
39
- }
40
- }
41
-
42
- export const restJoin = (rest, context, def) => tpl(rest.filter(Boolean).join(' ') || def, context)
31
+ export const msgJoin = (rest, context, def) => tpl(rest.filter(Boolean).join(' ') || def, context)
43
32
 
44
- export const createReporter = (file, logger = console) => {
45
- const state = {
46
- status: 'initial',
47
- queue: [],
48
- packages: [],
49
- events: [],
50
- }
33
+ export const getPromise = () => {
34
+ let resolve, reject
35
+ const promise = new Promise((...args) => {
36
+ [resolve, reject] = args
37
+ })
51
38
 
52
39
  return {
53
- setQueue(queue, packages) {
54
- state.queue = queue
55
- state.packages = queue.map(name => {
56
- const {manifest: {version}, absPath, relPath} = packages[name]
57
- return {
58
- status: 'initial',
59
- name,
60
- version,
61
- path: absPath,
62
- relPath
63
- }
64
- })
65
- },
66
- getState(key, pkgName) {
67
- const _state = pkgName ? state.packages.find(({name}) => name === pkgName) : state
68
- return get(_state, key)
69
- },
70
- setState(key, value, pkgName) {
71
- const _state = pkgName ? state.packages.find(({name}) => name === pkgName) : state
72
- set(_state, key, value)
73
- },
74
- setStatus(status, name) {
75
- this.setState('status', status, name)
76
-
77
- this.persistState()
78
- },
79
- getStatus (status, name) {
80
- return this.getState('status', name)
81
- },
82
- log(ctx = {}) { return (...chunks) => {
83
- const {pkg, scope = pkg?.name || '~', level = 'info'} = ctx
84
- const msg = chunks.map(c => typeof c === 'string' ? tpl(c, ctx) : c)
85
- const event = {msg, scope, date: Date.now(), level}
86
- state.events.push(event)
87
- logger[level](`[${scope}]`, ...msg)
88
- }},
89
- persistState() {
90
- file && fs.outputJsonSync(file, state)
91
- }
92
- }
93
- }
94
-
95
- export const log = (ctx) => {
96
- if (!$.r) {
97
- $.r = createReporter()
40
+ resolve,
41
+ reject,
42
+ promise,
98
43
  }
99
-
100
- return $.r.log(ctx)
101
44
  }
102
45
 
46
+ export const keyByValue = (obj, value) => Object.keys(obj).find((key) => obj[key] === value)