一、问题背景:两个流水线为什么打架?
1.1 我遇到的真实场景
有一次我在维护公司内部的代码发布流程时,发现 Bitbucket 自带的管道一直在跑,但 Jenkins 那边的状态却老是不对。明明 Bitbucket 上已经显示成功,Jenkins 还停留在失败上;有时候反过来,Jenkins 早就构建完了,Bitbucket 还卡在运行中。整个团队盯着两个面板,不知道信谁。最后大家只能靠猜,代码到底能不能上线,谁也不敢打包票。
我一开始以为是配置写错了,后来把日志翻了一遍才发现,问题根本不是“配置错了”,而是两个系统之间传递状态的通道太脆弱。公司项目用了 Bitbucket 托管代码,也用了它自带的 CI 功能来做单元测试和镜像打包。但有一些老任务还留在 Jenkins 上,比如发版时的自动化部署。为了让两边状态能对上,我最初用了 Webhook 回调:Jenkins 构建结束后,主动把结果发给 Bitbucket,让 Bitbucket 更新提交状态。想法是美好的,但现实很骨感。
1.2 为什么会同步失败
这种“Jenkins 主动推给 Bitbucket”的方式,听起来很直接,实际上坑很多。第一,Jenkins 和 Bitbucket 之间会有网络隔离,公司内部 Jenkins 在外网环境下根本访问不到 Bitbucket 的 API;第二,回调用的临时令牌很容易过期,过期后 Bitbucket 拒绝接收,日志里只留一个 400;第三,哪怕网络通、令牌没过期,Jenkins 的插件版本和 Bitbucket 的 API 版本之间也可能有兼容问题。反正结果就是,状态同步经常悄悄失败,而且没有通知。
我在排查的时候还发现了一个更要命的点:Bitbucket 收到回调时,要求请求头里必须带一个工作区标识,否则直接给你返回错误。很多回调插件根本没把这个头加上去。这一下子就让我明白,与其到处补丁,不如换一种更主动、更可控的方案。
二、解决思路:把“推送”改成“拉取后再推送”
2.1 先分清楚谁在管谁
遇到这种双 CI 混用的场景,不能同时让两边都有“最终决定权”。应该先定一个主系统。我的建议是:如果代码仓库在 Bitbucket 上,那就让 Bitbucket 当“裁判”。Jenkins 只负责干活,干得好不好,最后由 Bitbucket 来记录和展示。也就是说,状态同步的方向应该是:从 Jenkins 拿到构建结果,再写入 Bitbucket。
2.2 关键知识点:Bitbucket 的 commit status 是什么
这里需要先介绍一个概念:提交状态。Bitbucket 允许外部系统通过自己的 API,为某一次代码提交附加一条“构建状态”。展示在代码提交列表旁边,比如绿色勾代表成功,红色叉代表失败。这样就不用打开两个页面来回对照了。Jenkins 也可以调用这个接口,把自己针对某个提交的构建结果发过去。
但要注意,这个接口和 Bitbucket 自带的管道状态是分开的。也就是说,你可以让 Bitbucket 同时显示“自带管道”和“Jenkins 上报”的两条状态。只有两边都有明确的结果,开发人员才能放心。
2.3 一个更稳定的同步模型
我最后采用的思路是:不要等 Jenkins 回调,而是写一个同步器,由 Bitbucket 这边的流程主动去 Jenkins 查询构建状态,等 Jenkins 干完活之后,再把结果手动写到 Bitbucket 上。这样做的好处是,主动权掌握在我们自己手里,Bitbucket API 的调用方变成了我们自己的脚本,响应头、令牌这些东西都可以控制。
另外还能避免一个尴尬问题:如果 Jenkins 的回调发早了,Bitbucket 还觉得没开始,后面的状态就会被覆盖。现在改成同步器在查询时发现 Jenkins 还没跑完,就继续等一会儿,等它出结果再上报。这样脏数据就少了很多。
三、动手实现:用 Python 写状态同步器
3.1 准备工作:准备两边的访问凭证
写代码之前,得先准备两把钥匙。
第一把是 Jenkins 的 API 令牌。进入 Jenkins 后台,在用户设置页面里可以生成一个 API Token。这个令牌用来呼叫 Jenkins 的 REST API,查构建结果。注意一定不要把密码写死在脚本里,放到环境变量里更安全。
第二把是 Bitbucket 的访问令牌。在 Bitbucket 个人设置里创建一个 App Password,或者用 Repository Access Token 也行。至少需要提交状态写入的权限。这个令牌用来告诉 Bitbucket:“我要帮你更新这个提交的状态。”
这两个令牌准备好之后,我把它们放到环境变量里,让脚本自己去读取。这样脚本即使传到公共服务器上,也不会泄露密钥。
3.2 完整脚本示例
下面这个脚本做的事情很简单:先从 Jenkins 那边拿到指定任务的最新构建结果,然后把结果翻译成 Bitbucket 能看懂的状态,最后上报到 Bitbucket 上。脚本用的是 Python 3,依赖 requests 库,如果还没装,先用包管理器装一下。
# 技术栈:Python 3 + requests 库
# 作用:把 Jenkins 的构建结果同步到 Bitbucket 提交状态
import os
import sys
import time
import argparse
import requests
from urllib.parse import quote # 用来对 job 名称做 URL 转义
# 从环境变量里读取配置,避免把密钥写进代码
BITBUCKET_WORKSPACE = os.getenv("BITBUCKET_WORKSPACE") # Bitbucket 工作区名,例如 myteam
BITBUCKET_REPO = os.getenv("BITBUCKET_REPO") # 仓库名,例如 myapp
BITBUCKET_TOKEN = os.getenv("BITBUCKET_TOKEN") # Bitbucket 访问令牌
JENKINS_URL = os.getenv("JENKINS_URL") # Jenkins 地址,例如 https://jenkins.example.com
JENKINS_USER = os.getenv("JENKINS_USER") # Jenkins 登录用户名
JENKINS_TOKEN = os.getenv("JENKINS_TOKEN") # Jenkins API 令牌
def get_jenkins_build_status(job_name):
"""获取 Jenkins 某个任务的最新构建状态,并轮询直到构建完成。"""
# 先请求 Jenkins 的 lastBuild 接口,拿到最新构建的元数据
api_url = f"{JENKINS_URL}/job/{quote(job_name)}/lastBuild/api/json"
# Jenkins 大多数接口需要用户名和令牌做基本认证
resp = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN), timeout=10)
resp.raise_for_status() # 如果请求失败,这里会抛出异常
build_data = resp.json()
build_number = build_data["number"]
result = build_data.get("result") # 如果构建还在跑,这个字段会是 null
# 如果结果还是空,说明 Jenkins 还在构建中,每隔 10 秒查一次
while result is None:
print(f"构建 #{build_number} 还在进行中,10 秒后再查一次...")
time.sleep(10)
resp = requests.get(api_url, auth=(JENKINS_USER, JENKINS_TOKEN), timeout=10)
resp.raise_for_status()
result = resp.json().get("result")
print(f"构建 #{build_number} 的最终结果是:{result}")
return result
def map_to_bitbucket_status(jenkins_result):
"""把 Jenkins 的各种结果翻译成 Bitbucket 的提交状态。"""
# Jenkins 常见值有 SUCCESS、UNSTABLE、FAILURE、ABORTED
# Bitbucket 常见提交状态有 SUCCESSFUL、FAILED、INPROGRESS、STOPPED
mapping = {
"SUCCESS": "SUCCESSFUL", # 成功 -> 成功
"UNSTABLE": "FAILED", # 不稳定按失败算,方便提示风险
"FAILURE": "FAILED", # 失败 -> 失败
"ABORTED": "STOPPED", # 被中止 -> 停止
}
# 如果遇到没见过的状态,默认按失败处理
return mapping.get(jenkins_result, "FAILED")
def report_to_bitbucket(commit_hash, status):
"""把最终状态上报到 Bitbucket 指定提交上。"""
# 拼接 Bitbucket 提交状态接口地址
url = (
f"https://api.bitbucket.org/2.0/repositories/"
f"{BITBUCKET_WORKSPACE}/{BITBUCKET_REPO}/commit/{commit_hash}/statuses/build"
)
# 注意:Bitbucket 要求必须带一个工作区标识头,否则会返回 400
headers = {
"Authorization": f"Bearer {BITBUCKET_TOKEN}",
"Content-Type": "application/json",
"X-Workspace-Id": BITBUCKET_WORKSPACE,
}
# 上报的内容,给开发人员看的状态信息
payload = {
"key": "jenkins-sync", # 状态键,用来区分是谁上报的
"name": "Jenkins Build Sync", # 展示名称
"state": status, # 状态值
"description": f"Jenkins 同步结果: {status}", # 简单描述
"url": f"{JENKINS_URL}/job/jenkins-sync/lastBuild", # 跳转链接
}
resp = requests.post(url, json=payload, headers=headers, timeout=10)
resp.raise_for_status() # 上报失败时立刻报错,方便排查
print(f"Bitbucket 状态更新成功,当前状态:{status}")
def main():
# 支持命令行传参,这样可以在不同提交上复用同一个脚本
parser = argparse.ArgumentParser(description="同步 Jenkins 构建状态到 Bitbucket")
parser.add_argument("--job", required=True, help="Jenkins 任务名称")
parser.add_argument("--commit", required=True, help="Bitbucket 提交的完整哈希值")
args = parser.parse_args()
# 检查环境变量是否都配好了
required_vars = [
BITBUCKET_WORKSPACE,
BITBUCKET_REPO,
BITBUCKET_TOKEN,
JENKINS_URL,
JENKINS_USER,
JENKINS_TOKEN,
]
if not all(required_vars):
print("缺少环境变量配置。请先设置 BITBUCKET_WORKSPACE、BITBUCKET_REPO、")
print("BITBUCKET_TOKEN、JENKINS_URL、JENKINS_USER、JENKINS_TOKEN")
sys.exit(1)
# 第一步:从 Jenkins 获取构建结果
jenkins_result = get_jenkins_build_status(args.job)
# 第二步:转换成 Bitbucket 的状态
bitbucket_status = map_to_bitbucket_status(jenkins_result)
# 第三步:上报到 Bitbucket
report_to_bitbucket(args.commit, bitbucket_status)
# 当脚本被直接执行时,进入 main 函数
if __name__ == "__main__":
main()
这段脚本的核心逻辑就是三个函数,第一个负责从 Jenkins 拉状态,第二个负责翻译状态,第三个负责把状态推到 Bitbucket。注释也写在关键位置了,就算之前没接触过这两个平台的 API,也能顺着代码读明白。
3.3 运行验证
脚本准备好了,接下来怎么跑?我一般会在本地先手动执行一次,确认令牌、接口都通。下面是运行时需要设置的环境变量和命令示例:
# 设置环境变量,把实际的密钥换成你自己的
export BITBUCKET_WORKSPACE="myteam"
export BITBUCKET_REPO="myapp"
export BITBUCKET_TOKEN="ATBB你的令牌"
export JENKINS_URL="https://jenkins.example.com"
export JENKINS_USER="admin"
export JENKINS_TOKEN="你的Jenkins令牌"
# 执行脚本,传入 Jenkins 任务名和 Bitbucket 提交哈希
python3 sync_status.py --job "deploy-job" --commit "a1b2c3d4e5f6..."
如果看到输出里出现“Bitbucket 状态更新成功”,那就说明整条链路已经打通。这个时候回到 Bitbucket 提交详情页,就能看到一条来自 Jenkins 的状态。以后凡是跑这个脚本的提交,状态都会自动同步过去。
四、把同步器放进日常流程
4.1 放在 Bitbucket 管道里
手动跑脚本只能解决一次问题,真正要解决的是每天的自动化同步。最省事的办法是把这个脚本放到 Bitbucket 自带的管道里,作为一个额外的步骤。比如在管道配置的最后一步加上:先等待 Jenkins 构建结束,再调用这个脚本。这样每次代码提交后,Bitbucket 自带的步骤和 Jenkins 的步骤都完成时,状态就能统一。
4.2 定时任务兜底
除了放在管道里,我还会加上一个定时任务做兜底。原因很简单:有时候提交已经发布了,但是 Jenkins 那个构建可能被手动取消,或者 Bitbucket 管道本身在等待回调时超时了。用定时任务每隔五分钟扫描一遍尚未同步成功的提交,发现漏的就把状态补上。这个兜底看起来笨,但非常有效,能避免两边状态长时间不一致。
五、应用场景与优缺点分析
5.1 这个方案适合哪些场景
如果你现在正被两个 CI 系统的状态搞得焦头烂额,尤其是 Bitbucket 自带管道和 Jenkins 同时存在,这个方案就很对症。它适合那种“代码仓库在 Bitbucket,部分构建或部署任务在 Jenkins”的项目。因为最终状态又回到 Bitbucket 展示,所以开发人员只需要盯一个地方就够了。对于曾经用过 Webhook 回调但总出问题的小团队来说,这种“主动拉取”的脚本也更简单,不需要维护插件,也不需要去调整回调地址。
5.2 技术优缺点
先说说优点。第一是稳定,不再依赖 Jenkins 主动访问外部网络,所有请求都是从我们自己的环境发出的,网络策略很好控制。第二是可控性强,状态如何翻译、什么时候上报、上报什么内容,全部由我们代码决定。第三是排查方便,脚本里每一步都有输出,失败时能立刻看到是连接不上 Jenkins,还是 Bitbucket 拒绝了请求。
再说说缺点。这个方案并不是实时的,脚本每隔一段时间去轮询,所以状态可能会有几分钟延迟。相比之下,原来 Webhook 回调是实时的,但问题是经常失败。另外一个缺点是,脚本需要自己维护,如果有多个 Jenkins 任务,需要把任务名和提交哈希对应起来,不能靠自动发现。如果以后大家都只用同一个 CI,这套同步脚本也就退休了。
六、注意事项与踩坑记录
6.1 注意 Bitbucket 的响应头
这是我在实际调试中遇到最大的一个坑。Bitbucket 的提交状态接口确实要求请求头里带上工作区标识,没有这个头,接口直接返回 400。所以不论是用 Python、Java 还是别的方式调这个接口,都得记得加上这个响应头,否则会一直找不到为什么失败。
6.2 避免无限轮询
脚本里虽然用了轮询,但要注意别让它无限等下去。如果 Jenkins 上的构建因为某个原因永远挂在队列里,脚本也会一直卡着。我更建议在代码里增加一个最长等待时间,比如超过三十分钟就直接退出。这个限制在真实生产环境里非常重要。因为没有人想看到 CI 卡住一整天。
6.3 安全建议
访问令牌一定要放在环境变量或者密钥管理工具里,不能直接写进仓库。Bitbucket 的令牌也不要有太多权限,只要提交状态写入就行,其他权限一律不开。Jenkins 的 API 令牌也是一样,最好专门建一个只有查看构建权限的账号。这样即使脚本泄露,别人也做不了太多破坏。
七、文章总结
两个 CI 系统同时存在的时候,状态同步失败几乎是必然的,因为不同系统的消息格式、网络策略、令牌有效期都不一样。与其想办法修补脆弱的回调机制,不如写一个小脚本,主动到 Jenkins 那边查结果,再统一写到 Bitbucket 上。这样做虽然多写了一点代码,但换来的是稳定和可控。脚本本身也不复杂,核心就是“查状态、翻译状态、写状态”。如果你也遇到类似的问题,不妨按照这个思路试一试。
评论
围绕“Bitbucket自带CI与Jenkins集成时流水线状态同步失败的解决”参与讨论