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:
@@ -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 <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`。
|
||||
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 命名
|
||||
|
||||
|
||||
@@ -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/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
|
||||
|
||||
发布包结构:
|
||||
@@ -140,10 +140,12 @@ cd build/<timestamp>
|
||||
./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、日志轮转与守护进程配置。
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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[@]}"
|
||||
'
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
cat <<EOF
|
||||
用法:
|
||||
npm run deploy:rust:remote
|
||||
./scripts/deploy-rust-remote.sh --name 20260422-153000
|
||||
@@ -17,13 +17,13 @@ usage() {
|
||||
|
||||
常用参数:
|
||||
--name <folder-name> 指定 build 子目录名,默认使用当前时间 YYYYmmdd-HHMMSS
|
||||
--database <database> SpacetimeDB database,默认 genarrative-dev
|
||||
--api-port <port> api-server 端口,默认 8082
|
||||
--web-port <port> 静态网站端口,默认 3000
|
||||
--spacetime-port <port> SpacetimeDB 端口,默认 3101
|
||||
--ssh-key <path> 上传使用的 SSH 私钥,默认 ~\.ssh\dsk.pem
|
||||
--remote <user@host> 上传目标 SSH 主机,默认 ubuntu@82.157.175.59
|
||||
--remote-dir <path> 上传目标目录,默认 /home/ubuntu/genarrative
|
||||
--database <database> SpacetimeDB database,默认 ${DATABASE}
|
||||
--api-port <port> api-server 端口,默认 ${API_PORT}
|
||||
--web-port <port> 静态网站端口,默认 ${WEB_PORT}
|
||||
--spacetime-port <port> SpacetimeDB 端口,默认 ${SPACETIME_PORT}
|
||||
--ssh-key <path> 上传使用的 SSH 私钥,默认 ${SSH_KEY}
|
||||
--remote <user@host> 上传目标 SSH 主机,默认 ${REMOTE_TARGET}
|
||||
--remote-dir <path> 上传目标目录,默认 ${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" <<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_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
|
||||
- \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\`
|
||||
|
||||
@@ -5,17 +5,18 @@ set -euo pipefail
|
||||
usage() {
|
||||
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。
|
||||
2. 直接清空部署目录中的全部旧文件。
|
||||
3. 把指定发布目录中的内容移动到部署目录。
|
||||
2. 仅删除并替换发布产物文件,保留部署目录中的运行数据目录。
|
||||
3. 把指定发布目录中的内容覆盖到部署目录。
|
||||
4. 最后执行新版本 start.sh。
|
||||
|
||||
参数:
|
||||
--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
|
||||
}
|
||||
|
||||
@@ -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] 完成"
|
||||
|
||||
@@ -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}")))
|
||||
|
||||
Reference in New Issue
Block a user