cleanup: skip redundant Issue lifecycle ready runs

This commit is contained in:
Tianyi Cui
2026-08-08 01:50:48 +08:00
parent 8158a0e63a
commit eba4df1e86
5 changed files with 96 additions and 1 deletions
@@ -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-08-08-review-driven-issue-lifecycle-triggers.md
2026-08-08-review-driven-issue-lifecycle-triggers.md: 8a2d48ee23da4c20bb832ae0109e2ea9912dac83
2026-08-08-review-driven-issue-lifecycle-triggers.zh.md: 004739ff471815b0fe12e111eba0ec7aaaef9507
@@ -0,0 +1,31 @@
# Agent Note: Review-driven Issue lifecycle triggers
Status: implemented
English | [中文](2026-08-08-review-driven-issue-lifecycle-triggers.zh.md)
## Problem
The Issue lifecycle workflow reads the current pull request after each subscribed repository event and projects resolving Issues forward to `In progress` or `In review`. A resolving draft already reaches `In progress` from its `opened` event. Changing that draft to ready creates no new lifecycle outcome until a reviewer is requested or submits a review, yet subscribing to `ready_for_review` launches another hosted job and creates another GitHub App token.
Draft-to-ready automation commonly submits a review moments later. In that sequence the ready job cannot advance the Issue, while the review job is still required to observe the `In review` phase.
## Decision
[Issue lifecycle](../../../../.github/workflows/issue-lifecycle.yml) does not subscribe to `pull_request.ready_for_review`. It retains `pull_request.review_requested` and `pull_request_review.submitted`, so either a requested reviewer or a submitted review can advance a resolving Issue to `In review`. The handler continues to fetch the live pull request instead of deriving phase from the triggering payload.
[Issue policy](../../../../.github/workflows/issue-policy.yml) still subscribes to `ready_for_review`. That workflow owns the required check when a human pull request enters review; removing a lifecycle trigger does not weaken policy enforcement.
The workflow test parses both files and pins this split. The lifecycle policy tests separately pin that draft and open resolving pull requests reach `In progress`, while a review request or submitted review reaches `In review`.
## Alternatives considered
- **Keep both events and cancel an in-progress run** - rejected because concurrency can discard a pending run but cannot combine two webhook payloads into one execution. Cancelling the earlier mutation also makes correctness depend on arrival order, while a completed ready job still consumes the full runner setup.
- **Remove the submitted-review event** - rejected because a review may arrive without an explicit review request. In that path `pull_request_review.submitted` is the only repository event that exposes the transition to `In review`.
- **Delay every pull request event behind a debounce dispatcher** - rejected because another queue or scheduled workflow adds latency and control-plane state to eliminate a trigger that carries no lifecycle information.
## Consequences
A draft becoming ready no longer launches Issue lifecycle work. The resolving Issue remains `In progress` from an earlier pull request event until a review is requested or submitted, at which point one review-driven run can advance it to `In review`. The required Issue policy check still runs at the ready boundary.
If a future lifecycle phase depends on ready status itself, that change must restore the trigger and update the workflow test and this decision. Until then, omitting `ready_for_review` saves one hosted run from the common ready-then-review sequence without dropping a status transition.
@@ -0,0 +1,31 @@
# Agent Note: 由评审驱动的 Issue 生命周期触发器
Status: implemented
[English](2026-08-08-review-driven-issue-lifecycle-triggers.md) | 中文
## 问题
Issue 生命周期工作流会在每个已订阅的仓库事件发生后读取当前 PR(Pull Request),并将解决型 Issue 的状态向前推进到 `In progress``In review`。解决型草稿 PR 已通过其 `opened` 事件进入 `In progress`。在请求评审人或评审人提交评审之前,把该草稿转为可评审状态不会产生新的生命周期结果;但订阅 `ready_for_review` 仍会启动另一个托管作业,并创建另一个 GitHub App token。
草稿转为可评审状态的自动化通常会在片刻后提交评审。在这一事件序列中,转为可评审状态的作业无法推进 Issue,而要观察到 `In review` 阶段,仍必须运行评审作业。
## 决策
[Issue 生命周期](../../../../.github/workflows/issue-lifecycle.yml)不订阅 `pull_request.ready_for_review`。它保留 `pull_request.review_requested``pull_request_review.submitted`,因此无论是请求评审人还是提交评审,都可以将解决型 Issue 推进至 `In review`。处理程序仍会获取实时 PR,而不是根据触发事件的载荷推导阶段。
[Issue 政策](../../../../.github/workflows/issue-policy.yml)仍订阅 `ready_for_review`。该工作流负责在由人类发起的 PR 进入评审时执行必需检查;移除生命周期触发器不会削弱政策执行。
工作流测试会解析这两个文件,并固定这种划分。生命周期政策测试另行固定以下行为:草稿及开放状态的解决型 PR 会进入 `In progress`,评审请求或已提交评审则会使其进入 `In review`
## 考虑过的替代方案
- **保留两个事件并取消正在进行的工作流运行**:不予采纳,因为并发控制可以丢弃待处理的工作流运行,却无法把两个 webhook 载荷合并为一次执行。取消较早的状态变更操作也会使正确性依赖事件到达顺序;而已经完成的转为可评审状态作业仍会产生完整的运行器初始化开销。
- **移除已提交评审事件**:不予采纳,因为评审可能在没有明确评审请求的情况下直接提交。在这条路径中,`pull_request_review.submitted` 是唯一能让系统观察到进入 `In review` 这一状态转换的仓库事件。
- **让每个 PR 事件都先经过防抖分派器再处理**:不予采纳,因为新增一条队列或一个定时工作流会引入延迟和控制平面状态,只为消除一个不携带生命周期信息的触发器。
## 后果
草稿转为可评审状态后,不再启动 Issue 生命周期工作。解决型 Issue 会保持在更早的 PR 事件所设定的 `In progress`,直到请求或提交评审;届时,一次由评审驱动的工作流运行即可将其推进至 `In review`。必需的 Issue 政策检查仍会在转为可评审状态的边界运行。
如果未来某个生命周期阶段依赖可评审状态本身,相关变更必须恢复该触发器,并更新工作流测试和本决策。在此之前,省略 `ready_for_review` 可使常见的先转为可评审状态、再提交评审这一序列少启动一次托管工作流运行,而不会遗漏状态转换。
-1
View File
@@ -21,7 +21,6 @@ on:
- reopened
- labeled
- unlabeled
- ready_for_review
- review_requested
pull_request_review:
types:
+28
View File
@@ -28,6 +28,34 @@ describe('CI workflow', () => {
})
})
describe('Issue lifecycle workflow', () => {
it('uses review signals instead of rerunning when a draft becomes ready', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
const policyPullRequest = workflowEvent(policy, 'pull_request')
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
expect(lifecyclePullRequest.types).toContain('review_requested')
expect(lifecycleReview.types).toContain('submitted')
expect(policyPullRequest.types).toContain('ready_for_review')
})
})
function loadWorkflow(path: string): Record<string, unknown> {
const workflow: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
if (!isRecord(workflow)) throw new TypeError(`${path} must define a workflow`)
return workflow
}
function workflowEvent(workflow: Record<string, unknown>, event: string): Record<string, unknown> {
if (!isRecord(workflow.on) || !isRecord(workflow.on[event])) {
throw new TypeError(`workflow must define the ${event} event`)
}
return workflow.on[event]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}