Files
deepseek-harness/packages/experimental/forge-github/README.zh.md
T
Yichen Jiang 9b456b4e93 perf(forge): compare from the recorded base while confirming the head
Status snapshots carry the base commit the host recorded with the head
(baseSha). ForgeService.diff passes it to the provider when the latest
status names the requested head, and forge-github starts the compare read
from it while its GraphQL read confirms the head; a stale head or another
base cancels and joins the early read.
2026-11-07 16:22:24 +08:00

115 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
description: "通过宿主上的 GitHub CLI 为实验性 forge 注册表服务 GitHub 主机。"
kind: "package-reference"
---
# @deepseek-ai/dsh-experimental-forge-github
[English](README.md) | 中文
## 概述
借助宿主上已登录的 GitHub CLI,为 `ctx.forge` 读取 GitHub 与 GitHub Enterprise 的 pull request。每次轮询按主机批量查询,读取所有到期 pull request 的状态、改动行数、检查、评审结论、可合并性与未解决的评审讨论。
## 目录
- [使用本包](#use-this-package)
- [了解实现](#understand-the-implementation)
- [深入探索](#further-exploration)
- [模型体验](#model-experience)
- [已知限制与待办工作](#known-limitations-and-deferred-work)
- [开发备注](#dev-note)
-----
<a id="use-this-package"></a>
## 使用本包
在 forge 注册表之后挂载提供方,并显式列出每个 GitHub Enterprise 主机。
```yaml
- name: '@deepseek-ai/dsh-experimental-forge'
- name: '@deepseek-ai/dsh-experimental-forge-github'
config:
hosts: [github.com, ghe.example.com]
```
即使 Session 在其他执行环境中运行命令,提供方也以宿主进程的形式运行 `gh`。每个主机执行一次 `gh auth login --hostname <host>` 完成登录。子进程继承的宿主环境去掉了 `DSH_*` 变量以及名称包含 `KEY`、`PASSWORD`、`SECRET` 或 `TOKEN` 的变量,因此 `GH_TOKEN` 与 `GITHUB_TOKEN` 不会被转发,`gh` 使用自己的凭据存储。交互提示、更新提醒、分页器与颜色输出均被关闭。`maxOutputBytes`、`maxReviewThreads` 与 `maxCommits` 必须是正安全整数;只有 `timeoutMs` 受运行时定时器上限约束。`ghCommand`、`hosts`、`timeoutMs`、`maxOutputBytes`、`maxReviewThreads` 与 `maxCommits` 见[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-forge-github)。
当主机在配置中时,提供方服务 `https`、`http`、`ssh`、`git` 形式的远端 URL,以及 `[email protected]:owner/name.git` 这类 scp 形式。HTTP(S) 远端在主机标识中保留自定义端口;SSH 和 Git 传输端口不计入主机标识。它服务 `https://<host>/<owner>/<name>/pull/<number>` 形式的 pull request URL,允许其后附带任意路径、查询或片段,并以去掉这些后缀的形式作为规范 URL。就绪探测运行 `gh auth status --hostname <host>`:退出码为 0 时为 `ready`,其他退出码为 `unauthenticated` 并带第一行诊断,`gh` 无法启动时为 `unavailable`。按 head 分支查找时,最多返回 20 个以该分支为 head 的打开状态 pull request,按创建时间从新到旧排列,不包含来自 fork 的 pull request;查找默认分支时返回空结果。
轮询只返回 head 提交的前 100 条检查上下文,不追读后续页。预览不完整时设置 `checksTruncated` 并保留 GitHub 的汇总状态,局部计数不代表完整列表。`fetchChecks(ref, signal)` 仅按需读取指定 PR 的完整详情,后续页固定使用首次响应的 rollup。所有 JSON 响应(包括评审讨论分页)的 UTF-8 字节总量受 `maxOutputBytes` 限制,超出时返回 `too-large`。分页失败或游标不前进只使此次详情读取失败,保留轮询结果。同一应用、工作流、触发事件与检查名称的运行只保留运行 id 最新的一条;新的排队运行会替代旧的已完成运行。缺少数字数据库 id 的运行无法确定先后,因此按节点 id 保留。提交状态上下文按名称保留最新结果。完整列表从这些结果计算总体指示;未知上下文类型保留 GitHub 的汇总状态。
每次状态读取按每页最多 100 个统计评审讨论,直到已统计 `maxReviewThreads`(默认 1,000)个为止;第一页之后每读一页多一次 GraphQL 查询。讨论更多的 pull request 设置 `threadsTruncated`,其 `unresolvedThreads` 只统计已读取的讨论。讨论分页失败或游标不前进时,该 pull request 的状态读取失败,同批其他 pull request 不受影响;不发布不完整的计数。
未完成的检查运行(check run),以及结论已过期、缺失或无法识别的已完成运行,均映射为 `pending`。其他已完成的检查映射为 `success`、`failure`(失败、超时、启动失败或需要操作)、`cancelled`、`skipped` 或 `neutral`。已取消的检查使汇总保持未通过状态。检查运行保留其开始与完成时间。提交状态上下文把 `SUCCESS` 映射为 `success`,把 `FAILURE` 与 `ERROR` 映射为 `failure`,其余状态均映射为 `pending`,且不带时间。GraphQL 的 `NOT_FOUND` 错误转为 `not-found` 失败。响应不含 GraphQL data 或 errors 时(包括 HTTP 错误 JSON),使用 stderr 的首条诊断或响应体的 message。认证诊断转为 `unauthenticated`;超出输出大小上限的检查分页转为 `too-large`;限流与其他失败转为 `unavailable`。head 查找在拒绝时保留相同诊断。
pull request 处于打开状态、不是草稿、GitHub 报告其合并状态为 `CLEAN`、`HAS_HOOKS` 或 `UNSTABLE`,且已登录用户具有 write、maintain 或 admin 权限时,`mergeReady` 为真。`mergeMethods` 列出仓库允许的压缩合并、合并提交与变基合并方式,用户默认的合并方式排在最前。标记为可评审运行 `gh pr ready <number> --repo <host>/<owner>/<name>`。合并经 `gh api` 调用 `PUT repos/<owner>/<name>/pulls/<number>/merge`,带上合并方式与预期的 head SHA,因此有新提交到达后 GitHub 会拒绝,报告为 `stale`。与 `gh pr merge` 不同,它从不启用自动合并、从不加入合并队列,也从不删除 head 分支。被拒绝的操作报告 GitHub 的原因,提到身份验证的诊断转为 `unauthenticated`。
`diff` 在一次 GraphQL 查询中读取 `headRefOid` 与 `baseRefOid`,head 不是请求的提交时返回 `stale`;请求的提交必须是完整的 40 位或 64 位十六进制提交。否则经 `gh api --allow-escape-sequences` 以 diff 媒体类型读取 `repos/<owner>/<name>/compare/<baseRefOid>...<headSha>`,即两个提交的合并基准到 head 的补丁,两端都不跟随移动的分支。每个状态快照把 `baseRefOid` 作为 `baseSha` 携带;拿到它时,`diff` 在 GraphQL 查询确认 head 的同时就开始这次比较,head 已过时或基准提交不同时会取消提前开始的比较并等它结束,然后才返回。实测的所有 open 与已合并 pull request 上,该补丁与不接受提交参数的 `gh pr diff` 逐字节一致。与 pull request diff 不同,compare 接口会返回超过 300 个文件的 diff,因此这类 diff 只有在输出超过 `maxOutputBytes` 或 GitHub 自身拒绝时才为 `too-large`。`--allow-escape-sequences` 保留文件内容中的终端转义序列;否则 `gh` 2.97 及以上版本拒绝输出。更早的 `gh` 在发出请求前就以 `unknown flag` 拒绝该选项,且原样输出这类序列,因此遇到这一拒绝时去掉该选项重读一次。`fileText` 经 `gh api` 以 raw 媒体类型读取 `repos/<owner>/<name>/contents/<path>?ref=<commit>`,只接受完整的 40 位或 64 位十六进制提交。`fileUrl` 返回 `https://<host>/<owner>/<name>/blob/<commit>/<path>`。两者的路径都逐段做百分号编码。`commits` 经 GraphQL 读取拉取请求的提交,每页 100 个,owner、name、number 与游标作为变量传入,直到 `maxCommits`;每一页报告的 head 与提交总数必须与第一页相同,游标也不能与之前任何一页重复,否则返回 `unavailable`。`compare` 用与 `diff` 相同的 compare 请求读取两个完整提交的合并基准到第二个提交的补丁。被拒绝的读取在 HTTP 404 时为 `not-found`,诊断提到身份验证时为 `unauthenticated`,GitHub 以 HTTP 406 拒绝或报告 `too_large` 错误时(diff 或文件超过其大小上限即会如此)为 `too-large`,其余为 `unavailable`,并带有 GitHub 的原因。输出超过 `maxOutputBytes` 的读取为 `too-large`,超过 `timeoutMs` 的为 `unavailable`。
提交评审时先检查 PR 当前的 head,再通过 REST API 发布单条行内评论或一次完整评审,并显式指定 `commit_id`。多行评论携带所属侧及包含端点的起止行号。评审结论为“评论”“批准”或“请求更改”。JSON 请求正文通过标准输入传递。provider 不会重试写操作;发送后的超时、异常响应或服务端错误会返回结果不明,由调用方到 GitHub 核对。检查与写入之间到来的新提交可能让已明确定位到旧提交的评审过时;GitHub 的这些接口不提供原子的 head 前置条件。
-----
<a id="understand-the-implementation"></a>
## 了解实现
<details>
<summary>实现细节——点击展开</summary>
一次读取按主机对引用分组,每个主机先发送一个 `gh api graphql` 查询。后续分页使用原汇总节点 id,因此 PR 的 head 变化不会让同一检查列表混入其他提交。查询内联经过校验的 owner、name 与编号,并为每个 pull request 设置别名,因此局部 GraphQL 错误只影响对应的别名。出现局部错误时 `gh` 以非零状态退出,但仍会输出完整响应;提供方先解析该响应,再检查退出码。
| 源码 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | 配置校验与提供方注册 |
| [`src/provider.ts`](src/provider.ts) | URL 映射、按主机分组与失败分类 |
| [`src/graphql.ts`](src/graphql.ts) | 构造查询,并校验和映射响应 |
| [`src/compare.ts`](src/compare.ts) | 经 compare API 读取两个提交之间的补丁,供所有 diff 读取共用 |
| [`src/gh.ts`](src/gh.ts) | 在宿主上执行 `gh`,清理环境变量并施加限制 |
| [`src/urls.ts`](src/urls.ts) | 解析远端 URL 与 pull request URL,并校验写入请求的名称与提交 |
</details>
-----
<a id="further-exploration"></a>
## 深入探索
- [Forge 注册表](../forge/README.zh.md)——提供方注册与共享轮询。
- [Forge 子系统](../../../docs/subsystems/forge.zh.md)——提供方契约与变更请求类型。
- [子进程环境](../../subprocess/subprocess/README.zh.md)——共享的环境变量清理规则。
-----
<a id="model-experience"></a>
## 模型体验
无:提供方为宿主侧监听方读取 pull request 状态,不贡献模型上下文。
#### KV Cache 影响
无。提供方的输出只到达宿主侧监听方。
## 已知限制与待办工作
<a id="known-limitations-and-deferred-work"></a>
- **宿主需要 GitHub CLI**——必须安装 `gh` 并为每个已配置主机登录;环境变量中的令牌不会被转发。不在 `hosts` 中的主机永远不会被读取,包括配置变更后保留的引用。状态读取为这些引用返回 `unavailable`;head 查找会在启动 `gh` 前拒绝。
- **评审讨论上限**——每次状态读取每个 pull request 最多统计 `maxReviewThreads` 个评审讨论;超出时 `unresolvedThreads` 只是下限,由 `threadsTruncated` 标出。
- **批次时限**——评审讨论很多的 pull request 的续页会拉长其主机的轮询批次;批次超过 `ctx.forge` 的 `fetchTimeoutMs` 时,批次内每个 pull request 都报告 `unavailable`,而不只是讨论很多的那个。
- **基准分支被改写**——diff 从 head 与 GitHub 在 pull request 最后一次更新时记录的基准提交的合并基准开始。基准分支被强制推送后,该合并基准可能不同于 GitHub pull request 页面使用的合并基准,因此在 pull request 更新前,补丁可能多出或缺少提交。
- **不发现 fork**——按 head 分支查找时,不包含 head 位于其他仓库的 pull request。
- **速率限制**——每次轮询为每个主机消耗一次 GraphQL 查询,评审讨论每多一页(100 个)再消耗一次;完整检查读取时检查记录每多一页(100 条)再消耗一次;每次 diff 读取消耗一次 GraphQL 查询与一次 REST 请求;最新状态之后基准提交有变化时再多一次 REST 请求。这些都计入已登录账号的速率配额。
- **不支持合并队列**——要求合并队列的基准分支会拒绝直接合并,提供方也不会把变更请求加入队列。
<a id="dev-note"></a>
### 开发备注
<details>
<summary>维护者的工作上下文——点击展开</summary>
无。
</details>