-
Notifications
You must be signed in to change notification settings - Fork 5
163 lines (150 loc) · 8.39 KB
/
Copy pathmerge-queue-triage.yml
File metadata and controls
163 lines (150 loc) · 8.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
name: Merge Queue Triage
# Why this exists (#4859): red queue builds were being blind-requeued. On
# 2026-08-03 one PR failed the queue four times and landed unchanged on the
# fifth attempt (09:19 → 10:22), and every failure evicted and rebuilt every
# entry queued behind it — the queue's perceived slowness that morning was
# mostly this amplification, not build duration.
#
# A queue failure is a different animal from a PR failure: the PR's own CI ran
# affected-only, while the queue runs the FULL suite on the speculative merge
# result. The failing test is therefore often in a package the PR never
# touched — a flake, or a semantic conflict with another queued PR — and
# neither of those is fixed by re-queueing; re-queueing just burns another
# ~10-minute build for every entry behind it.
#
# So: every red merge_group CI run gets a triage comment on its PR — the
# failed jobs/steps, the failing test lines pulled from the logs (best
# effort), how many times THIS PR has already failed in the queue in the last
# 24 h, and the queue-wide failure count. The checklist tells the author
# (human or agent) to diagnose before re-queueing. The comment is the
# machine-readable signal the PM dispatch loop can key on, and the cross-PR
# flake evidence lives in these comments: the same test name appearing in two
# unrelated PRs' triage comments is a confirmed flake.
#
# Fires on conclusion == failure ONLY. 'cancelled' is the queue evicting an
# entry because something AHEAD of it failed (or a manual cancel) — it says
# nothing about this PR, so it gets no comment (the same run-lifecycle
# reasoning as dogfood-gate's cancelled handling in ci.yml, from the other
# side).
#
# workflow_run executes in the DEFAULT branch's context: this file must be on
# main before it fires, it never checks out or runs PR code, and it holds the
# minimum permissions (actions: read for logs, pull-requests: write for the
# comment).
on:
workflow_run:
workflows: [CI]
types: [completed]
permissions: {}
jobs:
triage:
name: Comment queue-failure triage on the PR
if: >-
github.event.workflow_run.event == 'merge_group' &&
github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read
pull-requests: write
steps:
- name: Post the triage comment
uses: actions/github-script@v9
with:
script: |
const run = context.payload.workflow_run;
const { owner, repo } = context.repo;
// Queue branches are named gh-readonly-queue/<base>/pr-<N>-<sha>.
const m = /^gh-readonly-queue\/.+\/pr-(\d+)-[0-9a-f]{40}$/.exec(run.head_branch ?? '');
if (!m) {
core.info(`head_branch '${run.head_branch}' is not a merge-queue branch — nothing to do.`);
return;
}
const prNumber = Number(m[1]);
const marker = `<!-- merge-queue-triage:${run.id} -->`;
// Idempotency: workflow_run deliveries can repeat; one comment per run.
const existing = await github.rest.issues.listComments({
owner, repo, issue_number: prNumber, per_page: 100,
});
if (existing.data.some((c) => (c.body ?? '').includes(marker))) {
core.info('triage comment for this run already exists — skipping.');
return;
}
// Failed jobs and their failed steps.
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner, repo, run_id: run.id, per_page: 100,
});
const failedJobs = jobs.filter((j) => j.conclusion === 'failure');
// Best-effort log harvest: the lines a human would grep for first.
// Aggregate gate jobs (Test Core / Dogfood Regression Gate) fail
// with no information of their own, so prefer real jobs when both
// are present. A 4xx on the logs endpoint degrades to names only.
const FAIL_LINE = /(?:^|[\s|])(?:✗|×|✕|FAIL\s|AssertionError|STALL)/;
const informative = failedJobs.filter((j) => (j.steps ?? []).some(
(s) => s.conclusion === 'failure' && !/^Verify .* results$/.test(s.name)));
const details = [];
for (const job of (informative.length ? informative : failedJobs).slice(0, 4)) {
const steps = (job.steps ?? [])
.filter((s) => s.conclusion === 'failure')
.map((s) => s.name);
let lines = [];
try {
const res = await github.request(
'GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs',
{ owner, repo, job_id: job.id });
const text = typeof res.data === 'string'
? res.data
: Buffer.from(res.data).toString('utf8');
lines = text.split('\n')
.map((l) => l.replace(/^[^ ]*Z /, ''))
.filter((l) => FAIL_LINE.test(l))
.map((l) => l.trim().slice(0, 200))
.slice(0, 12);
} catch (e) {
core.info(`logs unavailable for job ${job.id}: ${e.message}`);
}
details.push({ name: job.name, url: job.html_url, steps, lines });
}
// History: this PR's earlier queue failures + queue-wide count, 24 h.
const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
const recent = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
owner, repo, event: 'merge_group', created: `>=${since}`, per_page: 100,
});
const ciRuns = recent.filter((r) => r.workflow_id === run.workflow_id && r.id !== run.id);
const priorFailuresThisPr = ciRuns.filter((r) =>
r.conclusion === 'failure' && (r.head_branch ?? '').includes(`/pr-${prNumber}-`)).length;
const queueFailures24h = ciRuns.filter((r) => r.conclusion === 'failure').length;
const jobSections = details.map((d) => {
const head = `- **[${d.name}](${d.url})** — 失败步骤: ${d.steps.join('、') || '(无步骤级结论)'}`;
return d.lines.length
? `${head}\n\n \`\`\`\n ${d.lines.join('\n ')}\n \`\`\``
: `${head}(日志不可读,点进 job 看)`;
}).join('\n');
const flakeHint = priorFailuresThisPr > 0
? `⚠️ **本 PR 过去 24h 已在队列失败 ${priorFailuresThisPr} 次(不含本次)。** 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。`
: '本 PR 过去 24h 无队列失败记录(首次)。';
const body = [
'### ⛔ merge queue 构建失败 — 先分诊,再决定要不要重排',
'',
`队列构建 [${run.id}](${run.html_url}) 红了。队列跑的是**全量**套件(PR 侧 CI 只跑 affected 子集),`,
'所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。',
'',
'**失败的 job(日志抽取,best effort):**',
'',
jobSections || '- (没拿到 job 级信息,点上面的 run 链接看)',
'',
'**历史信号:**',
`- ${flakeHint}`,
`- 过去 24h 队列共有 ${queueFailures24h} 个失败构建(不含本次)。`,
'',
'**分诊清单:**',
'1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。',
'2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。',
'3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。',
'',
marker,
'---',
'_Generated by [Claude Code](https://claude.ai/code) · merge-queue-triage workflow (#4859)_',
].join('\n');
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
core.info(`triage comment posted on #${prNumber}.`);