zx-bulk-release 2.21.1 → 3.0.0

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 +17 -0
  2. package/README.md +94 -26
  3. package/package.json +1 -1
  4. package/src/main/js/index.js +1 -1
  5. package/src/main/js/{processor → post}/api/gh.js +0 -27
  6. package/src/main/js/{processor → post}/api/git.js +2 -10
  7. package/src/main/js/{processor → post}/api/npm.js +4 -2
  8. package/src/main/js/post/courier/channels/changelog.js +29 -0
  9. package/src/main/js/{processor/publishers → post/courier/channels}/cmd.js +1 -0
  10. package/src/main/js/post/courier/channels/gh-pages.js +30 -0
  11. package/src/main/js/post/courier/channels/gh-release.js +35 -0
  12. package/src/main/js/post/courier/channels/meta.js +34 -0
  13. package/src/main/js/post/courier/channels/npm.js +26 -0
  14. package/src/main/js/post/courier/index.js +113 -0
  15. package/src/main/js/post/courier/parcel.js +77 -0
  16. package/src/main/js/{processor → post/depot}/exec.js +2 -2
  17. package/src/main/js/{processor → post/depot}/generators/meta.js +3 -3
  18. package/src/main/js/{processor → post/depot}/generators/notes.js +1 -1
  19. package/src/main/js/{processor → post/depot}/steps/analyze.js +2 -2
  20. package/src/main/js/{processor → post/depot}/steps/build.js +2 -2
  21. package/src/main/js/{processor → post/depot}/steps/clean.js +2 -2
  22. package/src/main/js/{processor → post/depot}/steps/contextify.js +3 -3
  23. package/src/main/js/post/depot/steps/pack.js +59 -0
  24. package/src/main/js/post/depot/steps/publish.js +29 -0
  25. package/src/main/js/{processor → post/depot}/steps/test.js +1 -1
  26. package/src/main/js/{processor → post}/release.js +44 -37
  27. package/src/main/js/post/tar.js +73 -0
  28. package/src/test/js/utils/mock.js +1 -1
  29. package/src/main/js/processor/publishers/changelog.js +0 -26
  30. package/src/main/js/processor/publishers/gh-pages.js +0 -32
  31. package/src/main/js/processor/publishers/gh-release.js +0 -41
  32. package/src/main/js/processor/publishers/meta.js +0 -58
  33. package/src/main/js/processor/publishers/npm.js +0 -15
  34. package/src/main/js/processor/steps/publish.js +0 -39
  35. package/src/main/js/processor/steps/teardown.js +0 -58
  36. /package/src/main/js/{processor → post/depot}/deps.js +0 -0
  37. /package/src/main/js/{processor → post/depot}/generators/tag.js +0 -0
  38. /package/src/main/js/{processor → post}/log.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [3.0.0](https://github.com/semrel-extra/zx-bulk-release/compare/v2.21.1...v3.0.0) (2026-04-11)
2
+
3
+ ### Fixes & improvements
4
+ * refactor: enhance courier inners ([0a75468](https://github.com/semrel-extra/zx-bulk-release/commit/0a75468a1736291d194e307c8fef39cbaae55948))
5
+ * refactor: use `parcels` as default artifacts dir ([b1c589e](https://github.com/semrel-extra/zx-bulk-release/commit/b1c589edfcdfba2c548c09f35950b058e6cc2f8c))
6
+ * refactor: simplify parcel contracts ([0a93948](https://github.com/semrel-extra/zx-bulk-release/commit/0a93948b2b4013ad3a620bcf93cf468651c0380b))
7
+ * refactor: move git-tag phase to depot ([a736ead](https://github.com/semrel-extra/zx-bulk-release/commit/a736ead83a75014f4bf592b9b60d5a5073bb4381))
8
+ * refactor: extract pack step from publish ([213a286](https://github.com/semrel-extra/zx-bulk-release/commit/213a28603ccfd3a9deed0d48652bca2864e17ef9))
9
+ * refactor: lift api/ to post/ level as shared ([bc51e2c](https://github.com/semrel-extra/zx-bulk-release/commit/bc51e2c8e9517261de5107a7b67680018f8fe71d))
10
+ * refactor: extract courier module with sealed directive ([20617d8](https://github.com/semrel-extra/zx-bulk-release/commit/20617d83f87b0409963b178b8a34fd11ad22df33))
11
+
12
+ ### BREAKING CHANGES
13
+ * - --recover flag removed. Use --deliver <dir> to re-deliver pre-packed tars. ([c45ab47](https://github.com/semrel-extra/zx-bulk-release/commit/c45ab47a853a887f45abe6a0fa27eade0f71c4dc))
14
+
15
+ ### Features
16
+ * feat: seal delivery flow — tar containers, template credentials ([2ba819a](https://github.com/semrel-extra/zx-bulk-release/commit/2ba819aef7e2d74f55db6bd6fc05f6589ddbc700))
17
+
1
18
  ## [2.21.1](https://github.com/semrel-extra/zx-bulk-release/compare/v2.21.0...v2.21.1) (2026-04-11)
2
19
 
3
20
  ### Fixes & improvements
package/README.md CHANGED
@@ -14,10 +14,11 @@
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.
17
18
 
18
19
  ## Roadmap
19
20
  * [x] Store release metrics to `meta`.
20
- * [ ] ~~Self-repair. Restore broken/missing metadata from external registries (npm, pypi, m2)~~. Tags should be the only source of truth
21
+ * [x] Two-phase pipeline (pack / deliver).
21
22
  * [ ] Multistack. Add support for java/kt/py.
22
23
  * [ ] Semaphore. Let several release agents to serve the monorepo at the same time.
23
24
 
@@ -25,7 +26,6 @@
25
26
  * macOS / linux
26
27
  * Node.js >= 16.0.0
27
28
  * npm >=7 / yarn >= 3
28
- * ~~wget~~
29
29
  * tar
30
30
  * git
31
31
 
@@ -38,21 +38,56 @@ yarn add zx-bulk-release
38
38
  ```shell
39
39
  GH_TOKEN=ghtoken GH_USER=username NPM_TOKEN=npmtoken npx zx-bulk-release [opts]
40
40
  ```
41
- | Flag | Description | Default |
42
- |------------------------------|---------------------------------------------------------------------------------------------------------------------------|------------------|
43
- | `--ignore` | Packages to ignore: `a, b` | |
44
- | `--include-private` | Include `private` packages | `false` |
45
- | `--concurrency` | `build/publish` threads limit | `os.cpus.length` |
46
- | `--no-build` | Skip `buildCmd` invoke | |
47
- | `--no-test` | Disable `testCmd` run | |
48
- | `--no-npm-fetch` | Disable npm artifacts fetching | |
49
- | `--only-workspace-deps` | Recognize only `workspace:` deps as graph edges | |
50
- | `--dry-run` / `--no-publish` | Disable any publish logic | |
51
- | `--report` | Persist release state to file | |
52
- | `--snapshot` | Disable any publishing steps except of `npm` and `publishCmd` (if defined), then push packages to the `snapshot` channel | |
53
- | `--recover` | Remove orphan git tags (tag exists but npm publish failed) and exit. Re-run without this flag to release. | |
54
- | `--debug` | Enable [zx](https://github.com/google/zx#verbose) verbose mode | |
55
- | `--version` / `-v` | Print own version | |
41
+ | Flag | Description | Default |
42
+ |------------------------------|---------------------------------------------------------------------------------------------------|------------------|
43
+ | `--pack [dir]` | Pack only: build, test, and write delivery tars to `dir`. No credentials needed. | `parcels` |
44
+ | `--deliver [dir]` | Deliver only: read tars from `dir` and run delivery channels. No source code needed. | `parcels` |
45
+ | `--ignore` | Packages to ignore: `a, b` | |
46
+ | `--include-private` | Include `private` packages | `false` |
47
+ | `--concurrency` | `build/publish` threads limit | `os.cpus.length` |
48
+ | `--no-build` | Skip `buildCmd` invoke | |
49
+ | `--no-test` | Disable `testCmd` run | |
50
+ | `--no-npm-fetch` | Disable npm artifacts fetching | |
51
+ | `--only-workspace-deps` | Recognize only `workspace:` deps as graph edges | |
52
+ | `--dry-run` / `--no-publish` | Disable any publish logic | |
53
+ | `--report` | Persist release state to file | |
54
+ | `--snapshot` | Publish only to `npm` snapshot channel and run `publishCmd` (if defined), skip everything else | |
55
+ | `--debug` | Enable [zx](https://github.com/google/zx#verbose) verbose mode | |
56
+ | `--version` / `-v` | Print own version | |
57
+
58
+ ### Two-phase pipeline
59
+ By default, zbr runs the full pipeline in a single process. For better security isolation, split build and delivery into separate CI jobs:
60
+
61
+ ```yaml
62
+ # Job 1: build (minimal privileges — source code access only)
63
+ jobs:
64
+ pack:
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ with: { fetch-depth: 0 }
69
+ - run: npx zx-bulk-release --pack
70
+ - uses: actions/upload-artifact@v4
71
+ with: { name: parcels, path: parcels, retention-days: 1 }
72
+
73
+ # Job 2: deliver (only delivery credentials, no source code)
74
+ deliver:
75
+ needs: pack
76
+ runs-on: ubuntu-latest
77
+ steps:
78
+ - uses: actions/download-artifact@v4
79
+ with: { name: parcels, path: parcels }
80
+ - run: npx zx-bulk-release --deliver
81
+ env:
82
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
83
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
84
+ - uses: actions/upload-artifact@v4
85
+ with: { name: parcels, path: parcels, overwrite: true, retention-days: 1 }
86
+ ```
87
+
88
+ 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.
89
+
90
+ **Recovery** is simply re-running the deliver job. Only undelivered tars (not yet replaced with markers) will be processed. No rebuild required.
56
91
 
57
92
  ### JS API
58
93
  ```js
@@ -153,7 +188,7 @@ The `--snapshot` flag publishes packages to the `snapshot` npm dist-tag with a p
153
188
  **What snapshot does differently:**
154
189
  - Version gets a `-snap.<short-sha>` suffix instead of a clean bump
155
190
  - Git release tags are **not** pushed
156
- - Only `npm` and `publishCmd` publishers run (no gh-release, no changelog, no gh-pages, no meta)
191
+ - Only `npm` and `publishCmd` channels run (no gh-release, no changelog, no gh-pages, no meta)
157
192
  - npm tag is `snapshot` instead of `latest`
158
193
 
159
194
  **Workflow example** (`.github/workflows/snapshot.yml`):
@@ -204,10 +239,22 @@ See [antongolub/misc](https://github.com/antongolub/misc) for a real-world examp
204
239
  * [antongolub/misc](https://github.com/antongolub/misc)
205
240
 
206
241
  ## Implementation notes
242
+ ### Architecture
243
+ The release pipeline is split into three subsystems under `post/`:
244
+
245
+ ```
246
+ post/
247
+ depot/ — preparation: analysis, versioning, building, testing, tar packing
248
+ courier/ — sealed delivery: receives self-describing tars and delivers through channels
249
+ api/ — shared infrastructure wrappers (git, npm, gh)
250
+ ```
251
+
252
+ 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.
253
+
207
254
  ### Flow
208
255
  ```
209
- topo ─► contextify ─► analyze ──► build ──► test ──► publish ─► clean
210
- (per pkg) (per pkg) (per pkg) (per pkg) (per pkg)
256
+ topo ─► contextify ─► analyze ──► build ──► test ──► pack ──► publish ─► clean
257
+ (per pkg) (per pkg) (per pkg) (per pkg) (per pkg) (per pkg)
211
258
  ```
212
259
  [`@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
260
 
@@ -219,7 +266,8 @@ Each step has a uniform signature `(pkg, ctx)`:
219
266
  - **`analyze`** — determines semantic changes, release type, and next version.
220
267
  - **`build`** — runs `buildCmd` (with dep traversal and optional npm artifact fetch).
221
268
  - **`test`** — runs `testCmd`.
222
- - **`publish`** — orchestrates the publisher registry: prepare (serial) run (parallel) rollback on failure.
269
+ - **`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.
270
+ - **`publish`** — pushes the release tag (commitment point), hands tars to courier's `deliver()`, runs `cmd` channel separately.
223
271
  - **`clean`** — restores `package.json` files and unsets git user config.
224
272
 
225
273
  Set `config.releaseRules` to override the default rules preset:
@@ -231,19 +279,39 @@ Set `config.releaseRules` to override the default rules preset:
231
279
  ]
232
280
  ```
233
281
 
234
- ### Publishers
235
- Publish targets are a registry of `{name, when, prepare?, run, undo?, snapshot?}` objects:
282
+ ### Tar containers
283
+ Each delivery artifact is a self-describing tar archive:
284
+ ```
285
+ {tag}.{channel}.{hash8}.tar
286
+ manifest.json — channel name, delivery params, template credentials
287
+ package.tgz — (npm channel) npm tarball
288
+ assets/ — (gh-release channel) release assets
289
+ docs/ — (gh-pages channel) documentation files
290
+ ```
291
+
292
+ 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.
293
+
294
+ ### Channels
295
+ Delivery channels are a registry of `{name, when, prepare?, run, requires?, snapshot?, transport?}` objects:
236
296
  - **meta** — pushes release metadata to the `meta` branch (or as a GH release asset).
237
297
  - **npm** — publishes to the npm registry.
238
298
  - **gh-release** — creates a GitHub release with optional file assets.
239
299
  - **gh-pages** — pushes docs to a `gh-pages` branch.
240
300
  - **changelog** — pushes a changelog entry to a `changelog` branch.
241
- - **cmd** — runs a custom `publishCmd`.
301
+ - **cmd** — runs a custom `publishCmd` (depot-side, not through courier; `transport: false`).
242
302
 
243
- Teardown walks the registry in reverse, calling `undo()` on each publisher for rollback/recovery.
303
+ 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).
304
+
305
+ ### Credential flow
306
+ ```
307
+ depot (build phase) courier (deliver phase)
308
+ manifest: { token: '${{NPM_TOKEN}}' } → resolveManifest(manifest, env)
309
+ → { token: 'actual-secret' }
310
+ ```
311
+ 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
312
 
245
313
  ### Tags
246
- [Lerna](https://github.com/lerna/lerna) tags (like `@pkg/name@v1.0.0-beta.0`) are suitable for monorepos, but they dont follow [semver spec](https://semver.org/). Therefore, we propose another contract:
314
+ [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
315
  ```js
248
316
  '2022.6.13-optional-org.pkg-name.v1.0.0-beta.1+sha.1-f0'
249
317
  // date name version format
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zx-bulk-release",
3
3
  "alias": "bulk-release",
4
- "version": "2.21.1",
4
+ "version": "3.0.0",
5
5
  "description": "zx-based alternative for multi-semantic-release",
6
6
  "type": "module",
7
7
  "exports": {
@@ -1 +1 @@
1
- export {run} from './processor/release.js'
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 {absPath: cwd, name, version, manifest, config: {npmPublish, npmRegistry, npmToken, npmConfig, npmProvenance, npmOidc}} = pkg
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
- await $({cwd})`npm publish ${npmFlags.filter(Boolean)}`
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
+ }
@@ -3,4 +3,5 @@ export default {
3
3
  when: (pkg) => !!(pkg.ctx?.flags?.publishCmd ?? pkg.config.publishCmd),
4
4
  run: (pkg, exec) => exec(pkg, 'publishCmd'),
5
5
  snapshot: true,
6
+ transport: false,
6
7
  }
@@ -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
+ }