# Jenkins Rust 构建与部署流水线方案 日期:`2026-04-23` 状态:历史方案,已被 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 中的 `jenkins/Jenkinsfile.production-*` 生产流水线替代。旧 Jenkinsfile 已从仓库删除,本文只保留旧本地目录部署链的设计背景和迁移参考,不能再作为新 Jenkins Job 的脚本路径来源。 ## 1. 目标 本方案为当前仓库补齐 3 条 Jenkins 流水线: 1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。 2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`,允许人工按参数启动,并支持按参数决定是否清空 SpacetimeDB 数据。 3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成;同时暴露 `WEB_PORT` 参数,默认把发布包 Web 端口写成 `25001`,并把同名端口参数继续透传给下游部署,部署阶段以该参数作为最终监听端口。 本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。 ## 2. 执行约束 1. 构建产物目录统一使用 `build/<版本号>/`。 2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION`。 3. 所有使用仓库源码的 Jenkins 流水线在实际执行脚本前必须先执行 `git reset --hard HEAD`,避免固定源码目录内的 Git 变更影响本次构建、部署或迁移操作;其中 `构建` 与 `构建并部署` 在实际构建前还必须执行 `git clean -fd` 清理未跟踪文件,不使用 `-x`,避免删除 `node_modules/` 等忽略目录后与 `RUN_NPM_CI=false` 冲突。 4. `构建并部署` 可选填写 `COMMIT_HASH`。留空时使用 Jenkins SCM 当前检出的提交;填写时只能是 7 到 40 位十六进制 commit hash,流水线会先按 SCM checkout 得到仓库,再尽量拉取 `origin` 全部分支引用、解析该 hash 并 detached checkout 到对应 commit 后构建。 5. `部署` 流水线允许人工启动;没有上游触发 cause 时按人工部署处理,不再直接失败。 6. `部署` 流水线仅在存在上游触发 cause 时校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致。 7. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁。 8. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录。 9. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截。 10. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行。 11. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限。 12. `WEB_PORT` 必须在 `构建并部署` 与 `部署` 两条流水线之间使用同名参数传递;部署脚本会把最终端口写入固定部署目录 `.env.local` 的 `GENARRATIVE_WEB_PORT`,避免 `sudo` 启动 hook 时环境变量被清理导致端口回退。 13. `DATABASE` 必须匹配 SpacetimeDB CLI 数据库名规则 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会被拒绝,否则 `spacetime publish` 会报 `invalid characters in database name`。 14. Jenkins 日志必须能看到构建参数中的 SpacetimeDB 发布数据库,以及 `start.sh` 最终加载环境文件后的运行时数据库、server 和 root-dir,避免 `.env.local` 覆盖默认值后无法判断实际发布目标。 15. `构建并部署` 流水线开头通过 `GENARRATIVE_TOOLS_PATH` 固定声明 Jenkins 用户下的 Node、Cargo、SpacetimeDB 常用安装目录:`/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin`,并显式保留 `/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`,避免覆盖系统路径导致 `sh` 步骤无法启动。 ## 3. 节点与工作区要求 这套方案依赖“本地目录发布”,因此有两个前提: 1. `构建并部署` 与 `部署` 必须落到同一台 Ubuntu Jenkins Agent,或者落到同一块共享文件系统。 2. `构建并部署` 触发 `部署` 时,必须把 `SOURCE_NODE_NAME` 和 `SOURCE_WORKSPACE_ROOT` 一并传下去。 仓库中提供的 Jenkinsfile 已按这个约束实现: 1. `构建` / `构建并部署` 在指定源码目录内 `checkout scm` 并生成 `build/<版本号>/`。 2. `构建并部署` 结束构建节点占用后,再触发 `部署`。 3. `部署` 优先按 `SOURCE_NODE_NAME` 调度到同名节点,再读取 `SOURCE_WORKSPACE_ROOT/build/<版本号>/`。 ## 4. 三条流水线定义 ### 4.1 构建 旧脚本路径:`jenkins/Jenkinsfile.build`。该文件当前仓库已不存在;生产构建改用 `jenkins/Jenkinsfile.production-web-build`、`jenkins/Jenkinsfile.production-api-build`、`jenkins/Jenkinsfile.production-stdb-module-build`。 核心流程: 1. `checkout scm` 后执行 `git reset --hard HEAD` 与 `git clean -fd` 清理工作区。 2. 可选执行 `npm ci`。 3. 在源码根目录执行: ```bash npm run deploy:rust:remote -- --skip-upload --name ``` 4. 校验 `build//` 存在。 5. 归档 `build//**` 作为 Jenkins 产物。 默认版本号: ```text BUILD_VERSION = Jenkins BUILD_NUMBER ``` ### 4.2 部署 旧脚本路径:`jenkins/Jenkinsfile.deploy`。该文件当前仓库已删除;生产发布改用 `jenkins/Jenkinsfile.production-web-deploy`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-stdb-module-publish`。 核心流程: 1. 读取触发原因;人工启动时跳过上游门禁,上游触发时同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause` 并继续校验上游作业名。 2. 校验 `BUILD_VERSION`、`SOURCE_WORKSPACE_ROOT`、`DEPLOY_DIRECTORY` 非空。 3. 在 `SOURCE_WORKSPACE_ROOT` 内执行 `git reset --hard HEAD`,确保部署脚本和构建产物选择不受本地改动影响。 4. 执行: ```bash scripts/jenkins-deploy-release.sh \ --source-dir /build/ \ --deploy-dir /var/lib/jenkins/deploy/Genarrative \ --web-port \ [--clear-database] \ [--no-migrate-on-conflict] \ [--migration-dir ] \ --hook-with-sudo ``` 脚本语义: 1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`。 2. 覆盖前如果旧部署目录存在 `migration-bootstrap-secret.txt`,先复制到 `deploy-state/migration-bootstrap-secret.previous.txt`,供新版本 `start.sh` 在 schema 冲突自动迁移时授权导出旧库。该文件属于 Jenkins 部署状态,不放入 `run/`,避免 `sudo` 启停脚本生成的 root 私有运行目录阻断后续部署写入;如果后续部署失败,部署脚本必须把该快照复制回部署目录根下的 `migration-bootstrap-secret.txt`,避免当前仍在运行的数据库丢失对应迁移引导密钥。 3. 只删除发布产物白名单中的旧文件,例如 `web/`、`api-server`、`spacetime_module.wasm`、`migration-bootstrap-secret.txt`、`scripts/`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md`。 4. 将指定版本目录中的同名发布产物复制到部署目录;文件产物使用普通复制,`web/`、`scripts/` 等目录产物必须递归复制。 5. 把 `WEB_PORT`、`MIGRATE_ON_CONFLICT`、`MIGRATION_DIRECTORY` 写入部署目录 `.env.local`,确保通过 sudo 执行 `start.sh` 时仍能读取 Jenkins 参数;启动前读取 `.env` 与 `.env.local` 中最终的 `GENARRATIVE_SPACETIME_DATABASE`,打印并校验其符合 SpacetimeDB 数据库名规则。Jenkins 参数 `MIGRATION_EXPORT_TOKEN` / `MIGRATION_IMPORT_TOKEN` 会分别写入 `GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN` / `GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN`;如果参数为空,部署目录已有同名变量时会尽量保留。 6. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不进入自动导出和回灌。 7. 执行新版本 `start.sh`;普通发布遇到 schema 冲突时,默认由发布包内迁移脚本自动导出旧库、清库发布新 wasm、导入回灌。 如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,旧版本 `stop.sh` 和新版本 `start.sh` 会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo,否则部署会直接失败,不会进入交互式密码提示。 这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `.spacetimedb/`、`logs/`、`run/`、`deploy-state/`、`database-migrations/` 这类运行态目录,不会因为部署被整体删除。`run/` 只承载 pid 等启停运行状态;`deploy-state/` 承载 Jenkins 覆盖部署前保存的旧迁移引导密钥,必须由 Jenkins 用户保持可写,并在部署失败时作为恢复源写回根目录 `migration-bootstrap-secret.txt`。发布白名单内的 `.env`、`.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF,并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local` 的 `GENARRATIVE_WEB_PORT`,把 `MIGRATE_ON_CONFLICT` 写入 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT`,把 `MIGRATION_DIRECTORY` 写入 `GENARRATIVE_SPACETIME_MIGRATION_DIR`,并在启动前输出最终 `GENARRATIVE_SPACETIME_DATABASE`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口、数据库名和迁移配置只停留在上游构建阶段。`start.sh` 会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到部署目录 `.spacetimedb/bin/current/spacetimedb-cli`,后续启动、探活和 root-dir 占用判定都使用部署目录内 `.spacetimedb/`,且不再额外设置 `--data-dir`,避免 Jenkins 机器全局 `spacetime login` 变化影响本地库更新;如遇 `403 Forbidden`,按 `SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md` 排查数据库所有者与 CLI 身份。 ### 4.3 构建并部署 旧脚本路径:`jenkins/Jenkinsfile.build-and-deploy`。该文件当前仓库已删除;全量编排改用 `jenkins/Jenkinsfile.production-full-build-and-deploy`,并按 Web / API / Stdb 并行构建、Stdb / API / Web 顺序发布执行。 核心流程: 1. `checkout scm` 后,如果 `COMMIT_HASH` 非空,则先拉取远端分支和 tag,解析该 hash 指向的 commit,并 detached checkout 到该提交。 2. 执行 `git reset --hard HEAD` 与 `git clean -fd` 清理工作区。 3. 复用与 `构建` 相同的构建命令生成 `build//`。 4. 归档 `build//**`。 5. 记录当前 `NODE_NAME`、源码根目录、版本号与实际构建 commit。 6. 构建时额外透传 `--web-port `,默认生成监听 `25001` 的发布包。 7. 构建日志会输出 `SpacetimeDB 发布数据库: `、`构建 commit: `,发布包启动日志会输出最终 `database/server/root-dir`。 8. 触发 `部署` 流水线,并传递: - `BUILD_VERSION` - `SOURCE_WORKSPACE_ROOT` - `SOURCE_NODE_NAME` - `DEPLOY_DIRECTORY` - `WEB_PORT` - `CLEAR_DATABASE` - `MIGRATE_ON_CONFLICT` - `MIGRATION_DIRECTORY` - `EXPECTED_UPSTREAM_JOB` ## 5. Jenkins 参数建议 三条流水线统一建议暴露以下参数: 1. `AGENT_LABEL`:默认执行节点标签。 2. `GENARRATIVE_WORKSPACE_ROOT`:源码根目录;为空时回退到 Jenkins 当前工作区。 3. `BUILD_VERSION`:发布版本号;为空时回退到 `BUILD_NUMBER`。 4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`。 5. `WEB_PORT`:静态网站监听端口;`构建并部署` 默认值为 `25001`,并通过下游 `部署` 同名参数作为最终启动端口。 6. `CLEAR_DATABASE`:部署阶段是否清空 SpacetimeDB 数据后再发布 wasm;默认 `false`。 7. `MIGRATE_ON_CONFLICT`:普通部署遇到 SpacetimeDB schema 冲突时是否自动导出、清库发布、导入回灌;默认 `true`。 8. `MIGRATION_DIRECTORY`:自动迁移 JSON 输出目录;留空时使用部署目录内 `database-migrations/`。 9. `MIGRATION_EXPORT_TOKEN`:可选,旧库已授权迁移操作员 token,只在 schema 冲突导出旧库时使用。 10. `MIGRATION_IMPORT_TOKEN`:可选,新库已授权迁移操作员 token,只在清库发布新 wasm 后导入回灌时使用。 如果当前 Jenkins 没有额外配置独立 Agent,而是直接在控制器自身执行任务,`AGENT_LABEL` 应填写 `built-in`。 如果 `node`、`cargo` 或 `spacetime` 安装在 Jenkins 用户目录下,`构建并部署` 已默认把 `/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin`、`/var/lib/jenkins/.cargo/bin`、`/var/lib/jenkins/.local/bin`、`/var/lib/jenkins/bin` 写入流水线 `PATH` 前缀;仍应确保这些目录和其中二进制文件对 Jenkins 运行用户可读可执行。 如果 Jenkins 进程以默认 `jenkins` 用户运行,部署目录建议直接放在 `/var/lib/jenkins/deploy/Genarrative` 这类 Jenkins 自有目录下,避免再依赖 `/home/ubuntu/*` 的额外写权限。 如果目标 Ubuntu 的 Jenkins `sh` 默认实际落到 `/bin/sh -> dash`,而流水线脚本又使用了 `set -euo pipefail`,则必须显式通过 `bash -lc` 执行命令,不能直接依赖 Jenkins 默认 `sh` 解释器。 其中仅 `部署` 流水线还需要: 1. `SOURCE_WORKSPACE_ROOT` 2. `SOURCE_NODE_NAME` 3. `DEPLOY_DIRECTORY` 4. `CLEAR_DATABASE` 5. `MIGRATE_ON_CONFLICT` 6. `MIGRATION_DIRECTORY` 7. `RUN_DEPLOY_HOOKS_WITH_SUDO` 8. `EXPECTED_UPSTREAM_JOB` 9. `WEB_PORT` 10. `MIGRATION_EXPORT_TOKEN` 11. `MIGRATION_IMPORT_TOKEN` 其中仅 `构建并部署` 流水线还需要: 1. `DEPLOY_JOB_NAME` 2. `RUN_DEPLOY_HOOKS_WITH_SUDO` 3. `WEB_PORT` 4. `CLEAR_DATABASE` 5. `MIGRATE_ON_CONFLICT` 6. `MIGRATION_DIRECTORY` 7. `MIGRATION_EXPORT_TOKEN` 8. `MIGRATION_IMPORT_TOKEN` 9. `DATABASE`:发布包默认数据库名,默认 `genarrative-pipeline-local-test`,必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`。 10. `COMMIT_HASH`:可选指定构建提交;如果目标 commit 不在 Jenkins 当前浅克隆历史中,流水线会尝试 unshallow,仍找不到时构建失败。 如果你选择启用 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,推荐提前在服务器上增加一份最小 sudoers 配置,例如: ```text jenkins ALL=(root) NOPASSWD: /var/lib/jenkins/deploy/Genarrative/start.sh jenkins ALL=(root) NOPASSWD: /var/lib/jenkins/deploy/Genarrative/stop.sh ``` 这样可以把提权范围收敛到固定部署目录下的启停脚本,而不是把整个部署流程都交给 `sudo`。 ## 6. 推荐 Job 命名 以下旧 Job 命名只保留为历史记录,不再创建或关联到仓库脚本: 1. `Genarrative-Build` 2. `Genarrative-Deploy` 3. `Genarrative-Build-And-Deploy` 同时给 `Genarrative-Deploy` 配置环境变量: ```text GENARRATIVE_ALLOWED_UPSTREAM_JOB=Genarrative-Build-And-Deploy ``` 如果 Job 在 Jenkins Folder 下,值应填写完整上游作业名,例如: ```text game/Genarrative-Build-And-Deploy ``` ## 7. 文件清单 本方案原对应 Jenkinsfile 已删除。生产发布链的当前文件清单见 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`;旧部署脚本如 `scripts/jenkins-deploy-release.sh` 只作为旧发布包链路参考,不再作为生产 Jenkins 入口。 ## 8. 风险与边界 1. 该方案依赖本地目录切换,不适用于“构建节点”和“部署节点”完全隔离且不共享文件系统的 Jenkins 架构。 2. 当前 `部署` 采取的是“覆盖固定部署目录”的方式,不包含版本回滚目录管理;如需保留完整历史版本,应在后续单独补一层 release/current 软链接结构。 3. 当前 `start.sh` / `stop.sh` 仍以发布包内脚本为准,不替代 `systemd`、`supervisor`、`nginx`、`tls` 与日志轮转治理。