pipeline { agent none options { disableConcurrentBuilds() timestamps() 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') 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 本机监听端口') booleanParam(name: 'INSTALL_NGINX_CONFIG', defaultValue: true, description: '安装 Nginx 配置并执行 nginx -t') 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 不能为空。') } } } } 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}" \ 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 } render_nginx_config() { sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative.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:3000|" \ deploy/env/api-server.env.example } 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/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}, source_commit=$(cat .jenkins-source-commit)" run_cmd id 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 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 [[ "${INSTALL_NGINX_CONFIG}" == "true" ]]; then run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d echo "+ render deploy/nginx/genarrative.conf -> /etc/nginx/conf.d/genarrative.conf" if [[ "${DRY_RUN}" != "true" ]]; then render_nginx_config >/etc/nginx/conf.d/genarrative.conf chmod 0644 /etc/nginx/conf.d/genarrative.conf fi install_file deploy/nginx/snippets/genarrative-maintenance.conf /etc/nginx/snippets/genarrative-maintenance.conf 0644 run_cmd nginx -t 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 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 { success { echo "Server provision 完成: target=${params.DEPLOY_TARGET}, dryRun=${params.DRY_RUN}" } } }