opencode-magi 0.0.0-dev-20260727045328 → 0.0.0-dev-20260727095903
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/constant.js +1 -5
- package/dist/tools/merge/index.js +1 -1
- package/dist/tools/review/action.js +15 -3
- package/dist/tools/review/check.js +61 -21
- package/dist/tools/review/review.js +2 -1
- package/package.json +1 -1
- package/schema.json +110 -16
package/dist/constant.js
CHANGED
|
@@ -127,7 +127,7 @@ export const merge = function (magi) {
|
|
|
127
127
|
await run.editCycles(async () => editCycle(run));
|
|
128
128
|
const automation = await run.automate();
|
|
129
129
|
if (automation === "CONFLICT" &&
|
|
130
|
-
run.config.merge.automation.conflict) {
|
|
130
|
+
run.isAutomationEnabled(run.config.merge.automation.conflict)) {
|
|
131
131
|
await run.editCycles(async (cycle) => editCycle(run, { conflict: true, cycle }));
|
|
132
132
|
await run.automate();
|
|
133
133
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MagiError } from "@/magi";
|
|
2
2
|
import { Prompt } from "@/prompts";
|
|
3
|
-
import { command, filterEmpty, ignoreError, isArray, isObject, isString, loop, marker, omitNullish, quote, retry, wait, Worker, } from "@/utils";
|
|
3
|
+
import { command, filterEmpty, ignoreError, isArray, isBoolean, isObject, isString, loop, marker, omitNullish, quote, retry, wait, Worker, } from "@/utils";
|
|
4
|
+
import { getConditionErrors } from "./check";
|
|
4
5
|
const events = {
|
|
5
6
|
APPROVED: "APPROVE",
|
|
6
7
|
CHANGES_REQUESTED: "REQUEST_CHANGES",
|
|
@@ -181,9 +182,8 @@ export async function automate() {
|
|
|
181
182
|
throw new MagiError("blocked", "PR verdict not found.");
|
|
182
183
|
if (!["APPROVED", "CLOSED"].includes(this.state.pr.verdict))
|
|
183
184
|
return "SKIPPED";
|
|
184
|
-
const automation = this.config[this.state.command].automation;
|
|
185
185
|
const action = this.state.pr.verdict === "APPROVED" ? "merge" : "close";
|
|
186
|
-
if (!automation[action]) {
|
|
186
|
+
if (!isAutomationEnabled.call(this, this.config[this.state.command].automation[action])) {
|
|
187
187
|
await this.updateState({ pr: { automation: "SKIPPED" } });
|
|
188
188
|
await this.updateEvent(`Skipped ${action} automation.`);
|
|
189
189
|
return "SKIPPED";
|
|
@@ -345,6 +345,18 @@ export async function automate() {
|
|
|
345
345
|
await this.updateEvent(`Finished ${action} automation.`);
|
|
346
346
|
return action === "merge" ? "MERGED" : "CLOSED";
|
|
347
347
|
}
|
|
348
|
+
export function isAutomationEnabled(conditions) {
|
|
349
|
+
if (isBoolean(conditions))
|
|
350
|
+
return conditions;
|
|
351
|
+
const edited = !this.state.dryRun &&
|
|
352
|
+
this.state.editor?.outputs?.some(({ commitSha }) => !!commitSha);
|
|
353
|
+
return conditions.every(([expected, condition]) => {
|
|
354
|
+
const errors = getConditionErrors(condition, this.state.pr.metadata, this.state.pr.files ?? []);
|
|
355
|
+
const matches = !errors.length &&
|
|
356
|
+
(!("edited" in condition) || condition.edited === !!edited);
|
|
357
|
+
return matches === expected;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
348
360
|
function hasFailedChecks(checks) {
|
|
349
361
|
if (!isArray(checks))
|
|
350
362
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import picomatch from "picomatch";
|
|
2
2
|
import { MagiError } from "@/magi";
|
|
3
3
|
import { Prompt } from "@/prompts";
|
|
4
|
-
import { command, filterEmpty, isNumber, omitNullish, quote, retry, Worker, } from "@/utils";
|
|
4
|
+
import { command, filterEmpty, isBoolean, isNumber, omitNullish, quote, retry, Worker, } from "@/utils";
|
|
5
5
|
const ci = {
|
|
6
6
|
isExcluded(exclude, { name }) {
|
|
7
7
|
return exclude.some((pattern) => {
|
|
@@ -22,6 +22,65 @@ const ci = {
|
|
|
22
22
|
return !this.isFailed(check) && !this.isPassed(check);
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
+
function matchesValues(filter, values) {
|
|
26
|
+
if (!filter)
|
|
27
|
+
return true;
|
|
28
|
+
if ("include" in filter &&
|
|
29
|
+
!values.some((value) => filter.include.includes(value)))
|
|
30
|
+
return false;
|
|
31
|
+
return !("exclude" in filter &&
|
|
32
|
+
values.some((value) => filter.exclude.includes(value)));
|
|
33
|
+
}
|
|
34
|
+
function matchesPatterns(filter, values) {
|
|
35
|
+
if (!filter)
|
|
36
|
+
return true;
|
|
37
|
+
if ("include" in filter) {
|
|
38
|
+
const isIncluded = picomatch(filter.include, { dot: true });
|
|
39
|
+
if (!values.some((value) => isIncluded(value)))
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if ("exclude" in filter) {
|
|
43
|
+
const isExcluded = picomatch(filter.exclude, { dot: true });
|
|
44
|
+
if (values.some((value) => isExcluded(value)))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
export function getConditionErrors(condition, metadata, files) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const branches = condition.branches
|
|
52
|
+
? {
|
|
53
|
+
base: "base" in condition.branches ? condition.branches.base : undefined,
|
|
54
|
+
head: "head" in condition.branches ? condition.branches.head : undefined,
|
|
55
|
+
}
|
|
56
|
+
: {};
|
|
57
|
+
if (!matchesValues(condition.authors, [metadata.user.login]))
|
|
58
|
+
errors.push(`Author does not match safety filter: ${metadata.user.login}.`);
|
|
59
|
+
if (!matchesPatterns(branches.base, [metadata.base.ref]))
|
|
60
|
+
errors.push(`Base branch does not match safety filter: ${metadata.base.ref}.`);
|
|
61
|
+
if (!matchesPatterns(branches.head, [metadata.head.ref]))
|
|
62
|
+
errors.push(`Head branch does not match safety filter: ${metadata.head.ref}.`);
|
|
63
|
+
if (!matchesValues(condition.labels, metadata.labels.map(({ name }) => name)))
|
|
64
|
+
errors.push(`Labels do not match safety filter.`);
|
|
65
|
+
if (!matchesPatterns(condition.paths, files))
|
|
66
|
+
errors.push(`Paths do not match safety filter.`);
|
|
67
|
+
if (isNumber(condition.maxChangedFiles) &&
|
|
68
|
+
metadata.changed_files > condition.maxChangedFiles)
|
|
69
|
+
errors.push(`Changed files exceed limit: ${metadata.changed_files} > ${condition.maxChangedFiles}.`);
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
function getSafetyErrors(conditions, metadata, files) {
|
|
73
|
+
if (isBoolean(conditions))
|
|
74
|
+
return conditions ? [] : ["Safety is disabled."];
|
|
75
|
+
return conditions.flatMap(([expected, condition], index) => {
|
|
76
|
+
const errors = getConditionErrors(condition, metadata, files);
|
|
77
|
+
if (!errors.length === expected)
|
|
78
|
+
return [];
|
|
79
|
+
if (expected)
|
|
80
|
+
return errors;
|
|
81
|
+
return [`Safety condition ${index + 1} unexpectedly matched.`];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
25
84
|
export async function checkPr() {
|
|
26
85
|
this.context.abort.throwIfAborted();
|
|
27
86
|
await this.updateState({ status: "running" });
|
|
@@ -31,26 +90,7 @@ export async function checkPr() {
|
|
|
31
90
|
throw new MagiError("blocked", `PR is not open.`);
|
|
32
91
|
if (metadata.draft)
|
|
33
92
|
throw new MagiError("blocked", `PR is a draft.`);
|
|
34
|
-
const errors =
|
|
35
|
-
if (this.config.review.safety.allowAuthors.length)
|
|
36
|
-
if (!this.config.review.safety.allowAuthors.includes(metadata.user.login))
|
|
37
|
-
errors.push(`Author is not allowed: ${metadata.user.login}.`);
|
|
38
|
-
if (this.config.review.safety.requiredLabels.length) {
|
|
39
|
-
const missingLabels = this.config.review.safety.requiredLabels.filter((label) => !metadata.labels.some(({ name }) => name === label));
|
|
40
|
-
if (missingLabels.length)
|
|
41
|
-
errors.push(`Required labels missing: ${missingLabels.join(", ")}.`);
|
|
42
|
-
}
|
|
43
|
-
if (isNumber(this.config.review.safety.maxChangedFiles) &&
|
|
44
|
-
metadata.changed_files > this.config.review.safety.maxChangedFiles)
|
|
45
|
-
errors.push(`Changed files exceed limit: ${metadata.changed_files} > ${this.config.review.safety.maxChangedFiles}.`);
|
|
46
|
-
if (this.config.review.safety.blockedPaths.length) {
|
|
47
|
-
const isBlocked = picomatch(this.config.review.safety.blockedPaths, {
|
|
48
|
-
dot: true,
|
|
49
|
-
});
|
|
50
|
-
const blocked = files.filter((file) => isBlocked(file));
|
|
51
|
-
if (blocked.length)
|
|
52
|
-
errors.push(`Blocked paths changed: ${blocked.join(", ")}.`);
|
|
53
|
-
}
|
|
93
|
+
const errors = getSafetyErrors(this.config.review.safety, metadata, files);
|
|
54
94
|
if (errors.length)
|
|
55
95
|
throw new MagiError("blocked", `PR is safety blocked. ${errors.join(" ")}`);
|
|
56
96
|
if (this.config.mode === "single") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { MagiError } from "@/magi";
|
|
3
3
|
import { createExecWithGitHubApiRetry, quote } from "@/utils";
|
|
4
|
-
import { automate, postReviews } from "./action";
|
|
4
|
+
import { automate, isAutomationEnabled, postReviews } from "./action";
|
|
5
5
|
import { checkCi, checkPr, classifyChecks, rerunChecks } from "./check";
|
|
6
6
|
import { checkExistingReviews, fetchReviewContext } from "./context";
|
|
7
7
|
import { createReport } from "./report";
|
|
@@ -62,6 +62,7 @@ export class Review {
|
|
|
62
62
|
validateFindings = validateFindings;
|
|
63
63
|
reconsiderClose = reconsiderClose;
|
|
64
64
|
postReviews = postReviews;
|
|
65
|
+
isAutomationEnabled = isAutomationEnabled;
|
|
65
66
|
automate = automate;
|
|
66
67
|
createReport = createReport;
|
|
67
68
|
async cleanup() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260727095903",
|
|
4
4
|
"description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",
|
package/schema.json
CHANGED
|
@@ -174,21 +174,125 @@
|
|
|
174
174
|
"variant": { "type": "string" }
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
|
+
"safetyFilter": {
|
|
178
|
+
"type": "object",
|
|
179
|
+
"additionalProperties": false,
|
|
180
|
+
"minProperties": 1,
|
|
181
|
+
"properties": {
|
|
182
|
+
"exclude": { "type": "array", "items": { "type": "string" } },
|
|
183
|
+
"include": { "type": "array", "items": { "type": "string" } }
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"safetyBranchFilter": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"additionalProperties": false,
|
|
189
|
+
"minProperties": 1,
|
|
190
|
+
"properties": {
|
|
191
|
+
"base": { "$ref": "#/$defs/safetyFilter" },
|
|
192
|
+
"head": { "$ref": "#/$defs/safetyFilter" }
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"safety": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"additionalProperties": false,
|
|
198
|
+
"properties": {
|
|
199
|
+
"authors": { "$ref": "#/$defs/safetyFilter" },
|
|
200
|
+
"branches": { "$ref": "#/$defs/safetyBranchFilter" },
|
|
201
|
+
"labels": { "$ref": "#/$defs/safetyFilter" },
|
|
202
|
+
"maxChangedFiles": { "type": "integer", "minimum": 0 },
|
|
203
|
+
"paths": { "$ref": "#/$defs/safetyFilter" }
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"conditions": {
|
|
207
|
+
"oneOf": [
|
|
208
|
+
{ "type": "boolean" },
|
|
209
|
+
{
|
|
210
|
+
"type": "array",
|
|
211
|
+
"items": {
|
|
212
|
+
"type": "array",
|
|
213
|
+
"minItems": 2,
|
|
214
|
+
"maxItems": 2,
|
|
215
|
+
"prefixItems": [{ "type": "boolean" }, { "$ref": "#/$defs/safety" }]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
"automationSafety": {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"additionalProperties": false,
|
|
223
|
+
"properties": {
|
|
224
|
+
"authors": { "$ref": "#/$defs/safetyFilter" },
|
|
225
|
+
"branches": { "$ref": "#/$defs/safetyBranchFilter" },
|
|
226
|
+
"labels": { "$ref": "#/$defs/safetyFilter" },
|
|
227
|
+
"paths": { "$ref": "#/$defs/safetyFilter" }
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"automationConditions": {
|
|
231
|
+
"oneOf": [
|
|
232
|
+
{ "type": "boolean" },
|
|
233
|
+
{
|
|
234
|
+
"type": "array",
|
|
235
|
+
"items": {
|
|
236
|
+
"type": "array",
|
|
237
|
+
"minItems": 2,
|
|
238
|
+
"maxItems": 2,
|
|
239
|
+
"prefixItems": [
|
|
240
|
+
{ "type": "boolean" },
|
|
241
|
+
{ "$ref": "#/$defs/automationSafety" }
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
"mergeAutomationConditions": {
|
|
248
|
+
"oneOf": [
|
|
249
|
+
{ "type": "boolean" },
|
|
250
|
+
{
|
|
251
|
+
"type": "array",
|
|
252
|
+
"items": {
|
|
253
|
+
"type": "array",
|
|
254
|
+
"minItems": 2,
|
|
255
|
+
"maxItems": 2,
|
|
256
|
+
"prefixItems": [
|
|
257
|
+
{ "type": "boolean" },
|
|
258
|
+
{ "$ref": "#/$defs/mergeAutomationSafety" }
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
"mergeAutomationSafety": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"additionalProperties": false,
|
|
267
|
+
"properties": {
|
|
268
|
+
"authors": { "$ref": "#/$defs/safetyFilter" },
|
|
269
|
+
"branches": { "$ref": "#/$defs/safetyBranchFilter" },
|
|
270
|
+
"edited": { "type": "boolean" },
|
|
271
|
+
"labels": { "$ref": "#/$defs/safetyFilter" },
|
|
272
|
+
"paths": { "$ref": "#/$defs/safetyFilter" }
|
|
273
|
+
}
|
|
274
|
+
},
|
|
177
275
|
"automation": {
|
|
178
276
|
"type": "object",
|
|
179
277
|
"additionalProperties": false,
|
|
180
278
|
"properties": {
|
|
181
|
-
"merge": { "
|
|
182
|
-
"close": { "
|
|
279
|
+
"merge": { "$ref": "#/$defs/automationConditions", "default": true },
|
|
280
|
+
"close": { "$ref": "#/$defs/automationConditions", "default": false }
|
|
183
281
|
}
|
|
184
282
|
},
|
|
185
283
|
"mergeAutomation": {
|
|
186
284
|
"type": "object",
|
|
187
285
|
"additionalProperties": false,
|
|
188
286
|
"properties": {
|
|
189
|
-
"merge": {
|
|
190
|
-
|
|
191
|
-
|
|
287
|
+
"merge": {
|
|
288
|
+
"$ref": "#/$defs/mergeAutomationConditions",
|
|
289
|
+
"default": true
|
|
290
|
+
},
|
|
291
|
+
"close": {
|
|
292
|
+
"$ref": "#/$defs/mergeAutomationConditions",
|
|
293
|
+
"default": false
|
|
294
|
+
},
|
|
295
|
+
"conflict": { "$ref": "#/$defs/automationConditions", "default": false }
|
|
192
296
|
}
|
|
193
297
|
},
|
|
194
298
|
"reviewChecks": {
|
|
@@ -222,16 +326,6 @@
|
|
|
222
326
|
"runs": { "type": "integer", "minimum": 1, "default": 3 }
|
|
223
327
|
}
|
|
224
328
|
},
|
|
225
|
-
"safety": {
|
|
226
|
-
"type": "object",
|
|
227
|
-
"additionalProperties": false,
|
|
228
|
-
"properties": {
|
|
229
|
-
"allowAuthors": { "type": "array", "items": { "type": "string" } },
|
|
230
|
-
"blockedPaths": { "type": "array", "items": { "type": "string" } },
|
|
231
|
-
"maxChangedFiles": { "type": "integer", "minimum": 0 },
|
|
232
|
-
"requiredLabels": { "type": "array", "items": { "type": "string" } }
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
329
|
"reviewPrompts": {
|
|
236
330
|
"type": "object",
|
|
237
331
|
"additionalProperties": false,
|
|
@@ -376,7 +470,7 @@
|
|
|
376
470
|
},
|
|
377
471
|
"prompts": { "$ref": "#/$defs/reviewPrompts" },
|
|
378
472
|
"checks": { "$ref": "#/$defs/reviewChecks" },
|
|
379
|
-
"safety": { "$ref": "#/$defs/
|
|
473
|
+
"safety": { "$ref": "#/$defs/conditions" },
|
|
380
474
|
"automation": { "$ref": "#/$defs/automation" },
|
|
381
475
|
"concurrency": { "$ref": "#/$defs/concurrency" },
|
|
382
476
|
"merge": { "$ref": "#/$defs/reviewMerge" },
|