rvlib-shipkit 1.1.1 → 1.3.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 +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
package/src/core/check-runner.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { parallelCap } from './parallel.ts'
|
|
|
39
39
|
import { machinePool, SLOT_HELD_ENV, takeSlot, type SlotPool } from './slots.ts'
|
|
40
40
|
import { budgetLine, machineLoad } from './budget.ts'
|
|
41
41
|
import { killTree, spawnWithTimeout } from './spawn-timeout.ts'
|
|
42
|
-
import { formatLine, parseWireOutput, type Line } from './wire.ts'
|
|
42
|
+
import { FOREVER_MS, formatLine, parseWireOutput, type Line } from './wire.ts'
|
|
43
43
|
import { offLine, type WhenDecl } from './when.ts'
|
|
44
44
|
import { runCheckBody, type CheckDef, type CheckEnv } from '../check.ts'
|
|
45
45
|
import { Ledger, stampCached } from '../ledger/ledger.ts'
|
|
@@ -47,7 +47,7 @@ import { attachedStamp } from './outcome-words.ts'
|
|
|
47
47
|
import type { RunLog, RunStatus } from '../ledger/run-log.ts'
|
|
48
48
|
import { liveStateOf } from '../ledger/live-runs.ts'
|
|
49
49
|
import { CHECK_ID_ENV } from '../ledger/suite-log.ts'
|
|
50
|
-
import { rememberWorld, replayWorld, type WorldCache } from '../ledger/world-cache.ts'
|
|
50
|
+
import { rememberWorld, replayWorld, worldKeyOf, type WorldCache } from '../ledger/world-cache.ts'
|
|
51
51
|
import { extractProblems } from '../problems/extract.ts'
|
|
52
52
|
import { feedsProblemsPane, type Population, type Problem } from '../problems/problem-line.ts'
|
|
53
53
|
|
|
@@ -191,6 +191,9 @@ export type RunChecksOpts = {
|
|
|
191
191
|
* gate the whole repo, since the check's own gate already ran. */
|
|
192
192
|
exclude?: string[]
|
|
193
193
|
print?: (line: string) => void
|
|
194
|
+
/** where the green summary goes when the caller prints its own verdict
|
|
195
|
+
* (`shipkit ci` does). Default: print */
|
|
196
|
+
detail?: (line: string) => void
|
|
194
197
|
color?: boolean
|
|
195
198
|
onCheck?: (e: CheckEvent) => void
|
|
196
199
|
}
|
|
@@ -227,11 +230,21 @@ function closure(checks: RunnableCheck[], ids: string[]): Set<string> {
|
|
|
227
230
|
return keep
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
/** how many tests a suite ran, read from the headline it prints last
|
|
234
|
+
* (`Ran 920 tests across 12 files.`), null when it printed none */
|
|
235
|
+
export function testsRan(lastLine: string): number | null {
|
|
236
|
+
const m = /Ran (\d+) tests?/.exec(lastLine)
|
|
237
|
+
return m == null ? null : Number(m[1])
|
|
238
|
+
}
|
|
239
|
+
|
|
230
240
|
export type Selection = {
|
|
231
241
|
/** what this run measures */
|
|
232
242
|
selected: RunnableCheck[]
|
|
233
243
|
/** turned off by name, with the reason: never run, always reported */
|
|
234
244
|
off: RunnableCheck[]
|
|
245
|
+
/** off, and outside what `--only` asked about: reported where the caller
|
|
246
|
+
* keeps what nobody acts on */
|
|
247
|
+
offElsewhere?: RunnableCheck[]
|
|
235
248
|
/** not asked about by this run, and why */
|
|
236
249
|
parked: { id: string; text: string }[]
|
|
237
250
|
/** asked for, and it never said whether it blocks */
|
|
@@ -337,6 +350,8 @@ export function selectChecks(o: Pick<RunChecksOpts, 'checks' | 'audience' | 'ful
|
|
|
337
350
|
if (o.onlyIds != null && o.onlyIds.length > 0) {
|
|
338
351
|
const keep = closure(o.checks, onlyIds)
|
|
339
352
|
selected = selected.filter((u) => keep.has(u.id))
|
|
353
|
+
// a narrowed run says nothing about the checks it never asked about
|
|
354
|
+
return { selected, off: off.filter((u) => keep.has(u.id)), offElsewhere: off.filter((u) => !keep.has(u.id)), parked, undeclared, errors }
|
|
340
355
|
}
|
|
341
356
|
return { selected, off, parked, undeclared, errors }
|
|
342
357
|
}
|
|
@@ -346,35 +361,57 @@ export function selectChecks(o: Pick<RunChecksOpts, 'checks' | 'audience' | 'ful
|
|
|
346
361
|
* was still streaming into, so the surviving FAIL line pointed at a path
|
|
347
362
|
* that no longer existed. Now each run owns `run-<pid>-<started>/`.
|
|
348
363
|
*
|
|
349
|
-
* Old run dirs are swept on the way in, and only ones nothing
|
|
350
|
-
* the pid must not be running
|
|
351
|
-
* window, so a recycled pid cannot cost a live run its logs. */
|
|
364
|
+
* Old run dirs are swept on the way in (sweepRunDirs), and only ones nothing
|
|
365
|
+
* can be using: the pid must not be running, so a live run keeps its logs. */
|
|
352
366
|
function openRunDir(base: string): string {
|
|
353
367
|
// the pid plus the millisecond separates two PROCESSES; the counter
|
|
354
368
|
// separates two runs inside one (the TUI can start a second before the
|
|
355
369
|
// clock ticks), so no run ever writes into another's dir
|
|
356
370
|
const dir = join(base, `run-${process.pid}-${Date.now()}-${runSeq++}`)
|
|
357
371
|
mkdirSync(dir, { recursive: true })
|
|
358
|
-
|
|
372
|
+
sweepRunDirs(base, Date.now(), { keep: basename(dir) })
|
|
373
|
+
return dir
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** how long a finished run's logs stay readable, and the pid-reuse guard */
|
|
377
|
+
export const RUN_DIR_KEEP_MS = 6 * 60 * 60 * 1000
|
|
378
|
+
/** the most finished runs whose logs stay, newest first */
|
|
379
|
+
export const RUN_DIR_MAX = 40
|
|
380
|
+
|
|
381
|
+
const RUN_ENTRY = /^(?:run|releases)-(\d+)-(\d+)(?:-\d+)?(?:\.json(?:\.lock)?)?$/
|
|
382
|
+
|
|
383
|
+
const pidRuns = (pid: number): boolean => {
|
|
384
|
+
try { process.kill(pid, 0); return true } catch (e) { return (e as { code?: string }).code === 'EPERM' }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** the run dirs under a log base, and the per run release survey files a
|
|
388
|
+
* killed run leaves beside them: both carry the pid and the start time */
|
|
389
|
+
export function runEntries(base: string): { name: string; pid: number; started: number }[] {
|
|
359
390
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const m = /^(?:run|releases)-(\d+)-(\d+)(?:-\d+)?(?:\.json(?:\.lock)?)?$/.exec(name)
|
|
365
|
-
if (m == null || name === basename(dir)) continue
|
|
366
|
-
if (Number(m[2]) > cutoff) continue
|
|
367
|
-
try { process.kill(Number(m[1]), 0); continue } catch (e) { if ((e as { code?: string }).code === 'EPERM') continue }
|
|
368
|
-
rmSync(join(base, name), { recursive: true, force: true })
|
|
369
|
-
}
|
|
391
|
+
return readdirSync(base).flatMap((name) => {
|
|
392
|
+
const m = RUN_ENTRY.exec(name)
|
|
393
|
+
return m == null ? [] : [{ name, pid: Number(m[1]), started: Number(m[2]) }]
|
|
394
|
+
})
|
|
370
395
|
} catch {
|
|
371
|
-
|
|
396
|
+
return []
|
|
372
397
|
}
|
|
373
|
-
return dir
|
|
374
398
|
}
|
|
375
399
|
|
|
376
|
-
/**
|
|
377
|
-
|
|
400
|
+
/** the run logs' cap: every entry past the keep window (no run lasts that
|
|
401
|
+
* long, so a live pid there is a long lived process's finished run, or a
|
|
402
|
+
* reused pid), then the oldest past `max` whose pid no longer runs. `keep`
|
|
403
|
+
* spares the caller's own dir */
|
|
404
|
+
export function sweepRunDirs(base: string, now: number, o: { keep?: string; keepMs?: number; max?: number } = {}): void {
|
|
405
|
+
const all = runEntries(base).sort((a, b) => b.started - a.started)
|
|
406
|
+
const cutoff = now - (o.keepMs ?? RUN_DIR_KEEP_MS)
|
|
407
|
+
all.forEach((e, i) => {
|
|
408
|
+
if (e.name === o.keep) return
|
|
409
|
+
const old = e.started <= cutoff
|
|
410
|
+
if (!old && i < (o.max ?? RUN_DIR_MAX)) return
|
|
411
|
+
if (!old && pidRuns(e.pid)) return
|
|
412
|
+
try { rmSync(join(base, e.name), { recursive: true, force: true }) } catch { /* an unreadable log base is a housekeeping miss */ }
|
|
413
|
+
})
|
|
414
|
+
}
|
|
378
415
|
let runSeq = 0
|
|
379
416
|
|
|
380
417
|
type Live = RunnableCheck & CheckResult & {
|
|
@@ -423,9 +460,9 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
423
460
|
|
|
424
461
|
// OFF, in ONE place: skipped by name, never run, never stored, and the
|
|
425
462
|
// reason printed where the verdict would have been
|
|
426
|
-
for (const u of sel.off) {
|
|
463
|
+
for (const u of [...sel.off, ...(sel.offElsewhere ?? [])]) {
|
|
427
464
|
const line: Line = { kind: 'skip', text: offLine(u.id, u.when) }
|
|
428
|
-
emit(line)
|
|
465
|
+
if (sel.off.includes(u)) emit(line); else (o.detail ?? print)(formatLine(line, color))
|
|
429
466
|
report(u, { id: u.id, status: 'skip', advisory: false, replayed: false, ms: 0, exitCode: null, lines: [line], problems: [], lastLine: line.text, note: 'off', logPath: null })
|
|
430
467
|
}
|
|
431
468
|
// `--only` that leaves nothing to run asked a question nobody answers, and
|
|
@@ -477,10 +514,15 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
477
514
|
* or that a different shipkit measured it. */
|
|
478
515
|
const worldKey = (u: RunnableCheck): string => {
|
|
479
516
|
let code = ''
|
|
480
|
-
try { code = o.ledger?.layer.keyFor(u.id).codeKey ?? '' } catch { code = '' }
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
517
|
+
if (u.ttlMs < FOREVER_MS) try { code = o.ledger?.layer.keyFor(u.id).codeKey ?? '' } catch { code = '' }
|
|
518
|
+
return worldKeyOf({
|
|
519
|
+
ttlMs: u.ttlMs,
|
|
520
|
+
toolchain: u.ttlMs < FOREVER_MS ? o.ledger?.layer.toolchainId ?? '' : '',
|
|
521
|
+
code,
|
|
522
|
+
rotation: u.rotation ?? '',
|
|
523
|
+
// a check that writes answers a different question when frozen
|
|
524
|
+
frozen: u.writes === true ? process.env['SHIPKIT_FROZEN'] === '1' : null,
|
|
525
|
+
})
|
|
484
526
|
}
|
|
485
527
|
|
|
486
528
|
// a non-blocking failure becomes a WARNING here, at the source: printing
|
|
@@ -510,10 +552,18 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
510
552
|
// baked into a remembered line comes back on a replay as if this run
|
|
511
553
|
// had measured it (ledger/ledger.ts rule 5)
|
|
512
554
|
const head = u.lines[0]
|
|
555
|
+
// a suite's count rides on its own line: `test 920 tests (1.4s)`
|
|
556
|
+
const ran = u.status === 'pass' || u.status === 'warn' ? testsRan(u.lastLine) : null
|
|
557
|
+
const counted = ran != null && head != null && head.kind === 'ok' && (head.text === u.id || head.text.startsWith(`${u.id} (`))
|
|
558
|
+
const id = counted ? `${u.id} ${ran} test${ran === 1 ? '' : 's'}` : u.id
|
|
513
559
|
const shown = u.synthetic && !u.replayed && head != null
|
|
514
|
-
? [{ ...head, text: `${
|
|
515
|
-
: u.lines
|
|
516
|
-
|
|
560
|
+
? [{ ...head, text: `${id} (${(u.ms / 1000).toFixed(1)}s)${u.tail}` }, ...u.lines.slice(1)]
|
|
561
|
+
: counted ? [{ ...head, text: `${id}${head.text.slice(u.id.length)}` }, ...u.lines.slice(1)] : u.lines
|
|
562
|
+
// a caller printing its own verdict wants one line per green check and
|
|
563
|
+
// only the warnings of a warned one; a failure prints whole
|
|
564
|
+
const quiet = (l: Line, i: number): boolean =>
|
|
565
|
+
o.detail != null && (u.status === 'pass' ? i > 0 : u.status === 'warn' || u.advisory ? l.kind === 'ok' || l.kind === 'note' || l.kind === 'skip' : false)
|
|
566
|
+
for (const [i, l] of shown.entries()) if (quiet(l, i)) o.detail!(formatLine(l, color)); else emit(l)
|
|
517
567
|
if (u.status === 'fail' && !u.advisory && u.logPath != null)
|
|
518
568
|
emit({ kind: 'fix-fail', text: `full log: ${u.logPath}` })
|
|
519
569
|
report(u, { id: u.id, status: u.status, advisory: u.advisory, replayed: u.replayed, ms: u.ms, exitCode: u.exitCode, lines: u.lines, problems: u.problems, lastLine: u.lastLine, note: u.note, logPath: u.logPath }, finishedAt, u.attached)
|
|
@@ -820,7 +870,7 @@ export async function runChecks(o: RunChecksOpts): Promise<RunChecksSummary> {
|
|
|
820
870
|
const warned = results.filter((r) => r.status === 'warn' || (r.status === 'fail' && r.advisory))
|
|
821
871
|
const ran = results.filter((r) => r.status !== 'skip').length
|
|
822
872
|
if (blocked.length === 0 && !interrupted && results.length > 0)
|
|
823
|
-
|
|
873
|
+
(o.detail ?? print)(formatLine({ kind: 'ok', text: warned.length === 0 ? `all ${ran} check(s) green` : `all ${ran} check(s) passed, ${warned.length} with a warning: ${warned.map((r) => r.id).join(', ')}` }, color))
|
|
824
874
|
return {
|
|
825
875
|
ok: blocked.length === 0 && !interrupted,
|
|
826
876
|
interrupted,
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Skipped dirs: node_modules, dist, tmp, references (templates for other
|
|
19
19
|
// repos), and every dot-dir.
|
|
20
|
+
import { RETIRED_CHECK_IDS } from './retired-checks.ts'
|
|
20
21
|
import { readdirSync, readFileSync, realpathSync, statSync } from 'fs'
|
|
21
22
|
import { basename, join, relative } from 'path'
|
|
22
23
|
import { check, type CheckDef, type CheckFrom } from '../check.ts'
|
|
23
24
|
|
|
24
|
-
import { CHECK_SUFFIX as SUFFIX, fallbackNames, isLegacyCheckSource, SKIPPED_DIRS as IGNORED_DIRS } from './check-file-names.ts'
|
|
25
|
+
import { CHECK_SUFFIX as SUFFIX, fallbackNames, isLegacyCheckSource, SKIPPED_DIRS as IGNORED_DIRS, UNIT_SUFFIX, unitBase } from './check-file-names.ts'
|
|
26
|
+
import { unitKind } from '../shipkit-files.ts'
|
|
25
27
|
|
|
26
28
|
export { fallbackNames, isLegacyCheckSource } from './check-file-names.ts'
|
|
27
29
|
|
|
@@ -54,16 +56,39 @@ export type DiscoveredCheck = {
|
|
|
54
56
|
from: CheckFrom
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
function walk(dir: string, out: string[]): void {
|
|
59
|
+
function walk(dir: string, out: string[], suffix = SUFFIX): void {
|
|
58
60
|
let entries
|
|
59
61
|
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
60
62
|
for (const e of entries) {
|
|
61
63
|
if (e.isDirectory()) {
|
|
62
|
-
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), out)
|
|
63
|
-
} else if (e.name.endsWith(
|
|
64
|
+
if (!IGNORED_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), out, suffix)
|
|
65
|
+
} else if (e.name.endsWith(suffix)) out.push(join(dir, e.name))
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** every `*.shipkit.ts` file under a repo root, by the same walk. Sorted */
|
|
70
|
+
export function repoUnitFiles(root: string): string[] {
|
|
71
|
+
const out: string[] = []
|
|
72
|
+
walk(root, out, UNIT_SUFFIX)
|
|
73
|
+
return out.sort()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** a file's default export, re-imported whenever the file on disk moves. An
|
|
77
|
+
* ES module is cached by path for the life of the process, so a repo that
|
|
78
|
+
* rewrites a file mid process would keep the old one without the stamp */
|
|
79
|
+
export async function importFresh(path: string): Promise<unknown> {
|
|
80
|
+
let stamp = ''
|
|
81
|
+
// no dot in the stamp: a query ending `.1234` reads as the module's
|
|
82
|
+
// extension, and a load plugin then never sees the file
|
|
83
|
+
try { stamp = String(statSync(path).mtimeMs).replace('.', '_') } catch {}
|
|
84
|
+
// the REAL path: through a linked one, the resolver misses a file
|
|
85
|
+
// created after it first listed the directory, which is exactly the
|
|
86
|
+
// file a fix pass just wrote
|
|
87
|
+
let real = path
|
|
88
|
+
try { real = realpathSync(path) } catch {}
|
|
89
|
+
return ((await import(`${real}?v=${stamp}`)) as { default?: unknown }).default
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
/** every `*.check.ts` file under a repo root, check files and legacy
|
|
68
93
|
* scripts alike, by the same walk discovery takes, without importing one.
|
|
69
94
|
* Sorted. */
|
|
@@ -98,22 +123,14 @@ export function builtinCheckIds(): string[] {
|
|
|
98
123
|
/** the module, re-imported whenever the file on disk moves. An ES module is
|
|
99
124
|
* cached by path for the life of the process, so a repo that rewrites a
|
|
100
125
|
* check file mid process would keep running the old body without this. */
|
|
101
|
-
async function load(path: string): Promise<CheckDef> {
|
|
102
|
-
let stamp = ''
|
|
103
|
-
// no dot in the stamp: a query ending `.1234` reads as the module's
|
|
104
|
-
// extension, and a load plugin then never sees the file
|
|
105
|
-
try { stamp = String(statSync(path).mtimeMs).replace('.', '_') } catch {}
|
|
126
|
+
async function load(path: string): Promise<CheckDef | null> {
|
|
106
127
|
// the path rides on every message from here down, because `check()` knows
|
|
107
128
|
// the object and not where it was written, and an agent reading a refused
|
|
108
129
|
// declaration needs the file before it needs the rule
|
|
109
130
|
try {
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
let real = path
|
|
114
|
-
try { real = realpathSync(path) } catch {}
|
|
115
|
-
const mod = (await import(`${real}?v=${stamp}`)) as { default?: unknown }
|
|
116
|
-
const d = mod.default
|
|
131
|
+
const d = await importFresh(path)
|
|
132
|
+
// a unit file of another kind is its reader's, never a check
|
|
133
|
+
if (path.endsWith(UNIT_SUFFIX) && unitKind(d) !== 'check') return null
|
|
117
134
|
if (d == null || typeof d !== 'object' || (!('run' in d) && !('cmd' in d)))
|
|
118
135
|
throw new Error(`a check file must \`export default check({...})\` with a \`run\` or a \`cmd\``)
|
|
119
136
|
// re-validated here rather than trusted: a file can build the object
|
|
@@ -133,6 +150,7 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
133
150
|
walk(BUILTIN_CHECK_DIR, builtins)
|
|
134
151
|
const found: string[] = []
|
|
135
152
|
walk(root, found)
|
|
153
|
+
const units = repoUnitFiles(root)
|
|
136
154
|
// the repo that HOSTS the toolkit walks over `src/checks/` too, and a file
|
|
137
155
|
// discovered twice is a duplicate id rather than a second check. Real paths,
|
|
138
156
|
// because a consumer reaches the same files through a linked node_modules
|
|
@@ -147,17 +165,19 @@ export async function discoverChecks(root: string): Promise<DiscoveredCheck[]> {
|
|
|
147
165
|
|
|
148
166
|
const out: DiscoveredCheck[] = []
|
|
149
167
|
const ids = new Set<string>()
|
|
150
|
-
for (const f of [...builtins, ...own]) {
|
|
168
|
+
for (const f of [...builtins, ...own, ...units]) {
|
|
151
169
|
const isBuiltin = builtins.includes(f)
|
|
152
170
|
const def = await load(f)
|
|
171
|
+
if (def == null) continue
|
|
153
172
|
const rel = isBuiltin ? null : relative(root, f).replace(/\\/g, '/')
|
|
154
173
|
const fallback = isBuiltin
|
|
155
174
|
? (BUILTIN_ID_RE.exec(base(f))?.[1] ?? base(f))
|
|
156
|
-
: names.get(f)!
|
|
175
|
+
: f.endsWith(UNIT_SUFFIX) ? unitBase(f) : names.get(f)!
|
|
157
176
|
const id = def.id ?? fallback
|
|
158
177
|
if (isBuiltin) {
|
|
159
178
|
if (id !== fallback) throw new Error(`${f}: declares id '${id}' but its file name says '${fallback}' — a built-in id is its file name`)
|
|
160
179
|
if ((def.owner ?? '') === '') throw new Error(`${f}: a built-in check names the page that OWNS the invariant`)
|
|
180
|
+
if (RETIRED_CHECK_IDS[id] != null) throw new Error(`${f}: id '${id}' is retired (${RETIRED_CHECK_IDS[id]}), and an id is never reused`)
|
|
161
181
|
} else if (id.startsWith('SK')) {
|
|
162
182
|
throw new Error(`${rel}: id '${id}' uses the reserved SK prefix — a repo check picks its own (e.g. RV${id.slice(-3)}) or plain words`)
|
|
163
183
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// the content of a file no tree id reaches (outside the repo, gitignored, an
|
|
2
|
+
// install), as one short hash for a check's `cacheKey`. `absent` when nothing
|
|
3
|
+
// stands there, so a file appearing or vanishing moves the key too.
|
|
4
|
+
import { createHash } from 'crypto'
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { isAbsolute, join } from 'path'
|
|
8
|
+
|
|
9
|
+
export function fileStamp(abs: string): string {
|
|
10
|
+
try {
|
|
11
|
+
return createHash('sha256').update(readFileSync(abs)).digest('hex').slice(0, 16)
|
|
12
|
+
} catch {
|
|
13
|
+
return 'absent'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** a path as a config declares it (`~/...`, absolute, or repo relative), absolute */
|
|
18
|
+
export function configPath(root: string, p: string): string {
|
|
19
|
+
return p.startsWith('~/') ? join(homedir(), p.slice(2)) : isAbsolute(p) ? p : join(root, p)
|
|
20
|
+
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// visibility with it. gh keeps that login outside any secrets manager, so
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// the gh login the toolkit itself runs on: `init` and `start` create a
|
|
2
|
+
// PRIVATE repo with it, every push and release goes through it, SK007 reads
|
|
3
|
+
// visibility with it. gh keeps that login outside any secrets manager, so the
|
|
4
|
+
// house repo's one `github-login` check (a machine fact, asked once, never in
|
|
5
|
+
// every repo) runs `probeGhLogin`. It is a `core` credential: red on the
|
|
6
|
+
// secrets page and in `shipkit needs` (collections/providers.ts reads the
|
|
7
|
+
// `cred <name> missing` line).
|
|
8
8
|
//
|
|
9
9
|
// A private repo needs the classic `repo` scope. The token itself is never
|
|
10
10
|
// asked for: `gh auth status --json` carries no token field, and the text
|
|
11
11
|
// answer masks it. github.com unreachable is `unreachable`, a WARN that
|
|
12
12
|
// measured nothing, never a lost login.
|
|
13
13
|
import { carveGhJson, firstGhLine } from '../git-bootstrap.ts'
|
|
14
|
-
import {
|
|
14
|
+
import type { CheckCtx } from './check-ctx.ts'
|
|
15
15
|
|
|
16
16
|
const HOST = 'github.com'
|
|
17
17
|
|
|
18
|
-
/** the
|
|
19
|
-
|
|
18
|
+
/** the id of the house check that runs `probeGhLogin` */
|
|
19
|
+
export const GH_LOGIN_CHECK = 'github-login'
|
|
20
|
+
|
|
21
|
+
/** the secrets page row the probe proves: a built-in `core` credential, shown
|
|
22
|
+
* in the repo where the `github-login` check has run */
|
|
20
23
|
export const GH_LOGIN_CRED = {
|
|
21
24
|
name: 'gh/github/login',
|
|
22
25
|
service: 'gh login',
|
|
23
26
|
level: 'core' as const,
|
|
24
|
-
probedBy:
|
|
27
|
+
probedBy: GH_LOGIN_CHECK,
|
|
25
28
|
mintUrl: 'https://cli.github.com/manual/gh_auth_login',
|
|
26
29
|
/** what the row offers to copy when no failing line names a better fix */
|
|
27
30
|
command: 'gh auth login',
|
|
@@ -76,48 +79,36 @@ export function readGhText(out: string): GhLogin {
|
|
|
76
79
|
return { state: 'ok', login: login[1]!, scopes: scopesOf(scopes?.[1]) }
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
when: 'health',
|
|
88
|
-
ttl: '1h',
|
|
89
|
-
run: async (c) => {
|
|
90
|
-
const cred = GH_LOGIN_CRED.name
|
|
91
|
-
// the switch that keeps every github step off (git-bootstrap.ts), which every test suite sets
|
|
92
|
-
if ((process.env['SHIPKIT_NO_GITHUB'] ?? '') !== '') return void c.lines.push({ kind: 'skip', text: 'gh login not probed, SHIPKIT_NO_GITHUB is set' })
|
|
93
|
-
const gh = c.tool('gh')
|
|
94
|
-
if (gh == null)
|
|
95
|
-
return c.fail(`cred ${cred} missing: gh is not installed, so shipkit cannot create a repo, push or release`, 'install gh (https://cli.github.com), then run `gh auth login`')
|
|
96
|
-
|
|
97
|
-
let r = await c.exec([gh, 'auth', 'status', '--hostname', HOST, '--active', '--json', 'hosts'], { cwd: c.root, timeoutMs: 20_000 })
|
|
98
|
-
let seen: GhLogin
|
|
99
|
-
if (r.code !== 0 && /unknown flag/i.test(r.out)) {
|
|
100
|
-
r = await c.exec([gh, 'auth', 'status', '--hostname', HOST], { cwd: c.root, timeoutMs: 20_000 })
|
|
101
|
-
seen = readGhText(r.out)
|
|
102
|
-
} else seen = r.timedOut ? { state: 'unreachable', why: 'gh auth status timed out' } : readGhJson(r.out)
|
|
82
|
+
/** the probe: logged in to github.com, with the repo scope */
|
|
83
|
+
export async function probeGhLogin(c: CheckCtx): Promise<void> {
|
|
84
|
+
const cred = GH_LOGIN_CRED.name
|
|
85
|
+
// the switch that keeps every github step off (git-bootstrap.ts), which every test suite sets
|
|
86
|
+
if ((process.env['SHIPKIT_NO_GITHUB'] ?? '') !== '') return void c.lines.push({ kind: 'skip', text: 'gh login not probed, SHIPKIT_NO_GITHUB is set' })
|
|
87
|
+
const gh = c.tool('gh')
|
|
88
|
+
if (gh == null)
|
|
89
|
+
return c.fail(`cred ${cred} missing: gh is not installed, so shipkit cannot create a repo, push or release`, 'install gh (https://cli.github.com), then run `gh auth login`')
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return c.unreachable(`gh login unverified: ${HOST} unreachable (${seen.why.slice(0, 120)})`)
|
|
111
|
-
case 'unreadable':
|
|
112
|
-
return c.unreachable(`gh login unverified: gh answered something unreadable (${seen.why})`)
|
|
113
|
-
case 'ok':
|
|
114
|
-
if (seen.scopes == null)
|
|
115
|
-
return c.warn(`gh is logged in to ${HOST} as ${seen.login}, but its token lists no oauth scopes (a fine grained or app token), so creating a private repo is unverified`, 'gh auth login')
|
|
116
|
-
if (!seen.scopes.includes('repo'))
|
|
117
|
-
return c.fail(`cred ${cred} missing: the gh login ${seen.login} lacks the repo scope (it has ${seen.scopes.join(', ')}), so shipkit cannot create a private repo`, `gh auth refresh -h ${HOST} -s repo`)
|
|
118
|
-
return c.ok(`gh is logged in to ${HOST} as ${seen.login}, with the repo scope`)
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
})
|
|
91
|
+
let r = await c.exec([gh, 'auth', 'status', '--hostname', HOST, '--active', '--json', 'hosts'], { cwd: c.root, timeoutMs: 20_000 })
|
|
92
|
+
let seen: GhLogin
|
|
93
|
+
if (r.code !== 0 && /unknown flag/i.test(r.out)) {
|
|
94
|
+
r = await c.exec([gh, 'auth', 'status', '--hostname', HOST], { cwd: c.root, timeoutMs: 20_000 })
|
|
95
|
+
seen = readGhText(r.out)
|
|
96
|
+
} else seen = r.timedOut ? { state: 'unreachable', why: 'gh auth status timed out' } : readGhJson(r.out)
|
|
122
97
|
|
|
123
|
-
|
|
98
|
+
switch (seen.state) {
|
|
99
|
+
case 'none':
|
|
100
|
+
return c.fail(`cred ${cred} missing: gh is not logged in to ${HOST}, so shipkit cannot create a repo, push or release`, 'gh auth login')
|
|
101
|
+
case 'refused':
|
|
102
|
+
return c.fail(`cred ${cred} missing: the gh login from ${seen.source} was refused by ${HOST} (${seen.why.slice(0, 120)})`, 'gh auth login')
|
|
103
|
+
case 'unreachable':
|
|
104
|
+
return c.unreachable(`gh login unverified: ${HOST} unreachable (${seen.why.slice(0, 120)})`)
|
|
105
|
+
case 'unreadable':
|
|
106
|
+
return c.unreachable(`gh login unverified: gh answered something unreadable (${seen.why})`)
|
|
107
|
+
case 'ok':
|
|
108
|
+
if (seen.scopes == null)
|
|
109
|
+
return c.warn(`gh is logged in to ${HOST} as ${seen.login}, but its token lists no oauth scopes (a fine grained or app token), so creating a private repo is unverified`, 'gh auth login')
|
|
110
|
+
if (!seen.scopes.includes('repo'))
|
|
111
|
+
return c.fail(`cred ${cred} missing: the gh login ${seen.login} lacks the repo scope (it has ${seen.scopes.join(', ')}), so shipkit cannot create a private repo`, `gh auth refresh -h ${HOST} -s repo`)
|
|
112
|
+
return c.ok(`gh is logged in to ${HOST} as ${seen.login}, with the repo scope`)
|
|
113
|
+
}
|
|
114
|
+
}
|