bulk-release 3.0.4 → 3.1.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.
@@ -1,6 +1,9 @@
1
1
  import {$, tempy, within, path, semver, fs} from 'zx-extra'
2
2
  import {unpackTar} from '../tar.js'
3
3
  import {log} from '../log.js'
4
+ import {scanDirectives, invalidateOrphans} from './directive.js'
5
+ import {tryLock, unlock, signalRebuild} from './semaphore.js'
6
+ import gitTag from './channels/git-tag.js'
4
7
  import meta from './channels/meta.js'
5
8
  import npm from './channels/npm.js'
6
9
  import ghRelease from './channels/gh-release.js'
@@ -10,8 +13,8 @@ import cmd from './channels/cmd.js'
10
13
 
11
14
  export {buildParcels} from './parcel.js'
12
15
 
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']
16
+ export const channels = {'git-tag': gitTag, meta, npm, 'gh-release': ghRelease, 'gh-pages': ghPages, changelog, cmd}
17
+ export const defaultOrder = ['git-tag', 'meta', 'npm', 'gh-release', 'gh-pages', 'changelog', 'cmd']
15
18
 
16
19
  export const prepare = async (names, pkg) => {
17
20
  for (const n of names) await channels[n]?.prepare?.(pkg)
@@ -38,9 +41,11 @@ export const resolveManifest = (manifest, env = process.env) => {
38
41
  return resolved
39
42
  }
40
43
 
44
+ const MARKERS = new Set(['released', 'skip', 'conflict', 'orphan'])
45
+
41
46
  const openParcel = async (tarPath, env) => {
42
47
  const content = await fs.readFile(tarPath, 'utf8').catch(() => null)
43
- if (content === 'released' || content === 'skip') return null
48
+ if (MARKERS.has(content)) return null
44
49
 
45
50
  const destDir = tempy.temporaryDirectory()
46
51
  const {manifest} = await unpackTar(tarPath, destDir)
@@ -72,10 +77,9 @@ const pool = async (tasks, concurrency, fn) => {
72
77
  })
73
78
  }
74
79
 
75
- // Parcels grouped by package, sorted by semver asc (latest last).
76
- // Groups run in parallel (concurrency-limited), entries within a group — sequential.
77
- export const deliver = async (tars, env = process.env, {concurrency = 4} = {}) => {
78
- const groups = new Map()
80
+ export const inspect = async (tars, env = process.env) => {
81
+ const parcels = []
82
+ const skipped = []
79
83
 
80
84
  for (const tarPath of tars) {
81
85
  const fname = path.basename(tarPath)
@@ -83,32 +87,176 @@ export const deliver = async (tars, env = process.env, {concurrency = 4} = {}) =
83
87
  const p = await openParcel(tarPath, env)
84
88
  if (!p) continue
85
89
  if (p.warn) {
86
- log.warn(`skipping ${fname}: ${p.warn}`)
87
- if (p.tarPath) await fs.writeFile(tarPath, 'skip')
90
+ skipped.push({file: fname, reason: p.warn, tarPath: p.tarPath})
88
91
  continue
89
92
  }
90
- const key = p.resolved.name || p.resolved.channel
91
- if (!groups.has(key)) groups.set(key, [])
92
- groups.get(key).push(p)
93
+ parcels.push(p)
93
94
  } catch (e) {
94
- log.warn(`skipping ${fname}: ${e.message}`)
95
+ skipped.push({file: fname, reason: e.message})
95
96
  }
96
97
  }
97
98
 
99
+ const groups = new Map()
100
+ for (const p of parcels) {
101
+ const key = p.resolved.name || p.resolved.channel
102
+ if (!groups.has(key)) groups.set(key, [])
103
+ groups.get(key).push(p)
104
+ }
98
105
  for (const g of groups.values())
99
106
  g.sort((a, b) => semver.compare(a.resolved.version || '0.0.0', b.resolved.version || '0.0.0'))
100
107
 
101
- let delivered = 0
108
+ return {groups, skipped, total: tars.length, pending: parcels.length}
109
+ }
110
+
111
+ // --- Legacy deliver (no directive) ---
112
+
113
+ const deliverLegacy = async (tars, env, {concurrency, dryRun}) => {
114
+ const {groups, skipped, total, pending} = await inspect(tars, env)
115
+
116
+ for (const {file, reason, tarPath} of skipped) {
117
+ log.warn(`skipping ${file}: ${reason}`)
118
+ if (!dryRun && tarPath) await fs.writeFile(tarPath, 'skip')
119
+ }
120
+
121
+ const entries = []
122
+ const toEntry = ({resolved}) => ({channel: resolved.channel, name: resolved.name, version: resolved.version})
123
+
124
+ if (dryRun) {
125
+ for (const group of groups.values())
126
+ for (const p of group)
127
+ entries.push(toEntry(p))
128
+ return {total, pending: entries.length, delivered: 0, skipped: skipped.length, entries}
129
+ }
130
+
102
131
  await pool([...groups.values()], concurrency, async (group) => {
103
132
  for (const {ch, resolved, destDir, tarPath} of group) {
104
133
  await within(async () => {
105
134
  $.scope = resolved.name || resolved.channel
106
135
  await ch.run(resolved, destDir)
136
+ log.info(`${resolved.channel} ${resolved.version}`)
107
137
  })
108
138
  await fs.writeFile(tarPath, 'released')
109
- delivered++
139
+ entries.push(toEntry({resolved}))
110
140
  }
111
141
  })
112
142
 
113
- return delivered
143
+ return {total, pending, delivered: entries.length, skipped: skipped.length, entries}
144
+ }
145
+
146
+ // --- Directive-aware deliver ---
147
+
148
+ const deliverParcel = async (tarPath, channelName, pkgName, version, env, {dryRun}) => {
149
+ const p = await openParcel(tarPath, env)
150
+ if (!p) return 'already'
151
+ if (p.warn) {
152
+ log.warn(`skipping ${p.warn}`)
153
+ return 'skip'
154
+ }
155
+
156
+ if (dryRun) return 'dryrun'
157
+
158
+ const {ch, resolved, destDir} = p
159
+ const res = await within(async () => {
160
+ $.scope = pkgName
161
+ const r = await ch.run(resolved, destDir)
162
+ log.info(`${channelName} ${version}`)
163
+ return r
164
+ })
165
+
166
+ if (res === 'conflict') return 'conflict'
167
+
168
+ await fs.writeFile(tarPath, 'released')
169
+ return res === 'duplicate' ? 'duplicate' : 'ok'
170
+ }
171
+
172
+ const deliverDirective = async (directive, tarMap, env, {dryRun}) => {
173
+ const entries = []
174
+ const conflicts = []
175
+ const skipped = []
176
+
177
+ for (const pkgName of directive.queue) {
178
+ const pkg = directive.packages[pkgName]
179
+ if (!pkg) continue
180
+
181
+ let pkgConflict = false
182
+
183
+ for (const step of pkg.deliver) {
184
+ if (pkgConflict) break
185
+
186
+ const results = await Promise.all(step.map(async (channelName) => {
187
+ const parcelName = (pkg.parcels || []).find(p => p.includes(`.${channelName}.`))
188
+ const tarPath = parcelName && tarMap.get(parcelName)
189
+ if (!tarPath) return 'missing'
190
+
191
+ return deliverParcel(tarPath, channelName, pkgName, pkg.version, env, {dryRun})
192
+ }))
193
+
194
+ for (let i = 0; i < step.length; i++) {
195
+ const r = results[i]
196
+ if (r === 'skip') skipped.push({channelName: step[i], pkg: pkgName})
197
+ else if (r !== 'missing' && r !== 'already') entries.push({channel: step[i], name: pkgName, version: pkg.version})
198
+ }
199
+
200
+ if (results.includes('conflict')) {
201
+ pkgConflict = true
202
+ conflicts.push(pkgName)
203
+ for (const p of pkg.parcels || []) {
204
+ const tp = tarMap.get(p)
205
+ if (!tp) continue
206
+ const c = await fs.readFile(tp, 'utf8').catch(() => null)
207
+ if (c !== 'released') await fs.writeFile(tp, 'conflict')
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ return {entries, conflicts, skipped}
214
+ }
215
+
216
+ // --- Main entry point ---
217
+
218
+ export const deliver = async (tars, env = process.env, {concurrency = 4, dryRun = false, cwd} = {}) => {
219
+ const dir = tars.length ? path.dirname(tars[0]) : null
220
+ const directives = dir ? await scanDirectives(dir) : []
221
+
222
+ if (!directives.length) return deliverLegacy(tars, env, {concurrency, dryRun})
223
+
224
+ const tarMap = new Map(tars.map(t => [path.basename(t), t]))
225
+ const allEntries = []
226
+ const allConflicts = []
227
+ const allSkipped = []
228
+
229
+ for (const directive of directives) {
230
+ const gitRoot = cwd || dir
231
+ if (!await tryLock(gitRoot, directive)) {
232
+ log.info(`directive ${directive.sha.slice(0, 7)} locked, skipping`)
233
+ continue
234
+ }
235
+
236
+ try {
237
+ await invalidateOrphans(dir, directive)
238
+ const {entries, conflicts, skipped} = await deliverDirective(directive, tarMap, env, {dryRun})
239
+ allEntries.push(...entries)
240
+ allConflicts.push(...conflicts)
241
+ allSkipped.push(...skipped)
242
+
243
+ if (conflicts.length) {
244
+ await fs.writeFile(directive.tarPath, 'conflict')
245
+ await signalRebuild(gitRoot, directive.sha)
246
+ } else if (!skipped.length) {
247
+ await fs.writeFile(directive.tarPath, 'released')
248
+ }
249
+ } finally {
250
+ await unlock(gitRoot, directive)
251
+ }
252
+ }
253
+
254
+ return {
255
+ total: tars.length,
256
+ pending: allEntries.length,
257
+ delivered: allEntries.length,
258
+ skipped: allSkipped.length,
259
+ entries: allEntries,
260
+ conflicts: allConflicts,
261
+ }
114
262
  }
@@ -9,6 +9,18 @@ const gitFields = (a, pkg) => ({
9
9
  })
10
10
 
11
11
  const entry = {
12
+ 'git-tag': (pkg, ctx) => ({
13
+ channel: 'git-tag',
14
+ manifest: {
15
+ channel: 'git-tag',
16
+ name: pkg.name, version: pkg.version, tag: pkg.tag,
17
+ cwd: ctx.git.root,
18
+ gitCommitterName: '${{GIT_COMMITTER_NAME}}',
19
+ gitCommitterEmail: '${{GIT_COMMITTER_EMAIL}}',
20
+ },
21
+ files: [],
22
+ }),
23
+
12
24
  npm: (pkg, ctx, a) => ({
13
25
  channel: 'npm',
14
26
  manifest: {
@@ -24,6 +36,7 @@ const entry = {
24
36
  channel: 'gh-release',
25
37
  manifest: {
26
38
  channel: 'gh-release',
39
+ name: pkg.name, version: pkg.version,
27
40
  tag: pkg.tag, repoHost: a.repoHost, repoName: a.repoName, releaseNotes: a.releaseNotes,
28
41
  token: '${{GH_TOKEN}}', apiUrl: pkg.config.ghApiUrl,
29
42
  assets: pkg.config.ghAssets ? [...pkg.config.ghAssets] : undefined,
@@ -35,7 +48,7 @@ const entry = {
35
48
  const [branch = 'gh-pages', , to = '.', ..._msg] = asTuple(pkg.config.ghPages, ['branch', 'from', 'to', 'msg'])
36
49
  return {
37
50
  channel: 'gh-pages',
38
- manifest: {channel: 'gh-pages', branch, to, msg: msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}'), ...gitFields(a, pkg)},
51
+ manifest: {channel: 'gh-pages', name: pkg.name, version: pkg.version, branch, to, msg: msgJoin(_msg, pkg, 'docs: update docs ${{name}} ${{version}}'), ...gitFields(a, pkg)},
39
52
  files: a.docsDir ? [{name: 'docs', source: a.docsDir}] : [],
40
53
  }
41
54
  },
@@ -44,7 +57,7 @@ const entry = {
44
57
  const [branch = 'changelog', file = `${pkg.name.replace(/[^a-z0-9-]/ig, '')}-changelog.md`, ..._msg] = asTuple(pkg.config.changelog, ['branch', 'file', 'msg'])
45
58
  return {
46
59
  channel: 'changelog',
47
- manifest: {channel: 'changelog', releaseNotes: a.releaseNotes, branch, file, msg: msgJoin(_msg, pkg, 'chore: update changelog ${{name}}'), ...gitFields(a, pkg)},
60
+ manifest: {channel: 'changelog', name: pkg.name, version: pkg.version, releaseNotes: a.releaseNotes, branch, file, msg: msgJoin(_msg, pkg, 'chore: update changelog ${{name}}'), ...gitFields(a, pkg)},
48
61
  files: [],
49
62
  }
50
63
  },
@@ -0,0 +1,31 @@
1
+ import {pushAnnotatedTag, deleteRemoteTag} from '../api/git.js'
2
+
3
+ const tagName = ({sha}) =>
4
+ `zbr-deliver.${sha.slice(0, 7)}`
5
+
6
+ export const tryLock = async (cwd, directive) => {
7
+ const tag = tagName(directive)
8
+ const body = JSON.stringify({
9
+ ts: directive.timestamp,
10
+ sha: directive.sha,
11
+ packages: directive.queue,
12
+ })
13
+ try {
14
+ await pushAnnotatedTag(cwd, tag, body)
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ }
20
+
21
+ export const unlock = async (cwd, directive) => {
22
+ await deleteRemoteTag(cwd, tagName(directive))
23
+ }
24
+
25
+ export const signalRebuild = async (cwd, sha) => {
26
+ const tag = `zbr-rebuild.${sha.slice(0, 7)}`
27
+ try { await pushAnnotatedTag(cwd, tag, 'rebuild') } catch { /* already signaled */ }
28
+ }
29
+
30
+ export const consumeRebuildSignal = async (cwd, sha) =>
31
+ deleteRemoteTag(cwd, `zbr-rebuild.${sha.slice(0, 7)}`)
@@ -0,0 +1,19 @@
1
+ import {$, semver} from 'zx-extra'
2
+ import {parseTag} from '../depot/generators/tag.js'
3
+
4
+ export const hasHigherVersion = async (cwd, name, version) => {
5
+ const output = (await $({cwd, nothrow: true})`git ls-remote --tags origin`).toString()
6
+ if (!output) return false
7
+
8
+ for (const line of output.split('\n')) {
9
+ const ref = line.split('\t')[1]?.replace('refs/tags/', '')
10
+ if (!ref) continue
11
+ const parsed = parseTag(ref)
12
+ if (!parsed || parsed.name !== name) continue
13
+ try {
14
+ if (semver.gt(parsed.version, version)) return true
15
+ } catch { /* unparseable version, skip */ }
16
+ }
17
+
18
+ return false
19
+ }
@@ -0,0 +1,45 @@
1
+ import {fs, path} from 'zx-extra'
2
+ import {channels as channelRegistry} from '../courier/index.js'
3
+
4
+ const CONTEXT_FILE = '.zbr-context.json'
5
+
6
+ export const writeContext = async (cwd, context) => {
7
+ const filePath = path.resolve(cwd, CONTEXT_FILE)
8
+ await fs.writeJson(filePath, context, {spaces: 2})
9
+ return filePath
10
+ }
11
+
12
+ export const readContext = async (cwd) => {
13
+ const filePath = path.resolve(cwd, CONTEXT_FILE)
14
+ try {
15
+ return await fs.readJson(filePath)
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ const getActiveChannels = (pkg, channelNames, snapshot) =>
22
+ channelNames.filter(n => {
23
+ const ch = channelRegistry[n]
24
+ return ch && ch.transport !== false && (!snapshot || ch.snapshot) && ch.when(pkg)
25
+ })
26
+
27
+ export const buildContext = (packages, queue, sha, {channelNames = [], snapshot = false} = {}) => {
28
+ const pkgs = {}
29
+ for (const name of queue) {
30
+ const pkg = packages[name]
31
+ if (!pkg.releaseType || pkg.skipped) continue
32
+ pkgs[name] = {
33
+ version: pkg.version,
34
+ tag: pkg.tag,
35
+ channels: getActiveChannels(pkg, channelNames, snapshot),
36
+ }
37
+ }
38
+
39
+ return {
40
+ status: 'proceed',
41
+ sha,
42
+ sha7: sha.slice(0, 7),
43
+ packages: pkgs,
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import {$} from 'zx-extra'
2
+ import {log} from '../log.js'
3
+ import {getRemoteTagSha, clearTagsCache} from '../api/git.js'
4
+ import {formatTag} from './generators/tag.js'
5
+ import {resolvePkgVersion} from './steps/analyze.js'
6
+
7
+ export const isTagConflict = (e) =>
8
+ /already exists|updates were rejected|failed to push/i.test(e?.message || e?.stderr || '')
9
+
10
+ export const preflight = async (pkg, ctx) => {
11
+ if (!pkg.tag) return 'ok'
12
+
13
+ const cwd = ctx.git.root
14
+ const remoteSha = await getRemoteTagSha(cwd, pkg.tag)
15
+ if (!remoteSha) return 'ok'
16
+
17
+ // tag exists on remote
18
+ if (remoteSha === ctx.git.sha) {
19
+ log.info(`preflight: ${pkg.tag} already exists for our commit, skipping`)
20
+ return 'skip'
21
+ }
22
+
23
+ // need history for merge-base
24
+ await $({cwd, nothrow: true})`git fetch --deepen=100`
25
+
26
+ const isOursOlder = await $({cwd, nothrow: true})`git merge-base --is-ancestor ${ctx.git.sha} ${remoteSha}`
27
+ if (isOursOlder.exitCode === 0) {
28
+ log.info(`preflight: ${pkg.tag} — we are older, skipping`)
29
+ return 'skip'
30
+ }
31
+
32
+ const isRemoteOlder = await $({cwd, nothrow: true})`git merge-base --is-ancestor ${remoteSha} ${ctx.git.sha}`
33
+ if (isRemoteOlder.exitCode === 0) {
34
+ log.info(`preflight: ${pkg.tag} — we are newer, re-resolving version`)
35
+ await $({cwd})`git fetch origin --tags --force`
36
+ clearTagsCache()
37
+
38
+ const pre = ctx.flags.snapshot ? `-snap.${ctx.git.sha.slice(0, 7)}` : undefined
39
+ // re-resolve: the fetched tags will give us a new latest version
40
+ const latestVersion = pkg.latest.tag?.version || pkg.manifest.version
41
+ pkg.version = resolvePkgVersion(pkg.releaseType, latestVersion, pkg.manifest.version, pre)
42
+ pkg.manifest.version = pkg.version
43
+ pkg.tag = formatTag({name: pkg.name, version: pkg.version, format: pkg.config.tagFormat})
44
+ return 'ok'
45
+ }
46
+
47
+ // diverged — anomaly
48
+ log.warn(`preflight: ${pkg.tag} — diverged commits, skipping`)
49
+ return 'skip'
50
+ }
@@ -1,6 +1,6 @@
1
1
  import {getPkgConfig} from '../../../config.js'
2
2
  import {getLatest} from '../generators/meta.js'
3
- import {getRoot, getSha} from '../../api/git.js'
3
+ import {getRoot, getSha, getCommitTimestamp} from '../../api/git.js'
4
4
 
5
5
  /**
6
6
  * Global release context — one per `run()` invocation.
@@ -44,6 +44,7 @@ export const contextify = async (pkg, ctx) => {
44
44
  git: {
45
45
  sha: await getSha(pkg.absPath),
46
46
  root: await getRoot(pkg.absPath),
47
+ timestamp: await getCommitTimestamp(pkg.absPath),
47
48
  },
48
49
  }
49
50
  }
@@ -7,6 +7,7 @@ import {formatReleaseNotes} from '../generators/notes.js'
7
7
  import {ghPrepareAssets} from '../../api/gh.js'
8
8
  import {packTar, hashFile} from '../../tar.js'
9
9
 
10
+
10
11
  const filterActive = (names, pkg, {snapshot = false} = {}) =>
11
12
  names.filter(n => {
12
13
  const ch = channels[n]
@@ -49,7 +50,8 @@ export const pack = memoizeBy(async (pkg, ctx = pkg.ctx) => {
49
50
  const tmpPath = path.join(stageDir, `_tmp.${channel}.tar`)
50
51
  await packTar(tmpPath, manifest, files)
51
52
  const hash = await hashFile(tmpPath)
52
- const finalPath = path.join(stageDir, `parcel.${pkg.tag}.${channel}.${hash}.tar`)
53
+ const sha7 = ctx.git.sha.slice(0, 7)
54
+ const finalPath = path.join(stageDir, `parcel.${sha7}.${channel}.${pkg.tag}.${hash}.tar`)
53
55
  await fs.rename(tmpPath, finalPath)
54
56
  tars.push(finalPath)
55
57
  }
@@ -1,28 +1,18 @@
1
1
  import {memoizeBy} from '../../../util.js'
2
2
  import {exec} from '../exec.js'
3
- import {log} from '../../log.js'
4
3
  import {deliver, channels, runChannel} from '../../courier/index.js'
5
- import {pushTag} from '../../api/git.js'
6
4
 
7
5
  export const publish = memoizeBy(async (pkg, ctx = pkg.ctx) => {
8
6
  if (pkg.version !== pkg.manifest.version)
9
7
  throw new Error('package.json version not synced')
10
8
 
11
9
  const {run = exec, channels: channelNames = [], flags} = ctx
12
- const snapshot = !!flags.snapshot
13
10
  const {tars = []} = pkg
14
11
 
15
- if (!snapshot) {
16
- const {tag, config: {gitCommitterEmail, gitCommitterName}} = pkg
17
- ctx.git.tag = tag
18
- log.info(`push release tag ${tag}`)
19
- await pushTag({cwd: ctx.git.root, tag, gitCommitterEmail, gitCommitterName})
20
- }
21
-
22
12
  await deliver(tars, ctx.env)
23
13
 
24
14
  const cmd = channels.cmd
25
- if (channelNames.includes('cmd') && cmd?.when(pkg) && (!snapshot || cmd.snapshot))
15
+ if (channelNames.includes('cmd') && cmd?.when(pkg) && (!flags.snapshot || cmd.snapshot))
26
16
  await runChannel('cmd', pkg, run)
27
17
 
28
18
  pkg.published = true
@@ -0,0 +1,24 @@
1
+ import {$, glob, path} from 'zx-extra'
2
+ import {createReport, log} from '../log.js'
3
+ import {deliver} from '../courier/index.js'
4
+
5
+ const PARCELS_DIR = 'parcels'
6
+
7
+ export const runDeliver = async ({env, flags}) => {
8
+ const dir = typeof flags.deliver === 'string' ? flags.deliver : PARCELS_DIR
9
+ const report = createReport({flags})
10
+
11
+ $.memo = new Map()
12
+ $.report = report
13
+
14
+ report.setStatus('inspecting')
15
+
16
+ const tars = await glob(path.join(dir, 'parcel.*.tar'))
17
+ if (!tars.length) return report.setStatus('success').log(`no parcels in ${dir}`)
18
+
19
+ report.setStatus('delivering').log(`parcels: ${tars.length}`)
20
+ const result = await deliver(tars, env, {dryRun: flags.dryRun})
21
+ report.set('delivery', result).setStatus('success')
22
+
23
+ log.info(`done: ${result.delivered} delivered, ${result.skipped} skipped`)
24
+ }
@@ -0,0 +1,71 @@
1
+ import {$, within, path} from 'zx-extra'
2
+
3
+ import {log} from '../log.js'
4
+ import {traverseQueue} from '../depot/deps.js'
5
+ import {contextify} from '../depot/steps/contextify.js'
6
+ import {analyze} from '../depot/steps/analyze.js'
7
+ import {build} from '../depot/steps/build.js'
8
+ import {pack} from '../depot/steps/pack.js'
9
+ import {publish} from '../depot/steps/publish.js'
10
+ import {clean} from '../depot/steps/clean.js'
11
+ import {test} from '../depot/steps/test.js'
12
+ import {preflight} from '../depot/reconcile.js'
13
+ import {buildDirective} from '../courier/directive.js'
14
+
15
+ const PARCELS_DIR = 'parcels'
16
+
17
+ export const runPack = async ({cwd, env, flags}, ctx) => {
18
+ const {report, packages, queue, prev} = ctx
19
+
20
+ const forEachPkg = (cb) => traverseQueue({queue, prev, cb: (name) => within(async () => {
21
+ $.scope = name
22
+ await contextify(packages[name], ctx)
23
+ return cb(packages[name])
24
+ })})
25
+
26
+ report
27
+ .log('queue:', queue)
28
+ .log('graphs', ctx.graphs)
29
+
30
+ try {
31
+ await forEachPkg(async (pkg) => {
32
+ report.setStatus('analyzing', pkg.name)
33
+ await analyze(pkg)
34
+ report.set({
35
+ config: pkg.config,
36
+ version: pkg.version,
37
+ prevVersion: pkg.latest.tag?.version || pkg.manifest.version,
38
+ releaseType: pkg.releaseType,
39
+ tag: pkg.tag,
40
+ }, pkg.name)
41
+ })
42
+
43
+ report.setStatus('pending')
44
+
45
+ const packed = []
46
+ await forEachPkg(async (pkg) => {
47
+ if (!pkg.releaseType) { pkg.skipped = true; return report.setStatus('skipped', pkg.name) }
48
+ if (await preflight(pkg, pkg.ctx) === 'skip') { pkg.skipped = true; return report.setStatus('skipped', pkg.name) }
49
+ if (flags.build !== false) { report.setStatus('building', pkg.name); await build(pkg) }
50
+ if (flags.test !== false) { report.setStatus('testing', pkg.name); await test(pkg) }
51
+ if (flags.dryRun || flags.publish === false) return report.setStatus('success', pkg.name)
52
+
53
+ report.setStatus('packing', pkg.name); await pack(pkg)
54
+ if (flags.pack) { packed.push(pkg); return report.setStatus('packed', pkg.name) }
55
+
56
+ report.setStatus('publishing', pkg.name); await publish(pkg)
57
+ report.setStatus('success', pkg.name)
58
+ })
59
+
60
+ if (flags.pack && packed.length) {
61
+ const outputDir = path.resolve(ctx.cwd, typeof flags.pack === 'string' ? flags.pack : PARCELS_DIR)
62
+ await buildDirective(ctx, packed, outputDir)
63
+ }
64
+ } catch (e) {
65
+ report.error(e, e.stack).set('error', e).setStatus('failure')
66
+ throw e
67
+ } finally {
68
+ await clean(ctx)
69
+ }
70
+ report.setStatus('success').log('Great success!')
71
+ }
@@ -0,0 +1,71 @@
1
+ import {$, within} from 'zx-extra'
2
+
3
+ import {log} from '../log.js'
4
+ import {traverseQueue} from '../depot/deps.js'
5
+ import {contextify} from '../depot/steps/contextify.js'
6
+ import {analyze} from '../depot/steps/analyze.js'
7
+ import {clean} from '../depot/steps/clean.js'
8
+ import {preflight} from '../depot/reconcile.js'
9
+ import {consumeRebuildSignal} from '../courier/semaphore.js'
10
+ import {writeContext, buildContext} from '../depot/context.js'
11
+ import {getSha} from '../api/git.js'
12
+ import {setOutput, isRebuildTrigger} from '../api/gh.js'
13
+
14
+ export const runReceive = async ({cwd, env, flags}, ctx) => {
15
+ const {report, packages, queue, prev} = ctx
16
+
17
+ const sha = await getSha(cwd)
18
+ const sha7 = sha.slice(0, 7)
19
+
20
+ if (isRebuildTrigger(env)) {
21
+ const result = await consumeRebuildSignal(cwd, sha)
22
+ if (result?.exitCode !== 0 && result?.stderr?.includes('remote ref does not exist')) {
23
+ log.info(`rebuild signal already consumed by another process`)
24
+ await writeContext(cwd, {status: 'skip', reason: 'rebuild claimed by another process'})
25
+ setOutput('status', 'skip')
26
+ return report.setStatus('success')
27
+ }
28
+ log.info(`consumed rebuild signal for ${sha7}`)
29
+ }
30
+
31
+ const forEachPkg = (cb) => traverseQueue({queue, prev, cb: (name) => within(async () => {
32
+ $.scope = name
33
+ await contextify(packages[name], ctx)
34
+ return cb(packages[name])
35
+ })})
36
+
37
+ try {
38
+ await forEachPkg(async (pkg) => {
39
+ report.setStatus('analyzing', pkg.name)
40
+ await analyze(pkg)
41
+ })
42
+
43
+ await forEachPkg(async (pkg) => {
44
+ if (!pkg.releaseType) { pkg.skipped = true; return }
45
+ if (await preflight(pkg, pkg.ctx) === 'skip') { pkg.skipped = true; return }
46
+ })
47
+
48
+ const context = buildContext(packages, queue, sha, {
49
+ channelNames: ctx.channels,
50
+ snapshot: !!flags.snapshot,
51
+ })
52
+ await writeContext(cwd, context)
53
+
54
+ const count = Object.keys(context.packages).length
55
+ if (count === 0) {
56
+ log.info('nothing to release')
57
+ await writeContext(cwd, {status: 'skip', reason: 'nothing to release'})
58
+ setOutput('status', 'skip')
59
+ } else {
60
+ log.info(`${count} package(s) to release`)
61
+ setOutput('status', 'proceed')
62
+ }
63
+ } catch (e) {
64
+ report.error(e, e.stack).setStatus('failure')
65
+ throw e
66
+ } finally {
67
+ await clean(ctx)
68
+ }
69
+
70
+ report.setStatus('success')
71
+ }