zx-bulk-release 2.21.1 → 3.0.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 +22 -0
- package/README.md +98 -26
- package/package.json +1 -1
- package/src/main/js/index.js +1 -1
- package/src/main/js/{processor → post}/api/gh.js +0 -27
- package/src/main/js/{processor → post}/api/git.js +2 -10
- package/src/main/js/{processor → post}/api/npm.js +4 -2
- package/src/main/js/post/courier/channels/changelog.js +29 -0
- package/src/main/js/{processor/publishers → post/courier/channels}/cmd.js +1 -0
- package/src/main/js/post/courier/channels/gh-pages.js +30 -0
- package/src/main/js/post/courier/channels/gh-release.js +35 -0
- package/src/main/js/post/courier/channels/meta.js +34 -0
- package/src/main/js/post/courier/channels/npm.js +26 -0
- package/src/main/js/post/courier/index.js +113 -0
- package/src/main/js/post/courier/parcel.js +77 -0
- package/src/main/js/{processor → post/depot}/exec.js +2 -2
- package/src/main/js/{processor → post/depot}/generators/meta.js +3 -3
- package/src/main/js/{processor → post/depot}/generators/notes.js +1 -1
- package/src/main/js/{processor → post/depot}/steps/analyze.js +2 -2
- package/src/main/js/{processor → post/depot}/steps/build.js +2 -2
- package/src/main/js/{processor → post/depot}/steps/clean.js +2 -2
- package/src/main/js/{processor → post/depot}/steps/contextify.js +3 -3
- package/src/main/js/post/depot/steps/pack.js +59 -0
- package/src/main/js/post/depot/steps/publish.js +29 -0
- package/src/main/js/{processor → post/depot}/steps/test.js +1 -1
- package/src/main/js/{processor → post}/release.js +44 -37
- package/src/main/js/post/tar.js +73 -0
- package/src/test/js/utils/mock.js +1 -1
- package/src/main/js/processor/publishers/changelog.js +0 -26
- package/src/main/js/processor/publishers/gh-pages.js +0 -32
- package/src/main/js/processor/publishers/gh-release.js +0 -41
- package/src/main/js/processor/publishers/meta.js +0 -58
- package/src/main/js/processor/publishers/npm.js +0 -15
- package/src/main/js/processor/steps/publish.js +0 -39
- package/src/main/js/processor/steps/teardown.js +0 -58
- /package/src/main/js/{processor → post/depot}/deps.js +0 -0
- /package/src/main/js/{processor → post/depot}/generators/tag.js +0 -0
- /package/src/main/js/{processor → post}/log.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## [3.0.1](https://github.com/semrel-extra/zx-bulk-release/compare/v3.0.0...v3.0.1) (2026-04-11)
|
|
2
|
+
|
|
3
|
+
### Fixes & improvements
|
|
4
|
+
* docs: add v2 > v3 migration guide ([18f68fa](https://github.com/semrel-extra/zx-bulk-release/commit/18f68fa62d700ae48f0f2da19b42bf91b23b5d14))
|
|
5
|
+
|
|
6
|
+
## [3.0.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.21.1...v3.0.0) (2026-04-11)
|
|
7
|
+
|
|
8
|
+
### Fixes & improvements
|
|
9
|
+
* refactor: enhance courier inners ([0a75468](https://github.com/semrel-extra/zx-bulk-release/commit/0a75468a1736291d194e307c8fef39cbaae55948))
|
|
10
|
+
* refactor: use `parcels` as default artifacts dir ([b1c589e](https://github.com/semrel-extra/zx-bulk-release/commit/b1c589edfcdfba2c548c09f35950b058e6cc2f8c))
|
|
11
|
+
* refactor: simplify parcel contracts ([0a93948](https://github.com/semrel-extra/zx-bulk-release/commit/0a93948b2b4013ad3a620bcf93cf468651c0380b))
|
|
12
|
+
* refactor: move git-tag phase to depot ([a736ead](https://github.com/semrel-extra/zx-bulk-release/commit/a736ead83a75014f4bf592b9b60d5a5073bb4381))
|
|
13
|
+
* refactor: extract pack step from publish ([213a286](https://github.com/semrel-extra/zx-bulk-release/commit/213a28603ccfd3a9deed0d48652bca2864e17ef9))
|
|
14
|
+
* refactor: lift api/ to post/ level as shared ([bc51e2c](https://github.com/semrel-extra/zx-bulk-release/commit/bc51e2c8e9517261de5107a7b67680018f8fe71d))
|
|
15
|
+
* refactor: extract courier module with sealed directive ([20617d8](https://github.com/semrel-extra/zx-bulk-release/commit/20617d83f87b0409963b178b8a34fd11ad22df33))
|
|
16
|
+
|
|
17
|
+
### BREAKING CHANGES
|
|
18
|
+
* - --recover flag removed. Use --deliver <dir> to re-deliver pre-packed tars. ([c45ab47](https://github.com/semrel-extra/zx-bulk-release/commit/c45ab47a853a887f45abe6a0fa27eade0f71c4dc))
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
* feat: seal delivery flow — tar containers, template credentials ([2ba819a](https://github.com/semrel-extra/zx-bulk-release/commit/2ba819aef7e2d74f55db6bd6fc05f6589ddbc700))
|
|
22
|
+
|
|
1
23
|
## [2.21.1](https://github.com/semrel-extra/zx-bulk-release/compare/v2.21.0...v2.21.1) (2026-04-11)
|
|
2
24
|
|
|
3
25
|
### Fixes & improvements
|
package/README.md
CHANGED
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
* Pkg changelogs go to `changelog` branch (configurable).
|
|
15
15
|
* Docs are published to `gh-pages` branch (configurable).
|
|
16
16
|
* No extra builds. The required deps are fetched from the pkg registry (`npmFetch` config opt).
|
|
17
|
+
* **Two-phase pipeline**: build and delivery can run as separate jobs with isolated credentials.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
> [!NOTE]
|
|
21
|
+
> **[Migration guide v2 → v3](./MIGRATION_V2_V3.md)**
|
|
17
22
|
|
|
18
23
|
## Roadmap
|
|
19
24
|
* [x] Store release metrics to `meta`.
|
|
20
|
-
* [
|
|
25
|
+
* [x] Two-phase pipeline (pack / deliver).
|
|
21
26
|
* [ ] Multistack. Add support for java/kt/py.
|
|
22
27
|
* [ ] Semaphore. Let several release agents to serve the monorepo at the same time.
|
|
23
28
|
|
|
@@ -25,7 +30,6 @@
|
|
|
25
30
|
* macOS / linux
|
|
26
31
|
* Node.js >= 16.0.0
|
|
27
32
|
* npm >=7 / yarn >= 3
|
|
28
|
-
* ~~wget~~
|
|
29
33
|
* tar
|
|
30
34
|
* git
|
|
31
35
|
|
|
@@ -38,21 +42,56 @@ yarn add zx-bulk-release
|
|
|
38
42
|
```shell
|
|
39
43
|
GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
|
|
40
44
|
```
|
|
41
|
-
| Flag | Description
|
|
42
|
-
|
|
43
|
-
| `--
|
|
44
|
-
| `--
|
|
45
|
-
| `--
|
|
46
|
-
| `--
|
|
47
|
-
| `--
|
|
48
|
-
| `--no-
|
|
49
|
-
| `--
|
|
50
|
-
| `--
|
|
51
|
-
| `--
|
|
52
|
-
| `--
|
|
53
|
-
| `--
|
|
54
|
-
| `--
|
|
55
|
-
| `--
|
|
45
|
+
| Flag | Description | Default |
|
|
46
|
+
|------------------------------|---------------------------------------------------------------------------------------------------|------------------|
|
|
47
|
+
| `--pack [dir]` | Pack only: build, test, and write delivery tars to `dir`. No credentials needed. | `parcels` |
|
|
48
|
+
| `--deliver [dir]` | Deliver only: read tars from `dir` and run delivery channels. No source code needed. | `parcels` |
|
|
49
|
+
| `--ignore` | Packages to ignore: `a, b` | |
|
|
50
|
+
| `--include-private` | Include `private` packages | `false` |
|
|
51
|
+
| `--concurrency` | `build/publish` threads limit | `os.cpus.length` |
|
|
52
|
+
| `--no-build` | Skip `buildCmd` invoke | |
|
|
53
|
+
| `--no-test` | Disable `testCmd` run | |
|
|
54
|
+
| `--no-npm-fetch` | Disable npm artifacts fetching | |
|
|
55
|
+
| `--only-workspace-deps` | Recognize only `workspace:` deps as graph edges | |
|
|
56
|
+
| `--dry-run` / `--no-publish` | Disable any publish logic | |
|
|
57
|
+
| `--report` | Persist release state to file | |
|
|
58
|
+
| `--snapshot` | Publish only to `npm` snapshot channel and run `publishCmd` (if defined), skip everything else | |
|
|
59
|
+
| `--debug` | Enable [zx](https://github.com/google/zx#verbose) verbose mode | |
|
|
60
|
+
| `--version` / `-v` | Print own version | |
|
|
61
|
+
|
|
62
|
+
### Two-phase pipeline
|
|
63
|
+
By default, zbr runs the full pipeline in a single process. For better security isolation, split build and delivery into separate CI jobs:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
# Job 1: build (minimal privileges — source code access only)
|
|
67
|
+
jobs:
|
|
68
|
+
pack:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
with: { fetch-depth: 0 }
|
|
73
|
+
- run: npx zx-bulk-release --pack
|
|
74
|
+
- uses: actions/upload-artifact@v4
|
|
75
|
+
with: { name: parcels, path: parcels, retention-days: 1 }
|
|
76
|
+
|
|
77
|
+
# Job 2: deliver (only delivery credentials, no source code)
|
|
78
|
+
deliver:
|
|
79
|
+
needs: pack
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/download-artifact@v4
|
|
83
|
+
with: { name: parcels, path: parcels }
|
|
84
|
+
- run: npx zx-bulk-release --deliver
|
|
85
|
+
env:
|
|
86
|
+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
87
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
88
|
+
- uses: actions/upload-artifact@v4
|
|
89
|
+
with: { name: parcels, path: parcels, overwrite: true, retention-days: 1 }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
After delivery, each tar is replaced with a marker (`released` or `skip`). The final `upload-artifact` syncs these markers back to CI storage, so a re-run of the deliver job will skip already-delivered parcels. Artifacts expire naturally via retention policy.
|
|
93
|
+
|
|
94
|
+
**Recovery** is simply re-running the deliver job. Only undelivered tars (not yet replaced with markers) will be processed. No rebuild required.
|
|
56
95
|
|
|
57
96
|
### JS API
|
|
58
97
|
```js
|
|
@@ -153,7 +192,7 @@ The `--snapshot` flag publishes packages to the `snapshot` npm dist-tag with a p
|
|
|
153
192
|
**What snapshot does differently:**
|
|
154
193
|
- Version gets a `-snap.<short-sha>` suffix instead of a clean bump
|
|
155
194
|
- Git release tags are **not** pushed
|
|
156
|
-
- Only `npm` and `publishCmd`
|
|
195
|
+
- Only `npm` and `publishCmd` channels run (no gh-release, no changelog, no gh-pages, no meta)
|
|
157
196
|
- npm tag is `snapshot` instead of `latest`
|
|
158
197
|
|
|
159
198
|
**Workflow example** (`.github/workflows/snapshot.yml`):
|
|
@@ -204,10 +243,22 @@ See [antongolub/misc](https://github.com/antongolub/misc) for a real-world examp
|
|
|
204
243
|
* [antongolub/misc](https://github.com/antongolub/misc)
|
|
205
244
|
|
|
206
245
|
## Implementation notes
|
|
246
|
+
### Architecture
|
|
247
|
+
The release pipeline is split into three subsystems under `post/`:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
post/
|
|
251
|
+
depot/ — preparation: analysis, versioning, building, testing, tar packing
|
|
252
|
+
courier/ — sealed delivery: receives self-describing tars and delivers through channels
|
|
253
|
+
api/ — shared infrastructure wrappers (git, npm, gh)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This separation ensures that courier never touches the project directory — it works only with pre-packed tars and credentials resolved at delivery time. The two subsystems can run in separate CI jobs with different privilege levels.
|
|
257
|
+
|
|
207
258
|
### Flow
|
|
208
259
|
```
|
|
209
|
-
topo ─► contextify ─► analyze ──► build ──► test ──► publish ─► clean
|
|
210
|
-
(per pkg) (per pkg) (per pkg) (per pkg) (per pkg)
|
|
260
|
+
topo ─► contextify ─► analyze ──► build ──► test ──► pack ──► publish ─► clean
|
|
261
|
+
(per pkg) (per pkg) (per pkg) (per pkg) (per pkg) (per pkg)
|
|
211
262
|
```
|
|
212
263
|
[`@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.
|
|
213
264
|
|
|
@@ -219,7 +270,8 @@ Each step has a uniform signature `(pkg, ctx)`:
|
|
|
219
270
|
- **`analyze`** — determines semantic changes, release type, and next version.
|
|
220
271
|
- **`build`** — runs `buildCmd` (with dep traversal and optional npm artifact fetch).
|
|
221
272
|
- **`test`** — runs `testCmd`.
|
|
222
|
-
- **`
|
|
273
|
+
- **`pack`** — stages delivery artifacts into self-describing tar containers (`npm pack`, docs copy, assets, release notes). Each tar is named `{tag}.{channel}.{hash8}.tar` and contains a `manifest.json` with channel name, delivery instructions, and template credentials (`${{ENV_VAR}}`). After this step, everything the courier needs is outside the project dir.
|
|
274
|
+
- **`publish`** — pushes the release tag (commitment point), hands tars to courier's `deliver()`, runs `cmd` channel separately.
|
|
223
275
|
- **`clean`** — restores `package.json` files and unsets git user config.
|
|
224
276
|
|
|
225
277
|
Set `config.releaseRules` to override the default rules preset:
|
|
@@ -231,19 +283,39 @@ Set `config.releaseRules` to override the default rules preset:
|
|
|
231
283
|
]
|
|
232
284
|
```
|
|
233
285
|
|
|
234
|
-
###
|
|
235
|
-
|
|
286
|
+
### Tar containers
|
|
287
|
+
Each delivery artifact is a self-describing tar archive:
|
|
288
|
+
```
|
|
289
|
+
{tag}.{channel}.{hash8}.tar
|
|
290
|
+
manifest.json — channel name, delivery params, template credentials
|
|
291
|
+
package.tgz — (npm channel) npm tarball
|
|
292
|
+
assets/ — (gh-release channel) release assets
|
|
293
|
+
docs/ — (gh-pages channel) documentation files
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The manifest contains `${{ENV_VAR}}` placeholders that are resolved by the courier at delivery time via `resolveManifest()`. This ensures credentials never touch the build phase.
|
|
297
|
+
|
|
298
|
+
### Channels
|
|
299
|
+
Delivery channels are a registry of `{name, when, prepare?, run, requires?, snapshot?, transport?}` objects:
|
|
236
300
|
- **meta** — pushes release metadata to the `meta` branch (or as a GH release asset).
|
|
237
301
|
- **npm** — publishes to the npm registry.
|
|
238
302
|
- **gh-release** — creates a GitHub release with optional file assets.
|
|
239
303
|
- **gh-pages** — pushes docs to a `gh-pages` branch.
|
|
240
304
|
- **changelog** — pushes a changelog entry to a `changelog` branch.
|
|
241
|
-
- **cmd** — runs a custom `publishCmd
|
|
305
|
+
- **cmd** — runs a custom `publishCmd` (depot-side, not through courier; `transport: false`).
|
|
242
306
|
|
|
243
|
-
|
|
307
|
+
Each channel declares `requires` — a list of manifest fields that must be present after credential resolution. Courier validates all parcels before running any channel (fail-fast).
|
|
308
|
+
|
|
309
|
+
### Credential flow
|
|
310
|
+
```
|
|
311
|
+
depot (build phase) courier (deliver phase)
|
|
312
|
+
manifest: { token: '${{NPM_TOKEN}}' } → resolveManifest(manifest, env)
|
|
313
|
+
→ { token: 'actual-secret' }
|
|
314
|
+
```
|
|
315
|
+
Template credentials (`${{ENV_VAR}}`) are written into manifests at pack time. Courier resolves them from `process.env` at delivery time. This means the build phase never sees real secrets.
|
|
244
316
|
|
|
245
317
|
### Tags
|
|
246
|
-
[Lerna](https://github.com/lerna/lerna) tags (like `@pkg/name@v1.0.0-beta.0`) are suitable for monorepos, but they don
|
|
318
|
+
[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:
|
|
247
319
|
```js
|
|
248
320
|
'2022.6.13-optional-org.pkg-name.v1.0.0-beta.1+sha.1-f0'
|
|
249
321
|
// date name version format
|
package/package.json
CHANGED
package/src/main/js/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {run} from './
|
|
1
|
+
export {run} from './post/release.js'
|
|
@@ -40,15 +40,6 @@ export const ghCreateRelease = async ({ghApiUrl, ghToken, repoName, tag, body})
|
|
|
40
40
|
return res
|
|
41
41
|
}
|
|
42
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
43
|
// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
|
|
53
44
|
export const ghPrepareAssets = async (assets, _cwd) => {
|
|
54
45
|
const temp = tempy.temporaryDirectory()
|
|
@@ -83,24 +74,6 @@ export const ghPrepareAssets = async (assets, _cwd) => {
|
|
|
83
74
|
return temp
|
|
84
75
|
}
|
|
85
76
|
|
|
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
77
|
export const ghGetAsset = async ({repoName, tag, name, ghUrl}) => {
|
|
105
78
|
const url = `${ghUrl || 'https://github.com'}/${repoName}/releases/download/${tag.ref || tag}/${name}`
|
|
106
79
|
const res = await attempt2(() => fetch(url))
|
|
@@ -3,6 +3,7 @@ import {log} from '../log.js'
|
|
|
3
3
|
import {attempt2, attempt3, memoizeBy} from '../../util.js'
|
|
4
4
|
|
|
5
5
|
export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, basicAuth}) => {
|
|
6
|
+
if (!_origin && !_cwd) throw new Error('fetchRepo requires either origin or cwd')
|
|
6
7
|
const origin = _origin || (await getRepo(_cwd, {basicAuth})).repoAuthedUrl
|
|
7
8
|
const cwd = tempy.temporaryDirectory()
|
|
8
9
|
const _$ = $({cwd})
|
|
@@ -15,7 +16,7 @@ export const fetchRepo = memoizeBy(async ({cwd: _cwd, branch, origin: _origin, b
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
return cwd
|
|
18
|
-
}, async ({cwd, branch}) => `${await getRoot(cwd)}:${branch}`)
|
|
19
|
+
}, async ({cwd, branch, origin}) => origin ? `${origin}:${branch}` : `${await getRoot(cwd)}:${branch}`)
|
|
19
20
|
|
|
20
21
|
export const pushCommit = async ({cwd, from, to, branch, origin, msg, ignoreFiles, files = [], basicAuth, gitCommitterEmail, gitCommitterName}) => {
|
|
21
22
|
const _cwd = await fetchRepo({cwd, branch, origin, basicAuth})
|
|
@@ -106,15 +107,6 @@ export const pushTag = async ({cwd, tag, gitCommitterName, gitCommitterEmail}) =
|
|
|
106
107
|
git push origin ${tag}`
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
export const fetchTags = async (cwd) =>
|
|
110
|
-
$({cwd})`git fetch --tags`
|
|
111
|
-
|
|
112
|
-
export const deleteRemoteTag = async ({cwd, tag}) => {
|
|
113
|
-
log.info(`rolling back remote tag '${tag}'`)
|
|
114
|
-
await $({cwd, nothrow: true})`git push origin :refs/tags/${tag}`
|
|
115
|
-
await $({cwd, nothrow: true})`git tag -d ${tag}`
|
|
116
|
-
}
|
|
117
|
-
|
|
118
110
|
// Memoize prevents .git/config lock
|
|
119
111
|
// https://github.com/qiwi/packasso/actions/runs/4539987310/jobs/8000403413#step:7:282
|
|
120
112
|
export const setUserConfig = memoizeBy(async(cwd, gitCommitterName, gitCommitterEmail) => $({cwd})`
|
|
@@ -77,7 +77,7 @@ export const npmRestore = async (pkg) => {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export const npmPublish = async (pkg) => {
|
|
80
|
-
const {
|
|
80
|
+
const {name, version, manifest, config: {npmPublish, npmRegistry, npmToken, npmConfig, npmProvenance, npmOidc}} = pkg
|
|
81
81
|
|
|
82
82
|
if (manifest.private || npmPublish === false) return
|
|
83
83
|
|
|
@@ -105,7 +105,9 @@ export const npmPublish = async (pkg) => {
|
|
|
105
105
|
if (npmProvenance) npmFlags.push('--provenance')
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// Publish from pre-built tarball (courier mode) or project dir (legacy).
|
|
109
|
+
const target = pkg.npmTarball || pkg.absPath
|
|
110
|
+
await $`npm publish ${target} ${npmFlags.filter(Boolean)}`
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export const getNpmrc = memoizeBy(async ({npmConfig, npmToken, npmRegistry}) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {$} from 'zx-extra'
|
|
2
|
+
import {queuefy} from 'queuefy'
|
|
3
|
+
import {fetchRepo, pushCommit} from '../../api/git.js'
|
|
4
|
+
import {log} from '../../log.js'
|
|
5
|
+
|
|
6
|
+
const run = queuefy(async (manifest, dir) => {
|
|
7
|
+
const {releaseNotes, branch, file, msg, repoAuthedUrl, gitCommitterEmail, gitCommitterName, ghBasicAuth} = manifest
|
|
8
|
+
|
|
9
|
+
log.info('push changelog')
|
|
10
|
+
|
|
11
|
+
const _cwd = await fetchRepo({branch, origin: repoAuthedUrl, basicAuth: ghBasicAuth})
|
|
12
|
+
|
|
13
|
+
await $({cwd: _cwd})`echo ${releaseNotes}"\n$(cat ./${file})" > ./${file}`
|
|
14
|
+
await pushCommit({
|
|
15
|
+
branch,
|
|
16
|
+
msg,
|
|
17
|
+
origin: repoAuthedUrl,
|
|
18
|
+
gitCommitterEmail,
|
|
19
|
+
gitCommitterName,
|
|
20
|
+
basicAuth: ghBasicAuth,
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
name: 'changelog',
|
|
26
|
+
requires: ['repoAuthedUrl'],
|
|
27
|
+
when: (pkg) => !!pkg.config.changelog,
|
|
28
|
+
run,
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {path} from 'zx-extra'
|
|
2
|
+
import {queuefy} from 'queuefy'
|
|
3
|
+
import {log} from '../../log.js'
|
|
4
|
+
import {pushCommit} from '../../api/git.js'
|
|
5
|
+
|
|
6
|
+
const run = queuefy(async (manifest, dir) => {
|
|
7
|
+
const {branch, to, msg, repoAuthedUrl, gitCommitterEmail, gitCommitterName, ghBasicAuth} = manifest
|
|
8
|
+
const docsDir = path.join(dir, 'docs')
|
|
9
|
+
|
|
10
|
+
log.info(`publish docs to ${branch}`)
|
|
11
|
+
|
|
12
|
+
await pushCommit({
|
|
13
|
+
cwd: docsDir,
|
|
14
|
+
from: '.',
|
|
15
|
+
to,
|
|
16
|
+
branch,
|
|
17
|
+
origin: repoAuthedUrl,
|
|
18
|
+
msg,
|
|
19
|
+
gitCommitterEmail,
|
|
20
|
+
gitCommitterName,
|
|
21
|
+
basicAuth: ghBasicAuth,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
name: 'gh-pages',
|
|
27
|
+
requires: ['repoAuthedUrl'],
|
|
28
|
+
when: (pkg) => !!pkg.config.ghPages,
|
|
29
|
+
run,
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {fs, path} from 'zx-extra'
|
|
2
|
+
import {log} from '../../log.js'
|
|
3
|
+
import {ghCreateRelease, ghFetch} from '../../api/gh.js'
|
|
4
|
+
|
|
5
|
+
const run = async (manifest, dir) => {
|
|
6
|
+
const {tag, token, apiUrl, repoName, releaseNotes, assets} = manifest
|
|
7
|
+
if (!token) return null
|
|
8
|
+
|
|
9
|
+
log.info('create gh release')
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
|
|
12
|
+
const res = await ghCreateRelease({ghApiUrl: apiUrl, ghToken: token, repoName, tag, body: releaseNotes})
|
|
13
|
+
|
|
14
|
+
if (assets?.length) {
|
|
15
|
+
const uploadUrl = res.upload_url.slice(0, res.upload_url.indexOf('{'))
|
|
16
|
+
const assetsDir = path.join(dir, 'assets')
|
|
17
|
+
if (await fs.pathExists(assetsDir)) {
|
|
18
|
+
await Promise.all(assets.map(async ({name}) => {
|
|
19
|
+
const url = `${uploadUrl}?name=${name}`
|
|
20
|
+
const body = await fs.readFile(path.join(assetsDir, name))
|
|
21
|
+
const r = await ghFetch(url, {ghToken: token, method: 'POST', headers: {'Content-Type': 'application/octet-stream'}, body})
|
|
22
|
+
if (!r.ok) throw new Error(`gh asset upload failed for '${name}': ${r.status}`)
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log.info(`duration gh release: ${Date.now() - now}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
name: 'gh-release',
|
|
32
|
+
requires: ['token', 'repoName'],
|
|
33
|
+
when: (pkg) => !!pkg.config.ghToken,
|
|
34
|
+
run,
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {queuefy} from 'queuefy'
|
|
2
|
+
import {log} from '../../log.js'
|
|
3
|
+
import {pushCommit} from '../../api/git.js'
|
|
4
|
+
import {getArtifactPath, isAssetMode} from '../../depot/generators/meta.js'
|
|
5
|
+
import {prepareMeta} from '../../depot/generators/meta.js'
|
|
6
|
+
|
|
7
|
+
const pushMetaBranch = queuefy(async (manifest, dir) => {
|
|
8
|
+
const {name, version, tag, type, data: meta, repoAuthedUrl, gitCommitterEmail, gitCommitterName, ghBasicAuth} = manifest
|
|
9
|
+
if (type === null || isAssetMode(type)) return
|
|
10
|
+
|
|
11
|
+
log.info('push artifact to branch \'meta\'')
|
|
12
|
+
|
|
13
|
+
const msg = `chore: release meta ${name} ${version}`
|
|
14
|
+
const files = [{relpath: `${getArtifactPath(tag)}.json`, contents: meta}]
|
|
15
|
+
|
|
16
|
+
await pushCommit({
|
|
17
|
+
to: '.',
|
|
18
|
+
branch: 'meta',
|
|
19
|
+
msg,
|
|
20
|
+
files,
|
|
21
|
+
origin: repoAuthedUrl,
|
|
22
|
+
gitCommitterEmail,
|
|
23
|
+
gitCommitterName,
|
|
24
|
+
basicAuth: ghBasicAuth,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
name: 'meta',
|
|
30
|
+
requires: ['repoAuthedUrl'],
|
|
31
|
+
when: (pkg) => pkg.config.meta.type !== null,
|
|
32
|
+
prepare: prepareMeta,
|
|
33
|
+
run: pushMetaBranch,
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {path} from 'zx-extra'
|
|
2
|
+
import {npmPublish} from '../../api/npm.js'
|
|
3
|
+
|
|
4
|
+
export const isNpmPublished = (pkg) =>
|
|
5
|
+
!pkg.manifest.private && pkg.config.npmPublish !== false
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: 'npm',
|
|
9
|
+
requires: ['token'],
|
|
10
|
+
when: isNpmPublished,
|
|
11
|
+
run: (manifest, dir) => npmPublish({
|
|
12
|
+
name: manifest.name,
|
|
13
|
+
version: manifest.version,
|
|
14
|
+
npmTarball: path.join(dir, 'package.tgz'),
|
|
15
|
+
preversion: manifest.preversion,
|
|
16
|
+
manifest: {},
|
|
17
|
+
config: {
|
|
18
|
+
npmRegistry: manifest.registry,
|
|
19
|
+
npmToken: manifest.token,
|
|
20
|
+
npmConfig: manifest.config,
|
|
21
|
+
npmProvenance: manifest.provenance,
|
|
22
|
+
npmOidc: manifest.oidc,
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
snapshot: true,
|
|
26
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {$, tempy, within, path, semver, fs} from 'zx-extra'
|
|
2
|
+
import {unpackTar} from '../tar.js'
|
|
3
|
+
import {log} from '../log.js'
|
|
4
|
+
import meta from './channels/meta.js'
|
|
5
|
+
import npm from './channels/npm.js'
|
|
6
|
+
import ghRelease from './channels/gh-release.js'
|
|
7
|
+
import ghPages from './channels/gh-pages.js'
|
|
8
|
+
import changelog from './channels/changelog.js'
|
|
9
|
+
import cmd from './channels/cmd.js'
|
|
10
|
+
|
|
11
|
+
export {buildParcels} from './parcel.js'
|
|
12
|
+
|
|
13
|
+
export const channels = {meta, npm, 'gh-release': ghRelease, 'gh-pages': ghPages, changelog, cmd}
|
|
14
|
+
export const defaultOrder = ['meta', 'npm', 'gh-release', 'gh-pages', 'changelog', 'cmd']
|
|
15
|
+
|
|
16
|
+
export const prepare = async (names, pkg) => {
|
|
17
|
+
for (const n of names) await channels[n]?.prepare?.(pkg)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const runChannel = async (name, ...args) => channels[name]?.run(...args)
|
|
21
|
+
|
|
22
|
+
export const resolveManifest = (manifest, env = process.env) => {
|
|
23
|
+
const resolved = {}
|
|
24
|
+
for (const [k, v] of Object.entries(manifest))
|
|
25
|
+
resolved[k] = typeof v === 'string' ? v.replace(/\$\{\{(\w+)\}\}/g, (_, n) => env[n] || '') : v
|
|
26
|
+
|
|
27
|
+
const {repoHost, repoName, originUrl} = resolved
|
|
28
|
+
const token = env.GH_TOKEN || env.GITHUB_TOKEN || ''
|
|
29
|
+
const user = env.GH_USER || env.GH_USERNAME || env.GITHUB_USER || env.GITHUB_USERNAME || 'x-access-token'
|
|
30
|
+
|
|
31
|
+
if (repoHost && repoName && token) {
|
|
32
|
+
resolved.repoAuthedUrl = `https://${user}:${token}@${repoHost}/${repoName}.git`
|
|
33
|
+
resolved.ghBasicAuth = `${user}:${token}`
|
|
34
|
+
} else if (originUrl) {
|
|
35
|
+
resolved.repoAuthedUrl = originUrl
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return resolved
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const openParcel = async (tarPath, env) => {
|
|
42
|
+
const content = await fs.readFile(tarPath, 'utf8').catch(() => null)
|
|
43
|
+
if (content === 'released' || content === 'skip') return null
|
|
44
|
+
|
|
45
|
+
const destDir = tempy.temporaryDirectory()
|
|
46
|
+
const {manifest} = await unpackTar(tarPath, destDir)
|
|
47
|
+
const resolved = resolveManifest(manifest, env)
|
|
48
|
+
const ch = channels[resolved.channel]
|
|
49
|
+
|
|
50
|
+
if (!ch) return {warn: `unknown channel '${resolved.channel || '<none>'}'`}
|
|
51
|
+
|
|
52
|
+
const missing = (ch.requires || []).filter(f => !resolved[f])
|
|
53
|
+
if (missing.length) return {warn: `missing credentials — ${missing.join(', ')}`, tarPath}
|
|
54
|
+
|
|
55
|
+
return {ch, resolved, destDir, tarPath}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pool = async (tasks, concurrency, fn) => {
|
|
59
|
+
const active = new Set()
|
|
60
|
+
let i = 0
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
const next = () => {
|
|
63
|
+
if (i >= tasks.length && active.size === 0) return resolve()
|
|
64
|
+
while (active.size < concurrency && i < tasks.length) {
|
|
65
|
+
const t = tasks[i++]
|
|
66
|
+
const p = fn(t).then(() => { active.delete(p); next() }, reject)
|
|
67
|
+
active.add(p)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
next()
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Parcels grouped by package, sorted by semver asc (latest last).
|
|
75
|
+
// Groups run in parallel (concurrency-limited), entries within a group — sequential.
|
|
76
|
+
export const deliver = async (tars, env = process.env, {concurrency = 4} = {}) => {
|
|
77
|
+
const groups = new Map()
|
|
78
|
+
|
|
79
|
+
for (const tarPath of tars) {
|
|
80
|
+
const fname = path.basename(tarPath)
|
|
81
|
+
try {
|
|
82
|
+
const p = await openParcel(tarPath, env)
|
|
83
|
+
if (!p) continue
|
|
84
|
+
if (p.warn) {
|
|
85
|
+
log.warn(`skipping ${fname}: ${p.warn}`)
|
|
86
|
+
if (p.tarPath) await fs.writeFile(tarPath, 'skip')
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const key = p.resolved.name || p.resolved.channel
|
|
90
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
91
|
+
groups.get(key).push(p)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log.warn(`skipping ${fname}: ${e.message}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const g of groups.values())
|
|
98
|
+
g.sort((a, b) => semver.compare(a.resolved.version || '0.0.0', b.resolved.version || '0.0.0'))
|
|
99
|
+
|
|
100
|
+
let delivered = 0
|
|
101
|
+
await pool([...groups.values()], concurrency, async (group) => {
|
|
102
|
+
for (const {ch, resolved, destDir, tarPath} of group) {
|
|
103
|
+
await within(async () => {
|
|
104
|
+
$.scope = resolved.name || resolved.channel
|
|
105
|
+
await ch.run(resolved, destDir)
|
|
106
|
+
})
|
|
107
|
+
await fs.writeFile(tarPath, 'released')
|
|
108
|
+
delivered++
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return delivered
|
|
113
|
+
}
|