opencode-magi 0.0.0-dev-20260702001022 → 0.0.0-dev-20260714065541
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.
- package/dist/commands.js +0 -4
- package/dist/magi.js +36 -68
- package/dist/tools/index.js +0 -2
- package/dist/tools/merge/action.js +4 -8
- package/dist/tools/merge/context.js +6 -12
- package/dist/tools/merge/editor.js +21 -33
- package/dist/tools/merge/index.js +12 -32
- package/dist/tools/merge/merge.js +4 -13
- package/dist/tools/merge/report.js +8 -9
- package/dist/tools/review/action.js +36 -68
- package/dist/tools/review/check.js +26 -42
- package/dist/tools/review/context.js +7 -15
- package/dist/tools/review/index.js +12 -32
- package/dist/tools/review/report.js +14 -13
- package/dist/tools/review/review.js +15 -28
- package/dist/tools/review/reviewer.js +19 -31
- package/dist/tools/triage/index.js +1 -2
- package/dist/utils/fs.js +24 -0
- package/dist/utils/index.js +1 -0
- package/package.json +5 -5
- package/dist/tools/cancel/index.js +0 -30
|
@@ -11,9 +11,7 @@ export async function postReviews() {
|
|
|
11
11
|
this.context.abort.throwIfAborted();
|
|
12
12
|
if (this.state.dryRun)
|
|
13
13
|
return;
|
|
14
|
-
|
|
15
|
-
text: `Posting reviews for ${this.getLink()}.`,
|
|
16
|
-
});
|
|
14
|
+
await this.updateEvent(`Posting reviews.`);
|
|
17
15
|
if (!this.config.review.reviewers?.length)
|
|
18
16
|
throw new MagiError("blocked", "No reviewers configured.");
|
|
19
17
|
if (!this.state.reviewers)
|
|
@@ -45,7 +43,7 @@ export async function postReviews() {
|
|
|
45
43
|
.map((finding) => ({ ...finding, id }));
|
|
46
44
|
});
|
|
47
45
|
if (!findings.length) {
|
|
48
|
-
await this.
|
|
46
|
+
await this.updateEvent(`Finished posting reviews.`);
|
|
49
47
|
return;
|
|
50
48
|
}
|
|
51
49
|
params.comments = findings.map(({ body, id, line, path, startLine }) => ({
|
|
@@ -63,7 +61,7 @@ export async function postReviews() {
|
|
|
63
61
|
if (this.state.pr.verdict !== "APPROVED") {
|
|
64
62
|
if (!this.state.operator?.sessionId)
|
|
65
63
|
throw new MagiError("blocked", "Reporter session ID not found.");
|
|
66
|
-
await this.
|
|
64
|
+
await this.updateEvent(`Generating comment by operator.`);
|
|
67
65
|
const contents = JSON.stringify(reviewers.flatMap(([, { outputs }]) => {
|
|
68
66
|
const output = outputs?.at(-1);
|
|
69
67
|
if (!output || output.verdict === "APPROVED")
|
|
@@ -89,12 +87,12 @@ export async function postReviews() {
|
|
|
89
87
|
throw new Error("Invalid output for operator.");
|
|
90
88
|
return parsed.comment;
|
|
91
89
|
}, {
|
|
92
|
-
error: (_, count) => this.
|
|
90
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to post comment by operator. Retrying...`),
|
|
93
91
|
retries: this.config.output.repairAttempts,
|
|
94
92
|
});
|
|
95
93
|
if (!output)
|
|
96
94
|
throw new MagiError("blocked", "Invalid output for operator.");
|
|
97
|
-
await this.
|
|
95
|
+
await this.updateEvent(`Generated comment by operator.`);
|
|
98
96
|
params.body = output;
|
|
99
97
|
}
|
|
100
98
|
params.body = [
|
|
@@ -116,10 +114,10 @@ export async function postReviews() {
|
|
|
116
114
|
].join("\n\n");
|
|
117
115
|
const { data } = await octokit.rest.pulls.createReview(params);
|
|
118
116
|
const posted = data.html_url;
|
|
119
|
-
|
|
117
|
+
await this.updateState({
|
|
120
118
|
reviewers: Object.fromEntries(Object.keys(this.state.reviewers).map((id) => [id, { posted }])),
|
|
121
|
-
text: `Finished posting reviews for ${this.getLink()}.`,
|
|
122
119
|
});
|
|
120
|
+
await this.updateEvent(`Finished posting reviews.`);
|
|
123
121
|
}
|
|
124
122
|
else {
|
|
125
123
|
const worker = new Worker(this.config.review.concurrency.reviewers);
|
|
@@ -167,10 +165,8 @@ export async function postReviews() {
|
|
|
167
165
|
const { data } = await octokit.rest.pulls.createReview(params);
|
|
168
166
|
return [id, { posted: data.html_url }];
|
|
169
167
|
}))));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
text: `Finished posting reviews for ${this.getLink()}.`,
|
|
173
|
-
});
|
|
168
|
+
await this.updateState({ reviewers });
|
|
169
|
+
await this.updateEvent(`Finished posting reviews.`);
|
|
174
170
|
}
|
|
175
171
|
}
|
|
176
172
|
export async function automate() {
|
|
@@ -184,10 +180,8 @@ export async function automate() {
|
|
|
184
180
|
const automation = this.config[this.state.command].automation;
|
|
185
181
|
const action = this.state.pr.verdict === "APPROVED" ? "merge" : "close";
|
|
186
182
|
if (!automation[action]) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
text: `Skipped ${action} automation for ${this.getLink()}.`,
|
|
190
|
-
});
|
|
183
|
+
await this.updateState({ pr: { automation: "SKIPPED" } });
|
|
184
|
+
await this.updateEvent(`Skipped ${action} automation.`);
|
|
191
185
|
return "SKIPPED";
|
|
192
186
|
}
|
|
193
187
|
const account = this.state.operator.account;
|
|
@@ -197,25 +191,19 @@ export async function automate() {
|
|
|
197
191
|
const failed = this.state.pr.checks.failed;
|
|
198
192
|
const pending = this.state.pr.checks.pending;
|
|
199
193
|
if (failed.length || pending.length) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
text: `Skipped merge automation for ${this.getLink()} because unresolved CI checks remain.`,
|
|
203
|
-
});
|
|
194
|
+
await this.updateState({ pr: { automation: "SKIPPED" } });
|
|
195
|
+
await this.updateEvent(`Skipped merge automation because unresolved CI checks remain.`);
|
|
204
196
|
return "SKIPPED";
|
|
205
197
|
}
|
|
206
198
|
if (await isConflict.call(this)) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
text: `Merge automation found conflicts for ${this.getLink()}.`,
|
|
210
|
-
});
|
|
199
|
+
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
200
|
+
await this.updateEvent(`Merge automation found conflicts.`);
|
|
211
201
|
return "CONFLICT";
|
|
212
202
|
}
|
|
213
203
|
}
|
|
214
204
|
if (this.state.dryRun) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
text: `Skipped ${action} automation for ${this.getLink()} during dry run.`,
|
|
218
|
-
});
|
|
205
|
+
await this.updateState({ pr: { automation: "SKIPPED" } });
|
|
206
|
+
await this.updateEvent(`Skipped ${action} automation during dry run.`);
|
|
219
207
|
return "SKIPPED";
|
|
220
208
|
}
|
|
221
209
|
const token = await this.magi.getGhToken(account);
|
|
@@ -234,20 +222,14 @@ export async function automate() {
|
|
|
234
222
|
}
|
|
235
223
|
}
|
|
236
224
|
if (action !== "merge" || this.config.review.merge.queue)
|
|
237
|
-
|
|
238
|
-
text: `${action === "merge" ? "Merging" : "Closing"} ${this.getLink()}.`,
|
|
239
|
-
});
|
|
225
|
+
await this.updateEvent(`${action === "merge" ? "Merging" : "Closing"}.`);
|
|
240
226
|
else
|
|
241
|
-
|
|
242
|
-
text: `Waiting to merge ${this.getLink()}.`,
|
|
243
|
-
});
|
|
227
|
+
await this.updateEvent(`Waiting to merge.`);
|
|
244
228
|
if (action === "merge" && this.config.review.merge.queue) {
|
|
245
229
|
const octokit = await this.magi.createOctokit(this.config, this.context.abort, account);
|
|
246
230
|
const graphql = this.magi.createGraphql(octokit);
|
|
247
231
|
await graphql.enqueuePullRequest({ id: this.state.pr.metadata.node_id });
|
|
248
|
-
|
|
249
|
-
text: `Waiting for merge queue for ${this.getLink()}.`,
|
|
250
|
-
});
|
|
232
|
+
await this.updateEvent(`Waiting for merge queue.`);
|
|
251
233
|
const result = await waitMergeQueue.call(this);
|
|
252
234
|
if (result === "CONFLICT")
|
|
253
235
|
return result;
|
|
@@ -279,31 +261,25 @@ export async function automate() {
|
|
|
279
261
|
if (status === "DIRTY")
|
|
280
262
|
return "CONFLICT";
|
|
281
263
|
if (status === "BEHIND") {
|
|
282
|
-
|
|
283
|
-
text: `Updating ${this.getLink()} with the base branch before merging.`,
|
|
284
|
-
});
|
|
264
|
+
await this.updateEvent(`Updating with the base branch before merging.`);
|
|
285
265
|
await this.exec(command(...prefix, "update-branch", ...suffix), options);
|
|
286
266
|
return;
|
|
287
267
|
}
|
|
288
268
|
if (status === "BLOCKED" && hasFailedChecks(checks))
|
|
289
|
-
throw new MagiError("blocked", `Required checks failed before merging
|
|
269
|
+
throw new MagiError("blocked", `Required checks failed before merging.`);
|
|
290
270
|
if (!auto && state === "OPEN")
|
|
291
|
-
throw new MagiError("blocked", `Auto-merge is no longer enabled
|
|
271
|
+
throw new MagiError("blocked", `Auto-merge is no longer enabled.`);
|
|
292
272
|
}, 30_000);
|
|
293
273
|
};
|
|
294
274
|
const runAutomation = async () => {
|
|
295
275
|
try {
|
|
296
|
-
|
|
297
|
-
text: `Merging ${this.getLink()}.`,
|
|
298
|
-
});
|
|
276
|
+
await this.updateEvent(`Merging.`);
|
|
299
277
|
await this.exec(command(...args), options);
|
|
300
278
|
if (action === "merge" && this.config.review.merge.auto) {
|
|
301
279
|
const result = await waitMerge();
|
|
302
280
|
if (result === "CONFLICT") {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
text: `Merge automation found conflicts for ${this.getLink()}.`,
|
|
306
|
-
});
|
|
281
|
+
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
282
|
+
await this.updateEvent(`Merge automation found conflicts.`);
|
|
307
283
|
return "CONFLICT";
|
|
308
284
|
}
|
|
309
285
|
}
|
|
@@ -314,24 +290,18 @@ export async function automate() {
|
|
|
314
290
|
throw e;
|
|
315
291
|
switch (true) {
|
|
316
292
|
case /\bconflicts?\b|merge commit cannot be cleanly created/i.test(message):
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
text: `Merge automation found conflicts for ${this.getLink()}.`,
|
|
320
|
-
});
|
|
293
|
+
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
294
|
+
await this.updateEvent(`Merge automation found conflicts.`);
|
|
321
295
|
return "CONFLICT";
|
|
322
296
|
case /head (branch|ref) is (not up to date|out of date)/i.test(message):
|
|
323
|
-
|
|
324
|
-
text: `Updating ${this.getLink()} with the base branch before merging.`,
|
|
325
|
-
});
|
|
297
|
+
await this.updateEvent(`Updating with the base branch before merging.`);
|
|
326
298
|
await this.exec(command(...prefix, "update-branch", ...suffix), options);
|
|
327
299
|
await this.exec(command(...args), options);
|
|
328
300
|
if (this.config.review.merge.auto) {
|
|
329
301
|
const result = await waitMerge();
|
|
330
302
|
if (result === "CONFLICT") {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
text: `Merge automation found conflicts for ${this.getLink()}.`,
|
|
334
|
-
});
|
|
303
|
+
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
304
|
+
await this.updateEvent(`Merge automation found conflicts.`);
|
|
335
305
|
return "CONFLICT";
|
|
336
306
|
}
|
|
337
307
|
}
|
|
@@ -347,10 +317,10 @@ export async function automate() {
|
|
|
347
317
|
if (result)
|
|
348
318
|
return result;
|
|
349
319
|
}
|
|
350
|
-
|
|
320
|
+
await this.updateState({
|
|
351
321
|
pr: { automation: action === "merge" ? "MERGED" : "CLOSED" },
|
|
352
|
-
text: `Finished ${action} automation for ${this.getLink()}.`,
|
|
353
322
|
});
|
|
323
|
+
await this.updateEvent(`Finished ${action} automation.`);
|
|
354
324
|
return action === "merge" ? "MERGED" : "CLOSED";
|
|
355
325
|
}
|
|
356
326
|
function hasFailedChecks(checks) {
|
|
@@ -399,13 +369,11 @@ async function waitMergeQueue(leftMergeQueue = false) {
|
|
|
399
369
|
const nextLeftMergeQueue = state === "OPEN" && !isInMergeQueue && !mergeQueueEntry;
|
|
400
370
|
if (leftMergeQueue && nextLeftMergeQueue) {
|
|
401
371
|
if (await isConflict.call(this)) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
text: `Merge automation found conflicts for ${this.getLink()}.`,
|
|
405
|
-
});
|
|
372
|
+
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
373
|
+
await this.updateEvent(`Merge automation found conflicts.`);
|
|
406
374
|
return "CONFLICT";
|
|
407
375
|
}
|
|
408
|
-
throw new MagiError("blocked", `PR left the merge queue before merging
|
|
376
|
+
throw new MagiError("blocked", `PR left the merge queue before merging.`);
|
|
409
377
|
}
|
|
410
378
|
await wait(30_000);
|
|
411
379
|
return await waitMergeQueue.call(this, nextLeftMergeQueue);
|
|
@@ -24,10 +24,8 @@ const ci = {
|
|
|
24
24
|
};
|
|
25
25
|
export async function checkPr() {
|
|
26
26
|
this.context.abort.throwIfAborted();
|
|
27
|
-
await this.
|
|
28
|
-
|
|
29
|
-
text: `Checking PR ${this.getLink()}.`,
|
|
30
|
-
});
|
|
27
|
+
await this.updateState({ status: "running" });
|
|
28
|
+
await this.updateEvent(`Checking PR.`);
|
|
31
29
|
const { files, metadata } = await getMetadata.call(this);
|
|
32
30
|
if (metadata.state !== "open")
|
|
33
31
|
throw new MagiError("blocked", `PR is not open.`);
|
|
@@ -57,40 +55,32 @@ export async function checkPr() {
|
|
|
57
55
|
throw new MagiError("blocked", `PR is safety blocked. ${errors.join(" ")}`);
|
|
58
56
|
if (this.config.mode === "single") {
|
|
59
57
|
if (this.config.account === metadata.user.login)
|
|
60
|
-
throw new MagiError("blocked", `Single mode account ${this.config.account} cannot review
|
|
58
|
+
throw new MagiError("blocked", `Single mode account ${this.config.account} cannot review because it opened the pull request. Configure account to a different account.`);
|
|
61
59
|
}
|
|
62
60
|
else {
|
|
63
61
|
const accounts = Object.values(this.state.reviewers ?? {})
|
|
64
62
|
.filter(({ account }) => account === metadata.user.login)
|
|
65
63
|
.map(({ account }) => account);
|
|
66
64
|
if (accounts.length)
|
|
67
|
-
throw new MagiError("blocked", `Multi mode accounts ${accounts.join(", ")} cannot review
|
|
65
|
+
throw new MagiError("blocked", `Multi mode accounts ${accounts.join(", ")} cannot review because they opened the pull request. Configure accounts to different accounts.`);
|
|
68
66
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
text: `Finished checking PR ${this.getLink()}.`,
|
|
72
|
-
});
|
|
67
|
+
await this.updateState({ pr: { files, metadata } });
|
|
68
|
+
await this.updateEvent(`Finished checking PR.`);
|
|
73
69
|
}
|
|
74
70
|
export async function checkCi(wait = this.config.review.checks.wait) {
|
|
75
71
|
this.context.abort.throwIfAborted();
|
|
76
|
-
|
|
77
|
-
text: `Checking CI for ${this.getLink()}.`,
|
|
78
|
-
});
|
|
72
|
+
await this.updateEvent(`Checking CI.`);
|
|
79
73
|
if (wait)
|
|
80
74
|
await watchChecks.call(this);
|
|
81
75
|
const checks = await getChecks.call(this);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
text: `Finished checking CI for ${this.getLink()}.`,
|
|
85
|
-
});
|
|
76
|
+
await this.updateState({ pr: { checks } });
|
|
77
|
+
await this.updateEvent(`Finished checking CI.`);
|
|
86
78
|
}
|
|
87
79
|
export async function classifyChecks() {
|
|
88
80
|
this.context.abort.throwIfAborted();
|
|
89
81
|
if (!this.state.pr?.checks?.failed.length)
|
|
90
82
|
return;
|
|
91
|
-
await this.
|
|
92
|
-
text: `Classifying CI checks for ${this.getLink()}.`,
|
|
93
|
-
});
|
|
83
|
+
await this.updateEvent(`Classifying CI checks.`);
|
|
94
84
|
if (!this.state.pr.metadata)
|
|
95
85
|
throw new MagiError("blocked", "PR metadata not found.");
|
|
96
86
|
if (!this.state.worktree)
|
|
@@ -115,7 +105,7 @@ export async function classifyChecks() {
|
|
|
115
105
|
await Promise.all(Object.entries(this.state.reviewers ?? {}).map(([id, { sessionId }]) => worker.run(async () => {
|
|
116
106
|
if (!sessionId)
|
|
117
107
|
throw new Error(`No session ID found for reviewer ${id}.`);
|
|
118
|
-
await this.
|
|
108
|
+
await this.updateEvent(`Classifying CI checks with reviewer ${id}.`);
|
|
119
109
|
const output = await retry(async (count) => {
|
|
120
110
|
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
121
111
|
await this.createAgentFile("ci-classification", id, raw, count);
|
|
@@ -124,7 +114,7 @@ export async function classifyChecks() {
|
|
|
124
114
|
throw new Error(`Invalid output for reviewer ${id}.`);
|
|
125
115
|
return parsed;
|
|
126
116
|
}, {
|
|
127
|
-
error: (_, count) => this.
|
|
117
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to classify CI checks with reviewer ${id}. Retrying...`),
|
|
128
118
|
retries: this.config.output.repairAttempts,
|
|
129
119
|
});
|
|
130
120
|
if (!output)
|
|
@@ -152,10 +142,10 @@ export async function classifyChecks() {
|
|
|
152
142
|
.filter(([, { scope }]) => !scope)
|
|
153
143
|
.map(([id, { comment }]) => `- ${id}: ${comment}`),
|
|
154
144
|
};
|
|
155
|
-
await this.
|
|
145
|
+
await this.updateEvent(filterEmpty([
|
|
156
146
|
scope
|
|
157
|
-
? `Check ${name}
|
|
158
|
-
: `Check ${name}
|
|
147
|
+
? `Check ${name} was classified as in scope by majority vote.`
|
|
148
|
+
: `Check ${name} was classified as out of scope by majority vote. Rerunning it.`,
|
|
159
149
|
reasons.in.length
|
|
160
150
|
? `In scope reasons:\n${reasons.in.join("\n")}`
|
|
161
151
|
: undefined,
|
|
@@ -164,10 +154,8 @@ export async function classifyChecks() {
|
|
|
164
154
|
: undefined,
|
|
165
155
|
]).join("\n\n"));
|
|
166
156
|
}));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
text: `Finished classifying CI checks for ${this.getLink()}.`,
|
|
170
|
-
});
|
|
157
|
+
await this.updateState({ pr: { checks: { failed } } });
|
|
158
|
+
await this.updateEvent(`Finished classifying CI checks.`);
|
|
171
159
|
}
|
|
172
160
|
export async function rerunChecks() {
|
|
173
161
|
this.context.abort.throwIfAborted();
|
|
@@ -180,9 +168,7 @@ export async function rerunChecks() {
|
|
|
180
168
|
const failedChecks = checks.failed.filter(({ scope }) => !scope);
|
|
181
169
|
if (!failedChecks.length)
|
|
182
170
|
return;
|
|
183
|
-
await this.
|
|
184
|
-
text: `Rerunning CI checks for ${this.getLink()}.`,
|
|
185
|
-
});
|
|
171
|
+
await this.updateEvent(`Rerunning CI checks.`);
|
|
186
172
|
let label = failedChecks.map(({ name }) => name).join(", ");
|
|
187
173
|
if (this.state.dryRun) {
|
|
188
174
|
checks.passed = [
|
|
@@ -193,7 +179,7 @@ export async function rerunChecks() {
|
|
|
193
179
|
}
|
|
194
180
|
else {
|
|
195
181
|
await retry(async () => {
|
|
196
|
-
await this.
|
|
182
|
+
await this.updateEvent(`Rerunning checks ${label}.`);
|
|
197
183
|
await Promise.all(checks.failed
|
|
198
184
|
.filter(({ scope }) => !scope)
|
|
199
185
|
.map(async ({ id }) => rerunCheck.call(this, id)));
|
|
@@ -211,9 +197,9 @@ export async function rerunChecks() {
|
|
|
211
197
|
if (!failedChecks.length)
|
|
212
198
|
return;
|
|
213
199
|
label = failedChecks.map(({ name }) => name).join(", ");
|
|
214
|
-
throw new Error(`Checks ${label}
|
|
200
|
+
throw new Error(`Checks ${label} still failed.`);
|
|
215
201
|
}, {
|
|
216
|
-
error: (_, count) => this.
|
|
202
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to rerun checks ${label}. Retrying...`),
|
|
217
203
|
retries: this.config.review.checks.retryFailedJobs,
|
|
218
204
|
});
|
|
219
205
|
}
|
|
@@ -221,18 +207,16 @@ export async function rerunChecks() {
|
|
|
221
207
|
const failedChecksAfterRerun = checks.failed.filter(({ scope }) => !scope);
|
|
222
208
|
const message = filterEmpty([
|
|
223
209
|
passedChecksAfterRerun.length
|
|
224
|
-
? `Reran checks ${passedChecksAfterRerun.map(({ name }) => name).join(", ")}
|
|
210
|
+
? `Reran checks ${passedChecksAfterRerun.map(({ name }) => name).join(", ")} passed.`
|
|
225
211
|
: undefined,
|
|
226
212
|
failedChecksAfterRerun.length
|
|
227
|
-
? `Reran checks ${failedChecksAfterRerun.map(({ name }) => name).join(", ")}
|
|
213
|
+
? `Reran checks ${failedChecksAfterRerun.map(({ name }) => name).join(", ")} failed.`
|
|
228
214
|
: undefined,
|
|
229
215
|
]).join("\n");
|
|
230
216
|
if (message)
|
|
231
|
-
await this.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
text: `Finished rerunning checks for ${this.getLink()}.`,
|
|
235
|
-
});
|
|
217
|
+
await this.updateEvent(message);
|
|
218
|
+
await this.updateState({ pr: { checks } });
|
|
219
|
+
await this.updateEvent(`Finished rerunning checks.`);
|
|
236
220
|
}
|
|
237
221
|
export async function getMetadata() {
|
|
238
222
|
const [{ data }, files] = await Promise.all([
|
|
@@ -2,9 +2,7 @@ import { MagiError } from "@/magi";
|
|
|
2
2
|
import { command, filterEmpty, marker, quote } from "@/utils";
|
|
3
3
|
export async function checkExistingReviews() {
|
|
4
4
|
this.context.abort.throwIfAborted();
|
|
5
|
-
|
|
6
|
-
text: `Fetching existing reviews for ${this.getLink()}.`,
|
|
7
|
-
});
|
|
5
|
+
await this.updateEvent(`Fetching existing reviews.`);
|
|
8
6
|
if (!this.state.pr?.metadata)
|
|
9
7
|
throw new MagiError("blocked", "PR metadata not found.");
|
|
10
8
|
if (!this.config.review.reviewers?.length)
|
|
@@ -14,9 +12,7 @@ export async function checkExistingReviews() {
|
|
|
14
12
|
getCommits.call(this),
|
|
15
13
|
getReviewThreads.call(this),
|
|
16
14
|
]);
|
|
17
|
-
|
|
18
|
-
pr: { commits, reviews, threads },
|
|
19
|
-
});
|
|
15
|
+
await this.updateState({ pr: { commits, reviews, threads } });
|
|
20
16
|
const latestNonMergeCommit = commits
|
|
21
17
|
.toReversed()
|
|
22
18
|
.find(({ parents }) => parents.length < 2);
|
|
@@ -102,27 +98,23 @@ export async function checkExistingReviews() {
|
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
100
|
const skip = Object.values(reviewers).every(({ status }) => status === "skip");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
text: `Finished fetching existing reviews for ${this.getLink()}.`,
|
|
108
|
-
});
|
|
101
|
+
await this.updateState({ reviewers });
|
|
102
|
+
await this.updateEvent(`Finished fetching existing reviews.`);
|
|
109
103
|
return skip;
|
|
110
104
|
}
|
|
111
105
|
export async function fetchReviewContext() {
|
|
112
106
|
this.context.abort.throwIfAborted();
|
|
113
|
-
|
|
114
|
-
text: `Fetching review context for ${this.getLink()}.`,
|
|
115
|
-
});
|
|
107
|
+
await this.updateEvent(`Fetching review context.`);
|
|
116
108
|
const [comments, issues, threads] = await Promise.all([
|
|
117
109
|
getComments.call(this),
|
|
118
110
|
getClosingIssues.call(this),
|
|
119
111
|
getReviewThreads.call(this),
|
|
120
112
|
]);
|
|
121
113
|
const inlineCommentTargets = await getInlineCommentTargets.call(this);
|
|
122
|
-
|
|
114
|
+
await this.updateState({
|
|
123
115
|
pr: { comments, inlineCommentTargets, issues, threads },
|
|
124
|
-
text: `Finished fetching review context for ${this.getLink()}.`,
|
|
125
116
|
});
|
|
117
|
+
await this.updateEvent(`Finished fetching review context.`);
|
|
126
118
|
}
|
|
127
119
|
export async function getComments() {
|
|
128
120
|
return await this.octokit.paginate(this.octokit.rest.issues.listComments, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import { parsePrs, split, Worker } from "@/utils";
|
|
3
3
|
import { Review } from "./review";
|
|
4
|
-
function overrideConfig(config, args, dryRun = false
|
|
4
|
+
function overrideConfig(config, args, dryRun = false) {
|
|
5
5
|
for (let index = 0; index < args.length; index++) {
|
|
6
6
|
const arg = args[index];
|
|
7
7
|
const value = args[index + 1];
|
|
@@ -11,9 +11,6 @@ function overrideConfig(config, args, dryRun = false, sync = false) {
|
|
|
11
11
|
case "--dry-run":
|
|
12
12
|
dryRun = true;
|
|
13
13
|
break;
|
|
14
|
-
case "--sync":
|
|
15
|
-
sync = true;
|
|
16
|
-
break;
|
|
17
14
|
case "--retry-api-attempts":
|
|
18
15
|
config.github.retryApiAttempts = parseInt(value);
|
|
19
16
|
break;
|
|
@@ -49,7 +46,7 @@ function overrideConfig(config, args, dryRun = false, sync = false) {
|
|
|
49
46
|
break;
|
|
50
47
|
}
|
|
51
48
|
}
|
|
52
|
-
return { config, dryRun
|
|
49
|
+
return { config, dryRun };
|
|
53
50
|
}
|
|
54
51
|
export const review = function (magi) {
|
|
55
52
|
return {
|
|
@@ -57,23 +54,18 @@ export const review = function (magi) {
|
|
|
57
54
|
args: {
|
|
58
55
|
dryRun: tool.schema.boolean().optional(),
|
|
59
56
|
prs: tool.schema.string(),
|
|
60
|
-
sync: tool.schema.boolean().optional(),
|
|
61
57
|
},
|
|
62
|
-
description: "
|
|
58
|
+
description: "Review one or more pull requests and post the reviews.",
|
|
63
59
|
async execute(args, context) {
|
|
64
60
|
const prs = parsePrs(args.prs);
|
|
65
|
-
const { config, dryRun
|
|
61
|
+
const { config, dryRun } = overrideConfig(await magi.getConfig({ reviewers: true }), split(args.prs), args.dryRun);
|
|
66
62
|
const worker = new Worker(config.review.concurrency.runs);
|
|
67
|
-
const
|
|
68
|
-
const tasks = [];
|
|
69
|
-
for (const pr of prs) {
|
|
63
|
+
const reports = await Promise.all(prs.map(async (pr) => {
|
|
70
64
|
context.abort.throwIfAborted();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
states.push(run.state);
|
|
76
|
-
const task = worker.run(async () => {
|
|
65
|
+
return worker.run(async () => {
|
|
66
|
+
const run = await Review.init(pr, magi, config, context, {
|
|
67
|
+
dryRun,
|
|
68
|
+
});
|
|
77
69
|
try {
|
|
78
70
|
await run.checkPr();
|
|
79
71
|
const skip = await run.checkExistingReviews();
|
|
@@ -101,20 +93,8 @@ export const review = function (magi) {
|
|
|
101
93
|
await run.cleanup();
|
|
102
94
|
}
|
|
103
95
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
void task;
|
|
107
|
-
}
|
|
108
|
-
if (sync) {
|
|
109
|
-
const results = await Promise.all(tasks);
|
|
110
|
-
const output = filterEmpty(results.map(({ text }) => text)).join("\n\n");
|
|
111
|
-
if (results.some(({ status }) => status === "failed" || status === "cancelled"))
|
|
112
|
-
throw new Error(output);
|
|
113
|
-
return output;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
return filterEmpty(states.map(({ text }) => text)).join("\n");
|
|
117
|
-
}
|
|
96
|
+
}));
|
|
97
|
+
return reports.join("\n\n");
|
|
118
98
|
},
|
|
119
99
|
}),
|
|
120
100
|
};
|
|
@@ -5,13 +5,13 @@ import { filterEmpty, toTitleCase } from "@/utils";
|
|
|
5
5
|
export async function createReport(e) {
|
|
6
6
|
if (!e) {
|
|
7
7
|
const status = "completed";
|
|
8
|
-
const text = createContent.call(this, { status });
|
|
8
|
+
const text = await createContent.call(this, { status });
|
|
9
9
|
await writeFile(join(this.state.output, "report.md"), `${text}\n`);
|
|
10
|
-
|
|
10
|
+
await this.updateState({
|
|
11
11
|
completedAt: new Date().toISOString(),
|
|
12
12
|
status,
|
|
13
|
-
text,
|
|
14
13
|
});
|
|
14
|
+
return text;
|
|
15
15
|
}
|
|
16
16
|
else {
|
|
17
17
|
const error = e instanceof Error ? e.message : "Unknown error";
|
|
@@ -20,26 +20,27 @@ export async function createReport(e) {
|
|
|
20
20
|
: this.context.abort.aborted
|
|
21
21
|
? "cancelled"
|
|
22
22
|
: "failed";
|
|
23
|
-
const text = createContent.call(this, { error, status });
|
|
23
|
+
const text = await createContent.call(this, { error, status });
|
|
24
24
|
await writeFile(join(this.state.output, "report.md"), `${text}\n`);
|
|
25
|
-
|
|
25
|
+
await this.updateState({
|
|
26
26
|
completedAt: new Date().toISOString(),
|
|
27
|
-
error,
|
|
28
27
|
status,
|
|
29
|
-
text: `${toTitleCase(status)} reviewing ${this.getLink()}.\n\n${text}`,
|
|
30
28
|
});
|
|
29
|
+
return text;
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
|
-
function createContent(input) {
|
|
32
|
+
async function createContent(input) {
|
|
34
33
|
return filterEmpty([
|
|
35
|
-
...createMetaContent.call(this, input),
|
|
34
|
+
...(await createMetaContent.call(this, input)),
|
|
36
35
|
...createCheckContent.call(this),
|
|
37
36
|
...createReviewerContent.call(this),
|
|
38
37
|
]).join("\n");
|
|
39
38
|
}
|
|
40
|
-
export function createMetaContent(input) {
|
|
39
|
+
export async function createMetaContent(input) {
|
|
40
|
+
const events = await this.getEvents();
|
|
41
|
+
const lastAction = events.at(-1)?.message;
|
|
41
42
|
const rows = [
|
|
42
|
-
`- **Pull Request:** ${this.
|
|
43
|
+
`- **Pull Request:** [${this.state.pr.number}](${this.state.pr.url})`,
|
|
43
44
|
`- **Mode**: ${toTitleCase(this.config.mode)}`,
|
|
44
45
|
`- **Dry run**: ${this.state.dryRun ? "Yes" : "No"}`,
|
|
45
46
|
`- **Status**: ${toTitleCase(input.status)}`,
|
|
@@ -47,8 +48,8 @@ export function createMetaContent(input) {
|
|
|
47
48
|
];
|
|
48
49
|
if (this.state.pr?.verdict)
|
|
49
50
|
rows.push(`- **Verdict**: ${toTitleCase(this.state.pr.verdict.toLocaleLowerCase())}`);
|
|
50
|
-
if (
|
|
51
|
-
rows.push(`- **Last action**: ${
|
|
51
|
+
if (lastAction)
|
|
52
|
+
rows.push(`- **Last action**: ${lastAction}`);
|
|
52
53
|
if (input.error)
|
|
53
54
|
rows.push(`- **Error**: ${input.error}`);
|
|
54
55
|
return rows;
|