Răsfoiți Sursa

fix(ci): identify approval review runs by workflow path

turtle1999 2 săptămâni în urmă
părinte
comite
e7b8ebedaf

+ 6 - 0
.agents/notes/implemented/process/2026-09-10-approval-review-workflow-identity.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-10-approval-review-workflow-identity.md
+2026-09-10-approval-review-workflow-identity.md: 77164134e826acdf647db352cf73df2480055505
+2026-09-10-approval-review-workflow-identity.zh.md: 736851d6f3612080fd67ed6b72e1b8b667948d73

+ 25 - 0
.agents/notes/implemented/process/2026-09-10-approval-review-workflow-identity.md

@@ -0,0 +1,25 @@
+# Agent Note: Identify approval review workflows by file path
+
+Status: implemented
+
+English | [中文](2026-09-10-approval-review-workflow-identity.zh.md)
+
+## Problem
+
+GitHub can populate a workflow run's `name` with its expanded `run-name`. The approval review workflow includes the pull-request number in that title, so comparing `workflow_run.name` with the static workflow name rejects valid review events before refreshing the approval status.
+
+## Decision
+
+The [approval publisher](../../../../.github/review-ownership/check-approval.mjs) identifies the review-event workflow by its exact `workflow_run.path`. It also requires a successful `pull_request_review` run, parses the pull-request number from `display_title`, validates any supplied pull-request association, and compares the current pull-request head with the reviewed head before evaluating approvals.
+
+## Alternatives considered
+
+**Accept a name prefix.** A display name does not identify the workflow file; another workflow can use the same title.
+
+**Remove the numbered run title.** The title supplies the pull-request number when GitHub returns an empty `pull_requests` array. Removing it requires a different handoff mechanism.
+
+## Consequences
+
+Run-title expansion does not prevent approval refreshes, while an unexpected workflow file still fails validation. Moving the review-event workflow requires updating the publisher's expected path.
+
+[Approval policy tests](../../../../.github/review-ownership/check-approval.test.mjs) cover a numbered run name, invalid source paths and events, unsuccessful runs, invalid titles, and superseded heads. The [approval outcome policy](2026-09-09-blocked-weighted-approvals-remain-pending.md) continues to own pending and successful status semantics.

+ 25 - 0
.agents/notes/implemented/process/2026-09-10-approval-review-workflow-identity.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: 按文件路径识别审批评审工作流
+
+Status: implemented
+
+[English](2026-09-10-approval-review-workflow-identity.md) | 中文
+
+## 问题
+
+GitHub 可能用展开后的 `run-name` 填充工作流运行的 `name`。审批评审工作流在该标题中包含拉取请求编号,因此将 `workflow_run.name` 与静态工作流名称比较,会在刷新审批状态前拒绝有效的评审事件。
+
+## 决策
+
+[审批发布器](../../../../.github/review-ownership/check-approval.mjs) 按精确的 `workflow_run.path` 识别评审事件工作流。它还要求该运行由 `pull_request_review` 触发且成功完成,从 `display_title` 解析拉取请求编号,验证提供的拉取请求关联,并在计算审批结果前将拉取请求当前的头提交与已评审的头提交进行比较。
+
+## 考虑过的替代方案
+
+**接受名称前缀。** 显示名称无法识别工作流文件;另一个工作流可以使用相同的标题。
+
+**删除带编号的运行标题。** 当 GitHub 返回空的 `pull_requests` 数组时,标题提供拉取请求编号。删除它需要另一种传递机制。
+
+## 影响
+
+运行标题的展开不会阻止审批刷新,而非预期的工作流文件仍无法通过验证。移动评审事件工作流时,需要更新发布器预期的路径。
+
+[审批策略测试](../../../../.github/review-ownership/check-approval.test.mjs) 覆盖带编号的运行名称、无效的来源路径和事件、未成功的运行、无效标题以及已被替代的头提交。[审批结果策略](2026-09-09-blocked-weighted-approvals-remain-pending.zh.md) 继续负责待定与成功状态的语义。

+ 1 - 1
.github/review-ownership/README.md

@@ -27,7 +27,7 @@ The publisher runs when a pull request opens, synchronizes, reopens, becomes rea
 
 ## Security
 
-The status-writing job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews are treated as API data and escaped in logs.
+The status-writing job checks out only the repository default branch. It does not check out or execute pull-request code and does not use repository secrets. The review-event workflow has no `GITHUB_TOKEN` permissions and passes only a decimal pull-request number in its run title. The publisher accepts only successful `pull_request_review` runs from the review-event workflow file, identified by `workflow_run.path`; GitHub can populate `workflow_run.name` with the expanded run title. The publisher rejects an invalid run title and a number that does not resolve to the workflow run's current pull-request head. Pull-request reviews are treated as API data and escaped in logs.
 
 Approval policy changes take effect only after they merge into the default branch. This prevents an untrusted pull request from changing the program or policy for its own run.
 

+ 4 - 2
.github/review-ownership/check-approval.mjs

@@ -208,13 +208,15 @@ export async function runApprovalCheck({ event, policySource, api, runUrl, write
 }
 
 /**
- * Resolve the reviewed pull request from a completed review-event workflow run.
+ * Resolve the reviewed pull request from a completed run of the review-event workflow file.
  * @param {{event: unknown, api: (path: string, options?: {method?: string, body?: unknown}) => Promise<unknown>}} options Trusted workflow inputs.
  * @returns {Promise<Record<string, unknown> | null>} Event with a current pull request, or null after the pull-request head changes.
  */
 export async function approvalEventFromWorkflowRun({ event, api }) {
   const repository = repositoryFromEvent(event)
-  if (!isRecord(event.workflow_run) || event.workflow_run.name !== 'weighted-approval-review-event'
+  // GitHub can expand run-name into name; the file path identifies the source workflow.
+  if (!isRecord(event.workflow_run)
+    || event.workflow_run.path !== '.github/workflows/weighted-approval-review-event.yml'
     || event.workflow_run.event !== 'pull_request_review' || event.workflow_run.conclusion !== 'success') {
     throw new Error('event has no successful weighted approval review workflow run')
   }

+ 17 - 1
.github/review-ownership/check-approval.test.mjs

@@ -78,7 +78,8 @@ test('resolves a review workflow run to the current pull request and rejects sta
   const workflowRunEvent = {
     repository: { full_name: 'deepseek-harness/deepseek-harness' },
     workflow_run: {
-      name: 'weighted-approval-review-event',
+      name: 'weighted-approval-review-event:42',
+      path: '.github/workflows/weighted-approval-review-event.yml',
       event: 'pull_request_review',
       conclusion: 'success',
       head_sha: HEAD_SHA,
@@ -95,6 +96,21 @@ test('resolves a review workflow run to the current pull request and rejects sta
   })
   assert.equal(current.pull_request.number, 42)
 
+  for (const invalidRun of [
+    { path: '.github/workflows/other.yml' },
+    { path: undefined },
+    { event: 'push' },
+    { conclusion: 'failure' },
+  ]) {
+    await assert.rejects(approvalEventFromWorkflowRun({
+      event: {
+        ...workflowRunEvent,
+        workflow_run: { ...workflowRunEvent.workflow_run, ...invalidRun },
+      },
+      api: async () => { throw new Error('invalid source must not call GitHub') },
+    }), /successful weighted approval review workflow run/u)
+  }
+
   assert.equal(await approvalEventFromWorkflowRun({
     event: workflowRunEvent,
     api: async () => ({