bulk-release 2.20.0 → 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 (35) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +47 -74
  3. package/package.json +3 -3
  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/main/js/api/gh.js +0 -131
  30. package/src/main/js/log.js +0 -63
  31. package/src/main/js/steps/build.js +0 -23
  32. package/src/main/js/steps/clean.js +0 -7
  33. package/src/main/js/steps/contextify.js +0 -154
  34. package/src/main/js/steps/publish.js +0 -47
  35. package/src/main/js/steps/test.js +0 -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
+ }
@@ -0,0 +1,58 @@
1
+ // Meta publisher: pushes meta artifacts to the `meta` branch and undoes them on rollback.
2
+ // The meta payload itself is built by the generator (../generators/meta.js).
3
+
4
+ import {queuefy} from 'queuefy'
5
+ import {fs, path} from 'zx-extra'
6
+ import {log} from '../log.js'
7
+ import {fetchRepo, pushCommit} from '../api/git.js'
8
+ import {formatTag} from '../generators/tag.js'
9
+ import {prepareMeta, getArtifactPath, isAssetMode} from '../generators/meta.js'
10
+
11
+ // Push the meta artifact to the `meta` branch. No-op in asset mode (handled by gh-release).
12
+ const pushMetaBranch = queuefy(async (pkg) => {
13
+ const {type} = pkg.config.meta
14
+ if (type === null || isAssetMode(type)) return
15
+
16
+ log.info('push artifact to branch \'meta\'')
17
+
18
+ const {name, version, meta, tag = formatTag({name, version}), absPath: cwd, config: {gitCommitterEmail, gitCommitterName, ghBasicAuth: basicAuth}} = pkg
19
+ const msg = `chore: release meta ${name} ${version}`
20
+ const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
21
+
22
+ await pushCommit({cwd, to: '.', branch: 'meta', msg, files, gitCommitterEmail, gitCommitterName, basicAuth})
23
+ })
24
+
25
+ // Remove a meta artifact for a given tag from the meta branch.
26
+ // Returns true if something was removed and pushed.
27
+ const removeMetaArtifact = async (pkg, {tag, version, reason}) => {
28
+ const {config: {meta: {type}, ghBasicAuth: basicAuth, gitCommitterName, gitCommitterEmail}} = pkg
29
+ // Asset-mode meta lives inside the GH release; deleting the release already removes it.
30
+ if (type === null || isAssetMode(type)) return false
31
+
32
+ const cwd = pkg.ctx?.git?.root || pkg.absPath
33
+ const metaCwd = await fetchRepo({cwd, branch: 'meta', basicAuth})
34
+ const artifactPath = getArtifactPath(tag)
35
+ const candidates = [
36
+ path.resolve(metaCwd, `${artifactPath}.json`),
37
+ path.resolve(metaCwd, artifactPath),
38
+ ]
39
+ let removed = false
40
+ for (const p of candidates) {
41
+ if (fs.existsSync(p)) {
42
+ await fs.remove(p)
43
+ removed = true
44
+ }
45
+ }
46
+ if (removed) {
47
+ await pushCommit({cwd, branch: 'meta', msg: `chore: ${reason} ${pkg.name} ${version}`, gitCommitterName, gitCommitterEmail, basicAuth})
48
+ }
49
+ return removed
50
+ }
51
+
52
+ export default {
53
+ name: 'meta',
54
+ when: (pkg) => pkg.config.meta.type !== null,
55
+ prepare: prepareMeta,
56
+ run: pushMetaBranch,
57
+ undo: removeMetaArtifact,
58
+ }
@@ -0,0 +1,15 @@
1
+ import {npmPublish} from '../api/npm.js'
2
+
3
+ // Domain predicate: does this package get published to npm?
4
+ // Private packages and those with `npmPublish: false` are git-tag-only —
5
+ // the tag itself IS the release.
6
+ export const isNpmPublished = (pkg) =>
7
+ !pkg.manifest.private && pkg.config.npmPublish !== false
8
+
9
+ export default {
10
+ name: 'npm',
11
+ when: isNpmPublished,
12
+ run: (pkg) => npmPublish(pkg),
13
+ snapshot: true,
14
+ // No undo: npm unpublish is unreliable (time-limited, cached by mirrors).
15
+ }