尧图精选

ClickHouse Praktika CI 引擎:外部 PR 审批门禁机制与“误取消“假阳性问题修复

🕒 发布时间:2026/9/20 17:21:10 📁 来源:尧图网络
ClickHouse Praktika CI 引擎外部 PR 审批门禁机制与误取消假阳性问题修复【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse本文围绕 ClickHouse 自研 CI 引擎 Praktika 的 orchestrator 文档 EXTERNAL_PR_GATE_FLOW.md 展开系统讲解外部forkPR 的审批门禁完整链路为何外部 PR 不立即入队、审批状态如何持久化在 S3、维护者点击 Approve 后已保存的 workflow 载荷如何延迟入队以及新 push 到达时应立刻取消旧 run的规则为何曾导致已批准的当前 head run 被误取消——最终通过时间戳 head SHA双重条件修复。读完后你将理解这条门禁从 Lambda 到 orchestrator 的完整调用链、每个 S3 键的设计意图以及源码中判取消决条件的确切位置。背景Praktika CI 引擎与外部 PR 的特殊性Praktika 是 ClickHouse 仓库中替代 GitHub Actions 调度的独立 CI 引擎见 README 与 协议文档GitHub webhookHMAC 校验后的 PR 事件 ↓ Lambdaci/praktika/infrastructure/native/lambda_gh_trigger.py ↓ 入队 {type, repo, pr_number, head_sha, event_ts, ...} SQS praktika_clickhouse_workflows ↓ Orchestrator ASG → praktika orchestrate workflow event.json --ci ↓ 按 runs_on 分池 SQS praktika-{runner-type} → Runner ASG 执行 job在这条链路里来自内部仓库的 PR 事件可以直接入队。但来自外部 fork 的 PR 不同提交者可以不受控地推送任意代码因此需要一道人工门禁。门禁的设计目标原文档 Goal 一节完整继承外部 PR不立即入队 CIdo not enqueue CI immediately要求维护者对精确的 head SHA进行审批require maintainer approval for the exact head SHA同时保证一旦新 push 到达旧 run 立即被取消still cancel older runs as soon as a new push arrives。前两条是安全诉求第三条是 CI 的通用时效性诉求。本文要解决的核心矛盾正是审批会延迟入队而取消标记是 PR 级可变状态两者叠加会让旧的时间戳比较取消规则产生误判。流程一pull_request.opened/reopened—— 建门禁不入队Lambda 入口为 lambda_handler它对pull_request事件的通用处理是计算event_ts time.time()Lambda 接收时刻见 L1162构建 workflow 载荷_build_workflow携带action、event_ts、head_sha等字段回取线上 PR 做陈旧事件防护若 webhook 中的head_sha与 GitHub 上当前 live head 不一致则直接丢弃延迟重投递的旧事件不得覆盖门禁状态外部 PR 在回取失败时fail closed宁可跳过也不放行对external_pr分支调用 _handle_external_pr先_supersede_previous_gate关闭上一个 head 遗留的等待审批检查避免 PR 上堆积多个失效的 Approve 按钮然后为当前 head_sha创建一个 in_progress 的审批 check runExternal PR Approval带 Approve CI 动作按钮并把审批状态写入 S3状态为awaiting不向 SQS 入队返回enqueued: False。第 4 步写入的审批状态 JSON原文档示例实际由 _store_gate_state 生成S3 键为external-pr-approvals/repo/pr/pr_number.json{ status: awaiting|approved, head_sha: pr head sha, approval_check_id: 123, workflow: { action: opened|synchronize|reopened, event_ts: 1783353375.2960708, head_sha: pr head sha, ...: ... } }要点workflow字段就是日后真正要入队执行的载荷它在事件到达时就被冻结下来审批通过后才被取出入队。源码中该载荷的external_id形如{kind: external_pr_approval, repo: ..., pr_number: 123, head_sha: abc...}见 _approval_external_id。后续check_run.requested_actionwebhook 携带的 check run 会带回这个external_idLambda 据此自识别这是门禁检查无需查 GitHub API。补充一个源码中的增强机制原文档未展开如果该 PR 曾经被某维护者审批过且新 head 相对已审批 head 的改动全部落在白名单路径内由环境变量EXTERNAL_PR_AUTOAPPROVE_PATHS_JSON配置用 GitHub compare API 的filename/previous_filename判定重命名两侧都计入则门禁直接复用原审批check 显示 External PR approval reused 并立即入队若 compare 结果被 GitHub 的 300 文件上限截断则 fail closed 回退到人工审批见 _changes_are_autoapprovable。流程二pull_request.synchronize—— 先写取消标记再走审批逻辑新 commit 推送后synchronizeLambda 的处理顺序原文档第 2 节对应 L1304-L1310计算event_ts time.time()为新 PR head 构建 workflow 载荷立即写入 PR 级取消标记_cancel_runs_before然后对新 head 处理审批逻辑新建门禁 check、关闭旧门禁。取消标记的 S3 键scope由 SQS 队列名决定队列名以-base结尾为base否则为default见 _cancel_before_keypr/pr/cancel-before-scopeBody由 _cancel_runs_before 写入{ ts: 1783353375.3261397, head_sha: new pr head sha }语义原文档 Meaning 一节取消同一 orchestrator scope 内更旧的 run旧 SHA 的在跑构建立刻停掉但不取消针对本head_sha的 run新 SHA 自己不能把自己取消掉。注意这个标记是覆盖写的 PR 级单键同一个 PR 每次synchronize都重写它所以它是可变的、始终只保留最新值的状态——这一点正是后文误取消问题的根源之一。流程三维护者点击 Approve —— 只放行已保存的载荷GitHub 会把点击事件作为check_run.requested_actionidentifier 为approve发给 Lambda。_handle_gate_action 依次执行原文档第 3 节的六步完整继承校验审批 check 的external_id必须解析出kind external_pr_approval的自描述上下文验证操作者权限通过 GitHub API 确认该用户对该仓库至少为write权限_can_maintain_repo等级表none read triage write maintain admin从 S3 加载该 PR 的审批状态校验被点击的 check 与保存的approval_check_id、head_sha仍然一致——若 PR head 已前进则把该 check 标记为 Stale approval requestneutral 完成并跳过绝不把对旧 SHA 的点击当作对当前 head 的审批将门禁 check 标记为successApproved记录审批人把 S3 中保存的workflow载荷入队_approve_saved_workflow先 PATCH check 为 completed/success再把状态存为approved最后_enqueue(workflow)。原文档特别强调Important 一节审批动作不创建新的逻辑 PR 事件——它不重新构建 workflow、不重新打时间戳只是放行之前冻结并保存在 S3 里的那个载荷。这条性质是理解整个 bug 的关键入队发生在审批时刻 T3而载荷里的event_ts停留在 synchronize 时刻 T1。event_ts的职责只回答一个问题event_ts唯一要回答的问题是原文档逐字保留is this run older than the newest PR event in this scope? 这个 run 是否比本 scope 内最新的 PR 事件更旧修复前的旧规则只有一条cancel if cancel_before.ts run.event_ts即PR 级取消标记的ts比 run 自带的event_ts新则该 run 自杀。在事件到达即入队的内部 PR 路径上这完全正确——新 push 的 run 与取消标记写的是同一个event_ts严格大于比较保证新 run 不会取消自己PROTOCOL.md 的 Cancel semantics 也描述了这一约定。Bug延迟入队如何让时间戳比较失效原文档 The bug 一节的五步时序完整继承外部 PR 的synchronize到达Lambda 保存 workflow 载荷其中event_ts T1synchronize 到达时刻同一个 Lambda 调用写入 PR 级取消标记ts T2T2 T1审批发生在更晚的 T3T3 远大于 T2取决于维护者何时点击Lambda 在 T3 才把第 1 步保存的旧载荷入队——此时 run 携带的仍是event_ts T1orchestrator 每轮wait()周期执行sweep_cancel拿当前 PR 级标记与 run 的event_ts比较。问题在于T1 的 run 其实是当前 head 上真正该跑的构建但它的event_ts是过期的。只要标记的tsT2比 T1 大——甚至没有任何新 push 发生——旧规则就会判定存在更新的事件取消自己。若维护者点击审批前又有一次无关的 PR 事件重写了标记例如另一个 scope、或延迟重投递的 webhook误取消几乎必然发生。用一句话概括原文档结论这本质上是一个延迟入队问题——审批把 workflow 状态存下来、稍后才复用approval stores workflow state and reuses it later而cancel-before是 PR 级可变状态随时可能被重写cancel-before is mutable PR-scoped state。两者叠加单靠时间戳就无法区分过时的旧 run和迟到的当前 run。修复时间戳 head SHA 双重条件修复后的取消规则原文档 Fix 一节完整继承cancel if: cancel_before.ts run.event_ts AND cancel_before.head_sha ! run.head_sha保留了原有时间戳行为叠加一个 SHA 守卫取消标记记录的是触发它的新 head SHA如果某个 run 的 head 与标记里的 head 相同说明它就是当前 head 的 run无论时间戳看起来多旧都不取消。在 orchestrator 侧该规则落在 state.py 的WorkflowState.sweep_cancel# 读取 pr/pr/cancel-before-scope 的 {ts, head_sha} cancel_before float(payload.get(ts, 0)) cancel_sha str(payload.get(head_sha) or ).strip() ... current_sha str(self._event.get(head_sha) or ).strip() if cancel_before self._event_ts 0 and ( not cancel_sha or cancel_sha ! current_sha ): print( f[CANCEL] run {self._run_id} (newer event {cancel_before:.0f} fevent_ts {self._event_ts:.0f}) ) self.cancelled True两个通道在同一个 sweep 里处理runs/run_id/cancel-requestUI 手动取消存在即取消不比较时间戳与pr/pr/cancel-before-scope新 push 扇出走上面的双重条件。not cancel_sha分支是兼容旧格式标记只有ts没有head_sha的降级路径——旧标记没有 SHA 信息时退回纯时间戳语义。取消置位后由主循环一次性经cancel_unfinished_jobs收敛重复扫到是 no-op。Lambda 侧的写入端也同步改造_cancel_runs_before现在把head_sha一并写进标记 body其 docstring 直接点明了动机——orchestrator only self-cancels when it sees a newer marker for adifferentSHA; this avoids false cancels when an approved external PR re-enqueues the current head after the marker was already written见 L686-L707。为什么 SHA 检查是额外保险而不是主机制原文档 Why the SHA check is an extra check 一节给出了一个很好的直觉值得完整保留在理想路径上这个检查其实是冗余的——一次synchronizewebhook 只被处理一次同一个event_ts同时写入 workflow 载荷和取消标记刚创建的 run 不可能因为严格小于比较而取消自己。但在真实系统里这条保险是必要的防御逻辑因为审批会延迟入队保存的 workflow 状态可能在很久之后才被入队携带过期的event_ts;PR 级取消状态在此期间可能已被重写后续 push、重投递事件等。所以 SHA 检查的定位是原文档结论不是主机制而是一个防止误取消当前 head的额外安全检查。它把这个 run 到底对应哪个 commit这一身份信息引入了原本只有时间信息的比较中使规则在时间戳失效时仍有第二个独立判据。修复后的最终行为修复落地后三个性质同时成立原文档 Result 一节完整继承旧 SHA 的 run 在新 push 到达时立即被取消标记ts更新且 SHA 不同双重条件满足外部 PR 审批仍然门禁 CI未审批前不入队审批必须绑定精确 head SHA陈旧点击会被判 stale 并作废;已批准的当前 head run 不会被当前 PR 级标记误取消cancel_before.head_sha run.head_sha时跳过取消即使标记ts比 run 的event_ts新。这套延迟放行 身份守卫的设计本质上是把取消谁从纯时间维度升级为时间 身份双维度时间戳回答是否有更新的事件SHA 回答更新的事件是不是针对我。延伸阅读与源码索引主题文件本文对应的设计笔记ci/praktika/orchestrator/EXTERNAL_PR_GATE_FLOW.md门禁/审批/取消标记的 Lambda 实现ci/praktika/infrastructure/native/lambda_gh_trigger.py取消判定的 orchestrator 实现sweep_cancelci/praktika/orchestrator/state.py完整的队列/S3 通道、心跳与取消语义ci/praktika/orchestrator/PROTOCOL.mdPraktika 整体架构与本地调试方式ci/praktika/orchestrator/README.md补充说明适用前提以上机制针对pull_request事件中的external_pr分支fork 来源内部 PR 直接入队、不受门禁约束审批权限以 GitHub collaborator permission 为准需write及以上取消标记的 scope 与 Lambda 绑定的 SQS 队列名对应-base队列用basescope其余用default。若你正在评估在自家仓库落地类似的外部贡献者 CI 门禁本文给出的冻结事件载荷 S3 审批状态 时间戳/SHA 双条件取消是一个可直接参考的实现范本其中每一处设计决策都能在 ClickHouse 仓库的上述文件中找到对应代码。【免费下载链接】ClickHouseClickHouse® is a real-time analytics database management system项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →