修复维护退出阶段隐式拉取源码

将 Exit Maintenance 改为 agent none 并显式分配部署节点,只执行 current release 维护脚本。
增加生产运维静态门禁,禁止维护退出节点 checkout Git 或挂载 Git 凭据。
同步生产运维说明与 Jenkins 重启恢复场景的排障记录。
This commit is contained in:
2026-07-20 18:55:35 +08:00
parent eb608ac6f2
commit 8965a53fb8
4 changed files with 50 additions and 15 deletions
@@ -922,6 +922,14 @@
- 验证:检查 `jenkins/Jenkinsfile.production-stdb-module-publish` 文件开头字节不再是 `EF BB BF`,并用 Jenkins `validateDeclarativePipeline` 或重放 `Genarrative-Stdb-Module-Publish`,不应再停在 `No such DSL method 'pipeline'`
- 关联:`jenkins/Jenkinsfile.production-stdb-module-publish``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## Full Build 的维护退出节点不得 checkout Git
- 现象:Full Build 的 Stdb、API 和 Web 都已发布成功,`Exit Maintenance` 进入目标部署 agent 后却先执行 `checkout scm`,用 `ssh://git@127.0.0.1:2222/...` 拉仓库并报 `Connection refused`,导致已部署的维护退出脚本根本没有执行。
- 原因:`127.0.0.1:2222` 只是 Jenkins controller 上的 Gitea SSH 端口,在部署 agent 上代表部署机自身。该次流水线在 Jenkins 重启后恢复,Declarative 的阶段 `agent` 路径未继续遵守顶层 `skipDefaultCheckout(true)`,在 `steps` 前注入了不必要的 SCM checkout。
- 处理:`Exit Maintenance` 保持 `agent none`,在 `steps` 内根据 `DEPLOY_TARGET` 用显式 `node(deployLabel)` 分配目标机,只从绝对路径执行 current release 已携带的 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`。不要在这个节点添加 GitSCM、Git SSH 凭据或 Jenkins workspace 相对路径。
- 验证:运行 `npm run check:production-ops`;重放流水线时,`Exit Maintenance``Running on <deploy-agent>` 之后应直接进入 `sh`,不应出现 `checkout``GitSCM` 或 Git 凭据日志。
- 关联:`jenkins/Jenkinsfile.production-full-build-and-deploy``scripts/check-production-ops-guardrails.mjs``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## Linux 多用户 dev 端口冲突先查系统级端口段注册表
- 现象:同一台 Linux 机器上多个用户同时开发时,`npm run dev` 报端口段已被其他用户占用、同一用户已有活跃端口段,或 SpacetimeDB 复用记录指向当前用户端口段之外的地址;未手动指定时自动分配应从 `10000-10099` 起步。
@@ -80,7 +80,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
自 2026-07-11 起,`Genarrative-Full-Build-And-Deploy` 的每日 04:00 timer 默认以 `DEPLOY_TARGET=development``STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
需要验证“更新 API 不停 worker”和“worker 是否持续消费队列”时,优先使用隔离容器 smoke:`npm run container:worker-smoke -- smoke`。该脚本生成 gitignored 的 `deploy/container/worker-smoke/api-server.env`,启动独立 compose project 与独立 SpacetimeDB,发布当前 `spacetime-module` 后写入 `worker_smoke_unsupported` 测试 job;预期 worker claim 后执行 unsupported 失败分支,再执行 API-only recreate 并确认 worker 容器 ID 不变,最后再次入队验证 API 更新后队列仍可消费。`external_generation_job` 是 private table,脚本通过 worker 日志确认 job_id 被消费,不用 CLI SQL 查询私表。该 smoke 不读取 `.env.local`,也不依赖真实 VectorEngine / OSS 密钥;真实生图链路联调再在本地私有 env 中补齐 provider 配置。worker-smoke 默认把本机 `spacetime` CLI 打成轻量 SpacetimeDB 镜像,避免本机首次 smoke 依赖官方大镜像下载。若容器内 Cargo 拉取 crates.io 依赖不稳定,可用 `npm run container:worker-smoke -- smoke --local-binary` 让容器内 Cargo 复用本机 Cargo 缓存构建当前二进制,再打入 Debian bookworm smoke runtime 临时镜像;可用 `GENARRATIVE_WORKER_SMOKE_LOCAL_BASE_IMAGE` 覆盖运行时基础镜像;若隔离端口或库数据需要重建,追加 `--force`。完成 queue 链路验证时,还要用队列概览 BFF 和单 job 状态接口确认 job 从 queued/running 收敛,并用对应玩法 session/detail 接口确认业务状态同步完成。
@@ -261,21 +261,24 @@ pipeline {
when {
expression { return params.EXIT_MAINTENANCE_MODE_AFTER_COMPLETION != false }
}
agent {
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'}"
}
agent none
steps {
sh '''
bash -lc '
set -euo pipefail
maintenance_script="/opt/genarrative/current/scripts/deploy/maintenance-off.sh"
if [[ ! -f "${maintenance_script}" ]]; then
echo "Full 发布完成但 current release 缺少维护退出脚本: ${maintenance_script}" >&2
exit 1
fi
bash "${maintenance_script}"
'
'''
script {
def deployLabel = params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-dev-deploy' : 'linux && genarrative-release-deploy'
node(deployLabel) {
sh '''
bash -lc '
set -euo pipefail
maintenance_script="/opt/genarrative/current/scripts/deploy/maintenance-off.sh"
if [[ ! -f "${maintenance_script}" ]]; then
echo "Full 发布完成但 current release 缺少维护退出脚本: ${maintenance_script}" >&2
exit 1
fi
bash "${maintenance_script}"
'
'''
}
}
}
}
}
@@ -7092,6 +7092,30 @@ if ((fullPipelineMaintenanceHoldCalls?.length ?? 0) !== 2) {
);
}
const exitMaintenanceStageOffset = fullPipelineContent.indexOf(
"stage('Exit Maintenance')",
);
const fullPipelinePostOffset = fullPipelineContent.indexOf(
'\n post {',
exitMaintenanceStageOffset,
);
const exitMaintenanceStageContent =
exitMaintenanceStageOffset >= 0 && fullPipelinePostOffset > exitMaintenanceStageOffset
? fullPipelineContent.slice(exitMaintenanceStageOffset, fullPipelinePostOffset)
: '';
if (
!exitMaintenanceStageContent.includes('agent none') ||
!exitMaintenanceStageContent.includes('node(deployLabel)') ||
exitMaintenanceStageContent.includes("$class: 'GitSCM'") ||
exitMaintenanceStageContent.includes('checkout scm') ||
exitMaintenanceStageContent.includes('sshUserPrivateKey(')
) {
failed = true;
console.error(
'[check:production-ops] Full Build 的 Exit Maintenance 必须使用 agent none + 显式 node 执行 current release 脚本,不得在目标机 checkout Git 或挂载 Git SSH 凭据。',
);
}
for (const file of nodeEnvFileCommandFiles) {
const content = readFileSync(file, 'utf8');
const commandText = content.replace(/\\\r?\n\s*/g, ' ');