- 新增迁移回滚、AppData 退场、小宝后台任务 runbook\n- 新增生产 readiness 证据清单和 runbook placeholder 扫描\n- 更新部署文档与路线图到 V2.8 运维闭环阶段\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
88 lines
2.5 KiB
JavaScript
88 lines
2.5 KiB
JavaScript
#!/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);
|
|
});
|
|
}
|
|
|