rvlib-shipkit 1.4.0 → 1.5.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 +68 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/check.ts +68 -95
- package/src/checks/SK001-package-json-shape.check.ts +6 -2
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- 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/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- 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 +6 -5
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- 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 +4 -3
- 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_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +9 -4
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +37 -20
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/legacy-shapes.ts +39 -10
- 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/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +8 -4
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -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/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/hubs.ts +5 -5
- 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/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +113 -59
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +30 -55
- 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-view.tsx +13 -11
- 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-tables-view.tsx +13 -6
- 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 -8
- package/src/serve/hub-app/home-strip.tsx +111 -47
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +14 -4
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +65 -43
- 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 +5 -2
- 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 +38 -0
- package/src/serve/hub-app/projects-svc.ts +15 -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 +117 -0
- package/src/serve/hub-app/repo-st.ts +46 -9
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- 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 +3 -16
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- 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,52 @@
|
|
|
1
|
+
// THE WRITE HALF of the check file port (core/port-check-file.ts), in its own
|
|
2
|
+
// process: whether an undeclared `run` body can be keyed on its reads is a walk
|
|
3
|
+
// of its import closure on disk (core/raw-access.ts), which no check context
|
|
4
|
+
// read can stand for, so SK029's repair reaches it through `c.exec`.
|
|
5
|
+
//
|
|
6
|
+
// bun port-check-cli.ts <root> <rel> [<rel> …] → one json line on stdout
|
|
7
|
+
import { readFileSync } from 'fs'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { atomicWrite } from './locked-write.ts'
|
|
10
|
+
import { LIB_ROOT } from './lib-root.ts'
|
|
11
|
+
import { rawAccessInClosure } from './raw-access.ts'
|
|
12
|
+
import { findCheckObject, portCheckSource, retiredKeysIn } from './port-check-file.ts'
|
|
13
|
+
import { plainDefaultFor } from './port-check-files.ts'
|
|
14
|
+
|
|
15
|
+
export type PortRun = { did: string[]; hand: string[] }
|
|
16
|
+
|
|
17
|
+
/** why an undeclared `run` body cannot be keyed on its reads, or null */
|
|
18
|
+
function rawAccessOf(root: string, rel: string, src: string): string | null {
|
|
19
|
+
const obj = findCheckObject(src, { plainDefault: plainDefaultFor(rel) })
|
|
20
|
+
const keys = new Set(obj?.props.map((p) => p.key) ?? [])
|
|
21
|
+
if (!keys.has('run') || keys.has('inputs') || keys.has('ttl') || keys.has('never')) return null
|
|
22
|
+
const r = rawAccessInClosure(root, join(root, rel), { skip: LIB_ROOT })
|
|
23
|
+
return r == null ? null : `its imports reach the disk around c (${r.file}:${r.line}, ${r.what})`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function portFiles(root: string, rels: string[]): PortRun {
|
|
27
|
+
const did: string[] = []
|
|
28
|
+
const hand: string[] = []
|
|
29
|
+
for (const rel of rels) {
|
|
30
|
+
const abs = join(root, rel)
|
|
31
|
+
let src: string
|
|
32
|
+
try { src = readFileSync(abs, 'utf8') } catch (e) { hand.push(`${rel}: unreadable (${(e as Error).message})`); continue }
|
|
33
|
+
const plainDefault = plainDefaultFor(rel)
|
|
34
|
+
const keys = retiredKeysIn(src, { plainDefault })
|
|
35
|
+
if (keys.length === 0) continue
|
|
36
|
+
const r = portCheckSource(src, { plainDefault, rawAccess: rawAccessOf(root, rel, src) })
|
|
37
|
+
if ('error' in r) { hand.push(`${rel}: ${r.error}`); continue }
|
|
38
|
+
if (!r.changed) continue
|
|
39
|
+
atomicWrite(abs, r.source)
|
|
40
|
+
did.push(`${rel} declares runs, cache, fix and applies (was: ${keys.join(', ')})${r.notes.length > 0 ? `. Note: ${r.notes.join('; ')}` : ''}`)
|
|
41
|
+
}
|
|
42
|
+
return { did, hand }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (import.meta.main) {
|
|
46
|
+
const [root, ...rels] = process.argv.slice(2)
|
|
47
|
+
if (root == null) {
|
|
48
|
+
console.error('usage: bun port-check-cli.ts <root> <rel> [<rel> …]')
|
|
49
|
+
process.exit(2)
|
|
50
|
+
}
|
|
51
|
+
console.log(JSON.stringify(portFiles(root, rels)))
|
|
52
|
+
}
|
|
@@ -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/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
|
}
|
|
@@ -283,7 +287,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
283
287
|
if (ledger != null) for (const [i, l] of ledger.header(ledger.commit()).entries()) (i === 0 ? detail : sink)(formatLine(l, false))
|
|
284
288
|
// one write for the whole gate, after everything has finished
|
|
285
289
|
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
286
|
-
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.
|
|
290
|
+
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownOff.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
287
291
|
// THE VERDICT, one line and only on a pass: a red run already said so in
|
|
288
292
|
// its FAIL lines. The whole gate adds the tier and the cache counts, which
|
|
289
293
|
// a narrowed run asked no question about
|
package/src/core/run-fix.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// `shipkit check --fix [<id> …]` — apply check
|
|
2
|
-
// show the real end state. Never
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// `shipkit check --fix [<id> …]` — apply check repairs, then re-run and
|
|
2
|
+
// show the real end state. Never caches (a repair must see reality). No ids =
|
|
3
|
+
// every check declaring `fix: 'ci'`, which is also what the gate's fix pass
|
|
4
|
+
// applies; a `fix: 'manual'` repair runs only when its id is named. A
|
|
5
|
+
// requested id that is unknown or has no repair fails loud instead of
|
|
6
|
+
// silently narrowing.
|
|
6
7
|
import { CheckCtx } from './check-ctx.ts'
|
|
7
8
|
import { formatLine } from './wire.ts'
|
|
8
|
-
import {
|
|
9
|
+
import type { FixWhen } from './runs.ts'
|
|
10
|
+
import { gateInventory } from '../ledger/gate-checks.ts'
|
|
9
11
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
10
12
|
import { discoverChecks } from './discover-checks.ts'
|
|
11
13
|
import { runCheckBody, type CheckDef, type CheckEnv } from '../check.ts'
|
|
@@ -34,19 +36,25 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
34
36
|
const ctx = (c: RepoConfig): CheckCtx => CheckCtx.open({ root, config: c })
|
|
35
37
|
const color = process.stdout.isTTY && process.env.NO_COLOR == null
|
|
36
38
|
/** one fixable check, whatever declared it */
|
|
37
|
-
type Target = { id: string; title: string; owner: string; run: CheckDef['run']; fix?: CheckDef['
|
|
39
|
+
type Target = { id: string; title: string; owner: string; run: CheckDef['run']; fix?: CheckDef['repair']; when: FixWhen; off: string | null }
|
|
40
|
+
// what each row resolved to: its declared `fix` and whether it is off
|
|
41
|
+
const resolved = new Map((await gateInventory(root, config)).map((r) => [r.decl.id, r.runs]))
|
|
38
42
|
// a rule the repo still registers fixes exactly like a check file, one
|
|
39
|
-
// code path
|
|
43
|
+
// code path, with the fix word its derived row carries
|
|
40
44
|
const all: Target[] = [
|
|
41
|
-
...(await discoverChecks(root)).map((u): Target => ({ id: u.id, title: u.title, owner: u.owner, run: u.def.run, fix: u.def.
|
|
42
|
-
...(config.rules ?? []).map((r): Target =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
...(await discoverChecks(root)).map((u): Target => ({ id: u.id, title: u.title, owner: u.owner, run: u.def.run, fix: u.def.repair, when: resolved.get(u.id)?.fix ?? u.def.fix, off: resolved.get(u.id)?.off ?? null })),
|
|
46
|
+
...(config.rules ?? []).map((r): Target => {
|
|
47
|
+
const runs = resolved.get(r.id)
|
|
48
|
+
return {
|
|
49
|
+
id: r.id, title: r.title, owner: r.owner, when: runs?.fix ?? 'none', off: runs?.off ?? null,
|
|
50
|
+
run: (c) => r.lint(c, { root: c.root, config: c.config }),
|
|
51
|
+
fix: r.fix == null ? undefined : (c) => r.fix!({ root: c.root, config: c.config }),
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
47
54
|
]
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
// no ids: the repairs declared for the gate. A manual one runs by name
|
|
57
|
+
let targets = all.filter((r) => r.fix != null && r.when === 'ci')
|
|
50
58
|
if (o.ids != null && o.ids.length > 0) {
|
|
51
59
|
for (const id of o.ids) {
|
|
52
60
|
const r = all.find((x) => x.id === id)
|
|
@@ -54,17 +62,16 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
54
62
|
if (r.fix == null) { print(`FAIL: [${id}] has no autofix — fix by hand: ${config.docsBase ?? ''}${r.owner}`); return 1 }
|
|
55
63
|
}
|
|
56
64
|
const wanted = new Set(o.ids)
|
|
57
|
-
targets =
|
|
65
|
+
targets = all.filter((r) => r.fix != null && wanted.has(r.id))
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
let exit = 0
|
|
61
69
|
for (const rule of targets) {
|
|
62
70
|
const before = ctx(env.config)
|
|
63
|
-
|
|
64
|
-
await runCheckBody(rule, before, off.at === 'off' ? { reason: off.reason } : undefined)
|
|
71
|
+
await runCheckBody(rule, before, rule.off != null ? { reason: rule.off } : undefined)
|
|
65
72
|
// a check that is OFF never autofixes: the repo broke that invariant on purpose
|
|
66
|
-
if (off
|
|
67
|
-
if (o.quiet !== true) print(`skip: [${rule.id}] ${rule.id} exempt: ${off
|
|
73
|
+
if (rule.off != null) {
|
|
74
|
+
if (o.quiet !== true) print(`skip: [${rule.id}] ${rule.id} exempt: ${rule.off}`)
|
|
68
75
|
continue
|
|
69
76
|
}
|
|
70
77
|
if (!before.failed && !before.warned) {
|
|
@@ -97,8 +104,8 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
97
104
|
for (const l of after.lines) (o.quiet === true ? o.detail ?? print : say)(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix \S+$/, '') }, color))
|
|
98
105
|
if (after.failed) exit = 1
|
|
99
106
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
print(`${
|
|
107
|
+
const manual = all.filter((r) => r.fix != null && r.when === 'manual').map((r) => r.id)
|
|
108
|
+
if (manual.length > 0 && o.quiet !== true && (o.ids == null || o.ids.length === 0))
|
|
109
|
+
print(`${manual.length} repair(s) run only by name: shipkit check --fix ${manual.join(' ')}`)
|
|
103
110
|
return exit
|
|
104
111
|
}
|