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 99792d15..fa805051 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 @@ -7,7 +7,7 @@ 本方案为当前仓库补齐 3 条 Jenkins 流水线: 1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。 -2. `部署`:只负责把指定发布版本部署到 `/home/ubuntu/Genarrative-deploy/`,禁止人工直接点击执行。 +2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`,禁止人工直接点击执行。 3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成。 本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。 @@ -16,11 +16,13 @@ 1. 构建产物目录统一使用 `build/<版本号>/`。 2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION`。 -3. `部署` 流水线必须校验当前构建原因包含 `UpstreamCause`,没有上游触发则直接失败。 +3. `部署` 流水线必须校验当前构建原因包含上游触发 cause,没有上游触发则直接失败。 4. `部署` 流水线额外校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致。 5. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁。 6. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录。 -7. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause')` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截。 +7. `部署` 流水线读取触发原因时必须使用 `currentBuild.getBuildCauses(...)` 这类白名单方法,不能直接访问 `currentBuild.rawBuild`,否则会被 Jenkins Script Security 拦截。 +8. 由于 Jenkins Pipeline 的 `build` 步骤触发下游时,原因类型通常是 `org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause`,实现上需要同时兼容它和经典的 `hudson.model.Cause$UpstreamCause`,否则会把真实的上游触发误判成人工执行。 +9. 如果线上进程的启停必须经过 `sudo`,只允许 `start.sh` / `stop.sh` 这两个 hook 使用 `sudo -n` 执行,部署目录清空与文件覆盖仍保持普通权限。 ## 3. 节点与工作区要求 @@ -73,24 +75,27 @@ jenkins/Jenkinsfile.deploy 核心流程: -1. 校验触发原因必须是上游流水线,而不是人工点击。 +1. 校验触发原因必须是上游流水线,而不是人工点击;实现上同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause`。 2. 校验 `BUILD_VERSION`、`SOURCE_WORKSPACE_ROOT`、`DEPLOY_DIRECTORY` 非空。 3. 执行: ```bash scripts/jenkins-deploy-release.sh \ --source-dir /build/ \ - --deploy-dir /home/ubuntu/Genarrative-deploy + --deploy-dir /var/lib/jenkins/deploy/Genarrative \ + --hook-with-sudo ``` 脚本语义: 1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`。 -2. 直接清空部署目录中的全部旧文件。 -3. 将指定版本目录中的内容移动到部署目录。 +2. 只删除发布产物白名单中的旧文件,例如 `web/`、`api-server`、`spacetime_module.wasm`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md`。 +3. 将指定版本目录中的同名发布产物移动到部署目录。 4. 执行新版本 `start.sh`。 -这样可以满足你要求的“直接覆盖部署目录中的所有文件”。同时这也意味着部署目录内原有的 `.env`、`.env.local`、日志和本地 SpacetimeDB 数据都会被清掉,最终以构建产物中的文件为准。 +如果 `RUN_DEPLOY_HOOKS_WITH_SUDO=true`,第 1 步和第 4 步会改为 `sudo -n` 调用;这要求 Jenkins 运行用户提前配置免密 sudo,否则部署会直接失败,不会进入交互式密码提示。 + +这样可以满足“发布文件直接覆盖”的要求,同时保留部署目录里像 `spacetimedb-data/`、`logs/`、`run/` 这类运行态目录,不会因为部署被整体删除。发布白名单内的 `.env`、`.env.local` 仍会以构建产物中的文件为准。 ### 4.3 构建并部署 @@ -122,6 +127,7 @@ jenkins/Jenkinsfile.build-and-deploy 4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`。 如果当前 Jenkins 没有额外配置独立 Agent,而是直接在控制器自身执行任务,`AGENT_LABEL` 应填写 `built-in`。 +如果 Jenkins 进程以默认 `jenkins` 用户运行,部署目录建议直接放在 `/var/lib/jenkins/deploy/Genarrative` 这类 Jenkins 自有目录下,避免再依赖 `/home/ubuntu/*` 的额外写权限。 如果目标 Ubuntu 的 Jenkins `sh` 默认实际落到 `/bin/sh -> dash`,而流水线脚本又使用了 `set -euo pipefail`,则必须显式通过 `bash -lc` 执行命令,不能直接依赖 Jenkins 默认 `sh` 解释器。 其中仅 `部署` 流水线还需要: @@ -129,11 +135,22 @@ jenkins/Jenkinsfile.build-and-deploy 1. `SOURCE_WORKSPACE_ROOT` 2. `SOURCE_NODE_NAME` 3. `DEPLOY_DIRECTORY` -4. `EXPECTED_UPSTREAM_JOB` +4. `RUN_DEPLOY_HOOKS_WITH_SUDO` +5. `EXPECTED_UPSTREAM_JOB` 其中仅 `构建并部署` 流水线还需要: 1. `DEPLOY_JOB_NAME` +2. `RUN_DEPLOY_HOOKS_WITH_SUDO` + +如果你选择启用 `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 命名 diff --git a/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md b/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md index 724cf7fc..8acdf8f8 100644 --- a/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md +++ b/docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md @@ -104,7 +104,7 @@ npm run deploy:rust:remote 5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。 6. 把仓库根目录的 `.env` 与 `.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下。 7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*`、`/generated-*`、`/healthz` 反代到本包内的 `api-server`。 -8. 在目标目录写入 `start.sh` 与 `stop.sh`。 +8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先加载发布目录根部的 `.env`、`.env.local`,再回退到构建时通过 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件。 9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/ ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。 发布包结构: @@ -140,10 +140,12 @@ cd build/ ./stop.sh ``` +如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/`、`api-server`、`spacetime_module.wasm`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md` 等发布产物,不会删除部署目录中的 `spacetimedb-data/`、`logs/`、`run/` 这类运行态目录。 + 安全边界: 1. 构建脚本会把仓库根目录已有的 `.env`、`.env.local` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。 -2. 如果仓库根目录不存在 `.env` 或 `.env.local`,脚本会打印跳过日志,但不会因此失败。 +2. 如果仓库根目录不存在 `.env` 或 `.env.local`,脚本会打印跳过日志,但不会因此失败;此时 `start.sh` 仅使用构建时写入的默认值与运行时显式传入的环境变量。 3. `start.sh` 默认不清空 SpacetimeDB;只有显式执行 `./start.sh --clear-database` 才允许清库重发。 4. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm。 5. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。 diff --git a/jenkins/Jenkinsfile.build-and-deploy b/jenkins/Jenkinsfile.build-and-deploy index 47c256b7..88b66acd 100644 --- a/jenkins/Jenkinsfile.build-and-deploy +++ b/jenkins/Jenkinsfile.build-and-deploy @@ -12,7 +12,8 @@ pipeline { string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci') string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名') - string(name: 'DEPLOY_DIRECTORY', defaultValue: '/home/ubuntu/Genarrative-deploy', description: '固定部署目录') + string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录') + booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行') } stages { @@ -65,6 +66,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), + 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 be94c4f6..ce92e1b1 100644 --- a/jenkins/Jenkinsfile.deploy +++ b/jenkins/Jenkinsfile.deploy @@ -10,7 +10,8 @@ pipeline { string(name: 'SOURCE_NODE_NAME', defaultValue: '', description: '上游构建节点名') string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录') string(name: 'BUILD_VERSION', defaultValue: '', description: '待部署版本号') - string(name: 'DEPLOY_DIRECTORY', defaultValue: '/home/ubuntu/Genarrative-deploy', description: '固定部署目录') + string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录') + booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行') string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名') } @@ -22,13 +23,22 @@ pipeline { steps { script { - // 使用 RunWrapper 白名单方法读取触发原因,避免触发 Jenkins Script Security 审批。 - def upstreamCauses = currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause') - if (!upstreamCauses || upstreamCauses.isEmpty()) { + // Pipeline 的 build 步骤通常会把下游触发原因记录成 BuildUpstreamCause, + // 直接只查经典 UpstreamCause 会把真实的上游触发误判成“人工执行”。 + def pipelineUpstreamCauses = currentBuild.getBuildCauses('org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause') + def classicUpstreamCauses = currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause') + def upstreamCause = null + + if (pipelineUpstreamCauses && !pipelineUpstreamCauses.isEmpty()) { + upstreamCause = pipelineUpstreamCauses[0] + } else if (classicUpstreamCauses && !classicUpstreamCauses.isEmpty()) { + upstreamCause = classicUpstreamCauses[0] + } + + if (!upstreamCause) { error('部署流水线禁止人工直接执行,只允许由上游构建并部署流水线触发。') } - def upstreamCause = upstreamCauses[0] def actualUpstreamJob = upstreamCause?.upstreamProject ?: '' def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim() def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim() @@ -45,6 +55,10 @@ pipeline { error('SOURCE_NODE_NAME 不能为空。') } + if (!actualUpstreamJob?.trim()) { + error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。') + } + if (expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) { error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}") } @@ -70,10 +84,15 @@ pipeline { set -euo pipefail test -d "build/${params.BUILD_VERSION}" chmod +x scripts/jenkins-deploy-release.sh - # 只部署上游已构建好的版本目录,避免部署阶段再次构建产生漂移。 - ./scripts/jenkins-deploy-release.sh \ - --source-dir "build/${params.BUILD_VERSION}" \ + deploy_args=( + --source-dir "build/${params.BUILD_VERSION}" --deploy-dir "${params.DEPLOY_DIRECTORY}" + ) + if [[ "${params.RUN_DEPLOY_HOOKS_WITH_SUDO}" == "true" ]]; then + deploy_args+=(--hook-with-sudo) + fi + # 只部署上游已构建好的版本目录,避免部署阶段再次构建产生漂移。 + ./scripts/jenkins-deploy-release.sh "\${deploy_args[@]}" ' """ } diff --git a/scripts/deploy-rust-remote.sh b/scripts/deploy-rust-remote.sh index 2ef5ad3e..138a4809 100644 --- a/scripts/deploy-rust-remote.sh +++ b/scripts/deploy-rust-remote.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { - cat <<'EOF' + cat < 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS - --database SpacetimeDB database,默认 genarrative-dev - --api-port api-server 端口,默认 8082 - --web-port 静态网站端口,默认 3000 - --spacetime-port SpacetimeDB 端口,默认 3101 - --ssh-key 上传使用的 SSH 私钥,默认 ~\.ssh\dsk.pem - --remote 上传目标 SSH 主机,默认 ubuntu@82.157.175.59 - --remote-dir 上传目标目录,默认 /home/ubuntu/genarrative + --database SpacetimeDB database,默认 ${DATABASE} + --api-port api-server 端口,默认 ${API_PORT} + --web-port 静态网站端口,默认 ${WEB_PORT} + --spacetime-port SpacetimeDB 端口,默认 ${SPACETIME_PORT} + --ssh-key 上传使用的 SSH 私钥,默认 ${SSH_KEY} + --remote 上传目标 SSH 主机,默认 ${REMOTE_TARGET} + --remote-dir 上传目标目录,默认 ${REMOTE_DIR} --skip-upload 只生成本地发布包,不上传服务器 --skip-web-build 跳过 Vite 构建,仅用于调试 --skip-api-build 跳过 api-server 构建,仅用于调试 @@ -101,6 +101,17 @@ remote_shell_quote() { printf "'%s'" "$(printf "%s" "${value}" | sed "s/'/'\\\\''/g")" } +replace_placeholder_in_file() { + local file_path="$1" + local placeholder="$2" + local value="$3" + local escaped_value="${value//\\/\\\\}" + + escaped_value="${escaped_value//&/\\&}" + escaped_value="${escaped_value//|/\\|}" + sed -i "s|${placeholder}|${escaped_value}|g" "${file_path}" +} + SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" SERVER_RS_DIR="${REPO_ROOT}/server-rs" @@ -409,20 +420,25 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PID_DIR="${SCRIPT_DIR}/run" LOG_DIR="${SCRIPT_DIR}/logs" -SPACETIME_DATA_DIR="${GENARRATIVE_SPACETIME_DATA_DIR:-${SCRIPT_DIR}/spacetimedb-data}" -SPACETIME_HOST="${GENARRATIVE_SPACETIME_HOST:-127.0.0.1}" -SPACETIME_PORT="${GENARRATIVE_SPACETIME_PORT:-3101}" -SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_SERVER_URL:-http://${SPACETIME_HOST}:${SPACETIME_PORT}}" -SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_DATABASE:-genarrative-dev}" -API_HOST="${GENARRATIVE_API_HOST:-127.0.0.1}" -API_PORT="${GENARRATIVE_API_PORT:-8082}" -API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}" -WEB_HOST="${GENARRATIVE_WEB_HOST:-0.0.0.0}" -WEB_PORT="${GENARRATIVE_WEB_PORT:-3000}" CLEAR_DATABASE=0 cd "${SCRIPT_DIR}" +load_env_file() { + local env_file="$1" + + if [[ ! -f "${env_file}" ]]; then + return + fi + + echo "[start] 加载环境文件: ${env_file}" + set -a + # 发布包内环境文件由当前构建脚本生成,允许在启动时作为默认环境源加载。 + # shellcheck disable=SC1090 + source "${env_file}" + set +a +} + usage() { cat <<'EOF' 用法: @@ -454,6 +470,24 @@ while [[ $# -gt 0 ]]; do esac done +load_env_file "${SCRIPT_DIR}/.env" +load_env_file "${SCRIPT_DIR}/.env.local" + +SPACETIME_DATA_DIR="${GENARRATIVE_SPACETIME_DATA_DIR:-${SCRIPT_DIR}/spacetimedb-data}" +SPACETIME_HOST="${GENARRATIVE_SPACETIME_HOST:-__GENARRATIVE_DEFAULT_SPACETIME_HOST__}" +SPACETIME_PORT="${GENARRATIVE_SPACETIME_PORT:-__GENARRATIVE_DEFAULT_SPACETIME_PORT__}" +SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_SERVER_URL:-http://${SPACETIME_HOST}:${SPACETIME_PORT}}" +SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_DATABASE:-__GENARRATIVE_DEFAULT_SPACETIME_DATABASE__}" +API_HOST="${GENARRATIVE_API_HOST:-__GENARRATIVE_DEFAULT_API_HOST__}" +API_PORT="${GENARRATIVE_API_PORT:-__GENARRATIVE_DEFAULT_API_PORT__}" +API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}" +WEB_HOST="${GENARRATIVE_WEB_HOST:-__GENARRATIVE_DEFAULT_WEB_HOST__}" +WEB_PORT="${GENARRATIVE_WEB_PORT:-__GENARRATIVE_DEFAULT_WEB_PORT__}" + +# 日志默认落文件,显式关闭 ANSI 颜色码,避免控制字符写入 *.log。 +export NO_COLOR="${NO_COLOR:-1}" +export CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-never}" + require_command() { local command_name="$1" @@ -540,6 +574,14 @@ echo "[start] API: http://${API_HOST}:${API_PORT}" echo "[start] SpacetimeDB: ${SPACETIME_SERVER_URL}" START_SCRIPT +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_SPACETIME_HOST__" "${SPACETIME_HOST}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_SPACETIME_PORT__" "${SPACETIME_PORT}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_SPACETIME_DATABASE__" "${DATABASE}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_API_HOST__" "${API_HOST}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_API_PORT__" "${API_PORT}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_WEB_HOST__" "${WEB_HOST}" +replace_placeholder_in_file "${TARGET_DIR}/start.sh" "__GENARRATIVE_DEFAULT_WEB_PORT__" "${WEB_PORT}" + cat >"${TARGET_DIR}/stop.sh" <<'STOP_SCRIPT' #!/usr/bin/env bash @@ -611,6 +653,9 @@ cat >"${TARGET_DIR}/README.md" < 必填,待部署的发布目录,例如 build/123 - --deploy-dir 必填,固定部署目录,例如 /home/ubuntu/Genarrative-deploy + --deploy-dir 必填,固定部署目录,例如 /var/lib/jenkins/deploy/Genarrative + --hook-with-sudo 可选,仅对 start.sh/stop.sh 使用 sudo -n 执行 EOF } @@ -31,6 +32,18 @@ require_argument() { SOURCE_DIR="" DEPLOY_DIR="" +HOOK_WITH_SUDO="0" +DEPLOY_ITEMS=( + ".env" + ".env.local" + "README.md" + "api-server" + "spacetime_module.wasm" + "start.sh" + "stop.sh" + "web" + "web-server.mjs" +) while [[ $# -gt 0 ]]; do case "$1" in @@ -46,6 +59,10 @@ while [[ $# -gt 0 ]]; do DEPLOY_DIR="${2:?缺少 --deploy-dir 的值}" shift 2 ;; + --hook-with-sudo) + HOOK_WITH_SUDO="1" + shift + ;; *) echo "[jenkins-deploy] 未知参数: $1" >&2 usage >&2 @@ -57,6 +74,35 @@ done require_argument "${SOURCE_DIR}" "--source-dir" require_argument "${DEPLOY_DIR}" "--deploy-dir" +run_hook() { + local hook_dir="$1" + local hook_name="$2" + local hook_path="${hook_dir}/${hook_name}" + + if [[ ! -x "${hook_path}" ]]; then + echo "[jenkins-deploy] hook 不存在或不可执行: ${hook_path}" >&2 + exit 1 + fi + + # 仅在启停脚本阶段使用 sudo,文件清理与移动仍保持普通权限,避免放大授权范围。 + if [[ "${HOOK_WITH_SUDO}" == "1" ]]; then + echo "[jenkins-deploy] 使用 sudo 执行 ${hook_name}: ${hook_path}" + ( + cd "${hook_dir}" + sudo -n "${hook_path}" + ) || { + echo "[jenkins-deploy] sudo 执行 ${hook_name} 失败,请确认 jenkins 用户已配置免密 sudo 权限。" >&2 + exit 1 + } + return + fi + + ( + cd "${hook_dir}" + "./${hook_name}" + ) +} + if [[ ! -d "${SOURCE_DIR}" ]]; then echo "[jenkins-deploy] 发布目录不存在: ${SOURCE_DIR}" >&2 exit 1 @@ -73,26 +119,34 @@ fi if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}" - ( - cd "${DEPLOY_DIR}" - ./stop.sh - ) + run_hook "${DEPLOY_DIR}" "stop.sh" else echo "[jenkins-deploy] 部署目录无可执行 stop.sh,跳过停服" fi echo "[jenkins-deploy] 清空部署目录: ${DEPLOY_DIR}" -find "${DEPLOY_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + +for item in "${DEPLOY_ITEMS[@]}"; do + if [[ -e "${DEPLOY_DIR}/${item}" ]]; then + echo "[jenkins-deploy] 删除旧产物: ${DEPLOY_DIR}/${item}" + rm -rf "${DEPLOY_DIR:?}/${item}" + fi +done echo "[jenkins-deploy] 移动发布内容: ${SOURCE_DIR} -> ${DEPLOY_DIR}" -find "${SOURCE_DIR}" -mindepth 1 -maxdepth 1 -exec mv {} "${DEPLOY_DIR}/" \; +for item in "${DEPLOY_ITEMS[@]}"; do + if [[ -e "${SOURCE_DIR}/${item}" ]]; then + echo "[jenkins-deploy] 覆盖产物: ${item}" + mv "${SOURCE_DIR}/${item}" "${DEPLOY_DIR}/" + fi +done -chmod +x "${DEPLOY_DIR}/start.sh" "${DEPLOY_DIR}/stop.sh" +chmod +x "${DEPLOY_DIR}/start.sh" + +if [[ -f "${DEPLOY_DIR}/stop.sh" ]]; then + chmod +x "${DEPLOY_DIR}/stop.sh" +fi echo "[jenkins-deploy] 启动新版本: ${DEPLOY_DIR}" -( - cd "${DEPLOY_DIR}" - ./start.sh -) +run_hook "${DEPLOY_DIR}" "start.sh" echo "[jenkins-deploy] 完成" diff --git a/server-rs/crates/shared-logging/src/lib.rs b/server-rs/crates/shared-logging/src/lib.rs index 8d8ab925..a810340e 100644 --- a/server-rs/crates/shared-logging/src/lib.rs +++ b/server-rs/crates/shared-logging/src/lib.rs @@ -16,6 +16,7 @@ pub fn init_tracing(default_filter: &str) -> Result<(), io::Error> { fmt() .with_env_filter(env_filter) .with_target(true) + .with_ansi(false) .compact() .try_init() .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")))