zx-bulk-release 3.1.5 → 3.1.7

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,14 @@
1
+ ## [3.1.7](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.6...v3.1.7) (2026-04-13)
2
+
3
+ ### Fixes & improvements
4
+ * perf: apply traverseQueue to delivery phase ([6f1ba58](https://github.com/semrel-extra/zx-bulk-release/commit/6f1ba58885e7739b23de1d895f80ea4680645347))
5
+ * perf: let `--verify` accept out dir ([80015bb](https://github.com/semrel-extra/zx-bulk-release/commit/80015bb067306892bbcc969c5ee2700a256b22f4))
6
+
7
+ ## [3.1.6](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.5...v3.1.6) (2026-04-13)
8
+
9
+ ### Fixes & improvements
10
+ * fix: pack uses channel list from context instead of re-evaluating ([0fecefa](https://github.com/semrel-extra/zx-bulk-release/commit/0fecefab3549da98e24d42a2e1b4c7bb1b24b4c9))
11
+
1
12
  ## [3.1.5](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.4...v3.1.5) (2026-04-13)
2
13
 
3
14
  ### Fixes & improvements
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
3
  "alias": "bulk-release",
4
- "version": "3.1.5",
4
+ "version": "3.1.7",
5
5
  "description": "zx-based alternative for multi-semantic-release",
6
6
  "type": "module",
7
7
  "exports": {
@@ -15,7 +15,7 @@ Modes:
15
15
  (no flags) All-in-one: analyze, build, test, pack, deliver
16
16
  --receive Analyze & preflight, write zbr-context.json. Run BEFORE deps install
17
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]
18
+ --verify [in:out] Validate parcels against context, copy to out dir [default: parcels]
19
19
  --deliver [dir] Deliver parcels through channels [default: parcels]
20
20
 
21
21
  Options:
@@ -2,6 +2,7 @@ import {$, tempy, within, path, semver, fs} from 'zx-extra'
2
2
  import {unpackTar} from '../tar.js'
3
3
  import {log} from '../log.js'
4
4
  import {pool} from '../../util.js'
5
+ import {traverseQueue} from '../depot/deps.js'
5
6
  import {scanDirectives, invalidateOrphans} from '../parcel/directive.js'
6
7
  import {tryLock, unlock, signalRebuild} from './semaphore.js'
7
8
  import gitTag from './channels/git-tag.js'
@@ -160,43 +161,38 @@ const deliverParcel = async (tarPath, channelName, pkgName, version, env, {dryRu
160
161
  return res === 'duplicate' ? 'duplicate' : 'ok'
161
162
  }
162
163
 
163
- const deliverDirective = async (directive, tarMap, env, {dryRun, cwd}) => {
164
+ const deliverPkg = async (pkgName, pkg, tarMap, env, {dryRun, cwd}) => {
164
165
  const entries = []
165
166
  const conflicts = []
166
167
  const skipped = []
167
168
 
168
- for (const pkgName of directive.queue) {
169
- const pkg = directive.packages[pkgName]
170
- if (!pkg) continue
171
-
172
- let pkgConflict = false
169
+ let pkgConflict = false
173
170
 
174
- for (const step of pkg.deliver) {
175
- if (pkgConflict) break
171
+ for (const step of pkg.deliver) {
172
+ if (pkgConflict) break
176
173
 
177
- const results = await Promise.all(step.map(async (channelName) => {
178
- const parcelName = (pkg.parcels || []).find(p => p.includes(`.${channelName}.`))
179
- const tarPath = parcelName && tarMap.get(parcelName)
180
- if (!tarPath) return 'missing'
174
+ const results = await Promise.all(step.map(async (channelName) => {
175
+ const parcelName = (pkg.parcels || []).find(p => p.includes(`.${channelName}.`))
176
+ const tarPath = parcelName && tarMap.get(parcelName)
177
+ if (!tarPath) return 'missing'
181
178
 
182
- return deliverParcel(tarPath, channelName, pkgName, pkg.version, env, {dryRun, cwd})
183
- }))
179
+ return deliverParcel(tarPath, channelName, pkgName, pkg.version, env, {dryRun, cwd})
180
+ }))
184
181
 
185
- for (let i = 0; i < step.length; i++) {
186
- const r = results[i]
187
- if (r === 'skip') skipped.push({channelName: step[i], pkg: pkgName})
188
- else if (r !== 'missing' && r !== 'already') entries.push({channel: step[i], name: pkgName, version: pkg.version})
189
- }
182
+ for (let i = 0; i < step.length; i++) {
183
+ const r = results[i]
184
+ if (r === 'skip') skipped.push({channelName: step[i], pkg: pkgName})
185
+ else if (r !== 'missing' && r !== 'already') entries.push({channel: step[i], name: pkgName, version: pkg.version})
186
+ }
190
187
 
191
- if (results.includes('conflict')) {
192
- pkgConflict = true
193
- conflicts.push(pkgName)
194
- for (const p of pkg.parcels || []) {
195
- const tp = tarMap.get(p)
196
- if (!tp) continue
197
- const c = await fs.readFile(tp, 'utf8').catch(() => null)
198
- if (c !== 'released') await fs.writeFile(tp, 'conflict')
199
- }
188
+ if (results.includes('conflict')) {
189
+ pkgConflict = true
190
+ conflicts.push(pkgName)
191
+ for (const p of pkg.parcels || []) {
192
+ const tp = tarMap.get(p)
193
+ if (!tp) continue
194
+ const c = await fs.readFile(tp, 'utf8').catch(() => null)
195
+ if (c !== 'released') await fs.writeFile(tp, 'conflict')
200
196
  }
201
197
  }
202
198
  }
@@ -204,6 +200,25 @@ const deliverDirective = async (directive, tarMap, env, {dryRun, cwd}) => {
204
200
  return {entries, conflicts, skipped}
205
201
  }
206
202
 
203
+ const deliverDirective = async (directive, tarMap, env, {dryRun, cwd}) => {
204
+ const entries = []
205
+ const conflicts = []
206
+ const skipped = []
207
+
208
+ const prev = new Map(Object.entries(directive.prev || {}))
209
+
210
+ await traverseQueue({queue: directive.queue, prev, cb: async (pkgName) => {
211
+ const pkg = directive.packages[pkgName]
212
+ if (!pkg) return
213
+ const r = await deliverPkg(pkgName, pkg, tarMap, env, {dryRun, cwd})
214
+ entries.push(...r.entries)
215
+ conflicts.push(...r.conflicts)
216
+ skipped.push(...r.skipped)
217
+ }})
218
+
219
+ return {entries, conflicts, skipped}
220
+ }
221
+
207
222
  // --- Main entry point ---
208
223
 
209
224
  export const deliver = async (tars, env = process.env, {concurrency = 4, dryRun = false, cwd} = {}) => {
@@ -10,7 +10,8 @@ import {packTar, hashFile} from '../../tar.js'
10
10
  export const pack = memoizeBy(async (pkg, ctx = pkg.ctx) => {
11
11
  const {channels: channelNames = [], flags} = ctx
12
12
  const snapshot = !!flags.snapshot
13
- const active = getActiveChannels(pkg, channelNames, snapshot)
13
+ // In split pipeline, receive already decided which channels are active
14
+ const active = pkg.contextChannels || getActiveChannels(pkg, channelNames, snapshot)
14
15
 
15
16
  await prepare(active, pkg)
16
17
  await api.npm.npmPersist(pkg)
@@ -10,8 +10,8 @@ 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, PARCELS_DIR} from '../parcel/index.js'
14
13
  import {CONTEXT_FILE, readContext} from '../depot/context.js'
14
+ import {buildDirective, PARCELS_DIR} from '../parcel/index.js'
15
15
 
16
16
  export const runPack = async ({cwd, env, flags}, ctx) => {
17
17
  const {report, packages, queue, prev} = ctx
@@ -54,6 +54,7 @@ export const runPack = async ({cwd, env, flags}, ctx) => {
54
54
  pkg.version = ctxPkg.version
55
55
  pkg.tag = ctxPkg.tag
56
56
  pkg.manifest.version = ctxPkg.version
57
+ pkg.contextChannels = ctxPkg.channels
57
58
  if (!pkg.releaseType) pkg.releaseType = 'patch'
58
59
  } else {
59
60
  // Standalone: own analysis decides
@@ -4,9 +4,9 @@ import {PARCELS_DIR, verifyParcels} from '../parcel/index.js'
4
4
  import {CONTEXT_FILE, readContext} from '../depot/context.js'
5
5
 
6
6
  export const runVerify = async ({cwd, flags}) => {
7
- const inputDir = typeof flags.verify === 'string' ? flags.verify : PARCELS_DIR
7
+ const [inputDir = PARCELS_DIR, outputDir_ = PARCELS_DIR] = typeof flags.verify === 'string' ? flags.verify.split(':') : []
8
+ const outputDir = path.resolve(cwd, outputDir_)
8
9
  const contextPath = typeof flags.context === 'string' ? flags.context : path.resolve(cwd, CONTEXT_FILE)
9
- const outputDir = path.resolve(cwd, PARCELS_DIR)
10
10
 
11
11
  log.info(`verifying parcels in ${inputDir} against ${contextPath}`)
12
12
 
@@ -17,6 +17,7 @@ export const buildDirective = async (ctx, packedPkgs, outputDir) => {
17
17
  const {sha, timestamp} = packedPkgs[0].ctx.git
18
18
  const packages = {}
19
19
  const parcels = []
20
+ const packedNames = new Set(packedPkgs.map(p => p.name))
20
21
 
21
22
  for (const pkg of packedPkgs) {
22
23
  const pkgParcels = (pkg.tars || []).map(t => path.basename(t))
@@ -29,12 +30,21 @@ export const buildDirective = async (ctx, packedPkgs, outputDir) => {
29
30
  parcels.push(...pkgParcels)
30
31
  }
31
32
 
33
+ // Store dependency graph for parallel delivery in topo order
34
+ const prev = {}
35
+ if (ctx.prev) {
36
+ for (const name of packedNames) {
37
+ const deps = (ctx.prev.get(name) || []).filter(d => packedNames.has(d))
38
+ if (deps.length) prev[name] = deps
39
+ }
40
+ }
41
+
32
42
  const sha7 = sha.slice(0, 7)
33
43
  const manifest = {
34
44
  channel: 'directive',
35
45
  sha, timestamp: Number(timestamp),
36
46
  queue: packedPkgs.map(p => p.name),
37
- packages, parcels,
47
+ packages, parcels, prev,
38
48
  }
39
49
 
40
50
  const tarPath = path.join(outputDir, `parcel.${sha7}.directive.${timestamp}.tar`)