rvlib-shipkit 1.0.0 → 1.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.
- package/CHANGELOG.md +65 -0
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +30 -4
- package/src/assets/gen/derived.ts +1 -1
- package/src/check.ts +6 -0
- package/src/checks/SK004-gitignore-policy.check.ts +4 -4
- package/src/checks/SK005-rvlib-versions.check.ts +25 -7
- package/src/checks/SK021-github-login.check.ts +123 -0
- package/src/cli/cmd_Asks.ts +1 -1
- package/src/cli/cmd_Check.ts +1 -1
- package/src/cli/cmd_Ci.ts +1 -1
- package/src/cli/cmd_Fleet.ts +4 -47
- package/src/cli.ts +2 -3
- package/src/collections/checks.ts +12 -3
- package/src/collections/collection-types.ts +34 -18
- package/src/collections/comfy.ts +1 -0
- package/src/collections/posts.ts +1 -0
- package/src/collections/providers.ts +71 -43
- package/src/collections/reflections.ts +1 -0
- package/src/collections/state-rules.ts +29 -13
- package/src/collections/tests.ts +1 -0
- package/src/collections/wikis.ts +1 -0
- package/src/core/budget.ts +60 -0
- package/src/core/check-ctx.ts +2 -1
- package/src/core/check-runner.ts +43 -6
- package/src/core/fd-table.ts +37 -0
- package/src/core/floor-resolve.ts +2 -2
- package/src/core/git-root.ts +26 -2
- package/src/core/lib-version.ts +16 -0
- package/src/core/migrate-layout.ts +69 -9
- package/src/core/motherhome.ts +2 -4
- package/src/core/package-manager.ts +20 -16
- package/src/core/raw-access.ts +11 -3
- package/src/core/reexec.ts +26 -0
- package/src/core/run-check.ts +36 -16
- package/src/core/run-ci.ts +31 -27
- package/src/core/run-fix.ts +4 -1
- package/src/core/run-fixers.ts +19 -11
- package/src/core/self-update.ts +63 -26
- package/src/core/update-policy.ts +25 -0
- package/src/fleet/scan-fleet.ts +20 -15
- package/src/fleet/update-fleet.ts +85 -0
- package/src/hooks/leak-guard.ts +10 -1
- package/src/hooks/run-hooks.ts +3 -3
- package/src/ledger/gate-checks.ts +19 -5
- package/src/ledger/git-key.ts +10 -5
- package/src/ledger/git-scan.ts +29 -17
- package/src/ledger/ledger.ts +14 -6
- package/src/ledger/repo-view.ts +8 -6
- package/src/ledger/run-log.ts +10 -4
- package/src/ledger/run-timeline.ts +21 -9
- package/src/problems/problem-board.ts +13 -2
- package/src/problems/problem-line.ts +9 -0
- package/src/repo-config.ts +24 -4
- package/src/serve/hub-app/check-view.tsx +12 -11
- package/src/serve/hub-app/cols-svc.ts +16 -9
- package/src/serve/hub-app/dur.ts +10 -1
- package/src/serve/hub-app/header-bar.tsx +11 -0
- package/src/serve/hub-app/help.tsx +1 -0
- package/src/serve/hub-app/hub-st.ts +3 -4
- package/src/serve/hub-app/hub-types.ts +5 -0
- package/src/serve/hub-app/hub.css +3 -0
- package/src/serve/hub-app/timeline-view.tsx +4 -4
- package/src/serve/hub-app/tree-build.ts +1 -2
- package/src/serve/hub-problems.ts +35 -0
- package/src/serve/run-serve.ts +51 -12
- package/src/serve/serve-hub.ts +10 -3
- package/src/serve/stale-restart.ts +77 -0
- package/src/smoke/run-smoke.ts +6 -2
- package/src/suite/suite-cli.ts +14 -10
- package/src/ts/source-graph.ts +3 -2
- package/src/wiki/scan-md.ts +1 -1
package/src/core/run-fixers.ts
CHANGED
|
@@ -12,13 +12,13 @@ import { existsSync, readFileSync } from 'fs'
|
|
|
12
12
|
import { join } from 'path'
|
|
13
13
|
import { fileURLToPath } from 'url'
|
|
14
14
|
import { armHooks } from '../hooks/run-hooks.ts'
|
|
15
|
-
import type
|
|
15
|
+
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
16
16
|
import { runFix } from './run-fix.ts'
|
|
17
17
|
import { routesToShipkit } from './canonical-scripts.ts'
|
|
18
18
|
import { discoverLegacyChecks } from './discover-legacy-checks.ts'
|
|
19
19
|
import { repoCheckFiles } from './discover-checks.ts'
|
|
20
20
|
import { mutateLease, withMutateLease } from './locked-write.ts'
|
|
21
|
-
import { migrateLayout } from './migrate-layout.ts'
|
|
21
|
+
import { layoutSummary, migrateLayout } from './migrate-layout.ts'
|
|
22
22
|
import { renderPolicy } from '../checks/SK004-gitignore-policy.check.ts'
|
|
23
23
|
import { spawnWithTimeout } from './spawn-timeout.ts'
|
|
24
24
|
import { gitBin } from './git-bin.ts'
|
|
@@ -180,20 +180,29 @@ export async function runFixPass(root: string, config: RepoConfig, o: FixPassOpt
|
|
|
180
180
|
return { ran: false, touched: [], failed: false, reshaped: false }
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
async function fixPassBody(root: string,
|
|
183
|
+
async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, verify: boolean): Promise<FixPassResult> {
|
|
184
|
+
let config = loaded
|
|
185
|
+
const shapeBefore = shapeStamp(root)
|
|
184
186
|
// the old root layout moves under .shipkit/ before anything else reads a
|
|
185
187
|
// path: a move is not a decision, so it lands as one machine commit and
|
|
186
188
|
// only what a machine cannot decide reaches a human. Runs before the tree
|
|
187
189
|
// snapshot, and before hooks arm, because the hooks folder may be what moves
|
|
188
190
|
if (!verify) {
|
|
189
|
-
const m = migrateLayout(root, { config, renderGitignore: (t, legacy) => renderPolicy(t, config, legacy) })
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
const m = migrateLayout(root, { config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? config, legacy) })
|
|
192
|
+
// the move rewrote repo.config.ts, so the config loaded before it names
|
|
193
|
+
// folders that are gone, and every fixer below must read the new one
|
|
194
|
+
if (m.swept.includes('repo.config.ts')) {
|
|
195
|
+
try { config = await loadRepoConfig(root, { fresh: true }) } catch (e) {
|
|
196
|
+
o.print(`WARN repo.config.ts does not load after the move rewrote it: ${String((e as Error)?.message ?? e).slice(0, 160)}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const what = layoutSummary(m)
|
|
200
|
+
if (what !== '') {
|
|
192
201
|
if (m.uncommitted === true) {
|
|
193
|
-
o.print(`WARN
|
|
202
|
+
o.print(`WARN layout: ${what}. The commit did not land`)
|
|
194
203
|
const p = m.pending ?? []
|
|
195
|
-
o.print(` fix: commit exactly these paths, never a bare commit: git commit -m 'chore:
|
|
196
|
-
} else
|
|
204
|
+
o.print(` fix: commit exactly these paths, never a bare commit: git commit -m 'chore: ${what}' -- ${p.slice(0, 12).join(' ')}${p.length > 12 ? ` (+${p.length - 12} more, listed by git status)` : ''}`)
|
|
205
|
+
} else o.print(`ok: layout: ${what}${m.commit != null ? `, committed as ${m.commit}` : ''}`)
|
|
197
206
|
}
|
|
198
207
|
if (m.deferred != null) {
|
|
199
208
|
o.print(`WARN the move under .shipkit/ is deferred: ${m.deferred}`)
|
|
@@ -218,7 +227,6 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
218
227
|
for (const u of unfilled) o.print(`skip: ${u.id} (${u.reason})`)
|
|
219
228
|
for (const d of disabled) o.print(`skip: ${d.id} (${d.reason})`)
|
|
220
229
|
const before = treeState(root)
|
|
221
|
-
const shapeBefore = shapeStamp(root)
|
|
222
230
|
|
|
223
231
|
for (const s of slots) {
|
|
224
232
|
const r = await spawnWithTimeout(s.cmd, { cwd: root, timeoutMs: FIXER_TIMEOUT_MS })
|
|
@@ -228,7 +236,7 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
228
236
|
o.print(`WARN fix ${s.id} failed (exit ${r.code}) — ${r.out.trim().split('\n').at(-1)?.slice(0, 160) ?? ''}; the gate below still ran`)
|
|
229
237
|
}
|
|
230
238
|
// structural repo-config repair is never implicit, and never in verify mode
|
|
231
|
-
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print })
|
|
239
|
+
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print, config })
|
|
232
240
|
|
|
233
241
|
const after = treeState(root)
|
|
234
242
|
const reshaped = shapeStamp(root) !== shapeBefore
|
package/src/core/self-update.ts
CHANGED
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
// re-exec guard set, dirty package.json/lockfile (a tool must not mix its
|
|
11
11
|
// writes into human-staged work), registry unreachable (offline is not a
|
|
12
12
|
// defect). every skip is silent except the actionable ones, which WARN.
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'fs'
|
|
14
14
|
import { homedir } from 'os'
|
|
15
|
-
import { dirname, join } from 'path'
|
|
15
|
+
import { basename, dirname, join } from 'path'
|
|
16
16
|
import { fileURLToPath } from 'url'
|
|
17
|
-
import { detectPackageManager } from './package-manager.ts'
|
|
17
|
+
import { detectPackageManager, isRefusal } from './package-manager.ts'
|
|
18
18
|
import { gitBin } from './git-bin.ts'
|
|
19
|
+
import { reexecSelf } from './reexec.ts'
|
|
20
|
+
import { LIB_ROOT } from './lib-root.ts'
|
|
21
|
+
import { FRESH_ENV } from '../ledger/store.ts'
|
|
22
|
+
import { allowMajor, breakingAdvice, isBreaking } from './update-policy.ts'
|
|
19
23
|
|
|
20
24
|
const PKG = 'rvlib-shipkit'
|
|
21
25
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
@@ -34,6 +38,10 @@ export function planSelfUpdate(o: {
|
|
|
34
38
|
ci: boolean
|
|
35
39
|
frozen: boolean
|
|
36
40
|
manifestDirty: boolean
|
|
41
|
+
/** `fleet --update --major` asked for this jump on purpose */
|
|
42
|
+
allowMajor: boolean
|
|
43
|
+
/** the repo dir name, for the advice a breaking jump prints */
|
|
44
|
+
repo: string
|
|
37
45
|
}): SelfUpdatePlan {
|
|
38
46
|
if (o.reexecGuard) return { action: 'skip', reason: 'already re-executed once' }
|
|
39
47
|
if (o.ci) return { action: 'skip', reason: 'CI env' }
|
|
@@ -43,6 +51,7 @@ export function planSelfUpdate(o: {
|
|
|
43
51
|
if (o.latest == null) return { action: 'skip', reason: 'registry unreachable' }
|
|
44
52
|
if (o.installed === o.latest) return { action: 'skip', reason: 'current' }
|
|
45
53
|
if (newerThan(o.installed, o.latest)) return { action: 'skip', reason: 'ahead of the registry' }
|
|
54
|
+
if (isBreaking(o.installed, o.latest) && !o.allowMajor) return { action: 'warn', reason: breakingAdvice(PKG, o.latest, o.repo) }
|
|
46
55
|
if (o.manifestDirty) return { action: 'warn', reason: `rvlib-shipkit ${o.installed} is behind ${o.latest}, package.json or lockfile dirty, not touching — commit, then rerun` }
|
|
47
56
|
return { action: 'update', installed: o.installed, latest: o.latest }
|
|
48
57
|
}
|
|
@@ -58,23 +67,40 @@ const newerThan = (a: string, b: string): boolean => {
|
|
|
58
67
|
return false
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
/** overrides the per-machine cache file (tests) */
|
|
71
|
+
export const SELF_UPDATE_CACHE_ENV = 'SHIPKIT_SELF_UPDATE_CACHE'
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const cachePath = (): string => {
|
|
74
|
+
const env = process.env[SELF_UPDATE_CACHE_ENV]
|
|
75
|
+
return env != null && env !== '' ? env : join(homedir(), '.cache', 'shipkit', 'self-update.json')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** store a registry answer another path measured, so the next startup
|
|
79
|
+
* decides on it instead of on an older cached one */
|
|
80
|
+
export function rememberLatest(version: string): void {
|
|
65
81
|
const p = cachePath()
|
|
66
|
-
|
|
82
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
83
|
+
writeFileSync(p, JSON.stringify({ at: Date.now(), version }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const npmLatest = async (): Promise<string | null> => {
|
|
87
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, { signal: AbortSignal.timeout(5000) })
|
|
88
|
+
if (!res.ok) return null
|
|
89
|
+
return ((await res.json()) as { version?: string }).version ?? null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** npm latest with a per-machine 24h cache, null when unreachable. A fresh
|
|
93
|
+
* run skips the cache: it decides the one update that restarts the run, and
|
|
94
|
+
* a stale answer here leaves that update to SK005, mid run */
|
|
95
|
+
export async function latestCached(o: { fresh: boolean; fetchLatest?: () => Promise<string | null> }): Promise<string | null> {
|
|
96
|
+
const p = cachePath()
|
|
97
|
+
if (!o.fresh) try {
|
|
67
98
|
const c = JSON.parse(readFileSync(p, 'utf8')) as { at: number; version: string }
|
|
68
99
|
if (Date.now() - c.at < CACHE_TTL_MS) return c.version
|
|
69
100
|
} catch { /* no cache yet */ }
|
|
70
101
|
try {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
const v = ((await res.json()) as { version?: string }).version ?? null
|
|
74
|
-
if (v != null) {
|
|
75
|
-
mkdirSync(dirname(p), { recursive: true })
|
|
76
|
-
writeFileSync(p, JSON.stringify({ at: Date.now(), version: v }))
|
|
77
|
-
}
|
|
102
|
+
const v = await (o.fetchLatest ?? npmLatest)()
|
|
103
|
+
if (v != null) rememberLatest(v)
|
|
78
104
|
return v
|
|
79
105
|
} catch {
|
|
80
106
|
return null
|
|
@@ -87,10 +113,22 @@ const manifestDirty = (root: string): boolean => {
|
|
|
87
113
|
return p.stdout.toString().trim() !== ''
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
export
|
|
116
|
+
/** true when `root`'s installed rvlib-shipkit is the code of THIS process.
|
|
117
|
+
* Updating that tree in process would leave the rest of the run importing
|
|
118
|
+
* modules of two versions, so only the startup path below may update it */
|
|
119
|
+
export function isRunningCopy(root: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
return realpathSync(join(root, 'node_modules', PKG)) === realpathSync(LIB_ROOT)
|
|
122
|
+
} catch {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** run before dispatching an interactive ci/check, so no check has loaded
|
|
128
|
+
* yet. On update: re-exec the SAME argv with a loop guard and exit with the
|
|
129
|
+
* child's code (never returns). Otherwise returns after at most one registry
|
|
130
|
+
* probe, cached unless the run is fresh (`--fresh` or SHIPKIT_FRESH=1). */
|
|
131
|
+
export async function maybeSelfUpdateAndRestart(root: string, o: { fresh?: boolean } = {}): Promise<void> {
|
|
94
132
|
const here = fileURLToPath(import.meta.url)
|
|
95
133
|
const base = {
|
|
96
134
|
installed: installedVersion(root),
|
|
@@ -98,23 +136,22 @@ export async function maybeSelfUpdateAndRestart(root: string): Promise<void> {
|
|
|
98
136
|
reexecGuard: process.env.SHIPKIT_SELF_UPDATED === '1',
|
|
99
137
|
ci: process.env.CI != null && process.env.CI !== '',
|
|
100
138
|
frozen: process.env.SHIPKIT_FROZEN === '1',
|
|
139
|
+
allowMajor: allowMajor(),
|
|
140
|
+
repo: basename(root),
|
|
101
141
|
}
|
|
102
142
|
// cheap gates first: no network unless an update is even possible
|
|
103
143
|
if (base.reexecGuard || base.ci || base.frozen || !base.runningFromNodeModules || base.installed == null) return
|
|
104
|
-
const plan = planSelfUpdate({ ...base, latest: await latestCached(), manifestDirty: manifestDirty(root) })
|
|
144
|
+
const plan = planSelfUpdate({ ...base, latest: await latestCached({ fresh: o.fresh === true || process.env[FRESH_ENV] === '1' }), manifestDirty: manifestDirty(root) })
|
|
105
145
|
if (plan.action === 'warn') { console.error(`WARN self-update: ${plan.reason}`); return }
|
|
106
146
|
if (plan.action !== 'update') return
|
|
107
|
-
const
|
|
147
|
+
const pm = detectPackageManager(root)
|
|
148
|
+
if (isRefusal(pm)) { console.error(`WARN self-update: rvlib-shipkit ${plan.installed} is behind ${plan.latest}, not updating: ${pm.refused}`); return }
|
|
149
|
+
const cmd = pm.update([PKG])
|
|
108
150
|
console.error(`ok: rvlib-shipkit ${plan.installed} → ${plan.latest}, self-updating (${cmd.join(' ')})`)
|
|
109
151
|
const up = Bun.spawnSync(cmd, { cwd: root, stdout: 'inherit', stderr: 'inherit' })
|
|
110
152
|
if (up.exitCode !== 0) { console.error(`WARN self-update failed (exit ${up.exitCode}), continuing on ${plan.installed}`) ; return }
|
|
111
153
|
console.error(`ok: restarting on ${plan.latest} — commit package.json + lockfile with your work`)
|
|
112
|
-
|
|
113
|
-
cwd: process.cwd(),
|
|
114
|
-
stdout: 'inherit', stderr: 'inherit', stdin: 'inherit',
|
|
115
|
-
env: { ...process.env, SHIPKIT_SELF_UPDATED: '1' },
|
|
116
|
-
})
|
|
117
|
-
process.exit(child.exitCode ?? 1)
|
|
154
|
+
reexecSelf({ ...process.env, SHIPKIT_SELF_UPDATED: '1' })
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
const installedVersion = (root: string): string | null => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// the ONE update policy every automatic update path asks: rule SK005, the
|
|
2
|
+
// startup self-update and `shipkit fleet --update`. An automatic update never
|
|
3
|
+
// crosses a breaking boundary. It reports the jump and names the one command
|
|
4
|
+
// that takes it on purpose.
|
|
5
|
+
|
|
6
|
+
/** set by `shipkit fleet --update --major` on the repo run it spawns */
|
|
7
|
+
export const ALLOW_MAJOR_ENV = 'SHIPKIT_ALLOW_MAJOR'
|
|
8
|
+
|
|
9
|
+
export const allowMajor = (): boolean => process.env[ALLOW_MAJOR_ENV] === '1'
|
|
10
|
+
|
|
11
|
+
/** a jump across the boundary npm's caret draws: a changed major, or on a
|
|
12
|
+
* `0.x` package a changed minor (`^0.2.0` means `<0.3.0`). A porting job
|
|
13
|
+
* rather than a bump, so no automatic path takes it. */
|
|
14
|
+
export function isBreaking(installed: string, latest: string): boolean {
|
|
15
|
+
const parts = (v: string) => v.split('-')[0]!.split('.').map((n) => Number(n) || 0)
|
|
16
|
+
const [aMajor = 0, aMinor = 0] = parts(installed)
|
|
17
|
+
const [bMajor = 0, bMinor = 0] = parts(latest)
|
|
18
|
+
if (aMajor !== bMajor) return true
|
|
19
|
+
return aMajor === 0 && aMinor !== bMinor
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** the WARN text for a jump no automatic path takes. `repo` is the directory
|
|
23
|
+
* name, which is what `fleet --only` matches */
|
|
24
|
+
export const breakingAdvice = (name: string, latest: string, repo: string): string =>
|
|
25
|
+
`${name} ${latest} available (breaking): run \`shipkit fleet --update --major --only ${repo}\``
|
package/src/fleet/scan-fleet.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
//
|
|
9
9
|
// This module DISCOVERS and REPORTS. It never updates: rule SK005 owns the
|
|
10
10
|
// update, with the lease, the dirty-tree guard and the frozen guard that took
|
|
11
|
-
// three bugs to get right. `--update` runs each repo's own
|
|
12
|
-
// so the fix travels through that one code path.
|
|
11
|
+
// three bugs to get right. `--update` (update-fleet.ts) runs each repo's own
|
|
12
|
+
// `shipkit check`, so the fix travels through that one code path.
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
+
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
16
|
+
import { isBreaking } from '../core/update-policy.ts'
|
|
15
17
|
|
|
16
18
|
export type Pkg = { name?: string; version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
17
19
|
|
|
@@ -25,18 +27,6 @@ export type FleetDep = {
|
|
|
25
27
|
latest: string | null
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/** a jump that changes the MAJOR, which is a porting job rather than a bump.
|
|
29
|
-
*
|
|
30
|
-
* Deliberately NOT the semver rule. On a `0.x` package npm treats every
|
|
31
|
-
* minor as breaking, and by that rule every repo here is breaking, including
|
|
32
|
-
* the `0.24 → 0.35` shipkit jumps that rule SK005 already performs by itself
|
|
33
|
-
* every day. A flag that fires on everything is a flag nobody reads. What
|
|
34
|
-
* survives is the case a human has to own: `rvlib-pb-mobx 0.2.0 → 2.0.0`. */
|
|
35
|
-
export function isBreaking(installed: string, latest: string): boolean {
|
|
36
|
-
const major = (v: string) => Number(v.split('-')[0]!.split('.')[0]) || 0
|
|
37
|
-
return major(installed) !== major(latest)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
30
|
export type FleetRepo = {
|
|
41
31
|
name: string
|
|
42
32
|
dir: string
|
|
@@ -61,7 +51,7 @@ const readPkg = (dir: string): Pkg | null => {
|
|
|
61
51
|
}
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
const installedVersion = (dir: string, name: string): string | null => readPkg(join(dir, 'node_modules', name))?.version ?? null
|
|
54
|
+
export const installedVersion = (dir: string, name: string): string | null => readPkg(join(dir, 'node_modules', name))?.version ?? null
|
|
65
55
|
|
|
66
56
|
/** semver-ish compare, enough to answer "is this one behind that one". A
|
|
67
57
|
* prerelease tag sorts before its release, which is the safe direction: it
|
|
@@ -140,3 +130,18 @@ export async function npmLatest(name: string): Promise<string | null> {
|
|
|
140
130
|
if (!res.ok) return null
|
|
141
131
|
return ((await res.json()) as Pkg).version ?? null
|
|
142
132
|
}
|
|
133
|
+
|
|
134
|
+
/** the first release whose SK005 updates through the manager the repo's lock
|
|
135
|
+
* files name. An older one runs bun whatever the lock says */
|
|
136
|
+
export const PM_AWARE_SINCE = '1.0.0'
|
|
137
|
+
|
|
138
|
+
/** why `fleet --update` must not hand this repo its own update, or null.
|
|
139
|
+
* The fleet runs the REPO's shipkit, so what matters is what THAT binary
|
|
140
|
+
* does: one older than PM_AWARE_SINCE updates an npm repo with bun */
|
|
141
|
+
export function updateRefusal(r: FleetRepo): string | null {
|
|
142
|
+
const pm = detectPackageManager(r.dir)
|
|
143
|
+
if (isRefusal(pm)) return `cannot tell which package manager installs it: ${pm.refused}`
|
|
144
|
+
const own = r.deps.find((d) => d.name === 'rvlib-shipkit')?.installed
|
|
145
|
+
if (pm.name === 'bun' || own == null || !isBehind(own, PM_AWARE_SINCE)) return null
|
|
146
|
+
return `its shipkit ${own} updates with bun whatever the lock says, and it installs with ${pm.name}. Run: cd ${r.dir} && ${pm.update(r.behind.map((d) => d.name)).join(' ')}`
|
|
147
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// `shipkit fleet --update`: hand each behind repo its own update. The update
|
|
2
|
+
// is NOT performed here. Each repo runs its own binary, which is the only copy
|
|
3
|
+
// whose SK005 matches the rules that repo is pinned to, and the only one
|
|
4
|
+
// holding that repo's mutate lease.
|
|
5
|
+
import { existsSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
8
|
+
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
9
|
+
import { ALLOW_MAJOR_ENV, breakingAdvice } from '../core/update-policy.ts'
|
|
10
|
+
import { installedVersion, isBehind, updateRefusal, type FleetRepo } from './scan-fleet.ts'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export const short = (name: string): string => name.replace(/^rvlib-/, '')
|
|
14
|
+
|
|
15
|
+
// the hand-run fallback speaks the manager THAT repo installs with: telling
|
|
16
|
+
// an npm consumer to run bun leaves its package-lock.json behind
|
|
17
|
+
const manualUpdate = (r: FleetRepo): string => {
|
|
18
|
+
const pm = detectPackageManager(r.dir)
|
|
19
|
+
return isRefusal(pm) ? `<its package manager> update ${r.behind.map((d) => d.name).join(' ')} (${pm.refused})` : pm.update(r.behind.map((d) => d.name)).join(' ')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FleetUpdateOpts = {
|
|
23
|
+
/** cross a breaking boundary too (`--major`) */
|
|
24
|
+
major: boolean
|
|
25
|
+
print: (line: string) => void
|
|
26
|
+
/** per repo run (default 10 minutes: a fresh health pass probes the world) */
|
|
27
|
+
timeoutMs?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** returns how many repos did not update. A repo whose gate refused (dirty
|
|
31
|
+
* tree, frozen, a lease held elsewhere) is a REPORT, not a failure of the
|
|
32
|
+
* command: SK005 defers on purpose */
|
|
33
|
+
export async function updateFleet(behind: FleetRepo[], o: FleetUpdateOpts): Promise<number> {
|
|
34
|
+
const w = o.print
|
|
35
|
+
let failed = 0
|
|
36
|
+
for (const r of behind) {
|
|
37
|
+
// the same policy SK005 applies inside the repo: a breaking jump is reported,
|
|
38
|
+
// never taken, and a safe jump beside it still moves
|
|
39
|
+
const held = o.major ? [] : r.breaking
|
|
40
|
+
for (const d of held) w(`WARN ${r.name}: ${breakingAdvice(d.name, d.latest!, r.name)}`)
|
|
41
|
+
const wanted = r.behind.filter((d) => !held.includes(d))
|
|
42
|
+
if (wanted.length === 0) {
|
|
43
|
+
failed++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
const refused = updateRefusal(r)
|
|
47
|
+
if (refused != null) {
|
|
48
|
+
w(`WARN ${r.name}: not updating: ${refused}`)
|
|
49
|
+
failed++
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const cli = join(r.dir, 'node_modules', 'rvlib-shipkit', 'src', 'cli.ts')
|
|
53
|
+
if (!existsSync(cli)) {
|
|
54
|
+
w(`WARN ${r.name}: no shipkit installed, so it cannot update itself — run: cd ${r.dir} && ${manualUpdate(r)}`)
|
|
55
|
+
failed++
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
// `--fresh` because SK005 caches a clean pass for 24h: a plain `check`
|
|
59
|
+
// replays the answer it gave before this release existed, and the run
|
|
60
|
+
// meant to update never asks the registry. `check --fix SK005` is no
|
|
61
|
+
// way around it: SK005 has no autofix, so the binary refuses the id.
|
|
62
|
+
// The re-exec guard is dropped so the repo's own startup self-update
|
|
63
|
+
// can still fire, and --major travels as the one env the policy reads.
|
|
64
|
+
const env: Record<string, string> = Object.fromEntries(Object.entries(process.env).filter((e): e is [string, string] => e[1] != null && e[0] !== 'SHIPKIT_SELF_UPDATED' && e[0] !== ALLOW_MAJOR_ENV))
|
|
65
|
+
if (o.major) env[ALLOW_MAJOR_ENV] = '1'
|
|
66
|
+
const res = await spawnWithTimeout(['bun', cli, 'check', '--fresh'], { cwd: r.dir, env, timeoutMs: o.timeoutMs ?? 600_000 })
|
|
67
|
+
// judge by what node_modules carries now, never by the exit code or a
|
|
68
|
+
// printed line: the code covers every rule in that repo, and the line
|
|
69
|
+
// format belongs to whatever version that repo runs
|
|
70
|
+
const stuck = wanted.filter((d) => {
|
|
71
|
+
const now = installedVersion(r.dir, d.name)
|
|
72
|
+
return now == null || isBehind(now, d.latest!)
|
|
73
|
+
})
|
|
74
|
+
if (stuck.length === 0) {
|
|
75
|
+
w(`ok: ${r.name} — ${wanted.map((d) => `${short(d.name)} ${d.installed} → ${installedVersion(r.dir, d.name)}`).join(', ')}`)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
failed++
|
|
79
|
+
const said = res.out.split('\n').find((l) => /\brvlib-/.test(l) && (l.startsWith('WARN') || l.startsWith('FAIL')))
|
|
80
|
+
w(`WARN ${r.name} did not move ${stuck.map((d) => short(d.name)).join(', ')} — ${said ?? `check exited ${res.code}${res.timedOut ? ', timed out' : ''} with no rvlib line`}`)
|
|
81
|
+
w(` fix: cd ${r.dir} && ${manualUpdate(r)}`)
|
|
82
|
+
}
|
|
83
|
+
w(failed === 0 ? `ok: ${behind.length} repo(s) updated through their own gate` : `WARN ${failed}/${behind.length} repo(s) did not update — each line above says why`)
|
|
84
|
+
return failed
|
|
85
|
+
}
|
package/src/hooks/leak-guard.ts
CHANGED
|
@@ -80,6 +80,15 @@ export type ParsedPatterns = {
|
|
|
80
80
|
|
|
81
81
|
const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
82
82
|
|
|
83
|
+
/** a key shape is a token, so it starts where a token starts: `ask-the-...`
|
|
84
|
+
* or `desk-lamp-...` is a word that ends in `sk`, never a key */
|
|
85
|
+
export const SK_KEY_ROW = 're:(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{20,}'
|
|
86
|
+
|
|
87
|
+
/** rows the hooks seeded before their shape was fixed, read as the fixed
|
|
88
|
+
* shape. Their id stays the seeded row, so a waiver written against its
|
|
89
|
+
* fingerprint keeps working */
|
|
90
|
+
const SEEDED_ROW_FIXES: Record<string, string> = { 're:sk-[A-Za-z0-9_-]{20,}': SK_KEY_ROW }
|
|
91
|
+
|
|
83
92
|
/** Parse the keyword file: blank lines and `#` comments dropped, `re:<pattern>`
|
|
84
93
|
* rows become case-insensitive regexes, `w:<word>` rows match on word
|
|
85
94
|
* boundaries, everything else a case-insensitive substring.
|
|
@@ -95,7 +104,7 @@ export function parsePatterns(text: string): ParsedPatterns {
|
|
|
95
104
|
const row = raw.trim()
|
|
96
105
|
if (row === '' || row.startsWith('#')) continue
|
|
97
106
|
if (row.startsWith('re:')) {
|
|
98
|
-
try { regexes.push({ row, re: new RegExp(row.slice(3), 'i') }) }
|
|
107
|
+
try { regexes.push({ row, re: new RegExp((SEEDED_ROW_FIXES[row] ?? row).slice(3), 'i') }) }
|
|
99
108
|
catch { /* a malformed row must not disable the whole guard */ }
|
|
100
109
|
} else if (row.startsWith('w:')) {
|
|
101
110
|
const word = row.slice(2).trim().toLowerCase()
|
package/src/hooks/run-hooks.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname, join } from 'path'
|
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
9
|
import { KEYWORD_FILE, LAYOUT } from '../core/layout.ts'
|
|
10
10
|
import { commitMessageOffenses } from './commit-message.ts'
|
|
11
|
-
import { globalAllowFile, globalKeywordFile, maskPattern, patternFingerprint, runLeakGuard, type LeakAllowEntry, type LeakHit, type LeakWaiver } from './leak-guard.ts'
|
|
11
|
+
import { globalAllowFile, globalKeywordFile, maskPattern, patternFingerprint, runLeakGuard, SK_KEY_ROW, type LeakAllowEntry, type LeakHit, type LeakWaiver } from './leak-guard.ts'
|
|
12
12
|
import { runRootIdentity } from './root-identity.ts'
|
|
13
13
|
import { gitRoot } from '../core/git-root.ts'
|
|
14
14
|
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
@@ -143,14 +143,14 @@ export function installHooks(root: string, binInvocation: string, keywordFile?:
|
|
|
143
143
|
// generic secret shapes only. private names live in the machine-global
|
|
144
144
|
// ~/.shipkit/private/banned-keywords.txt, which the guard reads on every run
|
|
145
145
|
// (never copied into a repo: a copy rots and a copy can leak)
|
|
146
|
-
const KEYWORD_STARTER = `# banned-keywords — the leak guard greps every commit (added lines, paths,
|
|
146
|
+
export const KEYWORD_STARTER = `# banned-keywords — the leak guard greps every commit (added lines, paths,
|
|
147
147
|
# message) for these rows. one row = one case-insensitive substring, or
|
|
148
148
|
# re:<regex>. add repo-specific names here; machine-wide private names go in
|
|
149
149
|
# ~/.shipkit/private/banned-keywords.txt, read automatically alongside this file.
|
|
150
150
|
# this file is gitignored on purpose: the list itself is private.
|
|
151
151
|
re:-----BEGIN [A-Z ]*PRIVATE KEY-----
|
|
152
152
|
re:AKIA[0-9A-Z]{16}
|
|
153
|
-
|
|
153
|
+
${SK_KEY_ROW}
|
|
154
154
|
re:ghp_[A-Za-z0-9]{36}
|
|
155
155
|
re:xox[baprs]-[A-Za-z0-9-]{10,}
|
|
156
156
|
`
|
|
@@ -22,6 +22,7 @@ import { discoverChecks } from '../core/discover-checks.ts'
|
|
|
22
22
|
import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom } from '../check.ts'
|
|
23
23
|
import { CHECK_TIMEOUT_MS, type RunnableCheck, type CheckBody } from '../core/check-runner.ts'
|
|
24
24
|
import { parseDuration } from '../core/wire.ts'
|
|
25
|
+
import { budgetOf } from '../core/budget.ts'
|
|
25
26
|
import { whenOfLegacyCheck, whenOfDisabledSlot, whenOfRule, whenOfTask, whenOfCheck, type WhenDecl } from '../core/when.ts'
|
|
26
27
|
import type { CheckDecl } from './git-key.ts'
|
|
27
28
|
import { populationOf } from '../problems/problem-line.ts'
|
|
@@ -34,9 +35,9 @@ import { declaredTestUnits, inputsOutsideUnits, isTestFile } from '../suite/test
|
|
|
34
35
|
/** why an undeclared `run` check cannot be keyed on its reads, or null when it
|
|
35
36
|
* can. A built-in is walked inside the toolkit, a repo check inside the repo
|
|
36
37
|
* with the toolkit left out (the toolchain id covers it) */
|
|
37
|
-
function rawAccessOf(root: string, u: DiscoveredCheck, configs: Map<string, RvtConfig>): string | null {
|
|
38
|
+
function rawAccessOf(root: string, u: DiscoveredCheck, configs: Map<string, RvtConfig>, texts: Map<string, string>): string | null {
|
|
38
39
|
try {
|
|
39
|
-
const r = u.rel == null ? rawAccessInClosure(LIB_ROOT, u.path, { configs }) : rawAccessInClosure(root, u.path, { skip: LIB_ROOT, configs })
|
|
40
|
+
const r = u.rel == null ? rawAccessInClosure(LIB_ROOT, u.path, { configs, texts }) : rawAccessInClosure(root, u.path, { skip: LIB_ROOT, configs, texts })
|
|
40
41
|
return r == null ? null : rawAccessLine(r)
|
|
41
42
|
} catch (e) {
|
|
42
43
|
return `the modules ${u.rel ?? u.path} imports could not be listed (${String(e).slice(0, 120)}): declare inputs or never`
|
|
@@ -103,6 +104,9 @@ export type CheckRow = {
|
|
|
103
104
|
/** the wall clock ceiling it DECLARED, null when it named none and takes
|
|
104
105
|
* the default for its kind of body */
|
|
105
106
|
timeoutMs: number | null
|
|
107
|
+
/** what a healthy run takes, RESOLVED: the repo's `budgets`, else what
|
|
108
|
+
* the check declared, else the default for its kind (core/budget.ts) */
|
|
109
|
+
budgetMs: number
|
|
106
110
|
/** excluded from a default run unless named with `--with` */
|
|
107
111
|
optIn: boolean
|
|
108
112
|
/** an idempotent repair exists: `shipkit check --fix <id>` applies it,
|
|
@@ -150,7 +154,7 @@ const ttlMsOf = (ttl: string | undefined): number => (ttl == null || ttl === ''
|
|
|
150
154
|
|
|
151
155
|
/** the runnable half of a row: everything the runner needs, and nothing it
|
|
152
156
|
* has to look up again */
|
|
153
|
-
function runnableOf(row: CheckRow, body: CheckBody, o: { fix?: unknown; pointer?: string | null } = {}): RunnableCheck {
|
|
157
|
+
function runnableOf(row: CheckRow, body: CheckBody, o: { fix?: unknown; pointer?: string | null; unbudgeted?: boolean } = {}): RunnableCheck {
|
|
154
158
|
return {
|
|
155
159
|
id: row.decl.id,
|
|
156
160
|
title: row.title,
|
|
@@ -161,6 +165,7 @@ function runnableOf(row: CheckRow, body: CheckBody, o: { fix?: unknown; pointer?
|
|
|
161
165
|
deps: row.deps ?? [],
|
|
162
166
|
group: row.group,
|
|
163
167
|
timeoutMs: row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS),
|
|
168
|
+
...(o.unbudgeted === true ? {} : { budgetMs: row.budgetMs }),
|
|
164
169
|
ttlMs: ttlMsOf(row.decl.ttl),
|
|
165
170
|
rotation: row.decl.extra,
|
|
166
171
|
writes: row.decl.writes === true,
|
|
@@ -251,6 +256,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
251
256
|
owner: t.cmd == null ? null : t.cmd.join(' '),
|
|
252
257
|
group: t.group ?? null,
|
|
253
258
|
timeoutMs: t.timeoutMs ?? null,
|
|
259
|
+
budgetMs: budgetOf(t.id, undefined, { build: true, cmd: t.cmd != null, world: t.ttl != null }, c.budgets),
|
|
254
260
|
optIn: t.optIn === true,
|
|
255
261
|
// a build step runs a TOOL, and repairing what a tool reports is
|
|
256
262
|
// that tool's own job (a formatter, a codemod), never the gate's
|
|
@@ -259,7 +265,10 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
259
265
|
deps: t.deps ?? null,
|
|
260
266
|
from: taskFrom(t.id),
|
|
261
267
|
}
|
|
262
|
-
|
|
268
|
+
// at the full tier a build step runs every slow suite on purpose, and
|
|
269
|
+
// its env (and so its cache key) differs from the quick run, so its
|
|
270
|
+
// time is not judged and its answer never stands in for a quick one
|
|
271
|
+
out.push({ row, runnable: runnableOf(row, taskBody(t), { unbudgeted: full }) })
|
|
263
272
|
}
|
|
264
273
|
// a slot `defaults: { <id>: false }` turned off never resolves to a task,
|
|
265
274
|
// so without this it had no row at all: no `runs in` value, no reason, and
|
|
@@ -277,6 +286,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
277
286
|
owner: null,
|
|
278
287
|
group: null,
|
|
279
288
|
timeoutMs: null,
|
|
289
|
+
budgetMs: budgetOf(id, undefined, { build: true, cmd: false, world: false }, c.budgets),
|
|
280
290
|
optIn: false,
|
|
281
291
|
fixable: false,
|
|
282
292
|
when: whenOfDisabledSlot(id),
|
|
@@ -290,6 +300,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
290
300
|
}
|
|
291
301
|
// every `*.check.ts` this repo and the toolkit declare
|
|
292
302
|
const configs = new Map<string, RvtConfig>()
|
|
303
|
+
const texts = new Map<string, string>()
|
|
293
304
|
for (const u of checks) {
|
|
294
305
|
const w = whenOfCheck(c, u)
|
|
295
306
|
// an in process body declaring nothing is keyed on what it reads
|
|
@@ -299,7 +310,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
299
310
|
const undeclared = u.def.run != null && u.def.inputs == null && u.def.ttl == null && u.def.never == null
|
|
300
311
|
// conservative: any module its body can reach that touches the disk or
|
|
301
312
|
// a process around `c` keeps it off its reads (core/raw-access.ts)
|
|
302
|
-
const raw = undeclared ? rawAccessOf(root, u, configs) : null
|
|
313
|
+
const raw = undeclared ? rawAccessOf(root, u, configs, texts) : null
|
|
303
314
|
const reads = undeclared && raw == null
|
|
304
315
|
const extra = checkCacheExtra({ id: u.id, cacheKey: u.def.cacheKey }, env, w)
|
|
305
316
|
const row: CheckRow = {
|
|
@@ -323,6 +334,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
323
334
|
owner: u.owner,
|
|
324
335
|
group: u.def.group ?? null,
|
|
325
336
|
timeoutMs: u.def.timeoutMs ?? null,
|
|
337
|
+
budgetMs: budgetOf(u.id, u.def.budgetMs, { build: u.def.build === true, cmd: u.def.cmd != null, world: u.def.ttl != null }, c.budgets),
|
|
326
338
|
optIn: false,
|
|
327
339
|
fixable: u.def.fix != null,
|
|
328
340
|
when: w,
|
|
@@ -358,6 +370,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
358
370
|
owner: r.owner,
|
|
359
371
|
group: null,
|
|
360
372
|
timeoutMs: null,
|
|
373
|
+
budgetMs: budgetOf(r.id, undefined, { build: false, cmd: false, world: r.ttl != null }, c.budgets),
|
|
361
374
|
optIn: false,
|
|
362
375
|
fixable: r.fix != null,
|
|
363
376
|
when: whenOfRule(c, r.id),
|
|
@@ -401,6 +414,7 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
401
414
|
owner: rel,
|
|
402
415
|
group: task?.group ?? null,
|
|
403
416
|
timeoutMs: task?.timeoutMs ?? null,
|
|
417
|
+
budgetMs: budgetOf(d.name, undefined, { build: false, cmd: true, world: d.ttl != null }, c.budgets),
|
|
404
418
|
optIn: false,
|
|
405
419
|
// the reader is a source scanner, so it cannot carry a function
|
|
406
420
|
fixable: false,
|
package/src/ledger/git-key.ts
CHANGED
|
@@ -173,6 +173,9 @@ export type KeyLayerOpts = {
|
|
|
173
173
|
cliEntry: string
|
|
174
174
|
/** module-graph config; loaded from the repo when omitted */
|
|
175
175
|
cfg?: RvtConfig
|
|
176
|
+
/** the checks this run executes. Only they are scanned and hashed, and any
|
|
177
|
+
* other id gets no key. Omitted = every check */
|
|
178
|
+
scope?: ReadonlySet<string>
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
/**
|
|
@@ -191,6 +194,7 @@ export class KeyLayer {
|
|
|
191
194
|
private readonly codeOids: Map<string, string>,
|
|
192
195
|
private readonly toolchain: { id: string; uncacheable: string[] },
|
|
193
196
|
private readonly scanPathsUsed: string[],
|
|
197
|
+
private readonly scope: ReadonlySet<string> | null,
|
|
194
198
|
/** wall time of the WHOLE layer: toolchain identity, the module-graph
|
|
195
199
|
* closures, the git scan and the closure hashing. Reported rather
|
|
196
200
|
* than the scan alone, because the scan alone is the flattering
|
|
@@ -237,7 +241,8 @@ export class KeyLayer {
|
|
|
237
241
|
const closureByCheck = new Map<string, string[]>()
|
|
238
242
|
// computed for EVERY check with code, not only the content ones: a
|
|
239
243
|
// world check's TTL cache must break when the probe itself is rewritten
|
|
240
|
-
|
|
244
|
+
const scoped = o.scope == null ? decls : decls.filter((d) => o.scope!.has(d.id))
|
|
245
|
+
for (const d of scoped) {
|
|
241
246
|
const entries = d.code ?? []
|
|
242
247
|
if (entries.length > 0) closureByCheck.set(d.id, importClosure(cfg, entries))
|
|
243
248
|
}
|
|
@@ -246,14 +251,13 @@ export class KeyLayer {
|
|
|
246
251
|
// are mostly directories, mostly unchanged, and git's stat cache
|
|
247
252
|
// answers them without reading a byte.
|
|
248
253
|
const paths = new Set<string>()
|
|
249
|
-
for (const d of
|
|
254
|
+
for (const d of scoped) {
|
|
250
255
|
if (classify(d).population !== 'content') continue
|
|
251
256
|
for (const p of d.inputs ?? []) paths.add(p)
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
// the closure is INDIVIDUAL FILES belonging to no declared path, so it
|
|
255
|
-
//
|
|
256
|
-
// add` spawn per file (measured: 15ms for the whole set)
|
|
260
|
+
// is hashed in process (hashFiles) instead of staged one `git add` each
|
|
257
261
|
const closureFiles = new Set<string>()
|
|
258
262
|
for (const files of closureByCheck.values()) for (const f of files) closureFiles.add(f)
|
|
259
263
|
|
|
@@ -268,7 +272,7 @@ export class KeyLayer {
|
|
|
268
272
|
// downstream may key on it. Loud, and the whole run goes uncached.
|
|
269
273
|
scanError = e instanceof GitScanError ? e.message : String(e)
|
|
270
274
|
}
|
|
271
|
-
return new KeyLayer(root, decls, registration, scan, scanError, closureByCheck, codeOids, toolchain, [...paths].sort(), performance.now() - started)
|
|
275
|
+
return new KeyLayer(root, decls, registration, scan, scanError, closureByCheck, codeOids, toolchain, [...paths].sort(), o.scope ?? null, performance.now() - started)
|
|
272
276
|
}
|
|
273
277
|
|
|
274
278
|
/** the key for one check, or the reason there is none. */
|
|
@@ -284,6 +288,7 @@ export class KeyLayer {
|
|
|
284
288
|
codeKey: own.length === 0 || !ownReadable ? null : createHash('sha256').update(codeLines.join('\n')).digest('hex').slice(0, 16),
|
|
285
289
|
}
|
|
286
290
|
if (c.error != null) return { ...base, reason: c.error }
|
|
291
|
+
if (this.scope != null && !this.scope.has(d.id)) return { ...base, reason: 'not one of the checks this run executes, so nothing was scanned for it' }
|
|
287
292
|
if (c.population === 'world') return { ...base, reason: `world check — TTL ${d.ttl}, no content key` }
|
|
288
293
|
if (c.population === 'none') return { ...base, reason: c.reason ?? 'undeclared' }
|
|
289
294
|
if (this.scanError != null) return { ...base, reason: `git scan failed, nothing may be keyed: ${this.scanError}` }
|