zx-bulk-release 2.20.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 +25 -0
- package/README.md +91 -74
- package/package.json +14 -7
- package/src/main/js/index.js +0 -6
- package/src/main/js/processor/api/gh.js +111 -0
- package/src/main/js/{api → processor/api}/git.js +17 -26
- package/src/main/js/{api → processor/api}/npm.js +70 -28
- package/src/main/js/processor/deps.js +1 -1
- package/src/main/js/processor/exec.js +4 -4
- package/src/main/js/processor/generators/meta.js +80 -0
- package/src/main/js/{api/changelog.js → processor/generators/notes.js} +3 -21
- package/src/main/js/processor/{meta.js → generators/tag.js} +3 -109
- package/src/main/js/processor/log.js +86 -0
- package/src/main/js/processor/publishers/changelog.js +26 -0
- package/src/main/js/processor/publishers/cmd.js +6 -0
- package/src/main/js/processor/publishers/gh-pages.js +32 -0
- package/src/main/js/processor/publishers/gh-release.js +41 -0
- package/src/main/js/processor/publishers/meta.js +58 -0
- package/src/main/js/processor/publishers/npm.js +15 -0
- package/src/main/js/processor/release.js +71 -66
- package/src/main/js/{steps → processor/steps}/analyze.js +18 -24
- package/src/main/js/processor/steps/build.js +20 -0
- package/src/main/js/processor/steps/clean.js +7 -0
- package/src/main/js/processor/steps/contextify.js +49 -0
- package/src/main/js/processor/steps/publish.js +39 -0
- package/src/main/js/processor/steps/teardown.js +58 -0
- package/src/main/js/processor/steps/test.js +10 -0
- package/src/main/js/util.js +32 -77
- 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/src/main/js/api/gh.js +0 -131
- package/src/main/js/log.js +0 -63
- package/src/main/js/steps/build.js +0 -23
- package/src/main/js/steps/clean.js +0 -7
- package/src/main/js/steps/contextify.js +0 -154
- package/src/main/js/steps/publish.js +0 -47
- package/src/main/js/steps/test.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
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
|
+
|
|
6
|
+
## [2.21.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.20.0...v2.21.0) (2026-04-10)
|
|
7
|
+
|
|
8
|
+
### Fixes & improvements
|
|
9
|
+
* docs: update badges ([c9fc1af](https://github.com/semrel-extra/zx-bulk-release/commit/c9fc1af8cfb3d177dc328d1934cb7e663c36f0ca))
|
|
10
|
+
* fix: fix memoize store ([7fddefa](https://github.com/semrel-extra/zx-bulk-release/commit/7fddefa590ded43907d059d3b58e235adff6fa17))
|
|
11
|
+
* refactor: enhance logger ([89d9cff](https://github.com/semrel-extra/zx-bulk-release/commit/89d9cff7727e2190e6490d51f759e7039a25cdb2))
|
|
12
|
+
* docs: mention testing along the change graph flow ([44fd065](https://github.com/semrel-extra/zx-bulk-release/commit/44fd06568d552ae19e753c91f6536978a9c1d28e))
|
|
13
|
+
* docs: add cmd tpl usage example ([d228de0](https://github.com/semrel-extra/zx-bulk-release/commit/d228de09e456799c9e113b36b971187ae7c3c1be))
|
|
14
|
+
* refactor: rearrange utils ([8beeab6](https://github.com/semrel-extra/zx-bulk-release/commit/8beeab6906e4500429cc165f05b39fc5d57e5a09))
|
|
15
|
+
* refactor: optimize npm ver assert (oidc) ([e3b5939](https://github.com/semrel-extra/zx-bulk-release/commit/e3b5939cfb26ab2c0d84bc4a09ebf5971eb2a33a))
|
|
16
|
+
* refactor: bind zx pkg mdc with logger ([fd763a4](https://github.com/semrel-extra/zx-bulk-release/commit/fd763a42a2229ccbc902936fbbfa4a38652685e5))
|
|
17
|
+
* refactor: align context injection flow ([6edb11b](https://github.com/semrel-extra/zx-bulk-release/commit/6edb11bfa669ae9519a134383f5299d77bc4f1a6))
|
|
18
|
+
* refactor: rearrange processor layers ([ea7ef42](https://github.com/semrel-extra/zx-bulk-release/commit/ea7ef42e5632691dfee8705a4f4ef73e0c166979))
|
|
19
|
+
* refactor: align internal publishers contract ([c16b6d8](https://github.com/semrel-extra/zx-bulk-release/commit/c16b6d8d11ade2d5d159936a457c43c0a2989141))
|
|
20
|
+
* refactor: ghFetch helper ([fc3517e](https://github.com/semrel-extra/zx-bulk-release/commit/fc3517e24e12cc28a63d1088d93cecb1114ce5b5))
|
|
21
|
+
* refactor: introduce `attempt` helper ([57f0d06](https://github.com/semrel-extra/zx-bulk-release/commit/57f0d06a093136e57600f2362ab46e16165c9b59))
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
* feat: add secrets masker ([fffea8b](https://github.com/semrel-extra/zx-bulk-release/commit/fffea8bfbebd6155e40f318a9303551ec4ae6e52))
|
|
25
|
+
|
|
1
26
|
## [2.20.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.19.1...v2.20.0) (2026-04-05)
|
|
2
27
|
|
|
3
28
|
### Features
|
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
|
|
@@ -74,7 +74,8 @@ await run({
|
|
|
74
74
|
Any [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) compliant format: `.releaserc`, `.release.json`, `.release.yaml`, etc in the package root or in the repo root dir.
|
|
75
75
|
```json
|
|
76
76
|
{
|
|
77
|
-
"
|
|
77
|
+
"buildCmd": "yarn && yarn build",
|
|
78
|
+
"testCmd": "yarn test",
|
|
78
79
|
"npmFetch": true,
|
|
79
80
|
"changelog": "changelog",
|
|
80
81
|
"ghPages": "gh-pages",
|
|
@@ -83,6 +84,17 @@ Any [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) compliant format
|
|
|
83
84
|
}
|
|
84
85
|
```
|
|
85
86
|
|
|
87
|
+
#### Command templating
|
|
88
|
+
`buildCmd`, `testCmd` and `publishCmd` support `${{ variable }}` interpolation. The template context includes all `pkg` fields and the release context (`flags`, `git`, `env`, etc):
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"buildCmd": "yarn build --pkg=${{name}} --ver=${{version}}",
|
|
92
|
+
"testCmd": "yarn test --scope=${{name}}",
|
|
93
|
+
"publishCmd": "echo releasing ${{name}}@${{version}}"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
Available variables include: `name`, `version`, `absPath`, `relPath`, and anything from `pkg.ctx` (e.g. `git.sha`, `git.root`, `flags.*`).
|
|
97
|
+
|
|
86
98
|
#### Changelog diff URLs
|
|
87
99
|
By default, changelog entries link to GitHub compare/commit pages. Override `diffTagUrl` and `diffCommitUrl` to customize for other platforms (e.g. Gerrit):
|
|
88
100
|
```json
|
|
@@ -135,62 +147,83 @@ OIDC mode is also auto-detected when `NPM_TOKEN` is not set and `ACTIONS_ID_TOKE
|
|
|
135
147
|
|
|
136
148
|
When OIDC is active, `NPM_TOKEN` and `NPMRC` are ignored for publishing and `--provenance` is enabled automatically.
|
|
137
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
|
+
|
|
194
|
+
### Selective testing along the change graph
|
|
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:
|
|
196
|
+
```shell
|
|
197
|
+
npx zx-bulk-release --dry-run --no-build
|
|
198
|
+
```
|
|
199
|
+
See [antongolub/misc](https://github.com/antongolub/misc) for a real-world example of this pattern.
|
|
200
|
+
|
|
138
201
|
## Demo
|
|
139
202
|
* [demo-zx-bulk-release](https://github.com/semrel-extra/demo-zx-bulk-release)
|
|
140
203
|
* [qiwi/pijma](https://github.com/qiwi/pijma)
|
|
204
|
+
* [antongolub/misc](https://github.com/antongolub/misc)
|
|
141
205
|
|
|
142
206
|
## Implementation notes
|
|
143
207
|
### Flow
|
|
144
|
-
```js
|
|
145
|
-
try {
|
|
146
|
-
const {packages, queue, root} = await topo({cwd, flags})
|
|
147
|
-
console.log('queue:', queue)
|
|
148
|
-
|
|
149
|
-
for (let name of queue) {
|
|
150
|
-
const pkg = packages[name]
|
|
151
|
-
|
|
152
|
-
await analyze(pkg, packages, root)
|
|
153
|
-
|
|
154
|
-
if (pkg.changes.length === 0) continue
|
|
155
|
-
|
|
156
|
-
await build(pkg, packages)
|
|
157
|
-
|
|
158
|
-
if (flags.dryRun) continue
|
|
159
|
-
|
|
160
|
-
await publish(pkg)
|
|
161
|
-
}
|
|
162
|
-
} catch (e) {
|
|
163
|
-
console.error(e)
|
|
164
|
-
throw e
|
|
165
|
-
}
|
|
166
208
|
```
|
|
209
|
+
topo ─► contextify ─► analyze ──► build ──► test ──► publish ─► clean
|
|
210
|
+
(per pkg) (per pkg) (per pkg) (per pkg) (per pkg)
|
|
211
|
+
```
|
|
212
|
+
[`@semrel-extra/topo`](https://github.com/semrel-extra/topo) resolves the release queue respecting dependency graphs. The graph allows parallel execution where the dependency tree permits; `memoizeBy` prevents duplicate work when a package is reached by multiple paths.
|
|
167
213
|
|
|
168
|
-
|
|
169
|
-
[Toposort](https://github.com/semrel-extra/topo) is used to resolve the pkg release queue.
|
|
170
|
-
By default, it omits the packages marked as `private`. You can override this by setting the `--include-private` flag.
|
|
171
|
-
|
|
172
|
-
### `analyze`
|
|
173
|
-
Determines pkg changes, release type, next version etc.
|
|
174
|
-
|
|
175
|
-
```js
|
|
176
|
-
export const analyze = async (pkg, packages, root) => {
|
|
177
|
-
pkg.config = await getPkgConfig(pkg.absPath, root.absPath)
|
|
178
|
-
pkg.latest = await getLatest(pkg)
|
|
179
|
-
|
|
180
|
-
const semanticChanges = await getSemanticChanges(pkg.absPath, pkg.latest.tag?.ref)
|
|
181
|
-
const depsChanges = await updateDeps(pkg, packages)
|
|
182
|
-
const changes = [...semanticChanges, ...depsChanges]
|
|
183
|
-
|
|
184
|
-
pkg.changes = changes
|
|
185
|
-
pkg.version = resolvePkgVersion(changes, pkg.latest.tag?.version || pkg.manifest.version)
|
|
186
|
-
pkg.manifest.version = pkg.version
|
|
214
|
+
By default, packages marked as `private` are omitted. Override with `--include-private`.
|
|
187
215
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
### Steps
|
|
217
|
+
Each step has a uniform signature `(pkg, ctx)`:
|
|
218
|
+
- **`contextify`** — resolves per-package config, latest release metadata, and git context.
|
|
219
|
+
- **`analyze`** — determines semantic changes, release type, and next version.
|
|
220
|
+
- **`build`** — runs `buildCmd` (with dep traversal and optional npm artifact fetch).
|
|
221
|
+
- **`test`** — runs `testCmd`.
|
|
222
|
+
- **`publish`** — orchestrates the publisher registry: prepare (serial) → run (parallel) → rollback on failure.
|
|
223
|
+
- **`clean`** — restores `package.json` files and unsets git user config.
|
|
191
224
|
|
|
192
225
|
Set `config.releaseRules` to override the default rules preset:
|
|
193
|
-
```
|
|
226
|
+
```js
|
|
194
227
|
[
|
|
195
228
|
{group: 'Features', releaseType: 'minor', prefixes: ['feat']},
|
|
196
229
|
{group: 'Fixes & improvements', releaseType: 'patch', prefixes: ['fix', 'perf', 'refactor', 'docs', 'patch']},
|
|
@@ -198,32 +231,16 @@ Set `config.releaseRules` to override the default rules preset:
|
|
|
198
231
|
]
|
|
199
232
|
```
|
|
200
233
|
|
|
201
|
-
###
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
// ...
|
|
211
|
-
}
|
|
212
|
-
```
|
|
234
|
+
### Publishers
|
|
235
|
+
Publish targets are a registry of `{name, when, prepare?, run, undo?, snapshot?}` objects:
|
|
236
|
+
- **meta** — pushes release metadata to the `meta` branch (or as a GH release asset).
|
|
237
|
+
- **npm** — publishes to the npm registry.
|
|
238
|
+
- **gh-release** — creates a GitHub release with optional file assets.
|
|
239
|
+
- **gh-pages** — pushes docs to a `gh-pages` branch.
|
|
240
|
+
- **changelog** — pushes a changelog entry to a `changelog` branch.
|
|
241
|
+
- **cmd** — runs a custom `publishCmd`.
|
|
213
242
|
|
|
214
|
-
|
|
215
|
-
Publish the pkg to git, npm, gh-pages, gh-release, etc.
|
|
216
|
-
```js
|
|
217
|
-
export const publish = async (pkg) => {
|
|
218
|
-
await fs.writeJson(pkg.manifestPath, pkg.manifest, {spaces: 2})
|
|
219
|
-
await pushTag(pkg)
|
|
220
|
-
await pushMeta(pkg)
|
|
221
|
-
await pushChangelog(pkg)
|
|
222
|
-
await npmPublish(pkg)
|
|
223
|
-
await ghRelease(pkg)
|
|
224
|
-
await ghPages(pkg)
|
|
225
|
-
}
|
|
226
|
-
```
|
|
243
|
+
Teardown walks the registry in reverse, calling `undo()` on each publisher for rollback/recovery.
|
|
227
244
|
|
|
228
245
|
### Tags
|
|
229
246
|
[Lerna](https://github.com/lerna/lerna) tags (like `@pkg/name@v1.0.0-beta.0`) are suitable for monorepos, but they don’t follow [semver spec](https://semver.org/). Therefore, we propose another contract:
|
package/package.json
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zx-bulk-release",
|
|
3
3
|
"alias": "bulk-release",
|
|
4
|
-
"version": "2.
|
|
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/
|
|
10
|
-
"./meta": "./src/main/js/meta.js"
|
|
9
|
+
"./test-utils": "./src/test/js/utils/repo.js",
|
|
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"
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
"c8": "^11.0.0",
|
|
36
38
|
"esbuild": "^0.28.0",
|
|
37
39
|
"uvu": "^0.5.6",
|
|
38
|
-
"verdaccio": "6.
|
|
40
|
+
"verdaccio": "6.4.0"
|
|
39
41
|
},
|
|
40
42
|
"publishConfig": {
|
|
41
43
|
"access": "public"
|
|
@@ -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
|
}
|
package/src/main/js/index.js
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Low-level GitHub API primitives. No domain knowledge, no imports from processor/ or steps/.
|
|
2
|
+
|
|
3
|
+
import {$, path, tempy, glob, fs, fetch} from 'zx-extra'
|
|
4
|
+
import {asArray, attempt2} from '../../util.js'
|
|
5
|
+
|
|
6
|
+
export const getCommonPath = files => {
|
|
7
|
+
const f0 = files[0]
|
|
8
|
+
const common = files.length === 1
|
|
9
|
+
? f0.lastIndexOf('/') + 1
|
|
10
|
+
: [...f0].findIndex((c, i) => files.some(f => f.charAt(i) !== c))
|
|
11
|
+
const p = f0.slice(0, common)
|
|
12
|
+
return p.endsWith('/') ? p : p.slice(0, p.lastIndexOf('/') + 1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GH_API_VERSION = '2022-11-28'
|
|
16
|
+
export const GH_ACCEPT = 'application/vnd.github.v3+json'
|
|
17
|
+
|
|
18
|
+
export const ghFetch = (url, {ghToken, method = 'GET', headers, body} = {}) => fetch(url, {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
Accept: GH_ACCEPT,
|
|
22
|
+
'X-GitHub-Api-Version': GH_API_VERSION,
|
|
23
|
+
...(ghToken ? {Authorization: `token ${ghToken}`} : {}),
|
|
24
|
+
...headers,
|
|
25
|
+
},
|
|
26
|
+
body,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
|
|
30
|
+
export const ghCreateRelease = async ({ghApiUrl, ghToken, repoName, tag, body}) => {
|
|
31
|
+
const res = await (await ghFetch(`${ghApiUrl}/repos/${repoName}/releases`, {
|
|
32
|
+
ghToken,
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body: JSON.stringify({name: tag, tag_name: tag, body}),
|
|
35
|
+
})).json()
|
|
36
|
+
|
|
37
|
+
if (!res.upload_url) {
|
|
38
|
+
throw new Error(`gh release failed: ${JSON.stringify(res)}`)
|
|
39
|
+
}
|
|
40
|
+
return res
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#delete-a-release
|
|
44
|
+
export const ghDeleteReleaseByTag = async ({ghApiUrl, ghToken, repoName, tag}) => {
|
|
45
|
+
const res = await attempt2(() => ghFetch(`${ghApiUrl}/repos/${repoName}/releases/tags/${tag}`, {ghToken}))
|
|
46
|
+
if (!res.ok) return false
|
|
47
|
+
const {id} = await res.json()
|
|
48
|
+
await attempt2(() => ghFetch(`${ghApiUrl}/repos/${repoName}/releases/${id}`, {ghToken, method: 'DELETE'}))
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
|
|
53
|
+
export const ghPrepareAssets = async (assets, _cwd) => {
|
|
54
|
+
const temp = tempy.temporaryDirectory()
|
|
55
|
+
|
|
56
|
+
await Promise.all(assets.map(async ({name, contents, source = 'target/**/*', zip, cwd = _cwd, strip = true}) => {
|
|
57
|
+
const target = path.join(temp, name)
|
|
58
|
+
|
|
59
|
+
if (contents) {
|
|
60
|
+
await fs.outputFile(target, contents, 'utf8')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const patterns = asArray(source)
|
|
65
|
+
if (patterns.some(s => s.includes('*'))) {
|
|
66
|
+
zip = true
|
|
67
|
+
}
|
|
68
|
+
const files = await glob(patterns, {cwd, absolute: false, onlyFiles: true})
|
|
69
|
+
|
|
70
|
+
if (files.length === 0) {
|
|
71
|
+
throw new Error(`gh asset not found: ${name} ${source}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!zip && files.length === 1) {
|
|
75
|
+
await fs.copy(path.join(cwd, files[0]), target)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
const prefix = getCommonPath(files)
|
|
79
|
+
|
|
80
|
+
return $.raw`tar -C ${path.join(cwd, prefix)} -cv${zip ? 'z' : ''}f ${target} ${files.map(f => f.slice(prefix.length)).join(' ')}`
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
return temp
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const ghUploadAssets = async ({ghToken, ghAssets, uploadUrl, cwd}) => {
|
|
87
|
+
const temp = await ghPrepareAssets(ghAssets, cwd)
|
|
88
|
+
|
|
89
|
+
return Promise.all(ghAssets.map(async ({name}) => {
|
|
90
|
+
const url = `${uploadUrl}?name=${name}`
|
|
91
|
+
const res = await ghFetch(url, {
|
|
92
|
+
ghToken,
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {'Content-Type': 'application/octet-stream'},
|
|
95
|
+
body: await fs.readFile(path.join(temp, name)),
|
|
96
|
+
})
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
throw new Error(`gh asset upload failed for '${name}': ${res.status}`)
|
|
99
|
+
}
|
|
100
|
+
return res
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const ghGetAsset = async ({repoName, tag, name, ghUrl}) => {
|
|
105
|
+
const url = `${ghUrl || 'https://github.com'}/${repoName}/releases/download/${tag.ref || tag}/${name}`
|
|
106
|
+
const res = await attempt2(() => fetch(url))
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(`gh asset fetch failed for '${name}': ${res.status} ${url}`)
|
|
109
|
+
}
|
|
110
|
+
return res.text()
|
|
111
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {$, fs, path, tempy, copy} from 'zx-extra'
|
|
2
2
|
import {log} from '../log.js'
|
|
3
|
-
import {memoizeBy} from '
|
|
3
|
+
import {attempt2, attempt3, memoizeBy} from '../../util.js'
|
|
4
4
|
|
|
5
5
|
export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, basicAuth}) => {
|
|
6
6
|
const origin = _origin || (await getRepo(_cwd, {basicAuth})).repoAuthedUrl
|
|
@@ -9,7 +9,7 @@ export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, b
|
|
|
9
9
|
try {
|
|
10
10
|
await _$`git clone --single-branch --branch ${branch} --depth 1 ${origin} .`
|
|
11
11
|
} catch (e) {
|
|
12
|
-
log
|
|
12
|
+
log.warn(`ref '${branch}' does not exist in ${origin}`)
|
|
13
13
|
await _$`git init . &&
|
|
14
14
|
git remote add origin ${origin}`
|
|
15
15
|
}
|
|
@@ -18,8 +18,6 @@ export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, b
|
|
|
18
18
|
}, async ({cwd, branch}) => `${await getRoot(cwd)}:${branch}`)
|
|
19
19
|
|
|
20
20
|
export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = [], basicAuth, gitCommitterEmail, gitCommitterName}) => {
|
|
21
|
-
let retries = 3
|
|
22
|
-
|
|
23
21
|
const _cwd = await fetchRepo({cwd, branch, origin, basicAuth})
|
|
24
22
|
const _$ = $({cwd: _cwd})
|
|
25
23
|
|
|
@@ -34,31 +32,24 @@ export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFile
|
|
|
34
32
|
await _$`git add . &&
|
|
35
33
|
git commit -m ${msg}`
|
|
36
34
|
} catch {
|
|
37
|
-
log
|
|
35
|
+
log.warn(`no changes to commit to ${branch}`)
|
|
38
36
|
return
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (retries === 0) {
|
|
49
|
-
throw e
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
await _$`git fetch origin ${branch} &&
|
|
53
|
-
git rebase origin/${branch}`
|
|
39
|
+
return attempt3(
|
|
40
|
+
() => _$`git push origin HEAD:refs/heads/${branch}`,
|
|
41
|
+
(e) => {
|
|
42
|
+
log.warn('git push failed, rebasing', 'branch', branch, e)
|
|
43
|
+
return attempt2(() => _$`git fetch origin ${branch} &&
|
|
44
|
+
git rebase origin/${branch}`)
|
|
54
45
|
}
|
|
55
|
-
|
|
46
|
+
)
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
export const getSha = async (cwd) => (await $({cwd})`git rev-parse HEAD`).toString().trim()
|
|
59
|
-
|
|
60
49
|
export const getRoot = memoizeBy(async (cwd) => (await $({cwd})`git rev-parse --show-toplevel`).toString().trim())
|
|
61
50
|
|
|
51
|
+
export const getSha = memoizeBy(async (cwd) => (await $({cwd})`git rev-parse HEAD`).toString().trim(), getRoot)
|
|
52
|
+
|
|
62
53
|
export const parseOrigin = (originUrl) => {
|
|
63
54
|
const [, , repoHost, repoName] = originUrl.replace(':', '/').replace(/\.git/, '').match(/.+(@|\/\/)([^/]+)\/(.+)$/) || []
|
|
64
55
|
|
|
@@ -102,10 +93,10 @@ export const getCommits = async (cwd, from, to = 'HEAD') => {
|
|
|
102
93
|
})
|
|
103
94
|
}
|
|
104
95
|
|
|
105
|
-
export const getTags =
|
|
106
|
-
(await $({cwd})`git tag -l ${ref
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
export const getTags = memoizeBy(
|
|
97
|
+
async (cwd, ref = '*') => (await $({cwd})`git tag -l ${ref}`).toString().split('\n'),
|
|
98
|
+
async (cwd, ref = '*') => `${await getRoot(cwd)}:${ref}`,
|
|
99
|
+
)
|
|
109
100
|
|
|
110
101
|
export const pushTag = async ({cwd, tag, gitCommitterName, gitCommitterEmail}) => {
|
|
111
102
|
await setUserConfig(cwd, gitCommitterName, gitCommitterEmail)
|
|
@@ -119,7 +110,7 @@ export const fetchTags = async (cwd) =>
|
|
|
119
110
|
$({cwd})`git fetch --tags`
|
|
120
111
|
|
|
121
112
|
export const deleteRemoteTag = async ({cwd, tag}) => {
|
|
122
|
-
log(
|
|
113
|
+
log.info(`rolling back remote tag '${tag}'`)
|
|
123
114
|
await $({cwd, nothrow: true})`git push origin :refs/tags/${tag}`
|
|
124
115
|
await $({cwd, nothrow: true})`git tag -d ${tag}`
|
|
125
116
|
}
|