CMA:Issue 到 PR 全流程
从 Issue 创建到提交 PR 的完整自动化
🌐 查看英文原文 | 源码 Notebook
编排:从 issue 到合并的 PR
本 notebook 引导 Agent 通过一个现实的端到端循环:读取模糊的 bug 报告、找到 bug、修复它、打开 PR、通过 CI、处理审查反馈并合并。真正的维护者工作流从来不是线性的,这就是练习的重点——Agent 必须在多种不同工具类型(读取 JSON、grep 代码、编辑文件、运行模拟 CLI、解析 CI 输出)之间携带状态,同时从两个链中意外中恢复:CI 失败和要求 docstring 的审查机器人。
状态通过链条流动:issue 正文 → 文件路径 → 修复 diff → PR 编号 → CI 输出 → 审查评论 → 最终合并。fixture 中的 gh-mock CLI 将所有内容持久化在 .gh-state/ 中,以便每个步骤都能看到前一步做了什么,在没有任何网络访问的情况下模拟真实的 GitHub 工作流。
除了 iterate notebook 之外,这还教会你:
- 跨长链条的多轮引导。 Session 文件系统和对话历史在轮次之间持久化,因此每条用户消息都从上一条消息停止的地方继续。我们利用这一点在最后验证最终状态。
- 链中恢复。 Agent 必须读取 CI 失败或审查评论并进行调整,而不是盲目重试。
fixture 位于 example_data/orchestrate/ 中,包含一个模拟 gh CLI、一个 issue JSON 文件和一个带有植入 bug 的 src/ + tests/ 布局。
import io
import os
import zipfile
from pathlib import Path
from anthropic import Anthropic
from utilities import stream_until_end_turn, wait_for_idle_status
MODEL = os.environ.get("COOKBOOK_MODEL", "claude-sonnet-4-6")
GH_TOKEN = os.environ.get("GITHUB_TOKEN") # only needed for the github_repository sidebar
client = Anthropic()
FIXTURE = Path("example_data") / "orchestrate"1. 打包 fixture
模拟仓库打包了几个文件:src/url_utils.py 包含实际的 bug,src/blog.py 是一个使 bug 更容易观察的调用者,tests/test_urls.py 在 bug 修复之前会失败,而 gh-mock CLI 加 issue_42.json 提供了 Agent 将驱动的类 GitHub 工作流。我们在内存中压缩目录并作为单个文件资源上传——有关如何改为挂载真实 GitHub 仓库,请参见末尾的侧边栏。
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
for f in FIXTURE.rglob("*"):
if f.is_file() and f.name != "README.md":
zf.write(f, f.relative_to(FIXTURE))
buf.seek(0)
fixture_zip = client.beta.files.upload(file=("repo.zip", buf, "application/zip"))
print(f"fixture: {fixture_zip.id}")2. Agent + environment + session
Environment 声明 pytest 为 pip 依赖,以便 Agent 可以在其 CI 循环中实际运行测试套件。这是本 cookbook 中第一个需要网络访问来安装包的 notebook,因此在原本 limited 的网络配置旁边设置了 allow_package_managers: True。
agent = client.beta.agents.create(
name="cookbook-orchestrate",
model=MODEL,
system=(
"You are a maintainer bot. You read issues via `./gh-mock`, explore "
"the codebase, write fixes, and shepherd PRs through CI and review. "
"When CI fails or a reviewer requests changes, read what they said "
"and address it, don't just retry blindly.\n\n"
"Work in /mnt/user. All gh-mock commands run from there."
),
tools=[
{
"type": "agent_toolset_20260401",
"default_config": {
"enabled": True,
"permission_policy": {"type": "always_allow"},
},
}
],
)
env = client.beta.environments.create(
name="cookbook-orchestrate-env",
config={
"type": "cloud",
"networking": {"type": "limited", "allow_package_managers": True},
"packages": {"pip": ["pytest"]},
},
)
session = client.beta.sessions.create(
environment_id=env.id,
agent={"type": "agent", "id": agent.id, "version": agent.version},
resources=[{"type": "file", "file_id": fixture_zip.id, "mount_path": "repo.zip"}],
title="Issue #42 → PR",
)
print(f"session: {session.id}")3. 运行完整链条
一条指令启动整个循环。需要关注的两个恢复点是 CI 失败和审查评论。如果 Agent 的第一次修复不完整——例如,如果它只处理了 é 而遗漏了 ü——gh-mock pr checks 将以非零退出,带有 Agent 需要读取和迭代的 pytest 输出。然后,一旦 CI 变绿,如果 slugify() 缺少 docstring,审查机器人将阻止合并,给 Agent 在最终合并步骤之前再一次适应的机会。
client.beta.sessions.events.send(
session_id=session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": (
"Unpack /mnt/session/uploads/repo.zip into /mnt/user "
"and ship a fix for issue #42 end-to-end. Read the "
"./gh-mock script first to see what subcommands it "
"supports; use those to view the issue, open a PR, "
"run CI, handle review feedback, and merge. Show me "
"the final PR state when you're done."
),
}
],
}
],
)
print("=== full orchestrate chain ===")
stream_until_end_turn(client, session.id)4. 多轮验证
Session 是有状态的:容器文件系统和对话历史在轮次之间持久化,因此后续只需发送另一个 user.message。我们在这里利用这一点独立验证最终状态——模拟 CLI 将 PR 状态持久化在 .gh-state/pr_101.json 中,因此打印该文件是确认 PR 最终以 CI 绿色且在合并前至少有一个审查批准而合并的最简单方法。
client.beta.sessions.events.send(
session_id=session.id,
events=[
{
"type": "user.message",
"content": [
{
"type": "text",
"text": (
"Print the contents of /mnt/user/.gh-state/pr_101.json so "
"I can see the final state, CI status, and reviews."
),
}
],
}
],
)
stream_until_end_turn(client, session.id)
wait_for_idle_status(client, session.id)
client.beta.sessions.archive(session.id)
client.beta.environments.archive(env.id)
client.beta.agents.archive(agent.id)
print("archived")侧边栏:挂载真实 GitHub 仓库
上面的 fixture 是模拟的,这样 notebook 可以离线运行,你不需要任何 GitHub 凭证来尝试它。对于针对真实仓库的真实工作,将上面的 {"type": "file", ...} 挂载替换为 {"type": "github_repository", ...} 资源,API 将在 session 启动时将仓库克隆到容器中。
这与 iterate notebook 中的文件挂载使用相同的 resources= 字段——该列表接受混合类型,因此你也可以在同一调用中克隆仓库并挂载单独的配置文件。Agent 的 bash/read/grep 工具将工作树视为普通目录;唯一的区别是 API 处理克隆而不是你。
session = client.beta.sessions.create(
environment_id=env.id,
agent={"type": "agent", "id": agent.id, "version": agent.version},
resources=[
{
"type": "github_repository",
"url": "https://github.com/anthropics/claude-cookbooks",
"mount_path": "/workspace/cookbook",
"authorization_token": GH_TOKEN,
"checkout": {"type": "branch", "name": "main"},
}
],
title="Repo explorer",
)私有仓库和克隆认证都需要 GITHUB_TOKEN。克隆在 session 创建时发生一次;后续轮次在相同的工作树上工作,无需重新克隆。
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。