pipeline { agent none options { disableConcurrentBuilds() skipDefaultCheckout(true) buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20')) } environment { GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git' } parameters { choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent') string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送') booleanParam(name: 'CONFIRM_PROVISION', defaultValue: false, description: '确认执行服务器初始化;未勾选时只允许 dry-run') booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只打印将执行的服务器初始化命令,不写入系统配置') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit') string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: 'Nginx server_name 与证书域名') string(name: 'SPACETIME_BIN_SOURCE', defaultValue: '/usr/local/bin/spacetime', description: '服务器上已有 spacetime CLI 路径') string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir') string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录') string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接') string(name: 'WEB_LINK', defaultValue: '/srv/genarrative/web', description: 'Nginx 静态站点目录或软链接') string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'api-server 环境文件') string(name: 'API_PORT', defaultValue: '8082', description: 'api-server 本机监听端口') choice(name: 'NGINX_CONFIG_MODE', choices: ['none', 'production-https', 'development-http'], description: 'Nginx 配置模式;开发服无域名时选 development-http,release 正式入口选 production-https') booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务') } stages { stage('Prepare') { agent { label 'linux && genarrative-build' } steps { script { if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) { error('release provision 需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。') } if (!params.DRY_RUN && !params.CONFIRM_PROVISION) { error('执行服务器初始化前必须勾选 CONFIRM_PROVISION;否则请保持 DRY_RUN=true。') } if (!params.SERVER_NAME?.trim()) { error('SERVER_NAME 不能为空。') } if (!params.SPACETIME_BIN_SOURCE?.trim()) { error('SPACETIME_BIN_SOURCE 不能为空。') } def nginxMode = params.NGINX_CONFIG_MODE?.trim() if (!(nginxMode in ['none', 'production-https', 'development-http'])) { error("NGINX_CONFIG_MODE 只能是 none、production-https 或 development-http,当前值: ${params.NGINX_CONFIG_MODE}") } if (params.DEPLOY_TARGET == 'release' && nginxMode == 'development-http') { error('release 目标禁止安装 development-http Nginx 配置;无证书初始化请使用 NGINX_CONFIG_MODE=none。') } if (!params.DRY_RUN && nginxMode == 'production-https' && params.SERVER_NAME?.trim() == 'genarrative.example.com') { error('真实初始化安装 Nginx 配置时必须把 SERVER_NAME 改成真实域名,不能使用 genarrative.example.com 占位值。证书未准备好时请先保持 NGINX_CONFIG_MODE=none。') } } } } stage('Checkout Provision Files') { agent { label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { checkout([ $class: 'GitSCM', branches: [[name: "*/${params.SOURCE_BRANCH}"]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]], ]) sh ''' bash -lc ' set -euo pipefail chmod +x scripts/jenkins-checkout-source.sh SOURCE_BRANCH="${SOURCE_BRANCH:-master}" \ COMMIT_HASH="${COMMIT_HASH:-}" \ GIT_REMOTE_URL="${GIT_REMOTE_URL}" \ SOURCE_COMMIT_FILE=".jenkins-source-commit" \ scripts/jenkins-checkout-source.sh ' ''' } } stage('Provision Server') { agent { label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { sh ''' bash -lc ' 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 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 } 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。" ' ''' } } } post { always { script { def notificationParameters = [ string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), string(name: 'SOURCE_COMMIT', value: env.SOURCE_COMMIT ?: (params.COMMIT_HASH ?: '')), string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION ?: (params.BUILD_VERSION ?: '')), string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET ?: ''), string(name: 'DATABASE', value: params.DATABASE ?: ''), string(name: 'SUMMARY', value: '服务器初始化流水线结束'), ] def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() if (notificationRecipients) { notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) } try { build job: 'Genarrative-Notify-Email', wait: false, propagate: false, parameters: notificationParameters } catch (error) { echo "邮件通知触发失败: ${error.message}" } } } success { echo "Server provision 完成: target=${params.DEPLOY_TARGET}, dryRun=${params.DRY_RUN}, nginxConfigMode=${params.NGINX_CONFIG_MODE}" } } }