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' GIT_REMOTE_FALLBACK_URL = 'https://git.genarrative.world/GenarrativeAI/Genarrative.git' WEB_ARTIFACT_ROOT = '/var/cache/genarrative-build/web-artifacts' } parameters { choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent') booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支') string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit') string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加通知邮箱;会与 Jenkins Secret Text 凭据 genarrative-notification-emails 合并发送') string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号') string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名') string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号') 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 静态站点软链接') booleanParam(name: 'SYNC_WEB_ARTIFACT_FROM_BUILD_HOST', defaultValue: true, description: 'release 目标本地缺少 Web 大包时,是否通过 rsync 从构建机内网拉取') string(name: 'WEB_ARTIFACT_SYNC_HOST', defaultValue: 'genarrative-build-internal', description: 'rsync 源 SSH Host,通常来自 release 服务器上 Jenkins 运行用户的 ~/.ssh/config') string(name: 'WEB_ARTIFACT_SYNC_SSH_CONFIG', defaultValue: '', description: '可选,rsync 使用的 ssh config 绝对路径;留空使用当前用户默认 ~/.ssh/config') } stages { stage('Prepare') { agent { label 'linux && genarrative-build' } steps { script { if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) { error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。') } if (!params.BUILD_VERSION?.trim()) { error('BUILD_VERSION 不能为空。') } if (!params.BUILD_JOB_NAME?.trim()) { error('BUILD_JOB_NAME 不能为空。') } if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) { error('BUILD_NUMBER_TO_DEPLOY 不能为空。') } } } } stage('Checkout Deploy Scripts') { agent { label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { script { def checkoutFromRemote = { String remoteUrl -> checkout([ $class: 'GitSCM', branches: [[name: "*/${params.SOURCE_BRANCH}"]], doGenerateSubmoduleConfigurations: false, extensions: [ [$class: 'CleanBeforeCheckout'], [$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true], ], userRemoteConfigs: [[url: remoteUrl, refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}"]], ]) } try { checkoutFromRemote(env.GIT_REMOTE_URL) env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_URL } catch (error) { echo "Git 主地址拉取失败: ${env.GIT_REMOTE_URL},改用备用地址: ${env.GIT_REMOTE_FALLBACK_URL}" checkoutFromRemote(env.GIT_REMOTE_FALLBACK_URL) env.EFFECTIVE_GIT_REMOTE_URL = env.GIT_REMOTE_FALLBACK_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="${EFFECTIVE_GIT_REMOTE_URL:-${GIT_REMOTE_URL}}" \ GIT_REMOTE_FALLBACK_URL="${GIT_REMOTE_FALLBACK_URL:-}" \ SOURCE_COMMIT_FILE=".jenkins-source-commit" \ scripts/jenkins-checkout-source.sh ' ''' } } stage('Fetch Artifact') { agent { label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { copyArtifacts( projectName: params.BUILD_JOB_NAME, selector: specific(params.BUILD_NUMBER_TO_DEPLOY), filter: "build/${params.BUILD_VERSION}/web.tar.gz.sha256,build/${params.BUILD_VERSION}/release-manifest.json,build/${params.BUILD_VERSION}/web-artifact-pointer.txt", target: '.', fingerprintArtifacts: true ) sh ''' bash -lc ' set -euo pipefail artifact_dir="${WEB_ARTIFACT_ROOT}/${BUILD_JOB_NAME}/${BUILD_NUMBER_TO_DEPLOY}/${BUILD_VERSION}" if [[ ! -f "${artifact_dir}/web.tar.gz" ]]; then sync_enabled="${SYNC_WEB_ARTIFACT_FROM_BUILD_HOST:-true}" sync_host="${WEB_ARTIFACT_SYNC_HOST:-genarrative-build-internal}" sync_ssh_config="${WEB_ARTIFACT_SYNC_SSH_CONFIG:-}" if [[ "${DEPLOY_TARGET:-development}" == "release" && "${sync_enabled}" == "true" ]]; then if [[ -z "${sync_host}" ]]; then echo "[web-deploy] release 目标需要同步 Web 大包,但 WEB_ARTIFACT_SYNC_HOST 为空。" >&2 exit 1 fi echo "[web-deploy] release 目标本地缓存缺少 Web 大包,尝试从 ${sync_host} 同步: ${artifact_dir}" if ! command -v rsync >/dev/null 2>&1; then echo "[web-deploy] 当前 release agent 缺少 rsync,请先安装 rsync 或预先挂载 Web 产物目录。" >&2 exit 1 fi mkdir -p "${artifact_dir}" rsync_args=(-av --progress) if [[ -n "${sync_ssh_config}" ]]; then rsync_args+=(-e "ssh -F ${sync_ssh_config}") fi rsync "${rsync_args[@]}" "${sync_host}:${artifact_dir}/" "${artifact_dir}/" fi fi if [[ ! -f "${artifact_dir}/web.tar.gz" ]]; then echo "[web-deploy] 未找到构建机本地 Web 大包: ${artifact_dir}/web.tar.gz" >&2 echo "[web-deploy] development 目标要求 Web 构建与发布共享同一 Linux 构建/开发部署机;release 目标会默认通过 rsync 从 WEB_ARTIFACT_SYNC_HOST 拉取,也可预先同步或挂载 ${WEB_ARTIFACT_ROOT}。" >&2 exit 1 fi mkdir -p "build/${BUILD_VERSION}" cp -f "${artifact_dir}/web.tar.gz" "build/${BUILD_VERSION}/web.tar.gz" if [[ -f "${artifact_dir}/web.tar.gz.sha256" ]]; then cp -f "${artifact_dir}/web.tar.gz.sha256" "build/${BUILD_VERSION}/web.tar.gz.sha256" fi if [[ -f "${artifact_dir}/release-manifest.json" ]]; then cp -f "${artifact_dir}/release-manifest.json" "build/${BUILD_VERSION}/release-manifest.json" fi echo "[web-deploy] 已从构建机本地目录获取 Web 大包: ${artifact_dir}" ' ''' } } stage('Deploy Web') { agent { label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}" } steps { sh ''' bash -lc ' set -euo pipefail chmod +x scripts/deploy/production-web-deploy.sh scripts/deploy/production-web-deploy.sh \ --source-dir "build/${BUILD_VERSION}" \ --version "${BUILD_VERSION}" \ --release-root "${RELEASE_ROOT}" \ --current-link "${CURRENT_LINK}" \ --web-link "${WEB_LINK}" ' ''' } } } 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: 'Web 发布流水线结束'), ] 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 "Web 发布完成: version=${params.BUILD_VERSION}" } } }