071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
334 lines
9.7 KiB
JavaScript
334 lines
9.7 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 apiServerDockerfile = readFileSync(
|
|
'deploy/container/api-server.Dockerfile',
|
|
'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 environmentExample = readFileSync(
|
|
'deploy/env/preview-deployer.env.example',
|
|
'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(
|
|
compose,
|
|
' mem_limit: 2g\n ports:',
|
|
'完整容器 SpacetimeDB 必须允许当前模块首次实例化的内存峰值。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'external-generation-worker:\n build:',
|
|
'预览 compose override 必须配置外部生成 worker。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'restart: on-failure',
|
|
'预览外部生成 worker 必须在运行时身份初始化竞态后自动重启。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'GENARRATIVE_PREVIEW_SECRETS_SHA256',
|
|
'预览构建必须把固定 secrets 文件摘要作为镜像缓存与完整性校验参数。',
|
|
);
|
|
assertIncludes(
|
|
jenkinsfile,
|
|
"GENARRATIVE_PREVIEW_SECRETS_FILE = '/data/jenkins/preview-secrets/.env.secrets.local'",
|
|
'Jenkins 必须从受保护的固定宿主路径读取预览 secrets。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'[[ "${secrets_mode}" == "600" ]]',
|
|
'预览构建必须拒绝权限过宽的 secrets 文件。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'[[ "${secrets_owner}" == "${EUID}" ]]',
|
|
'预览构建必须校验 secrets 文件归 Jenkins 执行用户所有。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'genarrative_preview_secrets',
|
|
'预览 API 与外部生成 worker 构建必须使用固定名称的 BuildKit secret。',
|
|
);
|
|
assertCount(
|
|
deployer,
|
|
'target: genarrative_preview_secrets',
|
|
2,
|
|
'预览 secrets 必须且只能提供给 API 和外部生成 worker 两个构建。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'ARG GENARRATIVE_PREVIEW_SECRETS_SHA256=',
|
|
'API 镜像必须允许普通构建不提供预览 secrets 摘要。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'RUN --mount=type=secret,id=genarrative_preview_secrets,required=false',
|
|
'API 镜像必须通过可选 BuildKit secret 接收预览 secrets 文件。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'test -f /run/secrets/genarrative_preview_secrets;',
|
|
'提供预览 secrets 摘要时必须要求 BuildKit secret 存在。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'sha256sum /run/secrets/genarrative_preview_secrets',
|
|
'API 镜像必须在安装前校验预览 secrets 文件摘要。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'install -o genarrative -g genarrative -m 0400',
|
|
'预览 secrets 文件必须只允许 API 运行用户读取。',
|
|
);
|
|
assertIncludes(
|
|
apiServerDockerfile,
|
|
'/run/secrets/genarrative_preview_secrets /srv/genarrative/.env.secrets.local;',
|
|
'预览 secrets 文件必须安装到 API 启动时读取的固定路径。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
'GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true',
|
|
'预览独立空库必须允许首次密码登录自动创建预览账号。',
|
|
);
|
|
assertIncludes(
|
|
deployer,
|
|
"'SMS_AUTH_ENABLED=true',\n 'SMS_AUTH_PROVIDER=mock',\n 'SMS_AUTH_MOCK_VERIFY_CODE=123456'",
|
|
'预览短信登录必须使用固定 mock 验证码,不得继承生产短信密钥。',
|
|
);
|
|
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(
|
|
server,
|
|
'FAILED_RECORD_TTL_SECS',
|
|
'控制服务必须清理过期且不可卸载的失败/取消记录。',
|
|
);
|
|
assertIncludes(
|
|
server,
|
|
'STOPPED_RECORD_TTL_SECS',
|
|
'控制服务必须为已卸载记录配置有限审计保留期。',
|
|
);
|
|
assertIncludes(
|
|
environmentExample,
|
|
'GENARRATIVE_PREVIEW_DEPLOYER_JENKINS_PUBLIC_BASE_URL=http://192.168.35.82:8080/jenkins/',
|
|
'构建详情必须使用局域网 Jenkins 地址而非 loopback。',
|
|
);
|
|
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 契约。',
|
|
);
|
|
const normalizedCompose = compose.replaceAll("'", '"');
|
|
assertIncludes(
|
|
normalizedCompose,
|
|
'test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]',
|
|
'Nginx 容器健康检查必须验证 Web 首页。',
|
|
);
|
|
assertExcludes(
|
|
normalizedCompose,
|
|
'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);
|
|
}
|
|
|
|
function assertCount(content, expected, count, message) {
|
|
if (content.split(expected).length - 1 !== count) failures.push(message);
|
|
}
|