bulk-release 2.19.1 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +72 -77
  3. package/package.json +3 -3
  4. package/src/main/js/config.js +12 -3
  5. package/src/main/js/index.js +0 -6
  6. package/src/main/js/processor/api/gh.js +111 -0
  7. package/src/main/js/{api → processor/api}/git.js +17 -26
  8. package/src/main/js/{api → processor/api}/npm.js +70 -28
  9. package/src/main/js/processor/deps.js +1 -1
  10. package/src/main/js/processor/exec.js +4 -4
  11. package/src/main/js/processor/generators/meta.js +80 -0
  12. package/src/main/js/processor/generators/notes.js +37 -0
  13. package/src/main/js/processor/{meta.js → generators/tag.js} +3 -109
  14. package/src/main/js/processor/log.js +86 -0
  15. package/src/main/js/processor/publishers/changelog.js +26 -0
  16. package/src/main/js/processor/publishers/cmd.js +6 -0
  17. package/src/main/js/processor/publishers/gh-pages.js +32 -0
  18. package/src/main/js/processor/publishers/gh-release.js +41 -0
  19. package/src/main/js/processor/publishers/meta.js +58 -0
  20. package/src/main/js/processor/publishers/npm.js +15 -0
  21. package/src/main/js/processor/release.js +71 -66
  22. package/src/main/js/{steps → processor/steps}/analyze.js +18 -24
  23. package/src/main/js/processor/steps/build.js +20 -0
  24. package/src/main/js/processor/steps/clean.js +7 -0
  25. package/src/main/js/processor/steps/contextify.js +49 -0
  26. package/src/main/js/processor/steps/publish.js +39 -0
  27. package/src/main/js/processor/steps/teardown.js +58 -0
  28. package/src/main/js/processor/steps/test.js +10 -0
  29. package/src/main/js/util.js +32 -77
  30. package/src/main/js/api/changelog.js +0 -42
  31. package/src/main/js/api/gh.js +0 -131
  32. package/src/main/js/log.js +0 -63
  33. package/src/main/js/steps/build.js +0 -23
  34. package/src/main/js/steps/clean.js +0 -7
  35. package/src/main/js/steps/contextify.js +0 -154
  36. package/src/main/js/steps/publish.js +0 -47
  37. package/src/main/js/steps/test.js +0 -16
@@ -1,131 +0,0 @@
1
- import {queuefy} from 'queuefy'
2
- import {$, path, tempy, glob, fs, fetch} from 'zx-extra'
3
- import {log} from '../log.js'
4
- import {getRepo, pushCommit} from './git.js'
5
- import {formatTag} from '../processor/meta.js'
6
- import {formatReleaseNotes} from './changelog.js'
7
- import {asArray, getCommonPath, msgJoin} from '../util.js'
8
-
9
- // https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
10
- export const ghRelease = async (pkg) => {
11
- const {ghBasicAuth: basicAuth, ghToken, ghAssets} = pkg.config
12
- if (!ghToken) return null
13
-
14
- log({pkg})('create gh release')
15
-
16
- const now = Date.now()
17
- const {name, version, absPath: cwd, tag = formatTag({name, version})} = pkg
18
- const {repoName} = await getRepo(cwd, {basicAuth})
19
- const releaseNotes = await formatReleaseNotes(pkg)
20
- const releaseData = JSON.stringify({
21
- name: tag,
22
- tag_name: tag,
23
- body: releaseNotes
24
- })
25
-
26
- const res = await (await fetch(`https://api.github.com/repos/${repoName}/releases`, {
27
- method: 'POST',
28
- headers: {
29
- Accept: 'application/vnd.github.v3+json',
30
- Authorization: `token ${ghToken}`,
31
- 'X-GitHub-Api-Version': '2022-11-28'
32
- },
33
- body: releaseData
34
- })).json()
35
-
36
- if (!res.upload_url) {
37
- throw new Error(`gh release failed: ${JSON.stringify(res)}`)
38
- }
39
-
40
- if (ghAssets?.length) {
41
- // Lol. GH API literally returns pseudourl `...releases/110103594/assets{?name,label}` as shown in the docs
42
- const uploadUrl = res.upload_url.slice(0, res.upload_url.indexOf('{'))
43
- await ghUploadAssets({ghToken, ghAssets, uploadUrl, cwd})
44
- }
45
-
46
- log({pkg})(`duration gh release: ${Date.now() - now}`)
47
- }
48
-
49
- export const ghPages = queuefy(async (pkg) => {
50
- const {config: {ghPages: opts, gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
51
- if (!opts) return
52
-
53
- const [branch = 'gh-pages', from = 'docs', to = '.', ..._msg] = typeof opts === 'string'
54
- ? opts.split(' ')
55
- : [opts.branch, opts.from, opts.to, opts.msg]
56
- const msg = msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}')
57
-
58
- log({pkg})(`publish docs to ${branch}`)
59
-
60
- await pushCommit({
61
- cwd: path.join(pkg.absPath, from),
62
- from: '.',
63
- to,
64
- branch,
65
- msg,
66
- gitCommitterEmail,
67
- gitCommitterName,
68
- basicAuth
69
- })
70
- })
71
-
72
- // https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset8
73
- export const ghPrepareAssets = async (assets, _cwd) => {
74
- const temp = tempy.temporaryDirectory()
75
-
76
- await Promise.all(assets.map(async ({name, contents, source = 'target/**/*', zip, cwd = _cwd, strip = true}) => {
77
- const target = path.join(temp, name)
78
-
79
- if (contents) {
80
- await fs.outputFile(target, contents, 'utf8')
81
- return
82
- }
83
-
84
- const patterns = asArray(source)
85
- if (patterns.some(s => s.includes('*'))) {
86
- zip = true
87
- }
88
- const files = await glob(patterns, {cwd, absolute: false, onlyFiles: true})
89
-
90
- if (files.length === 0) {
91
- throw new Error(`gh asset not found: ${name} ${source}`)
92
- }
93
-
94
- if (!zip && files.length === 1) {
95
- await fs.copy(path.join(cwd, files[0]), target)
96
- return
97
- }
98
- const prefix = getCommonPath(files)
99
-
100
- return $.raw`tar -C ${path.join(cwd, prefix)} -cv${zip ? 'z' : ''}f ${target} ${files.map(f => f.slice(prefix.length)).join(' ')}`
101
- }))
102
-
103
- return temp
104
- }
105
-
106
- export const ghUploadAssets = async ({ghToken, ghAssets, uploadUrl, cwd}) => {
107
- const temp = await ghPrepareAssets(ghAssets, cwd)
108
-
109
- return Promise.all(ghAssets.map(async ({name}) => {
110
- const url = `${uploadUrl}?name=${name}`
111
- // return $.o({cwd: temp})`curl -H 'Authorization: token ${ghToken}' -H 'Accept: application/vnd.github.v3+json' -H 'Content-Type: application/octet-stream' ${url} --data-binary '@${name}'`
112
- return fetch(url, {
113
- method: 'POST',
114
- headers: {
115
- 'Content-Type': 'application/octet-stream',
116
- Accept: 'application/vnd.github.v3+json',
117
- Authorization: `token ${ghToken}`,
118
- 'X-GitHub-Api-Version': '2022-11-28'
119
- },
120
- body: await fs.readFile(path.join(temp, name))
121
- })
122
- }))
123
- }
124
-
125
- export const ghGetAsset = async ({repoName, tag, name}) => {
126
- return (await fetch(`https://github.com/${repoName}/releases/download/${tag.ref || tag}/${name}`, {
127
- headers: {
128
- // Accept: 'application/vnd.github.v3+json'
129
- }
130
- })).text()
131
- }
@@ -1,63 +0,0 @@
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
- })
@@ -1,23 +0,0 @@
1
- import {memoizeBy} from '../util.js'
2
- import {$, within} from 'zx-extra'
3
- import {fetchPkg} from '../api/npm.js'
4
- import {traverseDeps} from '../processor/deps.js'
5
- import {exec} from '../processor/exec.js'
6
-
7
- export const build = memoizeBy(async (pkg, run = exec, flags = {}, self = build) => within(async () => {
8
- $.scope = pkg.name
9
-
10
- await Promise.all([
11
- traverseDeps({pkg, packages: pkg.context.packages, cb: async({pkg}) => self(pkg, run, flags, self)}),
12
- pkg.manifest.private !== true && pkg.changes.length === 0 && pkg.config.npmFetch && flags.npmFetch !== false
13
- ? fetchPkg(pkg)
14
- : Promise.resolve()
15
- ])
16
-
17
- if (!pkg.fetched) {
18
- await run(pkg, 'buildCmd')
19
- // await run(pkg, 'testCmd')
20
- }
21
-
22
- pkg.built = true
23
- }))
@@ -1,7 +0,0 @@
1
- import {unsetUserConfig} from '../api/git.js'
2
- import {npmRestore} from '../api/npm.js'
3
-
4
- export const clean = async (cwd, packages) => {
5
- await unsetUserConfig(cwd)
6
- await Promise.all(Object.values(packages).filter(pkg => !pkg.skipped).map(npmRestore))
7
- }
@@ -1,154 +0,0 @@
1
- import {getPkgConfig} from '../config.js'
2
- import {getLatest, getArtifactPath} from '../processor/meta.js'
3
- import {getRoot, getSha, getRepo, deleteRemoteTag, fetchRepo, pushCommit} from '../api/git.js'
4
- import {fetchManifest} from '../api/npm.js'
5
- import {log} from '../log.js'
6
- import {$, fs, path, fetch} from 'zx-extra'
7
-
8
- // Inspired by https://docs.github.com/en/actions/learn-github-actions/contexts
9
- export const contextify = async (pkg, {packages, root, flags, env}) => {
10
- pkg.config = await getPkgConfig([pkg.absPath, root.absPath], env)
11
- pkg.latest = await getLatest(pkg)
12
- pkg.context = {
13
- git: {
14
- sha: await getSha(pkg.absPath),
15
- root: await getRoot(pkg.absPath)
16
- },
17
- env: $.env,
18
- flags,
19
- packages
20
- }
21
- }
22
-
23
- // Rollback a release that failed mid-publish (called inline from publish.js).
24
- // Unlike `recover`, this uses the current release tag (pkg.context.git.tag)
25
- // and skips the npm existence check — we already know the release failed.
26
- export const rollbackRelease = async (pkg) => {
27
- const tag = pkg.context.git.tag
28
- if (!tag) return
29
-
30
- const cwd = pkg.context.git.root
31
- const {ghBasicAuth: basicAuth, ghToken, gitCommitterName, gitCommitterEmail} = pkg.config
32
- if (!basicAuth) throw new Error('rollback requires git credentials (GH_TOKEN)')
33
- const {repoName} = await getRepo(cwd, {basicAuth})
34
-
35
- log({pkg})(`rollback: cleaning up failed release for tag '${tag}'`)
36
-
37
- // 1. Delete GitHub release
38
- if (ghToken) {
39
- try {
40
- const res = await fetch(`https://api.github.com/repos/${repoName}/releases/tags/${tag}`, {
41
- headers: {Authorization: `token ${ghToken}`, 'X-GitHub-Api-Version': '2022-11-28'}
42
- })
43
- if (res.ok) {
44
- const {id} = await res.json()
45
- await fetch(`https://api.github.com/repos/${repoName}/releases/${id}`, {
46
- method: 'DELETE',
47
- headers: {Authorization: `token ${ghToken}`, 'X-GitHub-Api-Version': '2022-11-28'}
48
- })
49
- log({pkg})(`rollback: deleted gh release for '${tag}'`)
50
- }
51
- } catch (e) {
52
- log({pkg, level: 'warn'})('rollback: failed to delete gh release', e)
53
- }
54
- }
55
-
56
- // 2. Remove meta entry from meta branch
57
- try {
58
- const metaBranch = 'meta'
59
- const metaCwd = await fetchRepo({cwd, branch: metaBranch, basicAuth})
60
- const artifactPath = getArtifactPath(tag)
61
- const candidates = [
62
- path.resolve(metaCwd, `${artifactPath}.json`),
63
- path.resolve(metaCwd, artifactPath)
64
- ]
65
- let removed = false
66
- for (const p of candidates) {
67
- if (fs.existsSync(p)) {
68
- await fs.remove(p)
69
- removed = true
70
- }
71
- }
72
- if (removed) {
73
- await pushCommit({cwd, branch: metaBranch, msg: `chore: rollback ${pkg.name} ${pkg.version}`, gitCommitterName, gitCommitterEmail, basicAuth})
74
- log({pkg})(`rollback: removed meta for '${tag}'`)
75
- }
76
- } catch (e) {
77
- log({pkg, level: 'warn'})('rollback: failed to clean meta branch', e)
78
- }
79
-
80
- // 3. Delete git tag
81
- await deleteRemoteTag({cwd, tag})
82
- }
83
-
84
- // Rollback a partially failed release: delete orphan tag, meta, changelog, gh release.
85
- export const recover = async (pkg) => {
86
- const needsNpm = !pkg.manifest.private && pkg.config.npmPublish !== false
87
- if (!needsNpm) return false
88
-
89
- const {tag} = pkg.latest
90
- if (!tag) return false
91
-
92
- const manifest = await fetchManifest({
93
- name: pkg.name,
94
- version: tag.version,
95
- config: pkg.config,
96
- }, {nothrow: true})
97
-
98
- if (manifest) return false
99
-
100
- const cwd = await getRoot(pkg.absPath)
101
- const {ghBasicAuth: basicAuth, ghToken, gitCommitterName, gitCommitterEmail} = pkg.config
102
- if (!basicAuth) throw new Error('recover requires git credentials (GH_TOKEN)')
103
- const {repoName} = await getRepo(cwd, {basicAuth})
104
-
105
- log({pkg})(`recover: tag '${tag.ref}' exists but ${pkg.name}@${tag.version} not found on npm, rolling back failed release`)
106
-
107
- // 1. Delete GitHub release (also removes attached meta assets)
108
- if (ghToken) {
109
- try {
110
- const res = await fetch(`https://api.github.com/repos/${repoName}/releases/tags/${tag.ref}`, {
111
- headers: {Authorization: `token ${ghToken}`, 'X-GitHub-Api-Version': '2022-11-28'}
112
- })
113
- if (res.ok) {
114
- const {id} = await res.json()
115
- await fetch(`https://api.github.com/repos/${repoName}/releases/${id}`, {
116
- method: 'DELETE',
117
- headers: {Authorization: `token ${ghToken}`, 'X-GitHub-Api-Version': '2022-11-28'}
118
- })
119
- log({pkg})(`recover: deleted gh release for '${tag.ref}'`)
120
- }
121
- } catch (e) {
122
- log({pkg, level: 'warn'})('recover: failed to delete gh release', e)
123
- }
124
- }
125
-
126
- // 2. Remove meta entry from meta branch
127
- try {
128
- const metaBranch = 'meta'
129
- const metaCwd = await fetchRepo({cwd, branch: metaBranch, basicAuth})
130
- const artifactPath = getArtifactPath(tag.ref)
131
- const candidates = [
132
- path.resolve(metaCwd, `${artifactPath}.json`),
133
- path.resolve(metaCwd, artifactPath)
134
- ]
135
- let removed = false
136
- for (const p of candidates) {
137
- if (fs.existsSync(p)) {
138
- await fs.remove(p)
139
- removed = true
140
- }
141
- }
142
- if (removed) {
143
- await pushCommit({cwd, branch: metaBranch, msg: `chore: recover ${pkg.name} ${tag.version}`, gitCommitterName, gitCommitterEmail, basicAuth})
144
- log({pkg})(`recover: removed meta for '${tag.ref}'`)
145
- }
146
- } catch (e) {
147
- log({pkg, level: 'warn'})('recover: failed to clean meta branch', e)
148
- }
149
-
150
- // 3. Delete orphan git tag
151
- await deleteRemoteTag({cwd, tag: tag.ref})
152
-
153
- return true
154
- }
@@ -1,47 +0,0 @@
1
- import {memoizeBy} from '../util.js'
2
- import {exec} from '../processor/exec.js'
3
- import {$, within} from 'zx-extra'
4
- import {npmPersist, npmPublish} from '../api/npm.js'
5
- import {prepareMeta, pushMeta, pushReleaseTag} from '../processor/meta.js'
6
- import {pushChangelog} from '../api/changelog.js'
7
- import {ghPages, ghRelease} from '../api/gh.js'
8
- import {rollbackRelease} from './contextify.js'
9
-
10
- export const publish = memoizeBy(async (pkg, run = exec) => within(async () => {
11
- $.scope = pkg.name
12
-
13
- if (pkg.version !== pkg.manifest.version) {
14
- throw new Error('package.json version not synced')
15
- }
16
-
17
- await npmPersist(pkg)
18
- await prepareMeta(pkg)
19
-
20
- if (pkg.context.flags.snapshot) {
21
- await Promise.all([
22
- npmPublish(pkg),
23
- run(pkg, 'publishCmd')
24
- ])
25
- } else {
26
- await pushReleaseTag(pkg)
27
- try {
28
- await Promise.all([
29
- pushMeta(pkg),
30
- pushChangelog(pkg),
31
- npmPublish(pkg),
32
- ghRelease(pkg),
33
- ghPages(pkg),
34
- run(pkg, 'publishCmd')
35
- ])
36
- } catch (e) {
37
- // Rollback the entire failed release for npm-published packages.
38
- // Git-tag-only packages (private or npmPublish: false) keep their tag — it IS the release.
39
- const needsNpm = !pkg.manifest.private && pkg.config.npmPublish !== false
40
- if (needsNpm) {
41
- await rollbackRelease(pkg)
42
- }
43
- throw e
44
- }
45
- }
46
- pkg.published = true
47
- }))
@@ -1,16 +0,0 @@
1
- import {$, within} from 'zx-extra'
2
- import {memoizeBy} from '../util.js'
3
- import {exec} from '../processor/exec.js'
4
- // import {traverseDeps} from "../processor/deps.js";
5
-
6
- export const test = memoizeBy(async (pkg, run = exec, flags = {}, self = test) => within(async () => {
7
- $.scope = pkg.name
8
-
9
- // await traverseDeps({pkg, packages: pkg.context.packages, cb: async({pkg}) => self(pkg, run, flags, self)})
10
-
11
- if (!pkg.fetched) {
12
- await run(pkg, 'testCmd')
13
- }
14
-
15
- pkg.tested = true
16
- }))