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 +5 -0
- package/README.md +47 -3
- package/package.json +12 -5
- package/src/test/js/utils/gh-server.js +33 -0
- package/src/test/js/utils/mock.js +132 -0
- package/src/test/js/{test-utils.js → utils/repo.js} +3 -3
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
|
-
[](https://github.com/semrel-extra/zx-bulk-release/actions/workflows/ci.yml)
|
|
5
|
+
[](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release)
|
|
6
|
+
[](https://qlty.sh/gh/semrel-extra/projects/zx-bulk-release)
|
|
7
7
|
[](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.
|
|
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/
|
|
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/
|
|
15
|
+
"src/test/js/utils",
|
|
16
16
|
"CHANGELOG.md",
|
|
17
17
|
"LICENSE",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "
|
|
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, '
|
|
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, '
|
|
14
|
-
const config = path.resolve(__dirname, '
|
|
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)
|