重放一条 webhook,流水线跑的是三天前的代码

流水线全绿、部署成功,构建的却是旧提交。请求体里的 after、head_commit.id、commits[0].id 三处都要换,时间戳写成 epoch 会直接 400。

有一次为了绕过 CI 平台的触发限制,我用「重放 git 服务的 push 回调」这个办法手动触发流水线。命令敲下去,几十秒后流水线全绿,部署也完成了,我还挺得意。

十分钟后同事来问:你刚才发的是哪个版本?我看线上好像还是早上那个。

我打开页面看了一遍——确实,构建产物里的提交号不是我刚才推的那个。流水线是绿的,部署是成功的,跑的是三天前的代码。

一、先确认「跑的是哪个提交」

判断这类问题不要从流水线界面看,界面上的「分支」「提交信息」是展示字段,可能是旧的。直接看构建日志里检出的 sha:

# 构建日志里最靠前的那几行
git rev-parse HEAD
# 输出:c7134db5f0a1...(不是我刚推的 2c4f45eb)

再看镜像 tag。我们的流水线用短 sha 打 tag,所以镜像叫什么名字,就代表它编的是哪个提交:

kubectl -n app get deploy app-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# registry.example.com/app/api:c7134db5

日志和镜像对上了,都是旧提交。所以不是「构建失败」也不是「部署没生效」,而是输入就是错的——流水线从头到尾忠实地构建了一份旧代码。

二、重放回调的时候,我改的是「分支」不是「提交」

手动触发的方式是:从 git 服务的回调记录里,找一条历史上的 push 事件,把它的请求体重新 POST 给 CI 平台的接收地址。这样能绕过「平台上没有手动触发按钮」的限制。

问题在于,那条历史请求体里写的是当时那次推送的提交。我重放的时候只改了分支名,没改提交号。CI 平台拿到请求体,照着里面的 sha 去检出代码,检出的自然还是三天前那个。

请求体里和提交有关的字段有三个,少改一个都可能出问题:

字段 作用 重放时的坑
after 本次推送后的分支 HEAD 最常被 CI 用来决定检出哪个 sha
head_commit.id 本次推送的头部提交 部分平台用它做展示和校验
commits[0].id 提交列表的第一条 有的平台按列表取
ref 分支引用 只改这个,等于是「在旧提交上重放」
timestamp 事件时间 格式错了直接 400

所以正确做法是:拿当前分支的真实 HEAD,把这三个 id 全部替换掉,再重新签名。

三、签名和时区:一行时间戳能让你 400

请求体改了,签名就得重算,否则对方验签失败直接 403。签名算法是 HMAC-SHA256,密钥就是配置 webhook 时填的那个:

import hmac, hashlib, json, time, urllib.request

secret = os.environ["WEBHOOK_SECRET"].encode()
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
sig = hmac.new(secret, body, hashlib.sha256).hexdigest()

req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-Hub-Signature-256", "sha256=" + sig)
req.add_header("X-Gitea-Event", "push")

这里踩了一个特别隐蔽的坑:timestamp 必须是 RFC3339 带时区的格式,比如 2026-11-12T09:30:00+08:00。我一开始图省事传了 Unix 时间戳(1762906200),返回 400,错误信息是 failure to parse hook。

这个报错几乎没提示性,看起来像「请求体格式不对」,实际只是时间格式。而且不同平台对时区的容忍度不一样,有的要求必须带 +08:00,写成 Z 也报错。稳妥做法是直接用带偏移量的写法。

还有一个细节:Content-Type 必须是 application/json,且请求体在签名和发送时必须是同一份字节。如果用 json.dumps 生成一次、发送时又 dump 一次,键顺序或空格差异会导致验签失败。

四、更稳的触发方式:改 ref 不触发,触发不靠猜

后来我们把「手动触发」这件事做成了另一个思路:把「改配置」和「触发部署」解耦。

git 的裸仓库里,update-ref 可以直接改引用,而它不会触发 post-receive 钩子。这意味着你可以先把某个 ref 指到想要的提交,确认没问题,再单独发一次回调去触发构建。中间不会有「改一半就自动部署」的意外。

# 只改引用,不触发任何钩子
git --git-dir=/srv/repos/app.git update-ref refs/heads/release 2c4f45eb

# 确认引用指向正确
git --git-dir=/srv/repos/app.git rev-parse refs/heads/release

反过来,如果平台提供了 OpenAPI 的触发接口(哪怕文档里没写清楚),优先用它而不是重放回调。重放回调本质上是在「伪造一个历史事件」,字段多、语义隐晦,每次都得对着请求体猜。

五、部署侧的另一半:apply 把镜像打回旧 tag

查这个问题时,顺手发现了另一条同源的坑,症状几乎一样:流水线绿了,线上还是旧版本。

我们的部署步骤原来是 docker build 之后直接 kubectl apply -f deploy.yaml。而 deploy.yaml 里写死了镜像 tag。于是 apply 把刚刚 set image 进去的新 tag 又改回了 YAML 里的旧 tag——看起来 apply 成功了,实际是把镜像回退了。

按 Kubernetes 的语义,apply 是「让集群状态等于文件里声明的状态」,文件里写的是旧 tag,那新 tag 就会被覆盖。这不是 bug,是声明式配置的本意。

修法有两条,我们两条都用了:

第一,把 Deployment/Service 的声明拆成两份文件,其中 Deployment 的镜像字段在 CI 里用 kubectl set image 单独改,apply 只负责其他字段(副本数、探针、资源)。

第二,如果一定要 apply,就在 CI 里先把 YAML 里的 tag 渲染成当前 sha,再 apply。渲染比 set image 更彻底,但要求 YAML 里用变量而不是写死。

SHA=$(git rev-parse --short=8 HEAD)
sed -i "s|image: .*app/api:.*|image: registry.example.com/app/api:${SHA}|" deploy.yaml
kubectl apply -f deploy.yaml
kubectl rollout status deploy/app-api --timeout=420s

rollout status 这一句不能省。apply 返回 0 只代表对象被接受,不代表 Pod 已经起来。加上它,至少能保证「新版本真的在跑」。

六、让「部署了哪个版本」一眼可查

这类问题的共同点是:中间环节都在报成功,只有结果不对。所以要有一个从外到内都能验证的锚点。

我们用镜像 label 做这件事。构建时把提交号写进 label,部署后用镜像 ID 反查:

kubectl -n app get pod -l app=api -o jsonpath='{.items[0].status.containerStatuses[0].imageID}'
# registry.example.com/app/api@sha256:4f52a927...

再配一条冒烟检查:请求 /api/version,返回体里带 commit 字段,和这次构建的 sha 比对。不一致就判定部署失败,流水线标红。

这三个点(CI 日志的 sha、镜像 tag/label、运行中 Pod 的 imageID)串起来,任何一环「跑的不是这次提交」都会被抓到。以前我们只验最后一个接口返回 200,那只能证明「服务活着」,证明不了「服务是新的」。

小结

  • 流水线绿了不代表跑的是你的代码。先看构建日志里的 git rev-parse HEAD 和镜像 tag,两处对上再往下查。
  • 重放 push 回调时,after、head_commit.id、commits[0].id 三处都要换成当前 HEAD,只改 ref 等于在旧提交上重放。
  • timestamp 必须 RFC3339 带时区,写成 Unix 时间戳会返回 400 failure to parse hook,这个报错没有提示性。
  • 签名用的字节和发送的字节必须是同一份,json.dumps 千万别生成两次。
  • git update-ref 不触发 post-receive 钩子,可以用它把「改配置」和「触发部署」拆开,避免半途自动部署。
  • kubectl apply 会把 YAML 里写死的旧 tag 覆盖回去,部署文件要拆开或用变量渲染,之后必须 rollout status。
  • 用「CI 日志 sha + 镜像 label + 运行中 imageID + 版本接口」四处对齐,才能证明线上跑的是这次提交。
查看「工程效能」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

重放一条 webhook,流水线跑的是三天前的代码 - 起航社区