zx-bulk-release 3.1.4 → 3.1.5
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 +7 -0
- package/package.json +1 -1
- package/src/main/js/post/api/gh.js +1 -2
- package/src/main/js/post/api/git.js +4 -4
- package/src/main/js/post/courier/channels/git-tag.js +12 -2
- package/src/main/js/post/courier/index.js +12 -11
- package/src/main/js/post/courier/semaphore.js +10 -0
- package/src/main/js/post/depot/steps/pack.js +3 -1
- package/src/main/js/post/modes/deliver.js +26 -2
- package/src/main/js/post/parcel/build.js +3 -1
- package/src/main/js/post/parcel/index.js +1 -1
- package/src/main/js/post/parcel/verify.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [3.1.5](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.4...v3.1.5) (2026-04-13)
|
|
2
|
+
|
|
3
|
+
### Fixes & improvements
|
|
4
|
+
* fix: make git-tag channel work with both local and remote repos ([0102cdd](https://github.com/semrel-extra/zx-bulk-release/commit/0102cddaf6c091e8cc68533be8034f95776c64b4))
|
|
5
|
+
* fix: use package name and version in parcel names ([6b5f1e7](https://github.com/semrel-extra/zx-bulk-release/commit/6b5f1e761c9821460267c1464db01af1029dbaa7))
|
|
6
|
+
* fix: enable git-tag and semaphore in bare deliver repo ([abe82d9](https://github.com/semrel-extra/zx-bulk-release/commit/abe82d9b299a489ce16c0a9f3867894657b4e2a4))
|
|
7
|
+
|
|
1
8
|
## [3.1.4](https://github.com/semrel-extra/zx-bulk-release/compare/v3.1.3...v3.1.4) (2026-04-13)
|
|
2
9
|
|
|
3
10
|
### Fixes & improvements
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Low-level GitHub API primitives. No domain knowledge, no imports from processor/ or steps/.
|
|
2
2
|
|
|
3
|
-
import nodeFs from 'node:fs'
|
|
4
3
|
import {$, path, tempy, glob, fs, fetch} from 'zx-extra'
|
|
5
4
|
import {asArray, attempt2} from '../../util.js'
|
|
6
5
|
|
|
@@ -93,7 +92,7 @@ export const ghGetAsset = async ({repoName, tag, name, ghUrl}) => {
|
|
|
93
92
|
export const setOutput = (name, value) => {
|
|
94
93
|
const outputFile = process.env.GITHUB_OUTPUT
|
|
95
94
|
if (outputFile) {
|
|
96
|
-
try {
|
|
95
|
+
try { fs.appendFileSync(outputFile, `${name}=${value}\n`) } catch { /* not in CI */ }
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
|
|
@@ -104,12 +104,12 @@ export const getTags = memoizeBy(
|
|
|
104
104
|
async (cwd, ref = '*') => `${await getRoot(cwd)}:${ref}`,
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
-
export const pushTag = async ({cwd, tag, gitCommitterName, gitCommitterEmail}) => {
|
|
107
|
+
export const pushTag = async ({cwd, tag, sha, gitCommitterName, gitCommitterEmail}) => {
|
|
108
108
|
await setUserConfig(cwd, gitCommitterName, gitCommitterEmail)
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const target = sha ? [sha] : []
|
|
111
|
+
await $({cwd})`git tag -m ${tag} ${tag} ${target}`
|
|
112
|
+
await $({cwd})`git push origin ${tag}`
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// Memoize prevents .git/config lock
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
import {$} from 'zx-extra'
|
|
1
2
|
import {api} from '../../api/index.js'
|
|
2
3
|
import {DEFAULT_GIT_COMMITTER_NAME, DEFAULT_GIT_COMMITTER_EMAIL} from '../../api/git.js'
|
|
3
4
|
|
|
4
5
|
export const isTagConflict = (e) =>
|
|
5
6
|
/already exists|updates were rejected|failed to push/i.test(e?.message || e?.stderr || '')
|
|
6
7
|
|
|
8
|
+
const ensureSha = async (cwd, sha) => {
|
|
9
|
+
if (!sha) return
|
|
10
|
+
const check = await $({cwd, nothrow: true, quiet: true})`git cat-file -e ${sha}`
|
|
11
|
+
if (check.exitCode !== 0) {
|
|
12
|
+
await $({cwd, quiet: true})`git fetch origin --depth 1`
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
const run = async (manifest, dir) => {
|
|
8
|
-
const {tag, cwd} = manifest
|
|
17
|
+
const {tag, sha, cwd} = manifest
|
|
9
18
|
const gitCommitterName = manifest.gitCommitterName || DEFAULT_GIT_COMMITTER_NAME
|
|
10
19
|
const gitCommitterEmail = manifest.gitCommitterEmail || DEFAULT_GIT_COMMITTER_EMAIL
|
|
11
20
|
try {
|
|
12
|
-
await
|
|
21
|
+
await ensureSha(cwd, sha)
|
|
22
|
+
await api.git.pushTag({cwd, tag, sha, gitCommitterName, gitCommitterEmail})
|
|
13
23
|
return 'ok'
|
|
14
24
|
} catch (e) {
|
|
15
25
|
if (isTagConflict(e)) return 'conflict'
|
|
@@ -49,13 +49,14 @@ export const resolveManifest = (manifest, env = process.env) => {
|
|
|
49
49
|
|
|
50
50
|
const MARKERS = new Set(['released', 'skip', 'conflict', 'orphan'])
|
|
51
51
|
|
|
52
|
-
const openParcel = async (tarPath, env) => {
|
|
52
|
+
const openParcel = async (tarPath, env, {cwd} = {}) => {
|
|
53
53
|
const content = await fs.readFile(tarPath, 'utf8').catch(() => null)
|
|
54
54
|
if (MARKERS.has(content)) return null
|
|
55
55
|
|
|
56
56
|
const destDir = tempy.temporaryDirectory()
|
|
57
57
|
const {manifest} = await unpackTar(tarPath, destDir)
|
|
58
58
|
const resolved = resolveManifest(manifest, env)
|
|
59
|
+
if (cwd) resolved.cwd = cwd
|
|
59
60
|
const ch = channels[resolved.channel]
|
|
60
61
|
|
|
61
62
|
if (!ch) return {warn: `unknown channel '${resolved.channel || '<none>'}'`}
|
|
@@ -67,14 +68,14 @@ const openParcel = async (tarPath, env) => {
|
|
|
67
68
|
return {ch, resolved, destDir, tarPath}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export const inspect = async (tars, env = process.env) => {
|
|
71
|
+
export const inspect = async (tars, env = process.env, {cwd} = {}) => {
|
|
71
72
|
const parcels = []
|
|
72
73
|
const skipped = []
|
|
73
74
|
|
|
74
75
|
for (const tarPath of tars) {
|
|
75
76
|
const fname = path.basename(tarPath)
|
|
76
77
|
try {
|
|
77
|
-
const p = await openParcel(tarPath, env)
|
|
78
|
+
const p = await openParcel(tarPath, env, {cwd})
|
|
78
79
|
if (!p) continue
|
|
79
80
|
if (p.warn) {
|
|
80
81
|
skipped.push({file: fname, reason: p.warn, tarPath: p.tarPath})
|
|
@@ -100,8 +101,8 @@ export const inspect = async (tars, env = process.env) => {
|
|
|
100
101
|
|
|
101
102
|
// --- Legacy deliver (no directive) ---
|
|
102
103
|
|
|
103
|
-
const deliverLegacy = async (tars, env, {concurrency, dryRun}) => {
|
|
104
|
-
const {groups, skipped, total, pending} = await inspect(tars, env)
|
|
104
|
+
const deliverLegacy = async (tars, env, {concurrency, dryRun, cwd}) => {
|
|
105
|
+
const {groups, skipped, total, pending} = await inspect(tars, env, {cwd})
|
|
105
106
|
|
|
106
107
|
for (const {file, reason, tarPath} of skipped) {
|
|
107
108
|
log.warn(`skipping ${file}: ${reason}`)
|
|
@@ -135,8 +136,8 @@ const deliverLegacy = async (tars, env, {concurrency, dryRun}) => {
|
|
|
135
136
|
|
|
136
137
|
// --- Directive-aware deliver ---
|
|
137
138
|
|
|
138
|
-
const deliverParcel = async (tarPath, channelName, pkgName, version, env, {dryRun}) => {
|
|
139
|
-
const p = await openParcel(tarPath, env)
|
|
139
|
+
const deliverParcel = async (tarPath, channelName, pkgName, version, env, {dryRun, cwd}) => {
|
|
140
|
+
const p = await openParcel(tarPath, env, {cwd})
|
|
140
141
|
if (!p) return 'already'
|
|
141
142
|
if (p.warn) {
|
|
142
143
|
log.warn(`skipping ${p.warn}`)
|
|
@@ -159,7 +160,7 @@ const deliverParcel = async (tarPath, channelName, pkgName, version, env, {dryRu
|
|
|
159
160
|
return res === 'duplicate' ? 'duplicate' : 'ok'
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
const deliverDirective = async (directive, tarMap, env, {dryRun}) => {
|
|
163
|
+
const deliverDirective = async (directive, tarMap, env, {dryRun, cwd}) => {
|
|
163
164
|
const entries = []
|
|
164
165
|
const conflicts = []
|
|
165
166
|
const skipped = []
|
|
@@ -178,7 +179,7 @@ const deliverDirective = async (directive, tarMap, env, {dryRun}) => {
|
|
|
178
179
|
const tarPath = parcelName && tarMap.get(parcelName)
|
|
179
180
|
if (!tarPath) return 'missing'
|
|
180
181
|
|
|
181
|
-
return deliverParcel(tarPath, channelName, pkgName, pkg.version, env, {dryRun})
|
|
182
|
+
return deliverParcel(tarPath, channelName, pkgName, pkg.version, env, {dryRun, cwd})
|
|
182
183
|
}))
|
|
183
184
|
|
|
184
185
|
for (let i = 0; i < step.length; i++) {
|
|
@@ -209,7 +210,7 @@ export const deliver = async (tars, env = process.env, {concurrency = 4, dryRun
|
|
|
209
210
|
const dir = tars.length ? path.dirname(tars[0]) : null
|
|
210
211
|
const directives = dir ? await scanDirectives(dir) : []
|
|
211
212
|
|
|
212
|
-
if (!directives.length) return deliverLegacy(tars, env, {concurrency, dryRun})
|
|
213
|
+
if (!directives.length) return deliverLegacy(tars, env, {concurrency, dryRun, cwd})
|
|
213
214
|
|
|
214
215
|
const tarMap = new Map(tars.map(t => [path.basename(t), t]))
|
|
215
216
|
const allEntries = []
|
|
@@ -225,7 +226,7 @@ export const deliver = async (tars, env = process.env, {concurrency = 4, dryRun
|
|
|
225
226
|
|
|
226
227
|
try {
|
|
227
228
|
await invalidateOrphans(dir, directive)
|
|
228
|
-
const {entries, conflicts, skipped} = await deliverDirective(directive, tarMap, env, {dryRun})
|
|
229
|
+
const {entries, conflicts, skipped} = await deliverDirective(directive, tarMap, env, {dryRun, cwd: gitRoot})
|
|
229
230
|
allEntries.push(...entries)
|
|
230
231
|
allConflicts.push(...conflicts)
|
|
231
232
|
allSkipped.push(...skipped)
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import {$} from 'zx-extra'
|
|
1
2
|
import {api} from '../api/index.js'
|
|
3
|
+
import {DEFAULT_GIT_COMMITTER_NAME, DEFAULT_GIT_COMMITTER_EMAIL} from '../api/git.js'
|
|
2
4
|
|
|
3
5
|
const tagName = ({sha}) =>
|
|
4
6
|
`zbr-deliver.${sha.slice(0, 7)}`
|
|
5
7
|
|
|
8
|
+
const ensureHead = async (cwd) => {
|
|
9
|
+
try { await $({cwd, quiet: true})`git rev-parse HEAD` } catch {
|
|
10
|
+
await api.git.setUserConfig(cwd, DEFAULT_GIT_COMMITTER_NAME, DEFAULT_GIT_COMMITTER_EMAIL)
|
|
11
|
+
await $({cwd, quiet: true})`git commit --allow-empty -m init`
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export const tryLock = async (cwd, directive) => {
|
|
7
16
|
const tag = tagName(directive)
|
|
8
17
|
const body = JSON.stringify({
|
|
@@ -11,6 +20,7 @@ export const tryLock = async (cwd, directive) => {
|
|
|
11
20
|
packages: directive.queue,
|
|
12
21
|
})
|
|
13
22
|
try {
|
|
23
|
+
await ensureHead(cwd)
|
|
14
24
|
await api.git.pushAnnotatedTag(cwd, tag, body)
|
|
15
25
|
return true
|
|
16
26
|
} catch {
|
|
@@ -3,6 +3,7 @@ import {memoizeBy, asTuple} from '../../../util.js'
|
|
|
3
3
|
import {api} from '../../api/index.js'
|
|
4
4
|
import {prepare, getActiveChannels} from '../../courier/index.js'
|
|
5
5
|
import {buildParcels, PARCELS_DIR} from '../../parcel/index.js'
|
|
6
|
+
import {sanitizePkgName} from '../../parcel/build.js'
|
|
6
7
|
import {formatReleaseNotes} from '../generators/notes.js'
|
|
7
8
|
import {packTar, hashFile} from '../../tar.js'
|
|
8
9
|
|
|
@@ -43,7 +44,8 @@ export const pack = memoizeBy(async (pkg, ctx = pkg.ctx) => {
|
|
|
43
44
|
await packTar(tmpPath, manifest, files)
|
|
44
45
|
const hash = await hashFile(tmpPath)
|
|
45
46
|
const sha7 = ctx.git.sha.slice(0, 7)
|
|
46
|
-
const
|
|
47
|
+
const safeName = sanitizePkgName(pkg.name)
|
|
48
|
+
const finalPath = path.join(stageDir, `parcel.${sha7}.${channel}.${safeName}.${pkg.version}.${hash}.tar`)
|
|
47
49
|
await fs.move(tmpPath, finalPath)
|
|
48
50
|
tars.push(finalPath)
|
|
49
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {$, glob, path} from 'zx-extra'
|
|
1
|
+
import {$, glob, path, tempy} from 'zx-extra'
|
|
2
2
|
import {createReport, log} from '../log.js'
|
|
3
3
|
import {deliver} from '../courier/index.js'
|
|
4
4
|
import {PARCELS_DIR} from '../parcel/index.js'
|
|
@@ -15,9 +15,33 @@ export const runDeliver = async ({env, flags}) => {
|
|
|
15
15
|
const tars = await glob(path.join(dir, 'parcel.*.tar'))
|
|
16
16
|
if (!tars.length) return report.setStatus('success').log(`no parcels in ${dir}`)
|
|
17
17
|
|
|
18
|
+
const cwd = await ensureGitRepo(env)
|
|
19
|
+
|
|
18
20
|
report.setStatus('delivering').log(`parcels: ${tars.length}`)
|
|
19
|
-
const result = await deliver(tars, env, {dryRun: flags.dryRun})
|
|
21
|
+
const result = await deliver(tars, env, {dryRun: flags.dryRun, cwd})
|
|
20
22
|
report.set('delivery', result).setStatus('success')
|
|
21
23
|
|
|
22
24
|
log.info(`done: ${result.delivered} delivered, ${result.skipped} skipped`)
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
const ensureGitRepo = async (env) => {
|
|
28
|
+
try {
|
|
29
|
+
return (await $({quiet: true})`git rev-parse --show-toplevel`).toString().trim()
|
|
30
|
+
} catch {
|
|
31
|
+
// Deliver runs in a clean runner with no checkout — init a temp repo for git push operations.
|
|
32
|
+
const tmp = tempy.temporaryDirectory()
|
|
33
|
+
const repo = env.GITHUB_REPOSITORY || ''
|
|
34
|
+
const token = env.GH_TOKEN || env.GITHUB_TOKEN || ''
|
|
35
|
+
const user = env.GH_USER || env.GH_USERNAME || 'x-access-token'
|
|
36
|
+
const server = env.GITHUB_SERVER_URL || 'https://github.com'
|
|
37
|
+
const host = new URL(server).host
|
|
38
|
+
const origin = token
|
|
39
|
+
? `https://${user}:${token}@${host}/${repo}.git`
|
|
40
|
+
: `${server}/${repo}.git`
|
|
41
|
+
|
|
42
|
+
await $({quiet: true})`git init ${tmp}`
|
|
43
|
+
await $({quiet: true, cwd: tmp})`git remote add origin ${origin}`
|
|
44
|
+
log.info(`initialized temp git repo for delivery: ${repo}`)
|
|
45
|
+
return tmp
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {asTuple, msgJoin} from '../../util.js'
|
|
2
2
|
|
|
3
|
+
export const sanitizePkgName = (name) => name.replace(/[^a-z0-9-]/ig, '-').replace(/^-+|-+$/g, '')
|
|
4
|
+
|
|
3
5
|
const gitFields = (a, pkg) => ({
|
|
4
6
|
repoHost: a.repoHost,
|
|
5
7
|
repoName: a.repoName,
|
|
@@ -14,7 +16,7 @@ const entry = {
|
|
|
14
16
|
manifest: {
|
|
15
17
|
channel: 'git-tag',
|
|
16
18
|
name: pkg.name, version: pkg.version, tag: pkg.tag,
|
|
17
|
-
cwd: ctx.git.root,
|
|
19
|
+
cwd: ctx.git.root, sha: ctx.git.sha,
|
|
18
20
|
gitCommitterName: '${{GIT_COMMITTER_NAME}}',
|
|
19
21
|
gitCommitterEmail: '${{GIT_COMMITTER_EMAIL}}',
|
|
20
22
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {buildParcels} from './build.js'
|
|
1
|
+
export {buildParcels, sanitizePkgName} from './build.js'
|
|
2
2
|
export {buildDirective, parseDirective, scanDirectives, invalidateOrphans, parcelChannel} from './directive.js'
|
|
3
3
|
export {verifyParcels} from './verify.js'
|
|
4
4
|
export const PARCELS_DIR = 'parcels'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {path} from 'zx-extra'
|
|
2
2
|
import {parcelChannel} from './directive.js'
|
|
3
|
+
import {sanitizePkgName} from './build.js'
|
|
3
4
|
|
|
4
5
|
export const verifyParcels = (tars, context) => {
|
|
5
6
|
const {sha7, packages: expected} = context
|
|
@@ -25,8 +26,8 @@ export const verifyParcels = (tars, context) => {
|
|
|
25
26
|
continue
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const belongsTo = Object.entries(expected).find(([, pkg]) =>
|
|
29
|
-
|
|
29
|
+
const belongsTo = Object.entries(expected).find(([pkgName, pkg]) =>
|
|
30
|
+
name.includes(`.${sanitizePkgName(pkgName)}.${pkg.version}.`)
|
|
30
31
|
)
|
|
31
32
|
if (!belongsTo) {
|
|
32
33
|
errors.push(`unexpected parcel (no matching package): ${name}`)
|