作者:来自 Elastic Pablo Pérez Hidalgo

我们团队如何将 GenAI 引入 CI 流水线,以创建自我纠正的 pull requests,自动化更新大型 monorepos 中数百个依赖项
通过我们的点播网络研讨会提升你的技能:使用 Elasticsearch 的 Agentic RAG,以及 Elasticsearch MCP Server 入门。
你也可以立即利用 Elastic 的 gen AI 功能,方法是开始免费的云试用或在本地运行 Elasticsearch。
在 Elastic Control Plane,也就是 cloud.elastic.co (Elastic Cloud Hosted) 和 Elastic Cloud Enterprise 背后的团队,我们已经将 agentic AI 技术引入到构建流水线中,为我们的代码库带来自愈能力:就像蝾螈能再生四肢一样,我们的 Pull Request 构建会自我修复。本文展示了我们为什么需要采取这一步,我们是如何设计和执行的,我们学到了什么,以及这一改变对我们日常工作的影响。
大型代码库及其维护者就像有机体,任何变化都可能让它们 "生病":构建失败、单元测试失败等等。维护者就像抗体,迅速介入,通过修复由此产生的 bug 来恢复健康,这一过程需要时间和精力。
这些代码库依赖于数百个依赖项。我们在托管或分发的产品中保持它们全部最新。这符合我们的安全标准,但也带来了巨大的工作量,因为更新---修复循环的频率很高。每次更新都是一个可能让构建" 生病" 的潜在病原体。
传统自动化应对的大问题:保持依赖项的最新状态
这篇文章讲的是自动化。在当下,有一个重要的区别需要明确:
- 传统自动化:由软件编码的算法执行步骤,驱动被自动化的流程。它是确定性的,其功能仅限于开发者设计时的预期。
- Generative AI (gen AI) 自动化:由大型语言模型技术从自然语言输入和提示驱动执行步骤。结果通常是非确定性的,并且需要人工监督。
回到代码库---维护者类比为有机体的话题,让我们来谈谈选择实验对象。没错,这篇文章讲的是一个实验。一个如此成功的实验,以至于它在成为完全打磨好的内部功能之前就已经开始帮助我们的团队。
我们维护着一个相当庞大的 monorepo。我们确保依赖项保持最新,并且没有已知的漏洞。
对于我们的核心服务来说,大约有 500 个正在活跃更新的依赖项,检查和提升依赖版本是一个非平凡的挑战。如果手动完成,可能每周会耗费掉数百个工程师工时。
我们采用了由内部工程生产力团队提供的依赖更新管理系统,作为传统自动化。它基于 Renovate。
Renovate 是一个简单但有效的机器人。简而言之,它做两件事:
-
比较我们的代码库依赖目录和上游包仓库。
-
当发现新版本时,打开 Pull Requests。
在大约六个月的运行中,Renovate 提交的 pull requests 已经更新了我们 41% 的依赖项,成为我们最活跃的贡献者之一。

自愈 Pull Requests:修复---批准---合并
我们与 Renovate 的集成非常成功。成功到让团队被大量 PR 审查淹没。当然,可以设置降频,但我们确实希望保持所有内容最新。这让我们意识到,这样的标准带来了代价,可能对项目推进时间产生不利影响。
Renovate 的 PR,就像我们代码库中的其他 PR 一样,需要经过构建流水线:编译代码、运行单元测试 (UTs) 和集成测试 (ITs)、构建 Docker 镜像并发布它们。所有这些都是在 Buildkite 平台上完成的,使用 Gradle 构建步骤,如下截图所示:

Renovate 被设置为在 ITs 成功通过时自动合并 Pull Requests。
人们可能会认为大多数依赖更新都会顺利通过,尤其是补丁版本。那么,麻烦从哪里来呢?问题在于,我们生活在这样一个世界里:依赖维护者会在补丁或次要版本中引入破坏性更改,更糟的是,他们会引入一些完全没有体现在接口上的细微运行时更改(例如 ZooKeeper 从 3.8.3 升级到 3.8.4 时在存在性检查操作中引入的新 ACL 约束 ------ ZOOKEEPER-2590,以及在 Apache Curator 客户端中引发的下游问题 CURATOR-715)。这时构建会失败,就需要人工介入。
真的需要人吗?
我们发现,这些失败的 PR 才是自动更新的真正瓶颈。失败表现为根本无法编译,或者 UTs、ITs 测试失败。我们团队的工程师不得不介入这些情况。这是未计划的工作中断,带来了频繁且对生产有害的上下文切换。
这些修复:
-
通常是自包含的:不需要软件重构,只需根据更新的库进行一些调整。也不需要深度创造性工作。
-
它们提供快速的反馈循环:编辑---编译---测试。
你能想到一种面向重复性自包含编码任务的新兴技术吗?
如果每个需要审查的失败 PR 都能附带修复它的代码变更提议呢?
这正是我们在实验周决定尝试的方法。而且,它奏效了!
但是,怎么做到的呢?
想法很简单:遵循你与同事协作的自然方式。让 AI 介入并对 PR 分支做出贡献。也就是说,把 AI agents 集成到 PR 的工作流中。
这也是编码代理行业正在探索的方法,例如 GitHub Copilot Coding Agent 或 Claude Code GitHub actions。
这类现成的解决方案让代理与代码交互变得容易,但灵活性较差。
我们希望代理能够有针对性地处理构建错误:某个模块的某个单元测试因为这个错误失败了,就去修复那个具体问题,并在修复完成后向工作分支添加一个 commit。

为此,我们需要做到:
-
能够将具体的错误信息和失败的构建任务输入给 AI agent,包括那些依赖内部 Elastic Cloud 服务的任务。
-
赋予它执行和迭代失败的 Gradle 步骤的能力,直到找到解决方案或放弃为止。
就像大多数系统中一样,人类软件工程师的生产力关键在于快速迭代循环:编辑---编译---测试,这也是我们在代码编辑 AI agents 上的决定。
为此,我们只是在本文开头描述的构建步骤集合上增加了一步。这个步骤是 Gradle 构建步骤的复制版本,但不同之处在于它由一个带有非常具体提示的编码代理控制:
bash
`1. # This step is used to fix compilation and UT failures when builds fail. It uses Claude Code to analyze the build logs and suggest fixes that
2. # are then applied to the original branch in the form of commits once verified to have fixed the build. Only branches in elastic/cloud repository
3. # can get these commits. If the source branch lives in a different repository, a new branch with the suggested branches will be posted in elastic/cloud.
4. #
5. # The effect is that automatic updates PR issued by Renovate can self heal. The moment Claude commits to elastic/cloud's PR branch, the build pipeline will be restarted
6. # with the fixes.
7. # Take into account that these commits are only added if Claude can verify that the fix the build by running the initially broken Gradle tasks.
8. # To save time, computational resources and genAI tokens, this step is first run for AMD64 architecture. Chances are that the pushed fixes will also work for ARM64 architecture.
9. #
10. # NOTE: This step is currently using Claude Code agent but this latter could be replaced by any other genAI agent that can be invoked in headless mode from the command line.
11. #
12. - label: ":github: :terminal: :gradle: Claude Fix Build (AMD64)"
13. key: "claude-fix-build-amd64"
14. depends_on: "publishPlatformIndependent-amd64"
15. allow_dependency_failure: true
16. command: |
17. if [ $$(buildkite-agent step get "outcome" --step "publishPlatformIndependent-amd64") != "passed" ]; then
18. echo "--- Trying to fix build with Claude Code"
19. .buildkite/scripts/claude-fix-build.sh
20. else
21. echo "--- There is nothing to fix"
22. fi
23. timeout_in_minutes: 240
24. ...
25. ...
26. ...`AI写代码

claude-fix-build.sh 是一个通过多个步骤调用 agent 的脚本:
-
运行一个 pre-hook 命令,获取我们与 Claude 和 GitHub 交互所需的凭证。
-
克隆目标 GitHub 仓库。
-
准备运行 Gradle 构建步骤。也就是为 Gradle 能够构建工作代码准备环境。到这里为止,这个脚本只是我们已经在 Buildkite workers 中用于常规构建步骤的脚本副本:publishPlatformIndependent-amd64 和 publishPlatformIndependent-arm64。
-
使用 Buildkite CLI 尝试获取前一步失败的构建步骤,从而触发修复日志。正如我们很快会看到的,这是一个性能提升点:AI agent 会分析该文件,推断出哪些构建步骤失败,并针对它们进行迭代修复循环。
bash` 1. # Obtain the result of the build step that failed thus triggering the Claude fix 2. echo "--- Obtaining Gradle log from the failed build step" 3. mkdir /tmp/previous_step_artifacts 4. buildkite-agent artifact download "tmp/gradle_*.log" /tmp/previous_step_artifacts/ 6. if [ -f /tmp/gradle.log ]; then 7. echo "Found Gradle logs from previous job, Claude will use them to analyze the build failure:" 8. ls -l /tmp/previous_step_artifacts 9. else 10. echo "Couldn't find previous stage gradle log, Claude will run the build commands to evaluate what is failing" 11. fi `AI写代码
-
安装并配置 Claude Code agent CLI 工具:
bash` 1. echo "--- Installing Claude" 2. sudo apt update -y 3. sudo apt install -y curl 5. curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash 6. # shellcheck disable=SC1091 7. \. "$HOME/.nvm/nvm.sh" 8. nvm install 22 9. npm install -g @anthropic-ai/claude-code 11. echo "Configuring Claude Code and running environment" 13. # This value needs to be passed with the --allowedTools parameter to claude run 14. export CLAUDE_ALLOWED_TOOLS='Bash,Bash(chmod:*),Bash(git:*),Bash(./gradlew:*),Edit,NotebookEdit,MultiEdit,View,GlobTool,GrepTool,BatchTool,Write,WebFetch,WebSearch' `AI写代码
-
准备 agent 操作日志文件,并将其内容在 stdout 上复现(更多内容见下文):
bash` 1. # Prepare Claude actions log file and replicate its contents in stdout 2. touch /tmp/claude-actions.log 3. tail -f /tmp/claude-actions.log & `AI写代码
-
设置 prompt,这是该集成的核心(这里我们略过细节,因为它值得单独在文章中讲解:The prompt),以及仓库中的 CLAUDE.md 文件。
markdown` 1. # Claude fix prompt string 2. CLAUDE_FIX_PROMPT=$(cat << 'EOF' 3. The build is failing, you might find a Gradle log ... 4. ... 5. ... 6. EOF 7. ) `AI写代码
-
这个功能的最初动机是帮助我们修复提升依赖版本的 Pull Requests。Pull Requests 是由 Renovate 版本管理机器人创建的。我们的 Renovate 设置在检测到上游 main 分支有变化时,会对其 PR 分支进行 rebase。我们观察到这种行为会打断 agent 的工作:通常它正在通过一个耗时的 Gradle 任务验证修复,但最新的 main 分支变化会取消构建。为避免这种情况,我们利用了 Renovate 的 "stop updating labels" 功能,使其停止向带有预配置标签的 PR 分支推送更改。在我们的案例中,标签为 stop-updatin。这是传统自动化与 generative AI 自动化握手实现共同目标的神奇点。我们的集成在告诉 Renovate:"嘿,现在由我来掌控了"。
bash` 1. function add-pr-label() { 2. local label="$1" 3. echo "Adding label '$label' to the PR" 4. if ! curl -f -X POST \ 5. -H "Authorization: token $GITHUB_TOKEN" \ 6. -H "Accept: application/vnd.github.v3+json" \ 7. -H "Content-Type: application/json" \ 8. -d "{\"labels\":[\"$label\"]}" \ 9. https://api.github.com/repos/elastic/cloud/issues/"$BUILDKITE_PULL_REQUEST"/labels; then 10. echo "Failed to add label '$label' to the PR" 11. exit 1 12. fi 13. } 15. ... 16. ... 17. ... 19. echo "--- Block further updates from Renovate until the PR is fixed" 21. # This is done adding a 'stop-updating' label to the PR (https://docs.renovatebot.com/configuration-options/#stopupdatinglabel) 22. add-pr-label "stop-updating" `AI写代码
-
在下一步,这个集成将开始向候选分支推送 AI 生成的 commits,合并一个在构建成功后可能被自动合并的分支。我们团队有一个核心原则:未经人工监督,绝不提交 AI 工作。因此,脚本会确保 GitHub PR 的自动合并被禁用:
bash` 1. echo "--- Making sure auto-merge is disabled for the PR when there are AI contributions" 3. # Get GraphQL PR node id 4. PR_NODE_ID=$(curl -s -X POST \ 5. -H "Authorization: bearer $GITHUB_TOKEN" \ 6. -H "Content-Type: application/json" \ 7. -d "{\"query\":\"query { repository(owner: \\\"elastic\\\", name: \\\"cloud\\\") { pullRequest(number: $BUILDKITE_PULL_REQUEST) { id } } }\"}" \ 8. https://api.github.com/graphql | jq '.data.repository.pullRequest.id' -r 9. ) 11. # Disable automerge 12. AUTOMERGE_FAILURE_MSG="Failed to disable auto-merge for the PR. Aborting: It is dangerous to allow auto-merge when there are AI contributions." 14. if ! AUTOMERGE_NO_ERRORS=$(curl -s -f -X POST \ 15. -H "Authorization: bearer $GITHUB_TOKEN" \ 16. -H "Content-Type: application/json" \ 17. -d "{\"query\":\"mutation { disablePullRequestAutoMerge(input: {pullRequestId: \\\"$PR_NODE_ID\\\"}) { clientMutationId } }\"}" \ 18. https://api.github.com/graphql | jq -r '.errors | length'); then 19. echo "$AUTOMERGE_FAILURE_MSG" 20. exit 1 21. fi 23. if [ "$AUTOMERGE_NO_ERRORS" -ne 0 ]; then 24. echo "$AUTOMERGE_FAILURE_MSG" 25. exit 1 26. fi `AI写代码
-
最后!使用所需配置调用 agent,并根据前面的步骤准备好 prompt。
--allowedTools
参数确定性地限制 Claude 可使用和执行的命令与操作。这对于安全至关重要,并作为第 (5) 步生成的配置的一部分进行设置。正如我们将在 prompt 描述和分析中看到的,我们要求 Claude 在操作发生时解释其动作,并将其附加到第 (6) 步创建的文件claude-actions.log
中。这对于实时监控和最终报告至关重要。 -
当 agent 完成工作后,会执行一些后处理步骤:
markdown
1. 上传操作日志。
markdown
2. 根据是否找到破损构建的解决方案,确定脚本返回码。
3. 向 pull request 添加成功报告标签。
bash
`
1. echo "~~~ Post processing"
3. # Upload claude output log
4. buildkite-agent artifact upload /tmp/claude-actions.log
6. # Evaluate the success of the step in function of the success of the Claude fix
7. if grep -qF "SUCCESSFUL FIX" ; then
8. EXIT_CODE=0
9. else
10. EXIT_CODE=1
11. fi
13. if [ $EXIT_CODE != "0" ] ; then
14. RED='\033[0;31m'
15. NC='\033[0m' # No Color
16. echo -e "${RED}Claude was unable to fix the build, check artifacts for details.${NC}"
17. add-pr-label "claude-fix-failed"
18. else
19. echo "Claude was able to fix the build"
20. add-pr-label "claude-fix-success"
21. fi
23. exit $EXIT_CODE
`AI写代码
关于这个流程,一个重要的点是构建流水线设置为在新提交推送时重新启动。因此,在 agent 推送其更改后,流水线会重新开始,agent 仅在新的迭代失败时才会再次介入。提交控制流水线流程,流水线构建步骤控制 agent 的调用,而 agent 在找到修复时会提交更改。这样,就形成了一个可以描述为"人类监督下的 AI 自主贡献"的完整用户体验闭环。

这种基于 AI agent 的编码方法,对于带有 AI 辅助的编辑器(VSCode+Copilot、Cursor、Windsurf...)来说,就像真正的自动驾驶汽车之于带有定速巡航和车道保持辅助的汽车一样。
"这不是用生成的代码替代我们的工作,也不是让 AI 伙伴在旁边提出建议。而是让 AI 伙伴通过 GitHub 为我们的代码库做出贡献,提供代码变更建议,体验类似于在线开源协作。"
Prompt
尽管它带有行为不确定和歧义的缺点,但 generative AI 的最大优势在于指令是自解释的。这是我们当前使用的 prompt(斜体黑色字体),附带为本文增加的上下文注释(常规,品红色):
构建失败,你可能会在 /tmp/previous_step_artifacts 下找到可供分析的 Gradle 日志,如果没有,你就必须运行构建命令来评估失败原因。读者可能还记得,在集成脚本的第 (4) 步中,我们从 Buildkite 获取了 Gradle 日志,以便 Claude 分析它们。这里我们告诉它查看这些日志并采取相应操作。
vbnet
`
1. These commands are './gradlew "--max-workers=$MAX_WORKERS" --console=plain publishForPlatform' and
3. './gradlew "--max-workers=$MAX_WORKERS" --console=plain publishPlatformIndependent' but you don't need to run them as a first step if the log files under /tmp/previous_step_artifacts exist and contain the necessary information to analyze the failure. ...to fall back to building everything from scratch in those cases where those logs are not available.
`AI写代码
无论如何,你必须找出哪些 Gradle 子任务失败并修复它们,以确保构建成功。这为 Claude 提供了目标:它必须确保构建成功,对源代码应用所有必要的更改(始终遵守下面设置的约束),并对全局或本地子任务的执行进行迭代。
请:
- 分析它们的输出并应用必要的修复以确保它们成功。
这个集成是我们的 AI 贡献伙伴,团队将其视为新入职的员工。一个有能力的员工,但仍需学习"我们的方式":代码风格、偏好技术、需要避免的陷阱等......简而言之,我们在贡献中学到的内容会被编码到 Cloud 仓库中一个不断更新的建议文件中:CLAUDE.MD。Claude 代码默认会查找此文件,但我们希望明确它必须遵循其中的建议:
-
遵循工作目录中 CLAUDE.md 文件下的建议。
-
如果具体的 Gradle 子任务失败,在尝试执行全局任务之前对它们进行迭代。Boom!就这样,你可以通过 Buildkite 实时跟踪 agent 的"思考"和操作。这就是 claude-actions.log 内容被生成的方式。
-
将你执行的每个操作实时记录到文件 /tmp/claude-actions.log 中。每条记录都应以当前时间戳为前缀,格式为
"Claude Action [YYYY-MM-DD HH:MM:SS]: "
,并描述所采取的操作。运行的命令及其输出也应被记录。

-
在同一文件中,用相同格式记录你将要执行的计划以及完成后的结果。
-
在调用
./gradlew
命令时始终包含--max-workers=$MAX_WORKERS
选项。该选项在集成脚本的后处理步骤(第 11 步)中使用。 -
如果构建已修复,请在 /tmp/claude-actions.log 文件最后添加
"SUCCESSFUL FIX"
;否则,在文件末尾添加"FAILED FIX"
。
接下来,是指示 Claude 在修复成功后提交更改的指令:
-
如果成功并添加了
"SUCCESSFUL FIX"
行,请提交修复问题的更改并推送到同一个 Git 仓库的源分支。分支名由BUILDKITE_BRANCH
环境变量提供。你的提交信息应以"Claude fix: "
为前缀。 -
推送更改时,必须使用 GitHub token 认证,例如:
git push https://token:$GITHUB_TOKEN@github.com/elastic/cloud.git HEAD:<BRANCH_NAME>
,其中 token 存储在GITHUB_TOKEN
环境变量中。 -
将每个修复分为不同的 commit,以保证历史记录清晰可理解。
-
如果 Git 推送因认证问题失败,1 分钟后重试一次。如果仍失败,则 5 分钟后再试一次,最后在 10 分钟后进行最后一次尝试。
-
在推送到 Git 之前:只有在修复成功且有更改需要推送时 ,为 PR 添加
"claude-fix-success"
标签。 -
不要更改与构建修复无关的内容。人类往往懒惰,AI 更甚。我们必须引入这一点,因为发现 Claude 往往通过......移除版本升级来修复版本提升问题。
-
绝不降级依赖版本,即 elastic/cloud 主分支版本目录中声明的依赖版本(如 gradle/libs.versions.toml),相对于 master 分支,宁可失败也不要降级版本。
结果与经验教训
在其运行的第一个月内,并且仅限于 45% 的依赖项,自愈 PR 成为我们 Cloud GitHub 仓库的主要贡献者之一。该插件成功修复了总共 24 个最初失败的 PR,Claude 作者在 7 月 22 日至 8 月 22 日期间提交了 22 个 commit。

我们的估算显示,在此期间,它的贡献为团队节省了 20 天的活跃开发工作。这大大减少了繁琐、重复且低价值的工程工作,这些工作对代码库有机体至关重要,但在性能指标中不可见。
即使它未能完全修复问题,它也会推动进展,提示解决方案的方向或缩小搜索空间。
我们还学到,调优 AI agent 的行为是成功与失败的关键。通过向 CLAUDE.md 仓库文件贡献来教授团队技能,使它停止遵循不良编码实践,但更重要的是,使其变得勤勉。我们学到,没有什么比未受教育的编码 agent 更懒。这个教学过程永无止境,但每个新增的提示和规则都能转化为节省的一天工作。
"信任但要验证"
我在文章开头强调了传统自动化与 generative AI 自动化的区别。后者的不确定性意味着你永远不能盲目信任它提出的更改。审查是必须的,对提议更改持批判态度对于这个类似蝾螈的代码有机体的健康至关重要。否则,我们可能会面对表面完美、实际却潜藏着异类错误的代码所带来的恶性事件和 bug。
结论
我们已经看到 CI 和 AI 如何协同工作,以满足不断扩展的信息安全质量标准需求。

工具与需求的交汇点促使我们设计了一个应用,它的行为几乎像人类,在协作代码仓库中添加贡献,并且可以轻松控制传统自动化,避免机器人之间的拉锯战。
这只是开始。有了这个工具,我们开始探索其他直接应用。
例如,通过在所有 pull requests 上启用它,我们可以仅提交不完整的 Pull Requests,将 API 规范重新生成或 lint 等不太有创意的任务留给集成,期望它添加必要的 commits 来完成这些对软件工程师既重要又枯燥的步骤。
我们惊讶地观察到,它能够绕过辅助构建服务中的临时问题,在必要时通过主动执行已批准的工具填补空缺。
我们预计,这将远超帮助自动更新的作用。
谁知道呢?甚至可能发展到极端的"反蝾螈 PR":人类编写接口及其单元测试,而自愈 PR 来完成其余工作。
这次试点的成功为我们制定了计划:在 Cloud 仓库中为所有 Renovate PR 启用该集成,并可能扩展到所有 Pull Requests,无论其来源如何。