Просмотр исходного кода

流水线优化 - 最后推送到Harbor 制品库

dujian 1 неделя назад
Родитель
Сommit
1ac3d39264

+ 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**。

+ 25 - 0
docs/jenkins/produ/docker/Dockerfile.java-service

@@ -0,0 +1,25 @@
+# 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.105.153.68/alien_cloud/base/openjdk8-ffmpeg:v1
+FROM ${BASE_IMAGE}
+
+ARG JAR_FILE=alien-gateway-1.0.0.jar
+ARG SERVER_PORT=8000
+ARG WITH_LIB=false
+
+WORKDIR /app
+
+COPY ${JAR_FILE} /app/app.jar
+COPY lib /app/lib
+
+ENV SERVER_PORT=${SERVER_PORT}
+EXPOSE ${SERVER_PORT}
+
+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 \
+      echo 'exec java -Dspring.profiles.active=prod -Dfile.encoding=UTF-8 -Xms800m -Xmx800m -jar /app/app.jar --server.port='"${SERVER_PORT}" > /app/entrypoint.sh; \
+    fi && chmod +x /app/entrypoint.sh
+
+ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]

+ 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,
+                        ])
+                    }
+                }
+            }
+        }
+    }
+}