zx-bulk-release 2.21.0 → 2.21.1

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,8 @@
1
+ ## [2.21.1](https://github.com/semrel-extra/zx-bulk-release/compare/v2.21.0...v2.21.1) (2026-04-11)
2
+
3
+ ### Fixes & improvements
4
+ * docs: add snapshot flow example ([5b2c2b5](https://github.com/semrel-extra/zx-bulk-release/commit/5b2c2b5faac712892c9feeea7ebae6e1dd36e93f))
5
+
1
6
  ## [2.21.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.20.0...v2.21.0) (2026-04-10)
2
7
 
3
8
  ### Fixes & improvements
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # zx-bulk-release
2
2
  > [zx](https://github.com/google/zx)-based alternative for [multi-semantic-release](https://github.com/dhoulb/multi-semantic-release)
3
3
 
4
- [![CI](https://github.com/semrel-extra/zx-bulk-release/workflows/CI/badge.svg?branch=master)](https://github.com/semrel-extra/zx-bulk-release/actions)
5
- [![Maintainability](https://qlty.sh/badges/semrel-extra/zx-bulk-release/maintainability.svg)](https://qlty.sh/gh/semrel-extra/zx-bulk-release)
6
- [![Test Coverage](https://qlty.sh/badges/semrel-extra/zx-bulk-release/test_coverage.svg)](https://qlty.sh/gh/semrel-extra/zx-bulk-release)
4
+ [![CI](https://github.com/semrel-extra/zx-bulk-release/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/semrel-extra/zx-bulk-release/actions/workflows/ci.yml)
5
+ [![Maintainability](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release/maintainability.svg)](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release)
6
+ [![Code Coverage](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release/coverage.svg)](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release)
7
7
  [![npm (tag)](https://img.shields.io/npm/v/zx-bulk-release)](https://www.npmjs.com/package/zx-bulk-release)
8
8
 
9
9
  ## Key features
@@ -147,6 +147,50 @@ OIDC mode is also auto-detected when `NPM_TOKEN` is not set and `ACTIONS_ID_TOKE
147
147
 
148
148
  When OIDC is active, `NPM_TOKEN` and `NPMRC` are ignored for publishing and `--provenance` is enabled automatically.
149
149
 
150
+ ### Snapshot releases from PRs
151
+ The `--snapshot` flag publishes packages to the `snapshot` npm dist-tag with a pre-release version like `1.2.1-snap.a3f0c12`. This is useful for testing changes from a feature branch before merging.
152
+
153
+ **What snapshot does differently:**
154
+ - Version gets a `-snap.<short-sha>` suffix instead of a clean bump
155
+ - Git release tags are **not** pushed
156
+ - Only `npm` and `publishCmd` publishers run (no gh-release, no changelog, no gh-pages, no meta)
157
+ - npm tag is `snapshot` instead of `latest`
158
+
159
+ **Workflow example** (`.github/workflows/snapshot.yml`):
160
+ ```yaml
161
+ name: Snapshot
162
+ on:
163
+ pull_request:
164
+ types: [labeled]
165
+
166
+ jobs:
167
+ snapshot:
168
+ if: github.event.label.name == 'snapshot'
169
+ runs-on: ubuntu-latest
170
+ permissions:
171
+ contents: read
172
+ id-token: write # for OIDC trusted publishing
173
+ steps:
174
+ - uses: actions/checkout@v4
175
+ with:
176
+ fetch-depth: 0
177
+ - uses: actions/setup-node@v4
178
+ with:
179
+ node-version: 20
180
+ - run: yarn install
181
+ - run: npx zx-bulk-release --snapshot
182
+ env:
183
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
185
+ ```
186
+
187
+ **How to use:**
188
+ 1. Push a feature branch and open a PR.
189
+ 2. Add the `snapshot` label to the PR.
190
+ 3. The workflow publishes snapshot versions to npm.
191
+ 4. Consumers install snapshots via `npm install yourpkg@snapshot`.
192
+ 5. After merge, the regular release flow on `master` publishes clean versions to `latest`.
193
+
150
194
  ### Selective testing along the change graph
151
195
  In a monorepo, `--dry-run` combined with `--no-build` lets you run tests only for packages affected by the current changes — following the dependency graph, without publishing anything. This gives you a precise CI check scoped to what actually changed:
152
196
  ```shell
package/package.json CHANGED
@@ -1,25 +1,27 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
3
  "alias": "bulk-release",
4
- "version": "2.21.0",
4
+ "version": "2.21.1",
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
- "./test-utils": "./src/test/js/test-utils.js",
9
+ "./test-utils": "./src/test/js/utils/repo.js",
10
10
  "./meta": "./src/main/js/processor/generators/meta.js"
11
11
  },
12
12
  "bin": "./src/main/js/cli.js",
13
13
  "files": [
14
14
  "src/main/js",
15
- "src/test/js/test-utils.js",
15
+ "src/test/js/utils",
16
16
  "CHANGELOG.md",
17
17
  "LICENSE",
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "test": "c8 uvu ./src/test -i fixtures -i utils && c8 report -r lcov",
21
+ "test": "npm run test:unit",
22
+ "test:unit": "uvu ./src/test -i fixtures -i utils -i integration",
22
23
  "test:it": "node ./src/test/js/integration.test.js",
24
+ "test:cov": "c8 sh -c 'npx uvu ./src/test -i fixtures -i utils -i integration && node ./src/test/js/integration.test.js' && c8 report -r lcov",
23
25
  "docs": "mkdir -p docs && cp ./README.md ./docs/README.md",
24
26
  "publish:beta": "npm publish --tag beta --no-git-tag-version",
25
27
  "build": "esbuild src/main/js/index.js --platform=node --outdir=target --bundle --format=esm --external:typescript"
@@ -45,5 +47,10 @@
45
47
  "url": "git+https://github.com/semrel-extra/zx-bulk-release.git"
46
48
  },
47
49
  "author": "Anton Golub <antongolub@antongolub.com>",
48
- "license": "MIT"
50
+ "license": "MIT",
51
+ "c8": {
52
+ "exclude": [
53
+ "src/test/**"
54
+ ]
55
+ }
49
56
  }
@@ -0,0 +1,33 @@
1
+ import http from 'node:http'
2
+
3
+ export const createGhServer = async () => {
4
+ const requests = []
5
+ let routes = {}
6
+
7
+ const server = http.createServer((req, res) => {
8
+ let body = ''
9
+ req.on('data', c => body += c)
10
+ req.on('end', () => {
11
+ requests.push({method: req.method, url: req.url, headers: req.headers, body})
12
+ const key = `${req.method} ${req.url}`
13
+ for (const [pattern, handler] of Object.entries(routes)) {
14
+ if (key.includes(pattern) || new RegExp(pattern).test(key)) {
15
+ return handler(req, res, body)
16
+ }
17
+ }
18
+ res.writeHead(404)
19
+ res.end('{}')
20
+ })
21
+ })
22
+
23
+ await new Promise(r => server.listen(0, '127.0.0.1', r))
24
+
25
+ return {
26
+ url: `http://127.0.0.1:${server.address().port}`,
27
+ requests,
28
+ routes,
29
+ reset() { requests.length = 0; routes = {}; return routes },
30
+ setRoutes(r) { Object.assign(routes, r) },
31
+ close() { server.close() },
32
+ }
33
+ }
@@ -0,0 +1,132 @@
1
+ import {PassThrough} from 'node:stream'
2
+ import {EventEmitter} from 'node:events'
3
+ import {tempy} from 'zx-extra'
4
+
5
+ export const tmpDir = tempy.temporaryDirectory()
6
+
7
+ function proc(stdout = '', code = 0) {
8
+ const p = new EventEmitter()
9
+ p.stdout = new PassThrough()
10
+ p.stderr = new PassThrough()
11
+ p.stdin = new PassThrough()
12
+ p.pid = 1
13
+ process.nextTick(() => {
14
+ p.stdout.end(stdout)
15
+ p.stderr.end('')
16
+ p.emit('close', code, null)
17
+ p.emit('exit', code, null)
18
+ })
19
+ return p
20
+ }
21
+
22
+ export function createMock(responses = []) {
23
+ const calls = []
24
+ const spawn = (cmd, args) => {
25
+ const command = args?.[args.length - 1] || cmd
26
+ calls.push(command)
27
+ for (const [pattern, output, code] of responses) {
28
+ if (typeof pattern === 'string' ? command.includes(pattern) : pattern.test(command))
29
+ return proc(output, code ?? 0)
30
+ }
31
+ return proc('')
32
+ }
33
+ return {spawn, calls}
34
+ }
35
+
36
+ export const has = (calls, pattern) =>
37
+ calls.some(c => typeof pattern === 'string' ? c.includes(pattern) : pattern.test(c))
38
+
39
+ export const gitResponses = (overrides = {}) => [
40
+ ['git rev-parse --show-toplevel', overrides.root ?? tmpDir],
41
+ ['git rev-parse HEAD', overrides.sha ?? 'abc1234567890'],
42
+ ['git config --get remote.origin.url', overrides.origin ?? 'https://github.com/test-org/test-repo.git'],
43
+ ['git tag -l', overrides.tags ?? ''],
44
+ [/git log .+--format/, overrides.log ?? ''],
45
+ ['git config user.name', ''],
46
+ ['git config user.email', ''],
47
+ [/git tag -m/, ''],
48
+ ['git push', ''],
49
+ ['git fetch', ''],
50
+ ['git clone', ''],
51
+ ['git init', ''],
52
+ ['git add', ''],
53
+ ['git commit', ''],
54
+ [/git remote/, ''],
55
+ [/git config --unset/, ''],
56
+ ]
57
+
58
+ export const npmResponses = (overrides = {}) => [
59
+ ['npm --version', overrides.npmVersion ?? '10.0.0'],
60
+ ['npm publish', ''],
61
+ ['npm view', overrides.view ?? ''],
62
+ ]
63
+
64
+ export const defaultResponses = (overrides = {}) => [
65
+ ...gitResponses(overrides),
66
+ ...npmResponses(overrides),
67
+ [/curl/, overrides.curl ?? '{}'],
68
+ [/echo/, ''],
69
+ ]
70
+
71
+ export const makePkg = (overrides = {}) => ({
72
+ name: overrides.name ?? 'test-pkg',
73
+ version: overrides.version ?? '1.0.1',
74
+ absPath: overrides.absPath ?? `${tmpDir}/packages/test-pkg`,
75
+ relPath: overrides.relPath ?? 'packages/test-pkg',
76
+ manifest: {
77
+ name: overrides.name ?? 'test-pkg',
78
+ version: overrides.version ?? '1.0.1',
79
+ private: overrides.private ?? false,
80
+ ...overrides.manifest,
81
+ },
82
+ config: {
83
+ buildCmd: 'echo build',
84
+ testCmd: 'echo test',
85
+ gitCommitterName: 'Bot',
86
+ gitCommitterEmail: 'bot@test.com',
87
+ ghBasicAuth: 'x-access-token:ghp_test',
88
+ ghToken: 'ghp_test',
89
+ npmPublish: true,
90
+ npmFetch: false,
91
+ changelog: 'changelog',
92
+ ...overrides.config,
93
+ },
94
+ changes: overrides.changes ?? [{group: 'Fixes', releaseType: 'patch', change: 'fix: test'}],
95
+ releaseType: overrides.releaseType ?? 'patch',
96
+ tag: overrides.tag ?? '2026.1.1-test-pkg.1.0.1-f0',
97
+ latest: overrides.latest ?? {tag: null, meta: null},
98
+ ctx: overrides.ctx ?? null,
99
+ ...overrides.extra,
100
+ })
101
+
102
+ export const makeCtx = (overrides = {}) => ({
103
+ cwd: overrides.cwd ?? tmpDir,
104
+ env: {PATH: process.env.PATH, HOME: process.env.HOME, GH_TOKEN: 'ghp_test', NPM_TOKEN: 'npm_test', ...overrides.env},
105
+ flags: {build: true, test: true, publish: true, ...overrides.flags},
106
+ root: {absPath: tmpDir, ...overrides.root},
107
+ packages: overrides.packages ?? {},
108
+ queue: overrides.queue ?? [],
109
+ prev: overrides.prev ?? {},
110
+ graphs: overrides.graphs ?? {},
111
+ report: overrides.report ?? makeReport(),
112
+ publishers: overrides.publishers ?? [],
113
+ run: overrides.run ?? (async () => {}),
114
+ git: {sha: 'abc1234567890', root: tmpDir, ...overrides.git},
115
+ })
116
+
117
+ export const makeReport = () => {
118
+ const events = []
119
+ return {
120
+ status: 'initial',
121
+ events,
122
+ packages: {},
123
+ log(...chunks) { events.push({level: 'info', msg: chunks}); return this },
124
+ warn(...chunks) { events.push({level: 'warn', msg: chunks}); return this },
125
+ error(...chunks) { events.push({level: 'error', msg: chunks}); return this },
126
+ set() { return this },
127
+ get() { return this },
128
+ setStatus() { return this },
129
+ getStatus() { return this },
130
+ save() { return this },
131
+ }
132
+ }
@@ -2,7 +2,7 @@ import {fs, path, tempy, $, sleep} from 'zx-extra'
2
2
  import {fileURLToPath} from 'node:url'
3
3
 
4
4
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
5
- export const fixtures = path.resolve(__dirname, '../fixtures')
5
+ export const fixtures = path.resolve(__dirname, '../../fixtures')
6
6
 
7
7
  export const createNpmRegistry = () => {
8
8
  let p
@@ -10,8 +10,8 @@ export const createNpmRegistry = () => {
10
10
  return {
11
11
  address: $.env.NPM_REGISTRY || 'http://localhost:4873',
12
12
  async start() {
13
- fs.removeSync(path.resolve(__dirname, '../../../storage'))
14
- const config = path.resolve(__dirname, '../../../verdaccio.config.yaml')
13
+ fs.removeSync(path.resolve(__dirname, '../../../../storage'))
14
+ const config = path.resolve(__dirname, '../../../../verdaccio.config.yaml')
15
15
  p = $({preferLocal: true})`verdaccio --config ${config}`
16
16
 
17
17
  return sleep(1000)