- 新增迁移回滚、AppData 退场、小宝后台任务 runbook\n- 新增生产 readiness 证据清单和 runbook placeholder 扫描\n- 更新部署文档与路线图到 V2.8 运维闭环阶段\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
271 lines
6.8 KiB
JavaScript
271 lines
6.8 KiB
JavaScript
import { existsSync, readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = dirname(fileURLToPath(new URL('../package.json', import.meta.url)));
|
|
|
|
const requiredRuntimeDependencies = [
|
|
{
|
|
packageFile: 'apps/server/package.json',
|
|
dependencies: ['express'],
|
|
},
|
|
];
|
|
|
|
const checks = [
|
|
{
|
|
file: 'Dockerfile.web',
|
|
snippets: [
|
|
'FROM node:20-alpine',
|
|
'pnpm --filter @ftb/shared build',
|
|
'pnpm --filter web build',
|
|
'NEXT_PUBLIC_API_URL',
|
|
'NEXT_PUBLIC_APP_VERSION',
|
|
'ARG APP_VERSION=unknown',
|
|
'COPY scripts ./scripts',
|
|
'CMD ["pnpm", "--filter", "web", "start"]',
|
|
],
|
|
},
|
|
{
|
|
file: 'Dockerfile.server',
|
|
snippets: [
|
|
'FROM node:20-alpine',
|
|
'pnpm --filter @ftb/shared build',
|
|
'pnpm --filter server build',
|
|
'prisma generate',
|
|
'APP_VERSION',
|
|
'ARG APP_VERSION=unknown',
|
|
'CMD ["pnpm", "--filter", "server", "start:prod"]',
|
|
],
|
|
},
|
|
{
|
|
file: 'docker-compose.prod.yml',
|
|
snippets: [
|
|
'web:',
|
|
'server:',
|
|
'postgres:',
|
|
'redis:',
|
|
'nginx:',
|
|
'server_data:',
|
|
'NEXT_PUBLIC_API_URL',
|
|
'SERVER_IMAGE',
|
|
'WEB_IMAGE',
|
|
'APP_VERSION',
|
|
'/api/v1/health/version',
|
|
'prometheus:',
|
|
"profiles: ['monitoring']",
|
|
'postgres-exporter:',
|
|
'blackbox-exporter:',
|
|
'prometheus_data:',
|
|
],
|
|
},
|
|
{
|
|
file: '.github/workflows/deploy-production.yml',
|
|
snippets: [
|
|
'docker/build-push-action',
|
|
'appleboy/ssh-action',
|
|
'docker compose --env-file .env.production -f docker-compose.prod.yml pull',
|
|
'pnpm --filter server db:deploy',
|
|
'scripts/smoke-test-release.mjs',
|
|
'--expected-version',
|
|
],
|
|
},
|
|
{
|
|
file: 'scripts/check-runtime-version.mjs',
|
|
snippets: ['Runtime version verified', '/api/v1/health/version', 'expectedVersion'],
|
|
},
|
|
{
|
|
file: 'scripts/smoke-test-release.mjs',
|
|
snippets: [
|
|
'Release smoke test passed',
|
|
'/api/v1/v2.2/requirements?productId=__smoke__',
|
|
'/api/v1/config/ai',
|
|
],
|
|
},
|
|
{
|
|
file: 'scripts/backup-postgres.mjs',
|
|
snippets: ['pg_dump', '--format=custom', '--dry-run'],
|
|
},
|
|
{
|
|
file: 'scripts/restore-postgres.mjs',
|
|
snippets: ['--confirm-overwrite', 'dropdb', 'pg_restore'],
|
|
},
|
|
{
|
|
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: [
|
|
'web:',
|
|
'server:',
|
|
'postgres:',
|
|
'redis:',
|
|
'nginx:',
|
|
'local_server_data:',
|
|
'NEXT_PUBLIC_API_URL',
|
|
'${LOCAL_HTTP_PORT:-8080}:80',
|
|
],
|
|
},
|
|
{
|
|
file: 'deploy/nginx/default.conf.template',
|
|
snippets: [
|
|
'proxy_pass http://web:3000',
|
|
'proxy_pass http://server:3001',
|
|
'client_max_body_size',
|
|
'X-Forwarded-Proto',
|
|
],
|
|
},
|
|
{
|
|
file: 'deploy/monitoring/README.md',
|
|
snippets: [
|
|
'--profile monitoring',
|
|
'FtbPostgresDown',
|
|
'FtbXiaobaoSummaryStale',
|
|
'no webhook URLs',
|
|
],
|
|
},
|
|
{
|
|
file: 'deploy/monitoring/prometheus/prometheus.yml',
|
|
snippets: ['postgres-exporter:9187', 'promtail:9080', 'blackbox-http'],
|
|
},
|
|
{
|
|
file: 'deploy/monitoring/prometheus/alert-rules.yml',
|
|
snippets: [
|
|
'FtbPostgresDown',
|
|
'FtbSlowApiLogBurst',
|
|
'FtbSlowPrismaLogBurst',
|
|
'FtbJobFailureLogBurst',
|
|
'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'],
|
|
},
|
|
{
|
|
file: 'deploy/monitoring/promtail/config.yml',
|
|
snippets: ['Slow API request', 'Slow Prisma query', 'AppData relation sync failed'],
|
|
},
|
|
{
|
|
file: 'deploy/monitoring/grafana/dashboards/ftb-production-overview.json',
|
|
snippets: ['FTB Production Overview', 'ftb_xiaobao_stale_summary_count', 'Server Warning/Error Logs'],
|
|
},
|
|
{
|
|
file: '.env.production.example',
|
|
snippets: [
|
|
'POSTGRES_PASSWORD=',
|
|
'DATABASE_URL=postgresql://',
|
|
'NEXT_PUBLIC_API_URL=',
|
|
'APP_VERSION=',
|
|
'WEB_IMAGE=',
|
|
'SERVER_IMAGE=',
|
|
'NEXTAUTH_SECRET=',
|
|
'ANTHROPIC_API_KEY=',
|
|
],
|
|
},
|
|
{
|
|
file: '.env.local-server.example',
|
|
snippets: [
|
|
'LOCAL_HTTP_PORT=8080',
|
|
'LOCAL_ACCESS_HOST=localhost',
|
|
'DATABASE_URL=postgresql://',
|
|
'NEXT_PUBLIC_API_URL=/api/v1',
|
|
'NEXTAUTH_URL=http://localhost:8080',
|
|
],
|
|
},
|
|
{
|
|
file: 'docs/deployment.md',
|
|
snippets: [
|
|
'docker-compose.prod.yml',
|
|
'docker-compose.local.yml',
|
|
'本地服务器部署',
|
|
'pnpm deploy:verify',
|
|
'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',
|
|
'Nginx',
|
|
],
|
|
},
|
|
];
|
|
|
|
let failed = false;
|
|
|
|
for (const check of checks) {
|
|
const abs = join(root, check.file);
|
|
if (!existsSync(abs)) {
|
|
console.error(`Missing deployment artifact: ${check.file}`);
|
|
failed = true;
|
|
continue;
|
|
}
|
|
|
|
const content = readFileSync(abs, 'utf8');
|
|
for (const snippet of check.snippets) {
|
|
if (!content.includes(snippet)) {
|
|
console.error(`Missing snippet in ${check.file}: ${snippet}`);
|
|
failed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const check of requiredRuntimeDependencies) {
|
|
const abs = join(root, check.packageFile);
|
|
if (!existsSync(abs)) {
|
|
console.error(`Missing package file: ${check.packageFile}`);
|
|
failed = true;
|
|
continue;
|
|
}
|
|
|
|
const pkg = JSON.parse(readFileSync(abs, 'utf8'));
|
|
const dependencies = pkg.dependencies ?? {};
|
|
for (const dependency of check.dependencies) {
|
|
if (!dependencies[dependency]) {
|
|
console.error(`Missing runtime dependency in ${check.packageFile}: ${dependency}`);
|
|
failed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('Production deployment artifacts verified.');
|