This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
- `scripts/deploy/maintenance-status.sh`
|
||||
- `scripts/build-production-release.sh`
|
||||
- `scripts/jenkins-checkout-source.sh`
|
||||
- `scripts/jenkins-server-provision.sh`
|
||||
- `scripts/deploy/production-web-deploy.sh`
|
||||
- `scripts/deploy/production-api-deploy.sh`
|
||||
- `scripts/deploy/production-stdb-publish.sh`
|
||||
@@ -97,6 +98,17 @@
|
||||
|
||||
API 发布阶段只使用上游 API 构建产物,不应回退到上游源码 commit 执行部署脚本;部署脚本应始终取 `SOURCE_BRANCH` 最新提交。否则全量流水线在修复部署脚本后仍可能按旧 `COMMIT_HASH` checkout,继续执行不认识新参数的旧版 `production-api-deploy.sh`。
|
||||
|
||||
### 服务器配置流水线
|
||||
|
||||
`Genarrative-Server-Provision` 的 Jenkinsfile 只负责参数、节点路由与调用脚本;服务器配置主体逻辑放在 `scripts/jenkins-server-provision.sh`。不要再把数百行 Bash 内联进 Jenkins `sh ''' ... '''` 或 `bash -lc '...'`,否则 Jenkins/Groovy/sh/bash 多层转义会把 `\"`、`${...}`、sed 表达式等内容二次改写,容易在运行时出现 `syntax error near unexpected token '}'` 这类难定位错误。
|
||||
|
||||
该脚本负责安装构建依赖、同步 SpacetimeDB current 目录、安装 systemd/Nginx 配置、创建或保留 `/etc/genarrative/api-server.env`、维护模式配置以及首次服务启动前的 SpacetimeDB client token 初始化。修改后应至少执行:
|
||||
|
||||
```bash
|
||||
bash -n scripts/jenkins-server-provision.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Nginx 规则
|
||||
|
||||
生产正式入口只保留必要路由:
|
||||
|
||||
@@ -98,532 +98,8 @@ BASH
|
||||
sh '''
|
||||
bash <<'BASH'
|
||||
set -euo pipefail
|
||||
|
||||
require_path() {
|
||||
local path="$1"
|
||||
if [[ ! -e "${path}" ]]; then
|
||||
echo "[server-provision] 缺少必要文件: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
echo "+ $*"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_file() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local mode="$3"
|
||||
echo "+ install -m ${mode} ${source} ${target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m "${mode}" "${source}" "${target}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_build_dependencies() {
|
||||
echo "[server-provision] 安装 Linux 构建依赖: clang, lld, pkg-config, OpenSSL headers"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
run_cmd apt-get update
|
||||
run_cmd apt-get install -y clang lld pkg-config libssl-dev ca-certificates
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
run_cmd dnf install -y clang lld pkgconf-pkg-config openssl-devel ca-certificates
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
run_cmd yum install -y clang lld pkgconf-pkg-config openssl-devel ca-certificates
|
||||
else
|
||||
echo "[server-provision] 未找到 apt-get/dnf/yum,无法自动安装 clang/lld。请手动安装后重跑构建。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_sccache() {
|
||||
for tool_dir in "${HOME:-}/.cargo/bin" /root/.cargo/bin /usr/local/cargo/bin; do
|
||||
if [[ -d "${tool_dir}" && ":${PATH}:" != *":${tool_dir}:"* ]]; then
|
||||
export PATH="${tool_dir}:${PATH}"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v sccache >/dev/null 2>&1; then
|
||||
echo "[server-provision] sccache 已存在: $(command -v sccache)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -x /root/.cargo/bin/sccache ]]; then
|
||||
echo "[server-provision] sccache 已存在: /root/.cargo/bin/sccache"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[server-provision] 未找到 sccache,准备通过 cargo install sccache 安装。"
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "[server-provision] 未找到 cargo,无法自动安装 sccache。请先安装 Rust 工具链后重跑 Server-Provision。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ cargo install sccache --locked"
|
||||
return
|
||||
fi
|
||||
|
||||
cargo install sccache --locked
|
||||
if ! command -v sccache >/dev/null 2>&1 && [[ ! -x /root/.cargo/bin/sccache ]]; then
|
||||
echo "[server-provision] sccache 安装后仍不可用,请检查 cargo bin 目录是否在 PATH 中。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sync_spacetime_install() {
|
||||
local root_dir="$1"
|
||||
local target_bin_dir="${root_dir}/bin/current"
|
||||
local target_cli="${target_bin_dir}/spacetimedb-cli"
|
||||
local target_standalone="${target_bin_dir}/spacetimedb-standalone"
|
||||
local resolved_command="${SPACETIME_BIN_SOURCE}"
|
||||
local install_dir=""
|
||||
local root_bin="${root_dir}/bin"
|
||||
local share_bin_dir=""
|
||||
local version_dir=""
|
||||
local parent_dir=""
|
||||
|
||||
if [[ -x "${target_cli}" && -x "${target_standalone}" ]]; then
|
||||
echo "[server-provision] SpacetimeDB current 目录已存在: ${target_bin_dir}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[server-provision] 同步 SpacetimeDB current 目录到 ${target_bin_dir}"
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ mkdir -p ${target_bin_dir}"
|
||||
echo "+ copy spacetimedb-cli and spacetimedb-standalone into ${target_bin_dir}"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v readlink >/dev/null 2>&1; then
|
||||
resolved_command="$(readlink -f "${SPACETIME_BIN_SOURCE}" 2>/dev/null || echo "${SPACETIME_BIN_SOURCE}")"
|
||||
fi
|
||||
install_dir="$(cd -- "$(dirname -- "${resolved_command}")" && pwd)"
|
||||
mkdir -p "${root_bin}"
|
||||
|
||||
for share_bin_dir in \
|
||||
"/usr/.local/share/spacetime/bin" \
|
||||
"/root/.local/share/spacetime/bin" \
|
||||
"${HOME:-}/.local/share/spacetime/bin"; do
|
||||
if [[ -d "${share_bin_dir}" ]]; then
|
||||
version_dir="$(find "${share_bin_dir}" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
|
||||
if [[ -n "${version_dir}" && -x "${version_dir}/spacetimedb-cli" && -x "${version_dir}/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${version_dir} -> ${target_bin_dir}"
|
||||
rm -rf "${target_bin_dir}"
|
||||
mkdir -p "${target_bin_dir}"
|
||||
cp -a "${version_dir}/." "${target_bin_dir}/"
|
||||
chmod +x "${target_cli}" "${target_standalone}"
|
||||
chown -R spacetimedb:spacetimedb "${root_bin}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -d "${install_dir}/bin" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir}/bin -> ${root_bin}"
|
||||
cp -a "${install_dir}/bin/." "${root_bin}/"
|
||||
elif [[ -x "${install_dir}/spacetimedb-cli" && -x "${install_dir}/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir} -> ${target_bin_dir}"
|
||||
rm -rf "${target_bin_dir}"
|
||||
mkdir -p "${target_bin_dir}"
|
||||
cp -f "${install_dir}/spacetimedb-cli" "${target_cli}"
|
||||
cp -f "${install_dir}/spacetimedb-standalone" "${target_standalone}"
|
||||
chmod +x "${target_cli}" "${target_standalone}"
|
||||
elif [[ -f "${resolved_command}" ]]; then
|
||||
parent_dir="$(cd -- "${install_dir}/.." && pwd)"
|
||||
if [[ -d "${parent_dir}/bin" && -x "${parent_dir}/bin/current/spacetimedb-cli" && -x "${parent_dir}/bin/current/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${parent_dir}/bin -> ${root_bin}"
|
||||
cp -a "${parent_dir}/bin/." "${root_bin}/"
|
||||
else
|
||||
echo "[server-provision] 未能从 spacetime 命令路径推断完整 SpacetimeDB 安装目录: ${resolved_command}" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装后仍缺少 current 目录。" >&2
|
||||
echo "[server-provision] 需要同时存在: ${target_cli} 与 ${target_standalone}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chown -R spacetimedb:spacetimedb "${root_bin}"
|
||||
}
|
||||
|
||||
is_spacetimedb_ready() {
|
||||
local server_url="http://127.0.0.1:3101"
|
||||
|
||||
if command -v curl >/dev/null 2>&1 && curl -fsS "${server_url}/v1/ping" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_spacetimedb_service() {
|
||||
local deadline=$((SECONDS + 60))
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ wait for spacetimedb.service on http://127.0.0.1:3101"
|
||||
return
|
||||
fi
|
||||
|
||||
while ((SECONDS < deadline)); do
|
||||
if is_spacetimedb_ready; then
|
||||
echo "[server-provision] spacetimedb.service 已就绪: http://127.0.0.1:3101"
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[server-provision] 等待 spacetimedb.service 就绪超时。" >&2
|
||||
systemctl status spacetimedb.service --no-pager -l >&2 || true
|
||||
journalctl -u spacetimedb.service --no-pager -n 80 >&2 || true
|
||||
ss -ltnp >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local line value quote_char
|
||||
quote_char='"'
|
||||
|
||||
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} -ge 2 && "${value:0:1}" == "${quote_char}" && "${value: -1}" == "${quote_char}" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
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
|
||||
}
|
||||
|
||||
render_nginx_development_http_config() {
|
||||
sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative-dev-http.conf
|
||||
}
|
||||
|
||||
render_api_env_example() {
|
||||
sed \
|
||||
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
|
||||
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101|" \
|
||||
deploy/env/api-server.env.example
|
||||
}
|
||||
|
||||
validate_nginx_tls() {
|
||||
local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}"
|
||||
if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then
|
||||
echo "[server-provision] SERVER_NAME 仍是占位域名,拒绝写入 Nginx HTTPS 配置。请填写真实域名,或先设置 NGINX_CONFIG_MODE=none。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${cert_dir}/fullchain.pem" || ! -f "${cert_dir}/privkey.pem" ]]; then
|
||||
echo "[server-provision] 未找到 Nginx HTTPS 证书: ${cert_dir}/fullchain.pem 或 ${cert_dir}/privkey.pem" >&2
|
||||
echo "[server-provision] 请先完成证书申请,或首次初始化时设置 NGINX_CONFIG_MODE=none,避免写入无法通过 nginx -t 的配置。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_nginx_config_with_rollback() {
|
||||
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
||||
local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf"
|
||||
local config_source
|
||||
local rendered_config rendered_snippet config_backup snippet_backup
|
||||
local had_config="false"
|
||||
local had_snippet="false"
|
||||
|
||||
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
|
||||
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
||||
config_source="deploy/nginx/genarrative.conf"
|
||||
elif [[ "${NGINX_CONFIG_MODE}" == "development-http" ]]; then
|
||||
config_source="deploy/nginx/genarrative-dev-http.conf"
|
||||
else
|
||||
echo "[server-provision] NGINX_CONFIG_MODE=${NGINX_CONFIG_MODE} 不需要安装 Nginx 配置。"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "+ render ${config_source} -> ${config_target}"
|
||||
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}"
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ nginx -t"
|
||||
echo "+ nginx -s reload"
|
||||
return
|
||||
fi
|
||||
|
||||
rendered_config="$(mktemp)"
|
||||
rendered_snippet="$(mktemp)"
|
||||
config_backup="$(mktemp)"
|
||||
snippet_backup="$(mktemp)"
|
||||
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
||||
validate_nginx_tls
|
||||
render_nginx_https_config >"${rendered_config}"
|
||||
else
|
||||
render_nginx_development_http_config >"${rendered_config}"
|
||||
fi
|
||||
cp deploy/nginx/snippets/genarrative-maintenance.conf "${rendered_snippet}"
|
||||
|
||||
if [[ -f "${config_target}" ]]; then
|
||||
cp -p "${config_target}" "${config_backup}"
|
||||
had_config="true"
|
||||
fi
|
||||
if [[ -f "${snippet_target}" ]]; then
|
||||
cp -p "${snippet_target}" "${snippet_backup}"
|
||||
had_snippet="true"
|
||||
fi
|
||||
|
||||
install -m 0644 "${rendered_config}" "${config_target}"
|
||||
install -m 0644 "${rendered_snippet}" "${snippet_target}"
|
||||
|
||||
if ! nginx -t; then
|
||||
echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2
|
||||
if [[ "${had_config}" == "true" ]]; then
|
||||
cp -p "${config_backup}" "${config_target}"
|
||||
else
|
||||
rm -f "${config_target}"
|
||||
fi
|
||||
if [[ "${had_snippet}" == "true" ]]; then
|
||||
cp -p "${snippet_backup}" "${snippet_target}"
|
||||
else
|
||||
rm -f "${snippet_target}"
|
||||
fi
|
||||
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
|
||||
exit 1
|
||||
fi
|
||||
echo "+ nginx -s reload"
|
||||
nginx -s reload
|
||||
|
||||
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
|
||||
}
|
||||
|
||||
cleanup_placeholder_nginx_config() {
|
||||
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
||||
local disabled_target
|
||||
|
||||
if [[ ! -f "${config_target}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! grep -q "/etc/letsencrypt/live/genarrative.example.com/" "${config_target}"; then
|
||||
return
|
||||
fi
|
||||
|
||||
disabled_target="${config_target}.disabled-placeholder-$(date +%Y%m%d%H%M%S)"
|
||||
echo "[server-provision] 发现上一轮初始化留下的占位域名 Nginx 配置,禁用: ${config_target} -> ${disabled_target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
mv "${config_target}" "${disabled_target}"
|
||||
if command -v nginx >/dev/null 2>&1; then
|
||||
if ! nginx -t; then
|
||||
echo "[server-provision] 占位配置已禁用,但 nginx -t 仍失败;请检查其他 Nginx 配置。" >&2
|
||||
else
|
||||
echo "+ nginx -s reload"
|
||||
nginx -s reload
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
|
||||
}
|
||||
|
||||
render_spacetimedb_service() {
|
||||
local root_escaped
|
||||
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
|
||||
sed \
|
||||
-e "s|/stdb|${root_escaped}|g" \
|
||||
deploy/systemd/spacetimedb.service
|
||||
}
|
||||
|
||||
render_api_service() {
|
||||
local current_escaped env_escaped
|
||||
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
||||
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
||||
sed \
|
||||
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
||||
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
||||
deploy/systemd/genarrative-api.service
|
||||
}
|
||||
|
||||
require_path deploy/systemd/spacetimedb.service
|
||||
require_path deploy/systemd/genarrative-api.service
|
||||
require_path deploy/nginx/genarrative.conf
|
||||
require_path deploy/nginx/genarrative-dev-http.conf
|
||||
require_path deploy/nginx/snippets/genarrative-maintenance.conf
|
||||
require_path deploy/env/api-server.env.example
|
||||
require_path scripts/deploy/maintenance-on.sh
|
||||
require_path scripts/deploy/maintenance-off.sh
|
||||
require_path scripts/deploy/maintenance-status.sh
|
||||
|
||||
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
|
||||
|
||||
run_cmd id
|
||||
install_build_dependencies
|
||||
install_sccache
|
||||
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth
|
||||
|
||||
if ! id spacetimedb >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
|
||||
else
|
||||
echo "[server-provision] 用户已存在: spacetimedb"
|
||||
fi
|
||||
|
||||
if ! id genarrative >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
|
||||
else
|
||||
echo "[server-provision] 用户已存在: genarrative"
|
||||
fi
|
||||
|
||||
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
|
||||
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
|
||||
|
||||
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
|
||||
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
|
||||
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
|
||||
fi
|
||||
sync_spacetime_install "${SPACETIME_ROOT}"
|
||||
|
||||
spacetimedb_service="$(mktemp)"
|
||||
api_service="$(mktemp)"
|
||||
render_spacetimedb_service >"${spacetimedb_service}"
|
||||
render_api_service >"${api_service}"
|
||||
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
|
||||
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
|
||||
rm -f "${spacetimedb_service}" "${api_service}"
|
||||
|
||||
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
||||
echo "+ create ${API_ENV_FILE} from example"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_api_env_example >"${API_ENV_FILE}"
|
||||
chmod 0600 "${API_ENV_FILE}"
|
||||
chown root:root "${API_ENV_FILE}"
|
||||
fi
|
||||
else
|
||||
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
|
||||
fi
|
||||
|
||||
if [[ "${NGINX_CONFIG_MODE}" != "none" ]]; then
|
||||
install_nginx_config_with_rollback
|
||||
else
|
||||
cleanup_placeholder_nginx_config
|
||||
fi
|
||||
|
||||
run_cmd systemctl daemon-reload
|
||||
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
|
||||
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
|
||||
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server,跳过 api-server 首次启动。后续 API deploy 会重启服务。"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"
|
||||
chmod +x scripts/jenkins-server-provision.sh
|
||||
scripts/jenkins-server-provision.sh
|
||||
BASH
|
||||
'''
|
||||
}
|
||||
|
||||
528
scripts/jenkins-server-provision.sh
Executable file
528
scripts/jenkins-server-provision.sh
Executable file
@@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
require_path() {
|
||||
local path="$1"
|
||||
if [[ ! -e "${path}" ]]; then
|
||||
echo "[server-provision] 缺少必要文件: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
echo "+ $*"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_file() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local mode="$3"
|
||||
echo "+ install -m ${mode} ${source} ${target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m "${mode}" "${source}" "${target}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_build_dependencies() {
|
||||
echo "[server-provision] 安装 Linux 构建依赖: clang, lld, pkg-config, OpenSSL headers"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
run_cmd apt-get update
|
||||
run_cmd apt-get install -y clang lld pkg-config libssl-dev ca-certificates
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
run_cmd dnf install -y clang lld pkgconf-pkg-config openssl-devel ca-certificates
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
run_cmd yum install -y clang lld pkgconf-pkg-config openssl-devel ca-certificates
|
||||
else
|
||||
echo "[server-provision] 未找到 apt-get/dnf/yum,无法自动安装 clang/lld。请手动安装后重跑构建。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_sccache() {
|
||||
for tool_dir in "${HOME:-}/.cargo/bin" /root/.cargo/bin /usr/local/cargo/bin; do
|
||||
if [[ -d "${tool_dir}" && ":${PATH}:" != *":${tool_dir}:"* ]]; then
|
||||
export PATH="${tool_dir}:${PATH}"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v sccache >/dev/null 2>&1; then
|
||||
echo "[server-provision] sccache 已存在: $(command -v sccache)"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -x /root/.cargo/bin/sccache ]]; then
|
||||
echo "[server-provision] sccache 已存在: /root/.cargo/bin/sccache"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[server-provision] 未找到 sccache,准备通过 cargo install sccache 安装。"
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "[server-provision] 未找到 cargo,无法自动安装 sccache。请先安装 Rust 工具链后重跑 Server-Provision。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ cargo install sccache --locked"
|
||||
return
|
||||
fi
|
||||
|
||||
cargo install sccache --locked
|
||||
if ! command -v sccache >/dev/null 2>&1 && [[ ! -x /root/.cargo/bin/sccache ]]; then
|
||||
echo "[server-provision] sccache 安装后仍不可用,请检查 cargo bin 目录是否在 PATH 中。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sync_spacetime_install() {
|
||||
local root_dir="$1"
|
||||
local target_bin_dir="${root_dir}/bin/current"
|
||||
local target_cli="${target_bin_dir}/spacetimedb-cli"
|
||||
local target_standalone="${target_bin_dir}/spacetimedb-standalone"
|
||||
local resolved_command="${SPACETIME_BIN_SOURCE}"
|
||||
local install_dir=""
|
||||
local root_bin="${root_dir}/bin"
|
||||
local share_bin_dir=""
|
||||
local version_dir=""
|
||||
local parent_dir=""
|
||||
|
||||
if [[ -x "${target_cli}" && -x "${target_standalone}" ]]; then
|
||||
echo "[server-provision] SpacetimeDB current 目录已存在: ${target_bin_dir}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "[server-provision] 同步 SpacetimeDB current 目录到 ${target_bin_dir}"
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ mkdir -p ${target_bin_dir}"
|
||||
echo "+ copy spacetimedb-cli and spacetimedb-standalone into ${target_bin_dir}"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v readlink >/dev/null 2>&1; then
|
||||
resolved_command="$(readlink -f "${SPACETIME_BIN_SOURCE}" 2>/dev/null || echo "${SPACETIME_BIN_SOURCE}")"
|
||||
fi
|
||||
install_dir="$(cd -- "$(dirname -- "${resolved_command}")" && pwd)"
|
||||
mkdir -p "${root_bin}"
|
||||
|
||||
for share_bin_dir in \
|
||||
"/usr/.local/share/spacetime/bin" \
|
||||
"/root/.local/share/spacetime/bin" \
|
||||
"${HOME:-}/.local/share/spacetime/bin"; do
|
||||
if [[ -d "${share_bin_dir}" ]]; then
|
||||
version_dir="$(find "${share_bin_dir}" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -n 1)"
|
||||
if [[ -n "${version_dir}" && -x "${version_dir}/spacetimedb-cli" && -x "${version_dir}/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${version_dir} -> ${target_bin_dir}"
|
||||
rm -rf "${target_bin_dir}"
|
||||
mkdir -p "${target_bin_dir}"
|
||||
cp -a "${version_dir}/." "${target_bin_dir}/"
|
||||
chmod +x "${target_cli}" "${target_standalone}"
|
||||
chown -R spacetimedb:spacetimedb "${root_bin}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -d "${install_dir}/bin" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir}/bin -> ${root_bin}"
|
||||
cp -a "${install_dir}/bin/." "${root_bin}/"
|
||||
elif [[ -x "${install_dir}/spacetimedb-cli" && -x "${install_dir}/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir} -> ${target_bin_dir}"
|
||||
rm -rf "${target_bin_dir}"
|
||||
mkdir -p "${target_bin_dir}"
|
||||
cp -f "${install_dir}/spacetimedb-cli" "${target_cli}"
|
||||
cp -f "${install_dir}/spacetimedb-standalone" "${target_standalone}"
|
||||
chmod +x "${target_cli}" "${target_standalone}"
|
||||
elif [[ -f "${resolved_command}" ]]; then
|
||||
parent_dir="$(cd -- "${install_dir}/.." && pwd)"
|
||||
if [[ -d "${parent_dir}/bin" && -x "${parent_dir}/bin/current/spacetimedb-cli" && -x "${parent_dir}/bin/current/spacetimedb-standalone" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装: ${parent_dir}/bin -> ${root_bin}"
|
||||
cp -a "${parent_dir}/bin/." "${root_bin}/"
|
||||
else
|
||||
echo "[server-provision] 未能从 spacetime 命令路径推断完整 SpacetimeDB 安装目录: ${resolved_command}" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then
|
||||
echo "[server-provision] 同步 SpacetimeDB 安装后仍缺少 current 目录。" >&2
|
||||
echo "[server-provision] 需要同时存在: ${target_cli} 与 ${target_standalone}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chown -R spacetimedb:spacetimedb "${root_bin}"
|
||||
}
|
||||
|
||||
is_spacetimedb_ready() {
|
||||
local server_url="http://127.0.0.1:3101"
|
||||
|
||||
if command -v curl >/dev/null 2>&1 && curl -fsS "${server_url}/v1/ping" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_spacetimedb_service() {
|
||||
local deadline=$((SECONDS + 60))
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ wait for spacetimedb.service on http://127.0.0.1:3101"
|
||||
return
|
||||
fi
|
||||
|
||||
while ((SECONDS < deadline)); do
|
||||
if is_spacetimedb_ready; then
|
||||
echo "[server-provision] spacetimedb.service 已就绪: http://127.0.0.1:3101"
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[server-provision] 等待 spacetimedb.service 就绪超时。" >&2
|
||||
systemctl status spacetimedb.service --no-pager -l >&2 || true
|
||||
journalctl -u spacetimedb.service --no-pager -n 80 >&2 || true
|
||||
ss -ltnp >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local line value quote_char
|
||||
quote_char='"'
|
||||
|
||||
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} -ge 2 && "${value:0:1}" == "${quote_char}" && "${value: -1}" == "${quote_char}" ]]; then
|
||||
value="${value:1:${#value}-2}"
|
||||
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
|
||||
}
|
||||
|
||||
render_nginx_development_http_config() {
|
||||
sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative-dev-http.conf
|
||||
}
|
||||
|
||||
render_api_env_example() {
|
||||
sed \
|
||||
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
|
||||
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101|" \
|
||||
deploy/env/api-server.env.example
|
||||
}
|
||||
|
||||
validate_nginx_tls() {
|
||||
local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}"
|
||||
if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then
|
||||
echo "[server-provision] SERVER_NAME 仍是占位域名,拒绝写入 Nginx HTTPS 配置。请填写真实域名,或先设置 NGINX_CONFIG_MODE=none。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "${cert_dir}/fullchain.pem" || ! -f "${cert_dir}/privkey.pem" ]]; then
|
||||
echo "[server-provision] 未找到 Nginx HTTPS 证书: ${cert_dir}/fullchain.pem 或 ${cert_dir}/privkey.pem" >&2
|
||||
echo "[server-provision] 请先完成证书申请,或首次初始化时设置 NGINX_CONFIG_MODE=none,避免写入无法通过 nginx -t 的配置。" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_nginx_config_with_rollback() {
|
||||
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
||||
local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf"
|
||||
local config_source
|
||||
local rendered_config rendered_snippet config_backup snippet_backup
|
||||
local had_config="false"
|
||||
local had_snippet="false"
|
||||
|
||||
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
|
||||
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
||||
config_source="deploy/nginx/genarrative.conf"
|
||||
elif [[ "${NGINX_CONFIG_MODE}" == "development-http" ]]; then
|
||||
config_source="deploy/nginx/genarrative-dev-http.conf"
|
||||
else
|
||||
echo "[server-provision] NGINX_CONFIG_MODE=${NGINX_CONFIG_MODE} 不需要安装 Nginx 配置。"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "+ render ${config_source} -> ${config_target}"
|
||||
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}"
|
||||
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
echo "+ nginx -t"
|
||||
echo "+ nginx -s reload"
|
||||
return
|
||||
fi
|
||||
|
||||
rendered_config="$(mktemp)"
|
||||
rendered_snippet="$(mktemp)"
|
||||
config_backup="$(mktemp)"
|
||||
snippet_backup="$(mktemp)"
|
||||
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
||||
validate_nginx_tls
|
||||
render_nginx_https_config >"${rendered_config}"
|
||||
else
|
||||
render_nginx_development_http_config >"${rendered_config}"
|
||||
fi
|
||||
cp deploy/nginx/snippets/genarrative-maintenance.conf "${rendered_snippet}"
|
||||
|
||||
if [[ -f "${config_target}" ]]; then
|
||||
cp -p "${config_target}" "${config_backup}"
|
||||
had_config="true"
|
||||
fi
|
||||
if [[ -f "${snippet_target}" ]]; then
|
||||
cp -p "${snippet_target}" "${snippet_backup}"
|
||||
had_snippet="true"
|
||||
fi
|
||||
|
||||
install -m 0644 "${rendered_config}" "${config_target}"
|
||||
install -m 0644 "${rendered_snippet}" "${snippet_target}"
|
||||
|
||||
if ! nginx -t; then
|
||||
echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2
|
||||
if [[ "${had_config}" == "true" ]]; then
|
||||
cp -p "${config_backup}" "${config_target}"
|
||||
else
|
||||
rm -f "${config_target}"
|
||||
fi
|
||||
if [[ "${had_snippet}" == "true" ]]; then
|
||||
cp -p "${snippet_backup}" "${snippet_target}"
|
||||
else
|
||||
rm -f "${snippet_target}"
|
||||
fi
|
||||
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
|
||||
exit 1
|
||||
fi
|
||||
echo "+ nginx -s reload"
|
||||
nginx -s reload
|
||||
|
||||
rm -f "${rendered_config}" "${rendered_snippet}" "${config_backup}" "${snippet_backup}"
|
||||
}
|
||||
|
||||
cleanup_placeholder_nginx_config() {
|
||||
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
||||
local disabled_target
|
||||
|
||||
if [[ ! -f "${config_target}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if ! grep -q "/etc/letsencrypt/live/genarrative.example.com/" "${config_target}"; then
|
||||
return
|
||||
fi
|
||||
|
||||
disabled_target="${config_target}.disabled-placeholder-$(date +%Y%m%d%H%M%S)"
|
||||
echo "[server-provision] 发现上一轮初始化留下的占位域名 Nginx 配置,禁用: ${config_target} -> ${disabled_target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
mv "${config_target}" "${disabled_target}"
|
||||
if command -v nginx >/dev/null 2>&1; then
|
||||
if ! nginx -t; then
|
||||
echo "[server-provision] 占位配置已禁用,但 nginx -t 仍失败;请检查其他 Nginx 配置。" >&2
|
||||
else
|
||||
echo "+ nginx -s reload"
|
||||
nginx -s reload
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
|
||||
}
|
||||
|
||||
render_spacetimedb_service() {
|
||||
local root_escaped
|
||||
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
|
||||
sed \
|
||||
-e "s|/stdb|${root_escaped}|g" \
|
||||
deploy/systemd/spacetimedb.service
|
||||
}
|
||||
|
||||
render_api_service() {
|
||||
local current_escaped env_escaped
|
||||
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
||||
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
||||
sed \
|
||||
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
||||
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
||||
deploy/systemd/genarrative-api.service
|
||||
}
|
||||
|
||||
require_path deploy/systemd/spacetimedb.service
|
||||
require_path deploy/systemd/genarrative-api.service
|
||||
require_path deploy/nginx/genarrative.conf
|
||||
require_path deploy/nginx/genarrative-dev-http.conf
|
||||
require_path deploy/nginx/snippets/genarrative-maintenance.conf
|
||||
require_path deploy/env/api-server.env.example
|
||||
require_path scripts/deploy/maintenance-on.sh
|
||||
require_path scripts/deploy/maintenance-off.sh
|
||||
require_path scripts/deploy/maintenance-status.sh
|
||||
|
||||
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
|
||||
|
||||
run_cmd id
|
||||
install_build_dependencies
|
||||
install_sccache
|
||||
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth
|
||||
|
||||
if ! id spacetimedb >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
|
||||
else
|
||||
echo "[server-provision] 用户已存在: spacetimedb"
|
||||
fi
|
||||
|
||||
if ! id genarrative >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
|
||||
else
|
||||
echo "[server-provision] 用户已存在: genarrative"
|
||||
fi
|
||||
|
||||
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
|
||||
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
|
||||
|
||||
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
|
||||
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
|
||||
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
|
||||
fi
|
||||
sync_spacetime_install "${SPACETIME_ROOT}"
|
||||
|
||||
spacetimedb_service="$(mktemp)"
|
||||
api_service="$(mktemp)"
|
||||
render_spacetimedb_service >"${spacetimedb_service}"
|
||||
render_api_service >"${api_service}"
|
||||
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
|
||||
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
|
||||
rm -f "${spacetimedb_service}" "${api_service}"
|
||||
|
||||
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
||||
echo "+ create ${API_ENV_FILE} from example"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_api_env_example >"${API_ENV_FILE}"
|
||||
chmod 0600 "${API_ENV_FILE}"
|
||||
chown root:root "${API_ENV_FILE}"
|
||||
fi
|
||||
else
|
||||
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
|
||||
fi
|
||||
|
||||
if [[ "${NGINX_CONFIG_MODE}" != "none" ]]; then
|
||||
install_nginx_config_with_rollback
|
||||
else
|
||||
cleanup_placeholder_nginx_config
|
||||
fi
|
||||
|
||||
run_cmd systemctl daemon-reload
|
||||
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
|
||||
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
|
||||
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server,跳过 api-server 首次启动。后续 API deploy 会重启服务。"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"
|
||||
Reference in New Issue
Block a user