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.
- package/CHANGELOG.md +25 -0
- package/README.md +91 -74
- package/package.json +14 -7
- package/src/main/js/index.js +0 -6
- package/src/main/js/processor/api/gh.js +111 -0
- package/src/main/js/{api → processor/api}/git.js +17 -26
- package/src/main/js/{api → processor/api}/npm.js +70 -28
- package/src/main/js/processor/deps.js +1 -1
- package/src/main/js/processor/exec.js +4 -4
- package/src/main/js/processor/generators/meta.js +80 -0
- package/src/main/js/{api/changelog.js → processor/generators/notes.js} +3 -21
- package/src/main/js/processor/{meta.js → generators/tag.js} +3 -109
- package/src/main/js/processor/log.js +86 -0
- package/src/main/js/processor/publishers/changelog.js +26 -0
- package/src/main/js/processor/publishers/cmd.js +6 -0
- package/src/main/js/processor/publishers/gh-pages.js +32 -0
- package/src/main/js/processor/publishers/gh-release.js +41 -0
- package/src/main/js/processor/publishers/meta.js +58 -0
- package/src/main/js/processor/publishers/npm.js +15 -0
- package/src/main/js/processor/release.js +71 -66
- package/src/main/js/{steps → processor/steps}/analyze.js +18 -24
- package/src/main/js/processor/steps/build.js +20 -0
- package/src/main/js/processor/steps/clean.js +7 -0
- package/src/main/js/processor/steps/contextify.js +49 -0
- package/src/main/js/processor/steps/publish.js +39 -0
- package/src/main/js/processor/steps/teardown.js +58 -0
- package/src/main/js/processor/steps/test.js +10 -0
- package/src/main/js/util.js +32 -77
- package/src/test/js/utils/gh-server.js +33 -0
- package/src/test/js/utils/mock.js +132 -0
- package/src/test/js/{test-utils.js → utils/repo.js} +3 -3
- package/src/main/js/api/gh.js +0 -131
- package/src/main/js/log.js +0 -63
- package/src/main/js/steps/build.js +0 -23
- package/src/main/js/steps/clean.js +0 -7
- package/src/main/js/steps/contextify.js +0 -154
- package/src/main/js/steps/publish.js +0 -47
- 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 {
|
|
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(
|
|
26
|
+
log.info(`fetching '${id}' from ${npmRegistry}`)
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
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:
|
|
26
|
-
})
|
|
27
|
-
clearTimeout(
|
|
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(
|
|
43
|
+
log.info(`fetch duration '${id}': ${Date.now() - now}`)
|
|
36
44
|
pkg.fetched = true
|
|
37
45
|
} catch (e) {
|
|
38
|
-
log
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
90
|
-
|
|
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(
|
|
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 =
|
|
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 {
|
|
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 '
|
|
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.
|
|
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(
|
|
10
|
+
log.info(`run ${name} '${cmd}'`)
|
|
11
11
|
const result = await $({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
|
|
12
12
|
|
|
13
|
-
log(
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
//
|
|
1
|
+
// Pure tag parsing/formatting. No IO, no side effects.
|
|
2
2
|
|
|
3
3
|
import {Buffer} from 'node:buffer'
|
|
4
|
-
import {
|
|
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,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
|
+
}
|