Read OSS

GitHub 自动化 — 基于 AI 的大规模 Issue 管理

中级

前置知识

  • 第 1 篇:架构概览
  • GitHub Actions 基础知识(工作流、事件、权限)
  • TypeScript / Shell 脚本基础

GitHub 自动化 — 基于 AI 的大规模 Issue 管理

claude-code 仓库不只是存放 Claude Code 插件的地方——它还使用 Claude Code 来管理自身。这种递归式 dogfooding 模式贯穿了 12 个 GitHub Actions 工作流、自定义 slash 命令、沙箱化 CLI 包装器以及 TypeScript 生命周期脚本。有人提交 Issue 时,Claude Code 会对其分类;出现重复 Issue 时,Claude Code 会自动识别;Issue 过期时,脚本会在提供宽限期和作者否决机制的前提下将其关闭。

本文将梳理一个 Issue 的完整生命周期,并深入分析让 AI 代理在生产环境中安全运行于 GitHub 仓库之上的三层安全模型。

Issue 生命周期流水线

在 claude-code 仓库中,一个 Issue 最多会经历七个阶段,每个阶段由不同的工作流或脚本负责处理:

stateDiagram-v2
    [*] --> Opened: Issue created
    Opened --> Triaged: claude-issue-triage.yml
    Triaged --> DupeChecked: claude-dedupe-issues.yml
    DupeChecked --> Active: No duplicates found
    DupeChecked --> DupeCommented: Duplicate detected
    DupeCommented --> AutoClosed: 3-day grace + no dispute
    DupeCommented --> Active: Author disputes (👎 reaction)
    Active --> Stale: sweep.ts (14d inactive)
    Stale --> Closed: sweep.ts (14d after stale label)
    Active --> NeedsInfo: Triage labels needs-info/needs-repro
    NeedsInfo --> Closed: sweep.ts (7d no response)
    NeedsInfo --> Active: Info provided → re-triage
    Closed --> Locked: lock-closed-issues.yml

生命周期标签及其超时时间统一定义在 scripts/issue-lifecycle.ts 中,作为唯一数据源:

export const lifecycle = [
  { label: "invalid",   days: 3,  reason: "this doesn't appear to be about Claude Code" },
  { label: "needs-repro", days: 7, reason: "we still need reproduction steps" },
  { label: "needs-info",  days: 7, reason: "we still need more information" },
  { label: "stale",      days: 14, reason: "inactive for too long" },
  { label: "autoclose",  days: 14, reason: "inactive for too long" },
] as const;

所有需要超时信息的脚本和工作流都从这个文件导入。常量 STALE_UPVOTE_THRESHOLD(10 个点赞反应)提供了一个安全阀——热门 Issue 即使长期不活跃也不会被标记为过期。

AI 驱动的分类与去重

Issue 被提交后,两个工作流会并行触发:

claude-issue-triage.yml(位于 /.github/workflows/claude-issue-triage.yml)通过 anthropics/claude-code-action@v1 执行 /triage-issue 命令。它使用 Claude Opus(claude-opus-4-6),超时时间为 5 分钟。并发组机制确保同一个 Issue 同一时刻只有一个分类任务在运行。

位于 /.claude/commands/triage-issue.md 的分类命令有严格的约束:

  • 只能添加或删除标签——不得评论,不得编辑 Issue
  • 只能使用已有的标签——先获取标签列表,绝不自创新标签
  • 对生命周期标签保持保守态度——"漏打标签比打错标签代价更小"
  • 对新 Issue 和评论采用不同的处理逻辑——新 Issue 进行全面分类;评论只触发生命周期标签更新

该命令会区分 issues 事件(完整分类)和 issue_comment 事件(生命周期标签管理)。在评论场景下,它会移除 stale/autoclose 标签(新活动意味着 Issue 仍然存活),并判断是否应移除 needs-repro/needs-info 标签(即所需信息已被提供)。

flowchart TD
    EVENT{"Event type?"} --> NEW["issues (new issue)"]
    EVENT --> COMMENT["issue_comment"]

    NEW --> LABELS["Fetch available labels"]
    LABELS --> READ["Read issue details"]
    READ --> VALID{"About<br/>Claude Code?"}
    VALID -->|No| INVALID["Label: invalid"]
    VALID -->|Yes| CATEGORIZE["Apply category labels<br/>(type, area, platform)"]
    CATEGORIZE --> DUPECHECK["Search for duplicates"]
    DUPECHECK --> LIFECYCLE["Evaluate lifecycle labels<br/>(needs-repro, needs-info)"]
    LIFECYCLE --> APPLY["Apply all labels"]

    COMMENT --> READ_CONV["Read full conversation"]
    READ_CONV --> STALE{"Has stale/<br/>autoclose?"}
    STALE -->|Yes| REMOVE["Remove stale labels"]
    STALE -->|No| NEEDS{"Has needs-repro/<br/>needs-info?"}
    NEEDS -->|Yes| PROVIDED{"Info<br/>provided?"}
    PROVIDED -->|Yes| REMOVE_NEEDS["Remove needs-* label"]
    PROVIDED -->|No| KEEP["Keep label"]

    style INVALID fill:#E53935,color:#fff

claude-dedupe-issues.yml(位于 /.github/workflows/claude-dedupe-issues.yml)使用 Sonnet 执行 /dedupe 命令。位于 /.claude/commands/dedupe.md 的去重命令会启动 5 个并行代理进行多样化搜索——每个代理使用不同的关键词和策略来寻找潜在的重复 Issue。最终由一个过滤代理消除误报,再通过评论脚本将结果发出。该工作流还会将事件记录到 Statsig 用于数据分析。

提示: 分类命令的工具限制(Bash(./scripts/gh.sh:*)Bash(./scripts/edit-issue-labels.sh:*))体现了与第 2 篇 allowed-tools 相同的设计模式——将 AI 代理约束在特定的、可审计的操作范围内。在公开仓库上运行 AI 时,这一点至关重要。

带宽限期的自动关闭

位于 scripts/auto-close-duplicates.ts 的自动关闭脚本实现了一套精细的宽限期机制。当去重命令发出"疑似重复"评论后,倒计时便开始。三天后,脚本会判断该 Issue 是否应被关闭:

flowchart TD
    START["Fetch open issues<br/>created >3 days ago"] --> SCAN["For each issue"]
    SCAN --> DUPE{"Has bot 'possible<br/>duplicate' comment?"}
    DUPE -->|No| SKIP["Skip"]
    DUPE -->|Yes| AGE{"Comment older<br/>than 3 days?"}
    AGE -->|No| SKIP
    AGE -->|Yes| ACTIVITY{"Any comments<br/>after dupe comment?"}
    ACTIVITY -->|Yes| SKIP
    ACTIVITY -->|No| REACTION{"Author gave 👎<br/>on dupe comment?"}
    REACTION -->|Yes| SKIP
    REACTION -->|No| EXTRACT["Extract duplicate<br/>issue number"]
    EXTRACT --> CLOSE["Close as duplicate"]

    style CLOSE fill:#E53935,color:#fff
    style SKIP fill:#4CAF50,color:#fff

作者否决机制是这里的关键设计细节。脚本会专门检查 Issue 作者是否在重复检测评论上点了踩(通过 reaction.user.id === issue.user.id 进行匹配):

scripts/auto-close-duplicates.ts#L228-L241

如果作者提出异议,Issue 将保持开放。如果三天内没有回应,Issue 将被关闭,同时附上说明并邀请作者重新开启。

@claude 提及处理器

claude.yml 工作流允许用户直接在 GitHub 的 Issue 和 PR 中用自然语言与 Claude Code 交互。当 @claude 出现在 Issue 评论、PR review 评论、PR review 正文或 Issue 正文中时,该工作流就会触发:

if: |
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
  (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
  ...

这使得每个 Issue 和 PR 都成为与 Claude Code 交互的入口。该工作流使用带有只读权限的 anthropics/claude-code-action@v1 和 Sonnet——它可以读取代码并给出回应,但无法推送变更。

sequenceDiagram
    participant U as User
    participant GH as GitHub
    participant WF as claude.yml
    participant CC as Claude Code Action

    U->>GH: Comment: "@claude explain this error"
    GH->>WF: issue_comment event
    WF->>WF: Check: contains '@claude'?
    WF->>CC: Run claude-code-action
    CC->>GH: Read repository context
    CC->>GH: Post response comment

安全沙箱:三层防御

在公开 GitHub 仓库上运行 AI 代理需要认真考量安全问题。claude-code 仓库实现了三层防御体系:

第一层:gh.sh — 沙箱化 CLI 包装器

位于 scripts/gh.sh 的脚本将 gh CLI 限制为四个以读取为主的子命令:

case "$CMD" in
  "issue view"|"issue list"|"search issues"|"label list")
    ;;
  *)
    echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed"
    exit 1
    ;;
esac

参数 flag 采用白名单机制(--comments--state--limit--label)。搜索命令明确屏蔽了 repo:org:user: 限定符,以防止跨仓库访问。Issue 编号必须为纯数字。仓库始终通过 GH_REPO 限定范围,无法跳转到其他仓库。

这是纵深防御的体现:即使 Claude Code 生成了意外的 gh 命令,包装器也会将其拒绝。

第二层:DevContainer 防火墙

位于 .devcontainer/devcontainer.json 的 DevContainer 配置需要 NET_ADMINNET_RAW 能力:

"runArgs": [
  "--cap-add=NET_ADMIN",
  "--cap-add=NET_RAW"
]

这些能力让位于 .devcontainer/init-firewall.sh 的防火墙脚本得以配置 iptables 规则。防火墙采用默认拒绝策略:

iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

然后有选择地放行以下流量:

  • GitHub(IP 从 api.github.com/meta 获取)
  • npm registryregistry.npmjs.org
  • Anthropic APIapi.anthropic.com
  • Statsigstatsig.anthropic.comstatsig.com
  • VS Code 插件市场和更新服务器
  • Sentry 错误上报

防火墙还会进行自我验证:确认 example.com 不可达,且 api.github.com 可达。任一检查失败,脚本都会报错退出。

第三层:企业级托管配置

位于 examples/settings/ 的示例展示了组织如何在策略层面锁定 Claude Code。

flowchart TD
    subgraph "Layer 1: CLI Wrapper"
        GH["gh.sh<br/>4 allowed subcommands<br/>Flag allowlist<br/>Injection prevention"]
    end

    subgraph "Layer 2: Network Firewall"
        FW["init-firewall.sh<br/>Default-deny iptables<br/>Allowlisted domains only<br/>Self-verification"]
    end

    subgraph "Layer 3: Enterprise Settings"
        ES["managed-settings.json<br/>Permission lockdown<br/>Hook restrictions<br/>Tool deny lists"]
    end

    GH --> FW --> ES

    style GH fill:#4CAF50,color:#fff
    style FW fill:#2196F3,color:#fff
    style ES fill:#9C27B0,color:#fff

企业配置档案

仓库提供了三种部署档案,展示了从宽松到严格的逐级锁定方式:

配置项 宽松 严格 Bash 沙箱
禁用 --dangerously-skip-permissions
屏蔽插件市场
屏蔽用户/项目权限规则
屏蔽用户/项目 hook
拒绝 WebSearch/WebFetch 工具
Bash 需要审批
Bash 必须在沙箱中运行

位于 examples/settings/settings-strict.json 的严格档案展示了完整的锁定配置:

{
  "permissions": {
    "disableBypassPermissionsMode": "disable",
    "ask": ["Bash"],
    "deny": ["WebSearch", "WebFetch"]
  },
  "allowManagedPermissionRulesOnly": true,
  "allowManagedHooksOnly": true,
  "strictKnownMarketplaces": []
}

各关键属性说明:

  • allowManagedPermissionRulesOnly:忽略用户和项目级别的权限规则,只有企业托管的规则生效
  • allowManagedHooksOnly:屏蔽来自用户配置和项目配置的 hook
  • strictKnownMarketplaces: []:空数组屏蔽所有插件市场
  • disableBypassPermissionsMode: "disable":阻止用户使用 --dangerously-skip-permissions 运行

bash-sandbox 档案中的沙箱配置限制更为彻底,可以限定 bash 命令能访问的域名,并屏蔽本地 socket 访问。

提示: 这些配置文件可以在配置层级的任何位置生效,但 strictKnownMarketplacesallowManagedHooksOnlyallowManagedPermissionRulesOnly 等属性只有在企业配置中才会起作用。建议先将其应用到 managed-settings.json 进行本地测试,再部署到整个组织。

过期清理(Staleness Sweep)

位于 scripts/sweep.ts 的清理脚本按计划运行,负责两件事:将不活跃的 Issue 标记为过期,以及关闭生命周期标签已到期的 Issue。

markStale 函数会分页遍历所有开放的 Issue,按更新时间排序(最旧的优先)。它会跳过 PR、已锁定的 Issue、已分配的 Issue、已标记为过期的 Issue,以及点赞数不低于 10 的 Issue。其余所有在过期时间窗口内未更新的 Issue 都会被打上标签。

closeExpired 函数会遍历每个生命周期标签,找出标签添加时间已超出超时限制的 Issue,并检查标签添加之后是否有真人活动。这是最后的安全网——如果真实用户(非机器人)在标签添加后留下了评论,即便脚本本应关闭该 Issue,它也会被保留:

scripts/sweep.ts#L124-L138

脚本还支持 --dry-run 模式,只记录将会发生的操作而不实际执行——这对于测试生命周期配置变更来说必不可少。

系列总结

在这五篇文章中,我们从高层架构出发,逐步深入到插件组件模型、多代理编排模式、hook 实现,最后到 GitHub 自动化基础设施,完整梳理了 anthropics/claude-code 仓库的全貌。这个仓库是扩展 Claude Code、以及用 Claude Code 管理自身的参考实现。

我们所探讨的这些模式——约定优于配置的插件发现机制、适配不同模型层级的代理设计、fail-open 与 fail-closed 的 hook 哲学、分层安全沙箱——不仅适用于这个特定的仓库,更是任何团队将 AI 代理融入开发工作流的通用构建模块。

如果你正在开发 Claude Code 插件,从 plugin-dev skill 的文档入手是最好的选择。如果你正在规划企业级部署,从 settings 示例开始。如果你对在 GitHub 上大规模运行 AI 代理感到好奇,这里的自动化基础设施就是目前最好的参考实现。