修复生产发布 worker unit 路径渲染
按部署参数渲染 BgFilter、外部生成 worker 与 controller unit 补齐 controller 环境文件参数及 Jenkins 全链路透传 新增安装后 unit 内容与流水线参数回归门禁 同步生产运维文档和共享项目记忆
This commit is contained in:
@@ -4408,3 +4408,10 @@
|
||||
- 运维与回滚:用 `scripts/gitea-ci-job-image.sh build|verify|export|load-runner` 管理镜像,按 `build/verify -> export 仓库外镜像归档和 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份 config -> 增加或替换 label -> docker restart --timeout 660` 切换。config 与镜像归档只放仓库外受控位置;共享文档只记录通用备份规则,不记录宿主绝对路径、注册信息或 token。重启后先验证真实 CI 再清理旧镜像;回滚先把 workflow `runs-on` 改回 `ubuntu-latest`,再恢复 config 备份并重启 runner。
|
||||
- 影响范围:`.gitea/workflows/project-ci.yml`、`deploy/container/gitea-ci-job.Dockerfile`、`scripts/gitea-ci-job-image.sh`、`scripts/check-gitea-ci-job-image.sh`、`scripts/check-gitea-ci-job-runtime.sh`、runner label/config 和 Gitea CI 运维文档。
|
||||
- 验证方式:构建脚本校验宿主与 runner 内层 Image ID 一致;环境脚本校验 Node、Rust、`rustfmt`、Chrome、bwrap、原生命令与 pkg-config 依赖;runtime 脚本执行完整 bwrap 和 Chrome headless canary;真实 PR 的四个 job 全部通过,同时复核 `Privileged=false`、`Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]` 和独立网络。
|
||||
|
||||
## 2026-07-23 生产 API 发布按实际路径渲染 worker systemd unit
|
||||
|
||||
- 背景:Server-Provision 支持自定义 current link、API env 和角色 env,并在首次安装时渲染三个 worker unit;API deploy 为下发随 release 更新的 unit 又原样覆盖目标机配置,导致自定义路径在下一次发布时退回模板默认值。
|
||||
- 决策:`production-api-deploy.sh` 继续随 release 安装默认命名的 BgFilter、external-generation worker 和 controller unit,但安装前必须用本次部署参数渲染临时文件;新增 `--controller-env-file` 补齐 controller 专属 env 输入。release 内模板保持默认路径,供 provision 和 deploy 共同作为单一模板来源;自定义服务名仍由目标机自行管理,不强制覆盖。
|
||||
- 影响范围:`scripts/deploy/production-api-deploy.sh`、`scripts/check-production-api-deploy.mjs`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-full-build-and-deploy`、`scripts/check-production-ops-guardrails.mjs`、生产运维文档和 worker systemd 发布契约。
|
||||
- 验证方式:`bash -n scripts/deploy/production-api-deploy.sh`、`node --check scripts/check-production-api-deploy.mjs`、`npm run check:production-api-deploy`、`npm run check:production-ops`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
@@ -3312,3 +3312,11 @@
|
||||
- 验证与回滚:重启后先跑真实 PR 的四个 job,再清理旧镜像。失败时先把 workflow `runs-on` 改回 `ubuntu-latest`,再恢复 runner config 备份并重启;不在 Git、共享文档或日志中记录 config 备份路径、注册信息或 token。
|
||||
- 重启边界:`docker restart --timeout 660` 只设置容器停止宽限,不能替代 Runner drain。rootless DinD supervisor 可能与 runner 同时停止内层 dockerd,使仍在收尾的 job 因连接关闭被标记失败;切换前必须同时确认 Gitea 没有 `in_progress` run 且内层 `docker ps` 为空。误触发时只重跑受影响的失败 job,不重跑已成功项。
|
||||
- 关联:`deploy/container/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。
|
||||
|
||||
## 生产 API 发布重装 worker unit 不能丢失自定义路径(2026-07-23)
|
||||
|
||||
- 现象:Server-Provision 已按自定义 current link 和 env 路径安装 worker systemd unit,但下一次 API 发布后,worker 可能重新读取 `/opt/genarrative/current` 与 `/etc/genarrative/*.env`;默认路径仍有旧 release 时,服务 active 和部署成功都不能证明新二进制已运行。
|
||||
- 原因:发布包中的 BgFilter、external-generation worker 和 controller unit 是带默认路径的模板;deploy 若直接 `install` 原文件,会覆盖 provision 已渲染的目标机 unit。external-generation 专属 env 还是可选加载,错误路径可能不会阻止服务进入 active。
|
||||
- 处理:API deploy 安装三个 unit 前必须按本次 current、API env 和各角色 env 参数渲染临时文件,安装后保留 release 内原始模板不变;controller 自定义 env 由 `--controller-env-file` 显式传入。API Deploy 与 Full Job 必须同步暴露并透传 controller/BgFilter env,不能让流水线回退默认路径。自定义服务名表示沿用目标机自管 unit,不进入默认 unit 安装分支。
|
||||
- 验证:部署 guard 使用临时自定义绝对路径,直接读取实际安装目录中的三个 unit,核对 `WorkingDirectory`、`ExecStart`、共享 API env 与角色 env,不能只用 fake `systemctl is-active` 判绿。
|
||||
- 关联:`scripts/deploy/production-api-deploy.sh`、`scripts/check-production-api-deploy.mjs`、`scripts/jenkins-server-provision.sh`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,6 +24,8 @@ pipeline {
|
||||
string(name: 'HEALTH_URL', defaultValue: 'http://127.0.0.1:8082/readyz', description: '本机 readiness 检查地址')
|
||||
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'api-server 环境文件')
|
||||
string(name: 'WORKER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-worker.env', description: '外部生成 worker 环境文件')
|
||||
string(name: 'CONTROLLER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-controller.env', description: '外部生成 controller 环境文件')
|
||||
string(name: 'BGFILTER_WORKER_ENV_FILE', defaultValue: '/etc/genarrative/bgfilter-worker.env', description: 'BgFilter worker 环境文件')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'api-server 连接的 SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER_URL', defaultValue: 'http://127.0.0.1:3101', description: 'api-server 连接的 SpacetimeDB server URL')
|
||||
}
|
||||
@@ -53,7 +55,7 @@ pipeline {
|
||||
if (!(params.DATABASE.trim() ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error("DATABASE 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*\$: ${params.DATABASE}")
|
||||
}
|
||||
[API_ENV_FILE: params.API_ENV_FILE, WORKER_ENV_FILE: params.WORKER_ENV_FILE].each { name, value ->
|
||||
[API_ENV_FILE: params.API_ENV_FILE, WORKER_ENV_FILE: params.WORKER_ENV_FILE, CONTROLLER_ENV_FILE: params.CONTROLLER_ENV_FILE, BGFILTER_WORKER_ENV_FILE: params.BGFILTER_WORKER_ENV_FILE].each { name, value ->
|
||||
def envFile = value?.trim()
|
||||
if (!envFile || envFile == '/' || !(envFile ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) {
|
||||
error("${name} 必须是 Linux 绝对路径且不能包含 ..: ${envFile}")
|
||||
@@ -117,6 +119,8 @@ pipeline {
|
||||
--health-url "${HEALTH_URL}" \
|
||||
--api-env-file "${API_ENV_FILE:-/etc/genarrative/api-server.env}" \
|
||||
--worker-env-file "${WORKER_ENV_FILE:-/etc/genarrative/external-generation-worker.env}" \
|
||||
--controller-env-file "${CONTROLLER_ENV_FILE:-/etc/genarrative/external-generation-controller.env}" \
|
||||
--bgfilter-worker-env-file "${BGFILTER_WORKER_ENV_FILE:-/etc/genarrative/bgfilter-worker.env}" \
|
||||
--database "${DATABASE}" \
|
||||
--spacetime-server-url "${SPACETIME_SERVER_URL:-http://127.0.0.1:3101}"
|
||||
'
|
||||
|
||||
@@ -41,6 +41,8 @@ pipeline {
|
||||
string(name: 'SPACETIME_RUN_AS_USER', defaultValue: 'spacetimedb', description: 'Stdb 发布使用的本机用户')
|
||||
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'API 与 Stdb publish 共用的 api-server 环境文件')
|
||||
string(name: 'WORKER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-worker.env', description: 'API 与 Stdb publish 共用的 worker 环境文件')
|
||||
string(name: 'CONTROLLER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-controller.env', description: 'API deploy 使用的外部生成 controller 环境文件')
|
||||
string(name: 'BGFILTER_WORKER_ENV_FILE', defaultValue: '/etc/genarrative/bgfilter-worker.env', description: 'API deploy 使用的 BgFilter worker 环境文件')
|
||||
choice(name: 'STDB_API_ROLLOUT_MODE', choices: ['normal', 'pause-after-stdb'], description: '定时任务默认 normal 完整发布 dev;人工维护窗口才选择 pause-after-stdb')
|
||||
string(name: 'STDB_API_ROLLOUT_APPROVERS', defaultValue: '', description: 'pause-after-stdb 必填:允许放行 API 的 Jenkins 用户/组,多个值用逗号分隔')
|
||||
booleanParam(name: 'EXIT_MAINTENANCE_MODE_AFTER_COMPLETION', defaultValue: true, description: '完整发布成功后是否退出维护模式;取消勾选会保留维护页,便于内网验收后人工恢复公网')
|
||||
@@ -233,6 +235,8 @@ pipeline {
|
||||
booleanParam(name: 'KEEP_MAINTENANCE_MODE', value: true),
|
||||
string(name: 'API_ENV_FILE', value: params.API_ENV_FILE ?: '/etc/genarrative/api-server.env'),
|
||||
string(name: 'WORKER_ENV_FILE', value: params.WORKER_ENV_FILE ?: '/etc/genarrative/external-generation-worker.env'),
|
||||
string(name: 'CONTROLLER_ENV_FILE', value: params.CONTROLLER_ENV_FILE ?: '/etc/genarrative/external-generation-controller.env'),
|
||||
string(name: 'BGFILTER_WORKER_ENV_FILE', value: params.BGFILTER_WORKER_ENV_FILE ?: '/etc/genarrative/bgfilter-worker.env'),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
string(name: 'SPACETIME_SERVER_URL', value: params.SPACETIME_SERVER_URL ?: ''),
|
||||
]
|
||||
|
||||
@@ -345,15 +345,92 @@ function assertDeployCopiesPingoraDirectReleaseDependencies() {
|
||||
),
|
||||
'API deploy 必须把随包 BgFilter worker 单元安装到 systemd unit 目录。',
|
||||
);
|
||||
const externalGenerationWorkerUnit = readFileSync(
|
||||
path.join(
|
||||
fixture.systemdUnitDir,
|
||||
'genarrative-external-generation-worker@.service',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const externalGenerationControllerUnit = readFileSync(
|
||||
path.join(
|
||||
fixture.systemdUnitDir,
|
||||
'genarrative-external-generation-controller.service',
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const bgfilterUnit = readFileSync(
|
||||
path.join(fixture.systemdUnitDir, 'genarrative-bgfilter-worker.service'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
for (const [label, unit] of [
|
||||
['外部生成 worker unit', externalGenerationWorkerUnit],
|
||||
['外部生成 controller unit', externalGenerationControllerUnit],
|
||||
['BgFilter worker unit', bgfilterUnit],
|
||||
]) {
|
||||
assertIncludes(
|
||||
unit,
|
||||
`WorkingDirectory=${fixture.currentLink}`,
|
||||
`${label} 必须使用本次部署的自定义 current link。`,
|
||||
);
|
||||
assertIncludes(
|
||||
unit,
|
||||
`${fixture.currentLink}/api-server`,
|
||||
`${label} 必须启动本次部署的自定义 current link 下的二进制。`,
|
||||
);
|
||||
assertIncludes(
|
||||
unit,
|
||||
`EnvironmentFile=${fixture.apiEnvFile}`,
|
||||
`${label} 必须加载本次部署指定的 API env。`,
|
||||
);
|
||||
assertNotIncludes(
|
||||
unit,
|
||||
'/opt/genarrative/current',
|
||||
`${label} 安装后不得继续引用模板默认 current link。`,
|
||||
);
|
||||
assertNotIncludes(
|
||||
unit,
|
||||
'/etc/genarrative/api-server.env',
|
||||
`${label} 安装后不得继续引用模板默认 API env。`,
|
||||
);
|
||||
}
|
||||
|
||||
assertIncludes(
|
||||
externalGenerationWorkerUnit,
|
||||
`EnvironmentFile=-${fixture.externalGenerationWorkerEnvFile}`,
|
||||
'外部生成 worker unit 必须加载本次部署指定的专属 env。',
|
||||
);
|
||||
assertNotIncludes(
|
||||
externalGenerationWorkerUnit,
|
||||
'/etc/genarrative/external-generation-worker.env',
|
||||
'外部生成 worker unit 安装后不得继续引用模板默认专属 env。',
|
||||
);
|
||||
assertIncludes(
|
||||
externalGenerationControllerUnit,
|
||||
`EnvironmentFile=-${fixture.externalGenerationControllerEnvFile}`,
|
||||
'外部生成 controller unit 必须加载本次部署指定的专属 env。',
|
||||
);
|
||||
assertNotIncludes(
|
||||
externalGenerationControllerUnit,
|
||||
'/etc/genarrative/external-generation-controller.env',
|
||||
'外部生成 controller unit 安装后不得继续引用模板默认专属 env。',
|
||||
);
|
||||
assertIncludes(
|
||||
bgfilterUnit,
|
||||
`EnvironmentFile=${fixture.bgfilterWorkerEnvFile}`,
|
||||
'BgFilter unit 必须加载本次部署指定的专属 env。',
|
||||
);
|
||||
assertNotIncludes(
|
||||
bgfilterUnit,
|
||||
'/etc/genarrative/bgfilter-worker.env',
|
||||
'BgFilter unit 安装后不得继续引用模板默认专属 env。',
|
||||
);
|
||||
const sharedEnvIndex = bgfilterUnit.indexOf(
|
||||
'EnvironmentFile=/etc/genarrative/api-server.env',
|
||||
`EnvironmentFile=${fixture.apiEnvFile}`,
|
||||
);
|
||||
const dedicatedEnvIndex = bgfilterUnit.indexOf(
|
||||
'EnvironmentFile=/etc/genarrative/bgfilter-worker.env',
|
||||
`EnvironmentFile=${fixture.bgfilterWorkerEnvFile}`,
|
||||
);
|
||||
if (
|
||||
sharedEnvIndex < 0 ||
|
||||
@@ -1905,6 +1982,11 @@ function prepareFixture(name) {
|
||||
'etc',
|
||||
'external-generation-worker.env',
|
||||
);
|
||||
const externalGenerationControllerEnvFile = path.join(
|
||||
root,
|
||||
'etc',
|
||||
'external-generation-controller.env',
|
||||
);
|
||||
const bgfilterWorkerEnvFile = path.join(
|
||||
root,
|
||||
'etc',
|
||||
@@ -1955,6 +2037,11 @@ function prepareFixture(name) {
|
||||
'GENARRATIVE_EXTERNAL_GENERATION_WORKER_CONCURRENCY=2\n',
|
||||
'utf8',
|
||||
);
|
||||
writeFileSync(
|
||||
externalGenerationControllerEnvFile,
|
||||
'GENARRATIVE_EXTERNAL_GENERATION_CONTROLLER_MAX_WORKERS=4\n',
|
||||
'utf8',
|
||||
);
|
||||
writeFileSync(
|
||||
bgfilterWorkerEnvFile,
|
||||
[
|
||||
@@ -2310,6 +2397,7 @@ function prepareFixture(name) {
|
||||
currentLink,
|
||||
apiEnvFile,
|
||||
externalGenerationWorkerEnvFile,
|
||||
externalGenerationControllerEnvFile,
|
||||
bgfilterWorkerEnvFile,
|
||||
bgfilterTokenFile,
|
||||
pingoraEnvFile,
|
||||
@@ -2386,6 +2474,8 @@ function runDeploy(fixture, options = {}) {
|
||||
options.apiEnvFile ?? fixture.apiEnvFile,
|
||||
'--worker-env-file',
|
||||
fixture.externalGenerationWorkerEnvFile,
|
||||
'--controller-env-file',
|
||||
fixture.externalGenerationControllerEnvFile,
|
||||
'--bgfilter-worker-env-file',
|
||||
fixture.bgfilterWorkerEnvFile,
|
||||
'--bgfilter-worker-health-url',
|
||||
@@ -2506,6 +2596,12 @@ function assertIncludes(content, needle, reason) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotIncludes(content, needle, reason) {
|
||||
if (content.includes(needle)) {
|
||||
failures.push(`${reason} 不应包含: ${needle}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPingoraEnvProductionDefaults(filePath, label) {
|
||||
if (!existsSync(filePath)) {
|
||||
return;
|
||||
|
||||
@@ -233,6 +233,12 @@ const checks = [
|
||||
reason:
|
||||
'API readiness 单次请求必须有超时,避免端口已建立但服务尚未响应时绕过重试上限无限挂起。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/deploy/production-api-deploy.sh',
|
||||
includes:
|
||||
'on_exit() {\n local exit_code=$?\n cleanup_rendered_systemd_unit',
|
||||
reason: 'API deploy 被中断时必须清理尚未安装完成的临时 systemd unit。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
@@ -244,6 +250,54 @@ const checks = [
|
||||
includes: 'maintenance_deploy_args+=(--keep-maintenance-mode)',
|
||||
reason: 'API Deploy Job 必须把保持维护参数传给发布产物内的部署脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
"string(name: 'CONTROLLER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-controller.env'",
|
||||
reason: 'API Deploy Job 必须暴露外部生成 controller env 路径。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
"string(name: 'BGFILTER_WORKER_ENV_FILE', defaultValue: '/etc/genarrative/bgfilter-worker.env'",
|
||||
reason: 'API Deploy Job 必须暴露 BgFilter worker env 路径。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
'--controller-env-file "${CONTROLLER_ENV_FILE:-/etc/genarrative/external-generation-controller.env}"',
|
||||
reason: 'API Deploy Job 必须把 controller env 路径传给发布脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-deploy',
|
||||
includes:
|
||||
'--bgfilter-worker-env-file "${BGFILTER_WORKER_ENV_FILE:-/etc/genarrative/bgfilter-worker.env}"',
|
||||
reason: 'API Deploy Job 必须把 BgFilter worker env 路径传给发布脚本。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
"string(name: 'CONTROLLER_ENV_FILE', defaultValue: '/etc/genarrative/external-generation-controller.env'",
|
||||
reason: 'Full Job 必须暴露外部生成 controller env 路径。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
"string(name: 'BGFILTER_WORKER_ENV_FILE', defaultValue: '/etc/genarrative/bgfilter-worker.env'",
|
||||
reason: 'Full Job 必须暴露 BgFilter worker env 路径。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
"string(name: 'CONTROLLER_ENV_FILE', value: params.CONTROLLER_ENV_FILE ?: '/etc/genarrative/external-generation-controller.env')",
|
||||
reason: 'Full Job 必须把 controller env 路径传给 API Deploy Job。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-full-build-and-deploy',
|
||||
includes:
|
||||
"string(name: 'BGFILTER_WORKER_ENV_FILE', value: params.BGFILTER_WORKER_ENV_FILE ?: '/etc/genarrative/bgfilter-worker.env')",
|
||||
reason: 'Full Job 必须把 BgFilter worker env 路径传给 API Deploy Job。',
|
||||
},
|
||||
{
|
||||
file: 'scripts/jenkins-server-provision.sh',
|
||||
includes: 'ensure_runtime_bootstrap_secret_file_env',
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./scripts/deploy/production-api-deploy.sh --source-dir build/<version> [--version <version>] [--release-root /opt/genarrative/releases] [--current-link /opt/genarrative/current] [--service genarrative-api.service] [--pingora-service genarrative-pingora-gateway.service] [--require-pingora-gateway] [--bgfilter-worker-service genarrative-bgfilter-worker.service] [--bgfilter-worker-health-url http://127.0.0.1:8083/readyz] [--bgfilter-worker-env-file /etc/genarrative/bgfilter-worker.env] [--no-bgfilter-worker] [--worker-service-pattern 'genarrative-external-generation-worker@*.service'] [--no-worker-services] [--worker-controller-service genarrative-external-generation-controller.service] [--no-worker-controller] [--health-url http://127.0.0.1:8082/readyz] [--api-env-file /etc/genarrative/api-server.env] [--worker-env-file /etc/genarrative/external-generation-worker.env] [--database genarrative-prod] [--spacetime-server-url http://127.0.0.1:3101] [--keep-maintenance-mode]
|
||||
./scripts/deploy/production-api-deploy.sh --source-dir build/<version> [--version <version>] [--release-root /opt/genarrative/releases] [--current-link /opt/genarrative/current] [--service genarrative-api.service] [--pingora-service genarrative-pingora-gateway.service] [--require-pingora-gateway] [--bgfilter-worker-service genarrative-bgfilter-worker.service] [--bgfilter-worker-health-url http://127.0.0.1:8083/readyz] [--bgfilter-worker-env-file /etc/genarrative/bgfilter-worker.env] [--no-bgfilter-worker] [--worker-service-pattern 'genarrative-external-generation-worker@*.service'] [--no-worker-services] [--worker-controller-service genarrative-external-generation-controller.service] [--controller-env-file /etc/genarrative/external-generation-controller.env] [--no-worker-controller] [--health-url http://127.0.0.1:8082/readyz] [--api-env-file /etc/genarrative/api-server.env] [--worker-env-file /etc/genarrative/external-generation-worker.env] [--database genarrative-prod] [--spacetime-server-url http://127.0.0.1:3101] [--keep-maintenance-mode]
|
||||
|
||||
说明:
|
||||
进入维护模式,校验并发布 api-server 单文件,更新 current 链接,重启 systemd 服务并执行 readiness 检查。
|
||||
@@ -35,6 +35,10 @@ require_absolute_path() {
|
||||
echo "[production-api-deploy] ${label} 必须使用绝对路径: ${value}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${value}" == *$'\n'* || "${value}" == *$'\r'* ]]; then
|
||||
echo "[production-api-deploy] ${label} 不能包含换行符。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_spacetime_database_name() {
|
||||
@@ -892,38 +896,103 @@ install_release_systemd_unit() {
|
||||
fi
|
||||
|
||||
echo "[production-api-deploy] 安装${label}: ${unit_name}"
|
||||
run_privileged install -d -m 0755 "${unit_dir}"
|
||||
if ! run_privileged install -d -m 0755 "${unit_dir}"; then
|
||||
return 1
|
||||
fi
|
||||
run_privileged install -m 0644 "${source_path}" "${unit_dir}/${unit_name}"
|
||||
}
|
||||
|
||||
render_and_install_release_systemd_unit() {
|
||||
local source_path="$1"
|
||||
local unit_name="$2"
|
||||
local label="$3"
|
||||
local rendered_path
|
||||
local install_status=0
|
||||
shift 3
|
||||
|
||||
if [[ ! -f "${source_path}" ]]; then
|
||||
echo "[production-api-deploy] 发布产物缺少${label}: ${source_path}" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$#" -eq 0 || $(( $# % 2 )) -ne 0 ]]; then
|
||||
echo "[production-api-deploy] ${label} 缺少成对的模板路径和实际路径。" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
RENDERED_SYSTEMD_UNIT_FILE="$(mktemp)"
|
||||
rendered_path="${RENDERED_SYSTEMD_UNIT_FILE}"
|
||||
if ! python3 - "${source_path}" "${rendered_path}" "$@" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_path = Path(sys.argv[1])
|
||||
rendered_path = Path(sys.argv[2])
|
||||
replacements = sys.argv[3:]
|
||||
content = source_path.read_text(encoding="utf-8")
|
||||
replacement_map = dict(zip(replacements[::2], replacements[1::2]))
|
||||
|
||||
for template_path in replacement_map:
|
||||
if template_path not in content:
|
||||
raise SystemExit(f"systemd unit 模板缺少占位路径: {template_path}")
|
||||
|
||||
pattern = re.compile("|".join(re.escape(path) for path in replacement_map))
|
||||
content = pattern.sub(lambda match: replacement_map[match.group(0)], content)
|
||||
|
||||
rendered_path.write_text(content, encoding="utf-8")
|
||||
PY
|
||||
then
|
||||
cleanup_rendered_systemd_unit
|
||||
return 1
|
||||
fi
|
||||
|
||||
install_release_systemd_unit "${rendered_path}" "${unit_name}" "${label}" || install_status=$?
|
||||
cleanup_rendered_systemd_unit
|
||||
return "${install_status}"
|
||||
}
|
||||
|
||||
install_worker_systemd_units() {
|
||||
local release_dir="$1"
|
||||
local pattern="$2"
|
||||
local controller_service="$3"
|
||||
local bgfilter_service="$4"
|
||||
local current_link="$5"
|
||||
local api_env_file="$6"
|
||||
local worker_env_file="$7"
|
||||
local controller_env_file="$8"
|
||||
local bgfilter_env_file="$9"
|
||||
local installed_any=0
|
||||
|
||||
if [[ "${bgfilter_service}" == "genarrative-bgfilter-worker.service" ]]; then
|
||||
install_release_systemd_unit \
|
||||
render_and_install_release_systemd_unit \
|
||||
"${release_dir}/deploy/systemd/genarrative-bgfilter-worker.service" \
|
||||
"genarrative-bgfilter-worker.service" \
|
||||
"BgFilter worker systemd 单元"
|
||||
"BgFilter worker systemd 单元" \
|
||||
"/opt/genarrative/current" "${current_link}" \
|
||||
"/etc/genarrative/api-server.env" "${api_env_file}" \
|
||||
"/etc/genarrative/bgfilter-worker.env" "${bgfilter_env_file}"
|
||||
installed_any=1
|
||||
fi
|
||||
|
||||
if [[ "${pattern}" == "genarrative-external-generation-worker@*.service" ]]; then
|
||||
install_release_systemd_unit \
|
||||
render_and_install_release_systemd_unit \
|
||||
"${release_dir}/deploy/systemd/genarrative-external-generation-worker@.service" \
|
||||
"genarrative-external-generation-worker@.service" \
|
||||
"外部生成 worker systemd 模板"
|
||||
"外部生成 worker systemd 模板" \
|
||||
"/opt/genarrative/current" "${current_link}" \
|
||||
"/etc/genarrative/api-server.env" "${api_env_file}" \
|
||||
"/etc/genarrative/external-generation-worker.env" "${worker_env_file}"
|
||||
installed_any=1
|
||||
fi
|
||||
|
||||
if [[ "${controller_service}" == "genarrative-external-generation-controller.service" ]]; then
|
||||
install_release_systemd_unit \
|
||||
render_and_install_release_systemd_unit \
|
||||
"${release_dir}/deploy/systemd/genarrative-external-generation-controller.service" \
|
||||
"genarrative-external-generation-controller.service" \
|
||||
"外部生成 worker controller systemd 单元"
|
||||
"外部生成 worker controller systemd 单元" \
|
||||
"/opt/genarrative/current" "${current_link}" \
|
||||
"/etc/genarrative/api-server.env" "${api_env_file}" \
|
||||
"/etc/genarrative/external-generation-controller.env" "${controller_env_file}"
|
||||
installed_any=1
|
||||
fi
|
||||
|
||||
@@ -1066,6 +1135,7 @@ SERVICE_NAME="genarrative-api.service"
|
||||
PINGORA_SERVICE_NAME="genarrative-pingora-gateway.service"
|
||||
WORKER_SERVICE_PATTERN="genarrative-external-generation-worker@*.service"
|
||||
WORKER_CONTROLLER_SERVICE="genarrative-external-generation-controller.service"
|
||||
CONTROLLER_ENV_FILE="/etc/genarrative/external-generation-controller.env"
|
||||
BGFILTER_WORKER_SERVICE="genarrative-bgfilter-worker.service"
|
||||
BGFILTER_WORKER_HEALTH_URL="http://127.0.0.1:8083/readyz"
|
||||
BGFILTER_WORKER_ENV_FILE="/etc/genarrative/bgfilter-worker.env"
|
||||
@@ -1083,6 +1153,7 @@ MAINTENANCE_FILE="${GENARRATIVE_MAINTENANCE_FILE:-/var/lib/genarrative/maintenan
|
||||
CURRENT_LINK_SWITCHED=0
|
||||
RELEASE_DIR=""
|
||||
STAGING_RELEASE_DIR=""
|
||||
RENDERED_SYSTEMD_UNIT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -1134,6 +1205,10 @@ while [[ $# -gt 0 ]]; do
|
||||
WORKER_CONTROLLER_SERVICE="${2:?缺少 --worker-controller-service 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--controller-env-file)
|
||||
CONTROLLER_ENV_FILE="${2:?缺少 --controller-env-file 的值}"
|
||||
shift 2
|
||||
;;
|
||||
--no-worker-controller)
|
||||
WORKER_CONTROLLER_SERVICE=""
|
||||
shift
|
||||
@@ -1190,6 +1265,9 @@ require_absolute_path "${API_ENV_FILE}" "--api-env-file"
|
||||
if [[ -n "${WORKER_ENV_FILE}" ]]; then
|
||||
require_absolute_path "${WORKER_ENV_FILE}" "--worker-env-file"
|
||||
fi
|
||||
if [[ -n "${CONTROLLER_ENV_FILE}" ]]; then
|
||||
require_absolute_path "${CONTROLLER_ENV_FILE}" "--controller-env-file"
|
||||
fi
|
||||
if [[ -n "${BGFILTER_WORKER_ENV_FILE}" ]]; then
|
||||
require_absolute_path "${BGFILTER_WORKER_ENV_FILE}" "--bgfilter-worker-env-file"
|
||||
fi
|
||||
@@ -1245,8 +1323,16 @@ cleanup_staging_release() {
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_rendered_systemd_unit() {
|
||||
if [[ -n "${RENDERED_SYSTEMD_UNIT_FILE:-}" && -f "${RENDERED_SYSTEMD_UNIT_FILE}" ]]; then
|
||||
rm -f "${RENDERED_SYSTEMD_UNIT_FILE}"
|
||||
fi
|
||||
RENDERED_SYSTEMD_UNIT_FILE=""
|
||||
}
|
||||
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
cleanup_rendered_systemd_unit
|
||||
if [[ "${exit_code}" -ne 0 && "${DEPLOY_COMPLETED}" -ne 1 ]]; then
|
||||
cleanup_staging_release
|
||||
if [[ "${MAINTENANCE_ENABLED_BY_DEPLOY}" -eq 1 && "${CURRENT_LINK_SWITCHED}" -ne 1 ]]; then
|
||||
@@ -1553,7 +1639,16 @@ if [[ "${PINGORA_INCLUDED}" -eq 1 ]]; then
|
||||
ensure_pingora_shadow_service "${PINGORA_SERVICE_NAME}" "${PINGORA_SHADOW_ENV_FILE}"
|
||||
fi
|
||||
|
||||
install_worker_systemd_units "${RELEASE_DIR}" "${WORKER_SERVICE_PATTERN}" "${WORKER_CONTROLLER_SERVICE}" "${BGFILTER_WORKER_SERVICE}"
|
||||
install_worker_systemd_units \
|
||||
"${RELEASE_DIR}" \
|
||||
"${WORKER_SERVICE_PATTERN}" \
|
||||
"${WORKER_CONTROLLER_SERVICE}" \
|
||||
"${BGFILTER_WORKER_SERVICE}" \
|
||||
"${CURRENT_LINK}" \
|
||||
"${API_ENV_FILE}" \
|
||||
"${WORKER_ENV_FILE}" \
|
||||
"${CONTROLLER_ENV_FILE}" \
|
||||
"${BGFILTER_WORKER_ENV_FILE}"
|
||||
|
||||
restart_and_wait_for_bgfilter_worker "${BGFILTER_WORKER_SERVICE}" "${BGFILTER_WORKER_HEALTH_URL}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user