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