Repository navigation
ci: Robin 改用 zengate 网关访问 big-pickle(删 bootstrap、零 secret) - #67
Conversation
原方案 pull_request_target → bootstrap 用 GH_PAT 代发 /review → issue_comment
真实审查。它有两个绕不开的问题:
1. 依赖仓库 secret GH_PAT。secret 一旦丢失,bootstrap 走静默 exit 0,
PR 打开后不再自动审查,且毫无征兆。
2. pull_request_target 把 PR 代码带进高权限上下文,为让审查跑起来不得不
引入一个持有 repo scope 的个人 PAT。
本改动删除整个 bootstrap,改由 pull_request 事件直连,并在 CI 内用
zengate 起本地网关,由真实的 opencode serve 子进程与 OpenCode Zen 通信:
- 全仓 .github/ 已无任何 secrets.* 引用,github.token 是每次运行自动轮换的
内建凭据、不是 secret,fork PR 同样拿得到。
- Zen 免费档有五道准入,runner 即便满足前四道仍恒 403;zengate 拉起真实
opencode 进程与 Zen 通信,绕开的是「来源」那一道,不涉及任何配额或限流。
- 网关自身鉴权用 ALLOW_NO_AUTH 关闭且只监听 127.0.0.1,故全链路无 API key。
两个非显然的关键点:
1. use-json-response-mode: false(必设)
review-retry.ts 的 shouldRetryStructuredReview 写着 `if (usedJson) return
false`,即 JSON 模式下空审查一次都不会重试;而 action.yml 里该项默认
true。模型偶发返回字面量 {} 时会被当成「0 条发现」直接放行——对审查闸门
是最坏的假通过。关掉后重试保护恢复,实测还更快、发现更多。
2. 冒烟判定必须先落盘再判状态码
`curl -sf | head -c N` 在 bash -e 下退出码取自管道末端的 head(恒 0),
网关返 4xx/5xx 时这道闸门依然报通过。改为显式取 %{http_code} 再断言。
另加一处防御:Zen 的免费模型目录会随时间漂移(实测两天内换过一批),就绪
后显式校验目标模型仍在售,不在售则退出 1 并列出当前在售 id,避免下游只看到
「冒烟没答 OK」这种误导性报错。
同时补充 code-reviewer.md:禁止工具调用的声明、项目渲染/连接铁律、15 条
误报红线,以及版本号检查的降级路径措辞收紧。
|
/review |
|
关于本 PR 上 review 检查显示 cancelled 的说明(不是失败,是预期行为): 本 PR 修改的正是
于是两条 workflow 同时在本 PR 上触发,而它们声明的是同一个并发组 时序证据(排除「评论链触发」这一竞争假设):
取消比 合并后此现象自动消失:base 只剩新 workflow,一条链路一条 run,不再有同组竞争。在 09 仓(base 已是新方案)实测端到端正常——见 PR 描述里的 run 另:本 PR 上的 |
排查发现 action 的投票窗口起点用的字段是错的,且偏差可达数小时。 `git-democracy` 的 main.ts:81 取 `pullRequest.data.head.repo.pushed_at` 当 `voteStartedAt`,voting.ts 用它 + minVotingWindowMinutes 判窗口是否满。 但 `repo.pushed_at` 是**头仓任意分支**的最后 push 时间,与本 PR 无关。 实测 PR SiliconMod#67: 头提交真实更新时间 2026-09-27T14:42:24Z action 用的窗口起点 pushed_at 2026-09-27T16:22:55Z 偏差 101 分钟 那次 16:22 的 push 是往同仓的 test 分支推的,与 PR SiliconMod#67 毫无关系。 后果:任何人对头仓任意分支的一次 push,都会把该 PR 的投票窗口向后推。 而全仓 `.github/` 里**没有任何 schedule 触发器**(已 grep 确认三个 workflow 的 on: 只有 pull_request / pull_request_target / pull_request_review / issue_comment / push / workflow_dispatch),窗口被推之后没有任何事件会 重新评估 → PR 静默卡住,且 required check democracy 会一直红着挡住合并。 本脚本早已改用头提交时间(正确值),但 required check democracy 仍由 action 用 pushed_at 计算,ruleset 卡的还是错的那个值。这是 action 上游的缺陷, workflow 内无法覆盖,故此处只做两件事: 1. 把两个窗口起点与差值打进日志,让偏差可见(否则只能靠猜) 2. 票数判定全过却被 ruleset 拒合并时,点明真正原因是 required check 而非票数 —— GitHub 对这种拒绝只回一句 "Pull Request is not mergeable",不提示原因, 维护者很容易误以为是投票没过。附上具体排查步骤。 同时更正 PR 描述里的一处不准确表述:先前称「票投够后那次评估会因窗口未满 而失败」,实测两次真评估失败(PR#67 14:43、PR#66 06:59)的 voting failure message **只有「票数不足」一条**、不含窗口那条,说明当时窗口是满足的 —— 当前的阻塞点是票数不足,不是窗口。 验证:47 项断言仍全过。另用 PR SiliconMod#67 的真实时间构造用例,确认偏差被正确算出 (+101 分钟)且合并被拒时给出了排查指引。
|
逐条看了三个文件的 diff。先说认同的部分:这套改动的注释把「为什么」写清楚了( 有一条我想请你确认——它可能让 Robin 在 fork PR 上整体失效: 1. fork PR 的
|
|
补一条与「fork PR 只读 token」互斥的取舍——两条处置路径不能各做一半: 如果为了修 fork PR 而打开仓库的 Send write tokens to workflows from pull requests,要注意 所以这两条是互斥的整体决策,不是可以各修一半:
另外 |
回应上游评审(SiliconMod#67)的四点意见,本次只动三处,均在 .github/workflows/robin.yml 内,不引入新依赖、不改审查语义。 1. fork PR 从 pull_request 自动审查路径显式分流 job 级 if 此前只判 draft,对任何非 draft PR 都放行。但 GitHub 对 fork 来源的 pull_request 运行会把 GITHUB_TOKEN 降级为只读,而 robin 发 review 评论需要 pull-requests: write,二者相撞必然 403 → 该 job 在 fork PR 上恒红。 现补 head.repo.full_name == github.repository 判断:同仓 PR 走自动审查, fork PR 改由已存在的 issue_comment /review 路径兜底(该路径在 base 仓上下文 运行、拿到可写 token,且本方案链路不依赖任何 secret)。 2. 改正「本方案对 fork PR 天然可用」这句与实测相反的注释 原注释的推理链是「github.token 不是 secret → fork PR 也拿得到 → 天然可用」。 前半句成立、后半句不成立:拿得到 ≠ 带写权限。已按 R72 的单变量对照实验结论 改写(同一头提交、只换事件类型,pull_request 报授权错、pull_request_target 零授权错),并写明不要试图靠开启「Send write tokens to workflows from pull requests」来解决——该开关只影响同仓 PR,放宽的却是对 pull_request 运行的 整体信任面,公共 fork 无论如何都是只读。 3. zengate 由 ^1 改为精确钉 1.0.4 依据实测:zengate 2026-09-27 01:47 首发,当天 3 小时内连发 5 个版本 (1.0.0→1.0.4),此后停更。范围写法意味着上游哪天发 1.1.0 就会静默换掉 本仓网关行为、review 结论随之漂移且事后难归因。 同时补装后回读校验:装到的版本与钉住的不一致则 ::error:: 退出 1。 判定走显式 [ ... ] + exit,不依赖管道退出码(末端 cut 恒 0,与本仓 「curl -sf | head 恒 0」是同一个坑)。提取逻辑已在 4 种 npm 输出形态下实测 (正常 / 空 / extraneous / invalid)。 旧注释「固定大版本」与实际写法 ^1 矛盾,属误导,本次一并改正。
落实评审意见的提交(本次 head 会移动,请三位重新投一次票)@rt334 @ARCloud217 @by514 评审意见已全部落实。但要先说清一件事:本次提交会移动 head commit,按本仓门禁「PR 更新后原票不算」的规则,你们现有三张赞成票会当场作废,需要重新投一次。 这不是疏忽,是权衡后的选择:修复里第 1 条是真 bug(此前 fork PR 上这个 job 恒红),拖下去等于让所有跨仓贡献者的自动审查一直是坏的;而等票等不到你回复的收益是零。 先说结论:在当前 head( 改了什么(只动
|
补充两点(不影响投票,只影响怎么看 robin 的输出)① 本 PR 改的 robin 配置,在合并前不会生效查 两个文件都从 PR 的 base 分支读,不是 head。所以:
这不影响各位投票与否(票投的是改动本身,不是 robin 这次审得准不准),但看 robin 的评论时需要知道:它这次并不知道我们给它补了哪些规则。 顺带解答一个可能的疑问:工作流里没有 ② 头提交时间的权威口径(澄清一个我之前没说准的地方)「PR 更新后原票作废」这条规则,代码里取的是 update_time = parse_ts(head_commit["commit"]["committer"]["date"])即 head commit 的 committer date,不是 用生产代码跑出来的当前状态:
⇒ 由此可知:在 PR 里发评论不会让票作废,只有推新 head 才会。上一条评论我不会导致各位重投,抱歉之前没讲清。 |
|
/review |
一条影响评审判断的事实:当前 head
|
| 门 | 依据 | 我的情况(author_association=COLLABORATOR) |
|---|---|---|
.github/workflows/robin.yml 的 if: |
author_association ∈ {OWNER, MEMBER, COLLABORATOR} |
放行 |
action 的 min-command-permission: triage |
repos.getCollaboratorPermissionLevel().permission(真实仓库角色) |
拒绝 |
⇒ 我的真实仓库角色低于 triage,所以 /review 恒被忽略;workflow 里 COLLABORATOR 那条放行实际是死代码。
这对各位评审的实际含义:这个 head 的正确性目前只靠 265 条断言的回归套件 + 真实快照实跑(自举前后各跑一遍,失败原因逐字对得上)支撑,没有 AI 第二意见。如果需要,可以由有 push 权限的维护者打 /review 补上。
(这两处都不是本 PR 引入的 —— min-command-permission: triage 在 base 的 robin.yml:89 早已存在,所以我不动它。)
补充:
|
R130 去掉 `tail -1`:npm 的 `ls` 输出在树之后会补一个空行,`tail -1` 取到的是
那个空行而非 `└── zengate@ver`,sed 永不匹配 ⇒ `$got` 恒空 ⇒ 这道版本回读
校验**每次必失败**。改用 `sed -n 's/...p' | head -1`。
复现:`cat -A` 证实树后确有空行;旧写法 got=[] 判失败,新写法 got=1.0.4 判通过。
R135 读嵌套的 opencode-ai:npm 安装全局包时**不提升**依赖,原写法
`npm ls -g opencode-ai` 查的是全局顶层,读到的必然是空(实测输出 `(empty)`),
而 binary.js 实际加载的是 $P/zengate/node_modules/opencode-ai。改为直接读那份
package.json 的 version(实测 0.1.2,与嵌套包一致)。
R132 订正注释里一处夸大的实测表述:原注释称已验证「同仓 PR 走自动路径正常」,
实际那次验证只确认了 fork 判定**没有误伤**同仓 PR(结论是 failure 而非 skipped),
并**未**验证其最终 review 成功。改为按实际验证范围陈述,并补上未验证项。
上一版注释标着「2026-09-27 实测修正」,实测复核后四个数全错,且错误互相加固: 1. 归属错:#16=501252 等五个数逐字吻合的是 fork Xiaobei09/Silicon, 而本仓 SiliconMod/Silicon 的 #16/#17 REST 直接 404。这是本项目 第 4 次栽在 fork/本仓混用上。 2. 推论错:「调到 200000 可覆盖 #17/#10/#15」在它自己列的数上就不成立 (292127/307805/302371 全 >200000)。 3. 占比错:「仅剩 16%–32%」真值 16.0%–27.4%,上界虚高 4.6 个百分点。 4. 倍数错:「字符数是行数的 2–3 倍」实测 45–58 倍,且全仓最大 diff 只有 9083 行,不存在 170k 行的 PR。 改动: - 重写注释:逐条标仓名、给出本仓真实分布(本仓最大 #10=130167, 80000 仍剩 61.5%)、写死度量口径为「API 响应原始字节数」 (经 shell $( ) 量会剥掉尾部换行,每个 PR 少 1–3 字符)。 - 删掉站不住的「行数 vs 字符数 2–3 倍」推断,不留作依据。 - max-diff-size 的**值不动**(仍是 80000):改它是行为变更、不在本次 评审点范围内,注释里给足数据与建议,由维护者定。 - code-reviewer.md:第 9 与第 12 条标题重名(「arc API 特殊性不报误用」), 人工引用无法区分,改第 12 条为「arc 工具类/样式 API 特殊性不报误用」。 新增闸门(根因治理,不只修这一次): - checks/check-robin-yml.py:把注释里每个 #NN=NNNN 从 GitHub API 逐个重新推导;校验截断占比算术、「N 可覆盖 X」类推论、跨仓存在性; 注释块必须逐条标仓名。取不到输入判失败;404 与瞬时故障分开报。 - checks/mutate-robin-yml.py:6 个反向变异(含复活旧错账、404 路径、 引文豁免双向)全部转红。 验证:check-robin-yml 14/14、mutate-robin-yml 6/6、 AGENTS.md 三检查、voting.yml 结构台 16/16 + 变异 5/5、 vote_merge 行为台 265/265。
- fork PR 403 声明:锚定 2026-09-27 双事件对照(run 36357503139 vs 36337473071,头提交 a56c600,唯一变量是事件类型),并注明 robin 自身 pull_request 仅 3 次运行、全为 cancelled/skipped、无 403 日志, 降级行为按 GitHub 文档延伸。 - 「正常路径 2-4 分钟」:注明实测范围是 09 仓测试 PR #28 的 CI (旧链路设 OPENCODE_PATH),run 36319218561≈114s、run 36319493968≈233s, 本工作流在生产尚无完整 run,仅供预算留量参考。纯注释改动,零行为变化。
testbed R182/R183 实证:<<'PY' 单引号 heredoc 不展开 $GITHUB_ENV(写字面文件名 静默建空文件);且 GITHUB_ENV 写入只对后续步骤可见,同步骤 echo 在 set -u 下 unbound variable 杀步。两处一并修复(=试验床已验证内容 a3b9014)。
R254 边界评审 P1/P3 落地(2026-09-29):
- P1: 解析步只从 workflow_run 上下文推导 PR 号/head SHA/头仓;artifact 仅作 diff+指纹;
meta.run_id/head_sha/head_repo/number 四元指纹与上下文不符即 fail-closed;
pull_requests 为空时按 head=owner:branch 反查 open PR 且须精确命中 head_sha+头仓。
resolve 步把 REVIEW_RUN_ID 写入 GITHUB_ENV,驱动侧 --run-id 三重指纹兜底(本提交修复接线缺口)。
- P3: concurrency group=头仓:头分支 + cancel-in-progress(per-PR 并发);
驱动按 github-actions[bot] COMMENTED + commit_id==head_sha 去重,一次 synchronize 只发一条;
sanitize_text 剥控制字符(含 ESC)防注入串污染评论。
- 本地测试 12/12(resolve 9 场景 + 驱动指纹/去重 3 分支)+ sanitize 5/5 + py_compile/YAML lint 全绿。
|
/robin |
🏹 RobinReason: Free model routes drop sometimes — comment |
…d#67) * ci: Robin 改用 zengate 网关访问 big-pickle(删 bootstrap、零 secret) 原方案 pull_request_target → bootstrap 用 GH_PAT 代发 /review → issue_comment 真实审查。它有两个绕不开的问题: 1. 依赖仓库 secret GH_PAT。secret 一旦丢失,bootstrap 走静默 exit 0, PR 打开后不再自动审查,且毫无征兆。 2. pull_request_target 把 PR 代码带进高权限上下文,为让审查跑起来不得不 引入一个持有 repo scope 的个人 PAT。 本改动删除整个 bootstrap,改由 pull_request 事件直连,并在 CI 内用 zengate 起本地网关,由真实的 opencode serve 子进程与 OpenCode Zen 通信: - 全仓 .github/ 已无任何 secrets.* 引用,github.token 是每次运行自动轮换的 内建凭据、不是 secret,fork PR 同样拿得到。 - Zen 免费档有五道准入,runner 即便满足前四道仍恒 403;zengate 拉起真实 opencode 进程与 Zen 通信,绕开的是「来源」那一道,不涉及任何配额或限流。 - 网关自身鉴权用 ALLOW_NO_AUTH 关闭且只监听 127.0.0.1,故全链路无 API key。 两个非显然的关键点: 1. use-json-response-mode: false(必设) review-retry.ts 的 shouldRetryStructuredReview 写着 `if (usedJson) return false`,即 JSON 模式下空审查一次都不会重试;而 action.yml 里该项默认 true。模型偶发返回字面量 {} 时会被当成「0 条发现」直接放行——对审查闸门 是最坏的假通过。关掉后重试保护恢复,实测还更快、发现更多。 2. 冒烟判定必须先落盘再判状态码 `curl -sf | head -c N` 在 bash -e 下退出码取自管道末端的 head(恒 0), 网关返 4xx/5xx 时这道闸门依然报通过。改为显式取 %{http_code} 再断言。 另加一处防御:Zen 的免费模型目录会随时间漂移(实测两天内换过一批),就绪 后显式校验目标模型仍在售,不在售则退出 1 并列出当前在售 id,避免下游只看到 「冒烟没答 OK」这种误导性报错。 同时补充 code-reviewer.md:禁止工具调用的声明、项目渲染/连接铁律、15 条 误报红线,以及版本号检查的降级路径措辞收紧。 * ci: 修正 robin 工作流对 fork PR 的凭据假设,并精确钉住 zengate 版本 回应上游评审(SiliconMod#67)的四点意见,本次只动三处,均在 .github/workflows/robin.yml 内,不引入新依赖、不改审查语义。 1. fork PR 从 pull_request 自动审查路径显式分流 job 级 if 此前只判 draft,对任何非 draft PR 都放行。但 GitHub 对 fork 来源的 pull_request 运行会把 GITHUB_TOKEN 降级为只读,而 robin 发 review 评论需要 pull-requests: write,二者相撞必然 403 → 该 job 在 fork PR 上恒红。 现补 head.repo.full_name == github.repository 判断:同仓 PR 走自动审查, fork PR 改由已存在的 issue_comment /review 路径兜底(该路径在 base 仓上下文 运行、拿到可写 token,且本方案链路不依赖任何 secret)。 2. 改正「本方案对 fork PR 天然可用」这句与实测相反的注释 原注释的推理链是「github.token 不是 secret → fork PR 也拿得到 → 天然可用」。 前半句成立、后半句不成立:拿得到 ≠ 带写权限。已按 R72 的单变量对照实验结论 改写(同一头提交、只换事件类型,pull_request 报授权错、pull_request_target 零授权错),并写明不要试图靠开启「Send write tokens to workflows from pull requests」来解决——该开关只影响同仓 PR,放宽的却是对 pull_request 运行的 整体信任面,公共 fork 无论如何都是只读。 3. zengate 由 ^1 改为精确钉 1.0.4 依据实测:zengate 2026-09-27 01:47 首发,当天 3 小时内连发 5 个版本 (1.0.0→1.0.4),此后停更。范围写法意味着上游哪天发 1.1.0 就会静默换掉 本仓网关行为、review 结论随之漂移且事后难归因。 同时补装后回读校验:装到的版本与钉住的不一致则 ::error:: 退出 1。 判定走显式 [ ... ] + exit,不依赖管道退出码(末端 cut 恒 0,与本仓 「curl -sf | head 恒 0」是同一个坑)。提取逻辑已在 4 种 npm 输出形态下实测 (正常 / 空 / extraneous / invalid)。 旧注释「固定大版本」与实际写法 ^1 矛盾,属误导,本次一并改正。 * fix(robin): 修正 zengate 回读校验的三个实测缺陷 R130 去掉 `tail -1`:npm 的 `ls` 输出在树之后会补一个空行,`tail -1` 取到的是 那个空行而非 `└── zengate@ver`,sed 永不匹配 ⇒ `$got` 恒空 ⇒ 这道版本回读 校验**每次必失败**。改用 `sed -n 's/...p' | head -1`。 复现:`cat -A` 证实树后确有空行;旧写法 got=[] 判失败,新写法 got=1.0.4 判通过。 R135 读嵌套的 opencode-ai:npm 安装全局包时**不提升**依赖,原写法 `npm ls -g opencode-ai` 查的是全局顶层,读到的必然是空(实测输出 `(empty)`), 而 binary.js 实际加载的是 $P/zengate/node_modules/opencode-ai。改为直接读那份 package.json 的 version(实测 0.1.2,与嵌套包一致)。 R132 订正注释里一处夸大的实测表述:原注释称已验证「同仓 PR 走自动路径正常」, 实际那次验证只确认了 fork 判定**没有误伤**同仓 PR(结论是 failure 而非 skipped), 并**未**验证其最终 review 成功。改为按实际验证范围陈述,并补上未验证项。 * fix(robin): 逐条修正 max-diff-size 注释的四处错账,并加可复算闸门 上一版注释标着「2026-09-27 实测修正」,实测复核后四个数全错,且错误互相加固: 1. 归属错:SiliconMod#16=501252 等五个数逐字吻合的是 fork Xiaobei09/Silicon, 而本仓 SiliconMod/Silicon 的 SiliconMod#16/SiliconMod#17 REST 直接 404。这是本项目 第 4 次栽在 fork/本仓混用上。 2. 推论错:「调到 200000 可覆盖 SiliconMod#17/SiliconMod#10/SiliconMod#15」在它自己列的数上就不成立 (292127/307805/302371 全 >200000)。 3. 占比错:「仅剩 16%–32%」真值 16.0%–27.4%,上界虚高 4.6 个百分点。 4. 倍数错:「字符数是行数的 2–3 倍」实测 45–58 倍,且全仓最大 diff 只有 9083 行,不存在 170k 行的 PR。 改动: - 重写注释:逐条标仓名、给出本仓真实分布(本仓最大 SiliconMod#10=130167, 80000 仍剩 61.5%)、写死度量口径为「API 响应原始字节数」 (经 shell $( ) 量会剥掉尾部换行,每个 PR 少 1–3 字符)。 - 删掉站不住的「行数 vs 字符数 2–3 倍」推断,不留作依据。 - max-diff-size 的**值不动**(仍是 80000):改它是行为变更、不在本次 评审点范围内,注释里给足数据与建议,由维护者定。 - code-reviewer.md:第 9 与第 12 条标题重名(「arc API 特殊性不报误用」), 人工引用无法区分,改第 12 条为「arc 工具类/样式 API 特殊性不报误用」。 新增闸门(根因治理,不只修这一次): - checks/check-robin-yml.py:把注释里每个 #NN=NNNN 从 GitHub API 逐个重新推导;校验截断占比算术、「N 可覆盖 X」类推论、跨仓存在性; 注释块必须逐条标仓名。取不到输入判失败;404 与瞬时故障分开报。 - checks/mutate-robin-yml.py:6 个反向变异(含复活旧错账、404 路径、 引文豁免双向)全部转红。 验证:check-robin-yml 14/14、mutate-robin-yml 6/6、 AGENTS.md 三检查、voting.yml 结构台 16/16 + 变异 5/5、 vote_merge 行为台 265/265。 * fix(robin): 两处「实测」注释补齐可复现范围与 run id 锚点 - fork PR 403 声明:锚定 2026-09-27 双事件对照(run 36357503139 vs 36337473071,头提交 a56c600,唯一变量是事件类型),并注明 robin 自身 pull_request 仅 3 次运行、全为 cancelled/skipped、无 403 日志, 降级行为按 GitHub 文档延伸。 - 「正常路径 2-4 分钟」:注明实测范围是 09 仓测试 PR SiliconMod#28 的 CI (旧链路设 OPENCODE_PATH),run 36319218561≈114s、run 36319493968≈233s, 本工作流在生产尚无完整 run,仅供预算留量参考。纯注释改动,零行为变化。 * ci(robin): 跨仓审查改 workflow_run 两段式(collect+review 驱动),零 secret、不代发 /review * fix(robin): review 二段 heredoc 下 $GITHUB_ENV 必须经 os.environ 且本步不 echo 自检 testbed R182/R183 实证:<<'PY' 单引号 heredoc 不展开 $GITHUB_ENV(写字面文件名 静默建空文件);且 GITHUB_ENV 写入只对后续步骤可见,同步骤 echo 在 set -u 下 unbound variable 杀步。两处一并修复(=试验床已验证内容 a3b9014)。 * fix(robin): P1 指纹链完整接线(run_id 从 resolve 步传给驱动)+ P3 并发/去重/输出净化 R254 边界评审 P1/P3 落地(2026-09-29): - P1: 解析步只从 workflow_run 上下文推导 PR 号/head SHA/头仓;artifact 仅作 diff+指纹; meta.run_id/head_sha/head_repo/number 四元指纹与上下文不符即 fail-closed; pull_requests 为空时按 head=owner:branch 反查 open PR 且须精确命中 head_sha+头仓。 resolve 步把 REVIEW_RUN_ID 写入 GITHUB_ENV,驱动侧 --run-id 三重指纹兜底(本提交修复接线缺口)。 - P3: concurrency group=头仓:头分支 + cancel-in-progress(per-PR 并发); 驱动按 github-actions[bot] COMMENTED + commit_id==head_sha 去重,一次 synchronize 只发一条; sanitize_text 剥控制字符(含 ESC)防注入串污染评论。 - 本地测试 12/12(resolve 9 场景 + 驱动指纹/去重 3 分支)+ sanitize 5/5 + py_compile/YAML lint 全绿。 --------- Co-authored-by: Xiaobei09 <Xiaobei09@users.noreply.github.com> Co-authored-by: xiaobei08 <89635391+xiaobei08@users.noreply.github.com> Co-authored-by: opencode <opencode@local>
生产实证(SiliconMod/Silicon#67,run 36585231278):一条 `Reason: ` 后面**什么都没有** 的失败评论。维护者看到它只能推断「Robin 挂了」,而挂掉的原因恰恰是最该被解释的那件事。 三件事在 errorMessage 这一个收敛点修掉: 1. **空 message 必须兜底。** `new Error("")` 的 message 是空串,instanceof Error 分支 会把它原样返回,于是评论里那行等于不存在。改成给明确的无信息量占位 (`HttpError (no message)` 之类),宁可承认没信息,也不要留空。 2. **优先取 response.data.message。** octokit RequestError 的 message 只是壳 (`Request failed due to error response: 403`),真正的原因在 `response.data.message`(例如 `Resource not accessible by integration`,正是 fork PR token 被降级成只读时的那个)。不取的话日志只有状态码,唯一有用的信息被丢掉。 3. **白名单取字段,绝不整体序列化 response。** response.data 在 OAuth 错误分支里会带 client_secret / access_token,而 errorMessage 的输出会进**公开的 PR 评论** —— 那条评论还写着 "No secrets are included in this message"。 顺带:输出单行化(换行会折断 `Reason: ` 那一行的可读性)、400 字符截断(评论不是日志)。 变异:M12 去掉空兜底 → 抓住 2 条;M13 不取 response.data.message → 抓住 1 条; M14 白名单换成 Object.keys 全遍历 → **首轮假通过**。 M14 那次是本轮最值得记的:我把 client_secret 排在 message **后面**,而 Object.keys 按插入序遍历,所以变异会先返回 message 而看起来安全。实测「secret 在前」的组合 真的返回 `cs_SUPERSECRET`。⇒ 把测试数据改成最坏插入序后,M14 被本该抓它的那条 断言抓住。教训:**断言的样本顺序也是判据的一部分**,对抗「按遍历顺序取第一个字段」 这类变异时,必须把危险项排在前面。 回归:tsc 绿,17 套件 265 断言全绿(新增 8 条)。本提交只含源码,dist 下轮。
镜像提交。逐字节来自 upstream 1818227(dist blob d11cea8…,action.yml 未变)。 生产实证 SiliconMod/Silicon#67(run 36585231278)留下一条 `Reason: ` 后面什么都没有 的失败评论。现在 errorMessage 三件事收口:空 message 给明确占位而不留空;优先取 response.data.message(octokit 的壳只有状态码,真正的原因在那里面);白名单取字段 而非整体序列化 response —— 后者会把 OAuth 分支里的凭据写进公开 PR 评论。 Silicon 钉的是 @main 浮动 ref,推送即生效。
R939 在 `errorMessage()` 上加了 8 条断言、跑了 3 个变异,全绿。但 `main.ts` 从头到尾 **没有 import 过 `errorMessage`** —— 它的 catch 一直在用内联三元 `error instanceof Error ? error.message : String(error)`。生产上那条空 `Reason:` 评论(SiliconMod/Silicon#67,run 36585231278,comment 5892637288)正是从这行产生的。 也就是说 R939 的修复**对线上零效果**,而测试套件对此毫无反应。 这是「helper 有单测 ≠ 调用点用上了」第 6 次现身(R926 / R934 / R937 / R939)。 本轮三处改动: 1. `main.ts` 的 catch 改走 `errorMessage(error)`。 只改「给人看的文本」:换 CI 的判定收的是**原始 `error`**(`planRelaunch` 内部自己 分类、不收 `errorText`),所以永久性错误的 fail-closed 完全不受影响。 2. `buildFailedStatusBody` 从 `main.ts` 搬进 `status-comment.ts` 并导出。 原因:它在 `main.ts` 里是模块私有函数,而 `main.ts` 一被 import 就整个 `run()` 起来 —— 这类函数永远拿不到测试,于是只能去测旁边那个「长得像」的 helper。 搬出来之后,生产组合本身可测。 3. 渲染点再做一次空值兜底。上游已经兜了,这里是刻意的冗余:空 Reason 恰恰出现在 最需要解释的那次失败上,而多写一个常量的成本是零。 ## 变异 | 变异 | 结果 | |---|---| | M15 catch 退回内联三元(= R939 修复未生效时的线上状态) | ✅ 2 条 | | M16 去掉渲染点兜底 | ✅ 2 条 | | M17 把人类可读文本重包成 Error 喂进换 CI 判定 |⚠️ 首轮**存活** | | M17 收紧断言后重做 | ✅ 1 条 | M17 首轮存活的原因值得记:断言写成 `expect(callSite).toContain("error,")`, 而 M17 把 `error,` 换成 `error: new Error(message),` 后,切片里**别处**仍有 `error,` 可命中。收紧成匹配 `error:` 的属性值后才被抓住。 更重要的一个教训来自收紧过程中的**基线报红**:我把断言写成 `error:\s*error\s*,`, 而 `main.ts` 用的是 ES shorthand `error,`(根本没有 `error:` 键)—— 结果基线自己 就红,M18 的三条报红里有一条是这个假阳性造成的。**变异验证的前置条件是基线必须先绿**, 否则「抓住变异」这句话本身没有意义。这与 R937 的「M11 编译失败 ≠ 变异存活」同源。 ## 回归 tsc 绿,17 套件 275 断言全绿(新增 10 条,其中 4 条是接线断言)。 接线断言比对前先剥注释 —— 解释这次 bug 的注释本身就写着那个内联三元, 朴素的「源码里不许出现」会因为注释而恒真失败。 本提交只含源码,dist 下轮。
生产取证时撞见的真问题(不是体积,是 marker):SiliconMod/Silicon#67 的失败评论 id=5892637288(2026-09-29)实测 has_marker=false。 后果不是「少个看不见的东西」,而是**整条收养链断掉**:findLatestStatusComment 只认 带 marker 的评论,一旦某条状态评论没戴 marker,下一轮就看不见它、于是新建一条而 不是更新它 —— 这正是 status-comment.ts 开头要解决的「评论堆积」,而且退化时完全 无声:评论照发,只是越攒越多。 已核实成因:ac3113c(2026-10-02)才引入 decorateStatusCommentBody 与收养机制, 那条评论产于 09-29,早于该提交 ⇒ 属预期历史行为、不是现存缺陷。 但真正的问题在这里:守卫修好之后,没为它留任何断言。R941 的 4 条接线断言覆盖了 errorMessage 与 buildFailedStatusBody,唯独没覆盖 marker 装饰。将来只要新增一条 状态评论写入路径而忘了装饰,收养链静默断掉、评论开始堆积,而测试全绿 —— 这正是 R941 那一族缺陷的成因模式本身,被复现在了守卫上。 判据是「body: 的**值**就是装饰调用」,不是「函数体里出现过这个标识符」: - 断言按 async function 起止做函数切片;搜全文会被 main.ts 里那行 import 恒真命中。 - 只搜标识符会被 M29 存活:把装饰改写成 `decorateStatusCommentBody(body);` 之后 仍然 `body,` 原样发出 —— 标识符在,行为已坏。这与 R937 的 M17、R943 的脆弱断言 同族:搜标识符 ≠ 钉住接线。 变异:M27 update 路径不装饰抓 1;M28 create 路径不装饰抓 1;M29 死调用(收紧前存活, 收紧后抓 1);M30 create 路径死调用抓 1;M31 装饰挪到函数末尾结果被丢弃抓 1。
概览
把自动审查改为 workflow_run 两段式(2026-09-28 定案),删 bootstrap、零 secret、不代发
/review:robin-collect.ymlpull_request(opened/synchronize/reopened/ready_for_review)upload-artifactrobin-diffrobin-review.ymlworkflow_run(collect success 后)pull-requests: write真实生效)→ 下载 artifact → 自研驱动review_from_diff.py审查 →gh api pulls/{n}/reviews发 COMMENT满足四项设计约束:自动触发(workflow_run 跟跑,无
/review代发)、能发评论(可写 token + review API)、不依赖 GH_PAT(全仓.github/无任何secrets.*引用)、只读采集(第①段仅读权限)。为什么是自研驱动而不是 robin action
R174 实测
antongulin/robin@v2.7.2(及 main 2.8.0)在workflow_run事件下落入No matching trigger found. Skipping.零产出(action.yml 无 PR/diff/artifact 输入,main.ts 只认 pull_request/issue_comment)。故第②段复用本仓既有的 zengate 网关链路(setup-node/钉版 zengate 1.0.4/起网关/冒烟+模型在售校验)+.github/code-reviewer.md规则 +.github/robin.yml阈值(skip-paths / max-diff-size 80000 / max-comments 12,单一来源),由review_from_diff.py完成:skip-paths 过滤 → head-only 截断 → 结构化审查 → 空审查重试 → 行内评论(422 容错)+COMMENT review。已实测(2026-09-28)
36452000722=success,日志指纹Posted review 5341739030 on PR #37。Robin Collectrun36452503812=success,robin-diffartifact(22201B)生产落库。第②段跨仓验证随本 PR 合并后自然成立(workflow_run 读默认分支文件)。<<'PY'单引号 heredoc 不展开$GITHUB_ENV(须os.environ读取);GITHUB_ENV 写入只对后续步骤可见(同步骤 echo 自检必 unbound)。均已在试验床 run 实测。边界修补(R254 十点评审 P1/P3,2026-09-29 落地 · head
9cdc4e4)补上 09-28 版的三处边界;其余评审点无代码改动要求(#68 票门为独立工单,见其 body):
artifact 取 PR 号。候选 PR =
workflow_run.pull_requests[0];该数组为空(fork PR 已知行为)时按
head={owner}:{branch}反查 open PR,且必须精确命中head_sha与头仓——命中 0 个或多个一律 fail-closed。候选 PR 再经 head_sha / 头仓 /
state=open三重核对;artifact 只当 diff 内容,其
meta.json四元指纹(run_id / head_sha /head_repo_full_name / number)必须与上下文完全一致,任一不符即不发审查、不猜。
所有 API 调用(含 pulls/{n}/reviews)只接受上下文推导出的 PR 号。
concurrency按头仓 + 头分支键控 + cancel-in-progress(同 PR 的旧审查 run 被新 run 取代,不串行排队);驱动脚本按「
github-actions[bot]已发 COMMENTED 且commit_id == head_sha」跳过重复,兜住取消窗口外的重触发。
\x1b),防 diff或模型输出里的注入串污染评论。
验证(本地 2026-09-29):resolve 步 9 场景(hint 正常 / head 过期 / closed / 反查正常 /
反查 0 个 / 反查多个 / meta run_id / number / head_sha 指纹不符→全 fail-closed)+ 驱动
3 分支(run_id 不符→exit1、head_sha 不符→exit1、已审同 head→跳过)共 12/12 通过;
sanitize 5/5;
py_compile+ 三个 workflow YAML lint 全绿;远端字节校验9cdc4e4(fetch + cmp 三文件逐字节一致)。审查段跨仓 E2E 仍需合并后自然成立(workflow_run 读默认
分支文件),归入下方「待验证」。
票况(head
9cdc4e4,basebd44662b)请分清两套计票:
democracy(action,required context)vote_merge.py(本仓严格门,合入时执行)democracy变绿只解除 GitHub 合并按钮,请维护者不要仅凭它点合并——判定够不够票看vote_merge.py输出(正票口径:rt334、ARCloud217、by514 需在最新 head 后重新 APPROVED)。另:保留
workflows/robin.yml保留为手动/review(issue_comment)路径与同仓自动路径;fork PR 的pull_request分支按同仓守卫跳过。use-json-response-mode语义)已在对应 workflow/驱动中落实。