GitHub 不只是代码托管平台,更是 Agent 工作流的调度中心。通过 GitHub App + Webhook + Check Run API,可以把 Issue 自动转成 Agent 任务,在 PR 中自动触发代码审查,把 CI 结果写回 Check Run,让 Agent 和开发者在同一个界面中协作。本文给出完整的集成架构和可运行的代码。

GitHub 集成实战:Issue、PR、Checks 与 Agent 任务流

GitHub 不只是代码托管平台,更是 Agent 工作流的调度中心。通过 GitHub App + Webhook + Check Run API,可以把 Issue 自动转成 Agent 任务,在 PR 中自动触发代码审查,把 CI 结果写回 Check Run,让 Agent 和开发者在同一个界面中协作。本文给出完整的集成架构和可运行的代码。

一、集成架构

text
┌─────────────────────────────────────────────────────────────────┐
│                        GitHub 平台                               │
│                                                                 │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────────┐ │
│  │  Issues   │   │ PR / Diff│   │Check Runs│   │  Discussions │ │
│  └────┬─────┘   └────┬─────┘   └─────▲────┘   └──────▲───────┘ │
│       │              │               │                │         │
└───────┼──────────────┼───────────────┼────────────────┼─────────┘
        │              │               │                │
   Webhook         Webhook         API 回写         API 回写
        │              │               │                │
┌───────▼──────────────▼───────────────────────────────┼─────────┐
│                    Agent 工作台                        │         │
│  ┌──────────────────────────────────────────────────┐│         │
│  │              Webhook 接收层                        ││         │
│  │  · 签名校验                                       ││         │
│  │  · 事件路由                                       ││         │
│  │  · 幂等去重                                       ││         │
│  └──────────────────┬───────────────────────────────┘│         │
│                     │                                 │         │
│  ┌──────────────────▼───────────────────────────────┐│         │
│  │              事件处理器                            ││         │
│  │  ┌─────────┐ ┌──────────┐ ┌──────────────────┐  ││         │
│  │  │Issue 处理│ │PR 处理   │ │Check Run 回写    │  ││         │
│  │  │→任务创建│ │→Agent审查│ │→状态/摘要/日志   │  ││         │
│  │  └─────────┘ └──────────┘ └──────────────────┘  ││         │
│  └──────────────────────────────────────────────────┘│         │
│                                                      │         │
│  ┌──────────────────────────────────────────────────┐│         │
│  │              Agent 执行引擎                        ││         │
│  │  上下文包生成 → Agent 修复/审查 → 质量门禁        ││         │
│  └──────────────────────────────────────────────────┘│         │
└──────────────────────────────────────────────────────┘         │

二、GitHub App 配置

2.1 权限范围(最小权限原则)

yaml
# github-app-manifest.yaml
# https://github.com/settings/apps → New GitHub App
github_app:
  name: "agent-workbench"
  description: "AI Agent 工作台 — 自动处理 Issue、审查 PR、回写检查结果"
  
  # 权限范围(只给必要的最小权限)
  permissions:
    # 读取权限
    issues: "read"           # 读取 Issue 创建 Agent 任务
    pull_requests: "read"    # 读取 PR diff 做审查
    contents: "read"         # 读取仓库代码
    metadata: "read"         # 读取仓库元信息
    
    # 写入权限
    checks: "write"          # 回写 Check Run 结果
    issues_comments: "write" # 在 Issue 中评论进度
    pull_request_reviews: "write"  # 提交 PR Review
    statuses: "write"        # 回写 Commit Status
  
  # 订阅的事件
  events:
    - issues              # Issue 创建/编辑/关闭
    - issue_comment       # Issue 评论(Agent 命令)
    - pull_request        # PR 创建/更新/关闭
    - push                # 代码推送(触发检查)
  
  # Webhook
  webhook_url: "https://agent.example.com/webhook/github"
  webhook_secret: "${WEBHOOK_SECRET}"  # 用于签名校验
  
  # OAuth(可选,用于以用户身份操作)
  oauth:
    callback_url: "https://agent.example.com/auth/github/callback"

2.2 Webhook 签名校验

python
# app/webhook/verifier.py
import hmac
import hashlib
import time
from fastapi import Request, HTTPException

WEBHOOK_SECRET = os.environ["GITHUB_WEBHOOK_SECRET"]
MAX_SKEW_SECONDS = 300  # 时间戳偏差容忍

async def verify_github_webhook(request: Request) -> dict:
    """
    校验 GitHub Webhook 签名。
    防止伪造请求触发 Agent 任务。
    """
    # 1. 获取签名头
    signature = request.headers.get("X-Hub-Signature-256")
    if not signature:
        raise HTTPException(401, "Missing signature")
    
    # 2. 读取请求体
    body = await request.body()
    
    # 3. 计算 HMAC
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    
    # 4. 比对(时间安全比对,防止时序攻击)
    if not hmac.compare_digest(f"sha256={expected}", signature):
        raise HTTPException(401, "Invalid signature")
    
    # 5. 检查时间戳(防止重放攻击)
    timestamp = request.headers.get("X-Hub-Signature-Timestamp")
    if timestamp:
        skew = abs(time.time() - int(timestamp))
        if skew > MAX_SKEW_SECONDS:
            raise HTTPException(401, "Timestamp too old")
    
    # 6. 解析 payload
    payload = json.loads(body)
    
    # 7. 幂等检查(GitHub 可能重发 Webhook)
    delivery_id = request.headers.get("X-GitHub-Delivery")
    if is_duplicate(delivery_id):
        return {"status": "duplicate", "data": payload}
    
    return payload

三、核心事件处理

3.1 Issue → Agent 任务

python
# app/handlers/issue_handler.py
"""
当 Issue 被打上 'agent-fix' 标签时,自动创建 Agent 任务。
"""
from typing import Optional

# Issue 标签 → Agent 任务类型映射
LABEL_TO_TASK = {
    "agent-fix": "bugfix",
    "agent-review": "code_review",
    "agent-test": "test_generation",
    "agent-docs": "documentation",
}

async def handle_issue_event(payload: dict):
    action = payload["action"]
    issue = payload["issue"]
    repo = payload["repository"]["full_name"]
    
    if action == "opened" or action == "labeled":
        # 检查是否有 Agent 标签
        labels = [l["name"] for l in issue["labels"]]
        agent_labels = [l for l in labels if l in LABEL_TO_TASK]
        
        if not agent_labels:
            return  # 不是 Agent 任务
        
        task_type = LABEL_TO_TASK[agent_labels[0]]
        
        # 创建 Agent 任务
        task = await create_agent_task(
            task_type=task_type,
            source={
                "platform": "github",
                "repo": repo,
                "issue_number": issue["number"],
                "title": issue["title"],
                "body": issue["body"],
                "author": issue["user"]["login"],
                "labels": labels,
            },
            priority=infer_priority(labels, issue["title"]),
        )
        
        # 在 Issue 中评论确认
        await github_client.create_comment(
            repo=repo,
            issue_number=issue["number"],
            body=f"🤖 Agent 任务已创建({task_type}),任务 ID: `{task.id}`\n\n"
                 f"预计处理时间: {task.estimated_time}\n"
                 f"进度将在此 Issue 中更新。"
        )

def infer_priority(labels: list, title: str) -> str:
    """从标签和标题推断任务优先级"""
    if "P0" in labels or "🔥 critical" in labels:
        return "P0"
    if "P1" in labels or "bug" in labels:
        return "P1"
    if any(kw in title.lower() for kw in ["紧急", "urgent", "hotfix"]):
        return "P0"
    return "P2"

3.2 PR → Agent 审查

python
# app/handlers/pr_handler.py
"""
PR 创建或更新时,自动触发 Agent 代码审查。
"""

async def handle_pull_request_event(payload: dict):
    action = payload["action"]
    pr = payload["pull_request"]
    repo = payload["repository"]["full_name"]
    
    if action not in ["opened", "synchronize"]:
        return  # 只处理 PR 创建和更新
    
    # 1. 创建 Check Run(状态:进行中)
    check_run = await github_client.create_check_run(
        repo=repo,
        name="Agent Code Review",
        head_sha=pr["head"]["sha"],
        status="in_progress",
        started_at=datetime.now().isoformat(),
    )
    
    try:
        # 2. 获取 PR diff
        diff = await github_client.get_pr_diff(
            repo=repo,
            pr_number=pr["number"],
        )
        
        # 3. 生成上下文包
        context = await build_review_context(
            repo=repo,
            diff=diff,
            pr_description=pr["body"],
            changed_files=[f["filename"] for f in pr["files"]],
        )
        
        # 4. Agent 审查
        review_result = await agent_review(
            context=context,
            review_prompt=REVIEW_PROM,
        )
        
        # 5. 提交 Review
        if review_result.comments:
            await github_client.create_review(
                repo=repo,
                pr_number=pr["number"],
                commit_id=pr["head"]["sha"],
                event="COMMENT" if review_result.approved else "REQUEST_CHANGES",
                body=review_result.summary,
                comments=[
                    {
                        "path": c["file"],
                        "line": c["line"],
                        "body": f"**[{c['severity']}]** {c['message']}",
                    }
                    for c in review_result.comments
                ],
            )
        
        # 6. 更新 Check Run 结果
        await github_client.update_check_run(
            repo=repo,
            check_run_id=check_run["id"],
            status="completed",
            conclusion="success" if review_result.approved else "action_required",
            output={
                "title": f"Agent Review: {'✅ Approved' if review_result.approved else '⚠️ Changes Requested'}",
                "summary": review_result.summary,
                "text": review_result.detail_report,
            },
        )
    
    except Exception as e:
        # 失败也要更新 Check Run
        await github_client.update_check_run(
            repo=repo,
            check_run_id=check_run["id"],
            status="completed",
            conclusion="failure",
            output={
                "title": "Agent Review Failed",
                "summary": f"审查过程出错: {str(e)}",
            },
        )
        raise

3.3 完整 Webhook 路由

python
# app/webhook/router.py
from fastapi import FastAPI, Request

app = FastAPI()

EVENT_HANDLERS = {
    "issues": handle_issue_event,
    "pull_request": handle_pull_request_event,
    "push": handle_push_event,
    "check_run": handle_check_run_event,
}

@app.post("/webhook/github")
async def github_webhook(request: Request):
    # 1. 签名校验
    payload = await verify_github_webhook(request)
    if payload.get("status") == "duplicate":
        return {"status": "ok", "message": "duplicate, skipped"}
    
    # 2. 事件路由
    event_type = request.headers.get("X-GitHub-Event")
    handler = EVENT_HANDLERS.get(event_type)
    
    if handler:
        # 异步处理,不阻塞 Webhook 响应
        asyncio.create_task(handler(payload))
    
    # 3. 快速返回(GitHub 要求 10 秒内响应)
    return {"status": "ok"}

四、GitHub Actions 集成

yaml
# .github/workflows/agent-trigger.yml
name: Agent 任务触发

on:
  issues:
    types: [opened, labeled]
  issue_comment:
    types: [created]
  pull_request:
    types: [opened, synchronize]

jobs:
  trigger-agent:
    runs-on: ubuntu-latest
    # 只在特定标签或评论触发
    if: |
      (github.event_name == 'issues' && contains(join(github.event.issue.labels.*.name), 'agent-')) ||
      (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/agent')) ||
      (github.event_name == 'pull_request')
    
    steps:
      - uses: actions/checkout@v4
      
      - name: 解析 Agent 命令
        id: parse
        run: |
          if [ "${{ github.event_name }}" == "issue_comment" ]; then
            COMMENT="${{ github.event.comment.body }}"
            TASK_TYPE=$(echo "$COMMENT" | head -1 | sed 's|/agent \([a-z]*\).*|\1|')
            echo "task_type=$TASK_TYPE" >> $GITHUB_OUTPUT
            echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
          elif [ "${{ github.event_name }}" == "issues" ]; then
            echo "task_type=bugfix" >> $GITHUB_OUTPUT
            echo "issue_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
          fi

      - name: 触发 Agent 任务
        env:
          AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }}
          AGENT_ENDPOINT: ${{ vars.AGENT_ENDPOINT }}
        run: |
          curl -X POST "$AGENT_ENDPOINT/tasks" \
            -H "Authorization: Bearer $AGENT_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "source": "github_actions",
              "repo": "${{ github.repository }}",
              "task_type": "${{ steps.parse.outputs.task_type }}",
              "issue_number": ${{ steps.parse.outputs.issue_number }},
              "check_run_id": "${{ github.run_id }}"
            }'

      - name: 更新 Issue 评论
        if: github.event_name == 'issues' || github.event_name == 'issue_comment'
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: ${{ steps.parse.outputs.issue_number }},
              body: '🤖 Agent 任务已触发,处理中... 进度将通过 Check Run 更新。'
            });

五、真实经验与踩坑

5.1 Webhook 超时不能阻塞

场景:Webhook handler 直接调用 Agent 做代码审查,审查耗时 2 分钟。 问题:GitHub 要求 Webhook 10 秒内响应,超时后会重试,导致同一个事件被处理多次,Agent 重复审查同一个 PR。 解决方案:Webhook handler 只做"接收 + 入队",快速返回 200。实际处理通过消息队列异步执行。同时用 X-GitHub-Delivery 头做幂等去重,同一个 delivery ID 只处理一次。

5.2 Check Run 状态要及时更新

场景:Agent 审查 PR 过程中出错,但 Check Run 一直停在 "in_progress" 状态。 问题:PR 页面显示"等待检查",开发者不知道出了问题,也无法合并。 解决方案:在 try/except 中确保异常时也更新 Check Run 状态为 failure。同时设置超时机制——如果 Check Run 超过 15 分钟还没完成,自动标记为 timeout 并通知维护者。

5.3 安装权限不要过宽

场景:GitHub App 一开始申请了 contents: write 权限,让 Agent 可以直接推送代码。 问题:安全审计时被标记为"权限过宽"——Agent 不应该有直接推送 main 分支的权限。 解决方案:收窄权限——Agent 只通过 PR 提交代码修改,不需要 contents: write。对于需要自动推送的场景(如自动格式化),限制在特定分支(agent/*)并使用 Branch Protection Rules 控制合并。

六、参数说明表

参数 类型 默认值 说明
webhook_secret string 必填 Webhook 签名密钥
app_id string 必填 GitHub App ID
private_key string 必填 GitHub App 私钥(PEM 格式)
permissions object 见 2.1 权限范围
events list 见 2.1 订阅的事件类型
check_run_timeout int 900 Check Run 超时时间(秒)
retry_max int 3 Webhook 重试最大次数
rate_limit_buffer int 100 预留的 API 调用配额
allowed_repos list ["*"] 允许触发 Agent 的仓库列表
auto_create_pr bool false Agent 修复后是否自动创建 PR

七、落地检查清单

  • GitHub App 权限遵循最小权限原则
  • Webhook 签名校验已启用(SHA-256)
  • Webhook handler 10 秒内返回响应
  • 事件处理有幂等去重(delivery ID)
  • Check Run 状态在异常和超时时也能正确更新
  • PR Review 评论内容有严重级别标注
  • Agent 推送代码限制在 agent/* 分支
  • Webhook 失败的告警机制已配置
  • API 调用有速率限制保护
  • 审计日志记录了每次 Agent 操作

八、系列导航

上一篇:案例 004:用 Agent 做数据库迁移前的风险评估 下一篇:Jira / Linear 集成实战:从需求卡片到 Agent 执行计划