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:
87
scripts/check-runbook-placeholders.mjs
Normal file
87
scripts/check-runbook-placeholders.mjs
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,4 +138,15 @@ describe('production ops scripts', () => {
|
||||
assert.doesNotMatch(combined, /sk-ant-[A-Za-z0-9]/);
|
||||
assert.doesNotMatch(combined, /hooks\.slack\.com\/services\//);
|
||||
});
|
||||
|
||||
it('passes the runbook placeholder scan', () => {
|
||||
const result = runScript('scripts/check-runbook-placeholders.mjs', [
|
||||
'--paths',
|
||||
'docs/runbooks',
|
||||
'docs/production-readiness.md',
|
||||
]);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Runbook placeholder scan passed/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +93,14 @@ const checks = [
|
||||
file: 'scripts/backup-server-data.mjs',
|
||||
snippets: ['server_data', 'tar -czf', '--dry-run'],
|
||||
},
|
||||
{
|
||||
file: 'scripts/check-runbook-placeholders.mjs',
|
||||
snippets: ['Runbook placeholder scan passed', 'Runbook placeholder scan failed', '--paths'],
|
||||
},
|
||||
{
|
||||
file: 'package.json',
|
||||
snippets: ['docs:check-runbooks', 'docs/runbooks', 'docs/production-readiness.md'],
|
||||
},
|
||||
{
|
||||
file: 'docker-compose.local.yml',
|
||||
snippets: [
|
||||
@@ -138,6 +146,31 @@ const checks = [
|
||||
'FtbXiaobaoSummaryStale',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'docs/runbooks/migration-rollback.md',
|
||||
snippets: ['Fresh Database Restore', 'Data Risks', 'pnpm restore:postgres'],
|
||||
},
|
||||
{
|
||||
file: 'docs/runbooks/appdata-retirement.md',
|
||||
snippets: ['Disable AppData Writes', 'Remove Fallback', 'Data Risks'],
|
||||
},
|
||||
{
|
||||
file: 'docs/runbooks/xiaobao-background-jobs.md',
|
||||
snippets: ['FtbXiaobaoSummaryStale', 'Future Scheduler Rules', 'Data Risks'],
|
||||
},
|
||||
{
|
||||
file: 'docs/production-readiness.md',
|
||||
snippets: [
|
||||
'Backup and restore',
|
||||
'Smoke tests',
|
||||
'Monitoring',
|
||||
'Audit',
|
||||
'RBAC',
|
||||
'Consistency',
|
||||
'Performance',
|
||||
'Post-release',
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'deploy/monitoring/postgres/postgres-queries.yml',
|
||||
snippets: ['xiaobao_risk_summaries', 'dirty = true', 'stale_summary_count'],
|
||||
@@ -183,6 +216,8 @@ const checks = [
|
||||
'pnpm backup:postgres',
|
||||
'pnpm restore:postgres',
|
||||
'pnpm backup:server-data',
|
||||
'docs/runbooks/migration-rollback.md',
|
||||
'docs/production-readiness.md',
|
||||
'--profile monitoring',
|
||||
'pnpm db:migrate',
|
||||
'NEXT_PUBLIC_API_URL',
|
||||
|
||||
Reference in New Issue
Block a user