zx-bulk-release 2.1.6 → 2.2.1

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,17 @@
1
+ ## [2.2.1](https://github.com/semrel-extra/zx-bulk-release/compare/v2.2.0...v2.2.1) (2023-03-23)
2
+
3
+ ### Fixes & improvements
4
+ * perf: make parallel internal `publish` tasks ([87f388e](https://github.com/semrel-extra/zx-bulk-release/commit/87f388ea2ff5cce96cca600903d80260f13f0908))
5
+ * refactor: improve internal reporter ([cc62df7](https://github.com/semrel-extra/zx-bulk-release/commit/cc62df762c0581db159ddd2fae8d90b085911884))
6
+
7
+ ## [2.2.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.1.6...v2.2.0) (2023-03-23)
8
+
9
+ ### Fixes & improvements
10
+ * fix: fix basicAuth tpl ([b6af2f5](https://github.com/semrel-extra/zx-bulk-release/commit/b6af2f51a2e13ddb5a3b6b1ef288595aae61e746))
11
+
12
+ ### Features
13
+ * feat: inherit gh and npm creds from pkg release configs ([f07dd4f](https://github.com/semrel-extra/zx-bulk-release/commit/f07dd4fa0cf9d26942780ccf0c34406b345dae39))
14
+
1
15
  ## [2.1.6](https://github.com/semrel-extra/zx-bulk-release/compare/v2.1.5...v2.1.6) (2023-03-22)
2
16
 
3
17
  ### Fixes & improvements
package/README.md CHANGED
@@ -146,9 +146,10 @@ By default, it omits the packages marked as `private`. You can override this by
146
146
 
147
147
  ### `analyze`
148
148
  Determines pkg changes, release type, next version etc.
149
+
149
150
  ```js
150
151
  export const analyze = async (pkg, packages, root) => {
151
- pkg.config = await getConfig(pkg.absPath, root.absPath)
152
+ pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
152
153
  pkg.latest = await getLatest(pkg)
153
154
 
154
155
  const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
- "version": "2.1.6",
3
+ "version": "2.2.1",
4
4
  "description": "zx-based alternative for multi-semantic-release",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,7 +1,8 @@
1
- import {ctx, semver} from 'zx-extra'
1
+ import {semver} from 'zx-extra'
2
2
  import {updateDeps} from './deps.js'
3
- import {formatTag} from './tag.js';
3
+ import {formatTag} from './meta.js';
4
4
  import {log} from './log.js'
5
+ import {getCommits} from './git.js'
5
6
 
6
7
  export const analyze = async (pkg, packages) => {
7
8
  const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
@@ -25,22 +26,8 @@ export const semanticRules = [
25
26
  {group: 'BREAKING CHANGES', releaseType: 'major', keywords: ['BREAKING CHANGE', 'BREAKING CHANGES']},
26
27
  ]
27
28
 
28
- export const getPkgCommits = async (cwd, since) => ctx(async ($) => {
29
- const range = since ? `${since}..HEAD` : 'HEAD'
30
-
31
- $.cwd = cwd
32
- return (await $.raw`git log ${range} --format=+++%s__%b__%h__%H -- .`)
33
- .toString()
34
- .split('+++')
35
- .filter(Boolean)
36
- .map(msg => {
37
- const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim())
38
- return {subj, body, short, hash}
39
- })
40
- })
41
-
42
- export const getSemanticChanges = async (cwd, since) => {
43
- const commits = await getPkgCommits(cwd, since)
29
+ export const getSemanticChanges = async (cwd, from, to) => {
30
+ const commits = await getCommits(cwd, from, to)
44
31
 
45
32
  return analyzeCommits(commits)
46
33
  }
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import { cosmiconfig } from 'cosmiconfig'
2
+ import { camelize } from './util.js'
2
3
 
3
4
  const CONFIG_NAME = 'release'
4
5
  const CONFIG_FILES = [
@@ -15,22 +16,26 @@ const CONFIG_FILES = [
15
16
 
16
17
  export const defaultConfig = {
17
18
  cmd: 'yarn && yarn build && yarn test',
18
- npmFetch: true,
19
19
  changelog: 'changelog',
20
+ npmFetch: true,
20
21
  ghRelease: true,
21
22
  // npmPublish: true,
22
23
  // ghPages: 'gh-pages'
23
24
  }
24
25
 
25
- export const getConfig = async (...cwds) =>
26
- normalizeConfig((await Promise.all(cwds.map(
26
+ export const getPkgConfig = async (...cwds) =>
27
+ normalizePkgConfig((await Promise.all(cwds.map(
27
28
  cwd => cosmiconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILES }).search(cwd).then(r => r?.config)
28
29
  ))).find(Boolean) || defaultConfig)
29
30
 
30
- export const normalizeConfig = config => ({
31
+ export const normalizePkgConfig = (config, env) => ({
32
+ ...parseEnv(env),
31
33
  ...config,
32
34
  npmFetch: config.npmFetch || config.fetch || config.fetchPkg,
33
- buildCmd: config.buildCmd || config.cmd
35
+ buildCmd: config.buildCmd || config.cmd,
36
+ get ghBasicAuth() {
37
+ return this.ghUser && this.ghToken ? `${this.ghUser}:${this.ghToken}` : false
38
+ }
34
39
  })
35
40
 
36
41
  export const parseEnv = (env = process.env) => {
@@ -47,5 +52,4 @@ export const parseEnv = (env = process.env) => {
47
52
  }
48
53
  }
49
54
 
50
- const camelize = s => s.replace(/-./g, x => x[1].toUpperCase())
51
55
  export const normalizeFlags = (flags = {}) => Object.entries(flags).reduce((acc, [k, v]) => ({...acc, [camelize(k)]: v}), {})
@@ -0,0 +1,48 @@
1
+ import {log} from './log.js'
2
+ import {getRepo, pushCommit} from './git.js'
3
+ import {formatTag} from './meta.js'
4
+ import {formatReleaseNotes} from './changelog.js'
5
+ import {$, path} from 'zx-extra'
6
+ import {msgJoin} from './util.js'
7
+
8
+ export const ghRelease = async (pkg) => {
9
+ log({pkg})(`create gh release`)
10
+
11
+ const {ghBasicAuth: basicAuth, ghToken} = pkg.config
12
+ if (!ghToken) return null
13
+
14
+ const {name, version, absPath: cwd} = pkg
15
+ const {repoName} = await getRepo(cwd, {basicAuth})
16
+ const tag = formatTag({name, version})
17
+ const releaseNotes = await formatReleaseNotes(pkg)
18
+ const releaseData = JSON.stringify({
19
+ name: tag,
20
+ tag_name: tag,
21
+ body: releaseNotes
22
+ })
23
+
24
+ await $.o({cwd})`curl -H 'Authorization: token ${ghToken}' -H 'Accept: application/vnd.github.v3+json' https://api.github.com/repos/${repoName}/releases -d ${releaseData}`
25
+ }
26
+
27
+ export const ghPages = async (pkg) => {
28
+ const {config: {ghPages: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
29
+ if (!opts) return
30
+
31
+ const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = typeof opts === 'string'
32
+ ? opts.split(' ')
33
+ : [opts.branch, opts.from, opts.to, opts.msg]
34
+ const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
35
+
36
+ log({pkg})(`publish docs to ${branch}`)
37
+
38
+ await pushCommit({
39
+ cwd: path.join(pkg.absPath, from),
40
+ from: '.',
41
+ to,
42
+ branch,
43
+ msg,
44
+ gitCommitterEmail,
45
+ gitCommitterName,
46
+ basicAuth
47
+ })
48
+ }
@@ -1,16 +1,15 @@
1
1
  import {$, ctx, fs, path, tempy, copy} from 'zx-extra'
2
- import {parseEnv} from './config.js'
3
2
  import {log} from './log.js'
4
3
  import {keyByValue} from './util.js'
5
4
 
6
5
  const branches = {}
7
- export const fetch = async ({cwd: _cwd, branch, origin: _origin}) => ctx(async ($) => {
6
+ export const fetchRepo = async ({cwd: _cwd, branch, origin: _origin, basicAuth}) => ctx(async ($) => {
8
7
  const root = await getRoot(_cwd)
9
8
  const id = `${root}:${branch}`
10
9
 
11
10
  if (branches[id]) return branches[id]
12
11
 
13
- const origin = _origin || (await parseRepo(_cwd)).repoAuthedUrl
12
+ const origin = _origin || (await getRepo(_cwd, {basicAuth})).repoAuthedUrl
14
13
  const cwd = tempy.temporaryDirectory()
15
14
  $.cwd = cwd
16
15
  try {
@@ -25,14 +24,13 @@ export const fetch = async ({cwd: _cwd, branch, origin: _origin}) => ctx(async (
25
24
  return branches[id]
26
25
  })
27
26
 
28
- export const push = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = []}) => ctx(async ($) => {
27
+ export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = [], basicAuth, gitCommitterEmail, gitCommitterName}) => ctx(async ($) => {
29
28
  let retries = 3
30
29
  let _cwd
31
30
 
32
31
  while (retries > 0) {
33
32
  try {
34
- const {gitCommitterEmail, gitCommitterName} = parseEnv($.env)
35
- _cwd = await fetch({cwd, branch, origin})
33
+ _cwd = await fetchRepo({cwd, branch, origin, basicAuth})
36
34
 
37
35
  for (let {relpath, contents} of files) {
38
36
  const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
@@ -56,22 +54,21 @@ export const push = async ({cwd, from, to, branch, origin, msg, ignoreFiles, fil
56
54
  } catch (e) {
57
55
  retries -= 1
58
56
  branches[keyByValue(branches, _cwd)] = null
59
- console.warn('git push failed', 'retries left', retries, e)
57
+ log({level: 'warn'})('git push failed', 'retries left', retries, e)
60
58
  }
61
59
  }
62
60
  })
63
61
 
64
62
  const repos = {}
65
- export const parseRepo = async (_cwd) => {
63
+ export const getRepo = async (_cwd, {basicAuth} = {}) => {
66
64
  const cwd = await getRoot(_cwd)
67
65
  if (repos[cwd]) return repos[cwd]
68
66
 
69
- const {ghToken, ghUser} = parseEnv($.env)
70
67
  const originUrl = await getOrigin(cwd)
71
68
  const [, , repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/) || []
72
69
  const repoPublicUrl = `https://${repoHost}/${repoName}`
73
- const repoAuthedUrl = ghToken && ghUser && repoHost && repoName
74
- ? `https://${ghUser}:${ghToken}@${repoHost}/${repoName}.git`
70
+ const repoAuthedUrl = basicAuth && repoHost && repoName
71
+ ? `https://${basicAuth}@${repoHost}/${repoName}.git`
75
72
  : originUrl
76
73
 
77
74
  repos[cwd] = {
@@ -95,3 +92,24 @@ export const getOrigin = async (cwd) => {
95
92
  }
96
93
 
97
94
  export const getRoot = async (cwd) => (await $.o({cwd})`git rev-parse --show-toplevel`).toString().trim()
95
+
96
+ export const getSha = async (cwd) => (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
97
+
98
+ export const getCommits = async (cwd, from, to = 'HEAD') => ctx(async ($) => {
99
+ const ref = from ? `${from}..${to}` : to
100
+
101
+ $.cwd = cwd
102
+ return (await $.raw`git log ${ref} --format=+++%s__%b__%h__%H -- .`)
103
+ .toString()
104
+ .split('+++')
105
+ .filter(Boolean)
106
+ .map(msg => {
107
+ const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim())
108
+ return {subj, body, short, hash}
109
+ })
110
+ })
111
+
112
+ export const getTags = async (cwd, ref = '') =>
113
+ (await $.o({cwd})`git tag -l ${ref}`)
114
+ .toString()
115
+ .split('\n')
@@ -1,9 +1,69 @@
1
- import {$} from 'zx-extra'
1
+ import {$, fs} from 'zx-extra'
2
+ import {get, set, tpl} from './util.js'
2
3
 
3
4
  export const log = (ctx) => {
4
- if ($.state) {
5
- return $.state.log(ctx)
5
+ if ($.report) {
6
+ return $.report.log(ctx)
6
7
  }
7
8
 
8
9
  return console.log
9
10
  }
11
+
12
+ export const createReport = ({logger = console, file = null} = {}) => ({
13
+ logger,
14
+ file,
15
+ status: 'initial',
16
+ queue: [],
17
+ packages: {},
18
+ events: [],
19
+ setPackages(packages) {
20
+ this.packages = Object.entries(packages).reduce((acc, [name, {manifest: {version}, absPath, relPath}]) => {
21
+ acc[name] = {
22
+ status: 'initial',
23
+ name,
24
+ version,
25
+ path: absPath,
26
+ relPath
27
+ }
28
+ return acc
29
+ }, {})
30
+ return this
31
+ },
32
+ get(key, pkgName) {
33
+ return get(
34
+ pkgName ? this.packages[pkgName] : this,
35
+ key
36
+ )
37
+ },
38
+ set(key, value, pkgName) {
39
+ set(
40
+ pkgName ? this.packages[pkgName] : this,
41
+ key,
42
+ value
43
+ )
44
+ return this
45
+ },
46
+ setStatus(status, name) {
47
+ this.set('status', status, name)
48
+ this.save()
49
+ return this
50
+ },
51
+ getStatus(status, name) {
52
+ return this.get('status', name)
53
+ },
54
+ log(ctx = {}) {
55
+ return function (...chunks) {
56
+ const {pkg, scope = pkg?.name || '~', level = 'info'} = ctx
57
+ const msg = chunks.map(c => typeof c === 'string' ? tpl(c, ctx) : c)
58
+ const event = {msg, scope, date: Date.now(), level}
59
+ this.events.push(event)
60
+ logger[level](`[${scope}]`, ...msg)
61
+
62
+ return this
63
+ }.bind(this)
64
+ },
65
+ save() {
66
+ this.file && fs.outputJsonSync(this.file, this)
67
+ return this
68
+ }
69
+ })
@@ -1,14 +1,14 @@
1
1
  // Semantic tags processing
2
2
 
3
- import {ctx, semver, $} from 'zx-extra'
3
+ import {ctx, semver, $, fs, path} from 'zx-extra'
4
4
  import {Buffer} from 'buffer'
5
- import {parseEnv} from './config.js'
6
5
  import {log} from './log.js'
6
+ import {fetchRepo, pushCommit, getTags as getGitTags} from './git.js'
7
+ import {fetchManifest} from './npm.js'
7
8
 
8
9
  export const pushTag = (pkg) => ctx(async ($) => {
9
- const {absPath: cwd, name, version} = pkg
10
+ const {absPath: cwd, name, version, config: {gitCommitterEmail, gitCommitterName}} = pkg
10
11
  const tag = formatTag({name, version})
11
- const {gitCommitterEmail, gitCommitterName} = parseEnv($.env)
12
12
 
13
13
  pkg.context.git.tag = tag
14
14
  log({pkg})(`push release tag ${tag}`)
@@ -20,6 +20,41 @@ export const pushTag = (pkg) => ctx(async ($) => {
20
20
  await $`git push origin ${tag}`
21
21
  })
22
22
 
23
+ export const pushMeta = async (pkg) => {
24
+ log({pkg})('push artifact to branch \'meta\'')
25
+
26
+ const {name, version, absPath: cwd, config: {gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
27
+ const tag = formatTag({name, version})
28
+ const to = '.'
29
+ const branch = 'meta'
30
+ const msg = `chore: release meta ${name} ${version}`
31
+ const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
32
+ const meta = {
33
+ META_VERSION: '1',
34
+ name: pkg.name,
35
+ hash,
36
+ version: pkg.version,
37
+ dependencies: pkg.dependencies,
38
+ devDependencies: pkg.devDependencies,
39
+ peerDependencies: pkg.peerDependencies,
40
+ optionalDependencies: pkg.optionalDependencies,
41
+ }
42
+ const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
43
+
44
+ await pushCommit({cwd, to, branch, msg, files, gitCommitterEmail, gitCommitterName, basicAuth})
45
+ }
46
+
47
+ export const getLatest = async (pkg) => {
48
+ const {absPath: cwd, name} = pkg
49
+ const tag = await getLatestTag(cwd, name)
50
+ const meta = await getLatestMeta(cwd, tag?.ref) || await fetchManifest(pkg, {nothrow: true})
51
+
52
+ return {
53
+ tag,
54
+ meta
55
+ }
56
+ }
57
+
23
58
  const f0 = {
24
59
  parse(tag) {
25
60
  if (!tag.endsWith('-f0')) return null
@@ -102,9 +137,8 @@ export const parseTag = (tag) => f0.parse(tag) || f1.parse(tag) || lerna.parse(t
102
137
 
103
138
  export const formatTag = (tag) => f0.format(tag) || f1.format(tag) || null
104
139
 
105
- export const getTags = async (cwd) =>
106
- (await $.o({cwd})`git tag -l`).toString()
107
- .split('\n')
140
+ export const getTags = async (cwd, ref = '') =>
141
+ (await getGitTags(cwd, ref))
108
142
  .map(tag => parseTag(tag.trim()))
109
143
  .filter(Boolean)
110
144
  .sort((a, b) => semver.rcompare(a.version, b.version))
@@ -118,3 +152,19 @@ export const getLatestTaggedVersion = async (cwd, name) =>
118
152
  export const formatDateTag = (date = new Date()) => `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}`
119
153
 
120
154
  export const parseDateTag = (date) => new Date(date.replaceAll('.', '-')+'Z')
155
+
156
+ export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
157
+
158
+ export const getLatestMeta = async (cwd, tag) => {
159
+ if (!tag) return null
160
+
161
+ try {
162
+ const _cwd = await fetchRepo({cwd, branch: 'meta'})
163
+ return await Promise.any([
164
+ fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
165
+ fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
166
+ ])
167
+ } catch {}
168
+
169
+ return null
170
+ }
@@ -1,14 +1,13 @@
1
- import {parseEnv} from './config.js'
2
1
  import {log} from './log.js'
3
2
  import {$, ctx, fs, path, INI, fetch} from 'zx-extra'
4
3
 
5
- export const fetchPkg = async (pkg, {env = $.env} = {}) => {
4
+ export const fetchPkg = async (pkg) => {
6
5
  const id = `${pkg.name}@${pkg.version}`
7
6
 
8
7
  try {
9
8
  log({pkg})(`fetching '${id}'`)
10
9
  const cwd = pkg.absPath
11
- const {npmRegistry, npmToken, npmConfig} = parseEnv(env)
10
+ const {npmRegistry, npmToken, npmConfig} = pkg.config
12
11
  const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
13
12
  const tarball = getTarballUrl(npmRegistry, pkg.name, pkg.version)
14
13
  await $.raw`wget --timeout=10 --header='Authorization: ${bearerToken}' -qO- ${tarball} | tar -xvz --strip-components=1 --exclude='package.json' -C ${cwd}`
@@ -19,8 +18,8 @@ export const fetchPkg = async (pkg, {env = $.env} = {}) => {
19
18
  }
20
19
  }
21
20
 
22
- export const fetchManifest = async (pkg, {nothrow, env = $.env} = {}) => {
23
- const {npmRegistry, npmToken, npmConfig} = parseEnv(env)
21
+ export const fetchManifest = async (pkg, {nothrow} = {}) => {
22
+ const {npmRegistry, npmToken, npmConfig} = pkg.config
24
23
  const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
25
24
  const url = getManifestUrl(npmRegistry, pkg.name, pkg.version)
26
25
 
@@ -36,9 +35,9 @@ export const fetchManifest = async (pkg, {nothrow, env = $.env} = {}) => {
36
35
  }
37
36
 
38
37
  export const npmPublish = (pkg) => ctx(async ($) => {
39
- const {absPath: cwd, name, version, manifest} = pkg
40
- if (manifest.private || pkg.config?.npmPublish === false) return
41
- const {npmRegistry, npmToken, npmConfig} = parseEnv($.env)
38
+ const {absPath: cwd, name, version, manifest, config} = pkg
39
+ if (manifest.private || config?.npmPublish === false) return
40
+ const {npmRegistry, npmToken, npmConfig} = config
42
41
  const npmrc = npmConfig ? npmConfig : path.resolve(cwd, '.npmrc')
43
42
 
44
43
  log({pkg})(`publish npm package ${name} ${version} to ${npmRegistry}`)
@@ -1,69 +1,76 @@
1
1
  import os from 'node:os'
2
- import {$, within} from 'zx-extra'
3
- import {log} from './log.js'
4
- import {topo, traverseQueue} from './deps.js'
2
+ import {$, fs, within} from 'zx-extra'
3
+ import {queuefy} from 'queuefy'
5
4
  import {analyze} from './analyze.js'
6
- import {build} from './build.js'
7
- import {getLatest, publish} from './publish.js'
8
- import {createState} from './state.js'
5
+ import {pushChangelog} from './changelog.js'
6
+ import {getPkgConfig} from './config.js'
7
+ import {topo, traverseDeps, traverseQueue} from './deps.js'
8
+ import {ghPages, ghRelease} from './gh.js'
9
+ import {getRoot, getSha} from './git.js'
10
+ import {log, createReport} from './log.js'
11
+ import {getLatest, pushMeta, pushTag} from './meta.js'
12
+ import {fetchPkg, npmPublish} from './npm.js'
9
13
  import {memoizeBy, tpl} from './util.js'
10
- import {queuefy} from 'queuefy'
11
- import {getConfig} from "./config.js";
12
14
 
13
15
  export const run = async ({cwd = process.cwd(), env, flags = {}} = {}) => within(async () => {
14
- const {state, build, publish} = createContext(flags, env)
16
+ const {report, build, publish} = createContext(flags, env)
15
17
 
16
- log()('zx-bulk-release')
18
+ report.log()('zx-bulk-release')
17
19
 
18
20
  try {
19
- const {packages, queue, root, sources, next, prev, nodes} = await topo({cwd, flags})
20
- log()('queue:', queue)
21
-
22
- state.setQueue(queue, packages)
21
+ const {packages, queue, root, prev, graphs} = await topo({cwd, flags})
22
+ report
23
+ .log()('queue:', queue)
24
+ .log()('graphs', graphs)
25
+ .set('queue', queue)
26
+ .setPackages(packages)
23
27
 
24
28
  await traverseQueue({queue, prev, async cb(name) {
25
- state.setStatus('analyzing', name)
29
+ report.setStatus('analyzing', name)
26
30
  const pkg = packages[name]
27
31
  await contextify(pkg, packages, root)
28
32
  await analyze(pkg, packages)
29
- state.set('config', pkg.config, name)
30
- state.set('version', pkg.version, name)
31
- state.set('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
32
- state.set('releaseType', pkg.releaseType, name)
33
- state.set('tag', pkg.tag, name)
33
+ report
34
+ .set('config', pkg.config, name)
35
+ .set('version', pkg.version, name)
36
+ .set('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
37
+ .set('releaseType', pkg.releaseType, name)
38
+ .set('tag', pkg.tag, name)
34
39
  }})
35
40
 
36
- state.setStatus('pending')
41
+ report.setStatus('pending')
37
42
 
38
43
  await traverseQueue({queue, prev, async cb(name) {
39
44
  const pkg = packages[name]
40
45
 
41
46
  if (!pkg.releaseType) {
42
- state.setStatus('skipped', name)
47
+ report.setStatus('skipped', name)
43
48
  return
44
49
  }
45
50
 
46
- state.setStatus('building', name)
51
+ report.setStatus('building', name)
47
52
  await build(pkg, packages)
48
53
 
49
54
  if (flags.dryRun) {
50
- state.setStatus('success', name)
55
+ report.setStatus('success', name)
51
56
  return
52
57
  }
53
58
 
54
- state.setStatus('publishing', name)
59
+ report.setStatus('publishing', name)
55
60
  await publish(pkg)
56
61
 
57
- state.setStatus('success', name)
62
+ report.setStatus('success', name)
58
63
  }})
59
64
  } catch (e) {
60
- log({level: 'error'})(e)
61
- state.set('error', e)
62
- state.setStatus('failure')
65
+ report
66
+ .log({level: 'error'})(e)
67
+ .set('error', e)
68
+ .setStatus('failure')
63
69
  throw e
64
70
  }
65
- state.setStatus('success')
66
- log()('Great success!')
71
+ report
72
+ .setStatus('success')
73
+ .log()('Great success!')
67
74
  })
68
75
 
69
76
  export const runCmd = async (pkg, name) => {
@@ -76,17 +83,17 @@ export const runCmd = async (pkg, name) => {
76
83
  }
77
84
 
78
85
  const createContext = (flags, env) => {
79
- const state = createState({file: flags.report})
86
+ const report = createReport({file: flags.report})
80
87
  const _runCmd = queuefy(runCmd, flags.concurrency || os.cpus().length)
81
88
  const _build = memoizeBy((pkg, packages) => build(pkg, packages, _runCmd, _build))
82
89
  const _publish = memoizeBy((pkg) => publish(pkg, _runCmd))
83
90
 
84
- $.state = state
91
+ $.report = report
85
92
  $.env = {...process.env, ...env}
86
93
  $.verbose = !!(flags.debug || $.env.DEBUG ) || $.verbose
87
94
 
88
95
  return {
89
- state,
96
+ report,
90
97
  runCmd: _runCmd,
91
98
  build: _build,
92
99
  publish: _publish
@@ -94,15 +101,47 @@ const createContext = (flags, env) => {
94
101
  }
95
102
 
96
103
  // Inspired by https://docs.github.com/en/actions/learn-github-actions/contexts
97
- export const contextify = async (pkg, packages, root) => {
98
- pkg.config = await getConfig(pkg.absPath, root.absPath)
104
+ const contextify = async (pkg, packages, root) => {
105
+ pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
99
106
  pkg.latest = await getLatest(pkg)
100
107
  pkg.context = {
101
108
  git: {
102
- sha: (await $`git rev-parse HEAD`).toString().trim(),
103
- root: (await $`git rev-parse --show-toplevel`).toString().trim(),
109
+ sha: await getSha(pkg.absPath),
110
+ root: await getRoot(pkg.absPath)
104
111
  },
105
112
  env: $.env,
106
113
  packages
107
114
  }
108
115
  }
116
+
117
+ const build = async (pkg, packages, run = runCmd, self = build) => {
118
+ if (pkg.built) return
119
+
120
+ await Promise.all([
121
+ traverseDeps(pkg, packages, async (_, {pkg}) => self(pkg, packages, run, self)),
122
+ pkg.changes.length === 0 && pkg.config.npmFetch
123
+ ? fetchPkg(pkg)
124
+ : Promise.resolve()
125
+ ])
126
+
127
+ if (!pkg.fetched) {
128
+ await run(pkg, 'buildCmd')
129
+ await run(pkg, 'testCmd')
130
+ }
131
+
132
+ pkg.built = true
133
+ }
134
+
135
+ const publish = async (pkg, run = runCmd) => {
136
+ await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
137
+ await pushTag(pkg)
138
+
139
+ await Promise.all([
140
+ pushMeta(pkg),
141
+ pushChangelog(pkg),
142
+ npmPublish(pkg),
143
+ ghRelease(pkg),
144
+ ghPages(pkg),
145
+ run(pkg, 'publishCmd')
146
+ ])
147
+ }
@@ -55,3 +55,5 @@ export const memoizeBy = (fn, memo = new Map(), getKey = v => v) => (...args) =>
55
55
  memo.set(key, value)
56
56
  return value
57
57
  }
58
+
59
+ export const camelize = s => s.replace(/-./g, x => x[1].toUpperCase())
@@ -1,21 +0,0 @@
1
- import {traverseDeps} from './deps.js'
2
- import {fetchPkg} from './npm.js'
3
- import {runCmd} from './processor.js'
4
-
5
- export const build = async (pkg, packages, run = runCmd, self = build) => {
6
- if (pkg.built) return true
7
-
8
- await traverseDeps(pkg, packages, async (_, {pkg}) => self(pkg, packages, run, self))
9
-
10
- if (pkg.changes.length === 0 && pkg.config.npmFetch) {
11
- await fetchPkg(pkg)
12
- }
13
-
14
- if (!pkg.fetched) {
15
- await run(pkg, 'buildCmd')
16
- await run(pkg, 'testCmd')
17
- }
18
-
19
- pkg.built = true
20
- return true
21
- }
@@ -1,146 +0,0 @@
1
- import {fs, path, $} from 'zx-extra'
2
- import {formatTag, getLatestTag, pushTag} from './tag.js'
3
- import {push, fetch, parseRepo} from './repo.js'
4
- import {parseEnv} from './config.js'
5
- import {fetchManifest, npmPublish} from './npm.js'
6
- import {msgJoin} from './util.js'
7
- import {runCmd} from './processor.js'
8
- import {log} from './log.js'
9
-
10
- export const publish = async (pkg, run = runCmd) => {
11
- await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
12
- await pushTag(pkg)
13
- await pushMeta(pkg)
14
- await pushChangelog(pkg)
15
- await npmPublish(pkg)
16
- await ghRelease(pkg)
17
- await ghPages(pkg)
18
- await run(pkg, 'publishCmd')
19
- }
20
-
21
- export const pushMeta = async (pkg) => {
22
- log({pkg})('push artifact to branch \'meta\'')
23
-
24
- const {name, version, absPath: cwd} = pkg
25
- const tag = formatTag({name, version})
26
- const to = '.'
27
- const branch = 'meta'
28
- const msg = `chore: release meta ${name} ${version}`
29
- const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
30
- const meta = {
31
- META_VERSION: '1',
32
- name: pkg.name,
33
- hash,
34
- version: pkg.version,
35
- dependencies: pkg.dependencies,
36
- devDependencies: pkg.devDependencies,
37
- peerDependencies: pkg.peerDependencies,
38
- optionalDependencies: pkg.optionalDependencies,
39
- }
40
- const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
41
-
42
- await push({cwd, to, branch, msg, files})
43
- }
44
-
45
- export const ghRelease = async (pkg) => {
46
- log({pkg})(`create gh release`)
47
-
48
- const {ghToken} = parseEnv($.env)
49
- if (!ghToken) return null
50
-
51
- const {name, version, absPath: cwd} = pkg
52
- const {repoName} = await parseRepo(cwd)
53
- const tag = formatTag({name, version})
54
- const releaseNotes = await formatReleaseNotes(pkg)
55
- const releaseData = JSON.stringify({
56
- name: tag,
57
- tag_name: tag,
58
- body: releaseNotes
59
- })
60
-
61
- await $.o({cwd})`curl -H "Authorization: token ${ghToken}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${repoName}/releases -d ${releaseData}`
62
- }
63
-
64
- const pushChangelog = async (pkg) => {
65
- const {config: {changelog: opts}} = pkg
66
- if (!opts) return
67
-
68
- log({pkg})('push changelog')
69
- const [branch = 'changelog', file = `${pkg.name.replace(/[^a-z0-9-]/ig, '')}-changelog.md`, ..._msg] = typeof opts === 'string'
70
- ? opts.split(' ')
71
- : [opts.branch, opts.file, opts.msg]
72
- const _cwd = await fetch({cwd: pkg.absPath, branch})
73
- const msg = msgJoin(_msg, pkg, 'chore: update changelog ${{name}}')
74
- const releaseNotes = await formatReleaseNotes(pkg)
75
-
76
- await $.o({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
77
- await push({cwd: pkg.absPath, branch, msg})
78
- }
79
-
80
- const formatReleaseNotes = async (pkg) => {
81
- const {name, version, absPath: cwd} = pkg
82
- const {repoPublicUrl} = await parseRepo(cwd)
83
- const tag = formatTag({name, version})
84
- const releaseDiffRef = `## [${name}@${version}](${repoPublicUrl}/compare/${pkg.latest.tag?.ref}...${tag}) (${new Date().toISOString().slice(0, 10)})`
85
- const releaseDetails = Object.values(pkg.changes
86
- .reduce((acc, {group, subj, short, hash}) => {
87
- const {commits} = acc[group] || (acc[group] = {commits: [], group})
88
- const commitRef = `* ${subj}${short ? ` [${short}](${repoPublicUrl}/commit/${hash})` : ''}`
89
-
90
- commits.push(commitRef)
91
-
92
- return acc
93
- }, {}))
94
- .map(({group, commits}) => `
95
- ### ${group}
96
- ${commits.join('\n')}`).join('\n')
97
-
98
- return releaseDiffRef + '\n' + releaseDetails + '\n'
99
- }
100
-
101
- const ghPages = async (pkg) => {
102
- const {config: {ghPages: opts}} = pkg
103
- if (!opts) return
104
-
105
- const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = typeof opts === 'string'
106
- ? opts.split(' ')
107
- : [opts.branch, opts.from, opts.to, opts.msg]
108
- const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
109
-
110
- log({pkg})(`publish docs to ${branch}`)
111
-
112
- await push({
113
- cwd: path.join(pkg.absPath, from),
114
- from: '.',
115
- to,
116
- branch,
117
- msg
118
- })
119
- }
120
-
121
- export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
122
-
123
- export const getLatestMeta = async (cwd, tag) => {
124
- if (!tag) return null
125
-
126
- try {
127
- const _cwd = await fetch({cwd, branch: 'meta'})
128
- return await Promise.any([
129
- fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
130
- fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
131
- ])
132
- } catch {}
133
-
134
- return null
135
- }
136
-
137
- export const getLatest = async (pkg) => {
138
- const {absPath: cwd, name} = pkg
139
- const tag = await getLatestTag(cwd, name)
140
- const meta = await getLatestMeta(cwd, tag?.ref) || await fetchManifest(pkg, {nothrow: true})
141
-
142
- return {
143
- tag,
144
- meta
145
- }
146
- }
@@ -1,56 +0,0 @@
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
- })