feat(deploy): 接入全自动生产发布校验

This commit is contained in:
Script Generator
2026-07-06 14:45:44 +08:00
parent 8d61bb7c13
commit dc780e4c7d
24 changed files with 662 additions and 3 deletions

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env node
const DEFAULT_URL = 'http://127.0.0.1/api/v1/health/version';
const url = process.argv[2] || process.env.RUNTIME_VERSION_URL || DEFAULT_URL;
const expectedVersion = process.argv[3] || process.env.APP_VERSION || process.env.GITHUB_SHA || '';
const attempts = Number.parseInt(process.env.RUNTIME_VERSION_ATTEMPTS || '30', 10);
const intervalMs = Number.parseInt(process.env.RUNTIME_VERSION_INTERVAL_MS || '2000', 10);
const timeoutMs = Number.parseInt(process.env.RUNTIME_VERSION_TIMEOUT_MS || '5000', 10);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isVersionPayload(value) {
return (
value &&
typeof value === 'object' &&
value.service === 'server' &&
typeof value.version === 'string'
);
}
async function fetchVersion() {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
if (!isVersionPayload(payload)) {
throw new Error(`Unexpected payload: ${JSON.stringify(payload)}`);
}
return payload;
} finally {
clearTimeout(timeout);
}
}
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const payload = await fetchVersion();
if (expectedVersion && payload.version !== expectedVersion) {
throw new Error(`Expected ${expectedVersion}, got ${payload.version}`);
}
console.log(`Runtime version verified: ${payload.version}`);
process.exit(0);
} catch (error) {
lastError = error;
if (attempt < attempts) {
await sleep(intervalMs);
}
}
}
console.error(`Runtime version check failed for ${url}: ${lastError?.message || 'unknown error'}`);
process.exit(1);

View File

@@ -12,6 +12,8 @@ const checks = [
'pnpm --filter @ftb/shared build',
'pnpm --filter web build',
'NEXT_PUBLIC_API_URL',
'NEXT_PUBLIC_APP_VERSION',
'ARG APP_VERSION=unknown',
'CMD ["pnpm", "--filter", "web", "start"]',
],
},
@@ -22,6 +24,8 @@ const checks = [
'pnpm --filter @ftb/shared build',
'pnpm --filter server build',
'prisma generate',
'APP_VERSION',
'ARG APP_VERSION=unknown',
'CMD ["pnpm", "--filter", "server", "start:prod"]',
],
},
@@ -35,9 +39,26 @@ const checks = [
'nginx:',
'server_data:',
'NEXT_PUBLIC_API_URL',
'/api/v1/config/ai',
'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',
'/api/v1/health/version',
],
},
{
file: 'scripts/check-runtime-version.mjs',
snippets: ['Runtime version verified', '/api/v1/health/version', 'expectedVersion'],
},
{
file: 'docker-compose.local.yml',
snippets: [
@@ -66,6 +87,9 @@ const checks = [
'POSTGRES_PASSWORD=',
'DATABASE_URL=postgresql://',
'NEXT_PUBLIC_API_URL=',
'APP_VERSION=',
'WEB_IMAGE=',
'SERVER_IMAGE=',
'NEXTAUTH_SECRET=',
'ANTHROPIC_API_KEY=',
],