Преглед изворни кода

Merge remote-tracking branch 'origin/sit' into uat-20260202

# Conflicts:
#	docs/jenkins/produ/README.md
#	docs/jenkins/produ/docker/Dockerfile.java-service
dujian пре 2 недеља
родитељ
комит
73cc71df03

+ 277 - 0
docs/jenkins/Jenkinsfile-uat-build-deploy.groovy

@@ -0,0 +1,277 @@
+/**
+ * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart.
+ *
+ * Jenkins Job: Pipeline script from SCM
+ * Script Path: docs/jenkins/Jenkinsfile-uat-build-deploy.groovy
+ *
+ * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g.
+ *   39.105.153.68/alien_cloud/gateway:uat-build-<BUILD_NUMBER>
+ * Production promote jobs use SOURCE_TAG=uat-build-<same number>.
+ */
+pipeline {
+    agent any
+
+    options {
+        buildDiscarder(logRotator(numToKeepStr: '15'))
+        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: true, description: 'mvn -U')
+        booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps')
+        booleanParam(
+                name: 'PUSH_TO_HARBOR',
+                defaultValue: false,
+                description: 'After Maven: docker build + push to 39.105.153.68/alien_cloud (tag uat-build-<BUILD_NUMBER>)'
+        )
+        choice(
+                name: 'HARBOR_PUSH_SCOPE',
+                choices: ['gateway-only', 'all-java-services'],
+                description: 'Only used when PUSH_TO_HARBOR=true'
+        )
+        string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
+        string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
+    }
+
+    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_IMAGE_TAG = "uat-build-${env.BUILD_NUMBER}"
+        DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service'
+    }
+
+    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">
+    <profiles>
+        <profile>
+            <id>repo-mix</id>
+            <repositories>
+                <repository>
+                    <id>central</id>
+                    <name>Maven Central</name>
+                    <url>https://repo.maven.apache.org/maven2</url>
+                    <releases><enabled>true</enabled><updatePolicy>always</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>always</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>always</updatePolicy></snapshots>
+                </repository>
+            </repositories>
+            <pluginRepositories>
+                <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="-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
+                            rm -rf /root/.m2/repository/org/springframework/cloud/spring-cloud-dependencies/Hoxton.SR1 || true
+                            rm -rf /root/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.3.2.RELEASE || true
+                            rm -rf ${WORKSPACE}/.m2/repository/org/springframework/cloud/spring-cloud-dependencies/Hoxton.SR1 || true
+                            rm -rf ${WORKSPACE}/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.3.2.RELEASE || true
+                            mvn clean package -DskipTests -s settings.xml ${updateFlag} -e -Dmaven.repo.local=${WORKSPACE}/.m2/repository
+                        """
+                    }
+                }
+            }
+        }
+
+        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 tag = env.UAT_HARBOR_IMAGE_TAG
+                    def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1"
+                    def dockerfile = env.DOCKERFILE_JAVA
+
+                    def harborServices = [
+                            [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],
+                    ]
+                    if (params.HARBOR_PUSH_SCOPE == 'gateway-only') {
+                        harborServices = harborServices.findAll { it.repo == 'gateway' }
+                    }
+
+                    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
+                        """
+                        harborServices.each { svc ->
+                            def jarName = "${svc.module}-1.0.0.jar"
+                            def imageRef = "${reg}/${proj}/${svc.repo}:${tag}"
+                            sh """
+                                set -e
+                                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 [ -d target/lib ]; then
+                                  cp -rf target/lib/. .jenkins_docker_ctx/lib/
+                                else
+                                  touch .jenkins_docker_ctx/lib/.keep
+                                fi
+                                cd .jenkins_docker_ctx
+                                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 ${imageRef} .
+                                docker push ${imageRef}
+                                echo ">>> pushed ${imageRef}"
+                            """
+                        }
+                    }
+                    echo ">>> Harbor tag for prod promote: SOURCE_TAG=${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',
+                    ]
+
+                    for (item in services) {
+                        def parts = item.split(':')
+                        def moduleName = parts[0]
+                        def dirName = parts[1]
+                        def sourceJar = "${env.WORKSPACE}/${moduleName}/target/${moduleName}-1.0.0.jar"
+                        def sourceLib = "${env.WORKSPACE}/${moduleName}/target/lib"
+                        def targetDir = "/app_deploy_uat/${dirName}"
+
+                        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
+                        """
+                    }
+                }
+            }
+        }
+    }
+
+    post {
+        always {
+            sh 'rm -f settings.xml || true'
+        }
+        success {
+            script {
+                if (params.PUSH_TO_HARBOR) {
+                    echo ">>> Harbor images tagged: ${env.UAT_HARBOR_IMAGE_TAG}"
+                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_IMAGE_TAG}"
+                }
+            }
+        }
+    }
+}

+ 161 - 0
docs/jenkins/README-UAT-HARBOR-PUSH.md

@@ -0,0 +1,161 @@
+# UAT 流水线:构建镜像并推送到 Harbor(方案 A)
+
+在现有 **UAT Maven + jar 部署** 流水线上增加可选阶段,把业务镜像推到:
+
+```text
+39.105.153.68/alien_cloud/<服务名>:uat-build-<Jenkins构建号>
+```
+
+生产侧 **Gateway-K8s / Alien-Cloud-K8s-Whole** 使用同一 tag 作为 `SOURCE_TAG` 做晋升。
+
+脚本:`docs/jenkins/Jenkinsfile-uat-build-deploy.groovy`
+
+---
+
+## 一、整体流程
+
+```text
+UAT Jenkins Job(88:30003)
+  1. Checkout(拉 alien_cloud 预生产分支)
+  2. Maven Build(mvn clean package)
+  3. Push images to Harbor(PUSH_TO_HARBOR=true 时)
+       docker build → push 39.105.153.68/alien_cloud/gateway:uat-build-42
+  4. Deploy Services(拷 jar 到 /app_deploy_uat + docker restart,与现网一致)
+
+Harbor Web(153.68)出现 gateway 仓库
+
+生产 Jenkins(同机,另一文件夹)
+  Gateway-K8s / Whole:SOURCE_TAG=uat-build-42 → produ-xxx → ACK
+```
+
+---
+
+## 二、一次性准备(运维 / Jenkins 管理员)
+
+### 2.1 Harbor(153.68)
+
+- [x] 项目 `alien_cloud` 已存在  
+- [x] 基础镜像 `alien_cloud/base/openjdk8-ffmpeg:v1` 已 push  
+- [ ] 机器人 `robot$alien_cloud+jenkins-k8s` 具备 **推送** 权限  
+
+### 2.2 Jenkins 凭据(全局,UAT / 生产 Job 共用)
+
+| ID | 类型 | 内容 |
+|----|------|------|
+| `harbor-robot-alien` | Username with password | 用户 `robot$alien_cloud+jenkins-k8s`,密码=Token |
+| `zhanghaomimapingzheng` | Git | UAT Job SCM 已在使用 |
+
+### 2.3 Jenkins 执行机 Docker(通常在 88 本机或 Jenkins 容器内)
+
+```bash
+# 探测 Registry
+curl -sI http://39.105.153.68/v2/
+
+# 登录(与凭据一致)
+echo '<TOKEN>' | docker login 39.105.153.68 -u 'robot$alien_cloud+jenkins-k8s' --password-stdin
+```
+
+若 HTTP + IP 访问,需在 **运行 docker 的宿主机** `/etc/docker/daemon.json` 增加:
+
+```json
+{
+  "insecure-registries": ["39.105.153.68"]
+}
+```
+
+然后 `systemctl restart docker`(注意:若在 153.68 上误执行会影响 Harbor,**应在 88 Jenkins 节点** 配置)。
+
+### 2.4 将脚本提交到 Gitea
+
+本地修改 `docs/jenkins/Jenkinsfile-uat-build-deploy.groovy` 后,push 到 UAT Job 使用的分支(如 `uat-20260202`)。
+
+---
+
+## 三、配置 UAT Jenkins Job
+
+| 项 | 值 |
+|----|-----|
+| 定义 | Pipeline script from SCM |
+| 仓库 | `http://8.152.195.41:3000/alien/alien_cloud` |
+| 分支 | `*/uat-20260202`(与你们一致) |
+| Script Path | **`docs/jenkins/Jenkinsfile-uat-build-deploy.groovy`** |
+| SCM 凭据 | Git(**不要**选 Harbor 机器人) |
+
+保存后先 **立即构建一次**,加载新参数。
+
+---
+
+## 四、Build with Parameters(推荐首次 gateway)
+
+| 参数 | 首次试跑建议 |
+|------|----------------|
+| `GIT_BRANCH` | `uat-20260202` |
+| `FORCE_UPDATE` | 按需 |
+| **`PUSH_TO_HARBOR`** | **`true`** |
+| **`HARBOR_PUSH_SCOPE`** | **`gateway-only`** |
+| `HARBOR_REGISTRY` | `39.105.153.68` |
+| `HARBOR_PROJECT` | `alien_cloud` |
+
+构建成功后日志末尾应有:
+
+```text
+>>> pushed 39.105.153.68/alien_cloud/gateway:uat-build-<N>
+>>> Prod promote: SOURCE_TAG=uat-build-<N>
+```
+
+记下 **`<N>`** = Jenkins 构建号。
+
+---
+
+## 五、验证 Harbor
+
+浏览器打开:`http://39.105.153.68/harbor/` → 项目 **alien_cloud** → 镜像仓库应出现 **`gateway`**,标签 **`uat-build-<N>`**。
+
+或命令行:
+
+```bash
+curl -s -u 'robot$alien_cloud+jenkins-k8s:<TOKEN>' \
+  'http://39.105.153.68/v2/alien_cloud/gateway/tags/list'
+```
+
+---
+
+## 六、接生产晋升流水线
+
+在 **Gateway-K8s** 或 **Alien-Cloud-K8s-Whole**:
+
+| 参数 | 值 |
+|------|-----|
+| `SOURCE_TAG` | `uat-build-<N>`(与 UAT 构建号一致) |
+| `DEPLOY_STRATEGY` | 先 `skip`(只晋升 Harbor),再 `rolling` |
+| `DEPLOY_MODE` | 单服务用 `single` + `gateway` |
+
+---
+
+## 七、参数说明
+
+| 参数 | 说明 |
+|------|------|
+| `PUSH_TO_HARBOR` | `false`=保持旧行为,只 jar 部署;`true`=增加 Harbor push |
+| `HARBOR_PUSH_SCOPE` | `gateway-only` 只推 gateway;`all-java-services` 推七个服务(Maven 须全部打包成功) |
+| `UAT_HARBOR_IMAGE_TAG` | 自动 `uat-build-${BUILD_NUMBER}`,无需手填 |
+
+---
+
+## 八、常见问题
+
+| 现象 | 处理 |
+|------|------|
+| `not found` pull 基础镜像 | 确认 Harbor 有 `base/openjdk8-ffmpeg:v1` |
+| `Login Succeeded` 后 push 失败 | 机器人是否有项目 push 权限 |
+| `x509` / 证书错误 | 88 上 `insecure-registries` 或 HTTPS + ca.crt |
+| Harbor 仍无 gateway | 是否勾选了 `PUSH_TO_HARBOR`;Maven 是否打出 `alien-gateway/target/*.jar` |
+| 生产 Whole 仍 not found | `SOURCE_TAG` 必须与 UAT 构建日志中的 `uat-build-<N>` 完全一致 |
+
+---
+
+## 九、与现网 jar 部署的关系
+
+- **Deploy Services** 阶段仍会拷 jar 并 `docker restart` UAT 容器,**不影响**现有 UAT 访问方式。  
+- Harbor 镜像供 **ACK / 生产晋升** 使用,两套路径可并行。  
+- 日常若只更新 UAT 机 jar、暂不发 ACK,可 `PUSH_TO_HARBOR=false`。

+ 124 - 0
docs/jenkins/produ/README-HARBOR-SETUP.md

@@ -0,0 +1,124 @@
+# Harbor:alien_cloud 项目与 Jenkins 对接
+
+> **重要:** Harbor Web 与 Registry 在 **39.105.153.68**(浏览器如 `http://39.105.153.68/harbor/...`)。  
+> **不是** 39.106.135.88。`docker login` / `HARBOR_REGISTRY` 须用 **153.68**。
+
+Jenkins(示例):`http://39.106.135.88:30003` — 可与 Harbor 分机部署,网络互通即可。
+
+---
+
+## 当前进度(对照打勾)
+
+- [x] Harbor 项目 `alien_cloud`(私有)
+- [x] 机器人 `robot$alien_cloud+jenkins-k8s`(拉取/推送等权限)
+- [ ] Token 写入 Jenkins 凭据 `harbor-robot-alien`
+- [ ] 在 **有 Docker 的机器** 上 `docker login` **39.105.153.68** 成功
+- [ ] 推送基础镜像 `39.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1`
+- [ ] UAT Job 推业务镜像(见 [README-UAT-HARBOR-PUSH.md](../README-UAT-HARBOR-PUSH.md))
+- [ ] Jenkins 凭据 `ack-kubeconfig-alien` + Job 试跑
+- [ ] ACK 命名空间 / Deployment / imagePullSecret
+
+---
+
+## 一、保存机器人 Token → Jenkins
+
+Harbor 2.x 里 **机器人名称不能点击**,这是正常现象;Token **不会**在详情页里长期展示。
+
+### 获取 Token 的两种方式
+
+**方式 A:重新生成密钥(推荐,保留原机器人)**
+
+1. 在 **机器人账户** 列表中,**勾选**左侧复选框(选中 `robot$alien_cloud+jenkins-k8s` 那一行)。  
+2. 点表格上方的 **「其他操作」** 下拉菜单。  
+3. 选择 **「重新生成密钥」** / **「Regenerate secret」** / **「刷新令牌」**(中文版措辞可能略有不同)。  
+4. 弹出框里会显示 **新 Token**,**立即复制**(关闭后无法再查看)。
+
+**方式 B:新建一个机器人**
+
+1. **+ 添加机器人账户**,名称如 `jenkins-k8s-2`,权限与现网相同。  
+2. 点 **完成** 后弹窗里的 Token **只出现一次**,务必当场复制。  
+3. Jenkins 凭据里改用新用户名;旧的 `jenkins-k8s` 可之后在 Harbor 里禁用或删除。
+
+若勾选后 **「其他操作」** 里没有「重新生成」:试该行最右侧 **⋮** 菜单,或换用 **admin** 账号登录。
+
+### 写入 Jenkins
+
+1. Jenkins 文件夹 **凭据** → **添加**:
+   - 类型:Username with password  
+   - **ID**:`harbor-robot-alien`  
+   - **用户名**:`robot$alien_cloud+jenkins-k8s`  
+   - **密码**:Token  
+
+---
+
+## 二、探测 Registry(在 Jenkins 所在机或 153.68 上)
+
+```bash
+curl -sI http://39.105.153.68/v2/
+curl -skI https://39.105.153.68/v2/
+```
+
+| 结果 | 做法 |
+|------|------|
+| **401** | 正常,继续 `docker login 39.105.153.68` |
+| **301** 到 https | 用 **https** 登录,勿对 153.68 配 `insecure-registries` 走 HTTP |
+| **404** | 把完整 `curl` 输出给运维;或试浏览器同协议的主机名 |
+
+登录(推荐):
+
+```bash
+echo '<TOKEN>' | docker login 39.105.153.68 -u 'robot$alien_cloud+jenkins-k8s' --password-stdin
+```
+
+若 HTTPS 证书报错:在 `/etc/docker/certs.d/39.105.153.68/ca.crt` 放 Harbor CA,或请运维提供证书;**优先不要用错 88 的 insecure-registries**。
+
+---
+
+## 三、推送基础镜像(153.68 上常有 `my-openjdk8-ffmpeg:v1`)
+
+```bash
+docker images | grep openjdk8-ffmpeg
+
+docker tag my-openjdk8-ffmpeg:v1 39.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1
+docker push 39.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1
+```
+
+Harbor Web → **alien_cloud** → 应看到 `base/openjdk8-ffmpeg`。
+
+---
+
+## 三.1、UAT 首次推送 gateway 镜像(方案 A)
+
+见 **[README-UAT-HARBOR-PUSH.md](../README-UAT-HARBOR-PUSH.md)**:在 UAT Job 勾选 `PUSH_GATEWAY_TO_HARBOR`,产出 `gateway:uat-build-<构建号>` 后,再跑生产晋升 Job。
+
+---
+
+## 四、Jenkins 参数(Build with Parameters)
+
+| 参数 | 值 |
+|------|-----|
+| `HARBOR_REGISTRY` | **`39.105.153.68`** |
+| `HARBOR_PROJECT` | `alien_cloud` |
+| `BASE_IMAGE` | `39.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1` |
+
+Git 凭据:`zhanghaomimapingzheng`(或你 Job SCM 里已选中的)。
+
+---
+
+## 五、下一步总序
+
+1. **本步**:Jenkins 凭据 `harbor-robot-alien` + 上节 `docker login` / `push` 基础镜像  
+2. 凭据 **`ack-kubeconfig-alien`**(ACK 下载的 kubeconfig)  
+3. Job **`gateway-k8s`** 或 **`alien-cloud-k8s-whole`**:先 `DRY_RUN`,再真实构建  
+4. ACK:`kubectl apply` namespace、harbor pull secret、deployment(见 README-ACK-GRAY-RELEASE.md)
+
+---
+
+## 六、两台机器分工
+
+| IP | 角色 |
+|----|------|
+| **39.105.153.68** | Harbor、现网 *-produ compose、Registry **login/push 目标** |
+| **39.106.135.88** | Jenkins :30003、UAT 等(构建机可 SSH 到 153.68 做 login 测试) |
+
+在 **88** 上构建时:Jenkins 容器/宿主机的 Docker 必须能访问 **153.68** 的 Registry(安全组放行)。

+ 63 - 0
docs/jenkins/produ/README-JOB-GATEWAY.md

@@ -0,0 +1,63 @@
+# Gateway-K8s Job 配置清单(对照 Jenkins 界面)
+
+目标:从 Harbor 取 **预生产 gateway 镜像** → 打上生产 tag → 推到 **39.105.153.68** → 可选更新 **ACK**。
+
+---
+
+## 常规(General)
+
+| 项 | 建议 | 说明 |
+|----|------|------|
+| 描述 | `gateway 镜像晋升 → ACK` | 可选 |
+| GitHub project | 不勾 | — |
+| Gogs secret | 可保留 | 仅 Webhook 用;手动发版可不配触发器 |
+| **参数化构建过程** | **可不勾** | 参数在 Jenkinsfile `parameters {}` 里;**首次 Build 一次**后会出现「Build with Parameters」 |
+| 丢弃旧的构建 | 建议勾,保留 15 次 | 与 Jenkinsfile `buildDiscarder` 一致 |
+| 不允许并发构建 | 按需 | gateway 单服务一般可勾,避免两次同时改 ACK |
+
+---
+
+## 流水线(Pipeline)
+
+| 项 | 应填 | 你截图中的注意点 |
+|----|------|------------------|
+| 定义 | **Pipeline script from SCM** | ✓ 正确 |
+| SCM | Git | ✓ |
+| Repository URL | `http://8.152.195.41:3000/alien/alien_cloud` | 须为 **`alien_cloud`(下划线)**,不要 `alien cloud`(空格) |
+| Credentials | **`dujian/****** (git)`** 等 Git 凭据 | ✓ 正确;**不要**选 `robot$alien_cloud+jenkins-k8s` |
+| 分支 | `*/uat-20260202` | 与 Gitea 实际分支一致;避免 `uat 20260202`(空格) |
+| Script Path | `docs/jenkins/produ/gateway/Jenkinsfile` | ✓ 正确 |
+| **轻量级检出** | **建议勾选** | 只拉 Jenkinsfile;脚本内再**稀疏检出** `_shared`,不拉 `alien-gateway` 等源码 |
+
+Harbor / ACK 凭据不在 SCM 里配,在 Jenkins **凭据管理**:
+
+| ID | 类型 | 用途 |
+|----|------|------|
+| `harbor-robot-alien` | Username/Password | `robot$alien_cloud+jenkins-k8s` + Token |
+| `ack-kubeconfig-alien` | Secret file | ACK kubeconfig |
+
+---
+
+## 构建触发器
+
+| 项 | 建议 |
+|----|------|
+| 全部 | 发版以 **手动 Build with Parameters** 为主时可全不勾 |
+
+---
+
+## 首次运行参数示例
+
+| 参数 | 示例 |
+|------|------|
+| `SOURCE_TAG` | `uat-build-42`(UAT 已推到 Harbor 的 tag) |
+| `TARGET_TAG` | 留空 → `produ-<构建号>` |
+| `DEPLOY_STRATEGY` | 先试 **`skip`**(只晋升 Harbor);确认后再 `rolling` |
+| `DRY_RUN` | 第一次可勾,只看计划 |
+
+---
+
+## 与旧版 build 型 Job 的区别
+
+本 Job **不会** Maven、不会 `docker build`、不会 Checkout `alien-gateway` 源码目录。  
+预生产镜像须已由 UAT 流水线推到:`39.105.153.68/alien_cloud/gateway:<SOURCE_TAG>`。

+ 108 - 0
docs/jenkins/produ/README-JOB-WHOLE.md

@@ -0,0 +1,108 @@
+# Alien-Cloud-K8s-Whole Job 配置清单
+
+目标:**一次 Job** 支持 `whole` / `single` / `multi`,把预生产 Harbor 镜像晋升到生产 tag,并可选批量更新 ACK。  
+**不**拉 `alien-*` 业务源码、**不** Maven、**不** docker build。
+
+与单服务 **Gateway-K8s** 的区别:Whole 管七个服务组合;日常只发 gateway 用 `gateway/Jenkinsfile` 即可。
+
+---
+
+## 常规(General)
+
+| 项 | 建议 | 你截图 |
+|----|------|--------|
+| 描述 | `预生产分支 → Harbor 晋升 → ACK;whole/single/multi` | ✓ 已写,可保留 |
+| **参数化构建过程** | **不必勾** | 未勾 ✓;`DEPLOY_MODE` 等在 Jenkinsfile `parameters {}` 里 |
+| 丢弃旧的构建 | 建议勾,保留 **15** | 与 Jenkinsfile `buildDiscarder` 一致 |
+| **不允许并发构建** | **建议勾** | 未勾 → 建议勾,避免两次 whole 同时改 ACK |
+| Gogs / 触发器 | 手动发版可全不勾 | ✓ |
+
+首次保存后请 **立即构建一次**,之后左侧会出现 **「Build with Parameters」**。
+
+---
+
+## 流水线(Pipeline)
+
+| 项 | 应填 | 你截图 |
+|----|------|--------|
+| 定义 | Pipeline script from SCM | ✓ |
+| Repository URL | `http://8.152.195.41:3000/alien/alien_cloud` | 若显示 `alien cloud`(空格)请改为 **`alien_cloud`** |
+| Credentials | `dujian (git)` | ✓;**不要** Harbor 机器人 |
+| 分支 | `*/uat-20260202` | ✓(与预生产分支一致) |
+| Script Path | **`docs/jenkins/produ/whole/Jenkinsfile`** | ✓ |
+| **轻量级检出** | **勾选** | ✓;脚本内再稀疏拉 `docs/jenkins/produ/_shared/` |
+
+凭据(全局,非 SCM):
+
+| ID | 用途 |
+|----|------|
+| `harbor-robot-alien` | docker login 153.68 |
+| `ack-kubeconfig-alien` | kubectl |
+
+---
+
+## Jenkinsfile 三阶段(与界面无关,自动执行)
+
+```text
+Plan            → 解析 DEPLOY_MODE,打印将晋升的服务列表
+Promote images  → docker pull/tag/push(七个或子集)
+Deploy to ACK   → DEPLOY_STRATEGY≠skip 且非 DRY_RUN 时 kubectl set image
+```
+
+---
+
+## 参数用法(Build with Parameters)
+
+### DEPLOY_MODE
+
+| 模式 | 行为 | 还需填写 |
+|------|------|----------|
+| **whole** | 七个 Java 服务全部晋升 | 忽略 SINGLE / MULTI_* |
+| **single** | 只晋升一个 | `SINGLE_SERVICE=gateway` 等 |
+| **multi** | 勾选多个 | 至少勾一项 `MULTI_gateway` … `MULTI_dining` |
+
+### 镜像 tag(七个服务共用同一 SOURCE_TAG)
+
+| 参数 | 说明 |
+|------|------|
+| `SOURCE_TAG` | UAT 已在 Harbor 的 tag,如 `uat-build-42` |
+| `TARGET_TAG` | 留空 → `produ-<构建号>`;七个服务打**同一**生产 tag |
+| `DEPLOY_STRATEGY` | `skip` 只推 Harbor;`rolling` 滚更 ACK;`canary` 灰度 |
+| `DRY_RUN` | 只 Plan,不 pull/push |
+
+### 推荐试跑顺序
+
+1. `DEPLOY_MODE=single`,`SINGLE_SERVICE=gateway`,`DRY_RUN=true` → 看 Plan  
+2. 同上,`DRY_RUN=false`,`DEPLOY_STRATEGY=skip` → 只晋升 gateway 镜像  
+3. `DEPLOY_MODE=whole`,`DEPLOY_STRATEGY=skip` → 七个服务只推 Harbor  
+4. 确认 Harbor 后,`DEPLOY_STRATEGY=rolling` → 更新 ACK  
+
+---
+
+## Whole Job vs 七个单服务 Job
+
+| 场景 | 用哪个 Job |
+|------|------------|
+| 只发 gateway | **Gateway-K8s**(更简单) |
+| 一次发七个 / 多选两三个 | **Alien-Cloud-K8s-Whole** |
+| 与 prod-promote 习惯一致 | Whole 的 `DEPLOY_MODE` 同 `Jenkinsfile-prod-promote-from-uat` |
+
+勿再建 `promote-image` Job(与 whole 脚本重复),二选一即可。
+
+---
+
+## 前置条件
+
+1. UAT 已将七个镜像推到 `39.105.153.68/alien_cloud/<服务>:<SOURCE_TAG>`(可只推部分,但 whole 会对缺失镜像 pull 失败)  
+2. Jenkins 节点(88)`insecure-registries` + `harbor-robot-alien` 已配置  
+3. Gitea 上 `uat-20260202` 分支已包含最新 `whole/Jenkinsfile`
+
+## 故障:`MissingContextVariableException: FilePath is missing`
+
+日志若在 **Start of Pipeline 后立刻出现 `[Pipeline] load` 然后失败**,说明 Gitea 上仍是**旧版** Jenkinsfile(在 `pipeline { agent any` **之外**顶层写了 `load`)。
+
+处理:
+
+1. 将仓库中最新 `docs/jenkins/produ/whole/Jenkinsfile` **push 到 Job 使用的分支**(如 `uat-20260202`)  
+2. Jenkins Job 点 **保存** 后重新构建(不要 Replay 旧运行)  
+3. 成功日志应先出现 `[Pipeline] agent`、`Checkout` / `sparse`,再在 **Plan** 阶段里 `load`

+ 66 - 0
docs/jenkins/produ/README-PROMOTE-IMAGE.md

@@ -0,0 +1,66 @@
+# 仅晋升镜像(Harbor → Harbor → ACK)
+
+与 `__alone/Jenkinsfile-prod-promote-from-uat.groovy`(拷 jar + compose restart)对应,本流水线只做:
+
+```text
+docker pull  39.105.153.68/alien_cloud/<服务>:<SOURCE_TAG>
+docker tag   → :<TARGET_TAG>
+docker push
+kubectl set image(可选)
+```
+
+**不** Checkout 业务代码、**不** Maven、**不** docker build。
+
+---
+
+## Jenkins Job 配置
+
+| 项 | 值 |
+|----|-----|
+| 建议 Job 名 | `alien-cloud-k8s-promote-image` |
+| 类型 | 流水线 |
+| 定义 | Pipeline script from SCM |
+| 仓库 | `http://8.152.195.41:3000/alien/alien_cloud` |
+| 分支 | 预生产分支,如 `*/uat-20260202` |
+| Script Path | **`docs/jenkins/produ/promote-image/Jenkinsfile`** |
+| SCM **Credentials** | **Git 凭据**(如 `zhanghaomimapingzheng` 或 `dujian/****** (git)`) |
+
+### SCM 的 Credentials 不是 Harbor 机器人
+
+| 下拉框 | 应选什么 | 用途 |
+|--------|----------|------|
+| **Repository URL 旁的 Credentials** | **Git** 账号(拉 Jenkinsfile) | **不要**选 `robot$alien_cloud+jenkins-k8s` |
+| 流水线内 Harbor 登录 | 凭据 ID **`harbor-robot-alien`** | 用户名 `robot$alien_cloud+jenkins-k8s`,密码为 Token |
+
+Harbor 机器人在 Jenkins **凭据管理**里单独建 **Username with password**,ID 必须为 `harbor-robot-alien`(与 Jenkinsfile `environment` 一致)。
+
+---
+
+## 参数(与 prod-promote 类似)
+
+| 参数 | 说明 |
+|------|------|
+| `DEPLOY_MODE` | `whole` / `single` / `multi` |
+| `SOURCE_TAG` | 预生产镜像在 Harbor 上的 tag,如 `uat-build-42` |
+| `TARGET_TAG` | 生产 tag,默认 `produ-${BUILD_NUMBER}` |
+| `HARBOR_REGISTRY` | `39.105.153.68` |
+| `HARBOR_PROJECT` | `alien_cloud` |
+| `DEPLOY_STRATEGY` | `rolling` / `canary` / **`skip`**(只推镜像、不改 ACK) |
+| `DRY_RUN` | 只打印计划 |
+
+---
+
+## 预生产镜像从哪来?
+
+现网 **88 上 UAT** 若只打 jar 到 `/alien_uat/java`、**未**推 Harbor,需先有一次:
+
+- UAT 构建后 `docker build` + `push` 到 `39.105.153.68/alien_cloud/<服务>:uat-build-<号>`,或  
+- 在 153.68 上从现网 jar 打镜像 push(一次性迁移)
+
+之后本 Job 只做 **tag 晋升**,不再拉 Git 代码。
+
+---
+
+单服务 Job(如 **Gateway-K8s**)与 **promote-image** 均为仅晋升镜像;界面配置见 [README-JOB-GATEWAY.md](./README-JOB-GATEWAY.md)。
+
+生产发版推荐:**UAT 构建推 `uat-build-*` → Gateway-K8s / promote-image 晋升 → ACK**。

+ 128 - 58
docs/jenkins/produ/README.md

@@ -1,87 +1,157 @@
-# 预生产 Harbor 镜像 → ACK(仅晋升,不构建)
+# 预生产分支 → Harbor → ACK 生产 K8S 流水线
 
-Harbor:**39.105.153.68**(`alien_cloud`)。Jenkins:**39.106.135.88:30003**。ACK:**alien-ack-cluster**。
+Harbor 宿主机:**39.105.153.68**(Web:`http://39.105.153.68/harbor/`)。Jenkins 示例:**39.106.135.88:30003**。ACK:**alien-ack-cluster**(华北2 北京)
 
-与 `__alone/Jenkinsfile-prod-promote-from-uat.groovy`(拷 jar + compose)并列;本目录只做:
+本目录支持 **单服务发版** 与 **整体/多选发版**(Harbor + ACK);与宿主机 `docker compose` 晋升流水线(`Jenkinsfile-prod-promote-from-uat.groovy`)并行存在,互不替代,直到 ACK 全量切流。
 
-```text
-docker pull  39.105.153.68/alien_cloud/<服务>:<SOURCE_TAG>
-docker tag   → :<TARGET_TAG>
-docker push
-kubectl set image(可选)
-```
+---
 
-**不** Checkout 业务代码、**不** Maven、**不** docker build。
+## 常见问题(Harbor / 目录结构 / 发版粒度)
 
----
+### Harbor 要不要在 Web 界面手工操作?
+
+**日常发版不需要。** 流水线在 `k8s-produ-lib.groovy` 里自动完成:
 
-## Jenkins Job 与 Script Path
+1. `docker login 39.106.135.88`(凭据 `harbor-robot-alien`)
+2. `docker build` → 打 tag `39.106.135.88/alien/<服务名>:<tag>`
+3. `docker push` 到 Harbor
+4. `kubectl set image` 让 ACK 拉同一 tag
 
-| 建议 Job 名 | Script Path | 粒度 |
-|-------------|-------------|------|
-| `gateway-k8s` | `docs/jenkins/produ/gateway/Jenkinsfile` | 单服务 |
-| `store-k8s` | `docs/jenkins/produ/store/Jenkinsfile` | 单服务 |
-| … | `produ/<prodDir>/Jenkinsfile` | … |
-| `alien-cloud-k8s-whole` | `docs/jenkins/produ/whole/Jenkinsfile` | whole/single/multi(界面见 [README-JOB-WHOLE.md](./README-JOB-WHOLE.md)) |
-| `alien-cloud-k8s-promote-image` | `docs/jenkins/produ/promote-image/Jenkinsfile` | 同 whole |
+运维**仅需一次性**在 Harbor Web(或 API)完成:`alien` 项目、机器人账号、(可选)镜像保留策略。之后查镜像/删旧 tag/排障时仍可打开 Harbor UI,**不是发版必经步骤**。
 
-### Pipeline from SCM 配置
+### `_shared/k8s-produ-lib.groovy` 是什么?能单独部署吗?
 
-| 项 | 值 |
-|----|-----|
-| 定义 | Pipeline script from SCM |
-| 仓库 | `http://8.152.195.41:3000/alien/alien_cloud` |
-| 分支 | 预生产分支,如 `*/uat-20260202` |
-| **SCM Credentials** | **Git**(`zhanghaomimapingzheng` 或 `dujian/****** (git)`) |
+| 文件 | 角色 | Jenkins Script Path 是否指向它 |
+|------|------|-------------------------------|
+| `produ/<服务>/Jenkinsfile` | **入口**(Jenkins 只认这个名字) | **是**,每个 Job 指向各自路径 |
+| `_shared/k8s-produ-lib.groovy` | **共享函数库**(checkout、mvn、Harbor、kubectl) | **否**,由入口 `load '.../k8s-produ-lib.groovy'` 引用 |
+| `_shared/service-registry.groovy` | 七个服务列表 + whole/multi 筛选 | 否,供 `whole/Jenkinsfile` 使用 |
 
-### SCM Credentials ≠ Harbor 机器人
+每个微服务 Job 仍是 **单独一条流水线**(例如只构建 gateway);共享库是为避免 7 份重复脚本,**不代替** `Jenkinsfile`。
 
-| 位置 | 选什么 | 不要选 |
-|------|--------|--------|
-| **Repository URL 旁 Credentials** | Git 账号 | `robot$alien_cloud+jenkins-k8s` |
-| Jenkins 凭据库 **`harbor-robot-alien`** | 用户名 `robot$alien_cloud+jenkins-k8s`,密码=Token | — |
-| Jenkins 凭据库 **`ack-kubeconfig-alien`** | ACK kubeconfig 文件 | — |
+### 既要单独发布,又要整体发布?
 
-Harbor 机器人只用于流水线里的 `docker login`,不是拉 Git 的 Jenkinsfile。
+| Jenkins Job(建议名) | Script Path | 粒度 |
+|----------------------|-------------|------|
+| `gateway-k8s` … `dining-k8s` | `docs/jenkins/produ/<prodDir>/Jenkinsfile` | **仅该服务**(Maven `-pl alien-xxx`) |
+| `alien-cloud-k8s-whole` | `docs/jenkins/produ/whole/Jenkinsfile` | **whole / single / multi**(参数 `DEPLOY_MODE`,语义同 prod-promote) |
+
+整体 Job 一次 `mvn package` 打全仓库,再按选中服务依次 build/push/deploy;单服务 Job 只打对应模块,适合热修复单个服务。
 
 ---
 
-## 参数说明(单服务 Job)
+## 一、Jenkins 任务如何组织(对应你截图中的「新建任务」)
+
+### 1. 是否选「文件夹」?
 
-| 参数 | 说明 |
+**是,推荐选「文件夹」。**
+
+| 类型 | 用途 |
 |------|------|
-| `SOURCE_TAG` | 预生产已在 Harbor 的 tag,如 `uat-build-42` |
-| `TARGET_TAG` | 生产 tag,默认 `produ-${BUILD_NUMBER}` |
-| `DEPLOY_STRATEGY` | `rolling` / `canary` / **`skip`**(只推镜像) |
-| `HARBOR_REGISTRY` | `39.105.153.68` |
+| **文件夹** | 独立命名空间,其下可建多个 **流水线** 任务,例如 `gateway-k8s`、`store-k8s`,名称可与其他视图重复 |
+| **流水线** | 真正执行构建的 Job,每个微服务一条 |
+| **多分支流水线** | 按 Git 分支自动建子 Job;若预生产固定一条分支(如 `uat-20260202`),用普通流水线 + 参数 `GIT_BRANCH` 更简单 |
+
+建议结构:
+
+```text
+alien-cloud-produ-K8S-from-uat/     ← 文件夹(你截图中的名称)
+├── alien-cloud-k8s-whole/          ← 流水线,Script Path: docs/jenkins/produ/whole/Jenkinsfile(整体/多选)
+├── gateway-k8s/                    ← 流水线,Script Path: docs/jenkins/produ/gateway/Jenkinsfile
+├── store-k8s/
+├── second-k8s/
+├── store-platform-k8s/
+├── lawyer-k8s/
+├── job-k8s/
+└── dining-k8s/
+```
+
+**不要**在文件夹里再选「文件夹」类型去套微服务——微服务一级用 **流水线** 即可。
+
+### 2. 是否用 Pipeline script from SCM?
+
+**可以,且推荐。**
+
+每个 `*-k8s` 流水线 Job 配置:
+
+1. **流水线** → **定义**:Pipeline script from SCM  
+2. **SCM**:Git,仓库 `http://8.152.195.41:3000/alien/alien_cloud`(与现网一致)  
+3. **分支**:`*/uat-20260202` 或 `*/你的预生产分支`  
+4. **Script Path**(按服务不同):
+   - gateway:`docs/jenkins/produ/gateway/Jenkinsfile`
+   - store:`docs/jenkins/produ/store/Jenkinsfile`
+   - … 见下表  
+
+5. **凭据**:`zhanghaomimapingzheng`(与 UAT 流水线一致)  
+6. **构建触发**:可选手动;稳定后加「上游 UAT 构建成功」触发  
 
-整体 Job 另增 `DEPLOY_MODE`:`whole` / `single` / `multi`(同 prod-promote)。
+与 `Jenkinsfile-prod-promote-from-uat.groovy` 的关系:
+
+| 维度 | prod-promote-from-uat | produ K8S 流水线 |
+|------|----------------------|------------------|
+| 制品 | UAT 目录 jar → 生产机 compose | Maven jar → 镜像 → Harbor → ACK |
+| 发版粒度 | 单 Job 内 whole/single/multi | **whole Job** + **每服务一个 Job** |
+| 灰度 | 无 | 支持 Ingress 权重灰度(见 README-ACK-GRAY-RELEASE.md) |
 
 ---
 
-## 预生产镜像从哪来?
+## 二、微服务与 Script Path 对照
+
+| prodDir | 模块 | Jenkins Script Path | ACK Deployment(默认) |
+|---------|------|---------------------|-------------------------|
+| gateway | alien-gateway | `docs/jenkins/produ/gateway/Jenkinsfile` | `gateway` |
+| store | alien-store | `docs/jenkins/produ/store/Jenkinsfile` | `store` |
+| second | alien-second | `docs/jenkins/produ/second/Jenkinsfile` | `second` |
+| store-platform | alien-store-platform | `docs/jenkins/produ/store-platform/Jenkinsfile` | `store-platform` |
+| lawyer | alien-lawyer | `docs/jenkins/produ/lawyer/Jenkinsfile` | `lawyer` |
+| job | alien-job | `docs/jenkins/produ/job/Jenkinsfile` | `job` |
+| dining | alien-dining | `docs/jenkins/produ/dining/Jenkinsfile` | `dining` |
+| **(整体)** | 全部七个 | `docs/jenkins/produ/whole/Jenkinsfile` | 按 `DEPLOY_MODE` |
+
+**未纳入本目录**(属其他仓库或镜像名不同,需另建 Job):
+
+- Python:`py_gateway_produ`、`py_contract_produ`、`py_esign_produ`(镜像 `alien_gateway:local` 等)  
+- `xxl-job-admin-produ`、Harbor 自身组件  
+
+---
+
+## 三、Jenkins 侧必配项(首次)
+
+在 Jenkins **全局**或文件夹 **凭据** 中准备:
+
+| 凭据 ID(示例,按实际改 Jenkinsfile 内常量) | 类型 | 用途 |
+|---------------------------------------------|------|------|
+| `zhanghaomimapingzheng` | Username/password | Git 拉取 |
+| `harbor-robot-alien` | Username/password | `docker login 39.106.135.88` |
+| `ack-kubeconfig-alien` | Secret file | `KUBECONFIG`,内容来自 ACK「连接信息」下载 |
 
-88 上 UAT 构建后需有一次把镜像推到 Harbor,例如:
+Jenkins 节点需具备
 
-`39.105.153.68/alien_cloud/gateway:uat-build-42`
+- `mvn`、`docker`(能访问 Harbor 443/80)、`kubectl`  
+- 若 Jenkins 在容器内:挂载 `docker.sock`,并将 `kubectl` 与 kubeconfig 挂入或装在镜像内  
 
-之后本目录流水线只做 **tag 晋升**。
+Harbor 上建议建项目 **`alien`**,镜像名:`39.106.135.88/alien/<prodDir>:<tag>`
 
-详见 [README-HARBOR-SETUP.md](./README-HARBOR-SETUP.md)、[README-PROMOTE-IMAGE.md](./README-PROMOTE-IMAGE.md)、[README-ACK-GRAY-RELEASE.md](./README-ACK-GRAY-RELEASE.md)。
+基础镜像 `my-openjdk8-ffmpeg:v1` 需 **先 push 到 Harbor**(如 `39.106.135.88/alien/base/openjdk8-ffmpeg:v1`),并在 Job 参数 `BASE_IMAGE` 中填写,否则 ACK 节点拉不到
 
 ---
 
-## 目录结构
+## 四、ACK 与灰度
 
-```text
-docs/jenkins/produ/
-├── __alone/                    # 参考:jar 晋升 compose(勿删)
-├── _shared/
-│   ├── k8s-produ-lib.groovy
-│   └── service-registry.groovy
-├── gateway/Jenkinsfile           # 单服务
-├── store/ … dining/
-├── whole/Jenkinsfile             # 整体/多选
-├── promote-image/Jenkinsfile     # 同 whole
-└── k8s/examples/                 # ACK 清单示例
-```
+详述见同目录 **[README-ACK-GRAY-RELEASE.md](./README-ACK-GRAY-RELEASE.md)**(集群接入、命名空间、Ingress 灰度、节点 Unknown 排查)。
+
+K8S 清单示例:`k8s/examples/`。
+
+---
+
+## 五、流水线参数速览
+
+各服务 Jenkinsfile 共用库 `docs/jenkins/produ/_shared/k8s-produ-lib.groovy`,主要参数:
+
+- `GIT_BRANCH`:预生产分支  
+- `IMAGE_TAG`:默认 `build-${BUILD_NUMBER}`,可填 `uat-20260202-123`  
+- `DEPLOY_STRATEGY`:`rolling`(直接替换)或 `canary`(Ingress 权重)  
+- `CANARY_WEIGHT`:灰度流量百分比(仅 canary)  
+- `K8S_NAMESPACE`:默认 `alien-produ`  
+
+首次部署前在 ACK 应用 `k8s/examples/namespace.yaml` 与各服务 `deployment-*.yaml`(或让流水线 `kubectl apply` 首次全量 apply)。

+ 2 - 4
docs/jenkins/produ/docker/Dockerfile.java-service

@@ -1,7 +1,7 @@
-# 单模�?Java 微服务镜像(Jenkins 构建上下�?= alien-<module>/ 目录�?
+# Single-module Java service image. Build context = module dir with JAR + lib/
 # build-arg: BASE_IMAGE, JAR_FILE, SERVER_PORT, WITH_LIB=true|false
 
-ARG BASE_IMAGE=39.106.135.88/alien_cloud/base/openjdk8-ffmpeg:v1
+ARG BASE_IMAGE=39.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1
 FROM ${BASE_IMAGE}
 
 ARG JAR_FILE=alien-gateway-1.0.0.jar
@@ -11,13 +11,11 @@ ARG WITH_LIB=false
 WORKDIR /app
 
 COPY ${JAR_FILE} /app/app.jar
-# �?jar �?target/lib 存在;WITH_LIB=false �?COPY 空目录可能失败,构建脚本保证目录存在
 COPY lib /app/lib
 
 ENV SERVER_PORT=${SERVER_PORT}
 EXPOSE ${SERVER_PORT}
 
-# �?docker-compose-prod 一致:prod profile;有 lib 时加 loader.path
 RUN if [ "${WITH_LIB}" = "true" ]; then \
       echo 'exec java -Dspring.profiles.active=prod -Dfile.encoding=UTF-8 -Xms800m -Xmx800m -Dloader.path=/app/lib -jar /app/app.jar --server.port='"${SERVER_PORT}" > /app/entrypoint.sh; \
     else \

+ 111 - 0
docs/jenkins/produ/promote-image/Jenkinsfile

@@ -0,0 +1,111 @@
+// 与 whole/Jenkinsfile 相同逻辑
+def sparseCheckoutProduShared() {
+    checkout scm: [
+        $class: 'GitSCM',
+        branches: scm.branches,
+        extensions: [
+            [$class: 'CloneOption', depth: 1, shallow: true, noTags: true],
+            [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [
+                [path: 'docs/jenkins/produ/_shared/'],
+            ]],
+        ],
+        userRemoteConfigs: scm.userRemoteConfigs,
+    ]
+}
+
+def getProduLibs() {
+    sparseCheckoutProduShared()
+    def k8s = load 'docs/jenkins/produ/_shared/k8s-produ-lib.groovy'
+    def reg = load 'docs/jenkins/produ/_shared/service-registry.groovy'
+    return [k8s, reg]
+}
+
+pipeline {
+    agent any
+    options {
+        buildDiscarder(logRotator(numToKeepStr: '20'))
+        disableConcurrentBuilds()
+        timestamps()
+        timeout(time: 120, unit: 'MINUTES')
+    }
+    parameters {
+        choice(name: 'DEPLOY_MODE', choices: ['whole', 'single', 'multi'])
+        choice(name: 'SINGLE_SERVICE', choices: [
+            'gateway', 'store', 'second', 'store-platform', 'lawyer', 'job', 'dining',
+        ])
+        booleanParam(name: 'MULTI_gateway', defaultValue: false)
+        booleanParam(name: 'MULTI_store', defaultValue: false)
+        booleanParam(name: 'MULTI_second', defaultValue: false)
+        booleanParam(name: 'MULTI_store_platform', defaultValue: false)
+        booleanParam(name: 'MULTI_lawyer', defaultValue: false)
+        booleanParam(name: 'MULTI_job', defaultValue: false)
+        booleanParam(name: 'MULTI_dining', defaultValue: false)
+        string(name: 'SOURCE_TAG', defaultValue: '', trim: true)
+        string(name: 'TARGET_TAG', defaultValue: '', trim: true)
+        string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
+        string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
+        choice(name: 'DEPLOY_STRATEGY', choices: ['rolling', 'canary', 'skip'])
+        string(name: 'CANARY_WEIGHT', defaultValue: '10', trim: true)
+        string(name: 'K8S_NAMESPACE', defaultValue: 'alien-produ', trim: true)
+        booleanParam(name: 'DRY_RUN', defaultValue: false)
+    }
+    environment {
+        HARBOR_CREDENTIALS = 'harbor-robot-alien'
+        KUBECONFIG_CREDENTIALS = 'ack-kubeconfig-alien'
+    }
+    stages {
+        stage('Plan') {
+            steps {
+                script {
+                    def (k8s, reg) = getProduLibs()
+                    def services = reg.filterServices(reg.getServiceRegistry(), params)
+                    env.PROMOTE_LIST = services*.prodDir.join(',')
+                    env.TARGET_TAG_RESOLVED = k8s.resolveTargetTag(this, params.TARGET_TAG)
+                    echo ">>> 服务=${env.PROMOTE_LIST} ${params.SOURCE_TAG} → ${env.TARGET_TAG_RESOLVED}"
+                }
+            }
+        }
+        stage('Promote images') {
+            steps {
+                script {
+                    def (k8s, reg) = getProduLibs()
+                    def services = reg.filterServices(reg.getServiceRegistry(), params)
+                    k8s.promoteHarborImages(this, services, [
+                        harborRegistry: params.HARBOR_REGISTRY,
+                        harborProject: params.HARBOR_PROJECT,
+                        sourceTag: params.SOURCE_TAG,
+                        targetTag: env.TARGET_TAG_RESOLVED,
+                        harborCredentialsId: env.HARBOR_CREDENTIALS,
+                        dryRun: params.DRY_RUN == true,
+                    ])
+                }
+            }
+        }
+        stage('Deploy to ACK') {
+            when { expression { return params.DEPLOY_STRATEGY != 'skip' && !params.DRY_RUN } }
+            steps {
+                script {
+                    def (k8s, reg) = getProduLibs()
+                    def services = reg.filterServices(reg.getServiceRegistry(), params)
+                    def regHost = params.HARBOR_REGISTRY.trim()
+                    def proj = params.HARBOR_PROJECT.trim()
+                    def tgtTag = env.TARGET_TAG_RESOLVED
+                    def strategy = params.DEPLOY_STRATEGY
+                    services.each { s ->
+                        def imageRef = "${regHost}/${proj}/${s.prodDir}:${tgtTag}"
+                        k8s.deployToAck(this, [
+                            k8sNamespace: params.K8S_NAMESPACE,
+                            imageRef: imageRef,
+                            deployStrategy: strategy == 'canary' ? 'canary' : 'rolling',
+                            deploymentStable: s.deployName,
+                            deploymentCanary: "${s.deployName}-canary",
+                            ingressCanary: "${s.deployName}-canary",
+                            canaryWeight: (params.CANARY_WEIGHT ?: '10').trim(),
+                            kubeCredentialsId: env.KUBECONFIG_CREDENTIALS,
+                        ])
+                    }
+                }
+            }
+        }
+    }
+}