Explorar o código

流水线优化

dujian hai 1 día
pai
achega
faecfcfffb

+ 5 - 451
docs/jenkins/Jenkinsfile-uat-build-deploy.groovy

@@ -1,455 +1,9 @@
 /**
- * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart.
+ * @deprecated 已迁移至 docs/jenkins/uat/Jenkinsfile
  *
- * Jenkins Job: Pipeline script from SCM
- * Script Path: docs/jenkins/Jenkinsfile-uat-build-deploy.groovy
+ * Jenkins Job 请将 Script Path 改为:
+ *   docs/jenkins/uat/Jenkinsfile
  *
- * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g.
- *   39.105.153.68/alien_cloud/gateway:uat-latest
- * Before push: existing uat-latest is archived as uat-build-<BUILD_NUMBER> (same digest).
- * New image is pushed only as uat-latest. Prod promote: SOURCE_TAG=uat-latest.
+ * 本文件保留仅为兼容旧 Job 配置;新 Job 请勿引用此路径。
  */
-
-/** HARBOR_PUSH_SCOPE: all-java-services | <repo>-only */
-def filterHarborPushScope(List allServices, String scope) {
-    def s = (scope ?: 'all-java-services').trim()
-    if (s == 'all-java-services') {
-        return allServices
-    }
-    if (s.endsWith('-only')) {
-        def repo = s.substring(0, s.length() - '-only'.length())
-        def picked = allServices.findAll { it.repo == repo }
-        if (picked.isEmpty()) {
-            error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
-        }
-        return picked
-    }
-    error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
-}
-
-def pushOneHarborImage(def script, Map svc, String reg, String proj, String latestTag, String buildTag,
-                      String baseImage, String dockerfile, String workspace) {
-    def jarName = "${svc.module}-1.0.0.jar"
-    def imageLatest = "${reg}/${proj}/${svc.repo}:${latestTag}"
-    def imageBuild = "${reg}/${proj}/${svc.repo}:${buildTag}"
-    def withLibFlag = svc.withLib ? 'true' : 'false'
-    script.sh """
-        set -e
-        export DOCKER_BUILDKIT=1
-        test -f ${workspace}/${svc.module}/target/${jarName}
-        cd ${workspace}/${svc.module}
-        rm -rf .jenkins_docker_ctx && mkdir -p .jenkins_docker_ctx/lib
-        cp -f target/${jarName} .jenkins_docker_ctx/${jarName}
-        if [ "${withLibFlag}" = "true" ] && [ -d target/lib ]; then
-          cp -rf target/lib/. .jenkins_docker_ctx/lib/
-        else
-          touch .jenkins_docker_ctx/lib/.keep
-        fi
-        cd .jenkins_docker_ctx
-        if docker pull ${imageLatest} 2>/dev/null; then
-          echo ">>> archive previous ${latestTag} -> ${buildTag}"
-          docker tag ${imageLatest} ${imageBuild}
-          docker push ${imageBuild}
-        fi
-        docker build -f ${workspace}/${dockerfile} \\
-          --build-arg BASE_IMAGE=${baseImage} \\
-          --build-arg JAR_FILE=${jarName} \\
-          --build-arg SERVER_PORT=${svc.port} \\
-          --build-arg WITH_LIB=${svc.withLib} \\
-          -t ${imageLatest} .
-        docker push ${imageLatest}
-        echo ">>> pushed ${imageLatest} (archived prior latest as ${buildTag} if any)"
-        docker rmi ${imageLatest} 2>/dev/null || true
-        cd ${workspace}/${svc.module}
-        rm -rf .jenkins_docker_ctx
-    """
-}
-
-def deployOneUatService(def script, String moduleName, String dirName, String workspace) {
-    def sourceJar = "${workspace}/${moduleName}/target/${moduleName}-1.0.0.jar"
-    def sourceLib = "${workspace}/${moduleName}/target/lib"
-    def targetDir = "/app_deploy_uat/${dirName}"
-    script.sh """
-        set -e
-        echo ">>> Deploy module: ${moduleName}"
-        if [ -f "${sourceJar}" ]; then
-            mkdir -p "${targetDir}"
-            if [ -d "${sourceLib}" ]; then
-                rm -rf "${targetDir}/lib"
-                cp -rf "${sourceLib}" "${targetDir}"
-            fi
-            cp -f "${sourceJar}" "${targetDir}/"
-            if docker ps -a --format '{{.Names}}' | grep -wq "${dirName}"; then
-                docker restart "${dirName}"
-                echo ">>> [${dirName}] restarted"
-            else
-                echo ">>> [${dirName}] container missing, jar copied only"
-            fi
-        else
-            echo ">>> [${dirName}] jar missing, skip"
-        fi
-    """
-}
-
-/** Delete oldest uat-build-* tags in Harbor, keep newest KEEP. Never deletes uat-latest or current build tag. */
-def pruneHarborUatTags(def script, String reg, String proj, List repoNames, int keepCount, String tagPrefix, String currentBuildTag, String latestTag) {
-    if (repoNames == null || repoNames.isEmpty() || keepCount < 1) {
-        return
-    }
-    def repos = repoNames.join(' ')
-    // POSIX sh only (Jenkins sh step); no mapfile / process substitution
-    script.sh """
-        set -e
-        REG='${reg}'
-        PROJ='${proj}'
-        KEEP=${keepCount}
-        PREFIX='${tagPrefix}'
-        CURRENT='${currentBuildTag}'
-        LATEST='${latestTag}'
-        if ! command -v jq >/dev/null 2>&1; then
-          echo '>>> Harbor prune skipped: jq not installed on Jenkins agent'
-          exit 0
-        fi
-        for repo in ${repos}; do
-          enc_repo=\$(printf '%s' "\${repo}" | jq -sRr @uri)
-          tags_file=\$(mktemp)
-          curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
-            "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts?page_size=100" \\
-            | jq -r '.[] | .tags[]? | .name' | grep "^\${PREFIX}" | sort -t- -k3 -n > "\${tags_file}" || true
-          count=\$(wc -l < "\${tags_file}" | tr -d ' ')
-          echo ">>> prune \${repo}: \${count} tag(s) matching \${PREFIX}*"
-          if [ "\${count}" -le "\${KEEP}" ]; then
-            rm -f "\${tags_file}"
-            continue
-          fi
-          del_count=\$((count - KEEP))
-          deleted=0
-          while IFS= read -r t; do
-            if [ -z "\${t}" ]; then
-              continue
-            fi
-            if [ "\${t}" = "\${CURRENT}" ] || [ "\${t}" = "\${LATEST}" ]; then
-              continue
-            fi
-            if [ "\${deleted}" -ge "\${del_count}" ]; then
-              break
-            fi
-            echo ">>> DELETE Harbor tag \${repo}:\${t}"
-            if ! curl -fsS -X DELETE -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
-              "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${t}/tags/\${t}"; then
-              echo ">>> WARN: delete failed \${repo}:\${t} (check robot delete permission)"
-            fi
-            deleted=\$((deleted + 1))
-          done < "\${tags_file}"
-          rm -f "\${tags_file}"
-        done
-    """
-}
-
-pipeline {
-    agent any
-
-    options {
-        buildDiscarder(logRotator(numToKeepStr: '15'))
-        disableConcurrentBuilds()
-        timestamps()
-        timeout(time: 90, unit: 'MINUTES')
-    }
-
-    parameters {
-        string(
-                name: 'GIT_BRANCH',
-                defaultValue: 'uat-20260202',
-                trim: true,
-                description: 'Git branch, must match remote (e.g. uat-20260202)'
-        )
-        booleanParam(name: 'FORCE_UPDATE', defaultValue: false, description: 'mvn -U (routine builds leave unchecked for speed)')
-        booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps')
-        booleanParam(
-                name: 'PUSH_TO_HARBOR',
-                defaultValue: true,
-                description: 'After Maven: docker build + push to Harbor (tags uat-latest and uat-build-<N>). Uncheck for jar-only UAT deploy.'
-        )
-        choice(
-                name: 'HARBOR_PUSH_SCOPE',
-                choices: [
-                        'all-java-services',
-                        'gateway-only',
-                        'store-only',
-                        'second-only',
-                        'store-platform-only',
-                        'lawyer-only',
-                        'job-only',
-                        'dining-only',
-                ],
-                description: 'Only when PUSH_TO_HARBOR=true; default=all seven; *-only=one service'
-        )
-        string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
-        string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
-        booleanParam(
-                name: 'HARBOR_PRUNE_OLD_TAGS',
-                defaultValue: true,
-                description: 'After push: delete old uat-build-* tags in Harbor, keep last N per repo (never deletes uat-latest)'
-        )
-        string(name: 'HARBOR_KEEP_TAG_COUNT', defaultValue: '10', trim: true,
-                description: 'How many uat-build-* tags to keep per repository')
-        booleanParam(
-                name: 'HARBOR_PUSH_PARALLEL',
-                defaultValue: true,
-                description: 'Parallel docker build/push per service (faster; needs enough CPU/disk on agent)'
-        )
-    }
-
-    environment {
-        MAVEN_HOME = tool '3.6.3'
-        PATH = "${MAVEN_HOME}/bin:${env.PATH}"
-        GIT_URL = 'http://8.152.195.41:3000/alien/alien_cloud'
-        GIT_CREDENTIALS = 'zhanghaomimapingzheng'
-        HARBOR_CREDENTIALS = 'harbor-robot-alien'
-        UAT_HARBOR_LATEST_TAG = 'uat-latest'
-        UAT_HARBOR_BUILD_TAG = "uat-build-${env.BUILD_NUMBER}"
-        DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service'
-        MAVEN_LOCAL_REPO = '/var/jenkins_home/.m2/repository'
-    }
-
-    stages {
-        stage('Checkout') {
-            steps {
-                script {
-                    def branch = (params.GIT_BRANCH ?: 'uat-20260202').trim()
-                    if (!branch) {
-                        error('GIT_BRANCH is required')
-                    }
-                    env.GIT_BRANCH = branch
-                    echo ">>> Checkout branch: ${env.GIT_BRANCH}"
-                    git branch: "${env.GIT_BRANCH}",
-                            credentialsId: "${env.GIT_CREDENTIALS}",
-                            url: "${env.GIT_URL}"
-                    sh """
-                        set -e
-                        git fetch origin
-                        git reset --hard origin/${env.GIT_BRANCH}
-                        git log -1 --oneline
-                    """
-                }
-            }
-        }
-
-        stage('Prepare Maven Settings') {
-            steps {
-                script {
-                    writeFile file: 'settings.xml', text: """<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
-    <mirrors>
-        <mirror>
-            <id>aliyun-central</id>
-            <mirrorOf>central</mirrorOf>
-            <name>Aliyun Central</name>
-            <url>https://maven.aliyun.com/repository/central</url>
-        </mirror>
-    </mirrors>
-    <profiles>
-        <profile>
-            <id>repo-mix</id>
-            <repositories>
-                <repository>
-                    <id>aliyunmaven</id>
-                    <name>Aliyun Maven</name>
-                    <url>https://maven.aliyun.com/repository/public</url>
-                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </repository>
-                <repository>
-                    <id>central</id>
-                    <name>Maven Central</name>
-                    <url>https://repo.maven.apache.org/maven2</url>
-                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </repository>
-                <repository>
-                    <id>spring-milestones</id>
-                    <name>Spring Milestones</name>
-                    <url>https://repo.spring.io/milestone</url>
-                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </repository>
-                <repository>
-                    <id>spring-snapshots</id>
-                    <name>Spring Snapshots</name>
-                    <url>https://repo.spring.io/snapshot</url>
-                    <releases><enabled>false</enabled></releases>
-                    <snapshots><enabled>true</enabled><updatePolicy>daily</updatePolicy></snapshots>
-                </repository>
-            </repositories>
-            <pluginRepositories>
-                <pluginRepository>
-                    <id>aliyunmaven</id>
-                    <url>https://maven.aliyun.com/repository/public</url>
-                    <releases><enabled>true</enabled></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </pluginRepository>
-                <pluginRepository>
-                    <id>central</id>
-                    <url>https://repo.maven.apache.org/maven2</url>
-                    <releases><enabled>true</enabled></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </pluginRepository>
-                <pluginRepository>
-                    <id>spring-milestones</id>
-                    <url>https://repo.spring.io/milestone</url>
-                    <releases><enabled>true</enabled></releases>
-                    <snapshots><enabled>false</enabled></snapshots>
-                </pluginRepository>
-            </pluginRepositories>
-        </profile>
-    </profiles>
-    <activeProfiles>
-        <activeProfile>repo-mix</activeProfile>
-    </activeProfiles>
-</settings>
-"""
-                }
-            }
-        }
-
-        stage('Maven Build') {
-            steps {
-                script {
-                    def updateFlag = params.FORCE_UPDATE ? '-U' : ''
-                    retry(2) {
-                        sh """
-                            set -e
-                            mvn -version
-                            unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY || true
-                            export MAVEN_OPTS="-Xms512m -Xmx2048m -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
-                            mkdir -p ${MAVEN_LOCAL_REPO}
-                            mvn clean package -DskipTests -s settings.xml ${updateFlag} -e \\
-                              -T 1C -Dmaven.artifact.threads=8 \\
-                              -Dmaven.repo.local=${MAVEN_LOCAL_REPO}
-                        """
-                    }
-                }
-            }
-        }
-
-        stage('Push images to Harbor') {
-            when {
-                expression { return params.PUSH_TO_HARBOR == true }
-            }
-            steps {
-                script {
-                    def reg = params.HARBOR_REGISTRY.trim()
-                    def proj = params.HARBOR_PROJECT.trim()
-                    def latestTag = env.UAT_HARBOR_LATEST_TAG
-                    def buildTag = env.UAT_HARBOR_BUILD_TAG
-                    def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1"
-                    def dockerfile = env.DOCKERFILE_JAVA
-
-                    def allHarborServices = [
-                            [module: 'alien-gateway',        repo: 'gateway',        port: '8000',  withLib: false],
-                            [module: 'alien-store',          repo: 'store',          port: '50014', withLib: true],
-                            [module: 'alien-second',         repo: 'second',         port: '50015', withLib: false],
-                            [module: 'alien-store-platform', repo: 'store-platform', port: '50016', withLib: false],
-                            [module: 'alien-lawyer',         repo: 'lawyer',         port: '50017', withLib: true],
-                            [module: 'alien-job',            repo: 'job',            port: '50108', withLib: false],
-                            [module: 'alien-dining',         repo: 'dining',         port: '50019', withLib: false],
-                    ]
-                    def harborServices = filterHarborPushScope(allHarborServices, params.HARBOR_PUSH_SCOPE)
-                    echo ">>> HARBOR_PUSH_SCOPE=${params.HARBOR_PUSH_SCOPE} repos=${harborServices*.repo.join(',')}"
-
-                    withCredentials([usernamePassword(
-                            credentialsId: env.HARBOR_CREDENTIALS,
-                            usernameVariable: 'HARBOR_USER',
-                            passwordVariable: 'HARBOR_PASS',
-                    )]) {
-                        sh """
-                            set -e
-                            echo "\${HARBOR_PASS}" | docker login ${reg} -u "\${HARBOR_USER}" --password-stdin
-                            echo ">>> docker disk before Harbor push:"
-                            df -h /var/lib/docker 2>/dev/null || df -h / || true
-                            docker system prune -f --filter until=48h 2>/dev/null || true
-                        """
-                        if (params.HARBOR_PUSH_PARALLEL) {
-                            def pushBranches = [:]
-                            harborServices.each { svc ->
-                                def s = svc
-                                pushBranches[s.repo] = {
-                                    pushOneHarborImage(
-                                        this, s, reg, proj, latestTag, buildTag,
-                                        baseImage, dockerfile, env.WORKSPACE,
-                                    )
-                                }
-                            }
-                            parallel pushBranches
-                        } else {
-                            harborServices.each { svc ->
-                                pushOneHarborImage(
-                                    this, svc, reg, proj, latestTag, buildTag,
-                                    baseImage, dockerfile, env.WORKSPACE,
-                                )
-                            }
-                        }
-                        if (params.HARBOR_PRUNE_OLD_TAGS == true) {
-                            def keepN = (params.HARBOR_KEEP_TAG_COUNT ?: '10').trim() as int
-                            catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
-                                pruneHarborUatTags(
-                                    this, reg, proj, harborServices*.repo,
-                                    keepN, 'uat-build-', buildTag, latestTag,
-                                )
-                            }
-                        }
-                    }
-                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}; archived tag this run: ${env.UAT_HARBOR_BUILD_TAG}"
-                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
-                }
-            }
-        }
-
-        stage('Deploy Services') {
-            steps {
-                script {
-                    def services = [
-                            'alien-gateway:gateway-uat',
-                            'alien-job:job-uat',
-                            'alien-lawyer:lawyer-uat',
-                            'alien-second:second-uat',
-                            'alien-store:store-uat',
-                            'alien-dining:dining-uat',
-                            'alien-store-platform:store-platform-uat',
-                    ]
-
-                    def deployBranches = [:]
-                    services.each { item ->
-                        def parts = item.split(':')
-                        def moduleName = parts[0]
-                        def dirName = parts[1]
-                        deployBranches[dirName] = {
-                            deployOneUatService(this, moduleName, dirName, env.WORKSPACE)
-                        }
-                    }
-                    parallel deployBranches
-                }
-            }
-        }
-    }
-
-    post {
-        always {
-            sh 'rm -f settings.xml || true'
-            script {
-                if (!params.PUSH_TO_HARBOR) {
-                    echo '>>> Harbor push SKIPPED: PUSH_TO_HARBOR is false. On "Build with Parameters" check PUSH_TO_HARBOR.'
-                }
-            }
-        }
-        success {
-            script {
-                if (params.PUSH_TO_HARBOR) {
-                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}"
-                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
-                }
-            }
-        }
-    }
-}
+error('UAT pipeline moved to docs/jenkins/uat/Jenkinsfile — update Jenkins Job Script Path and rebuild.')

+ 4 - 4
docs/jenkins/README-UAT-HARBOR-PUSH.md

@@ -1,4 +1,4 @@
-# UAT 流水线:构建镜像并推送到 Harbor(方案 A)
++# UAT 流水线:构建镜像并推送到 Harbor(方案 A)
 
 在现有 **UAT Maven + jar 部署** 流水线上增加可选阶段,把业务镜像推到:
 
@@ -9,7 +9,7 @@
 
 生产侧 **Gateway-K8s / Alien-Cloud-K8s-Whole** 推荐使用 **`SOURCE_TAG=uat-latest`** 做晋升;需要固定某一版快照时用 `uat-build-<N>`。
 
-脚本:`docs/jenkins/Jenkinsfile-uat-build-deploy.groovy`
+脚本:`docs/jenkins/uat/Jenkinsfile`
 
 ---
 
@@ -77,7 +77,7 @@ echo '<TOKEN>' | docker login 39.105.153.68 -u 'robot$alien_cloud+jenkins-k8s' -
 
 ### 2.4 将脚本提交到 Gitea
 
-本地修改 `docs/jenkins/Jenkinsfile-uat-build-deploy.groovy` 后,push 到 UAT Job 使用的分支(如 `uat-20260202`)。
+本地修改 `docs/jenkins/uat/Jenkinsfile` 后,push 到 UAT Job 使用的分支(如 `uat-20260202`)。
 
 ---
 
@@ -88,7 +88,7 @@ echo '<TOKEN>' | docker login 39.105.153.68 -u 'robot$alien_cloud+jenkins-k8s' -
 | 定义 | Pipeline script from SCM |
 | 仓库 | `http://8.152.195.41:3000/alien/alien_cloud` |
 | 分支 | `*/uat-20260202`(与你们一致) |
-| Script Path | **`docs/jenkins/Jenkinsfile-uat-build-deploy.groovy`** |
+| Script Path | **`docs/jenkins/uat/Jenkinsfile`** |
 | SCM 凭据 | Git(**不要**选 Harbor 机器人) |
 
 保存后先 **立即构建一次**,加载新参数。

+ 1 - 1
docs/jenkins/produ/__alone/Jenkinsfile-prod-promote-from-uat.groovy

@@ -26,7 +26,7 @@
  *   <UAT_DEPLOY_ROOT>/<uatDir>/lib/(若有)
  * 其中 uatDir 与 getServiceDefinitions() 中一致(如 gateway-uat、store-uat)。
  * Jenkins 跑在容器内时(工作区常为 /var/jenkins_home/workspace),宿主机 /alien_uat/java 需挂载进容器。
- * 预生产 Maven+拷贝+restart 流水线见 docs/jenkins/Jenkinsfile-uat-build-deploy.groovy
+ * 预生产 Maven+拷贝+restart 流水线见 docs/jenkins/uat/Jenkinsfile。
  * 【docker compose 启动方式】发版在拷贝 jar/lib 后,在 compose 工作目录下执行
  *   docker compose(V2 插件)或回退到 docker-compose(V1 独立可执行,未装 compose 插件时常用:unknown shorthand flag f in f 多系未装 V2)
  *   若缺 compose 子命令:可填 PROD_DOCKER_COMPOSE_V2_PLUGIN(或把宿主机 .../cli-plugins 挂进 Jenkins,脚本会临时 DOCKER_CONFIG 链到该插件)或 PROD_DOCKER_COMPOSE_PATH(V1 可执行件);或依赖 PROD_DOCKER_COMPOSE_DIND_IMAGE 用 docker run 持 socket 在默认 docker:24.0.9-cli 中执行 compose

+ 455 - 0
docs/jenkins/uat/Jenkinsfile

@@ -0,0 +1,455 @@
+/**
+ * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart.
+ *
+ * Jenkins Job: Pipeline script from SCM
+ * Script Path: docs/jenkins/uat/Jenkinsfile
+ *
+ * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g.
+ *   39.105.153.68/alien_cloud/gateway:uat-latest
+ * Before push: existing uat-latest is archived as uat-build-<BUILD_NUMBER> (same digest).
+ * New image is pushed only as uat-latest. Prod promote: SOURCE_TAG=uat-latest.
+ */
+
+/** HARBOR_PUSH_SCOPE: all-java-services | <repo>-only */
+def filterHarborPushScope(List allServices, String scope) {
+    def s = (scope ?: 'all-java-services').trim()
+    if (s == 'all-java-services') {
+        return allServices
+    }
+    if (s.endsWith('-only')) {
+        def repo = s.substring(0, s.length() - '-only'.length())
+        def picked = allServices.findAll { it.repo == repo }
+        if (picked.isEmpty()) {
+            error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
+        }
+        return picked
+    }
+    error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
+}
+
+def pushOneHarborImage(def script, Map svc, String reg, String proj, String latestTag, String buildTag,
+                      String baseImage, String dockerfile, String workspace) {
+    def jarName = "${svc.module}-1.0.0.jar"
+    def imageLatest = "${reg}/${proj}/${svc.repo}:${latestTag}"
+    def imageBuild = "${reg}/${proj}/${svc.repo}:${buildTag}"
+    def withLibFlag = svc.withLib ? 'true' : 'false'
+    script.sh """
+        set -e
+        export DOCKER_BUILDKIT=1
+        test -f ${workspace}/${svc.module}/target/${jarName}
+        cd ${workspace}/${svc.module}
+        rm -rf .jenkins_docker_ctx && mkdir -p .jenkins_docker_ctx/lib
+        cp -f target/${jarName} .jenkins_docker_ctx/${jarName}
+        if [ "${withLibFlag}" = "true" ] && [ -d target/lib ]; then
+          cp -rf target/lib/. .jenkins_docker_ctx/lib/
+        else
+          touch .jenkins_docker_ctx/lib/.keep
+        fi
+        cd .jenkins_docker_ctx
+        if docker pull ${imageLatest} 2>/dev/null; then
+          echo ">>> archive previous ${latestTag} -> ${buildTag}"
+          docker tag ${imageLatest} ${imageBuild}
+          docker push ${imageBuild}
+        fi
+        docker build -f ${workspace}/${dockerfile} \\
+          --build-arg BASE_IMAGE=${baseImage} \\
+          --build-arg JAR_FILE=${jarName} \\
+          --build-arg SERVER_PORT=${svc.port} \\
+          --build-arg WITH_LIB=${svc.withLib} \\
+          -t ${imageLatest} .
+        docker push ${imageLatest}
+        echo ">>> pushed ${imageLatest} (archived prior latest as ${buildTag} if any)"
+        docker rmi ${imageLatest} 2>/dev/null || true
+        cd ${workspace}/${svc.module}
+        rm -rf .jenkins_docker_ctx
+    """
+}
+
+def deployOneUatService(def script, String moduleName, String dirName, String workspace) {
+    def sourceJar = "${workspace}/${moduleName}/target/${moduleName}-1.0.0.jar"
+    def sourceLib = "${workspace}/${moduleName}/target/lib"
+    def targetDir = "/app_deploy_uat/${dirName}"
+    script.sh """
+        set -e
+        echo ">>> Deploy module: ${moduleName}"
+        if [ -f "${sourceJar}" ]; then
+            mkdir -p "${targetDir}"
+            if [ -d "${sourceLib}" ]; then
+                rm -rf "${targetDir}/lib"
+                cp -rf "${sourceLib}" "${targetDir}"
+            fi
+            cp -f "${sourceJar}" "${targetDir}/"
+            if docker ps -a --format '{{.Names}}' | grep -wq "${dirName}"; then
+                docker restart "${dirName}"
+                echo ">>> [${dirName}] restarted"
+            else
+                echo ">>> [${dirName}] container missing, jar copied only"
+            fi
+        else
+            echo ">>> [${dirName}] jar missing, skip"
+        fi
+    """
+}
+
+/** Delete oldest uat-build-* tags in Harbor, keep newest KEEP. Never deletes uat-latest or current build tag. */
+def pruneHarborUatTags(def script, String reg, String proj, List repoNames, int keepCount, String tagPrefix, String currentBuildTag, String latestTag) {
+    if (repoNames == null || repoNames.isEmpty() || keepCount < 1) {
+        return
+    }
+    def repos = repoNames.join(' ')
+    // POSIX sh only (Jenkins sh step); no mapfile / process substitution
+    script.sh """
+        set -e
+        REG='${reg}'
+        PROJ='${proj}'
+        KEEP=${keepCount}
+        PREFIX='${tagPrefix}'
+        CURRENT='${currentBuildTag}'
+        LATEST='${latestTag}'
+        if ! command -v jq >/dev/null 2>&1; then
+          echo '>>> Harbor prune skipped: jq not installed on Jenkins agent'
+          exit 0
+        fi
+        for repo in ${repos}; do
+          enc_repo=\$(printf '%s' "\${repo}" | jq -sRr @uri)
+          tags_file=\$(mktemp)
+          curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
+            "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts?page_size=100" \\
+            | jq -r '.[] | .tags[]? | .name' | grep "^\${PREFIX}" | sort -t- -k3 -n > "\${tags_file}" || true
+          count=\$(wc -l < "\${tags_file}" | tr -d ' ')
+          echo ">>> prune \${repo}: \${count} tag(s) matching \${PREFIX}*"
+          if [ "\${count}" -le "\${KEEP}" ]; then
+            rm -f "\${tags_file}"
+            continue
+          fi
+          del_count=\$((count - KEEP))
+          deleted=0
+          while IFS= read -r t; do
+            if [ -z "\${t}" ]; then
+              continue
+            fi
+            if [ "\${t}" = "\${CURRENT}" ] || [ "\${t}" = "\${LATEST}" ]; then
+              continue
+            fi
+            if [ "\${deleted}" -ge "\${del_count}" ]; then
+              break
+            fi
+            echo ">>> DELETE Harbor tag \${repo}:\${t}"
+            if ! curl -fsS -X DELETE -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
+              "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${t}/tags/\${t}"; then
+              echo ">>> WARN: delete failed \${repo}:\${t} (check robot delete permission)"
+            fi
+            deleted=\$((deleted + 1))
+          done < "\${tags_file}"
+          rm -f "\${tags_file}"
+        done
+    """
+}
+
+pipeline {
+    agent any
+
+    options {
+        buildDiscarder(logRotator(numToKeepStr: '5'))
+        disableConcurrentBuilds()
+        timestamps()
+        timeout(time: 90, unit: 'MINUTES')
+    }
+
+    parameters {
+        string(
+                name: 'GIT_BRANCH',
+                defaultValue: 'uat-20260202',
+                trim: true,
+                description: 'Git branch, must match remote (e.g. uat-20260202)'
+        )
+        booleanParam(name: 'FORCE_UPDATE', defaultValue: false, description: 'mvn -U (routine builds leave unchecked for speed)')
+        booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps')
+        booleanParam(
+                name: 'PUSH_TO_HARBOR',
+                defaultValue: true,
+                description: 'After Maven: docker build + push to Harbor (tags uat-latest and uat-build-<N>). Uncheck for jar-only UAT deploy.'
+        )
+        choice(
+                name: 'HARBOR_PUSH_SCOPE',
+                choices: [
+                        'all-java-services',
+                        'gateway-only',
+                        'store-only',
+                        'second-only',
+                        'store-platform-only',
+                        'lawyer-only',
+                        'job-only',
+                        'dining-only',
+                ],
+                description: 'Only when PUSH_TO_HARBOR=true; default=all seven; *-only=one service'
+        )
+        string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
+        string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
+        booleanParam(
+                name: 'HARBOR_PRUNE_OLD_TAGS',
+                defaultValue: true,
+                description: 'After push: delete old uat-build-* tags in Harbor, keep last N per repo (never deletes uat-latest)'
+        )
+        string(name: 'HARBOR_KEEP_TAG_COUNT', defaultValue: '10', trim: true,
+                description: 'How many uat-build-* tags to keep per repository')
+        booleanParam(
+                name: 'HARBOR_PUSH_PARALLEL',
+                defaultValue: true,
+                description: 'Parallel docker build/push per service (faster; needs enough CPU/disk on agent)'
+        )
+    }
+
+    environment {
+        MAVEN_HOME = tool '3.6.3'
+        PATH = "${MAVEN_HOME}/bin:${env.PATH}"
+        GIT_URL = 'http://8.152.195.41:3000/alien/alien_cloud'
+        GIT_CREDENTIALS = 'zhanghaomimapingzheng'
+        HARBOR_CREDENTIALS = 'harbor-robot-alien'
+        UAT_HARBOR_LATEST_TAG = 'uat-latest'
+        UAT_HARBOR_BUILD_TAG = "uat-build-${env.BUILD_NUMBER}"
+        DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service'
+        MAVEN_LOCAL_REPO = '/var/jenkins_home/.m2/repository'
+    }
+
+    stages {
+        stage('Checkout') {
+            steps {
+                script {
+                    def branch = (params.GIT_BRANCH ?: 'uat-20260202').trim()
+                    if (!branch) {
+                        error('GIT_BRANCH is required')
+                    }
+                    env.GIT_BRANCH = branch
+                    echo ">>> Checkout branch: ${env.GIT_BRANCH}"
+                    git branch: "${env.GIT_BRANCH}",
+                            credentialsId: "${env.GIT_CREDENTIALS}",
+                            url: "${env.GIT_URL}"
+                    sh """
+                        set -e
+                        git fetch origin
+                        git reset --hard origin/${env.GIT_BRANCH}
+                        git log -1 --oneline
+                    """
+                }
+            }
+        }
+
+        stage('Prepare Maven Settings') {
+            steps {
+                script {
+                    writeFile file: 'settings.xml', text: """<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
+    <mirrors>
+        <mirror>
+            <id>aliyun-central</id>
+            <mirrorOf>central</mirrorOf>
+            <name>Aliyun Central</name>
+            <url>https://maven.aliyun.com/repository/central</url>
+        </mirror>
+    </mirrors>
+    <profiles>
+        <profile>
+            <id>repo-mix</id>
+            <repositories>
+                <repository>
+                    <id>aliyunmaven</id>
+                    <name>Aliyun Maven</name>
+                    <url>https://maven.aliyun.com/repository/public</url>
+                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </repository>
+                <repository>
+                    <id>central</id>
+                    <name>Maven Central</name>
+                    <url>https://repo.maven.apache.org/maven2</url>
+                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </repository>
+                <repository>
+                    <id>spring-milestones</id>
+                    <name>Spring Milestones</name>
+                    <url>https://repo.spring.io/milestone</url>
+                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </repository>
+                <repository>
+                    <id>spring-snapshots</id>
+                    <name>Spring Snapshots</name>
+                    <url>https://repo.spring.io/snapshot</url>
+                    <releases><enabled>false</enabled></releases>
+                    <snapshots><enabled>true</enabled><updatePolicy>daily</updatePolicy></snapshots>
+                </repository>
+            </repositories>
+            <pluginRepositories>
+                <pluginRepository>
+                    <id>aliyunmaven</id>
+                    <url>https://maven.aliyun.com/repository/public</url>
+                    <releases><enabled>true</enabled></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </pluginRepository>
+                <pluginRepository>
+                    <id>central</id>
+                    <url>https://repo.maven.apache.org/maven2</url>
+                    <releases><enabled>true</enabled></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </pluginRepository>
+                <pluginRepository>
+                    <id>spring-milestones</id>
+                    <url>https://repo.spring.io/milestone</url>
+                    <releases><enabled>true</enabled></releases>
+                    <snapshots><enabled>false</enabled></snapshots>
+                </pluginRepository>
+            </pluginRepositories>
+        </profile>
+    </profiles>
+    <activeProfiles>
+        <activeProfile>repo-mix</activeProfile>
+    </activeProfiles>
+</settings>
+"""
+                }
+            }
+        }
+
+        stage('Maven Build') {
+            steps {
+                script {
+                    def updateFlag = params.FORCE_UPDATE ? '-U' : ''
+                    retry(2) {
+                        sh """
+                            set -e
+                            mvn -version
+                            unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY || true
+                            export MAVEN_OPTS="-Xms512m -Xmx2048m -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
+                            mkdir -p ${MAVEN_LOCAL_REPO}
+                            mvn clean package -DskipTests -s settings.xml ${updateFlag} -e \\
+                              -T 1C -Dmaven.artifact.threads=8 \\
+                              -Dmaven.repo.local=${MAVEN_LOCAL_REPO}
+                        """
+                    }
+                }
+            }
+        }
+
+        stage('Push images to Harbor') {
+            when {
+                expression { return params.PUSH_TO_HARBOR == true }
+            }
+            steps {
+                script {
+                    def reg = params.HARBOR_REGISTRY.trim()
+                    def proj = params.HARBOR_PROJECT.trim()
+                    def latestTag = env.UAT_HARBOR_LATEST_TAG
+                    def buildTag = env.UAT_HARBOR_BUILD_TAG
+                    def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1"
+                    def dockerfile = env.DOCKERFILE_JAVA
+
+                    def allHarborServices = [
+                            [module: 'alien-gateway',        repo: 'gateway',        port: '8000',  withLib: false],
+                            [module: 'alien-store',          repo: 'store',          port: '50014', withLib: true],
+                            [module: 'alien-second',         repo: 'second',         port: '50015', withLib: false],
+                            [module: 'alien-store-platform', repo: 'store-platform', port: '50016', withLib: false],
+                            [module: 'alien-lawyer',         repo: 'lawyer',         port: '50017', withLib: true],
+                            [module: 'alien-job',            repo: 'job',            port: '50108', withLib: false],
+                            [module: 'alien-dining',         repo: 'dining',         port: '50019', withLib: false],
+                    ]
+                    def harborServices = filterHarborPushScope(allHarborServices, params.HARBOR_PUSH_SCOPE)
+                    echo ">>> HARBOR_PUSH_SCOPE=${params.HARBOR_PUSH_SCOPE} repos=${harborServices*.repo.join(',')}"
+
+                    withCredentials([usernamePassword(
+                            credentialsId: env.HARBOR_CREDENTIALS,
+                            usernameVariable: 'HARBOR_USER',
+                            passwordVariable: 'HARBOR_PASS',
+                    )]) {
+                        sh """
+                            set -e
+                            echo "\${HARBOR_PASS}" | docker login ${reg} -u "\${HARBOR_USER}" --password-stdin
+                            echo ">>> docker disk before Harbor push:"
+                            df -h /var/lib/docker 2>/dev/null || df -h / || true
+                            docker system prune -f --filter until=48h 2>/dev/null || true
+                        """
+                        if (params.HARBOR_PUSH_PARALLEL) {
+                            def pushBranches = [:]
+                            harborServices.each { svc ->
+                                def s = svc
+                                pushBranches[s.repo] = {
+                                    pushOneHarborImage(
+                                        this, s, reg, proj, latestTag, buildTag,
+                                        baseImage, dockerfile, env.WORKSPACE,
+                                    )
+                                }
+                            }
+                            parallel pushBranches
+                        } else {
+                            harborServices.each { svc ->
+                                pushOneHarborImage(
+                                    this, svc, reg, proj, latestTag, buildTag,
+                                    baseImage, dockerfile, env.WORKSPACE,
+                                )
+                            }
+                        }
+                        if (params.HARBOR_PRUNE_OLD_TAGS == true) {
+                            def keepN = (params.HARBOR_KEEP_TAG_COUNT ?: '10').trim() as int
+                            catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
+                                pruneHarborUatTags(
+                                    this, reg, proj, harborServices*.repo,
+                                    keepN, 'uat-build-', buildTag, latestTag,
+                                )
+                            }
+                        }
+                    }
+                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}; archived tag this run: ${env.UAT_HARBOR_BUILD_TAG}"
+                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
+                }
+            }
+        }
+
+        stage('Deploy Services') {
+            steps {
+                script {
+                    def services = [
+                            'alien-gateway:gateway-uat',
+                            'alien-job:job-uat',
+                            'alien-lawyer:lawyer-uat',
+                            'alien-second:second-uat',
+                            'alien-store:store-uat',
+                            'alien-dining:dining-uat',
+                            'alien-store-platform:store-platform-uat',
+                    ]
+
+                    def deployBranches = [:]
+                    services.each { item ->
+                        def parts = item.split(':')
+                        def moduleName = parts[0]
+                        def dirName = parts[1]
+                        deployBranches[dirName] = {
+                            deployOneUatService(this, moduleName, dirName, env.WORKSPACE)
+                        }
+                    }
+                    parallel deployBranches
+                }
+            }
+        }
+    }
+
+    post {
+        always {
+            sh 'rm -f settings.xml || true'
+            script {
+                if (!params.PUSH_TO_HARBOR) {
+                    echo '>>> Harbor push SKIPPED: PUSH_TO_HARBOR is false. On "Build with Parameters" check PUSH_TO_HARBOR.'
+                }
+            }
+        }
+        success {
+            script {
+                if (params.PUSH_TO_HARBOR) {
+                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}"
+                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
+                }
+            }
+        }
+    }
+}

+ 47 - 0
docs/jenkins/uat/README.md

@@ -0,0 +1,47 @@
+# UAT Jenkins 流水线(Pipeline script from SCM)
+
+## Jenkins Job 配置
+
+| 项 | 值 |
+|----|-----|
+| 定义 | **Pipeline script from SCM** |
+| SCM | Git `http://8.152.195.41:3000/alien/alien_cloud` |
+| 分支 | `*/uat-20260202`(与你们实际 UAT 分支一致) |
+| **Script Path** | **`docs/jenkins/uat/Jenkinsfile`** |
+| SCM 凭据 | `zhanghaomimapingzheng` |
+
+保存后 **Build with Parameters 构建一次**,加载新参数(`HARBOR_PUSH_PARALLEL`、`FORCE_UPDATE` 默认 false 等)。
+
+## 与旧内嵌脚本的区别
+
+| 项 | 旧内嵌脚本 | 本脚本 |
+|----|-----------|--------|
+| Maven 本地仓库 | `${WORKSPACE}/.m2/repository`(冷缓存) | `/var/jenkins_home/.m2/repository`(持久共享) |
+| 依赖镜像 | 仅 Maven Central | 阿里云 mirror + Central |
+| 构建前删 BOM | `rm -rf spring-cloud-dependencies` | 无 |
+| `FORCE_UPDATE` | 默认 **true** | 默认 **false** |
+| Maven 并行 | 无 | `-T 1C -Dmaven.artifact.threads=8` |
+| Harbor push | 串行 | 可并行(`HARBOR_PUSH_PARALLEL`) |
+| Docker build | 旧 builder | `DOCKER_BUILDKIT=1` |
+| Deploy | 串行 7 次 `sh` | `parallel` |
+| 并发构建 | 允许(产生 `@2` 工作区) | `disableConcurrentBuilds()` |
+| Harbor prune | bash `mapfile`(易失败) | POSIX `sh` + `catchError` |
+| 构建历史 | 15 | **5** |
+
+## 验证新脚本已生效
+
+构建日志 Maven 阶段应出现:
+
+```text
+mkdir -p /var/jenkins_home/.m2/repository
+mvn clean package ... -T 1C -Dmaven.artifact.threads=8 ... -Dmaven.repo.local=/var/jenkins_home/.m2/repository
+```
+
+**不应**再出现:
+
+```text
+rm -rf .../spring-cloud-dependencies/Hoxton.SR1
+-Dmaven.repo.local=/var/jenkins_home/workspace/.../.m2/repository
+```
+
+Harbor 相关说明见 [README-UAT-HARBOR-PUSH.md](../README-UAT-HARBOR-PUSH.md)。