rvlib-shipkit 1.4.0 → 1.6.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 +114 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/assets/gen/comfyts-gen.ts +12 -8
- package/src/assets/server/studio-server.ts +14 -2
- package/src/check.ts +73 -103
- package/src/checks/SK001-package-json-shape.check.ts +11 -6
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +7 -3
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/checks/SK030-cache-audit.check.ts +34 -0
- package/src/checks/SK031-trace-audit.check.ts +39 -0
- package/src/checks/SK032-ignored-inputs.check.ts +79 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +17 -13
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_CacheAudit.ts +43 -0
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +8 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +120 -7
- package/src/cli/cmd_Home.ts +26 -19
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +20 -8
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +5 -4
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +6 -4
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_TraceAudit.ts +62 -0
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +11 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +12 -4
- package/src/collections/providers.ts +5 -3
- package/src/core/app-icon.ts +154 -0
- package/src/core/audit-report.ts +21 -0
- package/src/core/cache-audit.ts +72 -0
- package/src/core/canonical-scripts.ts +2 -2
- package/src/core/check-ctx.ts +14 -5
- package/src/core/check-runner.ts +43 -23
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/floor-resolve.ts +5 -7
- package/src/core/git-root.ts +13 -0
- package/src/core/ignored-block.ts +94 -0
- package/src/core/ignored-inputs.ts +47 -0
- package/src/core/jobs.ts +9 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-lib.ts +59 -4
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/read-set.ts +97 -9
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +14 -5
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +48 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/trace-audit.ts +120 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/gui-app.ts +24 -27
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/home-start.ts +11 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/open-app.ts +2 -2
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/via-hub.ts +14 -0
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/db.ts +2 -13
- package/src/ledger/gate-checks.ts +133 -61
- package/src/ledger/git-key.ts +153 -31
- package/src/ledger/git-scan.ts +5 -2
- package/src/ledger/ledger.ts +136 -21
- package/src/ledger/store.ts +14 -0
- package/src/ledger/toolchain-identity.ts +248 -45
- package/src/plan/repo-source.ts +18 -39
- package/src/problems/extract.ts +3 -0
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +46 -54
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-svc.ts +36 -3
- package/src/serve/hub-app/admin-view.tsx +46 -12
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-card.tsx +1 -44
- package/src/serve/hub-app/col-tables-view.tsx +84 -11
- package/src/serve/hub-app/col-tables.ts +27 -0
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -12
- package/src/serve/hub-app/home-strip.tsx +115 -64
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +32 -8
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +89 -63
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +15 -4
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +45 -0
- package/src/serve/hub-app/project-list.css +4 -1
- package/src/serve/hub-app/project-list.tsx +6 -3
- package/src/serve/hub-app/projects-svc.ts +25 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +131 -0
- package/src/serve/hub-app/repo-st.ts +70 -14
- package/src/serve/hub-app/sidebar.tsx +17 -42
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +151 -155
- package/src/serve/hub-app/tree-row.tsx +55 -12
- package/src/serve/hub-app/tree-svc.ts +97 -56
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +7 -18
- package/src/serve/project-gutter-route.ts +22 -1
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/suite/judge-suites.ts +6 -0
- package/src/trace/interpose.c +655 -0
- package/src/trace/read-trace.ts +485 -0
- package/src/trace/tracer.ts +282 -0
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// THE PORT of a check file written before every context was explicit: its
|
|
2
|
+
// source in, the same check out with `runs`, `cache`, `fix` and `applies`
|
|
3
|
+
// spelled out, carrying the values the retired keys resolved to (the gate,
|
|
4
|
+
// the health pass and the planner of 0.62), so a ported check runs exactly
|
|
5
|
+
// where it ran before. Pure: text in, text out. SK016 reports what this finds
|
|
6
|
+
// and its repair applies it (core/port-check-cli.ts reaches the disk).
|
|
7
|
+
//
|
|
8
|
+
// the one constraint a reader breaks first: it edits the check object by the
|
|
9
|
+
// RANGE of each top level property, found on a masked copy of the source
|
|
10
|
+
// (strings, comments and regex literals blanked), so a value, a comment or a
|
|
11
|
+
// multi line lambda it does not touch comes out byte for byte.
|
|
12
|
+
|
|
13
|
+
/** the keys the explicit declaration retired */
|
|
14
|
+
export const RETIRED_KEYS = ['when', 'inputs', 'ttl', 'never', 'every', 'cron', 'whenInactive', 'planOff', 'cacheKey'] as const
|
|
15
|
+
|
|
16
|
+
/** the range a repo row nothing keys ran on, before it declared one */
|
|
17
|
+
const DAILY = "['20h', '28h']"
|
|
18
|
+
|
|
19
|
+
/** the source with every string, comment, template text and regex literal
|
|
20
|
+
* blanked to spaces, newlines kept, so offsets line up with the original */
|
|
21
|
+
export function maskSource(src: string): string {
|
|
22
|
+
const out = src.split('')
|
|
23
|
+
const blank = (from: number, to: number): void => { for (let k = from; k < to; k++) if (out[k] !== '\n') out[k] = ' ' }
|
|
24
|
+
let i = 0
|
|
25
|
+
// one entry per open template: the brace depth of its current `${`
|
|
26
|
+
const tpl: number[] = []
|
|
27
|
+
let last = ''
|
|
28
|
+
const regexOk = (): boolean => last === '' || '(,=:[!&|?{};+-*%<>~^'.includes(last)
|
|
29
|
+
const readTemplateText = (): void => {
|
|
30
|
+
// at a character inside template text: blank until `${` or the closing backtick
|
|
31
|
+
const start = i
|
|
32
|
+
while (i < src.length && src[i] !== '`' && !(src[i] === '$' && src[i + 1] === '{')) i += src[i] === '\\' ? 2 : 1
|
|
33
|
+
blank(start, i)
|
|
34
|
+
if (src[i] === '`') { i++; tpl.pop(); last = 'x' } else { i += 2; tpl[tpl.length - 1] = 0; last = '{' }
|
|
35
|
+
}
|
|
36
|
+
while (i < src.length) {
|
|
37
|
+
const ch = src[i]!
|
|
38
|
+
const next = src[i + 1]
|
|
39
|
+
if (ch === '/' && next === '/') { const s = i; while (i < src.length && src[i] !== '\n') i++; blank(s, i); continue }
|
|
40
|
+
if (ch === '/' && next === '*') { const s = i; const e = src.indexOf('*/', i + 2); i = e < 0 ? src.length : e + 2; blank(s, i); continue }
|
|
41
|
+
if (ch === "'" || ch === '"') {
|
|
42
|
+
const s = i
|
|
43
|
+
i++
|
|
44
|
+
while (i < src.length && src[i] !== ch && src[i] !== '\n') i += src[i] === '\\' ? 2 : 1
|
|
45
|
+
i++
|
|
46
|
+
blank(s + 1, i - 1)
|
|
47
|
+
last = 'x'
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (ch === '`') { tpl.push(-1); i++; readTemplateText(); continue }
|
|
51
|
+
if (tpl.length > 0 && tpl[tpl.length - 1]! >= 0) {
|
|
52
|
+
if (ch === '{') tpl[tpl.length - 1]!++
|
|
53
|
+
if (ch === '}') {
|
|
54
|
+
if (tpl[tpl.length - 1] === 0) { tpl[tpl.length - 1] = -1; i++; readTemplateText(); continue }
|
|
55
|
+
tpl[tpl.length - 1]!--
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (ch === '/' && regexOk()) {
|
|
59
|
+
const s = i
|
|
60
|
+
let j = i + 1
|
|
61
|
+
let inClass = false
|
|
62
|
+
while (j < src.length && src[j] !== '\n') {
|
|
63
|
+
if (src[j] === '\\') { j += 2; continue }
|
|
64
|
+
if (src[j] === '[') inClass = true
|
|
65
|
+
else if (src[j] === ']') inClass = false
|
|
66
|
+
else if (src[j] === '/' && !inClass) break
|
|
67
|
+
j++
|
|
68
|
+
}
|
|
69
|
+
if (src[j] === '/') { j++; while (/[a-z]/.test(src[j] ?? '')) j++; blank(s + 1, j - 1); i = j; last = 'x'; continue }
|
|
70
|
+
}
|
|
71
|
+
if (!/\s/.test(ch)) last = ch
|
|
72
|
+
i++
|
|
73
|
+
}
|
|
74
|
+
return out.join('')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** one top level property of the check object: its whole segment (leading
|
|
78
|
+
* whitespace and comments included), and where its key and value sit */
|
|
79
|
+
export type Prop = { key: string | null; seg: [number, number]; keyAt: number; value: string }
|
|
80
|
+
|
|
81
|
+
export type CheckObject = { open: number; close: number; props: Prop[]; indent: string }
|
|
82
|
+
|
|
83
|
+
const KEY_RE = /^\s*(?:(?:async|get|set)\s+)?([A-Za-z_$][\w$]*)\s*\??\s*[:(]/
|
|
84
|
+
|
|
85
|
+
/** the check object of a file: `check({...})`, else `export default {...}`
|
|
86
|
+
* in a check file. Null when there is none */
|
|
87
|
+
export function findCheckObject(src: string, o: { plainDefault: boolean }): CheckObject | null {
|
|
88
|
+
const m = maskSource(src)
|
|
89
|
+
const call = /\bcheck\s*\(\s*\{/.exec(m)
|
|
90
|
+
const plain = o.plainDefault ? /\bexport\s+default\s*\{/.exec(m) : null
|
|
91
|
+
const hit = call ?? plain
|
|
92
|
+
if (hit == null) return null
|
|
93
|
+
const open = hit.index + hit[0].length - 1
|
|
94
|
+
let depth = 0
|
|
95
|
+
let start = open + 1
|
|
96
|
+
const segs: [number, number][] = []
|
|
97
|
+
let close = -1
|
|
98
|
+
for (let k = open; k < m.length; k++) {
|
|
99
|
+
const ch = m[k]
|
|
100
|
+
if (ch === '{' || ch === '(' || ch === '[') depth++
|
|
101
|
+
else if (ch === '}' || ch === ')' || ch === ']') {
|
|
102
|
+
depth--
|
|
103
|
+
if (depth === 0) { segs.push([start, k]); close = k; break }
|
|
104
|
+
} else if (ch === ',' && depth === 1) { segs.push([start, k]); start = k + 1 }
|
|
105
|
+
}
|
|
106
|
+
if (close < 0) return null
|
|
107
|
+
const props: Prop[] = segs.map(([a, b]) => {
|
|
108
|
+
const km = KEY_RE.exec(m.slice(a, b))
|
|
109
|
+
if (km == null) return { key: null, seg: [a, b], keyAt: -1, value: '' }
|
|
110
|
+
const keyAt = a + km.index + km[0].indexOf(km[1]!)
|
|
111
|
+
const colon = m.indexOf(':', keyAt)
|
|
112
|
+
const isColon = colon >= 0 && colon < b && m.slice(keyAt + km[1]!.length, colon).trim().replace('?', '') === ''
|
|
113
|
+
return { key: km[1]!, seg: [a, b], keyAt, value: isColon ? src.slice(colon + 1, b).trim() : '' }
|
|
114
|
+
})
|
|
115
|
+
const first = props.find((p) => p.key != null)
|
|
116
|
+
const indent = first == null ? ' ' : (/[ \t]*$/.exec(src.slice(0, first.keyAt))?.[0] ?? ' ')
|
|
117
|
+
return { open, close, props, indent }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** what a file's check object lacks or still says in the retired shape: each
|
|
121
|
+
* retired key it declares, `fix` when it is a function, and `runs` when the
|
|
122
|
+
* object declares none (a file that declared nothing ran on defaults) */
|
|
123
|
+
export function retiredKeysIn(src: string, o: { plainDefault: boolean }): string[] {
|
|
124
|
+
const obj = findCheckObject(src, o)
|
|
125
|
+
if (obj == null) return []
|
|
126
|
+
const keys = new Set(obj.props.map((p) => p.key))
|
|
127
|
+
const out: string[] = RETIRED_KEYS.filter((k) => keys.has(k))
|
|
128
|
+
const fix = obj.props.find((p) => p.key === 'fix')
|
|
129
|
+
if (fix != null && !/^(['"])(none|ci|manual)\1$/.test(fix.value)) out.push('fix')
|
|
130
|
+
if (!keys.has('runs') && (keys.has('run') || keys.has('cmd'))) out.push('runs')
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** a string literal's value, or null for any other expression */
|
|
135
|
+
export const literal = (v: string | undefined): string | null => {
|
|
136
|
+
const m = v == null ? null : /^(['"`])([^'"`$\\]*)\1$/.exec(v.trim())
|
|
137
|
+
return m == null ? null : m[2]!
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** what a check declared before, as the port reads it */
|
|
141
|
+
export type LegacyDecl = {
|
|
142
|
+
/** the `when` literal, null when absent, undefined when it is not a literal */
|
|
143
|
+
when: string | null | undefined
|
|
144
|
+
build: boolean
|
|
145
|
+
body: 'run' | 'cmd'
|
|
146
|
+
/** the expression text of each declaration, absent when not declared */
|
|
147
|
+
inputs?: string
|
|
148
|
+
ttl?: string
|
|
149
|
+
never?: string
|
|
150
|
+
cacheKey?: string
|
|
151
|
+
every?: string
|
|
152
|
+
cron?: string
|
|
153
|
+
whenInactive?: string
|
|
154
|
+
planOff?: string
|
|
155
|
+
/** a repair function is declared */
|
|
156
|
+
repair: boolean
|
|
157
|
+
/** why the body's imports reach the disk around `c`, for an undeclared
|
|
158
|
+
* `run` body (core/raw-access.ts), else null */
|
|
159
|
+
rawAccess: string | null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type Derived = { runs: string; cache: string; fix: string; applies: string; notes: string[] }
|
|
163
|
+
|
|
164
|
+
/** the explicit declaration a legacy one resolved to, or why it cannot be
|
|
165
|
+
* carried across without a person */
|
|
166
|
+
export function deriveExplicit(d: LegacyDecl, q: (s: string) => string): Derived | { error: string } {
|
|
167
|
+
const notes: string[] = []
|
|
168
|
+
if (d.when === undefined) return { error: '`when` is not a string literal' }
|
|
169
|
+
const whenLit = d.when
|
|
170
|
+
const off = whenLit != null && (whenLit === 'off' || whenLit.startsWith('off:')) ? whenLit.slice(4).trim() || 'off' : null
|
|
171
|
+
const at = off != null ? 'always' : (whenLit ?? 'always')
|
|
172
|
+
if (!['always', 'release', 'health'].includes(at)) return { error: `when '${whenLit}' is not always, release, health or off:<why>` }
|
|
173
|
+
const ttlLit = d.ttl == null ? null : literal(d.ttl)
|
|
174
|
+
const forever = ttlLit === 'forever'
|
|
175
|
+
// the population the gate read it as
|
|
176
|
+
const keyed = d.inputs != null || (d.ttl == null && d.never == null && d.body === 'run' && d.rawAccess == null)
|
|
177
|
+
const world = d.ttl != null
|
|
178
|
+
// the gate axes: a build step never ran in the health pass
|
|
179
|
+
const gates = at === 'always' ? [true, true, true] : at === 'release' ? [false, true, true] : [false, false, false]
|
|
180
|
+
const onHealth = !d.build && true
|
|
181
|
+
// the planner: only an always or health row, and never a build step off the gate
|
|
182
|
+
const plannable = (at === 'always' || at === 'health') && !(d.build && at === 'health')
|
|
183
|
+
const gate = at !== 'health'
|
|
184
|
+
const declaredClock = d.cron != null || d.every !== undefined
|
|
185
|
+
const fallback = forever ? "'once'" : d.ttl != null ? d.ttl : d.build ? null : keyed ? null : DAILY
|
|
186
|
+
const clocked = fallback != null || declaredClock
|
|
187
|
+
const onChange = forever || (!world && (keyed || (gate && !clocked)))
|
|
188
|
+
let onSave = 'false'
|
|
189
|
+
let onClock = 'false'
|
|
190
|
+
if (plannable) {
|
|
191
|
+
onSave = onChange ? (keyed || forever ? "'moved'" : "'burst'") : 'false'
|
|
192
|
+
const every = d.cron ?? (d.every !== undefined ? d.every : fallback)
|
|
193
|
+
if (every != null && every.trim() !== 'null') {
|
|
194
|
+
if (literal(every) === 'once') onClock = "'once'"
|
|
195
|
+
else onClock = `{ every: ${every}, inactive: ${d.whenInactive ?? q('wait')} }`
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (onClock === "'once'" && !forever) return { error: "every 'once' without ttl 'forever': a fact asked once needs a declared value" }
|
|
199
|
+
if (forever && onClock !== "'once'" && plannable) return { error: "ttl 'forever' with a declared every: a fact is asked once, never on a period" }
|
|
200
|
+
let cache: string
|
|
201
|
+
if (d.inputs != null) cache = `{ by: ${q('files')}, inputs: ${d.inputs}, key: ${d.cacheKey ?? 'null'} }`
|
|
202
|
+
else if (forever) cache = `{ by: ${q('fact')}, key: ${d.cacheKey ?? `() => ${q('declared')}`} }`
|
|
203
|
+
else if (d.ttl != null) cache = `{ by: ${q('ttl')}, span: ${d.ttl}, key: ${d.cacheKey ?? 'null'} }`
|
|
204
|
+
else if (d.never != null) cache = `{ by: ${q('never')}, why: ${d.never} }`
|
|
205
|
+
else if (d.body === 'run' && d.rawAccess == null) cache = `{ by: ${q('reads')} }`
|
|
206
|
+
else if (d.body === 'run') cache = `{ by: ${q('never')}, why: ${q(d.rawAccess!)} }`
|
|
207
|
+
else cache = `{ by: ${q('never')}, why: ${q('a cmd declaring no inputs: nothing keys it')} }`
|
|
208
|
+
if (forever && !plannable) return { error: "ttl 'forever' on a check the planner never ran: a fact nobody asks once is never answered" }
|
|
209
|
+
// the ci fix pass applied every repair; a check off the quick gate now
|
|
210
|
+
// repairs only by name
|
|
211
|
+
const fix = !d.repair ? 'none' : gates[0] ? 'ci' : 'manual'
|
|
212
|
+
if (d.repair && !gates[0]) notes.push("its repair ran in the ci fix pass although the gate did not run it: now fix 'manual', shipkit check --fix <id> applies it")
|
|
213
|
+
if (d.build && d.repair) return { error: 'a build step with a repair: a tool repairs its own findings, drop one of them' }
|
|
214
|
+
const runs = `{ onPreCommit: false, onCi: ${gates[0]}, onFull: ${gates[1]}, onRelease: ${gates[2]}, onHealth: ${d.build ? false : onHealth}, onSave: ${onSave}, onClock: ${onClock} }`
|
|
215
|
+
let applies = d.planOff ?? q('always')
|
|
216
|
+
if (d.planOff != null) notes.push('its planOff only kept it off the planner; as applies it is n/a in every context while it gives a reason')
|
|
217
|
+
if (off != null) {
|
|
218
|
+
if (d.planOff != null) return { error: '`when` off and a planOff: say one reason' }
|
|
219
|
+
applies = `() => ${q(off)}`
|
|
220
|
+
notes.push('it was off in its own file: applies now gives the reason, so it runs nowhere')
|
|
221
|
+
}
|
|
222
|
+
return { runs, cache, fix, applies, notes }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type PortResult =
|
|
226
|
+
| { changed: false; reason: string }
|
|
227
|
+
| { changed: true; source: string; notes: string[] }
|
|
228
|
+
| { changed: false; error: string }
|
|
229
|
+
|
|
230
|
+
/** the file ported, or why not. `rawAccess` is what the caller measured on
|
|
231
|
+
* the file's import closure, needed only for an undeclared `run` body */
|
|
232
|
+
export function portCheckSource(src: string, o: { plainDefault: boolean; rawAccess: string | null }): PortResult {
|
|
233
|
+
const obj = findCheckObject(src, o)
|
|
234
|
+
if (obj == null) return { changed: false, reason: 'no check object' }
|
|
235
|
+
const retired = retiredKeysIn(src, o)
|
|
236
|
+
if (retired.length === 0) return { changed: false, reason: 'already explicit' }
|
|
237
|
+
const byKey = new Map(obj.props.filter((p) => p.key != null).map((p) => [p.key!, p]))
|
|
238
|
+
for (const k of ['runs', 'cache', 'applies']) if (byKey.has(k)) return { changed: false, error: `it declares ${k} beside a retired shape: finish it by hand` }
|
|
239
|
+
const dq = /^\s*import[^\n]*"/m.test(src) && !/^\s*import[^\n]*'/m.test(src)
|
|
240
|
+
const q = (s: string): string => (dq ? `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`)
|
|
241
|
+
const val = (k: string): string | undefined => byKey.get(k)?.value
|
|
242
|
+
const build = val('build')?.trim() === 'true'
|
|
243
|
+
const body = byKey.has('cmd') ? 'cmd' : 'run'
|
|
244
|
+
const whenV = val('when')
|
|
245
|
+
const derived = deriveExplicit({
|
|
246
|
+
when: whenV == null ? null : (literal(whenV) ?? undefined),
|
|
247
|
+
build,
|
|
248
|
+
body,
|
|
249
|
+
inputs: val('inputs'),
|
|
250
|
+
ttl: val('ttl'),
|
|
251
|
+
never: val('never'),
|
|
252
|
+
cacheKey: val('cacheKey'),
|
|
253
|
+
every: val('every'),
|
|
254
|
+
cron: val('cron'),
|
|
255
|
+
whenInactive: val('whenInactive'),
|
|
256
|
+
planOff: val('planOff'),
|
|
257
|
+
repair: retired.includes('fix'),
|
|
258
|
+
rawAccess: o.rawAccess,
|
|
259
|
+
}, q)
|
|
260
|
+
if ('error' in derived) return { changed: false, error: derived.error }
|
|
261
|
+
// rebuild: drop the retired props, keep their comments above the new
|
|
262
|
+
// keys, rename a fix function to repair, insert before the body
|
|
263
|
+
const ind = obj.indent
|
|
264
|
+
const comments: string[] = []
|
|
265
|
+
const kept: string[] = []
|
|
266
|
+
let insertAt = -1
|
|
267
|
+
for (const p of obj.props) {
|
|
268
|
+
const text = src.slice(p.seg[0], p.seg[1])
|
|
269
|
+
if (p.key != null && (RETIRED_KEYS as readonly string[]).includes(p.key)) {
|
|
270
|
+
for (const l of src.slice(p.seg[0], p.keyAt).split('\n')) if (l.trim().startsWith('//')) comments.push(`${ind}${l.trim()}`)
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (insertAt < 0 && (p.key === 'run' || p.key === 'cmd' || (p.key === 'fix' && retired.includes('fix')))) insertAt = kept.length
|
|
274
|
+
if (p.key === 'fix' && retired.includes('fix')) {
|
|
275
|
+
const rel = p.keyAt - p.seg[0]
|
|
276
|
+
kept.push(`${text.slice(0, rel)}repair${text.slice(rel + 3)}`)
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
kept.push(text)
|
|
280
|
+
}
|
|
281
|
+
// a trailing comma leaves an empty last segment: keep it last
|
|
282
|
+
const tail = kept.length > 0 && kept[kept.length - 1]!.trim() === '' ? kept.pop()! : null
|
|
283
|
+
if (insertAt < 0 || insertAt > kept.length) insertAt = kept.length
|
|
284
|
+
// each new property is one segment shaped like the others: a newline,
|
|
285
|
+
// the indent, the key. The comments of the keys it replaced lead the first
|
|
286
|
+
const newProps = [
|
|
287
|
+
`\n${comments.map((c) => `${c}\n`).join('')}${ind}runs: ${derived.runs}`,
|
|
288
|
+
`\n${ind}cache: ${derived.cache}`,
|
|
289
|
+
`\n${ind}fix: ${q(derived.fix)}`,
|
|
290
|
+
`\n${ind}applies: ${derived.applies}`,
|
|
291
|
+
]
|
|
292
|
+
const props = [...kept.slice(0, insertAt), ...newProps, ...kept.slice(insertAt), ...(tail == null ? [] : [tail])]
|
|
293
|
+
const source = `${src.slice(0, obj.open + 1)}${props.join(',')}${src.slice(obj.close)}`
|
|
294
|
+
return { changed: true, source, notes: derived.notes }
|
|
295
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// which check files of a repo still need the port (core/port-check-file.ts):
|
|
2
|
+
// read through the context alone, so SK029 is cached on the listing and the
|
|
3
|
+
// sources. A legacy check SCRIPT is left to its own static reader.
|
|
4
|
+
import { CHECK_SUFFIX, isLegacyCheckSource, SKIPPED_DIRS, UNIT_SUFFIX } from './check-file-names.ts'
|
|
5
|
+
import type { ScanCtx } from './check-ctx.ts'
|
|
6
|
+
import { retiredKeysIn } from './port-check-file.ts'
|
|
7
|
+
|
|
8
|
+
/** a `.check.ts` or `.check.shipkit.ts` file may export a plain object; a
|
|
9
|
+
* plain unit file is a check only through a `check(...)` call */
|
|
10
|
+
export const plainDefaultFor = (rel: string): boolean => rel.endsWith(CHECK_SUFFIX) || rel.endsWith(`.check${UNIT_SUFFIX}`)
|
|
11
|
+
|
|
12
|
+
const walked = (rel: string): boolean => rel.split('/').slice(0, -1).every((d) => !SKIPPED_DIRS.has(d) && !d.startsWith('.'))
|
|
13
|
+
|
|
14
|
+
/** the cheap text test before the scan: a file that declares `runs` and none
|
|
15
|
+
* of the retired spellings is explicit already */
|
|
16
|
+
const RETIRED_RE = /\b(when|inputs|ttl|never|every|cron|whenInactive|planOff|cacheKey)\s*\??\s*:|\bfix\s*\??\s*:\s*(async\b|\(|function\b|[A-Za-z_$][\w$]*\s*[,\n])/
|
|
17
|
+
|
|
18
|
+
export type Unported = { rel: string; keys: string[] }
|
|
19
|
+
|
|
20
|
+
export function unportedCheckFiles(c: Pick<ScanCtx, 'list' | 'read'>): Unported[] {
|
|
21
|
+
const rels = [...c.list(`**/*${CHECK_SUFFIX}`), ...c.list(`**/*${UNIT_SUFFIX}`)].filter(walked)
|
|
22
|
+
const out: Unported[] = []
|
|
23
|
+
for (const rel of [...new Set(rels)].sort()) {
|
|
24
|
+
const src = c.read(rel)
|
|
25
|
+
if (src == null || isLegacyCheckSource(src)) continue
|
|
26
|
+
if (/\bruns\s*:/.test(src) && !RETIRED_RE.test(src)) continue
|
|
27
|
+
const keys = retiredKeysIn(src, { plainDefault: plainDefaultFor(rel) })
|
|
28
|
+
if (keys.length > 0) out.push({ rel, keys })
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
package/src/core/read-set.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// a millisecond later: the next run hashes the disk again, the two differ, and
|
|
10
10
|
// the check runs. Nothing here can turn a mid-run edit into a replay.
|
|
11
11
|
import { createHash } from 'crypto'
|
|
12
|
-
import { lstatSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
13
|
-
import { join } from 'path'
|
|
12
|
+
import { lstatSync, readdirSync, readFileSync, readlinkSync, statSync } from 'fs'
|
|
13
|
+
import { isAbsolute, join } from 'path'
|
|
14
14
|
|
|
15
15
|
/** `file` or `dir` when the path was there, null when it was not */
|
|
16
16
|
export type PathKind = 'file' | 'dir' | null
|
|
@@ -29,6 +29,60 @@ export type ReadSet = {
|
|
|
29
29
|
/** repo-relative path → whether a symlink stood there. Absent from a set
|
|
30
30
|
* stored before it existed */
|
|
31
31
|
links?: Record<string, boolean>
|
|
32
|
+
/** path → its stat signature (`statSig`). For a path in `files` it is a
|
|
33
|
+
* fast path: an unchanged signature skips the content hash. For an
|
|
34
|
+
* absolute path outside the repo it is the whole record: a traced task
|
|
35
|
+
* reads its runtime and its tools there, which are too big to hash on
|
|
36
|
+
* every replay and never shared between checkouts anyway */
|
|
37
|
+
sigs?: Record<string, string>
|
|
38
|
+
/** local port → `closed`: a traced task met a refusal there, and its
|
|
39
|
+
* answer holds only while nothing listens on it */
|
|
40
|
+
ports?: Record<string, 'closed'>
|
|
41
|
+
/** symlink → its target, null when no link stands there: what a traced
|
|
42
|
+
* task learned from readlink or lstat */
|
|
43
|
+
targets?: Record<string, string | null>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** the target a symlink at `abs` points to, null when none stands there */
|
|
47
|
+
export function linkTarget(abs: string): string | null {
|
|
48
|
+
try {
|
|
49
|
+
return readlinkSync(abs)
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let listenMemo: { at: number; ports: Set<string> } | null = null
|
|
56
|
+
|
|
57
|
+
/** the TCP ports something listens on right now, on any local address. One
|
|
58
|
+
* netstat per second at most: a replay asks for several at once */
|
|
59
|
+
export function listeningPorts(now = Date.now()): Set<string> {
|
|
60
|
+
if (listenMemo != null && now - listenMemo.at < 1_000) return listenMemo.ports
|
|
61
|
+
const ports = new Set<string>()
|
|
62
|
+
const r = Bun.spawnSync(['/usr/sbin/netstat', '-an', '-p', 'tcp'], { stdout: 'pipe', stderr: 'pipe' })
|
|
63
|
+
for (const line of r.stdout.toString().split('\n')) {
|
|
64
|
+
if (!/\bLISTEN\b/.test(line)) continue
|
|
65
|
+
const local = line.trim().split(/\s+/)[3] ?? ''
|
|
66
|
+
const port = local.slice(local.lastIndexOf('.') + 1)
|
|
67
|
+
if (/^\d+$/.test(port)) ports.add(port)
|
|
68
|
+
}
|
|
69
|
+
listenMemo = { at: now, ports }
|
|
70
|
+
return ports
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** a path as recorded: repo-relative, or absolute for one outside the repo */
|
|
74
|
+
const at = (root: string, p: string): string => (isAbsolute(p) ? p : join(root, p))
|
|
75
|
+
|
|
76
|
+
/** what stat says about a file: size, modification time to the nanosecond
|
|
77
|
+
* and inode, `-` when nothing is there. Content can change under an equal
|
|
78
|
+
* signature only when a tool restores the mtime on purpose */
|
|
79
|
+
export function statSig(abs: string): string {
|
|
80
|
+
try {
|
|
81
|
+
const s = statSync(abs, { bigint: true })
|
|
82
|
+
return `${s.size}:${s.mtimeNs}:${s.ino}`
|
|
83
|
+
} catch {
|
|
84
|
+
return '-'
|
|
85
|
+
}
|
|
32
86
|
}
|
|
33
87
|
|
|
34
88
|
/** one entry of a directory, as `c.entries` returns it */
|
|
@@ -103,6 +157,9 @@ export class Reads {
|
|
|
103
157
|
readonly lists = new Map<string, string>()
|
|
104
158
|
readonly dirs = new Map<string, string | null>()
|
|
105
159
|
readonly links = new Map<string, boolean>()
|
|
160
|
+
readonly sigs = new Map<string, string>()
|
|
161
|
+
readonly ports = new Map<string, 'closed'>()
|
|
162
|
+
readonly targets = new Map<string, string | null>()
|
|
106
163
|
|
|
107
164
|
file(path: string, oid: string | null): void { this.files.set(path, oid) }
|
|
108
165
|
exist(path: string, kind: PathKind): void { this.exists.set(path, kind) }
|
|
@@ -116,11 +173,26 @@ export class Reads {
|
|
|
116
173
|
for (const [k, v] of o.lists) this.lists.set(k, v)
|
|
117
174
|
for (const [k, v] of o.dirs) this.dirs.set(k, v)
|
|
118
175
|
for (const [k, v] of o.links) this.links.set(k, v)
|
|
176
|
+
for (const [k, v] of o.sigs) this.sigs.set(k, v)
|
|
177
|
+
for (const [k, v] of o.ports) this.ports.set(k, v)
|
|
178
|
+
for (const [k, v] of o.targets) this.targets.set(k, v)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** a whole set, as a traced process read it (trace/read-trace.ts) */
|
|
182
|
+
mergeSet(s: ReadSet): void {
|
|
183
|
+
for (const [k, v] of Object.entries(s.files)) this.files.set(k, v)
|
|
184
|
+
for (const [k, v] of Object.entries(s.exists)) this.exists.set(k, v)
|
|
185
|
+
for (const [k, v] of Object.entries(s.lists)) this.lists.set(k, v)
|
|
186
|
+
for (const [k, v] of Object.entries(s.dirs ?? {})) this.dirs.set(k, v)
|
|
187
|
+
for (const [k, v] of Object.entries(s.links ?? {})) this.links.set(k, v)
|
|
188
|
+
for (const [k, v] of Object.entries(s.sigs ?? {})) this.sigs.set(k, v)
|
|
189
|
+
for (const [k, v] of Object.entries(s.ports ?? {})) this.ports.set(k, v)
|
|
190
|
+
for (const [k, v] of Object.entries(s.targets ?? {})) this.targets.set(k, v)
|
|
119
191
|
}
|
|
120
192
|
|
|
121
193
|
snapshot(): ReadSet {
|
|
122
194
|
const sorted = <V>(m: Map<string, V>): Record<string, V> => Object.fromEntries([...m].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
123
|
-
return { files: sorted(this.files), exists: sorted(this.exists), lists: sorted(this.lists), dirs: sorted(this.dirs), links: sorted(this.links) }
|
|
195
|
+
return { files: sorted(this.files), exists: sorted(this.exists), lists: sorted(this.lists), dirs: sorted(this.dirs), links: sorted(this.links), ...(this.sigs.size > 0 ? { sigs: sorted(this.sigs) } : {}), ...(this.ports.size > 0 ? { ports: sorted(this.ports) } : {}), ...(this.targets.size > 0 ? { targets: sorted(this.targets) } : {}) }
|
|
124
196
|
}
|
|
125
197
|
}
|
|
126
198
|
|
|
@@ -128,25 +200,41 @@ export class Reads {
|
|
|
128
200
|
* does. `list` answers a glob the way `c.list` would right now, and returns
|
|
129
201
|
* null when it cannot answer honestly (no git scan), which counts as drift. */
|
|
130
202
|
export function readSetDrift(root: string, set: ReadSet, list: (glob: string) => readonly string[] | null): string | null {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
203
|
+
const sigs = set.sigs ?? {}
|
|
204
|
+
for (const [p, sig] of Object.entries(sigs)) {
|
|
205
|
+
if (p in set.files) continue
|
|
206
|
+
const now = statSig(at(root, p))
|
|
207
|
+
if (now !== sig) return `${p} ${sig === '-' ? 'appeared' : now === '-' ? 'is gone' : 'changed'}`
|
|
134
208
|
}
|
|
135
209
|
for (const [p, kind] of Object.entries(set.exists)) {
|
|
136
|
-
const now = kindOf(
|
|
210
|
+
const now = kindOf(at(root, p))
|
|
137
211
|
if (now !== kind) return `${p} ${kind == null ? 'appeared' : now == null ? 'is gone' : `is a ${now} now`}`
|
|
138
212
|
}
|
|
213
|
+
for (const [p, oid] of Object.entries(set.files)) {
|
|
214
|
+
const abs = at(root, p)
|
|
215
|
+
if (sigs[p] != null && statSig(abs) === sigs[p]) continue
|
|
216
|
+
const now = oidOnDisk(abs)
|
|
217
|
+
if (now !== oid) return `${p} ${oid == null ? 'appeared' : now == null ? 'is gone' : 'changed'}`
|
|
218
|
+
}
|
|
139
219
|
for (const [g, hash] of Object.entries(set.lists)) {
|
|
140
220
|
const now = list(g)
|
|
141
221
|
if (now == null) return `${g} cannot be listed without a git scan`
|
|
142
222
|
if (listingHash(now) !== hash) return `the files matching ${g} changed`
|
|
143
223
|
}
|
|
144
224
|
for (const [d, hash] of Object.entries(set.dirs ?? {})) {
|
|
145
|
-
const now = entriesHash(dirEntries(
|
|
225
|
+
const now = entriesHash(dirEntries(at(root, d)))
|
|
146
226
|
if (now !== hash) return `${d}/ ${hash == null ? 'appeared' : now == null ? 'is gone' : 'changed its entries'}`
|
|
147
227
|
}
|
|
228
|
+
for (const [p, t] of Object.entries(set.targets ?? {})) {
|
|
229
|
+
if (linkTarget(at(root, p)) !== t) return `${p} points elsewhere now`
|
|
230
|
+
}
|
|
231
|
+
const closed = Object.keys(set.ports ?? {})
|
|
232
|
+
if (closed.length > 0) {
|
|
233
|
+
const open = listeningPorts()
|
|
234
|
+
for (const port of closed) if (open.has(port)) return `a service listens on local port ${port} now`
|
|
235
|
+
}
|
|
148
236
|
for (const [p, was] of Object.entries(set.links ?? {})) {
|
|
149
|
-
if (isLinkAt(
|
|
237
|
+
if (isLinkAt(at(root, p)) !== was) return `${p} ${was ? 'is no longer a link' : 'became a link'}`
|
|
150
238
|
}
|
|
151
239
|
return null
|
|
152
240
|
}
|
package/src/core/retention.ts
CHANGED
|
@@ -16,8 +16,6 @@ import { JOBS, JOBS_KEEP_MS, JOBS_MAX, sweepJobs } from './jobs.ts'
|
|
|
16
16
|
import { CI_LOG_DIR } from './layout.ts'
|
|
17
17
|
import { compactDb, dbPath, shipkitRoot } from '../ledger/db.ts'
|
|
18
18
|
import { SCAN_INDEX_KEEP_MS, sweepScanIndexes } from '../ledger/git-scan.ts'
|
|
19
|
-
import { capLog, LOG_MAX_BYTES } from '../home/supervise.ts'
|
|
20
|
-
import { processDir } from '../serve/repo-processes.ts'
|
|
21
19
|
import { KEEP_MS as RECORD_KEEP_MS, MAX_ROWS, recordUse, sweepRuns } from '../ledger/run-log.ts'
|
|
22
20
|
import { answersDb, answersUse, importLegacyStore, KEEP_MS as ANSWERS_KEEP_MS, legacyStoreDir, MAX_ANSWERS, sweepAnswers } from '../ledger/store.ts'
|
|
23
21
|
|
|
@@ -169,18 +167,6 @@ export function repoStores(root: string): Store[] {
|
|
|
169
167
|
},
|
|
170
168
|
sweep: (now) => sweepJobs(root, now),
|
|
171
169
|
},
|
|
172
|
-
{
|
|
173
|
-
// the dev servers the hub runs for the repo (serve.processes)
|
|
174
|
-
id: 'process logs',
|
|
175
|
-
where: `${processDir(root)}/*.log`,
|
|
176
|
-
cap: { maxFileBytes: LOG_MAX_BYTES },
|
|
177
|
-
measure: () => walkUse(processDir(root), { match: (n) => n.endsWith('.log') }),
|
|
178
|
-
sweep: () => {
|
|
179
|
-
let names: string[] = []
|
|
180
|
-
try { names = readdirSync(processDir(root)) } catch { return }
|
|
181
|
-
for (const n of names) if (n.endsWith('.log')) capLog(join(processDir(root), n))
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
170
|
]
|
|
185
171
|
if (gitDir != null) {
|
|
186
172
|
const dir = join(gitDir, 'shipkit')
|
package/src/core/run-check.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { gateBinary } from '../ledger/toolchain-identity.ts'
|
|
|
17
17
|
import { runChecks, selectChecks, type Audience, type CheckEvent, type CheckResult, type RunnableCheck } from './check-runner.ts'
|
|
18
18
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
19
19
|
import { reportToMotherhome } from './motherhome.ts'
|
|
20
|
-
import {
|
|
20
|
+
import { unknownOffIds } from './when.ts'
|
|
21
21
|
import { unknownBudgetIds } from './budget.ts'
|
|
22
22
|
import { formatLine, type Line } from './wire.ts'
|
|
23
23
|
import { writeStatus } from './write-status.ts'
|
|
@@ -75,6 +75,8 @@ export type CheckOpts = {
|
|
|
75
75
|
buildOnly?: boolean
|
|
76
76
|
/** the gate asks the proactive checks too (`release.requireHealthy`) */
|
|
77
77
|
includeProactive?: boolean
|
|
78
|
+
/** the release preflight: the rows declaring `onRelease` run */
|
|
79
|
+
release?: boolean
|
|
78
80
|
/** ids this caller does not need (a sub-unit release) */
|
|
79
81
|
exclude?: string[]
|
|
80
82
|
/** the already-composed check list, when the caller built one (runCi does,
|
|
@@ -107,9 +109,9 @@ export type CheckRun = {
|
|
|
107
109
|
advisoryFailures: string[]
|
|
108
110
|
/** checks that never declared `blocking` — always fatal */
|
|
109
111
|
undeclared: string[]
|
|
110
|
-
/** `
|
|
112
|
+
/** `off` entries naming no check of this repo — a toggle that does
|
|
111
113
|
* nothing is worse than none, so it is fatal too */
|
|
112
|
-
|
|
114
|
+
unknownOff: string[]
|
|
113
115
|
/** `budgets` entries naming no check of this repo, fatal for the same reason */
|
|
114
116
|
unknownBudgets: string[]
|
|
115
117
|
/** what every asked-for check did, in declaration order */
|
|
@@ -125,6 +127,7 @@ function selectionOf(checks: RunnableCheck[], o: CheckOpts, tier: TestTier) {
|
|
|
125
127
|
checks,
|
|
126
128
|
audience: o.audience ?? 'health',
|
|
127
129
|
fullTier: tier === 'full',
|
|
130
|
+
release: o.release,
|
|
128
131
|
skipGroups: o.skipGroups,
|
|
129
132
|
onlyIds: o.only,
|
|
130
133
|
withIds: o.withIds,
|
|
@@ -211,7 +214,7 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
211
214
|
// page just replaced
|
|
212
215
|
if (summary.parked.length > 0) {
|
|
213
216
|
const head = summary.parked.slice(0, 8).join(', ')
|
|
214
|
-
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (
|
|
217
|
+
say({ kind: 'skip', text: `${summary.parked.length} check(s) not in this run (their runs do not name this context, or a skipped group, shipkit when shows each): ${head}${summary.parked.length > 8 ? `, and ${summary.parked.length - 8} more` : ''}` }, o.detail ?? print)
|
|
215
218
|
}
|
|
216
219
|
// the survey dies with the run that made it, and only the run that made it
|
|
217
220
|
if (inherited == null || inherited === '') for (const f of [sharePath, `${sharePath}.lock`]) { try { rmSync(f, { force: true }) } catch {} }
|
|
@@ -225,17 +228,17 @@ export async function runCheckPass(root: string, o: CheckOpts = {}): Promise<Che
|
|
|
225
228
|
if (o.ledger === undefined && ledger != null) for (const l of ledger.header(ledger.commit())) say(l)
|
|
226
229
|
// the caller that OPENED the record owns writing it, the same rule
|
|
227
230
|
if (o.runLog === undefined && log != null) for (const l of log.flush()) say(l)
|
|
228
|
-
//
|
|
231
|
+
// an `off` id naming nothing is a switch wired to no check: the id is read
|
|
229
232
|
// against the WHOLE inventory this run composed, never its narrowed list
|
|
230
233
|
const rows = composed.map((g) => g.row)
|
|
231
|
-
const
|
|
234
|
+
const unknownOff = unknownOffIds(rows, c)
|
|
232
235
|
const unknownBudgets = unknownBudgetIds(rows.map((r) => r.decl.id), c.budgets)
|
|
233
236
|
return {
|
|
234
237
|
lines: [...collected, ...lines],
|
|
235
238
|
blocked: !summary.ok,
|
|
236
239
|
advisoryFailures: summary.advisoryFailures,
|
|
237
240
|
undeclared: summary.undeclared,
|
|
238
|
-
|
|
241
|
+
unknownOff,
|
|
239
242
|
unknownBudgets,
|
|
240
243
|
results: summary.results,
|
|
241
244
|
interrupted: summary.interrupted,
|
|
@@ -247,13 +250,13 @@ export async function runCheck(root: string, o: CheckOpts = {}): Promise<number>
|
|
|
247
250
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
248
251
|
const c = o.config ?? (await loadRepoConfig(root))
|
|
249
252
|
const run = await runCheckPass(root, { ...o, config: c })
|
|
250
|
-
const code = run.blocked || run.undeclared.length > 0 || run.
|
|
253
|
+
const code = run.blocked || run.undeclared.length > 0 || run.unknownOff.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
251
254
|
if (o.json === true) { print(JSON.stringify(run)); return code }
|
|
252
255
|
for (const u of run.undeclared) {
|
|
253
256
|
print(`FAIL: ${u} does not declare whether it blocks`)
|
|
254
257
|
print(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
|
|
255
258
|
}
|
|
256
|
-
for (const key of [...run.
|
|
259
|
+
for (const key of [...run.unknownOff.map((id) => `off.${id}`), ...run.unknownBudgets.map((id) => `budgets.${id}`)]) {
|
|
257
260
|
print(`FAIL: ${key} names no check of this repo`)
|
|
258
261
|
print(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
|
|
259
262
|
}
|
package/src/core/run-ci.ts
CHANGED
|
@@ -32,8 +32,11 @@ export type CiOpts = {
|
|
|
32
32
|
/** repair the mechanical slots before gating. default on locally, verify
|
|
33
33
|
* mode under CI. core/run-fixers.ts owns the scope. */
|
|
34
34
|
fix?: boolean
|
|
35
|
-
/** also apply the
|
|
35
|
+
/** also apply the repairs of the checks declaring `fix: 'ci'` during the
|
|
36
|
+
* fix pass */
|
|
36
37
|
fixRules?: boolean
|
|
38
|
+
/** the release preflight: the rows declaring `onRelease` run */
|
|
39
|
+
release?: boolean
|
|
37
40
|
/** self-arm the git hooks in the fix pass (interactive ci entry only) */
|
|
38
41
|
armHooks?: boolean
|
|
39
42
|
/** run only the checks that can block (core/run-checks onlyBlocking) */
|
|
@@ -203,6 +206,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
203
206
|
composed,
|
|
204
207
|
audience: 'gate',
|
|
205
208
|
includeProactive: o.failOnAdvisory === true,
|
|
209
|
+
release: o.release,
|
|
206
210
|
frozen: o.frozen,
|
|
207
211
|
onlyBlocking: o.onlyBlockingChecks,
|
|
208
212
|
only: o.onlyIds,
|
|
@@ -226,7 +230,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
226
230
|
})
|
|
227
231
|
const reg = ledger.layer.registration
|
|
228
232
|
for (const e of reg.errors) { registrationFailed = true; sink(`FAIL: ${e}`) }
|
|
229
|
-
if (registrationFailed) sink(' fix:
|
|
233
|
+
if (registrationFailed) sink(' fix: each check declares one cache that its body can honour: files, reads, ttl, fact or never (build/shipkit/runs.md)')
|
|
230
234
|
// both counts, always. Printing only the burn-down made `never:` the
|
|
231
235
|
// cheap way to shrink the visible number, and that field carries no
|
|
232
236
|
// verification at all: a growing decided population has to be as
|
|
@@ -262,7 +266,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
262
266
|
}
|
|
263
267
|
// a switch wired to no check does nothing, silently, which is the one thing
|
|
264
268
|
// a toggle must never do
|
|
265
|
-
for (const key of [...run.
|
|
269
|
+
for (const key of [...run.unknownOff.map((id) => `off.${id}`), ...run.unknownBudgets.map((id) => `budgets.${id}`)]) {
|
|
266
270
|
sink(`FAIL: ${key} names no check of this repo`)
|
|
267
271
|
sink(` fix: \`shipkit checks\` lists every id; drop the entry or correct it`)
|
|
268
272
|
}
|
|
@@ -280,10 +284,15 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
280
284
|
// stored under a key that describes the file as it was before the edit.
|
|
281
285
|
// the counts line folds into the verdict; what else it says (a store
|
|
282
286
|
// that failed, answers thrown away) is printed as it is
|
|
283
|
-
if (ledger != null)
|
|
287
|
+
if (ledger != null) {
|
|
288
|
+
for (const [i, l] of ledger.header(ledger.commit()).entries()) (i === 0 ? detail : sink)(formatLine(l, false))
|
|
289
|
+
// why every check that did not replay ran, grouped: a re-run nobody
|
|
290
|
+
// can explain costs a reader more than the line does
|
|
291
|
+
for (const { line, quiet } of ledger.explain()) (quiet ? detail : sink)(formatLine(line, false))
|
|
292
|
+
}
|
|
284
293
|
// one write for the whole gate, after everything has finished
|
|
285
294
|
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
286
|
-
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.
|
|
295
|
+
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownOff.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
287
296
|
// THE VERDICT, one line and only on a pass: a red run already said so in
|
|
288
297
|
// its FAIL lines. The whole gate adds the tier and the cache counts, which
|
|
289
298
|
// a narrowed run asked no question about
|