docs(ops): 补齐生产 runbook 和 readiness 清单

- 新增迁移回滚、AppData 退场、小宝后台任务 runbook\n- 新增生产 readiness 证据清单和 runbook placeholder 扫描\n- 更新部署文档与路线图到 V2.8 运维闭环阶段\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
This commit is contained in:
2026-07-08 16:28:02 +08:00
parent 7837a809ca
commit 72a59f125c
10 changed files with 420 additions and 15 deletions

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { extname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { option, parseArgs } from './ops-utils.mjs';
const forbiddenPatterns = [
{ name: 'TBD', pattern: /\bTBD\b/i },
{ name: 'TODO', pattern: /\bTODO\b/i },
{ name: 'fill-in', pattern: /fill in|fill-in/i },
{ name: 'angle-token', pattern: /<[^>\n]+>/ },
{ name: 'Chinese pending marker', pattern: /待定|占位/ },
];
function usage() {
return `Usage: node scripts/check-runbook-placeholders.mjs --paths <path> [path...]
Scans Markdown runbooks for unresolved placeholder markers.
`;
}
function collectMarkdownFiles(path) {
if (!existsSync(path)) {
throw new Error(`Path not found: ${path}`);
}
const stat = statSync(path);
if (stat.isFile()) return extname(path) === '.md' ? [path] : [];
if (!stat.isDirectory()) return [];
const files = [];
for (const entry of readdirSync(path)) {
files.push(...collectMarkdownFiles(join(path, entry)));
}
return files;
}
function requestedPaths(argv) {
const args = parseArgs(argv);
if (args.flags.has('help')) return { help: true, paths: [] };
const first = option(args, 'paths');
const paths = [first, ...args.positionals].filter(Boolean);
if (paths.length === 0) {
throw new Error('Missing --paths <path> [path...]');
}
return { help: false, paths };
}
export function scanFiles(paths) {
const files = paths.flatMap(collectMarkdownFiles);
const findings = [];
for (const file of files) {
const lines = readFileSync(file, 'utf8').split(/\r?\n/);
lines.forEach((line, index) => {
for (const forbidden of forbiddenPatterns) {
if (forbidden.pattern.test(line)) {
findings.push(`${file}:${index + 1} ${forbidden.name}: ${line.trim()}`);
}
}
});
}
return { files, findings };
}
export async function main(argv = process.argv.slice(2)) {
const { help, paths } = requestedPaths(argv);
if (help) {
process.stdout.write(usage());
return;
}
const { files, findings } = scanFiles(paths);
if (findings.length > 0) {
process.stderr.write(`${findings.join('\n')}\n`);
throw new Error(`Runbook placeholder scan failed: ${findings.length} finding(s)`);
}
process.stdout.write(`Runbook placeholder scan passed (${files.length} file(s)).\n`);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exit(1);
});
}