Harden production publish flow #5

Merged
kdletters merged 26 commits from codex/publish-flow into master 2026-05-03 03:47:14 +08:00
3 changed files with 116 additions and 1 deletions
Showing only changes of commit cc38057c3c - Show all commits

View File

@@ -392,6 +392,7 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
- 可选安装 Nginx 配置和维护模式 snippet。
- 安装 Nginx 配置时执行 `nginx -t`,通过后必须执行 `nginx -s reload`,确保新配置对当前 Nginx master/worker 生效。
- 启用并启动 `spacetimedb.service``genarrative-api.service`;重启 `spacetimedb.service` 后必须等待 `http://127.0.0.1:3101/v1/ping` 确认就绪,不能只依赖 `systemctl restart` 的返回码。`<SPACETIME_ROOT>` 下所有运行态文件必须归属 `spacetimedb:spacetimedb`。不要在 root 身份下对同一个 `<SPACETIME_ROOT>` 执行 `spacetime --root-dir=<SPACETIME_ROOT> server ping`,否则会生成 root-owned CLI 配置,导致 `spacetimedb` 服务用户后续启动时遇到权限错误。
- 首次初始化时,如果 `/etc/genarrative/api-server.env` 里还没有 `GENARRATIVE_SPACETIME_TOKEN`,流水线会在 `spacetimedb.service` 就绪后调用本机 `POST http://127.0.0.1:3101/v1/identity` 生成 client identity/token只把 token 写入环境文件,并只在日志里显示 identity 前缀。随后流水线会以 `spacetimedb` 用户执行 `<SPACETIME_ROOT>/bin/current/spacetimedb-cli --root-dir <SPACETIME_ROOT> login --token [REDACTED]`,确保后续首次 `Stdb publish` 使用同一个 client identity 创建数据库;这个 identity 才会成为后台读取 private 表所需的 owner。若环境文件已有 `GENARRATIVE_SPACETIME_TOKEN`,初始化必须保留该值,只同步 CLI 登录态,不重新生成或覆盖。
该流水线属于高风险操作,默认要求人工确认后执行。
已落地的 Jenkinsfile 为 `jenkins/Jenkinsfile.production-server-provision`。该流水线默认 `DRY_RUN=true`只打印将执行的初始化动作真正写入系统用户、目录、systemd、环境文件并启动服务时必须设置 `DRY_RUN=false` 且勾选 `CONFIRM_PROVISION`。当 `DEPLOY_TARGET=release` 时,还必须勾选 `CONFIRM_RELEASE_DEPLOY_AGENT`,并通过 `linux && genarrative-release-deploy` 调度到独立 release 部署 agent。
@@ -452,8 +453,9 @@ WASM_SOURCE="${CARGO_TARGET_DIR}/wasm32-unknown-unknown/release/spacetime_module
- 先按 `SOURCE_BRANCH` / `COMMIT_HASH` 解析并 checkout 发布脚本源码,默认使用 `origin/master` 最新 commit上游构建触发时使用上游传入的实际构建 commit。
- 进入维护模式。
- 将 wasm 上传到生产实例。
- 在生产实例本机执行 `spacetime --root-dir=/stdb publish <database-name> --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes`
- 在生产实例本机执行 `spacetime --root-dir=/stdb publish <database-name> --server http://127.0.0.1:3101 --bin-path spacetime_module.wasm --yes --no-config`
- 发布动作默认以 `spacetimedb` 服务用户执行,避免 root 默认 CLI 身份对自托管数据库验签失败,也避免 root 写入 `/stdb/config` 造成后续服务用户启动权限错误。
- `Stdb publish` 固定追加 `--no-config`,只依赖显式传入的 `--root-dir``--server``--bin-path` 与数据库名,避免 agent 工作区、本机用户目录或仓库内 `spacetime` 配置干扰发布目标。
- 成功后执行必要 smoke test。
- 成功后解除维护模式。
- 失败时保留维护模式并发邮件。

View File

@@ -284,6 +284,115 @@ pipeline {
exit 1
}
read_env_value() {
local file="$1"
local key="$2"
local line value
if [[ ! -f "${file}" ]]; then
return
fi
while IFS= read -r line || [[ -n "${line}" ]]; do
if [[ "${line}" == "${key}="* ]]; then
value="${line#*=}"
value="$(printf "%s" "${value}" | tr -d "\\r")"
if [[ "${value}" == \"* && "${value}" == *\" ]]; then
value="${value#\"}"
value="${value%\"}"
fi
printf "%s" "${value}"
return
fi
done <"${file}"
}
write_env_value() {
local file="$1"
local key="$2"
local value="$3"
local tmp updated line
tmp="$(mktemp)"
updated="false"
while IFS= read -r line || [[ -n "${line}" ]]; do
if [[ "${line}" == "${key}="* ]]; then
if [[ "${updated}" != "true" ]]; then
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
updated="true"
fi
else
printf "%s\\n" "${line}" >>"${tmp}"
fi
done <"${file}"
if [[ "${updated}" != "true" ]]; then
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
fi
cat "${tmp}" >"${file}"
rm -f "${tmp}"
chmod 0600 "${file}"
chown root:root "${file}"
}
parse_json_string_field() {
local json="$1"
local key="$2"
printf "%s" "${json}" | sed -n "s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\([^\\\"]*\\)\\\".*/\\1/p" | head -n 1
}
ensure_spacetime_owner_client_token() {
local server_url="http://127.0.0.1:3101"
local cli_path="${SPACETIME_ROOT}/bin/current/spacetimedb-cli"
local token identity response login_output existing_token identity_preview
if [[ "${DRY_RUN}" == "true" ]]; then
echo "+ ensure GENARRATIVE_SPACETIME_TOKEN in ${API_ENV_FILE}"
echo "+ generate SpacetimeDB client identity when token is missing"
echo "+ runuser -u spacetimedb -- ${cli_path} --root-dir ${SPACETIME_ROOT} login --token [REDACTED]"
return
fi
if [[ ! -f "${API_ENV_FILE}" ]]; then
echo "[server-provision] 环境文件不存在,无法写入 GENARRATIVE_SPACETIME_TOKEN: ${API_ENV_FILE}" >&2
exit 1
fi
if [[ ! -x "${cli_path}" ]]; then
echo "[server-provision] SpacetimeDB CLI 不存在或不可执行: ${cli_path}" >&2
exit 1
fi
existing_token="$(read_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN")"
if [[ -n "${existing_token}" ]]; then
token="${existing_token}"
echo "[server-provision] GENARRATIVE_SPACETIME_TOKEN 已存在,保留并同步 SpacetimeDB CLI 登录态。"
else
response="$(curl -fsS -X POST "${server_url}/v1/identity")"
identity="$(parse_json_string_field "${response}" "identity")"
identity="${identity:-$(parse_json_string_field "${response}" "Identity")}"
identity="${identity:-$(parse_json_string_field "${response}" "identity_hex")}"
identity="${identity:-$(parse_json_string_field "${response}" "identityHex")}"
token="$(parse_json_string_field "${response}" "token")"
token="${token:-$(parse_json_string_field "${response}" "Token")}"
if [[ -z "${identity}" || -z "${token}" ]]; then
echo "[server-provision] 生成 SpacetimeDB client identity 失败,响应缺少 identity/token。" >&2
exit 1
fi
write_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN" "${token}"
identity_preview="${identity:0:12}"
echo "[server-provision] 已生成 SpacetimeDB client identity 并写入 GENARRATIVE_SPACETIME_TOKEN: ${identity_preview}..."
fi
if ! login_output="$(runuser -u spacetimedb -- "${cli_path}" --root-dir "${SPACETIME_ROOT}" login --token "${token}" 2>&1)"; then
echo "[server-provision] 使用 GENARRATIVE_SPACETIME_TOKEN 登录 SpacetimeDB CLI 失败。" >&2
printf "%s\\n" "${login_output}" | sed -E "s/[A-Za-z0-9_.=-]{24,}/[REDACTED]/g" >&2
exit 1
fi
echo "[server-provision] 已同步 SpacetimeDB CLI 登录态;后续首次 publish 将使用同一 client identity。"
}
render_nginx_https_config() {
sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative.conf
}
@@ -506,6 +615,7 @@ pipeline {
run_cmd systemctl enable spacetimedb.service genarrative-api.service
run_cmd systemctl restart spacetimedb.service
wait_for_spacetimedb_service
ensure_spacetime_owner_client_token
if [[ -x "${CURRENT_LINK}/api-server" ]]; then
run_cmd systemctl restart genarrative-api.service
else

View File

@@ -11,6 +11,7 @@ usage() {
进入维护模式,校验 spacetime_module.wasm.sha256并在生产实例本机执行 spacetime publish。
默认使用 http://127.0.0.1:3101避免与部署机本机 Git/Web 服务的 3000 端口冲突。
默认使用 /stdb 作为 spacetime CLI root-dir并以 spacetimedb 用户发布,避免 root CLI 身份污染自托管实例。
发布时固定追加 --no-config只使用显式参数避免工作区或用户目录里的 spacetime 配置干扰目标。
失败时保留维护模式。
EOF
}
@@ -141,6 +142,7 @@ PUBLISH_ARGS=(
"${DATABASE}"
--bin-path "${SOURCE_DIR}/spacetime_module.wasm"
--yes
--no-config
)
if [[ -n "${SERVER_URL}" ]]; then
@@ -173,6 +175,7 @@ if [[ -n "${RUN_AS_USER}" && "$(id -u)" -eq 0 ]]; then
"${DATABASE}"
--bin-path "${PUBLISH_TMP_DIR}/spacetime_module.wasm"
--yes
--no-config
)
if [[ -n "${SERVER_URL}" ]]; then
PUBLISH_ARGS+=(--server "${SERVER_URL}")