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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -74
  3. package/package.json +14 -7
  4. package/src/main/js/index.js +0 -6
  5. package/src/main/js/processor/api/gh.js +111 -0
  6. package/src/main/js/{api → processor/api}/git.js +17 -26
  7. package/src/main/js/{api → processor/api}/npm.js +70 -28
  8. package/src/main/js/processor/deps.js +1 -1
  9. package/src/main/js/processor/exec.js +4 -4
  10. package/src/main/js/processor/generators/meta.js +80 -0
  11. package/src/main/js/{api/changelog.js → processor/generators/notes.js} +3 -21
  12. package/src/main/js/processor/{meta.js → generators/tag.js} +3 -109
  13. package/src/main/js/processor/log.js +86 -0
  14. package/src/main/js/processor/publishers/changelog.js +26 -0
  15. package/src/main/js/processor/publishers/cmd.js +6 -0
  16. package/src/main/js/processor/publishers/gh-pages.js +32 -0
  17. package/src/main/js/processor/publishers/gh-release.js +41 -0
  18. package/src/main/js/processor/publishers/meta.js +58 -0
  19. package/src/main/js/processor/publishers/npm.js +15 -0
  20. package/src/main/js/processor/release.js +71 -66
  21. package/src/main/js/{steps → processor/steps}/analyze.js +18 -24
  22. package/src/main/js/processor/steps/build.js +20 -0
  23. package/src/main/js/processor/steps/clean.js +7 -0
  24. package/src/main/js/processor/steps/contextify.js +49 -0
  25. package/src/main/js/processor/steps/publish.js +39 -0
  26. package/src/main/js/processor/steps/teardown.js +58 -0
  27. package/src/main/js/processor/steps/test.js +10 -0
  28. package/src/main/js/util.js +32 -77
  29. package/src/test/js/utils/gh-server.js +33 -0
  30. package/src/test/js/utils/mock.js +132 -0
  31. package/src/test/js/{test-utils.js → utils/repo.js} +3 -3
  32. package/src/main/js/api/gh.js +0 -131
  33. package/src/main/js/log.js +0 -63
  34. package/src/main/js/steps/build.js +0 -23
  35. package/src/main/js/steps/clean.js +0 -7
  36. package/src/main/js/steps/contextify.js +0 -154
  37. package/src/main/js/steps/publish.js +0 -47
  38. 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
- [![CI](https://github.com/semrel-extra/zx-bulk-release/workflows/CI/badge.svg)](https://github.com/semrel-extra/zx-bulk-release/actions)
5
- [![Maintainability](https://api.codeclimate.com/v1/badges/bb94e929b1b6430781b5/maintainability)](https://codeclimate.com/github/semrel-extra/zx-bulk-release/maintainability)
6
- [![Test Coverage](https://api.codeclimate.com/v1/badges/bb94e929b1b6430781b5/test_coverage)](https://codeclimate.com/github/semrel-extra/zx-bulk-release/test_coverage)
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
@@ -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
- "cmd": "yarn && yarn build && yarn test",
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
- ### `topo`
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
- console.log(`[${pkg.name}] semantic changes`, changes)
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
- ```ts
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
- ### `build`
202
- Applies `config.cmd` to build pkg assets: bundles, docs, etc.
203
- ```js
204
- export const build = async (pkg, packages) => {
205
- // ...
206
- if (!pkg.fetched && config.cmd) {
207
- console.log(`[${pkg.name}] run cmd '${config.cmd}'`)
208
- await $.o({cwd: pkg.absPath, quote: v => v})`${config.cmd}`
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
- ### `publish`
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.20.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",
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/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"
@@ -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.3.2"
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
  }
@@ -1,7 +1 @@
1
- import process from 'node:process'
2
- import { $ } from 'zx-extra'
3
-
4
- $.quiet = !process.env.DEBUG
5
- $.verbose = !!process.env.DEBUG
6
-
7
1
  export {run} from './processor/release.js'
@@ -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 '../util.js'
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({level: 'warn'})(`ref '${branch}' does not exist in ${origin}`)
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({level: 'warn'})(`no changes to commit to ${branch}`)
35
+ log.warn(`no changes to commit to ${branch}`)
38
36
  return
39
37
  }
40
38
 
41
- while (retries > 0) {
42
- try {
43
- return await _$`git push origin HEAD:refs/heads/${branch}`
44
- } catch (e) {
45
- retries -= 1
46
- log({level: 'error'})('git push failed', 'branch', branch, 'retries left', retries, e)
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 = async (cwd, ref) =>
106
- (await $({cwd})`git tag -l ${ref || '*'}`)
107
- .toString()
108
- .split('\n')
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()(`rolling back remote tag '${tag}'`)
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
  }