bulk-release 2.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +540 -0
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/package.json +44 -0
- package/src/main/js/analyze.js +70 -0
- package/src/main/js/changelog.js +42 -0
- package/src/main/js/cli.js +7 -0
- package/src/main/js/config.js +52 -0
- package/src/main/js/deps.js +90 -0
- package/src/main/js/gh.js +49 -0
- package/src/main/js/git.js +101 -0
- package/src/main/js/index.js +1 -0
- package/src/main/js/log.js +63 -0
- package/src/main/js/meta.js +171 -0
- package/src/main/js/npm.js +65 -0
- package/src/main/js/processor.js +155 -0
- package/src/main/js/util.js +46 -0
- package/src/test/js/test-utils.js +63 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {$, ctx, fs, path, tempy, copy} from 'zx-extra'
|
|
2
|
+
import {log} from './log.js'
|
|
3
|
+
import {memoizeBy} from './util.js'
|
|
4
|
+
|
|
5
|
+
export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, basicAuth}) => ctx(async ($) => {
|
|
6
|
+
const origin = _origin || (await getRepo(_cwd, {basicAuth})).repoAuthedUrl
|
|
7
|
+
const cwd = tempy.temporaryDirectory()
|
|
8
|
+
$.cwd = cwd
|
|
9
|
+
try {
|
|
10
|
+
await $`git clone --single-branch --branch ${branch} --depth 1 ${origin} .`
|
|
11
|
+
} catch (e) {
|
|
12
|
+
log({level: 'warn'})(`ref '${branch}' does not exist in ${origin}`)
|
|
13
|
+
await $`git init . &&
|
|
14
|
+
git remote add origin ${origin}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return cwd
|
|
18
|
+
}), async ({cwd, branch}) => `${await getRoot(cwd)}:${branch}`)
|
|
19
|
+
|
|
20
|
+
export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = [], basicAuth, gitCommitterEmail, gitCommitterName}) => ctx(async ($) => {
|
|
21
|
+
let retries = 3
|
|
22
|
+
|
|
23
|
+
const _cwd = await fetchRepo({cwd, branch, origin, basicAuth})
|
|
24
|
+
$.cwd = _cwd
|
|
25
|
+
|
|
26
|
+
for (let {relpath, contents} of files) {
|
|
27
|
+
const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
|
|
28
|
+
await fs.outputFile(path.resolve(_cwd, to, relpath), _contents)
|
|
29
|
+
}
|
|
30
|
+
if (from) await copy({baseFrom: cwd, from, baseTo: _cwd, to, ignoreFiles, cwd})
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await $`git config user.name ${gitCommitterName} &&
|
|
34
|
+
git config user.email ${gitCommitterEmail} &&
|
|
35
|
+
git add . &&
|
|
36
|
+
git commit -m ${msg}`
|
|
37
|
+
} catch {
|
|
38
|
+
log({level: 'warn'})(`no changes to commit to ${branch}`)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while (retries > 0) {
|
|
43
|
+
try {
|
|
44
|
+
return await $.raw`git push origin HEAD:refs/heads/${branch}`
|
|
45
|
+
} catch (e) {
|
|
46
|
+
retries -= 1
|
|
47
|
+
log({level: 'error'})('git push failed', 'branch', branch, 'retries left', retries, e)
|
|
48
|
+
|
|
49
|
+
if (retries === 0) {
|
|
50
|
+
throw e
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await $`git fetch origin ${branch} &&
|
|
54
|
+
git rebase origin/${branch}`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const getSha = async (cwd) => (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
|
|
60
|
+
|
|
61
|
+
export const getRoot = memoizeBy(async (cwd) => (await $.o({cwd})`git rev-parse --show-toplevel`).toString().trim())
|
|
62
|
+
|
|
63
|
+
export const getRepo = memoizeBy(async (cwd, {basicAuth} = {}) => {
|
|
64
|
+
const originUrl = await getOrigin(cwd)
|
|
65
|
+
const [, , repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/) || []
|
|
66
|
+
const repoPublicUrl = `https://${repoHost}/${repoName}`
|
|
67
|
+
const repoAuthedUrl = basicAuth && repoHost && repoName
|
|
68
|
+
? `https://${basicAuth}@${repoHost}/${repoName}.git`
|
|
69
|
+
: originUrl
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
repoName,
|
|
73
|
+
repoHost,
|
|
74
|
+
repoPublicUrl,
|
|
75
|
+
repoAuthedUrl,
|
|
76
|
+
originUrl,
|
|
77
|
+
}
|
|
78
|
+
}, getRoot)
|
|
79
|
+
|
|
80
|
+
export const getOrigin = memoizeBy(async (cwd) =>
|
|
81
|
+
$.o({cwd})`git config --get remote.origin.url`.then(r => r.toString().trim())
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export const getCommits = async (cwd, from, to = 'HEAD') => ctx(async ($) => {
|
|
85
|
+
const ref = from ? `${from}..${to}` : to
|
|
86
|
+
|
|
87
|
+
$.cwd = cwd
|
|
88
|
+
return (await $.raw`git log ${ref} --format=+++%s__%b__%h__%H -- .`)
|
|
89
|
+
.toString()
|
|
90
|
+
.split('+++')
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(msg => {
|
|
93
|
+
const [subj, body, short, hash] = msg.split('__').map(raw => raw.trim())
|
|
94
|
+
return {subj, body, short, hash}
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const getTags = async (cwd, ref = '') =>
|
|
99
|
+
(await $.o({cwd})`git tag -l ${ref}`)
|
|
100
|
+
.toString()
|
|
101
|
+
.split('\n')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {run} from './processor.js'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {$, fs} from 'zx-extra'
|
|
2
|
+
import {get, set, tpl} from './util.js'
|
|
3
|
+
|
|
4
|
+
export const log = (ctx) =>
|
|
5
|
+
$.report
|
|
6
|
+
? $.report.log(ctx)
|
|
7
|
+
: console.log
|
|
8
|
+
|
|
9
|
+
export const createReport = ({logger = console, packages = {}, queue = [], flags} = {}) => ({
|
|
10
|
+
logger,
|
|
11
|
+
flags,
|
|
12
|
+
file: flags.report || flags.file,
|
|
13
|
+
status: 'initial',
|
|
14
|
+
events: [],
|
|
15
|
+
queue,
|
|
16
|
+
packages: Object.entries(packages).reduce((acc, [name, {manifest: {version}, absPath, relPath}]) => {
|
|
17
|
+
acc[name] = {
|
|
18
|
+
status: 'initial',
|
|
19
|
+
name,
|
|
20
|
+
version,
|
|
21
|
+
path: absPath,
|
|
22
|
+
relPath
|
|
23
|
+
}
|
|
24
|
+
return acc
|
|
25
|
+
}, {}),
|
|
26
|
+
get(key, pkgName) {
|
|
27
|
+
return get(
|
|
28
|
+
pkgName ? this.packages[pkgName] : this,
|
|
29
|
+
key
|
|
30
|
+
)
|
|
31
|
+
},
|
|
32
|
+
set(key, value, pkgName) {
|
|
33
|
+
set(
|
|
34
|
+
pkgName ? this.packages[pkgName] : this,
|
|
35
|
+
key,
|
|
36
|
+
value
|
|
37
|
+
)
|
|
38
|
+
return this
|
|
39
|
+
},
|
|
40
|
+
setStatus(status, name) {
|
|
41
|
+
this.set('status', status, name)
|
|
42
|
+
this.save()
|
|
43
|
+
return this
|
|
44
|
+
},
|
|
45
|
+
getStatus(status, name) {
|
|
46
|
+
return this.get('status', name)
|
|
47
|
+
},
|
|
48
|
+
log(ctx = {}) {
|
|
49
|
+
return function (...chunks) {
|
|
50
|
+
const {pkg, scope = pkg?.name || $.scope || '~', level = 'info'} = ctx
|
|
51
|
+
const msg = chunks.map(c => typeof c === 'string' ? tpl(c, ctx) : c)
|
|
52
|
+
const event = {msg, scope, date: Date.now(), level}
|
|
53
|
+
this.events.push(event)
|
|
54
|
+
logger[level](`[${scope}]`, ...msg)
|
|
55
|
+
|
|
56
|
+
return this
|
|
57
|
+
}.bind(this)
|
|
58
|
+
},
|
|
59
|
+
save() {
|
|
60
|
+
this.file && fs.outputJsonSync(this.file, this)
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
})
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Semantic tags processing
|
|
2
|
+
|
|
3
|
+
import {Buffer} from 'node:buffer'
|
|
4
|
+
import {queuefy} from 'queuefy'
|
|
5
|
+
import {ctx, semver, $, fs, path} from 'zx-extra'
|
|
6
|
+
import {log} from './log.js'
|
|
7
|
+
import {fetchRepo, pushCommit, getTags as getGitTags} from './git.js'
|
|
8
|
+
import {fetchManifest} from './npm.js'
|
|
9
|
+
|
|
10
|
+
export const pushTag = (pkg) => ctx(async ($) => {
|
|
11
|
+
const {absPath: cwd, name, version, config: {gitCommitterEmail, gitCommitterName}} = pkg
|
|
12
|
+
const tag = formatTag({name, version})
|
|
13
|
+
|
|
14
|
+
pkg.context.git.tag = tag
|
|
15
|
+
log({pkg})(`push release tag ${tag}`)
|
|
16
|
+
|
|
17
|
+
$.cwd = cwd
|
|
18
|
+
await $`git config user.name ${gitCommitterName}`
|
|
19
|
+
await $`git config user.email ${gitCommitterEmail}`
|
|
20
|
+
await $`git tag -m ${tag} ${tag}`
|
|
21
|
+
await $`git push origin ${tag}`
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const pushMeta = queuefy(async (pkg) => {
|
|
25
|
+
log({pkg})('push artifact to branch \'meta\'')
|
|
26
|
+
|
|
27
|
+
const {name, version, absPath: cwd, config: {gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
|
|
28
|
+
const tag = formatTag({name, version})
|
|
29
|
+
const to = '.'
|
|
30
|
+
const branch = 'meta'
|
|
31
|
+
const msg = `chore: release meta ${name} ${version}`
|
|
32
|
+
const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
|
|
33
|
+
const meta = {
|
|
34
|
+
META_VERSION: '1',
|
|
35
|
+
name: pkg.name,
|
|
36
|
+
hash,
|
|
37
|
+
version: pkg.version,
|
|
38
|
+
dependencies: pkg.dependencies,
|
|
39
|
+
devDependencies: pkg.devDependencies,
|
|
40
|
+
peerDependencies: pkg.peerDependencies,
|
|
41
|
+
optionalDependencies: pkg.optionalDependencies,
|
|
42
|
+
}
|
|
43
|
+
const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
|
|
44
|
+
|
|
45
|
+
await pushCommit({cwd, to, branch, msg, files, gitCommitterEmail, gitCommitterName, basicAuth})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const getLatest = async (pkg) => {
|
|
49
|
+
const {absPath: cwd, name, config: {ghBasicAuth: basicAuth}} = pkg
|
|
50
|
+
const tag = await getLatestTag(cwd, name)
|
|
51
|
+
const meta = await getLatestMeta(cwd, tag?.ref, basicAuth) || await fetchManifest(pkg, {nothrow: true})
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
tag,
|
|
55
|
+
meta
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const f0 = {
|
|
60
|
+
parse(tag) {
|
|
61
|
+
if (!tag.endsWith('-f0')) return null
|
|
62
|
+
|
|
63
|
+
const pattern = /^(\d{4}\.(?:[1-9]|1[012])\.(?:[1-9]|[12]\d|30|31))-((?:[a-z0-9-]+\.)?[a-z0-9-]+)\.(v?\d+\.\d+\.\d+.*)-f0$/
|
|
64
|
+
const matched = pattern.exec(tag) || []
|
|
65
|
+
const [, _date, _name, version] = matched
|
|
66
|
+
|
|
67
|
+
if (!semver.valid(version)) return null
|
|
68
|
+
|
|
69
|
+
const date = parseDateTag(_date)
|
|
70
|
+
const name = _name.includes('.') ? `@${_name.replace('.', '/')}` : _name
|
|
71
|
+
|
|
72
|
+
return {date, name, version, format: 'f0', ref: tag}
|
|
73
|
+
},
|
|
74
|
+
format({name, date = new Date(), version}) {
|
|
75
|
+
if (!/^(@?[a-z0-9-]+\/)?[a-z0-9-]+$/.test(name) || !semver.valid(version)) return null
|
|
76
|
+
|
|
77
|
+
const d = formatDateTag(date)
|
|
78
|
+
const n = name.replace('@', '').replace('/', '.')
|
|
79
|
+
|
|
80
|
+
return `${d}-${n}.${version}-f0`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const f1 = {
|
|
85
|
+
parse(tag) {
|
|
86
|
+
if (!tag.endsWith('-f1')) return null
|
|
87
|
+
|
|
88
|
+
const pattern = /^(\d{4}\.(?:[1-9]|1[012])\.(?:[1-9]|[12]\d|30|31))-[a-z0-9-]+\.(v?\d+\.\d+\.\d+.*)\.([^.]+)-f1$/
|
|
89
|
+
const matched = pattern.exec(tag) || []
|
|
90
|
+
const [, _date, version, b64] = matched
|
|
91
|
+
|
|
92
|
+
if (!semver.valid(version)) return null
|
|
93
|
+
|
|
94
|
+
const date = parseDateTag(_date)
|
|
95
|
+
const name = Buffer.from(b64, 'base64url').toString('utf8')
|
|
96
|
+
|
|
97
|
+
return {date, name, version, format: 'f1', ref: tag}
|
|
98
|
+
},
|
|
99
|
+
format({name, date = new Date(), version}) {
|
|
100
|
+
if (!semver.valid(version)) return null
|
|
101
|
+
|
|
102
|
+
const b64 = Buffer.from(name).toString('base64url')
|
|
103
|
+
const d = formatDateTag(date)
|
|
104
|
+
const n = name.replace(/[^a-z0-9-]/ig, '')
|
|
105
|
+
|
|
106
|
+
return `${d}-${n}.${version}.${b64}-f1`
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lerna = {
|
|
111
|
+
parse(tag) {
|
|
112
|
+
const pattern = /^(@?[a-z0-9-]+(?:\/[a-z0-9-]+)?)@(v?\d+\.\d+\.\d+.*)/
|
|
113
|
+
const [, name, version] = pattern.exec(tag) || []
|
|
114
|
+
|
|
115
|
+
if (!semver.valid(version)) return null
|
|
116
|
+
|
|
117
|
+
return {name, version, format: 'lerna', ref: tag}
|
|
118
|
+
},
|
|
119
|
+
// format({name, version}) {
|
|
120
|
+
// if (!semver.valid(version)) return null
|
|
121
|
+
//
|
|
122
|
+
// return `${name}@${version}`
|
|
123
|
+
// }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// TODO
|
|
127
|
+
// const variants = [f0, f1]
|
|
128
|
+
// export const parseTag = (tag) => {
|
|
129
|
+
// for (const variant of variants) {
|
|
130
|
+
// const parsed = variant.parse(tag)
|
|
131
|
+
// if (parsed) return parsed
|
|
132
|
+
// }
|
|
133
|
+
//
|
|
134
|
+
// return null
|
|
135
|
+
// }
|
|
136
|
+
|
|
137
|
+
export const parseTag = (tag) => f0.parse(tag) || f1.parse(tag) || lerna.parse(tag) || null
|
|
138
|
+
|
|
139
|
+
export const formatTag = (tag) => f0.format(tag) || f1.format(tag) || null
|
|
140
|
+
|
|
141
|
+
export const getTags = async (cwd, ref = '') =>
|
|
142
|
+
(await getGitTags(cwd, ref))
|
|
143
|
+
.map(tag => parseTag(tag.trim()))
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.sort((a, b) => semver.rcompare(a.version, b.version))
|
|
146
|
+
|
|
147
|
+
export const getLatestTag = async (cwd, name) =>
|
|
148
|
+
(await getTags(cwd)).find(tag => tag.name === name) || null
|
|
149
|
+
|
|
150
|
+
export const getLatestTaggedVersion = async (cwd, name) =>
|
|
151
|
+
(await getLatestTag(cwd, name))?.version || null
|
|
152
|
+
|
|
153
|
+
export const formatDateTag = (date = new Date()) => `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}`
|
|
154
|
+
|
|
155
|
+
export const parseDateTag = (date) => new Date(date.replaceAll('.', '-')+'Z')
|
|
156
|
+
|
|
157
|
+
export const getArtifactPath = (tag) => tag.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
158
|
+
|
|
159
|
+
export const getLatestMeta = async (cwd, tag, basicAuth) => {
|
|
160
|
+
if (!tag) return null
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const _cwd = await fetchRepo({cwd, branch: 'meta', basicAuth})
|
|
164
|
+
return await Promise.any([
|
|
165
|
+
fs.readJson(path.resolve(_cwd, `${getArtifactPath(tag)}.json`)),
|
|
166
|
+
fs.readJson(path.resolve(_cwd, getArtifactPath(tag), 'meta.json'))
|
|
167
|
+
])
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {log} from './log.js'
|
|
2
|
+
import {$, ctx, fs, path, INI, fetch} from 'zx-extra'
|
|
3
|
+
|
|
4
|
+
export const fetchPkg = async (pkg) => {
|
|
5
|
+
const id = `${pkg.name}@${pkg.version}`
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
log({pkg})(`fetching '${id}'`)
|
|
9
|
+
const cwd = pkg.absPath
|
|
10
|
+
const {npmRegistry, npmToken, npmConfig} = pkg.config
|
|
11
|
+
const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
|
|
12
|
+
const tarball = getTarballUrl(npmRegistry, pkg.name, pkg.version)
|
|
13
|
+
await $.raw`wget --timeout=10 --header='Authorization: ${bearerToken}' -qO- ${tarball} | tar -xvz --strip-components=1 --exclude='package.json' -C ${cwd}`
|
|
14
|
+
|
|
15
|
+
pkg.fetched = true
|
|
16
|
+
} catch (e) {
|
|
17
|
+
log({pkg, level: 'warn'})(`fetching '${id}' failed`, e)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const fetchManifest = async (pkg, {nothrow} = {}) => {
|
|
22
|
+
const {npmRegistry, npmToken, npmConfig} = pkg.config
|
|
23
|
+
const bearerToken = getBearerToken(npmRegistry, npmToken, npmConfig)
|
|
24
|
+
const url = getManifestUrl(npmRegistry, pkg.name, pkg.version)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url, {authorization: bearerToken})
|
|
28
|
+
if (!res.ok) throw res
|
|
29
|
+
|
|
30
|
+
return res.json() // NOTE .json() is async too
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (nothrow) return null
|
|
33
|
+
throw e
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const npmPublish = (pkg) => ctx(async ($) => {
|
|
38
|
+
const {absPath: cwd, name, version, manifest, config} = pkg
|
|
39
|
+
if (manifest.private || config?.npmPublish === false) return
|
|
40
|
+
const {npmRegistry, npmToken, npmConfig} = config
|
|
41
|
+
const npmrc = npmConfig ? npmConfig : path.resolve(cwd, '.npmrc')
|
|
42
|
+
|
|
43
|
+
log({pkg})(`publish npm package ${name} ${version} to ${npmRegistry}`)
|
|
44
|
+
$.cwd = cwd
|
|
45
|
+
if (!npmConfig) {
|
|
46
|
+
await $.raw`echo ${npmRegistry.replace(/https?:/, '')}/:_authToken=${npmToken} >> ${npmrc}`
|
|
47
|
+
}
|
|
48
|
+
await $`npm publish --no-git-tag-version --registry=${npmRegistry} --userconfig ${npmrc} --no-workspaces`
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// $`npm view ${name}@${version} dist.tarball`
|
|
52
|
+
export const getTarballUrl = (registry, name, version) => `${registry}/${name}/-/${name.replace(/^.+(%2f|\/)/,'')}-${version}.tgz`
|
|
53
|
+
|
|
54
|
+
export const getManifestUrl = (registry, name, version) => `${registry}/${name}/${version}`
|
|
55
|
+
|
|
56
|
+
export const getBearerToken = async (npmRegistry, npmToken, npmConfig) => {
|
|
57
|
+
const token = npmConfig
|
|
58
|
+
? getAuthToken(npmRegistry, INI.parse(await fs.readFile(npmConfig, 'utf8')))
|
|
59
|
+
: npmToken
|
|
60
|
+
return `Bearer ${token}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// NOTE registry-auth-token does not work with localhost:4873
|
|
64
|
+
export const getAuthToken = (registry, npmrc) =>
|
|
65
|
+
(Object.entries(npmrc).find(([reg]) => reg.startsWith(registry.replace(/^https?/, ''))) || [])[1]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import {$, fs, within} from 'zx-extra'
|
|
3
|
+
import {queuefy} from 'queuefy'
|
|
4
|
+
import {analyze} from './analyze.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'
|
|
13
|
+
import {memoizeBy, tpl} from './util.js'
|
|
14
|
+
|
|
15
|
+
export const run = async ({cwd = process.cwd(), env, flags = {}} = {}) => within(async () => {
|
|
16
|
+
const context = await createContext({flags, env, cwd})
|
|
17
|
+
const {report, packages, queue, prev, graphs} = context
|
|
18
|
+
const _runCmd = queuefy(runCmd, flags.concurrency || os.cpus().length)
|
|
19
|
+
|
|
20
|
+
report
|
|
21
|
+
.log()('zx-bulk-release')
|
|
22
|
+
.log()('queue:', queue)
|
|
23
|
+
.log()('graphs', graphs)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await traverseQueue({queue, prev, async cb(name) {
|
|
27
|
+
report.setStatus('analyzing', name)
|
|
28
|
+
const pkg = packages[name]
|
|
29
|
+
await contextify(pkg, context)
|
|
30
|
+
await analyze(pkg)
|
|
31
|
+
report
|
|
32
|
+
.set('config', pkg.config, name)
|
|
33
|
+
.set('version', pkg.version, name)
|
|
34
|
+
.set('prevVersion', pkg.latest.tag?.version || pkg.manifest.version, name)
|
|
35
|
+
.set('releaseType', pkg.releaseType, name)
|
|
36
|
+
.set('tag', pkg.tag, name)
|
|
37
|
+
}})
|
|
38
|
+
|
|
39
|
+
report.setStatus('pending')
|
|
40
|
+
|
|
41
|
+
await traverseQueue({queue, prev, async cb(name) {
|
|
42
|
+
const pkg = packages[name]
|
|
43
|
+
|
|
44
|
+
if (!pkg.releaseType) {
|
|
45
|
+
report.setStatus('skipped', name)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (!flags.noBuild) {
|
|
49
|
+
report.setStatus('building', name)
|
|
50
|
+
await build(pkg, _runCmd)
|
|
51
|
+
}
|
|
52
|
+
if (!flags.dryRun && !flags.noPublish) {
|
|
53
|
+
report.setStatus('publishing', name)
|
|
54
|
+
await publish(pkg, _runCmd)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
report.setStatus('success', name)
|
|
58
|
+
}})
|
|
59
|
+
} catch (e) {
|
|
60
|
+
report
|
|
61
|
+
.log({level: 'error'})(e, e.stack)
|
|
62
|
+
.set('error', e)
|
|
63
|
+
.setStatus('failure')
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
report
|
|
67
|
+
.setStatus('success')
|
|
68
|
+
.log()('Great success!')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const runCmd = async (pkg, name) => {
|
|
72
|
+
const cmd = tpl(pkg.config[name], {...pkg, ...pkg.context})
|
|
73
|
+
|
|
74
|
+
if (cmd) {
|
|
75
|
+
log({pkg})(`run ${name} '${cmd}'`)
|
|
76
|
+
return $.o({cwd: pkg.absPath, quote: v => v, preferLocal: true})`${cmd}`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createContext = async ({flags, env, cwd}) => {
|
|
81
|
+
const { packages, queue, root, prev, graphs } = await topo({cwd, flags})
|
|
82
|
+
const report = createReport({packages, queue, flags})
|
|
83
|
+
|
|
84
|
+
$.report = report
|
|
85
|
+
$.env = {...process.env, ...env}
|
|
86
|
+
$.verbose = !!(flags.debug || $.env.DEBUG ) || $.verbose
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
report,
|
|
90
|
+
packages,
|
|
91
|
+
root,
|
|
92
|
+
queue,
|
|
93
|
+
prev,
|
|
94
|
+
graphs,
|
|
95
|
+
flags
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Inspired by https://docs.github.com/en/actions/learn-github-actions/contexts
|
|
100
|
+
const contextify = async (pkg, {packages, root}) => {
|
|
101
|
+
pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
|
|
102
|
+
pkg.latest = await getLatest(pkg)
|
|
103
|
+
pkg.context = {
|
|
104
|
+
git: {
|
|
105
|
+
sha: await getSha(pkg.absPath),
|
|
106
|
+
root: await getRoot(pkg.absPath)
|
|
107
|
+
},
|
|
108
|
+
env: $.env,
|
|
109
|
+
packages
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const build = memoizeBy(async (pkg, run = runCmd, flags = {}, self = build) => within(async () => {
|
|
114
|
+
$.scope = pkg.name
|
|
115
|
+
|
|
116
|
+
await Promise.all([
|
|
117
|
+
traverseDeps(pkg, pkg.context.packages, async (_, {pkg}) => self(pkg, run, flags, self)),
|
|
118
|
+
pkg.changes.length === 0 && pkg.config.npmFetch && !flags.noNpmFetch
|
|
119
|
+
? fetchPkg(pkg)
|
|
120
|
+
: Promise.resolve()
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
if (!pkg.fetched) {
|
|
124
|
+
await run(pkg, 'buildCmd')
|
|
125
|
+
await run(pkg, 'testCmd')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pkg.built = true
|
|
129
|
+
}))
|
|
130
|
+
|
|
131
|
+
const publish = memoizeBy(async (pkg, run = runCmd) => within(async () => {
|
|
132
|
+
$.scope = pkg.name
|
|
133
|
+
|
|
134
|
+
// Debug
|
|
135
|
+
// https://www.npmjs.com/package/@packasso/preset-ts-tsc-uvu/v/0.0.0?activeTab=code
|
|
136
|
+
// https://github.com/qiwi/packasso/actions/runs/4514909191/jobs/7951564982#step:7:817
|
|
137
|
+
// https://github.com/qiwi/packasso/blob/meta/2023-3-24-packasso-preset-ts-tsc-uvu-0-21-0-f0.json
|
|
138
|
+
if (pkg.version !== pkg.manifest.version) {
|
|
139
|
+
throw new Error('package.json version not synced')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs.writeJsonSync(pkg.manifestPath, pkg.manifest, {spaces: 2})
|
|
143
|
+
await pushTag(pkg)
|
|
144
|
+
|
|
145
|
+
await Promise.all([
|
|
146
|
+
pushMeta(pkg),
|
|
147
|
+
pushChangelog(pkg),
|
|
148
|
+
npmPublish(pkg),
|
|
149
|
+
ghRelease(pkg),
|
|
150
|
+
ghPages(pkg),
|
|
151
|
+
run(pkg, 'publishCmd')
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
pkg.published = true
|
|
155
|
+
}))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const tpl = (str, context) =>
|
|
2
|
+
str?.replace(/\$\{\{\s*([.a-z0-9]+)\s*}}/gi, (matched, key) => get(context, key) ?? '')
|
|
3
|
+
|
|
4
|
+
export const get = (obj, path = '.') => {
|
|
5
|
+
const chunks = path.split('.').filter(Boolean)
|
|
6
|
+
let result = obj
|
|
7
|
+
|
|
8
|
+
for (let i = 0, len = chunks.length; i < len && result !== undefined && result !== null; i++) {
|
|
9
|
+
result = result[chunks[i]]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return result
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const set = (obj, path, value) => {
|
|
16
|
+
const chunks = path.split('.').filter(Boolean)
|
|
17
|
+
let result = obj
|
|
18
|
+
|
|
19
|
+
for (let i = 0, len = chunks.length; i < len && result !== undefined && result !== null; i++) {
|
|
20
|
+
if (i === len - 1) {
|
|
21
|
+
result[chunks[i]] = value
|
|
22
|
+
} else {
|
|
23
|
+
result[chunks[i]] = result[chunks[i]] || {}
|
|
24
|
+
result = result[chunks[i]]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const msgJoin = (rest, context, def) => tpl(rest.filter(Boolean).join(' ') || def, context)
|
|
32
|
+
|
|
33
|
+
export const keyByValue = (obj, value) => Object.keys(obj).find((key) => obj[key] === value)
|
|
34
|
+
|
|
35
|
+
export const memoizeBy = (fn, getKey = v => v, memo = new Map()) => async (...args) => {
|
|
36
|
+
const key = await getKey(...args)
|
|
37
|
+
if (memo.has(key)) {
|
|
38
|
+
return memo.get(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value = fn(...args)
|
|
42
|
+
memo.set(key, value)
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const camelize = s => s.replace(/-./g, x => x[1].toUpperCase())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {ctx, fs, path, tempy, $, sleep} from 'zx-extra'
|
|
2
|
+
import {fileURLToPath} from 'node:url'
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
export const fixtures = path.resolve(__dirname, '../fixtures')
|
|
6
|
+
|
|
7
|
+
export const createNpmRegistry = () => {
|
|
8
|
+
let p
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
address: $.env.NPM_REGISTRY,
|
|
12
|
+
async start() {
|
|
13
|
+
fs.removeSync(path.resolve(__dirname, '../../../storage'))
|
|
14
|
+
const config = path.resolve(__dirname, '../../../verdaccio.config.yaml')
|
|
15
|
+
ctx(($) => {
|
|
16
|
+
$.preferLocal = true
|
|
17
|
+
p = $`verdaccio --config ${config}`
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return sleep(1000)
|
|
21
|
+
},
|
|
22
|
+
async stop() {
|
|
23
|
+
p._nothrow = true
|
|
24
|
+
return p?.kill()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const createFakeRepo = async ({cwd = tempy.temporaryDirectory(), commits = []} = {}) =>
|
|
30
|
+
ctx(async ($) => {
|
|
31
|
+
$.cwd = cwd
|
|
32
|
+
await $`git init`
|
|
33
|
+
|
|
34
|
+
await addCommits({cwd, commits})
|
|
35
|
+
|
|
36
|
+
const bare = tempy.temporaryDirectory()
|
|
37
|
+
await $`git init --bare ${bare}`
|
|
38
|
+
await $`git remote add origin ${bare}`
|
|
39
|
+
|
|
40
|
+
return cwd
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const addCommits = async ({cwd, commits = []}) => ctx(async ($) => {
|
|
44
|
+
$.cwd = cwd
|
|
45
|
+
|
|
46
|
+
for (let {msg, files, name = 'Semrel-extra Bot', email = 'semrel-extra-bot@hotmail.com', tags = []} of commits) {
|
|
47
|
+
await $`git config user.name ${name}`
|
|
48
|
+
await $`git config user.email ${email}`
|
|
49
|
+
for (let {relpath, contents} of files) {
|
|
50
|
+
const _contents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2)
|
|
51
|
+
const file = path.resolve(cwd, relpath)
|
|
52
|
+
await fs.outputFile(file, _contents)
|
|
53
|
+
|
|
54
|
+
await $`git add ${file}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await $`git commit -m ${msg}`
|
|
58
|
+
|
|
59
|
+
for (let tag of tags) {
|
|
60
|
+
await $`git tag ${tag} -m ${tag}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|