rvlib-shipkit 1.8.0 → 1.9.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.
- package/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/assets/gen/comfyts-gen.ts +31 -14
- package/src/assets/gen/generate-roll.ts +20 -7
- package/src/assets/kit-config.ts +9 -0
- package/src/assets/kits/identity-kit.ts +7 -1
- package/src/assets/run-assets.ts +3 -3
- package/src/assets/server/studio-server.ts +3 -3
- package/src/cli/cmd_Ci.ts +5 -4
- package/src/collections/providers.ts +1 -1
- package/src/core/check-runner.ts +13 -4
- package/src/core/discover-checks.ts +37 -8
- package/src/core/fix-scope.ts +5 -0
- package/src/core/floor-resolve.ts +2 -1
- package/src/core/git-root.ts +22 -12
- package/src/core/one-lib.ts +4 -2
- package/src/core/run-ci.ts +12 -11
- package/src/core/slots.ts +36 -6
- package/src/home/gui-app/ShipkitGui.swift +96 -0
- package/src/home/resources.ts +3 -0
- package/src/plan/planner.ts +21 -4
- package/src/plan/repo-source.ts +7 -3
- package/src/release/run-release.ts +17 -8
- package/src/release/unit-gate.ts +12 -5
- package/src/repo-config.ts +7 -0
- package/src/serve/hub-runtime.ts +3 -2
- package/src/serve/project-gutter.ts +5 -2
- package/src/serve/run-lanes.ts +7 -3
- package/src/smoke/run-smoke.ts +13 -6
- package/src/suite/judge-suites.ts +52 -4
- package/src/suite/suite-cli.ts +21 -13
- package/src/ts/typecheck-cache.ts +11 -0
- package/src/ts/typecheck-cli.ts +2 -1
|
@@ -41,7 +41,8 @@ export const DEFAULT_BUDGET_MS = 2000
|
|
|
41
41
|
* 5s default while the preload said otherwise. */
|
|
42
42
|
export const FULL_TIMEOUT_MS = 120_000
|
|
43
43
|
/** the wall ceiling of one `bun test` phase. bun's per test timeout never
|
|
44
|
-
* fires on a worker stuck in a SYNCHRONOUS call,
|
|
44
|
+
* fires on a worker stuck in a SYNCHRONOUS call, or in `expect(p).resolves`
|
|
45
|
+
* or `.rejects` on a promise that never settles, and bun's spawnSync has been
|
|
45
46
|
* seen spinning on a child that already exited (planPhases avoids the known
|
|
46
47
|
* trigger), so without this a hang lasts until something outside kills it. The quick
|
|
47
48
|
* tier answers in seconds, so two minutes is far from any real run. */
|
|
@@ -182,14 +183,16 @@ export function poolWorkers(p: { cores: number; size: number; held: number }): n
|
|
|
182
183
|
|
|
183
184
|
type Timed = { file: string; ms: number }
|
|
184
185
|
|
|
185
|
-
export function judgeSuites(p: { timings: Record<string, number>; tests: TestsConfig; tier: 'quick' | 'full' }): { over: Timed[]; noLongerSlow: Timed[]; slowest: Timed[] } {
|
|
186
|
+
export function judgeSuites(p: { timings: Record<string, number>; tests: TestsConfig; tier: 'quick' | 'full'; red?: string[] }): { over: Timed[]; noLongerSlow: Timed[]; slowest: Timed[] } {
|
|
186
187
|
const budget = p.tests.budgetMs ?? DEFAULT_BUDGET_MS
|
|
187
188
|
const slow = new Set((p.tests.slow ?? []).map((s) => normSuitePath(s.file)))
|
|
189
|
+
const red = new Set((p.red ?? []).map(normSuitePath))
|
|
188
190
|
const timed = Object.entries(p.timings).map(([file, ms]) => ({ file: normSuitePath(file), ms })).sort((a, b) => b.ms - a.ms)
|
|
189
191
|
return {
|
|
190
192
|
// the budget guards the tier every edit waits on. The full tier is
|
|
191
|
-
// where slow suites belong, and it only reports
|
|
192
|
-
|
|
193
|
+
// where slow suites belong, and it only reports. A red suite took
|
|
194
|
+
// the time of its failure (a timeout, a crash), which its red explains
|
|
195
|
+
over: p.tier === 'quick' ? timed.filter((t) => t.ms > budget && !slow.has(t.file) && !red.has(t.file)) : [],
|
|
193
196
|
// half the budget, so a declaration does not flap on timing noise
|
|
194
197
|
noLongerSlow: timed.filter((t) => slow.has(t.file) && t.ms < budget / 2),
|
|
195
198
|
slowest: timed.slice(0, 3),
|
|
@@ -253,6 +256,20 @@ export function confirmOver(suspects: Timed[], alone: Record<string, AloneRun>,
|
|
|
253
256
|
return { over: judged.filter((c) => c.over != null), cleared: judged.filter((c) => c.over == null) }
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
/** a lone run over the budget on CPU runs once more. Under load the
|
|
260
|
+
* scheduler can spill a lone run onto the efficiency cores, where the same
|
|
261
|
+
* retired instructions cost up to 4x the CPU time (measured on Apple
|
|
262
|
+
* silicon), and a spill only ever raises the figure. A wait or a wall
|
|
263
|
+
* verdict is not CPU, so it keeps its one run */
|
|
264
|
+
export const retryAlone = (t: Timed, run: AloneRun, budgetMs: number, load: MachineLoad): boolean =>
|
|
265
|
+
confirmOver([t], { [t.file]: run }, budgetMs, load).over[0]?.over === 'cpu'
|
|
266
|
+
|
|
267
|
+
/** the run charged less, as confirmOver charges it: the smaller of CPU and wall */
|
|
268
|
+
export const smallerRun = (a: AloneRun, b: AloneRun): AloneRun => {
|
|
269
|
+
const charge = (r: AloneRun): number => Math.min(r.cpuMs ?? r.wallMs, r.wallMs)
|
|
270
|
+
return charge(b) < charge(a) ? b : a
|
|
271
|
+
}
|
|
272
|
+
|
|
256
273
|
const secs = (ms: number): string => `${(ms / 1000).toFixed(1)}s`
|
|
257
274
|
|
|
258
275
|
/** the line a confirmed suite fails with */
|
|
@@ -358,6 +375,37 @@ export function failLines(output: string): Map<string, string> {
|
|
|
358
375
|
|
|
359
376
|
const clip = (s: string): string => (s.length > 300 ? `${s.slice(0, 299)}…` : s)
|
|
360
377
|
|
|
378
|
+
/** the clause the headline carries for the gate line: the command phase one
|
|
379
|
+
* ran, and the flags the runner added to the script's own. Per file ignores
|
|
380
|
+
* stay out, the slow and serial counts already say what they leave out */
|
|
381
|
+
export function ranAs(phase: string[], rawArgs: string[]): string {
|
|
382
|
+
const shown = phase.filter((a) => !a.startsWith('--path-ignore-patterns'))
|
|
383
|
+
const added = shown.filter((a) => !rawArgs.includes(a))
|
|
384
|
+
return `as \`${['bun', 'test', ...shown].join(' ')}\`${added.length > 0 ? ` (added ${added.join(' ')})` : ''}`
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** the suites bun lists under `Interrupted while still running:` when a
|
|
388
|
+
* phase is killed, one indented `<file> (<n>s)` line each */
|
|
389
|
+
export function interruptedSuites(output: string): string[] {
|
|
390
|
+
const out: string[] = []
|
|
391
|
+
let inBlock = false
|
|
392
|
+
for (const line of output.split('\n')) {
|
|
393
|
+
if (/^Interrupted while still running:\s*$/.test(line)) { inBlock = true; continue }
|
|
394
|
+
if (!inBlock) continue
|
|
395
|
+
const m = /^\s+(\S+)\s+\(\d+(?:\.\d+)?m?s\)\s*$/.exec(line)
|
|
396
|
+
if (m == null) { inBlock = false; continue }
|
|
397
|
+
out.push(normSuitePath(m[1]!))
|
|
398
|
+
}
|
|
399
|
+
return out
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** the line a phase killed at its ceiling fails with. Measured on bun 1.4.3:
|
|
403
|
+
* a plain await of a promise that never settles fails at the per test
|
|
404
|
+
* timeout, while `expect(p).resolves` or `.rejects` on one blocks the worker
|
|
405
|
+
* past it, as a synchronous loop does */
|
|
406
|
+
export const stallWords = (name: string, ceilingMs: number, files: string[]): string =>
|
|
407
|
+
`${name} ran past its ${secs(ceilingMs)} ceiling and was killed${files.length > 0 ? ` with ${files.join(', ')} still running` : ''}: a test blocked its worker past the per test timeout, either a synchronous loop or expect(...).resolves or .rejects on a promise that never settles`
|
|
408
|
+
|
|
361
409
|
/** the suites bun names as a crashed parallel worker */
|
|
362
410
|
export function crashedWorkers(output: string): { file: string; signal: string }[] {
|
|
363
411
|
return [...output.matchAll(/^\s*✗\s+(\S+)\s+\(worker crashed: (SIG[A-Z0-9]+)\)\s*$/gm)].map((m) => ({ file: normSuitePath(m[1]!), signal: m[2]! }))
|
package/src/suite/suite-cli.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { SUITE_READS_ENV, SUITE_ROOT_ENV, SUITE_SELECT_ENV } from './reads-env.t
|
|
|
20
20
|
import { changedKeys, dropStaticEdges, extras, isBunSuite, envFilesOf, foldReads, loadErrorModules, loadState, preloadsOf, saveState, selectStatePath, selectSuites, snapshot, suiteFiles, taintedSuites, walkGraph, type GraphEntry, type Selection, type SelectState } from './select.ts'
|
|
21
21
|
import { gitRoot } from '../core/git-root.ts'
|
|
22
22
|
import { loadConfig } from '../ts/config.ts'
|
|
23
|
-
import { aloneRunsNeeded, bunfigTest, type BunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers, overWords, DEFAULT_BUDGET_MS, FULL_CEILING_MS, failedSuites, failLines, judgeSuites, normSuitePath, parseSuiteFlags, planPhases, QUICK_CEILING_MS, poolWorkers } from './judge-suites.ts'
|
|
23
|
+
import { aloneRunsNeeded, bunfigTest, type BunfigTest, readPhaseCpu, SUITE_CPU_ENV, type AloneRun, type Confirmed, confirmOver, crashedWorkers, interruptedSuites, overWords, ranAs, retryAlone, smallerRun, stallWords, DEFAULT_BUDGET_MS, FULL_CEILING_MS, failedSuites, failLines, judgeSuites, normSuitePath, parseSuiteFlags, planPhases, QUICK_CEILING_MS, poolWorkers } from './judge-suites.ts'
|
|
24
24
|
|
|
25
25
|
type Phase = { code: number; signal: string | null; stalled: boolean; out: string; timings: Record<string, number>; cpuMs: number | null }
|
|
26
26
|
|
|
@@ -194,7 +194,9 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
194
194
|
const ran = phases.reduce((n, p) => n + lastCount(p.out, /Ran (\d+) tests?/g), 0)
|
|
195
195
|
const failedTests = phases.reduce((n, p) => n + lastCount(p.out, /^\s*(\d+) fail$/gm), 0)
|
|
196
196
|
const timings = Object.assign({}, ...phases.map((p) => p.timings)) as Record<string, number>
|
|
197
|
-
const
|
|
197
|
+
const crashed = crashedWorkers(p1.out)
|
|
198
|
+
const red = new Set([...phases.flatMap((p) => failedSuites(p.out)), ...crashed.map((c) => c.file)])
|
|
199
|
+
const judged = judgeSuites({ timings, tests, tier, red: [...red] })
|
|
198
200
|
const slowest = judged.slowest.length > 0 ? ` (slowest: ${judged.slowest.map((t) => `${t.file} ${secs(t.ms)}`).join(', ')})` : ''
|
|
199
201
|
|
|
200
202
|
// a parallel timing carries every other suite's load, so a suspect
|
|
@@ -205,10 +207,20 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
205
207
|
const parallelCpu = phaseCpu.cpu
|
|
206
208
|
const { rerun, cleared: clearedInPhase } = aloneRunsNeeded(judged.over, parallelCpu, budget, load)
|
|
207
209
|
const aloneAt = Date.now()
|
|
208
|
-
|
|
209
|
-
const run = await runPhase([...plan.alone, t.file], join(dir,
|
|
210
|
+
const runAlone = async (t: { file: string }, name: string): Promise<AloneRun | null> => {
|
|
211
|
+
const run = await runPhase([...plan.alone, t.file], join(dir, `${name}.json`), ceilingMs, tmp, false, null, rec)
|
|
210
212
|
const wallMs = Object.entries(run.timings).find(([f]) => normSuitePath(f) === t.file)?.[1]
|
|
211
|
-
|
|
213
|
+
return wallMs == null ? null : { wallMs, cpuMs: run.cpuMs }
|
|
214
|
+
}
|
|
215
|
+
for (const [i, t] of rerun.entries()) {
|
|
216
|
+
const first = await runAlone(t, `alone-${i}`)
|
|
217
|
+
if (first == null) continue
|
|
218
|
+
aloneRuns[t.file] = first
|
|
219
|
+
if (!retryAlone(t, first, budget, load)) continue
|
|
220
|
+
const second = await runAlone(t, `alone-${i}-again`)
|
|
221
|
+
if (second == null) continue
|
|
222
|
+
aloneRuns[t.file] = smallerRun(first, second)
|
|
223
|
+
notes.push(`note: ${t.file} ran alone twice, ${secs(first.cpuMs ?? first.wallMs)} then ${secs(second.cpuMs ?? second.wallMs)} of CPU, and the smaller stands: a lone run the scheduler spills onto slower cores burns more CPU for the same work`)
|
|
212
224
|
}
|
|
213
225
|
const aloneMs = Date.now() - aloneAt
|
|
214
226
|
const { over, cleared } = rerun.length > 0 ? confirmOver(rerun, aloneRuns, budget, load) : { over: [], cleared: [] }
|
|
@@ -218,12 +230,9 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
218
230
|
// a ceiling kill makes bun report every worker it held as crashed, and
|
|
219
231
|
// those are the suites that never finished, not an outside kill
|
|
220
232
|
const stalledIn = (name: string, p: Phase | null, files: string[]): string | null =>
|
|
221
|
-
p?.stalled === true
|
|
222
|
-
? `${name} ran past its ${secs(ceilingMs)} ceiling and was killed${files.length > 0 ? ` with ${files.join(', ')} still running` : ''}: a test worker stopped answering, most likely stuck in a synchronous call, which no per test timeout can interrupt`
|
|
223
|
-
: null
|
|
224
|
-
const crashed = crashedWorkers(p1.out)
|
|
233
|
+
p?.stalled === true ? stallWords(name, ceilingMs, [...new Set([...files, ...interruptedSuites(p.out)])]) : null
|
|
225
234
|
const killed = p1.stalled ? [] : crashed.map((c) => `killed by ${c.signal} while running ${c.file} (out of memory, or killed from outside)`)
|
|
226
|
-
const stalls = [stalledIn('the parallel phase', p1, crashed.map((c) => c.file)), stalledIn('the serial phase', p2, phaseFiles(serialArgs ?? [], rawArgs).filter((a) => p2?.timings[normSuitePath(a)] == null))]
|
|
235
|
+
const stalls = [stalledIn('the parallel phase', p1, crashed.map((c) => c.file)), stalledIn('the serial phase', p2, phaseFiles(serialArgs ?? [], rawArgs).filter((a) => p2?.timings[normSuitePath(a)] == null).map(normSuitePath))]
|
|
227
236
|
for (const line of stalls) if (line != null) killed.unshift(line)
|
|
228
237
|
if (p1.signal != null) killed.push(`killed by ${p1.signal} during the parallel run (out of memory, or killed from outside)`)
|
|
229
238
|
if (p2?.signal != null) {
|
|
@@ -233,7 +242,6 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
233
242
|
// the record behind the hub's test files to fix: every suite this run
|
|
234
243
|
// timed, with the alone timing where one was taken, and red when a
|
|
235
244
|
// test in it failed or its worker died
|
|
236
|
-
const red = new Set([...phases.flatMap((p) => failedSuites(p.out)), ...crashedWorkers(p1.out).map((c) => c.file)])
|
|
237
245
|
const overSet = new Set(over.map((o) => o.file))
|
|
238
246
|
ctx.complete = killed.length === 0
|
|
239
247
|
ctx.out = phases.map((p) => p.out).join('\n')
|
|
@@ -241,7 +249,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
241
249
|
const at = Date.now()
|
|
242
250
|
const alone = new Map([...over, ...cleared, ...clearedInPhase].map((t) => [t.file, t.ms]))
|
|
243
251
|
const why = failLines(phases.map((p) => p.out).join('\n'))
|
|
244
|
-
for (const c of
|
|
252
|
+
for (const c of crashed) if (!why.has(c.file)) why.set(c.file, `its worker crashed: ${c.signal}`)
|
|
245
253
|
// which phase timed each file, and the CPU spent on it: an alone
|
|
246
254
|
// rerun, the parallel phase's own figure, or a phase that ran that one file
|
|
247
255
|
const phaseOf = (f: string): SuitePhase => (p2 != null && Object.keys(p2.timings).some((k) => normSuitePath(k) === f) ? 'serial' : parallel ? 'parallel' : 'sequential')
|
|
@@ -289,7 +297,7 @@ async function runSuites(argv: string[], ctx: SelectCtx): Promise<number> {
|
|
|
289
297
|
if (fails.length > 0) { console.log(fails[0]); return 1 }
|
|
290
298
|
const serialCount = serialArgs != null ? phaseFiles(serialArgs, rawArgs).length : 0
|
|
291
299
|
const chosen = ctx.sel?.picked != null ? `, the ${ctx.sel.picked.length} of ${ctx.sel.total} a change reaches` : ''
|
|
292
|
-
console.log(`ok: Ran ${ran} tests across ${Object.keys(timings).length || '?'} suites${chosen}, ${parallel ? `parallel on ${workers} workers` : 'one at a time'}${serialCount > 0 ? ` then ${serialCount} serial` : ''}, ${tier} tier${slowest}`)
|
|
300
|
+
console.log(`ok: Ran ${ran} tests across ${Object.keys(timings).length || '?'} suites${chosen}, ${parallel ? `parallel on ${workers} workers` : 'one at a time'}${serialCount > 0 ? ` then ${serialCount} serial` : ''}, ${tier} tier, ${ranAs(plan.parallel, rawArgs)}${slowest}`)
|
|
293
301
|
return 0
|
|
294
302
|
} finally {
|
|
295
303
|
rmSync(dir, { recursive: true, force: true })
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// the gate's typecheck keeps its own incremental cache, one file per project.
|
|
2
|
+
// a tsconfig's tsBuildInfoFile is shared by every tsc that runs that config (a
|
|
3
|
+
// watcher task, a raw script), and a verdict read off a cache another process
|
|
4
|
+
// writes is not a verdict on this tree. no fs here: it only names paths.
|
|
5
|
+
import { basename, dirname, join } from 'path'
|
|
6
|
+
|
|
7
|
+
export const gateBuildInfoArgs = (tsconfig: string): string[] => [
|
|
8
|
+
'--incremental',
|
|
9
|
+
'--tsBuildInfoFile',
|
|
10
|
+
join(dirname(tsconfig), 'node_modules/.cache/shipkit', `${basename(tsconfig, '.json')}.tsbuildinfo`),
|
|
11
|
+
]
|
package/src/ts/typecheck-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// who fixes the one error tsc showed and re-runs into a second one has been
|
|
11
11
|
// told the truth twice and believed it once.
|
|
12
12
|
import { resolveTypecheckProjects } from 'rvlib-shipkit/src/ts/tsconfig-projects.ts'
|
|
13
|
+
import { gateBuildInfoArgs } from 'rvlib-shipkit/src/ts/typecheck-cache.ts'
|
|
13
14
|
|
|
14
15
|
export function typecheckCli(argv: string[]): number {
|
|
15
16
|
const [tsc, tsconfig] = argv
|
|
@@ -30,7 +31,7 @@ export function typecheckCli(argv: string[]): number {
|
|
|
30
31
|
|
|
31
32
|
let worst = 0
|
|
32
33
|
for (const p of resolved.projects) {
|
|
33
|
-
const r = Bun.spawnSync([tsc, '--noEmit', '-p', p], { stdout: 'inherit', stderr: 'inherit' })
|
|
34
|
+
const r = Bun.spawnSync([tsc, '--noEmit', '-p', p, ...gateBuildInfoArgs(p)], { stdout: 'inherit', stderr: 'inherit' })
|
|
34
35
|
if (r.exitCode !== 0) {
|
|
35
36
|
console.log(`FAIL: ${p}`)
|
|
36
37
|
worst = r.exitCode ?? 1
|