-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathackExecutor.ts
More file actions
280 lines (264 loc) Β· 9.18 KB
/
Copy pathackExecutor.ts
File metadata and controls
280 lines (264 loc) Β· 9.18 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import type { Config } from "../../config.js";
import type { Pool } from "pg";
import { logWarn } from "../../evlog.js";
import { REVIEW_SUMMARY_SENTINEL } from "../../review/reviewSchema.js";
import { upsertSummaryCommentWithCreationClaim } from "../../review/publish/summaryCommentUpsert.js";
import {
DEFERRED_HEAD_SHA,
GITHUB_REACTION_EYES,
GITHUB_REACTION_MINUS_ONE,
GITHUB_REACTION_PLUS_ONE,
triageCancelledNotice,
} from "../../settings/index.js";
import { createPrSurface } from "../../github/prSurface.js";
import { mintInstallationToken } from "../durableJob.js";
import {
getProgressCommentOwner,
getReviewQueuePosition,
getWorkItemCore,
type ReviewQueuePosition,
} from "../repository.js";
import {
cancelReviewCheckRunsForWorkItems,
ensureReviewCheckRunStarted,
} from "../reviewCheckRun.js";
import { buildCiSummaryForSurface } from "../../review/ci/analyzeCi.js";
import {
parseProgressRevisionState,
renderReviewCancelledNotice,
renderReviewProgressComment,
} from "../../review/run/progressComment.js";
import { getAppBotIdentity } from "../../github/appAuth.js";
import type { ReviewMode } from "../../review/reviewSchema.js";
import { ACTIVE_WORK_STATUSES, prResourceKey, type AckJobData } from "../types.js";
/** True when this ack may still write the shared progress comment for its work item. */
export async function canAckPublishProgress(
pool: Pool,
params: {
readonly workItemId: string;
readonly resourceKey: string;
readonly reviewLens: ReviewMode;
},
): Promise<boolean> {
const workItem = await getWorkItemCore(pool, params.workItemId);
if (workItem == null || !(ACTIVE_WORK_STATUSES as readonly string[]).includes(workItem.status)) {
return false;
}
const owner = await getProgressCommentOwner(pool, params.resourceKey, params.reviewLens);
if (owner != null && owner.workItemId !== params.workItemId) {
return false;
}
return true;
}
type AckInstallation = Awaited<ReturnType<typeof mintInstallationToken>>;
function ackPrSurface(
cfg: Config,
data: Pick<AckJobData, "installationId" | "owner" | "repo" | "prNumber">,
installation: AckInstallation,
) {
return createPrSurface({
cfg,
installationId: data.installationId,
owner: data.owner,
repo: data.repo,
prNumber: data.prNumber,
installation,
});
}
async function publishAckProgress(
cfg: Config,
pool: Pool,
data: AckJobData & { readonly progress: NonNullable<AckJobData["progress"]> },
installation: AckInstallation,
resourceKey: string,
): Promise<void> {
const prSurface = ackPrSurface(cfg, data, installation);
const deferredHead = data.progress.headSha === DEFERRED_HEAD_SHA;
const headSha = deferredHead ? await prSurface.getHeadSha() : data.progress.headSha;
const ciSummary = await buildCiSummaryForSurface(prSurface, {
headSha,
lightweight: true,
waitMs: 0,
});
let queuePosition: ReviewQueuePosition | null = null;
if (data.workItemId != null) {
try {
queuePosition = await getReviewQueuePosition(pool, data.workItemId);
} catch (e) {
logWarn("ack_queue_position_failed", {
workItemId: data.workItemId,
message: e instanceof Error ? e.message : String(e),
});
}
}
// Queued stub: Head/Source/(Queue)/(CI) only β no Recon/specialist rows until the review worker starts.
const body = renderReviewProgressComment({
mode: data.progress.lens,
headSha,
source: data.progress.source,
ciSummary,
queuePosition,
progressRevision: 0,
progressWorkItemId: data.workItemId,
});
await upsertSummaryCommentWithCreationClaim({
pool,
workItemId: data.workItemId,
resourceKey,
reviewLens: data.progress.lens,
prSurface,
body,
sentinel: REVIEW_SUMMARY_SENTINEL,
progressRevision: 0,
});
// Deferred-head reviews resolve the binding head at claim time; starting the
// check run here would pin it to an earlier SHA if another push lands first.
if (data.workItemId && !deferredHead) {
await ensureReviewCheckRunStarted(pool, {
prSurface,
owner: data.owner,
repo: data.repo,
prNumber: data.prNumber,
headSha,
workItemId: data.workItemId,
resourceKey,
reviewLens: data.progress.lens,
});
}
}
async function publishCancelProgress(
cfg: Config,
pool: Pool,
data: AckJobData & { readonly cancelProgress: NonNullable<AckJobData["cancelProgress"]> },
installation: AckInstallation,
resourceKey: string,
): Promise<void> {
const prSurface = ackPrSurface(cfg, data, installation);
const existing = await prSurface.findProgressComment(REVIEW_SUMMARY_SENTINEL);
const rev = existing?.body != null ? parseProgressRevisionState(existing.body) : null;
const ownsStub =
existing != null &&
(rev?.workItemId == null || rev.workItemId === data.cancelProgress.workItemId);
const body = renderReviewCancelledNotice({
attribution: data.cancelProgress.attribution,
progressRevision: ownsStub ? (rev?.revision ?? 0) : 0,
progressWorkItemId: data.cancelProgress.workItemId,
});
// Comment I/O must not block check cancellation β stale checks stuck in_progress are worse.
try {
if (ownsStub && existing != null) {
await prSurface.editComment(existing.id, body);
} else {
await upsertSummaryCommentWithCreationClaim({
pool,
workItemId: data.cancelProgress.workItemId,
resourceKey,
reviewLens: "review",
prSurface,
body,
sentinel: REVIEW_SUMMARY_SENTINEL,
progressRevision: 0,
});
}
} catch (error) {
logWarn("ack_cancel_comment_failed", {
workItemId: data.cancelProgress.workItemId,
resourceKey,
message: error instanceof Error ? error.message : String(error),
});
}
// Stale pre-deploy ack jobs may omit cancelledWorkItemIds; fall back to the primary id.
const cancelledWorkItemIds = data.cancelProgress.cancelledWorkItemIds ?? [
data.cancelProgress.workItemId,
];
await cancelReviewCheckRunsForWorkItems(pool, {
prSurface,
owner: data.owner,
repo: data.repo,
prNumber: data.prNumber,
workItemIds: cancelledWorkItemIds,
});
}
async function publishTriageCancellation(
prSurface: ReturnType<typeof ackPrSurface>,
data: AckJobData & { readonly cancelTriage: NonNullable<AckJobData["cancelTriage"]> },
): Promise<void> {
await prSurface.setAcknowledgementReaction(data.cancelTriage.targets, GITHUB_REACTION_MINUS_ONE);
await prSurface.replyAt(
data.cancelTriage.replyTarget,
triageCancelledNotice(data.cancelTriage.attribution),
);
}
/** Fire-and-forget ack (reactions, progress stub, slash replies); not a durable work item. */
export async function executeAckJob(cfg: Config, pool: Pool, data: AckJobData): Promise<void> {
try {
const bot = await getAppBotIdentity(cfg);
if (data.commenterId != null && bot.userId === data.commenterId) return;
} catch (e) {
logWarn("ack_bot_identity_check_failed", {
message: e instanceof Error ? e.message : String(e),
});
}
const installation = await mintInstallationToken(cfg, data.installationId);
const prSurface = ackPrSurface(cfg, data, installation);
const resourceKey = prResourceKey(data.owner, data.repo, data.prNumber);
await prSurface.setAcknowledgementReaction(data.targets, GITHUB_REACTION_EYES);
// Cancel before progress: `/review force` acks carry both, and the new run's
// queued stub must be the final state after the cancelled notice lands.
if (data.cancelProgress) {
try {
await publishCancelProgress(
cfg,
pool,
{ ...data, cancelProgress: data.cancelProgress },
installation,
resourceKey,
);
} catch (error) {
logWarn("ack_cancel_progress_failed", {
workItemId: data.cancelProgress.workItemId,
resourceKey,
message: error instanceof Error ? error.message : String(error),
});
}
}
if (data.cancelTriage) {
try {
await publishTriageCancellation(prSurface, { ...data, cancelTriage: data.cancelTriage });
} catch (error) {
logWarn("ack_cancel_triage_failed", {
workItemId: data.cancelTriage.workItemId,
resourceKey,
message: error instanceof Error ? error.message : String(error),
});
}
}
if (data.progress) {
const progressData = { ...data, progress: data.progress };
if (data.workItemId != null) {
const mayPublish = await canAckPublishProgress(pool, {
workItemId: data.workItemId,
resourceKey,
reviewLens: progressData.progress.lens,
});
if (!mayPublish) {
logWarn("ack_progress_skipped_stale_owner", {
workItemId: data.workItemId,
resourceKey,
reviewLens: progressData.progress.lens,
});
} else {
await publishAckProgress(cfg, pool, progressData, installation, resourceKey);
}
} else {
await publishAckProgress(cfg, pool, progressData, installation, resourceKey);
}
}
if (data.reply) {
await prSurface.replyAt(data.reply.target, data.reply.body);
}
// Ack-only interactions (help / disabled / usage / cancel) finish here β no durable work item.
if (data.reply && data.workItemId == null) {
await prSurface.setAcknowledgementReaction(data.targets, GITHUB_REACTION_PLUS_ONE);
}
}