Files
Genarrative/scripts/check-preview-deployer.mjs
T
kdletters ea81d8ebb5
Project CI / Native shell tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Successful in 1m20s
修复预览数据库容器内存不足
将分支预览 SpacetimeDB 内存上限覆盖为 2 GiB

保留生产与压测 Compose 的原有资源基线

增加预览内存覆盖防回归检查和故障说明
2026-08-15 18:33:30 +08:00

220 lines
6.0 KiB
JavaScript

import { readFileSync } from 'node:fs';
const jenkinsfile = readFileSync(
'jenkins/Jenkinsfile.preview-deployer',
'utf8',
);
const deployer = readFileSync('scripts/jenkins-preview-deployer.sh', 'utf8');
const resultWriter = readFileSync(
'scripts/preview-deployment-status.mjs',
'utf8',
);
const compose = readFileSync(
'deploy/container/docker-compose.loadtest.yml',
'utf8',
);
const webConfig = readFileSync(
'apps/preview-deployer-web/vite.config.ts',
'utf8',
);
const systemd = readFileSync(
'deploy/systemd/genarrative-preview-deployer.service',
'utf8',
);
const server = readFileSync(
'server-rs/crates/preview-deployer-server/src/lib.rs',
'utf8',
);
const nginx = readFileSync(
'deploy/nginx/genarrative-preview-deployer-lan.conf',
'utf8',
);
const jobConfig = readFileSync(
'jenkins/preview-deployer-job-config.xml',
'utf8',
);
const failures = [];
assertIncludes(
jenkinsfile,
"choice(name: 'ACTION', choices: ['DEPLOY', 'STATUS', 'UNINSTALL']",
'Jenkins 参数必须固定为 DEPLOY/STATUS/UNINSTALL。',
);
assertIncludes(
jenkinsfile,
"string(name: 'SOURCE_BRANCH'",
'Jenkins 必须暴露 SOURCE_BRANCH。',
);
assertIncludes(
jenkinsfile,
"string(name: 'COMMIT_HASH'",
'Jenkins 必须暴露 COMMIT_HASH。',
);
assertIncludes(
jenkinsfile,
"string(name: 'DEPLOYMENT_ID'",
'Jenkins 必须暴露 DEPLOYMENT_ID。',
);
assertIncludes(
jenkinsfile,
"archiveArtifacts artifacts: 'preview-result.json'",
'Jenkins 必须归档 preview-result.json。',
);
assertIncludes(
jenkinsfile,
"writeFile file: 'preview-result.json'",
'Jenkins 失败路径也必须生成 preview-result.json。',
);
assertIncludes(
jenkinsfile,
'disableConcurrentBuilds()',
'Jenkins Job 必须禁止并发构建。',
);
assertIncludes(
jenkinsfile,
'COMMIT_HASH="${COMMIT_HASH:-}"',
'Jenkins Checkout 阶段必须允许 COMMIT_HASH 未导出或为空。',
);
assertIncludes(
jenkinsfile,
'bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh"',
'Jenkins Checkout 阶段必须显式使用 bash 执行源码检出脚本。',
);
assertExcludes(
jenkinsfile,
'COMMIT_HASH="${COMMIT_HASH}"',
'Jenkins Checkout 阶段不得在 set -u 下直接展开未导出的 COMMIT_HASH。',
);
assertIncludes(deployer, 'flock -x 9', '部署脚本必须使用跨进程独占锁。');
assertIncludes(
deployer,
'git check-ref-format --branch',
'部署脚本必须校验分支格式。',
);
assertIncludes(
deployer,
'genarrative-preview-[0-9a-f]{16}',
'卸载必须限制到预览 compose project 白名单。',
);
assertIncludes(
deployer,
'allocate_port webPort 8400 8499',
'Web 端口必须从 8400-8499 分配。',
);
assertIncludes(
deployer,
'ports: !reset []',
'预览 compose override 必须取消 SpacetimeDB 和 OTLP 宿主端口映射。',
);
assertIncludes(
deployer,
'spacetimedb:\n mem_limit: 2g\n ports: !reset []',
'预览 SpacetimeDB 必须覆盖压测基线内存限制,避免模块首次加载时 OOM。',
);
assertIncludes(
deployer,
'^preview-[0-9a-f]{16}$',
'部署 ID 必须固定为 preview- 加 16 位摘要。',
);
assertIncludes(
deployer,
'state.active === true && String(state[key]) === expected',
'只有活动实例才能占用端口租约。',
);
assertIncludes(
deployer,
'--yes=remote,migrate,break-clients',
'SpacetimeDB 预览发布必须显式确认受控容器网络目标,且不得默认删除实例数据。',
);
assertExcludes(
deployer,
'--delete-data',
'预览部署不得使用 SpacetimeDB 删除数据参数。',
);
assertIncludes(
jenkinsfile,
"GENARRATIVE_PREVIEW_WEB_HOST = '192.168.35.82'",
'Jenkins 必须固定输出可供同事访问的内网 Web 主机地址。',
);
assertIncludes(webConfig, "?? '/build/'", 'SPA 默认 base 必须固定为 /build/。');
assertIncludes(
systemd,
'User=jenkins',
'控制服务必须使用 Jenkins 运行用户以访问受控构建状态。',
);
assertIncludes(
nginx,
'location ^~ /build/',
'内网 Nginx 必须提供 /build/ 路由。',
);
assertIncludes(
server,
'.nest_service("/build/assets", ServeDir::new(static_dir.join("assets")))',
'控制服务必须原生托管 /build 子路径,不能依赖 Nginx 隐式改写。',
);
assertIncludes(
jobConfig,
'<scriptPath>jenkins/Jenkinsfile.preview-deployer</scriptPath>',
'Jenkins Job 必须从受保护 master 读取固定流水线。',
);
assertIncludes(
jobConfig,
'<hudson.model.ChoiceParameterDefinition>',
'Jenkins Job 首次触发前必须已经声明 buildWithParameters 参数。',
);
assertIncludes(
nginx,
'location ^~ /api/preview-deployer/',
'内网 Nginx 必须代理预览控制 API。',
);
assertIncludes(
resultWriter,
'schemaVersion: 1',
'结果文件必须带版本化 schema。',
);
assertIncludes(resultWriter, 'healthStatus', '结果文件必须提供健康状态。');
assertIncludes(resultWriter, 'webUrl', '结果文件必须提供 Web URL。');
assertIncludes(
resultWriter,
'resolvedCommit',
'结果文件必须提供 resolvedCommit。',
);
assertIncludes(
resultWriter,
'status: publicStatus(phase)',
'结果文件必须提供小写 status 契约。',
);
assertIncludes(
resultWriter,
'health: publicHealth(healthStatus)',
'结果文件必须提供小写 health 契约。',
);
assertIncludes(
compose,
'test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]',
'Nginx 容器健康检查必须验证 Web 首页。',
);
assertExcludes(
compose,
'test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"]',
'Nginx 容器健康检查不能访问公开拒绝的 /healthz。',
);
if (failures.length > 0) {
for (const failure of failures)
console.error(`[check:preview-deployer] ${failure}`);
process.exit(1);
}
console.log(
'[check:preview-deployer] Jenkins 参数、隔离部署、卸载白名单、结果契约和 Nginx 健康检查通过。',
);
function assertIncludes(content, expected, message) {
if (!content.includes(expected)) failures.push(message);
}
function assertExcludes(content, expected, message) {
if (content.includes(expected)) failures.push(message);
}