From ea550de6a1227b2262de5bc98f2cace0d507f1d3 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 26 Apr 2026 22:44:04 +0800 Subject: [PATCH] chore: pass web port through jenkins deploy --- ..._RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md | 14 +++-- jenkins/Jenkinsfile.build-and-deploy | 23 +++++++- jenkins/Jenkinsfile.deploy | 22 +++++++ scripts/jenkins-deploy-release.sh | 58 ++++++++++++++++++- 4 files changed, 108 insertions(+), 9 deletions(-) diff --git a/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md b/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md index 271a80c5..d48609bc 100644 --- a/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md +++ b/docs/technical/JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md @@ -8,7 +8,7 @@ 1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。 2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`,允许人工按参数启动,并支持按参数决定是否清空 SpacetimeDB 数据。 -3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成;同时暴露 `WEB_PORT` 参数,默认把发布包 Web 端口写成 `80`,并透传是否清库。 +3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成;同时暴露 `WEB_PORT` 参数,默认把发布包 Web 端口写成 `25001`,并把同名端口参数继续透传给下游部署,部署阶段以该参数作为最终监听端口。 本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。 @@ -24,6 +24,7 @@ 8. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截。 9. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行。 10. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限。 +11. `WEB_PORT` 必须在 `构建并部署` 与 `部署` 两条流水线之间使用同名参数传递;部署脚本会把最终端口写入固定部署目录 `.env.local` 的 `GENARRATIVE_WEB_PORT`,避免 `sudo` 启动 hook 时环境变量被清理导致端口回退。 ## 3. 节点与工作区要求 @@ -85,6 +86,7 @@ jenkins/Jenkinsfile.deploy scripts/jenkins-deploy-release.sh \ --source-dir /build/ \ --deploy-dir /var/lib/jenkins/deploy/Genarrative \ + --web-port \ [--clear-database] \ --hook-with-sudo ``` @@ -94,12 +96,12 @@ scripts/jenkins-deploy-release.sh \ 1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`。 2. 只删除发布产物白名单中的旧文件,例如 `web/`、`api-server`、`spacetime_module.wasm`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md`。 3. 将指定版本目录中的同名发布产物移动到部署目录。 -4. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c always`。 +4. 如果 `CLEAR_DATABASE=true`,部署脚本会以 `./start.sh --clear-database` 启动新版本;这样发布阶段的 `spacetime publish` 会追加 `-c=on-conflict`。 5. 执行新版本 `start.sh`。 如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,第 1 步和第 4 步会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo,否则部署会直接失败,不会进入交互式密码提示。 -这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `spacetimedb-data/`、`logs/`、`run/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env`、`.env.local` 仍会以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令。 +这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `spacetimedb-data/`、`logs/`、`run/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env`、`.env.local` 会先以构建产物中的文件为准;部署脚本会在启动 hook 前移除这些环境文件中的 UTF-8 BOM 与 CRLF,并把 Jenkins 部署参数 `WEB_PORT` 写入 `.env.local` 的 `GENARRATIVE_WEB_PORT`,避免 `start.sh` 在 Bash 下把首行变量名误解析成命令,也避免端口配置只停留在上游构建阶段。 ### 4.3 构建并部署 @@ -115,12 +117,13 @@ jenkins/Jenkinsfile.build-and-deploy 2. 复用与 `构建` 相同的构建命令生成 `build//`。 3. 归档 `build//**`。 4. 记录当前 `NODE_NAME`、源码根目录、版本号。 -5. 构建时额外透传 `--web-port `,默认生成监听 `80` 的发布包。 +5. 构建时额外透传 `--web-port `,默认生成监听 `25001` 的发布包。 6. 触发 `部署` 流水线,并传递: - `BUILD_VERSION` - `SOURCE_WORKSPACE_ROOT` - `SOURCE_NODE_NAME` - `DEPLOY_DIRECTORY` + - `WEB_PORT` - `CLEAR_DATABASE` - `EXPECTED_UPSTREAM_JOB` @@ -132,7 +135,7 @@ jenkins/Jenkinsfile.build-and-deploy 2. `GENARRATIVE_WORKSPACE_ROOT`:源码根目录;为空时回退到 Jenkins 当前工作区。 3. `BUILD_VERSION`:发布版本号;为空时回退到 `BUILD_NUMBER`。 4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`。 -5. `WEB_PORT`:发布包内静态网站监听端口;`构建并部署` 默认值为 `80`。 +5. `WEB_PORT`:静态网站监听端口;`构建并部署` 默认值为 `25001`,并通过下游 `部署` 同名参数作为最终启动端口。 6. `CLEAR_DATABASE`:部署阶段是否清空 SpacetimeDB 数据后再发布 wasm;默认 `false`。 如果当前 Jenkins 没有额外配置独立 Agent,而是直接在控制器自身执行任务,`AGENT_LABEL` 应填写 `built-in`。 @@ -147,6 +150,7 @@ jenkins/Jenkinsfile.build-and-deploy 4. `CLEAR_DATABASE` 5. `RUN_DEPLOY_HOOKS_WITH_SUDO` 6. `EXPECTED_UPSTREAM_JOB` +7. `WEB_PORT` 其中仅 `构建并部署` 流水线还需要: diff --git a/jenkins/Jenkinsfile.build-and-deploy b/jenkins/Jenkinsfile.build-and-deploy index 7fb541d2..62554c07 100644 --- a/jenkins/Jenkinsfile.build-and-deploy +++ b/jenkins/Jenkinsfile.build-and-deploy @@ -10,7 +10,7 @@ pipeline { string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '构建节点标签') string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') - string(name: 'WEB_PORT', defaultValue: '80', description: '发布包内静态网站端口,默认 80') + string(name: 'WEB_PORT', defaultValue: '25001', description: '发布包内静态网站端口,默认 25001') booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm') booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名') @@ -30,6 +30,22 @@ pipeline { env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER // 允许 Jenkins Job 直接指定固定源码目录,未指定时回退到当前工作区。 env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd() + def webPort = params.WEB_PORT?.trim() + if (!webPort) { + error('WEB_PORT 不能为空。') + } + if (!(webPort ==~ /^[0-9]+$/)) { + error("WEB_PORT 必须是数字端口,当前值: ${webPort}") + } + if (webPort.length() > 5) { + error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}") + } + def parsedWebPort = webPort.toInteger() + if (parsedWebPort < 1 || parsedWebPort > 65535) { + error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}") + } + // 后续构建与下游部署都使用校验后的同一端口值,避免参数空格导致上下游不一致。 + env.EFFECTIVE_WEB_PORT = webPort // 记录当前构建节点名,部署阶段必须回到同一节点读取本地 build 目录。 env.SOURCE_NODE_NAME = env.NODE_NAME } @@ -57,8 +73,8 @@ pipeline { sh """ bash -lc ' set -euo pipefail - # 构建并部署流水线显式透传 Web 端口,确保部署包默认监听 80,同时允许 Jenkins 参数覆盖。 - npm run deploy:rust:remote -- --skip-upload --name "${env.EFFECTIVE_BUILD_VERSION}" --web-port "${params.WEB_PORT}" + # 构建并部署流水线显式透传 Web 端口,确保部署包默认监听 25001,同时允许 Jenkins 参数覆盖。 + npm run deploy:rust:remote -- --skip-upload --name "${env.EFFECTIVE_BUILD_VERSION}" --web-port "${env.EFFECTIVE_WEB_PORT}" test -d "build/${env.EFFECTIVE_BUILD_VERSION}" ' """ @@ -79,6 +95,7 @@ pipeline { string(name: 'SOURCE_WORKSPACE_ROOT', value: env.WORKSPACE_ROOT), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), string(name: 'DEPLOY_DIRECTORY', value: params.DEPLOY_DIRECTORY), + string(name: 'WEB_PORT', value: env.EFFECTIVE_WEB_PORT), booleanParam(name: 'CLEAR_DATABASE', value: params.CLEAR_DATABASE), booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', value: params.RUN_DEPLOY_HOOKS_WITH_SUDO), string(name: 'EXPECTED_UPSTREAM_JOB', value: env.JOB_NAME), diff --git a/jenkins/Jenkinsfile.deploy b/jenkins/Jenkinsfile.deploy index 6dfdafe0..c2e4e69b 100644 --- a/jenkins/Jenkinsfile.deploy +++ b/jenkins/Jenkinsfile.deploy @@ -11,6 +11,7 @@ pipeline { string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录') string(name: 'BUILD_VERSION', defaultValue: '', description: '待部署版本号') string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录') + string(name: 'WEB_PORT', defaultValue: '25001', description: '静态网站监听端口,默认 25001,上游构建并部署流水线会透传同名参数') booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm') booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行') string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名') @@ -53,6 +54,26 @@ pipeline { error('SOURCE_NODE_NAME 不能为空。') } + def webPort = params.WEB_PORT?.trim() + if (!webPort) { + error('WEB_PORT 不能为空。') + } + + if (!(webPort ==~ /^[0-9]+$/)) { + error("WEB_PORT 必须是数字端口,当前值: ${webPort}") + } + + if (webPort.length() > 5) { + error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}") + } + + def parsedWebPort = webPort.toInteger() + if (parsedWebPort < 1 || parsedWebPort > 65535) { + error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}") + } + // 部署脚本只接收校验后的端口值,避免手工参数前后空格传到 Bash。 + env.EFFECTIVE_WEB_PORT = webPort + if (upstreamCause && !actualUpstreamJob?.trim()) { error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。') } @@ -85,6 +106,7 @@ pipeline { deploy_args=( --source-dir "build/${params.BUILD_VERSION}" --deploy-dir "${params.DEPLOY_DIRECTORY}" + --web-port "${env.EFFECTIVE_WEB_PORT}" ) if [[ "${params.CLEAR_DATABASE}" == "true" ]]; then deploy_args+=(--clear-database) diff --git a/scripts/jenkins-deploy-release.sh b/scripts/jenkins-deploy-release.sh index eee0c71b..6b3314e9 100644 --- a/scripts/jenkins-deploy-release.sh +++ b/scripts/jenkins-deploy-release.sh @@ -5,7 +5,7 @@ set -euo pipefail usage() { cat <<'EOF' 用法: - ./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /var/lib/jenkins/deploy/Genarrative [--clear-database] [--hook-with-sudo] + ./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /var/lib/jenkins/deploy/Genarrative --web-port 25001 [--clear-database] [--hook-with-sudo] 说明: 1. 如果部署目录已有旧版本且存在 stop.sh,则先执行旧版本 stop.sh。 @@ -17,6 +17,7 @@ usage() { 参数: --source-dir 必填,待部署的发布目录,例如 build/123 --deploy-dir 必填,固定部署目录,例如 /var/lib/jenkins/deploy/Genarrative + --web-port 必填,本次部署后静态网站监听端口 --clear-database 可选,启动新版本时追加 --clear-database --hook-with-sudo 可选,仅对 start.sh/stop.sh 使用 sudo -n 执行 EOF @@ -32,6 +33,28 @@ require_argument() { fi } +validate_port() { + local value="$1" + local label="$2" + local numeric_value + + if [[ ! "${value}" =~ ^[0-9]+$ ]]; then + echo "[jenkins-deploy] ${label} 必须是数字端口: ${value}" >&2 + exit 1 + fi + + if ((${#value} > 5)); then + echo "[jenkins-deploy] ${label} 必须在 1-65535 之间: ${value}" >&2 + exit 1 + fi + + numeric_value=$((10#${value})) + if ((numeric_value < 1 || numeric_value > 65535)); then + echo "[jenkins-deploy] ${label} 必须在 1-65535 之间: ${value}" >&2 + exit 1 + fi +} + normalize_env_file() { local env_file="$1" local temp_file="${env_file}.tmp.$$" @@ -54,8 +77,34 @@ normalize_release_env_files() { normalize_env_file "${release_dir}/web/.env.local" } +write_env_override() { + local env_file="$1" + local key="$2" + local value="$3" + local temp_file="${env_file}.tmp.$$" + + mkdir -p "$(dirname "${env_file}")" + if [[ -f "${env_file}" ]]; then + # 先移除旧的同名变量,再追加 Jenkins 本次部署参数,确保 sudo 启动时也能被 start.sh 读取。 + awk -v target_key="${key}" ' + BEGIN { + pattern = "^[[:space:]]*(export[[:space:]]+)?" target_key "=" + } + $0 !~ pattern { + print + } + ' "${env_file}" >"${temp_file}" + else + : >"${temp_file}" + fi + + printf "%s=%s\n" "${key}" "${value}" >>"${temp_file}" + mv "${temp_file}" "${env_file}" +} + SOURCE_DIR="" DEPLOY_DIR="" +WEB_PORT="" CLEAR_DATABASE="0" HOOK_WITH_SUDO="0" DEPLOY_ITEMS=( @@ -84,6 +133,10 @@ while [[ $# -gt 0 ]]; do DEPLOY_DIR="${2:?缺少 --deploy-dir 的值}" shift 2 ;; + --web-port) + WEB_PORT="${2:?缺少 --web-port 的值}" + shift 2 + ;; --clear-database) CLEAR_DATABASE="1" shift @@ -102,6 +155,8 @@ done require_argument "${SOURCE_DIR}" "--source-dir" require_argument "${DEPLOY_DIR}" "--deploy-dir" +require_argument "${WEB_PORT}" "--web-port" +validate_port "${WEB_PORT}" "--web-port" run_hook() { local hook_dir="$1" @@ -179,6 +234,7 @@ if [[ -f "${DEPLOY_DIR}/stop.sh" ]]; then fi normalize_release_env_files "${DEPLOY_DIR}" +write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_WEB_PORT" "${WEB_PORT}" echo "[jenkins-deploy] 启动新版本: ${DEPLOY_DIR}" if [[ "${CLEAR_DATABASE}" == "1" ]]; then