一条 push 进去,五分钟后两个企业微信群分别收到带下载二维码的结果通知——这条链路上真正难的不是"打通",而是让每一次打的包都永远是对的。这篇是完整的搭建实录与踩坑手册(敏感信息已脱敏)。
代码从 GitLab 推送开始,到两个企业微信群收到消息为止,全链路自动完成,平均耗时 4~5 分钟(编译约 4 分钟 + 上传通知约 1 分钟)。
开发者 push
│
▼
GitLab(私有部署)
│ ① Webhook(push 事件,全部分支)
▼
Jenkins(Windows 打包机,打包任务)
│ ② 强制检出本次推送的确切提交
│ ③ merge 提交 → 自动跳过打包
│ ④ 按分支选 flavor → Gradle 编译
│ ⑤ 产物落盘 D:\ApkOutput\build-N\{debug,release}
▼
上传 + 通知编排脚本
├─ ⑥ 上传蒲公英(debug→测试通道,release→正式通道)
└─ ⑦ 企业微信消息
├─ 主打包群:所有消息(成功 + 失败)
└─ 上线验收群:仅需求整合分支的出包成功消息
另有三个旁路机制保证"打的包永远是对的":
| 机制 | 解决的问题 |
|---|---|
| 强制检出推送提交 | 几秒内多人同时推送时,构建的 cause 和实际代码交叉错乱 |
| merge 提交跳过 | merge 操作触发无意义的重复打包 |
| 同分支去重守护(每分钟定时任务) | 同分支连续多次 push 打出多个重复包,只保留最后一次 |
| 系统 | 用途 | 凭据存放 |
|---|---|---|
| Jenkins 2.580 | 打包任务(FreeStyle,串行执行) | 专用账号,凭据存打包机 |
| GitLab 私有部署 | 代码仓库、webhook 配置 | 具备 webhook 管理权限的账号 |
| 蒲公英 | APK 托管、二维码 | API Key 存本地密钥文件(严禁入库) |
| 企业微信 AI 机器人 | 群消息推送(MCP 接口) | MCP 接口地址存本地密钥文件(严禁入库) |
FreeStyle 任务,串行执行(不勾选 Execute concurrent builds),三个 Batch 构建步骤:
:: 步骤一(准备):写 SDK 路径 + 强制检出本次推送的提交
chcp 65001 >nul
echo sdk.dir=C:/Users/<用户>/AppData/Local/Android/Sdk> local.properties
powershell -NoProfile -ExecutionPolicy Bypass -File D:\jenkins-setup\checkout-pushed.ps1
if errorlevel 1 (powershell ... upload-notify.ps1 -Failed & exit /b 1)
:: 步骤二(编译):开头两行是 merge 跳过
powershell ... is-merge-tip.ps1
if not errorlevel 1 (echo === SKIP: merge commit tip === & exit /b 0)
...按分支选 flavor,gradlew 打包,产物拷贝到 D:\ApkOutput\build-%BUILD_NUMBER%...
powershell ... prune-apkoutput.ps1 -Keep 20 :: 只保留最近 20 次产物
:: 步骤三(上传+通知)
chcp 65001 >nul
powershell -NoProfile -ExecutionPolicy Bypass -File D:\jenkins-setup\upload-notify.ps1
构建后设置两个关键项:
Could not connect|Connection timed out|Failed to connect——GitLab 偶发连接抖动时自动重跑,不用人工干预。_raw 后缀的那个才是可写的;问题:Jenkins git 插件配通配分支时,检出的是"所有分支中最新 tip"。两人几秒内各推一次,会出现构建 A 的推送人/分支来自甲、实际检出代码却是乙的——消息全部错乱(历史事故:推送记录显示某同学,打包出来的却是别人的提交)。
方案:每个构建开始时,从本构建的 build.xml 里读取 GitLabWebHookCause 的 after(本次推送的最后一个提交 SHA),git checkout -f <sha> 强制对齐。于是打包人、分支、最新提交、merge 判断永远来自同一次推送。手动构建(无 webhook cause)不干预。
git log -1 --pretty=%P HEAD 父提交数 >1 即 merge → 直接跳过编译、上传、通知(配合 allowEmptyArchive,构建安静地 SUCCESS)。业务规则:"merge 不要打包"。
需求:同分支多个 commit 一次 push 本来就只出一个包(webhook 的 after = 最后一个提交);但短时间多次 push 会出多个重复包,业务上只要最后一个。
实现(Windows 计划任务,SYSTEM 账户,每分钟):
data:{},分支信息拿不到,只能走服务端脚本);POST /queue/cancelItem?id=N——注意不是 /queue/item/N/cancel,那个在本版本返回 404),只留最新;POST /job/xxx/<N>/stop(顺序不能反!先 stop 会让旧队列项立刻出队复活);边界:旧构建已进入上传的最后一分钟时新推送才到,旧包仍可能上传(约 1 分钟窗口,正常推送间隔都能拦住)。
POST https://www.pgyer.com/apiv2/app/upload(_api_key + 文件 + 更新说明);buildBuildVersion(蒲公英后台"最新 build"页显示的数字);buildVersionNo 只是应用内序号,不要用——历史上因此出过"后台显示 #42、群里发 #1814"的乌龙;buildShortcutUrl 时轮询 buildInfo 接口补齐(最多 15 次 × 3 秒)。机器人是企业微信 AI 机器人,对外提供 MCP 接口(streamable-http),脚本直接 POST tools/call:sessions_list 发现群 chat_id,send 以 markdown 格式发消息。
| 配置文件 | 作用 | 投递规则 |
|---|---|---|
| wecom-main-group.txt | 主打包群 chat_id | 所有成功 + 失败消息 |
| wecom-accept-group.txt | 上线验收群 chat_id | 仅需求整合分支 + 打包成功 |
| wecom-dm-userid.txt | 私聊兜底 | 无群会话时用 |
固定 chat_id 的另一个好处:机器人加入多个群后,不会因为"取最近会话"而发错群。
### ✅ APK 打包完成,已上传蒲公英,请尽快测试验证
**打包人**: ××× **分支**: main_dev_xx **构建号**: #60
> **[debug] 测试通道** v1.x.x · [蒲公英build #18xx](下载页链接) · 41.2MB
[点击下载](链接) [二维码](链接)
> **[release] 正式通道** v1.x.x · [蒲公英build #18xx](下载页链接) · 38.9MB
**最新提交**: `a1b2c3d` ×××:提交说明……
### ❌ APK 打包失败
**打包人**: ××× **分支**: ××× **构建号**: #N
请相关同学检查代码,[点击查看构建日志](链接)
字段来源的可靠性设计:
GIT_BRANCH 环境变量——交叉时它会指向别的分支);通过 REST API 提交 config.xml,三件事缺一不可:
# 1. 取 crumb(xpath 方式的 crumbIssuer 已被禁用,必须用 JSON)
curl -s -c cookies.txt -u <user>:<pass> -o crumb.json \
http://<jenkins>:8080/crumbIssuer/api/json
# 2. 提交配置(必须带 charset,否则中文配置按 ISO-8859-1 解码 → 乱码写坏线上配置!)
curl -s -b cookies.txt -u <user>:<pass> -H "Jenkins-Crumb:<crumb>" \
-H "Content-Type: text/xml; charset=utf-8" \
--data-binary @new-config.xml -X POST \
http://<jenkins>:8080/job/<job>/config.xml
<?xml version="1.1"?> 改回 1.0 再提交,否则 HTTP 500;build.xml 不走 HTTP(会 302),直接读盘,同样要 1.1→1.0 降级才能 XmlDocument 解析。[char] 码拼接;$ 和反斜杠会被吃掉,一律写脚本文件后 -File 执行,路径用正斜杠。project.path_with_namespace、commit 的 title/timestamp/author 等),缺字段插件直接 500。| 坑 | 症状 | 解法 |
|---|---|---|
| GitLab 禁止内网 webhook | 推代码无反应 | 实例级 allowlist 加 Jenkins 地址 |
| config.xml 提交 500 | HTTP 500 | XML 声明 1.1 → 1.0 |
| 提交后中文分支乱码 | main_dev_??? | POST 必须带 charset=utf-8 |
| crumb 接口 403 | primitive XPath forbidden | 改用 /crumbIssuer/api/json |
| 队列 REST 拿不到推送分支 | cause 的 data 为空 | scriptText Groovy 服务端查询 |
| 队列取消 404 | cancel 不生效 | 用 POST /queue/cancelItem?id=N |
| 先 stop 后 cancel | 旧队列项复活成新构建 | 先去重队列,再停运行中构建 |
| 无产物构建 FAILURE | merge 跳过被标红 | ArtifactArchiver 勾 allowEmptyArchive |
| 蒲公英 build 号错误 | 显示应用内序号 | 用 buildBuildVersion 字段 |
| 轮询引发老分支补打 | 蒲公英出现老版本 | 关闭 Poll SCM |
| 中文 .ps1 语法错误 | 莫名解析失败 | UTF-8 带 BOM 保存 |
| 双 push 交叉错乱 | 打包人/分支/提交对不上 | 强制检出推送提交 |
以上五条全过,说明体系各环节运转正常。