patchrelay 0.81.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,309 +0,0 @@
1
- import { summarizeGitHubFailureContext } from "./github-failure-context.js";
2
- import { buildQueueRepairContextFromEvent } from "./merge-queue-incident.js";
3
- import { isIssueTerminal } from "./pr-state.js";
4
- import { buildGitHubQueueFailureContext, getRelevantGitHubCiSnapshot, resolveGitHubBranchFailureContext, resolveGitHubCheckClass, } from "./github-webhook-failure-context.js";
5
- import { isQueueEvictionFailure, isSettledBranchFailure } from "./github-webhook-policy.js";
6
- import { buildRequestedChangesWakeIdentity } from "./reactive-wake-keys.js";
7
- const WRITER = "github-webhook-reactive-run";
8
- export async function maybeEnqueueGitHubReactiveRun(params) {
9
- const { issue, event, project, logger, feed, wakeDispatcher, db, fetchImpl, failureContextResolver } = params;
10
- if (isIssueTerminal(issue))
11
- return;
12
- if (!issue.delegatedToPatchRelay) {
13
- feed?.publish({
14
- level: "info",
15
- kind: "github",
16
- issueKey: issue.issueKey,
17
- projectId: issue.projectId,
18
- stage: issue.factoryState,
19
- status: "ignored_undelegated",
20
- summary: `Ignored ${event.triggerEvent} because the issue is undelegated`,
21
- });
22
- return;
23
- }
24
- if (event.triggerEvent === "check_failed" && issue.prState === "open") {
25
- if (issue.activeRunId !== undefined) {
26
- return;
27
- }
28
- await handleCheckFailedEvent({
29
- db,
30
- logger,
31
- feed,
32
- wakeDispatcher,
33
- issue,
34
- event,
35
- project,
36
- failureContextResolver,
37
- });
38
- return;
39
- }
40
- if (event.triggerEvent === "review_changes_requested") {
41
- await handleRequestedChangesEvent({
42
- db,
43
- logger,
44
- feed,
45
- wakeDispatcher,
46
- issue,
47
- event,
48
- fetchImpl,
49
- });
50
- }
51
- }
52
- async function handleCheckFailedEvent(params) {
53
- const { db, logger, feed, wakeDispatcher, issue, event, project, failureContextResolver } = params;
54
- // Plan §4.3: while In Deploy (`awaiting_queue`), branch CI is metadata
55
- // only — the lander owns admission, and its spec CI on the integration
56
- // tree is the gate. Queue eviction failures still flow through (they're
57
- // how the lander signals a real integration regression).
58
- if (issue.factoryState === "awaiting_queue" && !isQueueEvictionFailure(issue, event, project)) {
59
- feed?.publish({
60
- level: "info",
61
- kind: "github",
62
- issueKey: issue.issueKey,
63
- projectId: issue.projectId,
64
- stage: issue.factoryState,
65
- status: "branch_ci_metadata_in_deploy",
66
- summary: `Ignored ${event.checkName ?? "branch CI"} failure while In Deploy; lander owns admission`,
67
- });
68
- return;
69
- }
70
- if (isQueueEvictionFailure(issue, event, project)) {
71
- const queueRepairContext = buildQueueRepairContextFromEvent(event);
72
- const failureContext = buildGitHubQueueFailureContext(event, project, queueRepairContext);
73
- if (hasDuplicatePendingReactiveRun(db, feed, issue, "queue_repair", failureContext)) {
74
- return;
75
- }
76
- const queuedRunType = wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
77
- eventType: "merge_steward_incident",
78
- eventJson: JSON.stringify({
79
- ...queueRepairContext,
80
- ...failureContext,
81
- }),
82
- ...(failureContext.failureSignature ? { dedupeKey: failureContext.failureSignature } : {}),
83
- });
84
- logger.info({ issueKey: issue.issueKey, checkName: event.checkName }, "Queue eviction detected, enqueued queue repair");
85
- feed?.publish({
86
- level: "warn",
87
- kind: "github",
88
- issueKey: issue.issueKey,
89
- projectId: issue.projectId,
90
- stage: "repairing_queue",
91
- status: "queue_repair_queued",
92
- summary: `${queuedRunType ?? "queue_repair"} queued after external failure from ${event.checkName}`,
93
- detail: queueRepairContext.incidentSummary ?? queueRepairContext.incidentUrl ?? event.checkUrl,
94
- });
95
- return;
96
- }
97
- if (!isSettledBranchFailure(db, issue, event, project)) {
98
- feed?.publish({
99
- level: "info",
100
- kind: "github",
101
- issueKey: issue.issueKey,
102
- projectId: issue.projectId,
103
- stage: issue.factoryState,
104
- status: "ci_waiting_for_settlement",
105
- summary: `Waiting for settled ${project?.gateChecks?.[0] ?? "verify"} result before starting CI repair`,
106
- });
107
- return;
108
- }
109
- const failureContext = await resolveGitHubBranchFailureContext({
110
- db,
111
- issue,
112
- event,
113
- project,
114
- failureContextResolver,
115
- });
116
- if (hasDuplicatePendingReactiveRun(db, feed, issue, "ci_repair", failureContext)) {
117
- return;
118
- }
119
- const snapshot = getRelevantGitHubCiSnapshot(db, issue, event);
120
- db.issueSessions.commitIssueState({
121
- writer: WRITER,
122
- update: {
123
- projectId: issue.projectId,
124
- linearIssueId: issue.linearIssueId,
125
- lastGitHubFailureSource: "branch_ci",
126
- lastGitHubFailureHeadSha: failureContext.failureHeadSha ?? null,
127
- lastGitHubFailureSignature: failureContext.failureSignature ?? null,
128
- lastGitHubFailureCheckName: failureContext.checkName ?? event.checkName ?? null,
129
- lastGitHubFailureCheckUrl: failureContext.checkUrl ?? event.checkUrl ?? null,
130
- lastGitHubFailureContextJson: JSON.stringify(failureContext),
131
- lastGitHubFailureAt: new Date().toISOString(),
132
- lastQueueIncidentJson: null,
133
- },
134
- });
135
- const queuedRunType = wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
136
- eventType: "settled_red_ci",
137
- eventJson: JSON.stringify({
138
- ...failureContext,
139
- checkClass: resolveGitHubCheckClass(failureContext.checkName ?? event.checkName, project),
140
- ...(snapshot ? { ciSnapshot: snapshot } : {}),
141
- }),
142
- ...(failureContext.failureSignature ? { dedupeKey: failureContext.failureSignature } : {}),
143
- });
144
- logger.info({ issueKey: issue.issueKey, checkName: failureContext.checkName ?? event.checkName }, "Enqueued CI repair run");
145
- feed?.publish({
146
- level: "warn",
147
- kind: "github",
148
- issueKey: issue.issueKey,
149
- projectId: issue.projectId,
150
- stage: "repairing_ci",
151
- status: "ci_repair_queued",
152
- summary: `${queuedRunType ?? "ci_repair"} queued for ${failureContext.jobName ?? failureContext.checkName ?? "failed check"}`,
153
- detail: summarizeGitHubFailureContext(failureContext),
154
- });
155
- }
156
- async function handleRequestedChangesEvent(params) {
157
- const { logger, feed, wakeDispatcher, issue, event, fetchImpl } = params;
158
- const reviewComments = await fetchReviewCommentsForEvent(event, fetchImpl).catch((error) => {
159
- logger.warn({
160
- issueKey: issue.issueKey,
161
- prNumber: event.prNumber,
162
- reviewId: event.reviewId,
163
- error: error instanceof Error ? error.message : String(error),
164
- }, "Failed to fetch inline review comments for requested-changes event");
165
- return undefined;
166
- });
167
- const identity = buildRequestedChangesWakeIdentity({
168
- linearIssueId: issue.linearIssueId,
169
- headSha: issue.prHeadSha ?? event.headSha,
170
- reviewCommitId: event.reviewCommitId,
171
- reviewId: event.reviewId,
172
- reviewerName: event.reviewerName,
173
- });
174
- const queuedRunType = wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
175
- eventType: "review_changes_requested",
176
- eventJson: JSON.stringify({
177
- requestedChangesCoalesceKey: identity.coalesceKey,
178
- ...(identity.headSha ? { requestedChangesHeadSha: identity.headSha } : {}),
179
- reviewBody: event.reviewBody,
180
- reviewCommitId: event.reviewCommitId,
181
- reviewId: event.reviewId,
182
- reviewUrl: buildGitHubReviewUrl(event.repoFullName, event.prNumber, event.reviewId),
183
- reviewerName: event.reviewerName,
184
- ...(reviewComments && reviewComments.length > 0 ? { reviewComments } : {}),
185
- }),
186
- dedupeKey: identity.dedupeKey,
187
- });
188
- logger.info({
189
- issueKey: issue.issueKey,
190
- reviewerName: event.reviewerName,
191
- deferredUntilRunRelease: issue.activeRunId !== undefined,
192
- }, "Captured requested-changes follow-up");
193
- feed?.publish({
194
- level: "warn",
195
- kind: "github",
196
- issueKey: issue.issueKey,
197
- projectId: issue.projectId,
198
- stage: "changes_requested",
199
- status: "review_fix_queued",
200
- summary: issue.activeRunId === undefined
201
- ? `${queuedRunType ?? "review_fix"} queued after requested changes`
202
- : `${queuedRunType ?? "review_fix"} recorded and will resume after the active run finishes`,
203
- detail: reviewComments && reviewComments.length > 0
204
- ? `${reviewComments.length} inline review comment${reviewComments.length === 1 ? "" : "s"} captured`
205
- : event.reviewBody?.slice(0, 200) ?? event.reviewerName,
206
- });
207
- }
208
- function hasDuplicatePendingReactiveRun(db, feed, issue, runType, failureContext) {
209
- const signature = typeof failureContext.failureSignature === "string" ? failureContext.failureSignature : undefined;
210
- const headSha = typeof failureContext.failureHeadSha === "string"
211
- ? failureContext.failureHeadSha
212
- : typeof failureContext.headSha === "string" ? failureContext.headSha : undefined;
213
- if (!signature)
214
- return false;
215
- const pendingWake = db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
216
- if (pendingWake?.runType === runType && pendingWake.eventIds.length > 0) {
217
- const existing = pendingWake.context;
218
- if (existing?.failureSignature === signature
219
- && (headSha === undefined || existing.failureHeadSha === headSha || existing.headSha === headSha)) {
220
- feed?.publish({
221
- level: "info",
222
- kind: "github",
223
- issueKey: issue.issueKey,
224
- projectId: issue.projectId,
225
- stage: issue.factoryState,
226
- status: "repair_deduped",
227
- summary: `Skipped duplicate ${runType} for ${signature}`,
228
- });
229
- return true;
230
- }
231
- }
232
- if (issue.lastAttemptedFailureSignature === signature
233
- && (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha)) {
234
- feed?.publish({
235
- level: "info",
236
- kind: "github",
237
- issueKey: issue.issueKey,
238
- projectId: issue.projectId,
239
- stage: issue.factoryState,
240
- status: "repair_deduped",
241
- summary: `Already attempted ${runType} for this failing PR head`,
242
- });
243
- return true;
244
- }
245
- return false;
246
- }
247
- async function fetchReviewCommentsForEvent(event, fetchImpl) {
248
- if (event.triggerEvent !== "review_changes_requested") {
249
- return undefined;
250
- }
251
- if (!event.repoFullName || event.prNumber === undefined || event.reviewId === undefined) {
252
- return undefined;
253
- }
254
- const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
255
- if (!token) {
256
- return undefined;
257
- }
258
- const [owner, repo] = event.repoFullName.split("/", 2);
259
- if (!owner || !repo) {
260
- return undefined;
261
- }
262
- const response = await fetchImpl(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${event.prNumber}/reviews/${event.reviewId}/comments?per_page=100`, {
263
- headers: {
264
- Authorization: `Bearer ${token}`,
265
- Accept: "application/vnd.github+json",
266
- "User-Agent": "patchrelay",
267
- "X-GitHub-Api-Version": "2022-11-28",
268
- },
269
- });
270
- if (!response.ok) {
271
- throw new Error(`GitHub review comment fetch failed (${response.status})`);
272
- }
273
- const payload = await response.json();
274
- if (!Array.isArray(payload)) {
275
- return undefined;
276
- }
277
- const comments = [];
278
- for (const entry of payload) {
279
- if (!entry || typeof entry !== "object")
280
- continue;
281
- const record = entry;
282
- const body = typeof record.body === "string" ? record.body.trim() : "";
283
- const id = typeof record.id === "number" ? record.id : undefined;
284
- if (!body || id === undefined)
285
- continue;
286
- comments.push({
287
- id,
288
- body,
289
- ...(typeof record.path === "string" ? { path: record.path } : {}),
290
- ...(typeof record.line === "number" ? { line: record.line } : {}),
291
- ...(typeof record.side === "string" ? { side: record.side } : {}),
292
- ...(typeof record.start_line === "number" ? { startLine: record.start_line } : {}),
293
- ...(typeof record.start_side === "string" ? { startSide: record.start_side } : {}),
294
- ...(typeof record.commit_id === "string" ? { commitId: record.commit_id } : {}),
295
- ...(typeof record.html_url === "string" ? { url: record.html_url } : {}),
296
- ...(typeof record.diff_hunk === "string" ? { diffHunk: record.diff_hunk } : {}),
297
- ...(typeof record.user?.login === "string"
298
- ? { authorLogin: String(record.user.login) }
299
- : {}),
300
- });
301
- }
302
- return comments;
303
- }
304
- function buildGitHubReviewUrl(repoFullName, prNumber, reviewId) {
305
- if (!repoFullName || prNumber === undefined || reviewId === undefined) {
306
- return undefined;
307
- }
308
- return `https://github.com/${repoFullName}/pull/${prNumber}#pullrequestreview-${reviewId}`;
309
- }