bulk-release 2.20.0 → 2.21.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -74
  3. package/package.json +14 -7
  4. package/src/main/js/index.js +0 -6
  5. package/src/main/js/processor/api/gh.js +111 -0
  6. package/src/main/js/{api → processor/api}/git.js +17 -26
  7. package/src/main/js/{api → processor/api}/npm.js +70 -28
  8. package/src/main/js/processor/deps.js +1 -1
  9. package/src/main/js/processor/exec.js +4 -4
  10. package/src/main/js/processor/generators/meta.js +80 -0
  11. package/src/main/js/{api/changelog.js → processor/generators/notes.js} +3 -21
  12. package/src/main/js/processor/{meta.js → generators/tag.js} +3 -109
  13. package/src/main/js/processor/log.js +86 -0
  14. package/src/main/js/processor/publishers/changelog.js +26 -0
  15. package/src/main/js/processor/publishers/cmd.js +6 -0
  16. package/src/main/js/processor/publishers/gh-pages.js +32 -0
  17. package/src/main/js/processor/publishers/gh-release.js +41 -0
  18. package/src/main/js/processor/publishers/meta.js +58 -0
  19. package/src/main/js/processor/publishers/npm.js +15 -0
  20. package/src/main/js/processor/release.js +71 -66
  21. package/src/main/js/{steps → processor/steps}/analyze.js +18 -24
  22. package/src/main/js/processor/steps/build.js +20 -0
  23. package/src/main/js/processor/steps/clean.js +7 -0
  24. package/src/main/js/processor/steps/contextify.js +49 -0
  25. package/src/main/js/processor/steps/publish.js +39 -0
  26. package/src/main/js/processor/steps/teardown.js +58 -0
  27. package/src/main/js/processor/steps/test.js +10 -0
  28. package/src/main/js/util.js +32 -77
  29. package/src/test/js/utils/gh-server.js +33 -0
  30. package/src/test/js/utils/mock.js +132 -0
  31. package/src/test/js/{test-utils.js → utils/repo.js} +3 -3
  32. package/src/main/js/api/gh.js +0 -131
  33. package/src/main/js/log.js +0 -63
  34. package/src/main/js/steps/build.js +0 -23
  35. package/src/main/js/steps/clean.js +0 -7
  36. package/src/main/js/steps/contextify.js +0 -154
  37. package/src/main/js/steps/publish.js +0 -47
  38. package/src/main/js/steps/test.js +0 -16
@@ -1,6 +1,15 @@
1
+ import zlib from 'node:zlib'
2
+ import _fs from 'node:fs/promises'
3
+ import _path from 'node:path'
4
+ import tar from 'tar-stream'
5
+ import {Readable} from 'node:stream'
1
6
  import {log} from '../log.js'
2
- import {$, fs, INI, fetch, tempy} from 'zx-extra'
3
- import {pipify, unzip} from '../util.js'
7
+ import {$, semver, fs, INI, fetch, tempy} from 'zx-extra'
8
+ import {attempt2, memoizeBy} from '../../util.js'
9
+
10
+ const FETCH_TIMEOUT_MS = 15_000
11
+ const NPM_OIDC_VER = '11.5.0'
12
+ const NPM_VER = (await $`npm --version`).toString().trim()
4
13
 
5
14
  // https://stackoverflow.com/questions/19978452/how-to-extract-single-file-from-tar-gz-archive-using-node-js
6
15
 
@@ -14,17 +23,16 @@ export const fetchPkg = async (pkg) => {
14
23
  const tarballUrl = getTarballUrl(npmRegistry, pkg.name, pkg.version)
15
24
  const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
16
25
  const headers = bearerToken ? {Authorization: bearerToken} : {}
17
- log({pkg})(`fetching '${id}' from ${npmRegistry}`)
26
+ log.info(`fetching '${id}' from ${npmRegistry}`)
18
27
 
19
- // https://stackoverflow.com/questions/46946380/fetch-api-request-timeout
20
- const controller = new AbortController()
21
- const timeoutId = setTimeout(() => controller.abort(), 15_000)
22
- const tarball = await fetch(tarballUrl, {
28
+ const ac = new AbortController()
29
+ const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS)
30
+ const tarball = await attempt2(() => fetch(tarballUrl, {
23
31
  method: 'GET',
24
32
  headers,
25
- signal: controller.signal
26
- })
27
- clearTimeout(timeoutId)
33
+ signal: ac.signal,
34
+ }))
35
+ clearTimeout(timer)
28
36
 
29
37
  if (!tarball.ok) {
30
38
  throw new Error(`registry responded with ${tarball.status} for ${tarballUrl}`)
@@ -32,10 +40,10 @@ export const fetchPkg = async (pkg) => {
32
40
 
33
41
  await unzip(pipify(tarball.body), {cwd, strip: 1, omit: ['package.json']})
34
42
 
35
- log({pkg})(`fetch duration '${id}': ${Date.now() - now}`)
43
+ log.info(`fetch duration '${id}': ${Date.now() - now}`)
36
44
  pkg.fetched = true
37
45
  } catch (e) {
38
- log({pkg, level: 'warn'})(`fetching '${id}' failed`, e)
46
+ log.warn(`fetching '${id}' failed`, e)
39
47
  }
40
48
  }
41
49
 
@@ -46,8 +54,8 @@ export const fetchManifest = async (pkg, {nothrow} = {}) => {
46
54
  const reqOpts = bearerToken ? {headers: {authorization: bearerToken}} : {}
47
55
 
48
56
  try {
49
- const res = await fetch(url, reqOpts)
50
- if (!res.ok) throw res
57
+ const res = await attempt2(() => fetch(url, reqOpts))
58
+ if (!res.ok) throw new Error(`npm registry responded with ${res.status} for ${url}`)
51
59
 
52
60
  return res.json() // NOTE .json() is async too
53
61
  } catch (e) {
@@ -58,13 +66,13 @@ export const fetchManifest = async (pkg, {nothrow} = {}) => {
58
66
 
59
67
  export const npmPersist = async (pkg) => {
60
68
  const {name, version, manifest, manifestAbsPath} = pkg
61
- log({pkg})(`updating ${manifestAbsPath} inners: ${name} ${version}`)
69
+ log.info(`updating ${manifestAbsPath} inners: ${name} ${version}`)
62
70
  await fs.writeJson(manifestAbsPath, manifest, {spaces: 2})
63
71
  }
64
72
 
65
73
  export const npmRestore = async (pkg) => {
66
74
  const {manifestRaw, manifestAbsPath} = pkg
67
- log({pkg})(`rolling back ${manifestAbsPath} inners to manifestRaw`)
75
+ log.info(`rolling back ${manifestAbsPath} inners to manifestRaw`)
68
76
  await fs.writeFile(manifestAbsPath, manifestRaw, {encoding: 'utf8'})
69
77
  }
70
78
 
@@ -73,7 +81,7 @@ export const npmPublish = async (pkg) => {
73
81
 
74
82
  if (manifest.private || npmPublish === false) return
75
83
 
76
- log({pkg})(`publishing npm package ${name} ${version} to ${npmRegistry}`)
84
+ log.info(`publishing npm package ${name} ${version} to ${npmRegistry}`)
77
85
 
78
86
  const npmTag = pkg.preversion ? 'snapshot' : 'latest'
79
87
  const npmFlags = [
@@ -86,12 +94,10 @@ export const npmPublish = async (pkg) => {
86
94
  // OIDC trusted publishing: no auth token must be present for npm to use OIDC flow.
87
95
  // https://docs.npmjs.com/trusted-publishers/
88
96
  if (npmOidc) {
89
- const npmVersion = (await $`npm --version`).toString().trim()
90
- const [major, minor] = npmVersion.split('.').map(Number)
91
- if (major < 11 || (major === 11 && minor < 5)) {
92
- throw new Error(`npm OIDC trusted publishing requires npm >= 11.5.0, got ${npmVersion}`)
97
+ if (!semver.gte(NPM_VER, NPM_OIDC_VER)) {
98
+ throw new Error(`npm OIDC trusted publishing requires npm >= ${NPM_OIDC_VER}, got ${NPM_VER}`)
93
99
  }
94
- log({pkg})('npm publish: OIDC trusted publishing enabled')
100
+ log.info('npm publish: OIDC trusted publishing enabled')
95
101
  npmFlags.push('--provenance')
96
102
  } else {
97
103
  const npmrc = await getNpmrc({npmConfig, npmToken, npmRegistry})
@@ -102,16 +108,14 @@ export const npmPublish = async (pkg) => {
102
108
  await $({cwd})`npm publish ${npmFlags.filter(Boolean)}`
103
109
  }
104
110
 
105
- export const getNpmrc = async ({npmConfig, npmToken, npmRegistry}) => {
106
- if (npmConfig) {
107
- return npmConfig
108
- }
111
+ export const getNpmrc = memoizeBy(async ({npmConfig, npmToken, npmRegistry}) => {
112
+ if (npmConfig) return npmConfig
109
113
 
110
- const npmrc = tempy.temporaryFile({name: '.npmrc'})
114
+ const npmrc = tempy.temporaryFile({name: '.npmrc'})
111
115
  await fs.writeFile(npmrc, `${npmRegistry.replace(/^https?:\/\//, '//')}/:_authToken=${npmToken}`, {encoding: 'utf8'})
112
116
 
113
117
  return npmrc
114
- }
118
+ }, ({npmConfig, npmToken, npmRegistry}) => `${npmConfig}:${npmToken}:${npmRegistry}`)
115
119
 
116
120
  // $`npm view ${name}@${version} dist.tarball`
117
121
  export const getTarballUrl = (registry, name, version) => `${registry}/${name}/-/${name.replace(/^.+(%2f|\/)/,'')}-${version}.tgz`
@@ -128,3 +132,41 @@ export const getBearerToken = (npmRegistry, npmToken, npmConfig) => {
128
132
  // NOTE registry-auth-token does not work with localhost:4873
129
133
  export const getAuthToken = (registry, npmrc) =>
130
134
  (Object.entries(npmrc).find(([reg]) => reg.startsWith(registry.replace(/^https?/, ''))) || [])[1]
135
+
136
+ const pipify = (stream) => stream.pipe ? stream : Readable.from(stream)
137
+
138
+ const safePath = v => _path.resolve('/', v).slice(1)
139
+
140
+ const unzip = (stream, {pick, omit, cwd = process.cwd(), strip = 0} = {}) => new Promise((resolve, reject) => {
141
+ const extract = tar.extract()
142
+ const results = []
143
+
144
+ extract.on('entry', ({name, type}, stream, cb) => {
145
+ const _name = safePath(strip ? name.split('/').slice(strip).join('/') : name)
146
+ const fp = _path.join(cwd, _name)
147
+
148
+ let data = ''
149
+ stream.on('data', (chunk) => {
150
+ if (type !== 'file' || omit?.includes(_name) || (pick && !pick.includes(_name))) return
151
+ data += chunk
152
+ })
153
+
154
+ stream.on('end', () => {
155
+ if (data) {
156
+ results.push(
157
+ _fs.mkdir(_path.dirname(fp), {recursive: true})
158
+ .then(() => _fs.writeFile(fp, data, 'utf8'))
159
+ )
160
+ }
161
+ cb()
162
+ })
163
+
164
+ stream.resume()
165
+ })
166
+
167
+ extract.on('finish', () => resolve(Promise.all(results)))
168
+
169
+ stream
170
+ .pipe(zlib.createGunzip())
171
+ .pipe(extract)
172
+ })
@@ -5,7 +5,7 @@ export {traverseQueue, traverseDeps} from '@semrel-extra/topo'
5
5
 
6
6
  export const updateDeps = async (pkg) => {
7
7
  const changes = []
8
- const {context: {packages}} = pkg
8
+ const {packages} = pkg.ctx
9
9
 
10
10
  await traverseDeps({pkg, packages, cb: async ({name, version, deps, scope, pkg: dep}) => {
11
11
  const prev = pkg.latest.meta?.[scope]?.[name]
@@ -1,16 +1,16 @@
1
1
  import {tpl} from '../util.js'
2
- import {log} from '../log.js'
2
+ import {log} from './log.js'
3
3
  import {$} from 'zx-extra'
4
4
 
5
5
  export const exec = async (pkg, name) => {
6
- const cmd = tpl(pkg.context.flags[name] ?? pkg.config[name], {...pkg, ...pkg.context})
6
+ const cmd = tpl(pkg.ctx.flags[name] ?? pkg.config[name], {...pkg, ...pkg.ctx})
7
7
  const now = Date.now()
8
8
 
9
9
  if (cmd) {
10
- log({pkg})(`run ${name} '${cmd}'`)
10
+ log.info(`run ${name} '${cmd}'`)
11
11
  const result = await $({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
12
12
 
13
- log({pkg})(`duration ${name}: ${Date.now() - now}`)
13
+ log.info(`duration ${name}: ${Date.now() - now}`)
14
14
  return result
15
15
  }
16
16
  }
@@ -0,0 +1,80 @@
1
+ // Meta generator: builds pkg.meta payload and resolves latest-release meta from git tags / gh assets / meta branch.
2
+
3
+ import {semver, $, fs, path} from 'zx-extra'
4
+ import {fetchRepo, getTags as getGitTags, getRepo} from '../api/git.js'
5
+ import {fetchManifest} from '../api/npm.js'
6
+ import {ghGetAsset} from '../api/gh.js'
7
+ import {parseTag} from './tag.js'
8
+
9
+ export const isAssetMode = (type) => type === 'asset' || type === 'assets'
10
+
11
+ // Build pkg.meta and, in asset mode, inject a meta.json entry into pkg.config.ghAssets.
12
+ // Runs in the prepare phase — serial, before any publisher.run() — so the asset mutation
13
+ // is guaranteed visible to gh-release.
14
+ export const prepareMeta = async (pkg) => {
15
+ const {type} = pkg.config.meta
16
+ if (type === null) return
17
+
18
+ const {absPath: cwd} = pkg
19
+ const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
20
+ pkg.meta = {
21
+ META_VERSION: '1',
22
+ hash,
23
+ name: pkg.name,
24
+ version: pkg.version,
25
+ dependencies: pkg.dependencies,
26
+ devDependencies: pkg.devDependencies,
27
+ peerDependencies: pkg.peerDependencies,
28
+ optionalDependencies: pkg.optionalDependencies,
29
+ }
30
+
31
+ if (isAssetMode(type)) {
32
+ pkg.config.ghAssets = [...pkg.config.ghAssets || [], {
33
+ name: 'meta.json',
34
+ contents: JSON.stringify(pkg.meta, null, 2),
35
+ }]
36
+ }
37
+ }
38
+
39
+ export const getLatest = async (pkg) => {
40
+ const {absPath: cwd, name} = pkg
41
+ const tag = await getLatestTag(cwd, name)
42
+ const meta = await getLatestMeta(pkg, tag)
43
+
44
+ return {tag, meta}
45
+ }
46
+
47
+ export const getTags = async (cwd, ref) =>
48
+ (await getGitTags(cwd, ref))
49
+ .map(tag => parseTag(tag.trim()))
50
+ .filter(Boolean)
51
+ .sort((a, b) => semver.rcompare(a.version, b.version))
52
+
53
+ export const getLatestTag = async (cwd, name) =>
54
+ (await getTags(cwd)).find(tag => tag.name === name)
55
+
56
+ export const getLatestTaggedVersion = async (cwd, name) =>
57
+ (await getLatestTag(cwd, name))?.version || undefined
58
+
59
+ export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
60
+
61
+ export const getLatestMeta = async (pkg, tag) => {
62
+ if (tag) {
63
+ const {absPath: cwd, config: {ghBasicAuth: basicAuth, ghUrl}} = pkg
64
+ const {repoName} = await getRepo(cwd, {basicAuth})
65
+
66
+ try {
67
+ return JSON.parse(await ghGetAsset({repoName, tag, name: 'meta.json', ghUrl}))
68
+ } catch {}
69
+
70
+ try {
71
+ const _cwd = await fetchRepo({cwd, branch: 'meta', basicAuth})
72
+ return await Promise.any([
73
+ fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
74
+ fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
75
+ ])
76
+ } catch {}
77
+ }
78
+
79
+ return fetchManifest(pkg, {nothrow: true})
80
+ }
@@ -1,25 +1,7 @@
1
- import {$} from 'zx-extra'
2
- import {queuefy} from 'queuefy'
3
- import {fetchRepo, getRepo, pushCommit} from './git.js'
4
- import {log} from '../log.js'
5
- import {formatTag} from '../processor/meta.js'
6
- import {msgJoin} from '../util.js'
1
+ // Release notes formatting. Pure except for a single getRepo() call to resolve repoPublicUrl.
7
2
 
8
- export const pushChangelog = queuefy(async (pkg) => {
9
- const {absPath: cwd, config: {changelog: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
10
- if (!opts) return
11
-
12
- log({pkg})('push changelog')
13
- const [branch = 'changelog', file = `${pkg.name.replace(/[^a-z0-9-]/ig, '')}-changelog.md`, ..._msg] = typeof opts === 'string'
14
- ? opts.split(' ')
15
- : [opts.branch, opts.file, opts.msg]
16
- const _cwd = await fetchRepo({cwd, branch, basicAuth})
17
- const msg = msgJoin(_msg, pkg, 'chore: update changelog ${{name}}')
18
- const releaseNotes = await formatReleaseNotes(pkg)
19
-
20
- await $({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
21
- await pushCommit({cwd, branch, msg, gitCommitterEmail, gitCommitterName, basicAuth})
22
- })
3
+ import {getRepo} from '../api/git.js'
4
+ import {formatTag} from './tag.js'
23
5
 
24
6
  export const DIFF_TAG_URL = '${repoPublicUrl}/compare/${prevTag}...${newTag}'
25
7
  export const DIFF_COMMIT_URL = '${repoPublicUrl}/commit/${hash}'
@@ -1,78 +1,7 @@
1
- // Semantic tags processing
1
+ // Pure tag parsing/formatting. No IO, no side effects.
2
2
 
3
3
  import {Buffer} from 'node:buffer'
4
- import {queuefy} from 'queuefy'
5
- import {semver, $, fs, path} from 'zx-extra'
6
- import {log} from '../log.js'
7
- import {fetchRepo, pushCommit, getTags as getGitTags, pushTag, getRepo} from '../api/git.js'
8
- import {fetchManifest} from '../api/npm.js'
9
- import {ghGetAsset} from '../api/gh.js'
10
-
11
- export const pushReleaseTag = async (pkg) => {
12
- const {name, version, tag = formatTag({name, version}), config: {gitCommitterEmail, gitCommitterName}} = pkg
13
- const cwd = pkg.context.git.root
14
-
15
- pkg.context.git.tag = tag
16
- log({pkg})(`push release tag ${tag}`)
17
-
18
- await pushTag({cwd, tag, gitCommitterEmail, gitCommitterName})
19
- }
20
-
21
- export const prepareMeta = async (pkg) => {
22
- const {absPath: cwd} = pkg
23
- const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
24
- pkg.meta = {
25
- META_VERSION: '1',
26
- hash,
27
- name: pkg.name,
28
- version: pkg.version,
29
- dependencies: pkg.dependencies,
30
- devDependencies: pkg.devDependencies,
31
- peerDependencies: pkg.peerDependencies,
32
- optionalDependencies: pkg.optionalDependencies,
33
- }
34
- }
35
-
36
- export const pushMeta = queuefy(async (pkg) => {
37
- const {type} = pkg.config.meta
38
-
39
- if (type === null) {
40
- return
41
- }
42
-
43
- if (!pkg.meta) {
44
- await prepareMeta(pkg)
45
- }
46
-
47
- if (type === 'asset' || type === 'assets') {
48
- pkg.config.ghAssets = [...pkg.config.ghAssets || [], {
49
- name: 'meta.json',
50
- contents: JSON.stringify(pkg.meta, null, 2)
51
- }]
52
- return
53
- }
54
-
55
- log({pkg})('push artifact to branch \'meta\'')
56
-
57
- const {name, version, meta, tag = formatTag({name, version}), absPath: cwd, config: {gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
58
- const to = '.'
59
- const branch = 'meta'
60
- const msg = `chore: release meta ${name} ${version}`
61
- const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
62
-
63
- await pushCommit({cwd, to, branch, msg, files, gitCommitterEmail, gitCommitterName, basicAuth})
64
- })
65
-
66
- export const getLatest = async (pkg) => {
67
- const {absPath: cwd, name } = pkg
68
- const tag = await getLatestTag(cwd, name)
69
- const meta = await getLatestMeta(pkg, tag)
70
-
71
- return {
72
- tag,
73
- meta
74
- }
75
- }
4
+ import {semver} from 'zx-extra'
76
5
 
77
6
  const isSafeName = n => /^(@?[a-z0-9-]+\/)?[a-z0-9-]+$/.test(n)
78
7
 
@@ -197,41 +126,6 @@ export const parseTag = (tag) => f0.parse(tag) || f1.parse(tag) || lerna.parse(t
197
126
 
198
127
  export const formatTag = (tag, tagFormat = tag.format) => getFormatter(tagFormat).format(tag) || f0.format(tag) || f1.format(tag) || null
199
128
 
200
- export const getTags = async (cwd, ref = '') =>
201
- (await getGitTags(cwd, ref))
202
- .map(tag => parseTag(tag.trim()))
203
- .filter(Boolean)
204
- .sort((a, b) => semver.rcompare(a.version, b.version))
205
-
206
- export const getLatestTag = async (cwd, name) =>
207
- (await getTags(cwd)).find(tag => tag.name === name)
208
-
209
- export const getLatestTaggedVersion = async (cwd, name) =>
210
- (await getLatestTag(cwd, name))?.version || undefined
211
-
212
129
  export const formatDateTag = (date = new Date()) => `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}`
213
130
 
214
- export const parseDateTag = (date) => new Date(date.replaceAll('.', '-')+'Z')
215
-
216
- export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
217
-
218
- export const getLatestMeta = async (pkg, tag) => {
219
- if (tag) {
220
- const {absPath: cwd, config: {ghBasicAuth: basicAuth, ghUrl}} = pkg
221
- const {repoName} = await getRepo(cwd, {basicAuth})
222
-
223
- try {
224
- return JSON.parse(await ghGetAsset({repoName, tag, name: 'meta.json', ghUrl}))
225
- } catch {}
226
-
227
- try {
228
- const _cwd = await fetchRepo({cwd, branch: 'meta', basicAuth})
229
- return await Promise.any([
230
- fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
231
- fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
232
- ])
233
- } catch {}
234
- }
235
-
236
- return fetchManifest(pkg, {nothrow: true})
237
- }
131
+ export const parseDateTag = (date) => new Date(date.replaceAll('.', '-') + 'Z')
@@ -0,0 +1,86 @@
1
+ import {$, fs} from 'zx-extra'
2
+ import {get, set} from '../util.js'
3
+
4
+ // Credential redactor: masks tokens/passwords that may leak into log output.
5
+ // Secrets are registered via `log.secret(value)` — typically once, when env/config is parsed.
6
+ const secrets = new Set()
7
+ export const redact = (v) => {
8
+ if (!secrets.size || typeof v !== 'string') return v
9
+ for (const s of secrets) v = v.replaceAll(s, '***')
10
+ return v
11
+ }
12
+
13
+ // Module-level logger. Delegates to $.report when inside a release run, falls back to console.
14
+ const sanitize = (args) => args.map(a => typeof a === 'string' ? redact(a) : a)
15
+ const r = () => $.report || console
16
+
17
+ export const log = Object.assign(
18
+ (...args) => r().log(...sanitize(args)),
19
+ {
20
+ info: (...args) => r().log(...sanitize(args)),
21
+ warn: (...args) => r().warn(...sanitize(args)),
22
+ error: (...args) => r().error(...sanitize(args)),
23
+ secret: (...values) => values.forEach(v => { if (v) secrets.add(String(v)) }),
24
+ }
25
+ )
26
+
27
+ export const createReport = ({logger = console, packages = {}, queue = [], flags} = {}) => ({
28
+ logger,
29
+ flags,
30
+ file: flags.report || flags.file,
31
+ status: 'initial',
32
+ events: [],
33
+ queue,
34
+ packages: Object.entries(packages).reduce((acc, [name, {manifest: {version}, absPath, relPath}]) => {
35
+ acc[name] = {
36
+ status: 'initial',
37
+ name,
38
+ version,
39
+ path: absPath,
40
+ relPath
41
+ }
42
+ return acc
43
+ }, {}),
44
+ get(key, pkgName) {
45
+ return get(
46
+ pkgName ? this.packages[pkgName] : this,
47
+ key
48
+ )
49
+ },
50
+ set(key, value, pkgName) {
51
+ // set({k1: v1, k2: v2}, pkgName) — batch
52
+ if (key && typeof key === 'object') {
53
+ const target = value ? this.packages[value] : this
54
+ for (const [k, v] of Object.entries(key)) set(target, k, v)
55
+ return this
56
+ }
57
+ set(
58
+ pkgName ? this.packages[pkgName] : this,
59
+ key,
60
+ value
61
+ )
62
+ return this
63
+ },
64
+ setStatus(status, name) {
65
+ this.set('status', status, name)
66
+ this.save()
67
+ return this
68
+ },
69
+ getStatus(status, name) {
70
+ return this.get('status', name)
71
+ },
72
+ _log(level, ...chunks) {
73
+ const scope = $.scope || '~'
74
+ const msg = sanitize(chunks)
75
+ this.events.push({msg, scope, date: Date.now(), level})
76
+ logger[level === 'info' ? 'log' : level](`[${scope}]`, ...msg)
77
+ return this
78
+ },
79
+ log(...chunks) { return this._log('info', ...chunks) },
80
+ warn(...chunks) { return this._log('warn', ...chunks) },
81
+ error(...chunks) { return this._log('error', ...chunks) },
82
+ save() {
83
+ this.file && fs.outputJsonSync(this.file, this)
84
+ return this
85
+ }
86
+ })
@@ -0,0 +1,26 @@
1
+ import {$} from 'zx-extra'
2
+ import {queuefy} from 'queuefy'
3
+ import {fetchRepo, pushCommit} from '../api/git.js'
4
+ import {formatReleaseNotes} from '../generators/notes.js'
5
+ import {log} from '../log.js'
6
+ import {asTuple, msgJoin} from '../../util.js'
7
+
8
+ const run = queuefy(async (pkg) => {
9
+ const {absPath: cwd, config: {changelog: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
10
+ if (!opts) return
11
+
12
+ log.info('push changelog')
13
+ const [branch = 'changelog', file = `${pkg.name.replace(/[^a-z0-9-]/ig, '')}-changelog.md`, ..._msg] = asTuple(opts, ['branch', 'file', 'msg'])
14
+ const _cwd = await fetchRepo({cwd, branch, basicAuth})
15
+ const msg = msgJoin(_msg, pkg, 'chore: update changelog ${{name}}')
16
+ const releaseNotes = await formatReleaseNotes(pkg)
17
+
18
+ await $({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
19
+ await pushCommit({cwd, branch, msg, gitCommitterEmail, gitCommitterName, basicAuth})
20
+ })
21
+
22
+ export default {
23
+ name: 'changelog',
24
+ when: (pkg) => !!pkg.config.changelog,
25
+ run,
26
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ name: 'cmd',
3
+ when: (pkg) => !!(pkg.ctx?.flags?.publishCmd ?? pkg.config.publishCmd),
4
+ run: (pkg, exec) => exec(pkg, 'publishCmd'),
5
+ snapshot: true,
6
+ }
@@ -0,0 +1,32 @@
1
+ import {queuefy} from 'queuefy'
2
+ import {path} from 'zx-extra'
3
+ import {log} from '../log.js'
4
+ import {pushCommit} from '../api/git.js'
5
+ import {asTuple, msgJoin} from '../../util.js'
6
+
7
+ const run = queuefy(async (pkg) => {
8
+ const {config: {ghPages: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
9
+ if (!opts) return
10
+
11
+ const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = asTuple(opts, ['branch', 'from', 'to', 'msg'])
12
+ const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
13
+
14
+ log.info(`publish docs to ${branch}`)
15
+
16
+ await pushCommit({
17
+ cwd: path.join(pkg.absPath, from),
18
+ from: '.',
19
+ to,
20
+ branch,
21
+ msg,
22
+ gitCommitterEmail,
23
+ gitCommitterName,
24
+ basicAuth,
25
+ })
26
+ })
27
+
28
+ export default {
29
+ name: 'gh-pages',
30
+ when: (pkg) => !!pkg.config.ghPages,
31
+ run,
32
+ }
@@ -0,0 +1,41 @@
1
+ import {log} from '../log.js'
2
+ import {getRepo, getRoot} from '../api/git.js'
3
+ import {ghCreateRelease, ghDeleteReleaseByTag, ghUploadAssets} from '../api/gh.js'
4
+ import {formatTag} from '../generators/tag.js'
5
+ import {formatReleaseNotes} from '../generators/notes.js'
6
+
7
+ const run = async (pkg) => {
8
+ const {ghBasicAuth: basicAuth, ghToken, ghAssets, ghApiUrl} = pkg.config
9
+ if (!ghToken) return null
10
+
11
+ log.info('create gh release')
12
+
13
+ const now = Date.now()
14
+ const {name, version, absPath: cwd, tag = formatTag({name, version})} = pkg
15
+ const {repoName} = await getRepo(cwd, {basicAuth})
16
+ const body = await formatReleaseNotes(pkg)
17
+ const res = await ghCreateRelease({ghApiUrl, ghToken, repoName, tag, body})
18
+
19
+ if (ghAssets?.length) {
20
+ // GH API returns a pseudo-url `...releases/110103594/assets{?name,label}` — strip the template.
21
+ const uploadUrl = res.upload_url.slice(0, res.upload_url.indexOf('{'))
22
+ await ghUploadAssets({ghToken, ghAssets, uploadUrl, cwd})
23
+ }
24
+
25
+ log.info(`duration gh release: ${Date.now() - now}`)
26
+ }
27
+
28
+ const undo = async (pkg, {tag}) => {
29
+ const {ghBasicAuth: basicAuth, ghToken, ghApiUrl} = pkg.config
30
+ if (!ghToken) return
31
+ const cwd = await getRoot(pkg.absPath)
32
+ const {repoName} = await getRepo(cwd, {basicAuth})
33
+ await ghDeleteReleaseByTag({ghApiUrl, ghToken, repoName, tag})
34
+ }
35
+
36
+ export default {
37
+ name: 'gh-release',
38
+ when: (pkg) => !!pkg.config.ghToken,
39
+ run,
40
+ undo,
41
+ }