dsh-dbhub-live 5.2.0-dev.3 → 5.2.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/AGENTS.md +116 -116
- package/README.en.md +22 -22
- package/README.md +23 -23
- package/doc/ARCHITECTURE.md +108 -108
- package/doc/CLIENT.md +124 -124
- package/doc/DESENSITIZE.md +56 -56
- package/doc/DEVELOPMENT.md +139 -139
- package/doc/IMPACT-MATRIX.md +36 -36
- package/doc/RELEASE.md +93 -93
- package/lib/meta.mjs +267 -267
- package/lib/policy.mjs +92 -92
- package/lib/update.mjs +244 -244
- package/package.json +1 -1
- package/scripts/desensitize-check.mjs +331 -331
- package/scripts/docs-check.mjs +214 -214
- package/scripts/release-preflight.mjs +180 -180
- package/test/meta.test.mjs +234 -234
- package/test/policy.test.mjs +132 -132
- package/test/update.test.mjs +270 -270
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Release preflight gate.
|
|
3
|
-
//
|
|
4
|
-
// Answers one question, with evidence rather than memory: may this exact commit
|
|
5
|
-
// and version ship? It refuses unless a release test was run recently, against
|
|
6
|
-
// this commit, with this matrix, with no claim drift, and an AI reviewer ruled
|
|
7
|
-
// on it. Designed to be the step you cannot forget: it is wired into the release
|
|
8
|
-
// commands and into a pre-push hook.
|
|
9
|
-
//
|
|
10
|
-
// node scripts/release-preflight.mjs # the newest run
|
|
11
|
-
// node scripts/release-preflight.mjs --run <dir> # a specific run
|
|
12
|
-
// node scripts/release-preflight.mjs --max-age-hours 12 --json
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { REPO_ROOT, readJson } from '../release-test/lib/util.mjs'
|
|
15
|
-
import { run } from '../release-test/lib/proc.mjs'
|
|
16
|
-
import { packageMeta } from '../release-test/lib/artifact.mjs'
|
|
17
|
-
import { gitInfoAsync, dirtyFilesAsync } from '../release-test/lib/git.mjs'
|
|
18
|
-
import { loadMatrix, matrixClaimWarnings, matrixHash } from '../release-test/lib/matrix.mjs'
|
|
19
|
-
import { latestRun, runPath } from '../release-test/lib/runs.mjs'
|
|
20
|
-
import { checkRun } from '../release-test/lib/verdict.mjs'
|
|
21
|
-
import fs from 'node:fs'
|
|
22
|
-
|
|
23
|
-
const argv = process.argv.slice(2)
|
|
24
|
-
const arg = (name, fallback = null) => {
|
|
25
|
-
const index = argv.indexOf(`--${name}`)
|
|
26
|
-
return index === -1 ? fallback : argv[index + 1] ?? true
|
|
27
|
-
}
|
|
28
|
-
const has = (name) => argv.includes(`--${name}`)
|
|
29
|
-
|
|
30
|
-
const MAX_AGE_HOURS = Number(arg('max-age-hours', 24))
|
|
31
|
-
const JSON_OUT = has('json')
|
|
32
|
-
const c = {
|
|
33
|
-
reset: '\u001b[0m', dim: '\u001b[2m', bold: '\u001b[1m', red: '\u001b[31m', green: '\u001b[32m', yellow: '\u001b[33m', cyan: '\u001b[36m',
|
|
34
|
-
}
|
|
35
|
-
const say = (text) => process.stdout.write(`${text}\n`)
|
|
36
|
-
const ok = (text) => say(` ${c.green}✓${c.reset} ${text}`)
|
|
37
|
-
const bad = (text) => say(` ${c.red}✗${c.reset} ${text}`)
|
|
38
|
-
const warnline = (text) => say(` ${c.yellow}!${c.reset} ${text}`)
|
|
39
|
-
|
|
40
|
-
const problems = []
|
|
41
|
-
const warnings = []
|
|
42
|
-
const facts = {}
|
|
43
|
-
|
|
44
|
-
async function main() {
|
|
45
|
-
const pkg = packageMeta()
|
|
46
|
-
const git = await gitInfoAsync()
|
|
47
|
-
const matrix = loadMatrix()
|
|
48
|
-
const currentMatrixHash = matrixHash(matrix)
|
|
49
|
-
const claims = matrixClaimWarnings(matrix)
|
|
50
|
-
|
|
51
|
-
facts.plugin = `${pkg.name}@${pkg.version}`
|
|
52
|
-
facts.commit = git.commit
|
|
53
|
-
facts.branch = git.branch
|
|
54
|
-
facts.dirty = git.dirty
|
|
55
|
-
facts.matrixHash = currentMatrixHash
|
|
56
|
-
|
|
57
|
-
// 1. A run must exist at all.
|
|
58
|
-
const explicit = arg('run')
|
|
59
|
-
const resolved = explicit ? runPath(String(explicit)) : latestRun()?.dir ?? null
|
|
60
|
-
if (!resolved) {
|
|
61
|
-
problems.push('没有找到任何发布测试报告 —— 先跑 `npm run test:release`')
|
|
62
|
-
return finish()
|
|
63
|
-
}
|
|
64
|
-
const reportDir = path.resolve(resolved)
|
|
65
|
-
const report = readJson(path.join(reportDir, 'report.json'))
|
|
66
|
-
facts.run = report.runId
|
|
67
|
-
facts.reportStatus = report.status
|
|
68
|
-
|
|
69
|
-
// 2. The report must be about THIS version, built from THIS commit, recently.
|
|
70
|
-
if (report.plugin?.version !== pkg.version) {
|
|
71
|
-
problems.push(`报告测的是 ${report.plugin?.version},当前 package.json 是 ${pkg.version} —— 版本变了,必须重跑`)
|
|
72
|
-
}
|
|
73
|
-
// Compare TREE hashes: that is exactly the set of files the artifact was packed
|
|
74
|
-
// from. A squash/merge that lands the same content is fine; any real change is not.
|
|
75
|
-
// Generated documents are rewritten from the evidence of the run itself, so a
|
|
76
|
-
// report may legitimately be followed by a docs-only commit. Any other change
|
|
77
|
-
// means the artifact would differ and the report is stale.
|
|
78
|
-
// Generated docs (rewritten from this run's evidence) and test tooling (not
|
|
79
|
-
// shipped in the plugin's runtime path) cannot change what users execute.
|
|
80
|
-
const GENERATED = new Set(['doc/SUPPORT-MATRIX.md', 'README.md', 'README.en.md', 'release-test/mise.toml'])
|
|
81
|
-
const TOOLING_PREFIXES = ['release-test/', '.github/']
|
|
82
|
-
const TOOLING_FILES = new Set(['scripts/release-preflight.mjs'])
|
|
83
|
-
const isAllowedDiff = (file) => GENERATED.has(file) || TOOLING_FILES.has(file) || TOOLING_PREFIXES.some((prefix) => file.startsWith(prefix))
|
|
84
|
-
const sameTree = report.git?.tree && git.tree && report.git.tree === git.tree
|
|
85
|
-
if (!sameTree) {
|
|
86
|
-
const changed = report.git?.tree && git.tree ? await changedPaths(report.git.tree, git.tree) : null
|
|
87
|
-
if (changed && changed.length && changed.every(isAllowedDiff)) {
|
|
88
|
-
warnings.push(`报告之后只有生成文档变化(${changed.join(', ')})—— 视为同一产物,有效`)
|
|
89
|
-
} else if (report.git?.commit !== git.commit) {
|
|
90
|
-
problems.push(`报告对应的提交是 ${String(report.git?.commit).slice(0, 8)}(tree ${String(report.git?.tree).slice(0, 8)}),当前 HEAD 是 ${String(git.commit).slice(0, 8)}(tree ${String(git.tree).slice(0, 8)})—— 内容变了,必须重跑`)
|
|
91
|
-
} else {
|
|
92
|
-
problems.push('无法确认报告与当前工作树一致(缺少 tree 记录)—— 用当前版本的 harness 重跑')
|
|
93
|
-
}
|
|
94
|
-
} else if (report.git?.commit !== git.commit) {
|
|
95
|
-
warnings.push(`tree 相同但提交不同(报告 ${String(report.git?.commit).slice(0, 8)} → HEAD ${String(git.commit).slice(0, 8)}):内容一致,视为有效`)
|
|
96
|
-
}
|
|
97
|
-
if (report.git?.dirty) {
|
|
98
|
-
problems.push('那次测试是在脏工作区跑的:产物可能不包含已提交的代码 —— 先提交,再在干净工作区重跑')
|
|
99
|
-
}
|
|
100
|
-
const ageHours = (Date.now() - Date.parse(report.generatedAt)) / 3_600_000
|
|
101
|
-
facts.reportAgeHours = Math.round(ageHours * 10) / 10
|
|
102
|
-
if (!(ageHours <= MAX_AGE_HOURS)) {
|
|
103
|
-
problems.push(`报告已过期:${Math.round(ageHours)} 小时前(上限 ${MAX_AGE_HOURS})—— 重跑 \`npm run test:release\``)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 3. The support matrix must not have changed since the run.
|
|
107
|
-
if (report.matrix?.hash !== currentMatrixHash) {
|
|
108
|
-
problems.push('matrix.json 在测试之后被改过 —— 矩阵变了旧报告不再有效,重跑')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 4. Claim drift blocks the release even if tests are green.
|
|
112
|
-
if (claims.length) {
|
|
113
|
-
for (const claim of claims) problems.push(`声明偏差:${claim.detail}`)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 5. No failing declared target, and every declared target was actually run.
|
|
117
|
-
const declared = matrix.targets.filter((t) => t.tier === 'declared').map((t) => t.version)
|
|
118
|
-
const tested = new Map((report.targets ?? []).map((t) => [t.version, t]))
|
|
119
|
-
for (const version of declared) {
|
|
120
|
-
const target = tested.get(version)
|
|
121
|
-
if (!target) problems.push(`声明支持的 ${version} 未出现在这次测试里(用了 --only/--tier 的局部运行不能作为发版依据)`)
|
|
122
|
-
else if (target.status === 'fail' || target.status === 'error') problems.push(`${version} 未通过:${target.status}${target.error ? ` — ${String(target.error).split('\n')[0]}` : ''}`)
|
|
123
|
-
else if (target.status === 'warn') warnings.push(`${version} 通过但有告警(见 AI 判定)`)
|
|
124
|
-
}
|
|
125
|
-
if (report.status === 'red') problems.push('报告总体为 red')
|
|
126
|
-
|
|
127
|
-
// 6. The AI reviewer must have ruled on this exact report.
|
|
128
|
-
const checked = checkRun(reportDir)
|
|
129
|
-
facts.verdictOverall = checked.verdict?.overall ?? null
|
|
130
|
-
if (!checked.ok) {
|
|
131
|
-
for (const problem of checked.problems) problems.push(`AI 判定无效:${problem}`)
|
|
132
|
-
problems.push('生成判定骨架并让 AI 复核:`node release-test/run.mjs verdict --template`')
|
|
133
|
-
} else {
|
|
134
|
-
for (const warning of checked.warnings) warnings.push(warning)
|
|
135
|
-
if (checked.verdict.overall === 'fail') problems.push('AI 判定为 fail —— 禁止发版')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 7. Dirty tree now: the artifact the users get would include uncommitted code.
|
|
139
|
-
if (git.dirty) {
|
|
140
|
-
const files = await dirtyFilesAsync(5)
|
|
141
|
-
problems.push(`当前工作区不干净(${files.length} 个改动,例如 ${files.slice(0, 3).map((f) => f.trim()).join(';')})—— 发版必须从干净提交出`)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return finish()
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function finish() {
|
|
148
|
-
const pass = problems.length === 0
|
|
149
|
-
if (JSON_OUT) {
|
|
150
|
-
process.stdout.write(JSON.stringify({ pass, facts, problems, warnings }, null, 2) + '\n')
|
|
151
|
-
} else {
|
|
152
|
-
say(`${c.bold}Release preflight${c.reset} — ${facts.plugin} @ ${String(facts.commit).slice(0, 8)}${facts.dirty ? ' (dirty)' : ''}`)
|
|
153
|
-
say(` run ${facts.run ?? '—'} · report ${facts.reportStatus ?? '—'} · age ${facts.reportAgeHours ?? '—'}h · verdict ${facts.verdictOverall ?? '—'}`)
|
|
154
|
-
if (pass) {
|
|
155
|
-
ok('可以发版:报告新鲜、矩阵一致、声明无偏差、AI 判定通过、工作区干净')
|
|
156
|
-
for (const warning of warnings) warnline(warning)
|
|
157
|
-
} else {
|
|
158
|
-
bad(`禁止发版(${problems.length} 项):`)
|
|
159
|
-
for (const problem of problems) say(` - ${problem}`)
|
|
160
|
-
for (const warning of warnings) warnline(warning)
|
|
161
|
-
say('')
|
|
162
|
-
say(`${c.dim}修完后重跑:npm run test:release && (AI 判定)node release-test/run.mjs verdict --check${c.reset}`)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
process.exitCode = pass ? 0 : 1
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
await main()
|
|
169
|
-
|
|
170
|
-
/** Files that differ between two tree hashes (null when git refuses). */
|
|
171
|
-
async function changedPaths(treeA, treeB) {
|
|
172
|
-
const logFile = path.join(REPO_ROOT, '.release-test', 'preflight-git.log')
|
|
173
|
-
const result = await run('git', ['diff', '--name-only', treeA, treeB], { cwd: REPO_ROOT, logFile, timeoutMs: 30_000 })
|
|
174
|
-
if (result.code !== 0) return null
|
|
175
|
-
// run() captures output into the log with framing lines; keep only path lines.
|
|
176
|
-
return fs.readFileSync(logFile, 'utf8')
|
|
177
|
-
.split('\n')
|
|
178
|
-
.map((line) => line.trim())
|
|
179
|
-
.filter((line) => line && !line.startsWith('=====') && !line.startsWith('[') && !line.startsWith('$'))
|
|
180
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Release preflight gate.
|
|
3
|
+
//
|
|
4
|
+
// Answers one question, with evidence rather than memory: may this exact commit
|
|
5
|
+
// and version ship? It refuses unless a release test was run recently, against
|
|
6
|
+
// this commit, with this matrix, with no claim drift, and an AI reviewer ruled
|
|
7
|
+
// on it. Designed to be the step you cannot forget: it is wired into the release
|
|
8
|
+
// commands and into a pre-push hook.
|
|
9
|
+
//
|
|
10
|
+
// node scripts/release-preflight.mjs # the newest run
|
|
11
|
+
// node scripts/release-preflight.mjs --run <dir> # a specific run
|
|
12
|
+
// node scripts/release-preflight.mjs --max-age-hours 12 --json
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { REPO_ROOT, readJson } from '../release-test/lib/util.mjs'
|
|
15
|
+
import { run } from '../release-test/lib/proc.mjs'
|
|
16
|
+
import { packageMeta } from '../release-test/lib/artifact.mjs'
|
|
17
|
+
import { gitInfoAsync, dirtyFilesAsync } from '../release-test/lib/git.mjs'
|
|
18
|
+
import { loadMatrix, matrixClaimWarnings, matrixHash } from '../release-test/lib/matrix.mjs'
|
|
19
|
+
import { latestRun, runPath } from '../release-test/lib/runs.mjs'
|
|
20
|
+
import { checkRun } from '../release-test/lib/verdict.mjs'
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
|
|
23
|
+
const argv = process.argv.slice(2)
|
|
24
|
+
const arg = (name, fallback = null) => {
|
|
25
|
+
const index = argv.indexOf(`--${name}`)
|
|
26
|
+
return index === -1 ? fallback : argv[index + 1] ?? true
|
|
27
|
+
}
|
|
28
|
+
const has = (name) => argv.includes(`--${name}`)
|
|
29
|
+
|
|
30
|
+
const MAX_AGE_HOURS = Number(arg('max-age-hours', 24))
|
|
31
|
+
const JSON_OUT = has('json')
|
|
32
|
+
const c = {
|
|
33
|
+
reset: '\u001b[0m', dim: '\u001b[2m', bold: '\u001b[1m', red: '\u001b[31m', green: '\u001b[32m', yellow: '\u001b[33m', cyan: '\u001b[36m',
|
|
34
|
+
}
|
|
35
|
+
const say = (text) => process.stdout.write(`${text}\n`)
|
|
36
|
+
const ok = (text) => say(` ${c.green}✓${c.reset} ${text}`)
|
|
37
|
+
const bad = (text) => say(` ${c.red}✗${c.reset} ${text}`)
|
|
38
|
+
const warnline = (text) => say(` ${c.yellow}!${c.reset} ${text}`)
|
|
39
|
+
|
|
40
|
+
const problems = []
|
|
41
|
+
const warnings = []
|
|
42
|
+
const facts = {}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const pkg = packageMeta()
|
|
46
|
+
const git = await gitInfoAsync()
|
|
47
|
+
const matrix = loadMatrix()
|
|
48
|
+
const currentMatrixHash = matrixHash(matrix)
|
|
49
|
+
const claims = matrixClaimWarnings(matrix)
|
|
50
|
+
|
|
51
|
+
facts.plugin = `${pkg.name}@${pkg.version}`
|
|
52
|
+
facts.commit = git.commit
|
|
53
|
+
facts.branch = git.branch
|
|
54
|
+
facts.dirty = git.dirty
|
|
55
|
+
facts.matrixHash = currentMatrixHash
|
|
56
|
+
|
|
57
|
+
// 1. A run must exist at all.
|
|
58
|
+
const explicit = arg('run')
|
|
59
|
+
const resolved = explicit ? runPath(String(explicit)) : latestRun()?.dir ?? null
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
problems.push('没有找到任何发布测试报告 —— 先跑 `npm run test:release`')
|
|
62
|
+
return finish()
|
|
63
|
+
}
|
|
64
|
+
const reportDir = path.resolve(resolved)
|
|
65
|
+
const report = readJson(path.join(reportDir, 'report.json'))
|
|
66
|
+
facts.run = report.runId
|
|
67
|
+
facts.reportStatus = report.status
|
|
68
|
+
|
|
69
|
+
// 2. The report must be about THIS version, built from THIS commit, recently.
|
|
70
|
+
if (report.plugin?.version !== pkg.version) {
|
|
71
|
+
problems.push(`报告测的是 ${report.plugin?.version},当前 package.json 是 ${pkg.version} —— 版本变了,必须重跑`)
|
|
72
|
+
}
|
|
73
|
+
// Compare TREE hashes: that is exactly the set of files the artifact was packed
|
|
74
|
+
// from. A squash/merge that lands the same content is fine; any real change is not.
|
|
75
|
+
// Generated documents are rewritten from the evidence of the run itself, so a
|
|
76
|
+
// report may legitimately be followed by a docs-only commit. Any other change
|
|
77
|
+
// means the artifact would differ and the report is stale.
|
|
78
|
+
// Generated docs (rewritten from this run's evidence) and test tooling (not
|
|
79
|
+
// shipped in the plugin's runtime path) cannot change what users execute.
|
|
80
|
+
const GENERATED = new Set(['doc/SUPPORT-MATRIX.md', 'README.md', 'README.en.md', 'release-test/mise.toml'])
|
|
81
|
+
const TOOLING_PREFIXES = ['release-test/', '.github/']
|
|
82
|
+
const TOOLING_FILES = new Set(['scripts/release-preflight.mjs'])
|
|
83
|
+
const isAllowedDiff = (file) => GENERATED.has(file) || TOOLING_FILES.has(file) || TOOLING_PREFIXES.some((prefix) => file.startsWith(prefix))
|
|
84
|
+
const sameTree = report.git?.tree && git.tree && report.git.tree === git.tree
|
|
85
|
+
if (!sameTree) {
|
|
86
|
+
const changed = report.git?.tree && git.tree ? await changedPaths(report.git.tree, git.tree) : null
|
|
87
|
+
if (changed && changed.length && changed.every(isAllowedDiff)) {
|
|
88
|
+
warnings.push(`报告之后只有生成文档变化(${changed.join(', ')})—— 视为同一产物,有效`)
|
|
89
|
+
} else if (report.git?.commit !== git.commit) {
|
|
90
|
+
problems.push(`报告对应的提交是 ${String(report.git?.commit).slice(0, 8)}(tree ${String(report.git?.tree).slice(0, 8)}),当前 HEAD 是 ${String(git.commit).slice(0, 8)}(tree ${String(git.tree).slice(0, 8)})—— 内容变了,必须重跑`)
|
|
91
|
+
} else {
|
|
92
|
+
problems.push('无法确认报告与当前工作树一致(缺少 tree 记录)—— 用当前版本的 harness 重跑')
|
|
93
|
+
}
|
|
94
|
+
} else if (report.git?.commit !== git.commit) {
|
|
95
|
+
warnings.push(`tree 相同但提交不同(报告 ${String(report.git?.commit).slice(0, 8)} → HEAD ${String(git.commit).slice(0, 8)}):内容一致,视为有效`)
|
|
96
|
+
}
|
|
97
|
+
if (report.git?.dirty) {
|
|
98
|
+
problems.push('那次测试是在脏工作区跑的:产物可能不包含已提交的代码 —— 先提交,再在干净工作区重跑')
|
|
99
|
+
}
|
|
100
|
+
const ageHours = (Date.now() - Date.parse(report.generatedAt)) / 3_600_000
|
|
101
|
+
facts.reportAgeHours = Math.round(ageHours * 10) / 10
|
|
102
|
+
if (!(ageHours <= MAX_AGE_HOURS)) {
|
|
103
|
+
problems.push(`报告已过期:${Math.round(ageHours)} 小时前(上限 ${MAX_AGE_HOURS})—— 重跑 \`npm run test:release\``)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. The support matrix must not have changed since the run.
|
|
107
|
+
if (report.matrix?.hash !== currentMatrixHash) {
|
|
108
|
+
problems.push('matrix.json 在测试之后被改过 —— 矩阵变了旧报告不再有效,重跑')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. Claim drift blocks the release even if tests are green.
|
|
112
|
+
if (claims.length) {
|
|
113
|
+
for (const claim of claims) problems.push(`声明偏差:${claim.detail}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 5. No failing declared target, and every declared target was actually run.
|
|
117
|
+
const declared = matrix.targets.filter((t) => t.tier === 'declared').map((t) => t.version)
|
|
118
|
+
const tested = new Map((report.targets ?? []).map((t) => [t.version, t]))
|
|
119
|
+
for (const version of declared) {
|
|
120
|
+
const target = tested.get(version)
|
|
121
|
+
if (!target) problems.push(`声明支持的 ${version} 未出现在这次测试里(用了 --only/--tier 的局部运行不能作为发版依据)`)
|
|
122
|
+
else if (target.status === 'fail' || target.status === 'error') problems.push(`${version} 未通过:${target.status}${target.error ? ` — ${String(target.error).split('\n')[0]}` : ''}`)
|
|
123
|
+
else if (target.status === 'warn') warnings.push(`${version} 通过但有告警(见 AI 判定)`)
|
|
124
|
+
}
|
|
125
|
+
if (report.status === 'red') problems.push('报告总体为 red')
|
|
126
|
+
|
|
127
|
+
// 6. The AI reviewer must have ruled on this exact report.
|
|
128
|
+
const checked = checkRun(reportDir)
|
|
129
|
+
facts.verdictOverall = checked.verdict?.overall ?? null
|
|
130
|
+
if (!checked.ok) {
|
|
131
|
+
for (const problem of checked.problems) problems.push(`AI 判定无效:${problem}`)
|
|
132
|
+
problems.push('生成判定骨架并让 AI 复核:`node release-test/run.mjs verdict --template`')
|
|
133
|
+
} else {
|
|
134
|
+
for (const warning of checked.warnings) warnings.push(warning)
|
|
135
|
+
if (checked.verdict.overall === 'fail') problems.push('AI 判定为 fail —— 禁止发版')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 7. Dirty tree now: the artifact the users get would include uncommitted code.
|
|
139
|
+
if (git.dirty) {
|
|
140
|
+
const files = await dirtyFilesAsync(5)
|
|
141
|
+
problems.push(`当前工作区不干净(${files.length} 个改动,例如 ${files.slice(0, 3).map((f) => f.trim()).join(';')})—— 发版必须从干净提交出`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return finish()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function finish() {
|
|
148
|
+
const pass = problems.length === 0
|
|
149
|
+
if (JSON_OUT) {
|
|
150
|
+
process.stdout.write(JSON.stringify({ pass, facts, problems, warnings }, null, 2) + '\n')
|
|
151
|
+
} else {
|
|
152
|
+
say(`${c.bold}Release preflight${c.reset} — ${facts.plugin} @ ${String(facts.commit).slice(0, 8)}${facts.dirty ? ' (dirty)' : ''}`)
|
|
153
|
+
say(` run ${facts.run ?? '—'} · report ${facts.reportStatus ?? '—'} · age ${facts.reportAgeHours ?? '—'}h · verdict ${facts.verdictOverall ?? '—'}`)
|
|
154
|
+
if (pass) {
|
|
155
|
+
ok('可以发版:报告新鲜、矩阵一致、声明无偏差、AI 判定通过、工作区干净')
|
|
156
|
+
for (const warning of warnings) warnline(warning)
|
|
157
|
+
} else {
|
|
158
|
+
bad(`禁止发版(${problems.length} 项):`)
|
|
159
|
+
for (const problem of problems) say(` - ${problem}`)
|
|
160
|
+
for (const warning of warnings) warnline(warning)
|
|
161
|
+
say('')
|
|
162
|
+
say(`${c.dim}修完后重跑:npm run test:release && (AI 判定)node release-test/run.mjs verdict --check${c.reset}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
process.exitCode = pass ? 0 : 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await main()
|
|
169
|
+
|
|
170
|
+
/** Files that differ between two tree hashes (null when git refuses). */
|
|
171
|
+
async function changedPaths(treeA, treeB) {
|
|
172
|
+
const logFile = path.join(REPO_ROOT, '.release-test', 'preflight-git.log')
|
|
173
|
+
const result = await run('git', ['diff', '--name-only', treeA, treeB], { cwd: REPO_ROOT, logFile, timeoutMs: 30_000 })
|
|
174
|
+
if (result.code !== 0) return null
|
|
175
|
+
// run() captures output into the log with framing lines; keep only path lines.
|
|
176
|
+
return fs.readFileSync(logFile, 'utf8')
|
|
177
|
+
.split('\n')
|
|
178
|
+
.map((line) => line.trim())
|
|
179
|
+
.filter((line) => line && !line.startsWith('=====') && !line.startsWith('[') && !line.startsWith('$'))
|
|
180
|
+
}
|