一、问题背景:两个流水线为什么打架?

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 上。这样做虽然多写了一点代码,但换来的是稳定和可控。脚本本身也不复杂,核心就是“查状态、翻译状态、写状态”。如果你也遇到类似的问题,不妨按照这个思路试一试。