Files
ftb-project-management/scripts/verify-production-deploy.mjs
eef09d5af4 feat(ops): 添加发布 smoke test
- 新增只读发布 smoke runner 和 dry-run 测试\n- 将生产部署 workflow 从单点版本检查升级为 smoke test\n- 补充 Docker web runtime 脚本复制、package script 和部署文档\n\nCo-Authored-By: GPT-5 Codex <codex@openai.com>
2026-07-08 16:14:48 +08:00

195 lines
4.6 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',
],
},
{
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: '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: '.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',
'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.');