# PR #2877 完整报告

- 仓库：`radixark/miles`
- 标题：fix(ci): mint the command-App token with pull-requests: write for label commands
- 合并时间：2026-09-02 14:33
- 原文链接：http://prhub.com.cn/radixark/miles/pull/2877

---

# 执行摘要

- 一句话：修复 CI 命令应用令牌权限，解决标签命令的 HTTP 403 错误。
- 推荐动作：该 PR 值得精读，因为它修复了阻塞性的 CI 问题，并展示了良好的工程实践：在修复权限的同时增强错误诊断能力。设计决策如在错误消息中包含权限信息，有助于未来快速调试权限相关问题。

# 功能与动机

标签命令在处理拉取请求时，使用问题 API 添加标签需要 `pull-requests: write` 权限。之前的令牌只请求了 `pull-requests: read`，导致 GitHub API 返回 403 错误，阻止了标签添加和后续 CI 启动。PR body 中明确指出：'POST /repos/radixark/miles/issues/{n}/labels for an issue that is a pull request' 需要 `pull_requests` 范围的写权限。

# 实现拆解

变更分为四个核心步骤：
1. **权限配置调整**：修改 `.github/workflows/comment-ci-command.yml`，将 `permission-pull-requests: read` 更改为 `write`，并添加注释说明标签命令在 PR 上需要写权限。这直接修复了令牌铸造错误。
2. **错误处理增强**：在 `.github/workflows/scripts/comment_ci_command.py` 的 `GitHubAPI._request` 方法中，改进 HTTP 错误处理，构造包含请求方法、路径和 GitHub 接受权限头的详细错误消息，避免仅报告 'HTTP 403'。
3. **测试覆盖补充**：在 `tests/ci/test/test_comment_ci_command.py` 中新增两个测试函数 `test_http_error_names_the_request_and_accepted_permissions` 和 `test_http_error_without_accepted_permissions_still_names_the_request`，验证错误消息格式；同时更新现有工作流测试以断言新权限行的存在（如 `\n          permission-pull-requests: write\n`）。
4. **文档同步更新**：更新 `docs/ci/01-label.md` 和 `docs/ci/05-command-identity.md`，说明新权限要求、安装批准需求，以及缓解措施（固定处理代码和作业作用域令牌）。

所有变更都围绕 CI 基础设施，没有涉及核心源码逻辑。

关键文件：
- `tests/ci/test/test_comment_ci_command.py`（模块 CI 命令测试；类别 test；类型 test-coverage；符号 test_http_error_names_the_request_and_accepted_permissions, test_http_error_without_accepted_permissions_still_names_the_request, urlopen）: 测试文件是本次变更的核心验证部分，新增测试覆盖错误消息和权限断言，确保修复有效性。
- `.github/workflows/scripts/comment_ci_command.py`（模块 CI 工作流脚本；类别 infra；类型 infrastructure）: 脚本中的错误处理改进是本次修复的关键部分，使权限错误更易诊断。
- `.github/workflows/comment-ci-command.yml`（模块 CI 工作流；类别 config；类型 configuration）: 工作流配置文件，直接修复令牌权限问题。
- `docs/ci/01-label.md`（模块 CI 文档；类别 docs；类型 documentation）: 文档更新，说明新权限要求和缓解措施。
- `docs/ci/05-command-identity.md`（模块 CI 文档；类别 docs；类型 documentation）: 文档更新，详细说明标签令牌需要 `Pull requests: write` 的原因和缓解措施。

关键符号：test_http_error_names_the_request_and_accepted_permissions, test_http_error_without_accepted_permissions_still_names_the_request

## 关键源码片段

### `tests/ci/test/test_comment_ci_command.py`

测试文件是本次变更的核心验证部分，新增测试覆盖错误消息和权限断言，确保修复有效性。

```python
# 新增测试：验证 HTTP 错误消息包含请求方法、路径和接受权限头
def test_http_error_names_the_request_and_accepted_permissions(monkeypatch):
    def urlopen(_request, *, timeout):
        # 模拟 GitHub API 返回 403 错误，附带接受权限头
        raise urllib.error.HTTPError(
            "url", 403, "forbidden", {"X-Accepted-GitHub-Permissions": "pull_requests=write"}, None
        )

    monkeypatch.setattr(HANDLER.urllib.request, "urlopen", urlopen)
    with pytest.raises(HANDLER.CommentCommandError) as excinfo:
        HANDLER.GitHubAPI("secret-token").add_label(123, "run-ci-short")
    message = str(excinfo.value)
    # 断言错误消息包含方法、路径和权限信息
    assert message == (
        "GitHub API returned HTTP 403 for POST /repos/radixark/miles/issues/123/labels; "
        "accepted permissions: pull_requests=write"
    )
    # 确保令牌不在消息中泄露
    assert "secret-token" not in message

# 新增测试：验证错误消息在无接受权限头时仍包含请求信息
def test_http_error_without_accepted_permissions_still_names_the_request(monkeypatch):
    def urlopen(_request, *, timeout):
        raise urllib.error.HTTPError("url", 404, "missing", {}, None)

    monkeypatch.setattr(HANDLER.urllib.request, "urlopen", urlopen)
    with pytest.raises(HANDLER.CommentCommandError) as excinfo:
        HANDLER.GitHubAPI("secret-token").get_permission("actor")
    # 断言错误消息包含方法和路径，但不包含权限头
    assert str(excinfo.value) == (
        "GitHub API returned HTTP 404 for GET /repos/radixark/miles/collaborators/actor/permission"
    )

```

### `.github/workflows/scripts/comment_ci_command.py`

脚本中的错误处理改进是本次修复的关键部分，使权限错误更易诊断。

```python
# 在 GitHubAPI._request 方法中，改进 HTTP 错误处理
def _request(self, path, *, method="GET", payload=None, expected_statuses=None, ...):
    ...
    try:
        response = self.urllib.request.urlopen(request, timeout=15)
        ...
    except urllib.error.HTTPError as error:
        # 原错误消息仅为 'GitHub API returned HTTP {error.code}'
        # 改进后包含方法和路径，以及可选的接受权限头，便于调试
        detail = f"GitHub API returned HTTP {error.code} for {method} {path}"
        # GitHub 的 X-Accepted-GitHub-Permissions 头指示了该调用所需的安装权限
        accepted = error.headers.get("X-Accepted-GitHub-Permissions")
        if accepted:
            detail = f"{detail}; accepted permissions: {accepted}"
        # 抛出详细的错误消息，但不包含令牌
        raise CommentCommandError(detail) from error
    except urllib.error.URLError as error:
        ...

```

# 评论区精华

无实质性技术讨论；仅有一个来自 `claude[bot]` 的自动化评论，说明手动代码审查配置，未涉及设计或实现细节。

- CI 命令应用令牌权限修复 (other): PR 直接合并，无未解决疑虑。

# 风险与影响

- 风险：风险较低，但需注意：
 - **兼容性风险**：如果 GitHub 应用安装未更新权限（`Pull requests: write`），令牌铸造可能失败。但 PR body 提到 App 已持有该权限，因此无失败窗口。
 - **回归风险**：错误消息格式变更可能影响依赖旧消息的监控或日志解析，但测试已覆盖新格式。
 - **安全风险**：错误消息现在包含请求路径和权限头，但不包含令牌，符合安全实践。
 - 影响：影响范围限于 CI 命令网关的标签命令功能，修复后标签操作将正常工作，允许后续 CI 工作流启动。影响程度为中等，因为 CI 流程的稳定性得到提升，但变更不涉及训练或推理核心路径。
 - 风险标记：CI 权限修复 , 错误消息改进 , 文档同步更新

# 关联脉络

- PR #2813 feat(ci): add user allowlists for command tiers: 同属 CI 命令网关功能线，都涉及 `.github/workflows/comment-ci-command.yml` 和相关测试文件。