diff --git a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md index fc1f92d1..6a317d95 100644 --- a/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md +++ b/docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md @@ -452,7 +452,8 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 发布脚本源码,默认使用 `origin/master` 最新 commit;上游构建触发时使用上游传入的实际构建 commit。 - 进入维护模式。 - 将 wasm 上传到生产实例。 -- 在生产实例本机执行 `spacetime publish -s local --bin-path spacetime_module.wasm `。 +- 在生产实例本机执行 `spacetime --root-dir=/stdb publish --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes`。 +- 发布动作默认以 `spacetimedb` 服务用户执行,避免 root 默认 CLI 身份对自托管数据库验签失败,也避免 root 写入 `/stdb/config` 造成后续服务用户启动权限错误。 - 成功后执行必要 smoke test。 - 成功后解除维护模式。 - 失败时保留维护模式并发邮件。 @@ -463,6 +464,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module - 并行执行 Web / API / Stdb 三条构建流水线。 - 并行构建阶段必须开启 fail-fast:任一构建流水线失败时,立即中断其他仍在执行的并行构建分支,本次全量编排不再继续进入发布阶段。 - 构建全部成功后,按顺序执行 Stdb publish、API deploy、Web deploy,并把同一个 `DEPLOY_TARGET` 透传给三条发布流水线。 +- Stdb publish 同时透传 `SPACETIME_SERVER_URL`、`SPACETIME_ROOT_DIR` 与 `SPACETIME_RUN_AS_USER`,默认分别为 `http://127.0.0.1:3101`、`/stdb`、`spacetimedb`。 - 每条下游构建都只消费自己的归档产物,不直接复用别的 workspace。 - 生产 Web 发布只处理 `web.tar.gz` 与 checksum,API 发布只处理 `api-server` 与 checksum,Stdb 发布只处理 `spacetime_module.wasm` 与 checksum。 diff --git a/jenkins/Jenkinsfile.production-full-build-and-deploy b/jenkins/Jenkinsfile.production-full-build-and-deploy index 1a6d7ccd..d3c50f4b 100644 --- a/jenkins/Jenkinsfile.production-full-build-and-deploy +++ b/jenkins/Jenkinsfile.production-full-build-and-deploy @@ -31,6 +31,8 @@ pipeline { booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database') string(name: 'SPACETIME_SERVER_URL', defaultValue: 'http://127.0.0.1:3101', description: 'Stdb 发布目标 URL;默认避开本机 Git/Web 使用的 3000 端口') + string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'Stdb 发布使用的 spacetime CLI root-dir') + string(name: 'SPACETIME_RUN_AS_USER', defaultValue: 'spacetimedb', description: 'Stdb 发布使用的本机用户') } stages { @@ -136,6 +138,8 @@ pipeline { string(name: 'NOTIFICATION_EMAILS', value: params.NOTIFICATION_EMAILS ?: ''), string(name: 'DATABASE', value: params.DATABASE), string(name: 'SPACETIME_SERVER_URL', value: params.SPACETIME_SERVER_URL ?: ''), + string(name: 'SPACETIME_ROOT_DIR', value: params.SPACETIME_ROOT_DIR ?: '/stdb'), + string(name: 'SPACETIME_RUN_AS_USER', value: params.SPACETIME_RUN_AS_USER ?: 'spacetimedb'), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET), booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT), string(name: 'BUILD_JOB_NAME', value: params.STDB_BUILD_JOB_NAME), diff --git a/jenkins/Jenkinsfile.production-stdb-module-publish b/jenkins/Jenkinsfile.production-stdb-module-publish index bd00bb5f..5aee3862 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-publish +++ b/jenkins/Jenkinsfile.production-stdb-module-publish @@ -23,6 +23,8 @@ pipeline { string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database') string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias') string(name: 'SPACETIME_SERVER_URL', defaultValue: 'http://127.0.0.1:3101', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER') + string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;需与自托管 spacetimedb.service 一致') + string(name: 'SPACETIME_RUN_AS_USER', defaultValue: 'spacetimedb', description: '执行 spacetime publish 的本机用户,默认使用自托管服务用户') booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '是否清空数据库后发布') } @@ -51,6 +53,14 @@ pipeline { if (!params.SPACETIME_SERVER?.trim() && !params.SPACETIME_SERVER_URL?.trim()) { error('SPACETIME_SERVER 与 SPACETIME_SERVER_URL 不能同时为空。') } + def spacetimeRootDir = params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb' + if (!(spacetimeRootDir ==~ /^\/(?!.*\.\.)[A-Za-z0-9._\/-]+$/)) { + error("SPACETIME_ROOT_DIR 必须是 Linux 绝对路径且不能包含 ..: ${spacetimeRootDir}") + } + def spacetimeRunAsUser = params.SPACETIME_RUN_AS_USER?.trim() + if (spacetimeRunAsUser && !(spacetimeRunAsUser ==~ /^[A-Za-z_][A-Za-z0-9_-]*$/)) { + error("SPACETIME_RUN_AS_USER 只能是本机用户名: ${spacetimeRunAsUser}") + } def spacetimeServerUrl = params.SPACETIME_SERVER_URL?.trim() if (spacetimeServerUrl && !(spacetimeServerUrl ==~ /^https?:\/\/[A-Za-z0-9._:-]+$/)) { error("SPACETIME_SERVER_URL 只能是 http(s) URL,且不能包含路径或 shell 特殊字符: ${spacetimeServerUrl}") @@ -111,6 +121,10 @@ pipeline { steps { script { def clearArg = params.CLEAR_DATABASE ? '--clear-database' : '' + def rootArg = "--root-dir \"${params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'}\"" + def runAsArg = params.SPACETIME_RUN_AS_USER?.trim() + ? "--run-as-user \"${params.SPACETIME_RUN_AS_USER.trim()}\"" + : '' def serverArg = params.SPACETIME_SERVER_URL?.trim() ? "--server-url \"${params.SPACETIME_SERVER_URL.trim()}\"" : "--server \"${params.SPACETIME_SERVER}\"" @@ -121,6 +135,8 @@ pipeline { scripts/deploy/production-stdb-publish.sh \\ --source-dir "build/${params.BUILD_VERSION}" \\ --database "${params.DATABASE}" \\ + ${rootArg} \\ + ${runAsArg} \\ ${serverArg} \\ ${clearArg} ' diff --git a/scripts/deploy/production-stdb-publish.sh b/scripts/deploy/production-stdb-publish.sh index 8ecb797e..c14028f9 100644 --- a/scripts/deploy/production-stdb-publish.sh +++ b/scripts/deploy/production-stdb-publish.sh @@ -5,11 +5,12 @@ set -euo pipefail usage() { cat <<'EOF' 用法: - ./scripts/deploy/production-stdb-publish.sh --source-dir build/ --database [--server-url http://127.0.0.1:3101] [--server local] [--clear-database] + ./scripts/deploy/production-stdb-publish.sh --source-dir build/ --database [--server-url http://127.0.0.1:3101] [--server local] [--root-dir /stdb] [--run-as-user spacetimedb] [--clear-database] 说明: 进入维护模式,校验 spacetime_module.wasm.sha256,并在生产实例本机执行 spacetime publish。 默认使用 http://127.0.0.1:3101,避免与部署机本机 Git/Web 服务的 3000 端口冲突。 + 默认使用 /stdb 作为 spacetime CLI root-dir,并以 spacetimedb 用户发布,避免 root CLI 身份污染自托管实例。 失败时保留维护模式。 EOF } @@ -38,8 +39,11 @@ SOURCE_DIR="" DATABASE="" SERVER_ALIAS="local" SERVER_URL="http://127.0.0.1:3101" +SPACETIME_ROOT_DIR="/stdb" +RUN_AS_USER="spacetimedb" CLEAR_DATABASE=0 DEPLOY_COMPLETED=0 +PUBLISH_TMP_DIR="" while [[ $# -gt 0 ]]; do case "$1" in @@ -64,6 +68,14 @@ while [[ $# -gt 0 ]]; do SERVER_URL="${2:?缺少 --server-url 的值}" shift 2 ;; + --root-dir) + SPACETIME_ROOT_DIR="${2:?缺少 --root-dir 的值}" + shift 2 + ;; + --run-as-user) + RUN_AS_USER="${2:?缺少 --run-as-user 的值}" + shift 2 + ;; --clear-database) CLEAR_DATABASE=1 shift @@ -80,6 +92,16 @@ require_argument "${SOURCE_DIR}" "--source-dir" require_argument "${DATABASE}" "--database" validate_spacetime_database_name "${DATABASE}" +if [[ ! "${SPACETIME_ROOT_DIR}" == /* || "${SPACETIME_ROOT_DIR}" == *".."* ]]; then + echo "[production-stdb-publish] --root-dir 必须是 Linux 绝对路径且不能包含 ..: ${SPACETIME_ROOT_DIR}" >&2 + exit 1 +fi + +if [[ -n "${RUN_AS_USER}" && ! "${RUN_AS_USER}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then + echo "[production-stdb-publish] --run-as-user 只能是本机用户名: ${RUN_AS_USER}" >&2 + exit 1 +fi + if [[ ! -d "${SOURCE_DIR}" ]]; then echo "[production-stdb-publish] 发布目录不存在: ${SOURCE_DIR}" >&2 exit 1 @@ -94,6 +116,9 @@ fi on_exit() { local exit_code=$? + if [[ -n "${PUBLISH_TMP_DIR}" && -d "${PUBLISH_TMP_DIR}" ]]; then + rm -rf "${PUBLISH_TMP_DIR}" + fi if [[ "${exit_code}" -ne 0 && "${DEPLOY_COMPLETED}" -ne 1 ]]; then echo "[production-stdb-publish] 发布失败,保持维护模式。" >&2 fi @@ -111,6 +136,7 @@ echo "[production-stdb-publish] 校验 wasm" ) PUBLISH_ARGS=( + --root-dir="${SPACETIME_ROOT_DIR}" publish "${DATABASE}" --bin-path "${SOURCE_DIR}/spacetime_module.wasm" @@ -128,11 +154,38 @@ if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then fi if [[ -n "${SERVER_URL}" ]]; then - echo "[production-stdb-publish] 发布 SpacetimeDB module: ${DATABASE} -> ${SERVER_URL}" + echo "[production-stdb-publish] 发布 SpacetimeDB module: ${DATABASE} -> ${SERVER_URL}, root=${SPACETIME_ROOT_DIR}" else - echo "[production-stdb-publish] 发布 SpacetimeDB module: ${DATABASE} -> ${SERVER_ALIAS}" + echo "[production-stdb-publish] 发布 SpacetimeDB module: ${DATABASE} -> ${SERVER_ALIAS}, root=${SPACETIME_ROOT_DIR}" +fi + +if [[ -n "${RUN_AS_USER}" && "$(id -u)" -eq 0 ]]; then + if ! id "${RUN_AS_USER}" >/dev/null 2>&1; then + echo "[production-stdb-publish] 发布用户不存在: ${RUN_AS_USER}" >&2 + exit 1 + fi + PUBLISH_TMP_DIR="$(mktemp -d /tmp/genarrative-stdb-publish.XXXXXX)" + install -m 0644 "${SOURCE_DIR}/spacetime_module.wasm" "${PUBLISH_TMP_DIR}/spacetime_module.wasm" + chown -R "${RUN_AS_USER}:${RUN_AS_USER}" "${PUBLISH_TMP_DIR}" + PUBLISH_ARGS=( + --root-dir="${SPACETIME_ROOT_DIR}" + publish + "${DATABASE}" + --bin-path "${PUBLISH_TMP_DIR}/spacetime_module.wasm" + --yes + ) + if [[ -n "${SERVER_URL}" ]]; then + PUBLISH_ARGS+=(--server "${SERVER_URL}") + else + PUBLISH_ARGS+=(--server "${SERVER_ALIAS}") + fi + if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then + PUBLISH_ARGS+=(--clear-database) + fi + runuser -u "${RUN_AS_USER}" -- spacetime "${PUBLISH_ARGS[@]}" +else + spacetime "${PUBLISH_ARGS[@]}" fi -spacetime "${PUBLISH_ARGS[@]}" "${SCRIPT_DIR}/maintenance-off.sh" DEPLOY_COMPLETED=1