Merge branch 'codex/backend-rewrite-spacetimedb' of http://82.157.175.59:3000/GenarrativeAI/Genarrative into codex/backend-rewrite-spacetimedb

This commit is contained in:
2026-04-23 06:01:18 +08:00
7 changed files with 193 additions and 53 deletions

View File

@@ -7,7 +7,7 @@
本方案为当前仓库补齐 3 条 Jenkins 流水线: 本方案为当前仓库补齐 3 条 Jenkins 流水线:
1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。 1. `构建`:只负责在仓库根目录执行 `npm run deploy:rust:remote -- --skip-upload`,生成发布包。
2. `部署`:只负责把指定发布版本部署到 `/home/ubuntu/Genarrative-deploy/`,禁止人工直接点击执行。 2. `部署`:只负责把指定发布版本部署到 `/var/lib/jenkins/deploy/Genarrative/`,禁止人工直接点击执行。
3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成。 3. `构建并部署`:先构建,再把构建出的版本号传给 `部署` 流水线并等待部署完成。
本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。 本次只补 Jenkins 编排与本地部署脚本,不改现有 Rust 发布包构建逻辑,不恢复旧 `server-node` 部署链。
@@ -16,11 +16,13 @@
1. 构建产物目录统一使用 `build/<版本号>/` 1. 构建产物目录统一使用 `build/<版本号>/`
2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION` 2. 默认使用 Jenkins `BUILD_NUMBER` 作为版本号,避免依赖时间戳;如有需要也允许显式传 `BUILD_VERSION`
3. `部署` 流水线必须校验当前构建原因包含 `UpstreamCause`,没有上游触发则直接失败。 3. `部署` 流水线必须校验当前构建原因包含上游触发 cause没有上游触发则直接失败。
4. `部署` 流水线额外校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致。 4. `部署` 流水线额外校验上游作业名与传入的 `EXPECTED_UPSTREAM_JOB` 一致;如配置了环境变量 `GENARRATIVE_ALLOWED_UPSTREAM_JOB`,还必须与该值一致。
5. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁。 5. `构建并部署` 在触发 `部署` 前先释放自己的构建节点,避免单执行器节点出现死锁。
6. `部署` 不重新构建,不重新上传,不从 Jenkins 插件仓库复制产物,直接使用上游构建节点的本地 `build/<版本号>/` 目录。 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. 节点与工作区要求 ## 3. 节点与工作区要求
@@ -73,24 +75,27 @@ jenkins/Jenkinsfile.deploy
核心流程: 核心流程:
1. 校验触发原因必须是上游流水线,而不是人工点击。 1. 校验触发原因必须是上游流水线,而不是人工点击;实现上同时兼容 `BuildUpstreamCause` 与经典 `UpstreamCause`
2. 校验 `BUILD_VERSION``SOURCE_WORKSPACE_ROOT``DEPLOY_DIRECTORY` 非空。 2. 校验 `BUILD_VERSION``SOURCE_WORKSPACE_ROOT``DEPLOY_DIRECTORY` 非空。
3. 执行: 3. 执行:
```bash ```bash
scripts/jenkins-deploy-release.sh \ scripts/jenkins-deploy-release.sh \
--source-dir <SOURCE_WORKSPACE_ROOT>/build/<BUILD_VERSION> \ --source-dir <SOURCE_WORKSPACE_ROOT>/build/<BUILD_VERSION> \
--deploy-dir /home/ubuntu/Genarrative-deploy --deploy-dir /var/lib/jenkins/deploy/Genarrative \
--hook-with-sudo
``` ```
脚本语义: 脚本语义:
1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh` 1. 若部署目录已有旧版本且存在 `stop.sh`,先执行旧版本 `stop.sh`
2. 直接清空部署目录中的全部旧文件 2. 只删除发布产物白名单中的旧文件,例如 `web/``api-server``spacetime_module.wasm``.env*``start.sh``stop.sh``web-server.mjs``README.md`
3. 将指定版本目录中的内容移动到部署目录。 3. 将指定版本目录中的同名发布产物移动到部署目录。
4. 执行新版本 `start.sh` 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 构建并部署 ### 4.3 构建并部署
@@ -122,6 +127,7 @@ jenkins/Jenkinsfile.build-and-deploy
4. `RUN_NPM_CI`:是否在构建前执行 `npm ci` 4. `RUN_NPM_CI`:是否在构建前执行 `npm ci`
如果当前 Jenkins 没有额外配置独立 Agent而是直接在控制器自身执行任务`AGENT_LABEL` 应填写 `built-in` 如果当前 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` 解释器。 如果目标 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` 1. `SOURCE_WORKSPACE_ROOT`
2. `SOURCE_NODE_NAME` 2. `SOURCE_NODE_NAME`
3. `DEPLOY_DIRECTORY` 3. `DEPLOY_DIRECTORY`
4. `EXPECTED_UPSTREAM_JOB` 4. `RUN_DEPLOY_HOOKS_WITH_SUDO`
5. `EXPECTED_UPSTREAM_JOB`
其中仅 `构建并部署` 流水线还需要: 其中仅 `构建并部署` 流水线还需要:
1. `DEPLOY_JOB_NAME` 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 命名 ## 6. 推荐 Job 命名

View File

@@ -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` 复制到目标目录。 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/` 下。 6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下。
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*``/generated-*``/healthz` 反代到本包内的 `api-server` 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/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。 9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
发布包结构: 发布包结构:
@@ -140,10 +140,12 @@ cd build/<timestamp>
./stop.sh ./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` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。 1. 构建脚本会把仓库根目录已有的 `.env``.env.local` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。
2. 如果仓库根目录不存在 `.env``.env.local`,脚本会打印跳过日志,但不会因此失败。 2. 如果仓库根目录不存在 `.env``.env.local`,脚本会打印跳过日志,但不会因此失败;此时 `start.sh` 仅使用构建时写入的默认值与运行时显式传入的环境变量
3. `start.sh` 默认不清空 SpacetimeDB只有显式执行 `./start.sh --clear-database` 才允许清库重发。 3. `start.sh` 默认不清空 SpacetimeDB只有显式执行 `./start.sh --clear-database` 才允许清库重发。
4. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm。 4. `start.sh` 使用 `spacetime publish --bin-path spacetime_module.wasm --yes` 发布当前包内 wasm。
5. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。 5. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。

View File

@@ -12,7 +12,8 @@ pipeline {
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER') string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci') booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci')
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名') 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 { stages {
@@ -65,6 +66,7 @@ pipeline {
string(name: 'SOURCE_WORKSPACE_ROOT', value: env.WORKSPACE_ROOT), string(name: 'SOURCE_WORKSPACE_ROOT', value: env.WORKSPACE_ROOT),
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
string(name: 'DEPLOY_DIRECTORY', value: params.DEPLOY_DIRECTORY), 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), string(name: 'EXPECTED_UPSTREAM_JOB', value: env.JOB_NAME),
] ]
} }

View File

@@ -10,7 +10,8 @@ pipeline {
string(name: 'SOURCE_NODE_NAME', defaultValue: '', description: '上游构建节点名') string(name: 'SOURCE_NODE_NAME', defaultValue: '', description: '上游构建节点名')
string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录') string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录')
string(name: 'BUILD_VERSION', 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: '允许触发本作业的上游作业名') string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名')
} }
@@ -22,13 +23,22 @@ pipeline {
steps { steps {
script { script {
// 使用 RunWrapper 白名单方法读取触发原因,避免触发 Jenkins Script Security 审批。 // Pipeline 的 build 步骤通常会把下游触发原因记录成 BuildUpstreamCause
def upstreamCauses = currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause') // 直接只查经典 UpstreamCause 会把真实的上游触发误判成“人工执行”。
if (!upstreamCauses || upstreamCauses.isEmpty()) { 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('部署流水线禁止人工直接执行,只允许由上游构建并部署流水线触发。') error('部署流水线禁止人工直接执行,只允许由上游构建并部署流水线触发。')
} }
def upstreamCause = upstreamCauses[0]
def actualUpstreamJob = upstreamCause?.upstreamProject ?: '' def actualUpstreamJob = upstreamCause?.upstreamProject ?: ''
def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim() def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim()
def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim() def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim()
@@ -45,6 +55,10 @@ pipeline {
error('SOURCE_NODE_NAME 不能为空。') error('SOURCE_NODE_NAME 不能为空。')
} }
if (!actualUpstreamJob?.trim()) {
error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。')
}
if (expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) { if (expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) {
error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}") error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}")
} }
@@ -70,10 +84,15 @@ pipeline {
set -euo pipefail set -euo pipefail
test -d "build/${params.BUILD_VERSION}" test -d "build/${params.BUILD_VERSION}"
chmod +x scripts/jenkins-deploy-release.sh chmod +x scripts/jenkins-deploy-release.sh
# 只部署上游已构建好的版本目录,避免部署阶段再次构建产生漂移。 deploy_args=(
./scripts/jenkins-deploy-release.sh \ --source-dir "build/${params.BUILD_VERSION}"
--source-dir "build/${params.BUILD_VERSION}" \
--deploy-dir "${params.DEPLOY_DIRECTORY}" --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[@]}"
' '
""" """
} }

View File

@@ -3,7 +3,7 @@
set -euo pipefail set -euo pipefail
usage() { usage() {
cat <<'EOF' cat <<EOF
用法: 用法:
npm run deploy:rust:remote npm run deploy:rust:remote
./scripts/deploy-rust-remote.sh --name 20260422-153000 ./scripts/deploy-rust-remote.sh --name 20260422-153000
@@ -17,13 +17,13 @@ usage() {
常用参数: 常用参数:
--name <folder-name> 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS --name <folder-name> 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS
--database <database> SpacetimeDB database默认 genarrative-dev --database <database> SpacetimeDB database默认 ${DATABASE}
--api-port <port> api-server 端口,默认 8082 --api-port <port> api-server 端口,默认 ${API_PORT}
--web-port <port> 静态网站端口,默认 3000 --web-port <port> 静态网站端口,默认 ${WEB_PORT}
--spacetime-port <port> SpacetimeDB 端口,默认 3101 --spacetime-port <port> SpacetimeDB 端口,默认 ${SPACETIME_PORT}
--ssh-key <path> 上传使用的 SSH 私钥,默认 ~\.ssh\dsk.pem --ssh-key <path> 上传使用的 SSH 私钥,默认 ${SSH_KEY}
--remote <user@host> 上传目标 SSH 主机,默认 ubuntu@82.157.175.59 --remote <user@host> 上传目标 SSH 主机,默认 ${REMOTE_TARGET}
--remote-dir <path> 上传目标目录,默认 /home/ubuntu/genarrative --remote-dir <path> 上传目标目录,默认 ${REMOTE_DIR}
--skip-upload 只生成本地发布包,不上传服务器 --skip-upload 只生成本地发布包,不上传服务器
--skip-web-build 跳过 Vite 构建,仅用于调试 --skip-web-build 跳过 Vite 构建,仅用于调试
--skip-api-build 跳过 api-server 构建,仅用于调试 --skip-api-build 跳过 api-server 构建,仅用于调试
@@ -101,6 +101,17 @@ remote_shell_quote() {
printf "'%s'" "$(printf "%s" "${value}" | sed "s/'/'\\\\''/g")" 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)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
SERVER_RS_DIR="${REPO_ROOT}/server-rs" SERVER_RS_DIR="${REPO_ROOT}/server-rs"
@@ -409,20 +420,25 @@ set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PID_DIR="${SCRIPT_DIR}/run" PID_DIR="${SCRIPT_DIR}/run"
LOG_DIR="${SCRIPT_DIR}/logs" 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 CLEAR_DATABASE=0
cd "${SCRIPT_DIR}" 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() { usage() {
cat <<'EOF' cat <<'EOF'
用法: 用法:
@@ -454,6 +470,24 @@ while [[ $# -gt 0 ]]; do
esac esac
done 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() { require_command() {
local command_name="$1" local command_name="$1"
@@ -540,6 +574,14 @@ echo "[start] API: http://${API_HOST}:${API_PORT}"
echo "[start] SpacetimeDB: ${SPACETIME_SERVER_URL}" echo "[start] SpacetimeDB: ${SPACETIME_SERVER_URL}"
START_SCRIPT 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' cat >"${TARGET_DIR}/stop.sh" <<'STOP_SCRIPT'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -611,6 +653,9 @@ cat >"${TARGET_DIR}/README.md" <<EOF
## 环境变量 ## 环境变量
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
- 脚本内默认值来自构建时的 `--database`、`--api-port`、`--web-port`、`--spacetime-host`、`--spacetime-port` 参数。
- 默认导出 \`NO_COLOR=1\` 与 \`CARGO_TERM_COLOR=never\`,避免 ANSI 颜色控制码写入日志文件;如确有需要可在启动前显式覆盖。
- \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\` - \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\`
- \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\` - \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
- \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\` - \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\`

View File

@@ -5,17 +5,18 @@ set -euo pipefail
usage() { usage() {
cat <<'EOF' cat <<'EOF'
用法: 用法:
./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /home/ubuntu/Genarrative-deploy ./scripts/jenkins-deploy-release.sh --source-dir /path/to/build/123 --deploy-dir /var/lib/jenkins/deploy/Genarrative [--hook-with-sudo]
说明: 说明:
1. 如果部署目录已有旧版本且存在 stop.sh则先执行旧版本 stop.sh。 1. 如果部署目录已有旧版本且存在 stop.sh则先执行旧版本 stop.sh。
2. 直接清空部署目录中的全部旧文件 2. 仅删除并替换发布产物文件,保留部署目录中的运行数据目录
3. 把指定发布目录中的内容移动到部署目录。 3. 把指定发布目录中的内容覆盖到部署目录。
4. 最后执行新版本 start.sh。 4. 最后执行新版本 start.sh。
参数: 参数:
--source-dir <path> 必填,待部署的发布目录,例如 build/123 --source-dir <path> 必填,待部署的发布目录,例如 build/123
--deploy-dir <path> 必填,固定部署目录,例如 /home/ubuntu/Genarrative-deploy --deploy-dir <path> 必填,固定部署目录,例如 /var/lib/jenkins/deploy/Genarrative
--hook-with-sudo 可选,仅对 start.sh/stop.sh 使用 sudo -n 执行
EOF EOF
} }
@@ -31,6 +32,18 @@ require_argument() {
SOURCE_DIR="" SOURCE_DIR=""
DEPLOY_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 while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@@ -46,6 +59,10 @@ while [[ $# -gt 0 ]]; do
DEPLOY_DIR="${2:?缺少 --deploy-dir 的值}" DEPLOY_DIR="${2:?缺少 --deploy-dir 的值}"
shift 2 shift 2
;; ;;
--hook-with-sudo)
HOOK_WITH_SUDO="1"
shift
;;
*) *)
echo "[jenkins-deploy] 未知参数: $1" >&2 echo "[jenkins-deploy] 未知参数: $1" >&2
usage >&2 usage >&2
@@ -57,6 +74,35 @@ done
require_argument "${SOURCE_DIR}" "--source-dir" require_argument "${SOURCE_DIR}" "--source-dir"
require_argument "${DEPLOY_DIR}" "--deploy-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 if [[ ! -d "${SOURCE_DIR}" ]]; then
echo "[jenkins-deploy] 发布目录不存在: ${SOURCE_DIR}" >&2 echo "[jenkins-deploy] 发布目录不存在: ${SOURCE_DIR}" >&2
exit 1 exit 1
@@ -73,26 +119,34 @@ fi
if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then
echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}" echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}"
( run_hook "${DEPLOY_DIR}" "stop.sh"
cd "${DEPLOY_DIR}"
./stop.sh
)
else else
echo "[jenkins-deploy] 部署目录无可执行 stop.sh跳过停服" echo "[jenkins-deploy] 部署目录无可执行 stop.sh跳过停服"
fi fi
echo "[jenkins-deploy] 清空部署目录: ${DEPLOY_DIR}" 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}" 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}" echo "[jenkins-deploy] 启动新版本: ${DEPLOY_DIR}"
( run_hook "${DEPLOY_DIR}" "start.sh"
cd "${DEPLOY_DIR}"
./start.sh
)
echo "[jenkins-deploy] 完成" echo "[jenkins-deploy] 完成"

View File

@@ -16,6 +16,7 @@ pub fn init_tracing(default_filter: &str) -> Result<(), io::Error> {
fmt() fmt()
.with_env_filter(env_filter) .with_env_filter(env_filter)
.with_target(true) .with_target(true)
.with_ansi(false)
.compact() .compact()
.try_init() .try_init()
.map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}"))) .map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")))