zx-bulk-release 3.1.0 → 3.1.2

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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## [3.1.2](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.1...v3.1.2) (2026-04-13)
2
+
3
+ ### Fixes & improvements
4
+ * perf: append pkg scope to internal zx logs ([08471b9](https://github.com/semrel-extra/zx-bulk-release/commit/08471b9c87d9ef2bf24b30a99977c308cee1feaf))
5
+ * perf: update default zx ctx file name ([9857dcd](https://github.com/semrel-extra/zx-bulk-release/commit/9857dcdcf9d1e5221eb3fc62f4919a8af2b885fc))
6
+ * refactor: enhance cli ([8909ab2](https://github.com/semrel-extra/zx-bulk-release/commit/8909ab2496402102b33fa134275ac415b6d8297a))
7
+
8
+ ## [3.1.1](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.0...v3.1.1) (2026-04-12)
9
+
10
+ ### Fixes & improvements
11
+ * perf: add `--help` ([99fcfde](https://github.com/semrel-extra/zx-bulk-release/commit/99fcfde332c7730e335e46b4c2a8c844d53f8986))
12
+ * refactor: separate parcel subdomain ([b168044](https://github.com/semrel-extra/zx-bulk-release/commit/b16804497693b42998cea2638ff6d11bd29eb84f))
13
+
1
14
  ## [3.1.0](https://github.com/semrel-extra/zx-bulk-release/compare/v3.0.5...v3.1.0) (2026-04-12)
2
15
 
3
16
  ### Features
package/README.md CHANGED
@@ -44,10 +44,10 @@ GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
44
44
  ```
45
45
  | Flag | Description | Default |
46
46
  |------------------------------|---------------------------------------------------------------------------------------------------|------------------|
47
- | `--receive` | Consume rebuild signal, analyze, preflight. Writes `.zbr-context.json`. Run before deps install. | |
47
+ | `--receive` | Consume rebuild signal, analyze, preflight. Writes `zbr-context.json`. Run before deps install. | |
48
48
  | `--pack [dir]` | Pack only: build, test, and write delivery tars to `dir`. No credentials needed. | `parcels` |
49
49
  | `--verify [dir]` | Verify untrusted parcels against context, copy valid ones to `parcels/`. Use `--context` for path. | `parcels` |
50
- | `--context <path>` | Path to trusted `.zbr-context.json` (used with `--verify`). | `.zbr-context.json` |
50
+ | `--context <path>` | Path to trusted `zbr-context.json` (used with `--verify`). | `zbr-context.json` |
51
51
  | `--deliver [dir]` | Deliver only: read tars from `dir` and run delivery channels. No source code needed. | `parcels` |
52
52
  | `--ignore` | Packages to ignore: `a, b` | |
53
53
  | `--include-private` | Include `private` packages | `false` |
@@ -152,7 +152,7 @@ jobs:
152
152
  - uses: actions/upload-artifact@v4
153
153
  with:
154
154
  name: context-${{ github.run_id }}
155
- path: .zbr-context.json
155
+ path: zbr-context.json
156
156
 
157
157
  # pack — deps installed, hostile code may run, zero credentials
158
158
  - if: steps.receive.outputs.status == 'proceed'
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
3
  "alias": "bulk-release",
4
- "version": "3.1.0",
4
+ "version": "3.1.2",
5
5
  "description": "zx-based alternative for multi-semantic-release",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./src/main/js/index.js",
9
9
  "./test-utils": "./src/test/js/utils/repo.js",
10
- "./meta": "./src/main/js/processor/generators/meta.js"
10
+ "./meta": "./src/main/js/post/depot/generators/meta.js"
11
11
  },
12
12
  "bin": "./src/main/js/cli.js",
13
13
  "files": [
@@ -1,7 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import {argv} from 'zx-extra'
4
- import {run} from './index.js'
4
+ import {run, version} from './index.js'
5
5
  import {normalizeFlags} from './config.js'
6
6
 
7
- run({flags: normalizeFlags(argv)})
7
+ const flags = normalizeFlags(argv)
8
+
9
+ if (flags.h || flags.help) {
10
+ console.log(`zx-bulk-release v${version}
11
+
12
+ Usage: npx zx-bulk-release [options]
13
+
14
+ Modes:
15
+ (no flags) All-in-one: analyze, build, test, pack, deliver
16
+ --receive Analyze & preflight, write zbr-context.json. Run BEFORE deps install
17
+ --pack [dir] Build, test, pack tars to dir [default: parcels]
18
+ --verify [input-dir] Validate parcels against context, copy valid to parcels/ [default: parcels]
19
+ --deliver [dir] Deliver parcels through channels [default: parcels]
20
+
21
+ Options:
22
+ --context <path> Trusted context path (with --verify) [default: zbr-context.json]
23
+ --dry-run, --no-publish Disable publish and any remote-mutating operations
24
+ --no-build Skip buildCmd
25
+ --no-test Skip testCmd
26
+ --snapshot Publish snapshot versions to npm only
27
+ --ignore <a,b> Packages to ignore
28
+ --include-private Include private packages
29
+ --concurrency <n> Build/publish thread limit [default: os.cpus().length]
30
+ --only-workspace-deps Recognize only workspace: deps as graph edges
31
+ --no-npm-fetch Disable npm artifact fetching
32
+ --report <path> Persist release state to file
33
+ --debug Enable verbose mode
34
+ -v, --version Print version
35
+ -h, --help Show this help`)
36
+ process.exit(0)
37
+ }
38
+
39
+ if (flags.v || flags.version) {
40
+ console.log(version)
41
+ process.exit(0)
42
+ }
43
+
44
+ try {
45
+ await run({flags})
46
+ process.exit(0)
47
+ } catch (e) {
48
+ if (e?.exitCode !== undefined) process.exit(e.exitCode)
49
+ console.error(e)
50
+ process.exit(1)
51
+ }
@@ -1 +1 @@
1
- export {run} from './post/release.js'
1
+ export {run, version} from './post/release.js'
@@ -146,18 +146,18 @@ const unzip = (stream, {pick, omit, cwd = process.cwd(), strip = 0} = {}) => new
146
146
  extract.on('entry', ({name, type}, stream, cb) => {
147
147
  const _name = safePath(strip ? name.split('/').slice(strip).join('/') : name)
148
148
  const fp = _path.join(cwd, _name)
149
+ const skip = type !== 'file' || omit?.includes(_name) || (pick && !pick.includes(_name))
149
150
 
150
- let data = ''
151
+ const chunks = []
151
152
  stream.on('data', (chunk) => {
152
- if (type !== 'file' || omit?.includes(_name) || (pick && !pick.includes(_name))) return
153
- data += chunk
153
+ if (!skip) chunks.push(chunk)
154
154
  })
155
155
 
156
156
  stream.on('end', () => {
157
- if (data) {
157
+ if (chunks.length) {
158
158
  results.push(
159
159
  _fs.mkdir(_path.dirname(fp), {recursive: true})
160
- .then(() => _fs.writeFile(fp, data, 'utf8'))
160
+ .then(() => _fs.writeFile(fp, Buffer.concat(chunks)))
161
161
  )
162
162
  }
163
163
  cb()
@@ -1,4 +1,4 @@
1
- import {$} from 'zx-extra'
1
+ import {fs, path} from 'zx-extra'
2
2
  import {queuefy} from 'queuefy'
3
3
  import {fetchRepo, pushCommit} from '../../api/git.js'
4
4
  import {log} from '../../log.js'
@@ -15,8 +15,10 @@ const run = queuefy(async (manifest, dir) => {
15
15
  log.info('push changelog')
16
16
 
17
17
  const _cwd = await fetchRepo({branch, origin: repoAuthedUrl, basicAuth: ghBasicAuth})
18
+ const filePath = path.resolve(_cwd, file)
19
+ const prev = await fs.readFile(filePath, 'utf8').catch(() => '')
20
+ await fs.outputFile(filePath, releaseNotes + '\n' + prev)
18
21
 
19
- await $({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
20
22
  await pushCommit({
21
23
  branch,
22
24
  msg,
@@ -1,5 +1,4 @@
1
- import {pushTag} from '../../api/git.js'
2
- import {DEFAULT_GIT_COMMITTER_NAME, DEFAULT_GIT_COMMITTER_EMAIL} from '../../api/git.js'
1
+ import {pushTag, DEFAULT_GIT_COMMITTER_NAME, DEFAULT_GIT_COMMITTER_EMAIL} from '../../api/git.js'
3
2
 
4
3
  export const isTagConflict = (e) =>
5
4
  /already exists|updates were rejected|failed to push/i.test(e?.message || e?.stderr || '')
@@ -1,8 +1,7 @@
1
1
  import {queuefy} from 'queuefy'
2
2
  import {log} from '../../log.js'
3
3
  import {pushCommit} from '../../api/git.js'
4
- import {getArtifactPath, isAssetMode} from '../../depot/generators/meta.js'
5
- import {prepareMeta} from '../../depot/generators/meta.js'
4
+ import {getArtifactPath, isAssetMode, prepareMeta} from '../../depot/generators/meta.js'
6
5
  import {hasHigherVersion} from '../seniority.js'
7
6
 
8
7
  const pushMetaBranch = queuefy(async (manifest, dir) => {
@@ -1,7 +1,8 @@
1
1
  import {$, tempy, within, path, semver, fs} from 'zx-extra'
2
2
  import {unpackTar} from '../tar.js'
3
3
  import {log} from '../log.js'
4
- import {scanDirectives, invalidateOrphans} from './directive.js'
4
+ import {pool} from '../../util.js'
5
+ import {scanDirectives, invalidateOrphans} from '../parcel/directive.js'
5
6
  import {tryLock, unlock, signalRebuild} from './semaphore.js'
6
7
  import gitTag from './channels/git-tag.js'
7
8
  import meta from './channels/meta.js'
@@ -11,11 +12,16 @@ import ghPages from './channels/gh-pages.js'
11
12
  import changelog from './channels/changelog.js'
12
13
  import cmd from './channels/cmd.js'
13
14
 
14
- export {buildParcels} from './parcel.js'
15
15
 
16
16
  export const channels = {'git-tag': gitTag, meta, npm, 'gh-release': ghRelease, 'gh-pages': ghPages, changelog, cmd}
17
17
  export const defaultOrder = ['git-tag', 'meta', 'npm', 'gh-release', 'gh-pages', 'changelog', 'cmd']
18
18
 
19
+ export const getActiveChannels = (pkg, channelNames, snapshot) =>
20
+ channelNames.filter(n => {
21
+ const ch = channels[n]
22
+ return ch && ch.transport !== false && (!snapshot || ch.snapshot) && ch.when(pkg)
23
+ })
24
+
19
25
  export const prepare = async (names, pkg) => {
20
26
  for (const n of names) await channels[n]?.prepare?.(pkg)
21
27
  }
@@ -61,22 +67,6 @@ const openParcel = async (tarPath, env) => {
61
67
  return {ch, resolved, destDir, tarPath}
62
68
  }
63
69
 
64
- const pool = async (tasks, concurrency, fn) => {
65
- const active = new Set()
66
- let i = 0
67
- await new Promise((resolve, reject) => {
68
- const next = () => {
69
- if (i >= tasks.length && active.size === 0) return resolve()
70
- while (active.size < concurrency && i < tasks.length) {
71
- const t = tasks[i++]
72
- const p = fn(t).then(() => { active.delete(p); next() }, reject)
73
- active.add(p)
74
- }
75
- }
76
- next()
77
- })
78
- }
79
-
80
70
  export const inspect = async (tars, env = process.env) => {
81
71
  const parcels = []
82
72
  const skipped = []
@@ -1,7 +1,6 @@
1
1
  import {fs, path} from 'zx-extra'
2
- import {channels as channelRegistry} from '../courier/index.js'
3
2
 
4
- const CONTEXT_FILE = '.zbr-context.json'
3
+ export const CONTEXT_FILE = 'zbr-context.json'
5
4
 
6
5
  export const writeContext = async (cwd, context) => {
7
6
  const filePath = path.resolve(cwd, CONTEXT_FILE)
@@ -9,22 +8,24 @@ export const writeContext = async (cwd, context) => {
9
8
  return filePath
10
9
  }
11
10
 
12
- export const readContext = async (cwd) => {
13
- const filePath = path.resolve(cwd, CONTEXT_FILE)
14
- try {
15
- return await fs.readJson(filePath)
16
- } catch {
17
- return null
11
+ export const readContext = async (filePath) => {
12
+ let data
13
+ try { data = await fs.readJson(filePath) } catch {
14
+ throw new Error(`context not found: ${filePath}`)
18
15
  }
19
- }
20
16
 
21
- const getActiveChannels = (pkg, channelNames, snapshot) =>
22
- channelNames.filter(n => {
23
- const ch = channelRegistry[n]
24
- return ch && ch.transport !== false && (!snapshot || ch.snapshot) && ch.when(pkg)
25
- })
17
+ if (!data || typeof data !== 'object') throw new Error(`context is not an object: ${filePath}`)
18
+ if (!data.status) throw new Error(`context missing status: ${filePath}`)
19
+ if (data.status === 'proceed') {
20
+ if (!data.sha || !data.sha7) throw new Error(`context missing sha: ${filePath}`)
21
+ if (!data.packages || typeof data.packages !== 'object')
22
+ throw new Error(`context missing packages: ${filePath}`)
23
+ }
24
+
25
+ return data
26
+ }
26
27
 
27
- export const buildContext = (packages, queue, sha, {channelNames = [], snapshot = false} = {}) => {
28
+ export const buildContext = (packages, queue, sha, {getChannels} = {}) => {
28
29
  const pkgs = {}
29
30
  for (const name of queue) {
30
31
  const pkg = packages[name]
@@ -32,7 +33,7 @@ export const buildContext = (packages, queue, sha, {channelNames = [], snapshot
32
33
  pkgs[name] = {
33
34
  version: pkg.version,
34
35
  tag: pkg.tag,
35
- channels: getActiveChannels(pkg, channelNames, snapshot),
36
+ channels: getChannels ? getChannels(pkg) : [],
36
37
  }
37
38
  }
38
39
 
@@ -1,7 +1,7 @@
1
1
  // Meta generator: builds pkg.meta payload and resolves latest-release meta from git tags / gh assets / meta branch.
2
2
 
3
3
  import {semver, $, fs, path} from 'zx-extra'
4
- import {fetchRepo, getTags as getGitTags, getRepo} from '../../api/git.js'
4
+ import {fetchRepo, getTags as getGitTags, getRepo, getSha} from '../../api/git.js'
5
5
  import {fetchManifest} from '../../api/npm.js'
6
6
  import {ghGetAsset} from '../../api/gh.js'
7
7
  import {parseTag} from './tag.js'
@@ -16,7 +16,7 @@ export const prepareMeta = async (pkg) => {
16
16
  if (type === null) return
17
17
 
18
18
  const {absPath: cwd} = pkg
19
- const hash = (await $.o({cwd})`git rev-parse HEAD`).toString().trim()
19
+ const hash = await getSha(cwd)
20
20
  pkg.meta = {
21
21
  META_VERSION: '1',
22
22
  hash,
@@ -4,9 +4,6 @@ import {getRemoteTagSha, clearTagsCache} from '../api/git.js'
4
4
  import {formatTag} from './generators/tag.js'
5
5
  import {resolvePkgVersion} from './steps/analyze.js'
6
6
 
7
- export const isTagConflict = (e) =>
8
- /already exists|updates were rejected|failed to push/i.test(e?.message || e?.stderr || '')
9
-
10
7
  export const preflight = async (pkg, ctx) => {
11
8
  if (!pkg.tag) return 'ok'
12
9
 
@@ -42,9 +42,9 @@ export const contextify = async (pkg, ctx) => {
42
42
  pkg.ctx = {
43
43
  ...ctx,
44
44
  git: {
45
- sha: await getSha(pkg.absPath),
46
- root: await getRoot(pkg.absPath),
47
- timestamp: await getCommitTimestamp(pkg.absPath),
45
+ sha: await getSha(ctx.cwd),
46
+ root: await getRoot(ctx.cwd),
47
+ timestamp: await getCommitTimestamp(ctx.cwd),
48
48
  },
49
49
  }
50
50
  }
@@ -1,28 +1,22 @@
1
1
  import {$, tempy, fs, path} from 'zx-extra'
2
2
  import {memoizeBy, asTuple} from '../../../util.js'
3
- import {channels, prepare, buildParcels} from '../../courier/index.js'
3
+ import {prepare, getActiveChannels} from '../../courier/index.js'
4
+ import {buildParcels, PARCELS_DIR} from '../../parcel/index.js'
4
5
  import {npmPersist} from '../../api/npm.js'
5
6
  import {getRepo} from '../../api/git.js'
6
7
  import {formatReleaseNotes} from '../generators/notes.js'
7
8
  import {ghPrepareAssets} from '../../api/gh.js'
8
9
  import {packTar, hashFile} from '../../tar.js'
9
10
 
10
-
11
- const filterActive = (names, pkg, {snapshot = false} = {}) =>
12
- names.filter(n => {
13
- const ch = channels[n]
14
- return ch && ch.transport !== false && (!snapshot || ch.snapshot) && ch.when(pkg)
15
- })
16
-
17
11
  export const pack = memoizeBy(async (pkg, ctx = pkg.ctx) => {
18
12
  const {channels: channelNames = [], flags} = ctx
19
13
  const snapshot = !!flags.snapshot
20
- const active = filterActive(channelNames, pkg, {snapshot})
14
+ const active = getActiveChannels(pkg, channelNames, snapshot)
21
15
 
22
16
  await prepare(active, pkg)
23
17
  await npmPersist(pkg)
24
18
 
25
- const outputDir = flags.pack ? path.resolve(ctx.git.root, typeof flags.pack === 'string' ? flags.pack : 'parcels') : null
19
+ const outputDir = flags.pack ? path.resolve(ctx.git.root, typeof flags.pack === 'string' ? flags.pack : PARCELS_DIR) : null
26
20
  const stageDir = outputDir || tempy.temporaryDirectory()
27
21
  if (outputDir) await fs.ensureDir(outputDir)
28
22
  const {repoName, repoHost, originUrl} = await getRepo(pkg.absPath, {basicAuth: pkg.config.ghBasicAuth})
@@ -1,8 +1,7 @@
1
1
  import {$, glob, path} from 'zx-extra'
2
2
  import {createReport, log} from '../log.js'
3
3
  import {deliver} from '../courier/index.js'
4
-
5
- const PARCELS_DIR = 'parcels'
4
+ import {PARCELS_DIR} from '../parcel/index.js'
6
5
 
7
6
  export const runDeliver = async ({env, flags}) => {
8
7
  const dir = typeof flags.deliver === 'string' ? flags.deliver : PARCELS_DIR
@@ -10,9 +10,7 @@ import {publish} from '../depot/steps/publish.js'
10
10
  import {clean} from '../depot/steps/clean.js'
11
11
  import {test} from '../depot/steps/test.js'
12
12
  import {preflight} from '../depot/reconcile.js'
13
- import {buildDirective} from '../courier/directive.js'
14
-
15
- const PARCELS_DIR = 'parcels'
13
+ import {buildDirective, PARCELS_DIR} from '../parcel/index.js'
16
14
 
17
15
  export const runPack = async ({cwd, env, flags}, ctx) => {
18
16
  const {report, packages, queue, prev} = ctx
@@ -7,6 +7,7 @@ import {analyze} from '../depot/steps/analyze.js'
7
7
  import {clean} from '../depot/steps/clean.js'
8
8
  import {preflight} from '../depot/reconcile.js'
9
9
  import {consumeRebuildSignal} from '../courier/semaphore.js'
10
+ import {getActiveChannels} from '../courier/index.js'
10
11
  import {writeContext, buildContext} from '../depot/context.js'
11
12
  import {getSha} from '../api/git.js'
12
13
  import {setOutput, isRebuildTrigger} from '../api/gh.js'
@@ -17,7 +18,7 @@ export const runReceive = async ({cwd, env, flags}, ctx) => {
17
18
  const sha = await getSha(cwd)
18
19
  const sha7 = sha.slice(0, 7)
19
20
 
20
- if (isRebuildTrigger(env)) {
21
+ if (isRebuildTrigger(env) && !flags.dryRun) {
21
22
  const result = await consumeRebuildSignal(cwd, sha)
22
23
  if (result?.exitCode !== 0 && result?.stderr?.includes('remote ref does not exist')) {
23
24
  log.info(`rebuild signal already consumed by another process`)
@@ -45,9 +46,9 @@ export const runReceive = async ({cwd, env, flags}, ctx) => {
45
46
  if (await preflight(pkg, pkg.ctx) === 'skip') { pkg.skipped = true; return }
46
47
  })
47
48
 
49
+ const snapshot = !!flags.snapshot
48
50
  const context = buildContext(packages, queue, sha, {
49
- channelNames: ctx.channels,
50
- snapshot: !!flags.snapshot,
51
+ getChannels: (pkg) => getActiveChannels(pkg, ctx.channels, snapshot),
51
52
  })
52
53
  await writeContext(cwd, context)
53
54
 
@@ -1,20 +1,19 @@
1
1
  import {glob, path, fs} from 'zx-extra'
2
2
  import {log} from '../log.js'
3
- import {parcelChannel} from '../courier/directive.js'
4
-
5
- const PARCELS_DIR = 'parcels'
3
+ import {PARCELS_DIR, verifyParcels} from '../parcel/index.js'
4
+ import {CONTEXT_FILE, readContext} from '../depot/context.js'
6
5
 
7
6
  export const runVerify = async ({cwd, flags}) => {
8
7
  const inputDir = typeof flags.verify === 'string' ? flags.verify : PARCELS_DIR
9
- const contextPath = typeof flags.context === 'string' ? flags.context : path.resolve(cwd, '.zbr-context.json')
8
+ const contextPath = typeof flags.context === 'string' ? flags.context : path.resolve(cwd, CONTEXT_FILE)
10
9
  const outputDir = path.resolve(cwd, PARCELS_DIR)
11
10
 
12
11
  log.info(`verifying parcels in ${inputDir} against ${contextPath}`)
13
12
 
14
- let context
15
- try { context = await fs.readJson(contextPath) } catch { context = null }
16
- if (!context || context.status !== 'proceed') {
17
- throw new Error(`no valid context at ${contextPath}`)
13
+ const context = await readContext(contextPath)
14
+ if (context.status !== 'proceed') {
15
+ log.info(`context status is '${context.status}', nothing to verify`)
16
+ return
18
17
  }
19
18
 
20
19
  const tars = await glob(path.join(inputDir, 'parcel.*.tar'))
@@ -23,54 +22,13 @@ export const runVerify = async ({cwd, flags}) => {
23
22
  return
24
23
  }
25
24
 
26
- const {sha7, packages: expected} = context
27
- const errors = []
28
- const verified = []
29
-
30
- for (const tarPath of tars) {
31
- const name = path.basename(tarPath)
32
-
33
- // sha7 prefix must match
34
- if (!name.startsWith(`parcel.${sha7}.`)) {
35
- errors.push(`sha mismatch: ${name}`)
36
- continue
37
- }
38
-
39
- const channel = parcelChannel(name)
40
- if (!channel) {
41
- errors.push(`malformed name: ${name}`)
42
- continue
43
- }
44
-
45
- if (channel === 'directive') {
46
- verified.push(tarPath)
47
- continue
48
- }
49
-
50
- // match to an expected package by tag
51
- const belongsTo = Object.entries(expected).find(([, pkg]) =>
52
- pkg.tag && name.includes(`.${pkg.tag}.`)
53
- )
54
- if (!belongsTo) {
55
- errors.push(`unexpected parcel (no matching package): ${name}`)
56
- continue
57
- }
58
-
59
- const [pkgName, pkg] = belongsTo
60
- if (!pkg.channels.includes(channel)) {
61
- errors.push(`unexpected channel '${channel}' for ${pkgName}: ${name}`)
62
- continue
63
- }
64
-
65
- verified.push(tarPath)
66
- }
25
+ const {verified, errors} = verifyParcels(tars, context)
67
26
 
68
27
  if (errors.length) {
69
28
  for (const e of errors) log.error(`verify: ${e}`)
70
29
  throw new Error(`parcel verification failed: ${errors.length} error(s)`)
71
30
  }
72
31
 
73
- // copy verified parcels to output
74
32
  if (path.resolve(inputDir) !== outputDir) {
75
33
  await fs.ensureDir(outputDir)
76
34
  for (const tarPath of verified) {
@@ -0,0 +1,4 @@
1
+ export {buildParcels} from './build.js'
2
+ export {buildDirective, parseDirective, scanDirectives, invalidateOrphans, parcelChannel} from './directive.js'
3
+ export {verifyParcels} from './verify.js'
4
+ export const PARCELS_DIR = 'parcels'
@@ -0,0 +1,46 @@
1
+ import {path} from 'zx-extra'
2
+ import {parcelChannel} from './directive.js'
3
+
4
+ export const verifyParcels = (tars, context) => {
5
+ const {sha7, packages: expected} = context
6
+ const errors = []
7
+ const verified = []
8
+
9
+ for (const tarPath of tars) {
10
+ const name = path.basename(tarPath)
11
+
12
+ if (!name.startsWith(`parcel.${sha7}.`)) {
13
+ errors.push(`sha mismatch: ${name}`)
14
+ continue
15
+ }
16
+
17
+ const channel = parcelChannel(name)
18
+ if (!channel) {
19
+ errors.push(`malformed name: ${name}`)
20
+ continue
21
+ }
22
+
23
+ if (channel === 'directive') {
24
+ verified.push(tarPath)
25
+ continue
26
+ }
27
+
28
+ const belongsTo = Object.entries(expected).find(([, pkg]) =>
29
+ pkg.tag && name.includes(`.${pkg.tag}.`)
30
+ )
31
+ if (!belongsTo) {
32
+ errors.push(`unexpected parcel (no matching package): ${name}`)
33
+ continue
34
+ }
35
+
36
+ const [pkgName, pkg] = belongsTo
37
+ if (!pkg.channels.includes(channel)) {
38
+ errors.push(`unexpected channel '${channel}' for ${pkgName}: ${name}`)
39
+ continue
40
+ }
41
+
42
+ verified.push(tarPath)
43
+ }
44
+
45
+ return {verified, errors}
46
+ }
@@ -14,14 +14,21 @@ import {runPack} from './modes/pack.js'
14
14
 
15
15
  const ZBR_VERSION = createRequire(import.meta.url)('../../../../package.json').version
16
16
 
17
- export const run = async ({cwd = process.cwd(), env: _env, flags = {}} = {}) => within(async () => {
18
- if (flags.v || flags.version)
19
- return console.log(ZBR_VERSION)
17
+ export {ZBR_VERSION as version}
20
18
 
19
+ export const run = async ({cwd = process.cwd(), env: _env, flags = {}} = {}) => within(async () => {
21
20
  const env = {...process.env, ..._env}
22
21
  log.secret(env.GH_TOKEN, env.GITHUB_TOKEN, env.NPM_TOKEN)
23
22
  log.info(`zx-bulk-release@${ZBR_VERSION}`)
24
23
 
24
+ const _log = $.log
25
+ $.log = (entry) => {
26
+ if (entry.kind === 'cmd' && $.scope) {
27
+ entry = {...entry, cmd: `[${$.scope}] ${entry.cmd}`}
28
+ }
29
+ _log(entry)
30
+ }
31
+
25
32
  if (flags.verify) return runVerify({cwd, flags})
26
33
  if (flags.deliver) return runDeliver({env, flags})
27
34
 
@@ -72,3 +72,19 @@ export const memoizeBy = (fn, getKey = v => v) => {
72
72
  export const camelize = s => s.replace(/-./g, x => x[1].toUpperCase())
73
73
 
74
74
  export const asArray = v => Array.isArray(v) ? v : [v]
75
+
76
+ export const pool = async (tasks, concurrency, fn) => {
77
+ const active = new Set()
78
+ let i = 0
79
+ await new Promise((resolve, reject) => {
80
+ const next = () => {
81
+ if (i >= tasks.length && active.size === 0) return resolve()
82
+ while (active.size < concurrency && i < tasks.length) {
83
+ const t = tasks[i++]
84
+ const p = fn(t).then(() => { active.delete(p); next() }, reject)
85
+ active.add(p)
86
+ }
87
+ }
88
+ next()
89
+ })
90
+ }