plugin-git-manager 1.1.9 → 1.1.12
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/client/187.d5545b7cc8b90bfc.js +10 -0
- package/dist/client/components/RunReviewButton.d.ts +1 -1
- package/dist/client/context/GitManagerContext.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -4
- package/dist/locale/en-US.json +10 -1
- package/dist/locale/vi-VN.json +2 -0
- package/dist/server/actions/git-actions.js +15 -12
- package/dist/server/actions/gitlab-api.js +14 -13
- package/dist/server/actions/review.d.ts +5 -2
- package/dist/server/actions/review.js +184 -37
- package/dist/server/ai-tools.js +2 -0
- package/dist/server/collections/gitCodeReviews.js +1 -0
- package/dist/server/collections/gitRepositories.js +12 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
- package/dist/server/plugin.d.ts +4 -0
- package/dist/server/plugin.js +43 -6
- package/dist/server/poller.js +3 -1
- package/package.json +1 -1
- package/src/client/components/CommitHistory.tsx +21 -3
- package/src/client/components/FileExplorer.tsx +29 -24
- package/src/client/components/GitOperations.tsx +32 -16
- package/src/client/components/PollingStatus.tsx +27 -1
- package/src/client/components/RepositoryConfig.tsx +76 -3
- package/src/client/components/ReviewFlows.tsx +11 -1
- package/src/client/components/ReviewHistory.tsx +14 -1
- package/src/client/components/RunReviewButton.tsx +375 -278
- package/src/client/context/GitManagerContext.tsx +2 -0
- package/src/client/index.tsx +31 -31
- package/src/locale/en-US.json +10 -1
- package/src/locale/vi-VN.json +2 -0
- package/src/server/actions/git-actions.ts +15 -12
- package/src/server/actions/gitlab-api.ts +8 -4
- package/src/server/actions/review.ts +226 -41
- package/src/server/ai-tools.ts +1 -0
- package/src/server/collections/gitCodeReviews.ts +1 -0
- package/src/server/collections/gitRepositories.ts +12 -0
- package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
- package/src/server/plugin.ts +205 -164
- package/src/server/poller.ts +11 -2
- package/dist/client/187.eec7be93247463d7.js +0 -10
|
@@ -32,6 +32,10 @@ __export(gitlab_api_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(gitlab_api_exports);
|
|
34
34
|
var import_gitlab_url = require("../utils/gitlab-url");
|
|
35
|
+
function getActionParams(ctx) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
|
|
38
|
+
}
|
|
35
39
|
async function gitlabFetch(apiBase, endpoint, pat, params) {
|
|
36
40
|
const url = new URL(`${apiBase}${endpoint}`);
|
|
37
41
|
if (params) {
|
|
@@ -57,8 +61,7 @@ async function gitlabFetch(apiBase, endpoint, pat, params) {
|
|
|
57
61
|
return { data, totalPages: totalPages ? parseInt(totalPages, 10) : null, total: total ? parseInt(total, 10) : null };
|
|
58
62
|
}
|
|
59
63
|
async function getRepoApiContext(ctx) {
|
|
60
|
-
|
|
61
|
-
const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
|
|
64
|
+
const params = getActionParams(ctx);
|
|
62
65
|
const { repositoryId } = params;
|
|
63
66
|
const repo = await ctx.db.getRepository("gitRepositories").findOne({
|
|
64
67
|
filterByTk: repositoryId
|
|
@@ -105,9 +108,8 @@ async function githubFetch(endpoint, pat, params) {
|
|
|
105
108
|
return { data, totalPages, total: null };
|
|
106
109
|
}
|
|
107
110
|
async function mergeRequests(ctx, next) {
|
|
108
|
-
var _a;
|
|
109
111
|
const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
|
|
110
|
-
const params =
|
|
112
|
+
const params = getActionParams(ctx);
|
|
111
113
|
const {
|
|
112
114
|
state = "opened",
|
|
113
115
|
search,
|
|
@@ -138,14 +140,14 @@ async function mergeRequests(ctx, next) {
|
|
|
138
140
|
result.total = null;
|
|
139
141
|
}
|
|
140
142
|
items = pullRequests.map((pr) => {
|
|
141
|
-
var
|
|
143
|
+
var _a, _b;
|
|
142
144
|
return {
|
|
143
145
|
id: pr.id,
|
|
144
146
|
iid: pr.number,
|
|
145
147
|
title: pr.title,
|
|
146
148
|
description: pr.body,
|
|
147
149
|
state: pr.state === "open" ? "opened" : pr.merged_at ? "merged" : "closed",
|
|
148
|
-
sourceBranch: (
|
|
150
|
+
sourceBranch: (_a = pr.head) == null ? void 0 : _a.ref,
|
|
149
151
|
targetBranch: (_b = pr.base) == null ? void 0 : _b.ref,
|
|
150
152
|
author: pr.user ? { name: pr.user.login, username: pr.user.login, avatarUrl: pr.user.avatar_url } : null,
|
|
151
153
|
assignees: (pr.assignees || []).map((a) => ({ name: a.login, username: a.login, avatarUrl: a.avatar_url })),
|
|
@@ -215,9 +217,9 @@ async function mergeRequests(ctx, next) {
|
|
|
215
217
|
await next();
|
|
216
218
|
}
|
|
217
219
|
async function mergeRequestDetail(ctx, next) {
|
|
218
|
-
var _a, _b, _c
|
|
220
|
+
var _a, _b, _c;
|
|
219
221
|
const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
|
|
220
|
-
const params =
|
|
222
|
+
const params = getActionParams(ctx);
|
|
221
223
|
const { mrIid } = params;
|
|
222
224
|
if (!mrIid) {
|
|
223
225
|
ctx.throw(400, "mrIid is required");
|
|
@@ -243,8 +245,8 @@ async function mergeRequestDetail(ctx, next) {
|
|
|
243
245
|
title: pr.title,
|
|
244
246
|
description: pr.body,
|
|
245
247
|
state: pr.state === "open" ? "opened" : pr.merged_at ? "merged" : "closed",
|
|
246
|
-
sourceBranch: (
|
|
247
|
-
targetBranch: (
|
|
248
|
+
sourceBranch: (_a = pr.head) == null ? void 0 : _a.ref,
|
|
249
|
+
targetBranch: (_b = pr.base) == null ? void 0 : _b.ref,
|
|
248
250
|
author: pr.user ? { name: pr.user.login, username: pr.user.login, avatarUrl: pr.user.avatar_url } : null,
|
|
249
251
|
assignees: (pr.assignees || []).map((a) => ({ name: a.login, username: a.login, avatarUrl: a.avatar_url })),
|
|
250
252
|
reviewers: (pr.requested_reviewers || []).map((r) => ({ name: r.login, username: r.login, avatarUrl: r.avatar_url })),
|
|
@@ -276,7 +278,7 @@ async function mergeRequestDetail(ctx, next) {
|
|
|
276
278
|
gitlabFetch(apiBase, `/projects/${encodedProject}/merge_requests/${mrIid}/changes`, pat).catch(() => ({ data: {} }))
|
|
277
279
|
]);
|
|
278
280
|
const mr = mrResult.data;
|
|
279
|
-
const changes = (((
|
|
281
|
+
const changes = (((_c = changesResult.data) == null ? void 0 : _c.changes) || []).map((c) => ({
|
|
280
282
|
oldPath: c.old_path,
|
|
281
283
|
newPath: c.new_path,
|
|
282
284
|
newFile: c.new_file,
|
|
@@ -318,9 +320,8 @@ async function mergeRequestDetail(ctx, next) {
|
|
|
318
320
|
await next();
|
|
319
321
|
}
|
|
320
322
|
async function mergeRequestNotes(ctx, next) {
|
|
321
|
-
var _a;
|
|
322
323
|
const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
|
|
323
|
-
const params =
|
|
324
|
+
const params = getActionParams(ctx);
|
|
324
325
|
const { mrIid, page = 1, perPage = 50 } = params;
|
|
325
326
|
if (!mrIid) {
|
|
326
327
|
ctx.throw(400, "mrIid is required");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context } from '@nocobase/actions';
|
|
2
2
|
import type { Application } from '@nocobase/server';
|
|
3
|
+
export declare const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
|
|
3
4
|
interface TriggerArgs {
|
|
4
5
|
flowId?: number | null;
|
|
5
6
|
repositoryId: number;
|
|
@@ -14,8 +15,8 @@ interface TriggerArgs {
|
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Trigger an AI-driven code review for an MR / commit / branch.
|
|
17
|
-
* The review record is upserted synchronously, then
|
|
18
|
-
*
|
|
18
|
+
* The review record is upserted synchronously, then queued for an available
|
|
19
|
+
* git-review worker. The action returns immediately with the reviewId.
|
|
19
20
|
*/
|
|
20
21
|
export declare function triggerReview(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
21
22
|
/**
|
|
@@ -23,6 +24,8 @@ export declare function triggerReview(ctx: Context, next: () => Promise<void>):
|
|
|
23
24
|
* Returns the reviewId of the upserted record.
|
|
24
25
|
*/
|
|
25
26
|
export declare function triggerReviewInternal(app: Application, args: TriggerArgs): Promise<number>;
|
|
27
|
+
export declare function registerReviewQueue(app: Application): void;
|
|
28
|
+
export declare function unregisterReviewQueue(app: Application): void;
|
|
26
29
|
/**
|
|
27
30
|
* Mark a review as approved and post its content to GitLab as an MR note.
|
|
28
31
|
*/
|
|
@@ -26,17 +26,34 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
27
|
var review_exports = {};
|
|
28
28
|
__export(review_exports, {
|
|
29
|
+
WORKER_JOB_GIT_REVIEW_PROCESS: () => WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
29
30
|
branchMatches: () => branchMatches,
|
|
30
31
|
pickFlowMatchingBranch: () => pickFlowMatchingBranch,
|
|
31
32
|
recoverStuckReviews: () => recoverStuckReviews,
|
|
33
|
+
registerReviewQueue: () => registerReviewQueue,
|
|
32
34
|
reviewApprovePost: () => reviewApprovePost,
|
|
33
35
|
reviewReject: () => reviewReject,
|
|
34
36
|
triggerReview: () => triggerReview,
|
|
35
|
-
triggerReviewInternal: () => triggerReviewInternal
|
|
37
|
+
triggerReviewInternal: () => triggerReviewInternal,
|
|
38
|
+
unregisterReviewQueue: () => unregisterReviewQueue
|
|
36
39
|
});
|
|
37
40
|
module.exports = __toCommonJS(review_exports);
|
|
38
41
|
var import_gitlab_url = require("../utils/gitlab-url");
|
|
39
42
|
var import_redact = require("../utils/redact");
|
|
43
|
+
const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
|
|
44
|
+
const REVIEW_QUEUE_CHANNEL = "plugin-git-manager.review";
|
|
45
|
+
const REVIEW_QUEUE_CONCURRENCY = Math.max(
|
|
46
|
+
1,
|
|
47
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_CONCURRENCY || process.env.GIT_REVIEW_MAX_CONCURRENCY || "3", 10) || 3
|
|
48
|
+
);
|
|
49
|
+
const REVIEW_QUEUE_TIMEOUT_MS = Math.max(
|
|
50
|
+
6e4,
|
|
51
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_TIMEOUT_MS || "", 10) || 10 * 60 * 1e3
|
|
52
|
+
);
|
|
53
|
+
function getActionParams(ctx) {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
|
|
56
|
+
}
|
|
40
57
|
function targetKey(args) {
|
|
41
58
|
if (args.targetType === "mr") return `${args.repositoryId}:mr:${args.mrIid}`;
|
|
42
59
|
if (args.targetType === "commit") return `${args.repositoryId}:commit:${args.commitSha}`;
|
|
@@ -48,8 +65,8 @@ async function withTriggerLock(app, key, fn) {
|
|
|
48
65
|
return app.lockManager.runExclusive(lockKey, fn, TRIGGER_LOCK_TTL_MS);
|
|
49
66
|
}
|
|
50
67
|
async function triggerReview(ctx, next) {
|
|
51
|
-
var _a, _b
|
|
52
|
-
const params =
|
|
68
|
+
var _a, _b;
|
|
69
|
+
const params = getActionParams(ctx);
|
|
53
70
|
const {
|
|
54
71
|
flowId,
|
|
55
72
|
repositoryId,
|
|
@@ -65,7 +82,7 @@ async function triggerReview(ctx, next) {
|
|
|
65
82
|
if (targetType === "mr" && !mrIid) ctx.throw(400, "mrIid is required for MR review");
|
|
66
83
|
if (targetType === "commit" && !commitSha) ctx.throw(400, "commitSha is required for commit review");
|
|
67
84
|
if (targetType === "branch" && !branch) ctx.throw(400, "branch is required for branch review");
|
|
68
|
-
const userId = (
|
|
85
|
+
const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
|
|
69
86
|
try {
|
|
70
87
|
const reviewId = await triggerReviewInternal(ctx.app, {
|
|
71
88
|
flowId,
|
|
@@ -138,13 +155,12 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
138
155
|
latestSha: headSha || existingLatestSha || null,
|
|
139
156
|
triggeredBy: args.triggeredBy || "manual",
|
|
140
157
|
status: "pending",
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
158
|
+
// `startedAt` is stamped by the queue worker when execution actually
|
|
159
|
+
// starts. Pending rows may be legitimately waiting in Redis.
|
|
160
|
+
startedAt: null,
|
|
145
161
|
finishedAt: null,
|
|
146
162
|
durationMs: null,
|
|
147
|
-
postStatus: flow.
|
|
163
|
+
postStatus: getInitialPostStatus(flow, args.targetType),
|
|
148
164
|
error: null
|
|
149
165
|
};
|
|
150
166
|
let reviewId;
|
|
@@ -162,29 +178,133 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
162
178
|
const review = await reviewsRepo.create({ values: baseValues });
|
|
163
179
|
reviewId = review.get("id");
|
|
164
180
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
await enqueueReview(app, {
|
|
182
|
+
reviewId,
|
|
183
|
+
repositoryId: args.repositoryId,
|
|
184
|
+
targetType: args.targetType,
|
|
185
|
+
mrIid: args.targetType === "mr" ? args.mrIid : null,
|
|
186
|
+
commitSha: args.targetType === "commit" ? args.commitSha : null,
|
|
187
|
+
branch: args.branch || void 0,
|
|
188
|
+
headSha,
|
|
189
|
+
aiEmployeeUsername,
|
|
190
|
+
extraInstructions: args.extraInstructions,
|
|
191
|
+
userId: args.userId ?? null,
|
|
192
|
+
flowSnapshot: createFlowSnapshot(flow)
|
|
193
|
+
});
|
|
194
|
+
return reviewId;
|
|
195
|
+
}
|
|
196
|
+
function registerReviewQueue(app) {
|
|
197
|
+
app.eventQueue.subscribe(REVIEW_QUEUE_CHANNEL, {
|
|
198
|
+
concurrency: REVIEW_QUEUE_CONCURRENCY,
|
|
199
|
+
idle: () => isGitReviewWorker(app),
|
|
200
|
+
process: async (message) => {
|
|
201
|
+
await processQueuedReview(app, message);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function unregisterReviewQueue(app) {
|
|
206
|
+
app.eventQueue.unsubscribe(REVIEW_QUEUE_CHANNEL);
|
|
207
|
+
}
|
|
208
|
+
function createFlowSnapshot(flow) {
|
|
209
|
+
return {
|
|
210
|
+
id: Number(flow.get("id")),
|
|
211
|
+
name: flow.get("name"),
|
|
212
|
+
postMode: flow.get("postMode"),
|
|
213
|
+
llmService: flow.get("llmService"),
|
|
214
|
+
model: flow.get("model"),
|
|
215
|
+
instructions: flow.get("instructions")
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createFlowFromSnapshot(snapshot, fallback) {
|
|
219
|
+
return {
|
|
220
|
+
get(name) {
|
|
221
|
+
var _a;
|
|
222
|
+
if (snapshot && Object.prototype.hasOwnProperty.call(snapshot, name)) {
|
|
223
|
+
return snapshot[name];
|
|
224
|
+
}
|
|
225
|
+
return (_a = fallback == null ? void 0 : fallback.get) == null ? void 0 : _a.call(fallback, name);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function isGitReviewWorker(app) {
|
|
230
|
+
const workerMode = process.env.WORKER_MODE || "";
|
|
231
|
+
return app.serving(WORKER_JOB_GIT_REVIEW_PROCESS) || workerMode === "worker" || workerMode === "task" || process.env.APP_ROLE === "worker";
|
|
232
|
+
}
|
|
233
|
+
async function enqueueReview(app, message) {
|
|
234
|
+
try {
|
|
235
|
+
await app.eventQueue.publish(REVIEW_QUEUE_CHANNEL, message, {
|
|
236
|
+
timeout: REVIEW_QUEUE_TIMEOUT_MS,
|
|
237
|
+
maxRetries: 1
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
241
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
242
|
+
filterByTk: message.reviewId,
|
|
243
|
+
values: {
|
|
244
|
+
status: "failed",
|
|
245
|
+
error: `Failed to enqueue review: ${safeMessage}`,
|
|
246
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function failQueuedReview(app, reviewId, err) {
|
|
253
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
254
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
255
|
+
filterByTk: reviewId,
|
|
256
|
+
values: {
|
|
257
|
+
status: "failed",
|
|
258
|
+
error: safeMessage,
|
|
259
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function processQueuedReview(app, message) {
|
|
264
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
265
|
+
const db = app.db;
|
|
266
|
+
const reviewsRepo = db.getRepository("gitCodeReviews");
|
|
267
|
+
const review = await reviewsRepo.findOne({ filterByTk: message.reviewId });
|
|
268
|
+
if (!review) {
|
|
269
|
+
(_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `git review queue: review ${message.reviewId} not found, skipping`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (review.get("status") !== "pending") {
|
|
273
|
+
(_d = (_c = app.log) == null ? void 0 : _c.info) == null ? void 0 : _d.call(_c, `git review queue: review ${message.reviewId} is ${review.get("status")}, skipping`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const repo = await db.getRepository("gitRepositories").findOne({
|
|
278
|
+
filterByTk: message.repositoryId || review.get("repositoryId")
|
|
279
|
+
});
|
|
280
|
+
if (!repo) throw new Error("Repository not found");
|
|
281
|
+
const storedFlow = await db.getRepository("gitReviewFlows").findOne({
|
|
282
|
+
filterByTk: ((_e = message.flowSnapshot) == null ? void 0 : _e.id) || review.get("flowId")
|
|
283
|
+
});
|
|
284
|
+
const flow = createFlowFromSnapshot(message.flowSnapshot, storedFlow);
|
|
285
|
+
const aiEmployeeUsername = message.aiEmployeeUsername || flow.get("aiEmployeeUsername");
|
|
286
|
+
if (!aiEmployeeUsername) throw new Error("Flow has no AI employee configured");
|
|
287
|
+
await runReview(app, {
|
|
288
|
+
reviewId: message.reviewId,
|
|
168
289
|
flow,
|
|
169
290
|
repo,
|
|
170
|
-
targetType:
|
|
171
|
-
mrIid:
|
|
172
|
-
commitSha:
|
|
173
|
-
branch:
|
|
174
|
-
headSha,
|
|
291
|
+
targetType: message.targetType || review.get("targetType"),
|
|
292
|
+
mrIid: message.targetType === "mr" ? message.mrIid ?? Number(review.get("mrIid")) : null,
|
|
293
|
+
commitSha: message.targetType === "commit" ? message.commitSha || review.get("commitSha") : null,
|
|
294
|
+
branch: message.branch || review.get("branch"),
|
|
295
|
+
headSha: message.headSha || review.get("headSha"),
|
|
175
296
|
aiEmployeeUsername,
|
|
176
|
-
extraInstructions:
|
|
177
|
-
userId:
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return reviewId;
|
|
297
|
+
extraInstructions: message.extraInstructions,
|
|
298
|
+
userId: message.userId ?? null
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
(_g = (_f = app.log) == null ? void 0 : _f.error) == null ? void 0 : _g.call(_f, "git review queue: failed before review execution", err);
|
|
302
|
+
await failQueuedReview(app, message.reviewId, err);
|
|
303
|
+
}
|
|
184
304
|
}
|
|
185
305
|
async function reviewApprovePost(ctx, next) {
|
|
186
|
-
var _a, _b
|
|
187
|
-
const params =
|
|
306
|
+
var _a, _b;
|
|
307
|
+
const params = getActionParams(ctx);
|
|
188
308
|
const { reviewId, editedMarkdown } = params;
|
|
189
309
|
if (!reviewId) ctx.throw(400, "reviewId is required");
|
|
190
310
|
const reviewsRepo = ctx.db.getRepository("gitCodeReviews");
|
|
@@ -199,7 +319,7 @@ async function reviewApprovePost(ctx, next) {
|
|
|
199
319
|
});
|
|
200
320
|
if (!repo) ctx.throw(404, "Repository not found");
|
|
201
321
|
const noteId = await postNoteToGitLab(repo, Number(review.get("mrIid")), markdown);
|
|
202
|
-
const userId = (
|
|
322
|
+
const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
|
|
203
323
|
await reviewsRepo.update({
|
|
204
324
|
filterByTk: reviewId,
|
|
205
325
|
values: {
|
|
@@ -207,21 +327,22 @@ async function reviewApprovePost(ctx, next) {
|
|
|
207
327
|
postStatus: "posted",
|
|
208
328
|
postedNoteId: String(noteId),
|
|
209
329
|
approvedBy: userId ? String(userId) : null,
|
|
210
|
-
approvedAt: /* @__PURE__ */ new Date()
|
|
330
|
+
approvedAt: /* @__PURE__ */ new Date(),
|
|
331
|
+
error: null
|
|
211
332
|
}
|
|
212
333
|
});
|
|
213
334
|
ctx.body = { success: true, data: { reviewId, postedNoteId: noteId } };
|
|
214
335
|
await next();
|
|
215
336
|
}
|
|
216
337
|
async function reviewReject(ctx, next) {
|
|
217
|
-
var _a, _b
|
|
218
|
-
const params =
|
|
338
|
+
var _a, _b;
|
|
339
|
+
const params = getActionParams(ctx);
|
|
219
340
|
const { reviewId, reason } = params;
|
|
220
341
|
if (!reviewId) ctx.throw(400, "reviewId is required");
|
|
221
342
|
const reviewsRepo = ctx.db.getRepository("gitCodeReviews");
|
|
222
343
|
const review = await reviewsRepo.findOne({ filterByTk: reviewId });
|
|
223
344
|
if (!review) ctx.throw(404, "Review not found");
|
|
224
|
-
const userId = (
|
|
345
|
+
const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
|
|
225
346
|
await reviewsRepo.update({
|
|
226
347
|
filterByTk: reviewId,
|
|
227
348
|
values: {
|
|
@@ -353,9 +474,10 @@ async function runReview(app, args) {
|
|
|
353
474
|
}
|
|
354
475
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
355
476
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
356
|
-
const postMode = args.flow
|
|
357
|
-
let postStatus =
|
|
477
|
+
const postMode = getFlowPostMode(args.flow);
|
|
478
|
+
let postStatus = getInitialPostStatus(args.flow, args.targetType);
|
|
358
479
|
let postedNoteId = null;
|
|
480
|
+
let autoPostError = null;
|
|
359
481
|
if (postMode === "disabled") {
|
|
360
482
|
postStatus = "skipped";
|
|
361
483
|
} else if (postMode === "auto" && args.targetType === "mr" && args.mrIid) {
|
|
@@ -363,6 +485,8 @@ async function runReview(app, args) {
|
|
|
363
485
|
postedNoteId = String(await postNoteToGitLab(args.repo, args.mrIid, content));
|
|
364
486
|
postStatus = "posted";
|
|
365
487
|
} catch (err) {
|
|
488
|
+
autoPostError = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
489
|
+
postStatus = "post_failed";
|
|
366
490
|
(_c = (_b = app.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, "Auto-post review note failed", err);
|
|
367
491
|
}
|
|
368
492
|
}
|
|
@@ -380,11 +504,14 @@ async function runReview(app, args) {
|
|
|
380
504
|
finishedAt,
|
|
381
505
|
postStatus,
|
|
382
506
|
postedNoteId,
|
|
507
|
+
error: autoPostError ? `Auto-post failed: ${autoPostError}` : null,
|
|
383
508
|
metadata: {
|
|
384
509
|
flowName: args.flow.get("name"),
|
|
385
510
|
aiEmployeeUsername: args.aiEmployeeUsername,
|
|
386
511
|
llmService,
|
|
387
|
-
model
|
|
512
|
+
model,
|
|
513
|
+
postMode,
|
|
514
|
+
autoPostError
|
|
388
515
|
}
|
|
389
516
|
}
|
|
390
517
|
});
|
|
@@ -555,6 +682,23 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
555
682
|
}
|
|
556
683
|
const MAX_BRANCH_FILTER_LENGTH = 200;
|
|
557
684
|
const loggedBadFilters = /* @__PURE__ */ new Set();
|
|
685
|
+
function getFlowPostMode(flow) {
|
|
686
|
+
var _a;
|
|
687
|
+
const rawValue = (_a = flow == null ? void 0 : flow.get) == null ? void 0 : _a.call(flow, "postMode");
|
|
688
|
+
const value = rawValue && typeof rawValue === "object" && "value" in rawValue ? rawValue.value : rawValue;
|
|
689
|
+
const normalized = String(value || "manual").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
690
|
+
if (["auto", "auto_post", "autopost", "auto_post_to_mr"].includes(normalized)) return "auto";
|
|
691
|
+
if (["disabled", "disable", "do_not_post", "dont_post", "none", "skip", "skipped"].includes(normalized)) {
|
|
692
|
+
return "disabled";
|
|
693
|
+
}
|
|
694
|
+
return "manual";
|
|
695
|
+
}
|
|
696
|
+
function getInitialPostStatus(flow, targetType) {
|
|
697
|
+
const postMode = getFlowPostMode(flow);
|
|
698
|
+
if (postMode === "disabled") return "skipped";
|
|
699
|
+
if (postMode === "auto" && targetType !== "mr") return "skipped";
|
|
700
|
+
return "pending_approval";
|
|
701
|
+
}
|
|
558
702
|
function warnInvalidBranchFilter(filter, reason) {
|
|
559
703
|
if (loggedBadFilters.has(filter)) return;
|
|
560
704
|
loggedBadFilters.add(filter);
|
|
@@ -597,7 +741,7 @@ async function recoverStuckReviews(app) {
|
|
|
597
741
|
const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
|
|
598
742
|
const stuck = await reviewsRepo.find({
|
|
599
743
|
filter: {
|
|
600
|
-
status:
|
|
744
|
+
status: "running",
|
|
601
745
|
startedAt: { $lt: cutoff }
|
|
602
746
|
}
|
|
603
747
|
});
|
|
@@ -625,11 +769,14 @@ async function recoverStuckReviews(app) {
|
|
|
625
769
|
}
|
|
626
770
|
// Annotate the CommonJS export names for ESM import in node:
|
|
627
771
|
0 && (module.exports = {
|
|
772
|
+
WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
628
773
|
branchMatches,
|
|
629
774
|
pickFlowMatchingBranch,
|
|
630
775
|
recoverStuckReviews,
|
|
776
|
+
registerReviewQueue,
|
|
631
777
|
reviewApprovePost,
|
|
632
778
|
reviewReject,
|
|
633
779
|
triggerReview,
|
|
634
|
-
triggerReviewInternal
|
|
780
|
+
triggerReviewInternal,
|
|
781
|
+
unregisterReviewQueue
|
|
635
782
|
});
|
package/dist/server/ai-tools.js
CHANGED
|
@@ -97,12 +97,14 @@ function registerGitReviewAiTools(app) {
|
|
|
97
97
|
return obj;
|
|
98
98
|
};
|
|
99
99
|
const runResourceAction = async (ctx, handler, params, gate) => {
|
|
100
|
+
var _a2;
|
|
100
101
|
enforceAcl(ctx, gate.resource, gate.action, params);
|
|
101
102
|
const synthCtx = {
|
|
102
103
|
...ctx,
|
|
103
104
|
app: ctx.app,
|
|
104
105
|
db: ctx.db,
|
|
105
106
|
action: { params },
|
|
107
|
+
request: { ...ctx.request || {}, body: ((_a2 = ctx.request) == null ? void 0 : _a2.body) || {} },
|
|
106
108
|
throw: (status, message) => {
|
|
107
109
|
const err = new Error(message);
|
|
108
110
|
err.status = status;
|
|
@@ -166,6 +166,7 @@ var gitCodeReviews_default = (0, import_database.defineCollection)({
|
|
|
166
166
|
{ value: "pending_approval", label: "Pending Approval" },
|
|
167
167
|
{ value: "approved", label: "Approved" },
|
|
168
168
|
{ value: "posted", label: "Posted" },
|
|
169
|
+
{ value: "post_failed", label: "Post Failed" },
|
|
169
170
|
{ value: "skipped", label: "Skipped" },
|
|
170
171
|
{ value: "rejected", label: "Rejected" }
|
|
171
172
|
]
|
|
@@ -87,6 +87,18 @@ var gitRepositories_default = (0, import_database.defineCollection)({
|
|
|
87
87
|
interface: "checkbox",
|
|
88
88
|
uiSchema: { title: "Auto Review", type: "boolean", "x-component": "Checkbox" }
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
type: "belongsTo",
|
|
92
|
+
name: "autoReviewFlow",
|
|
93
|
+
target: "gitReviewFlows",
|
|
94
|
+
foreignKey: "autoReviewFlowId",
|
|
95
|
+
interface: "m2o",
|
|
96
|
+
uiSchema: {
|
|
97
|
+
title: "Primary Auto Review Flow",
|
|
98
|
+
"x-component": "AssociationField",
|
|
99
|
+
"x-component-props": { fieldNames: { label: "name", value: "id" } }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
90
102
|
{
|
|
91
103
|
type: "date",
|
|
92
104
|
name: "lastPolledAt",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var add_auto_review_flow_id_exports = {};
|
|
28
|
+
__export(add_auto_review_flow_id_exports, {
|
|
29
|
+
default: () => AddAutoReviewFlowIdMigration
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(add_auto_review_flow_id_exports);
|
|
32
|
+
var import_server = require("@nocobase/server");
|
|
33
|
+
var import_sequelize = require("sequelize");
|
|
34
|
+
class AddAutoReviewFlowIdMigration extends import_server.Migration {
|
|
35
|
+
on = "afterLoad";
|
|
36
|
+
async up() {
|
|
37
|
+
var _a;
|
|
38
|
+
const queryInterface = this.db.sequelize.getQueryInterface();
|
|
39
|
+
const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
|
|
40
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
41
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
42
|
+
if (!tableInfo || tableInfo.autoReviewFlowId) return;
|
|
43
|
+
await queryInterface.addColumn(tableName, "autoReviewFlowId", {
|
|
44
|
+
type: import_sequelize.DataTypes.INTEGER,
|
|
45
|
+
allowNull: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async down() {
|
|
49
|
+
var _a;
|
|
50
|
+
const queryInterface = this.db.sequelize.getQueryInterface();
|
|
51
|
+
const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
|
|
52
|
+
const tableName = `${tablePrefix}gitRepositories`;
|
|
53
|
+
const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
|
|
54
|
+
if (!(tableInfo == null ? void 0 : tableInfo.autoReviewFlowId)) return;
|
|
55
|
+
await queryInterface.removeColumn(tableName, "autoReviewFlowId");
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/server/plugin.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Plugin } from '@nocobase/server';
|
|
2
2
|
export declare class PluginGitManagerServer extends Plugin {
|
|
3
|
+
app: any;
|
|
4
|
+
db: any;
|
|
5
|
+
beforeLoad(): Promise<void>;
|
|
3
6
|
load(): Promise<void>;
|
|
4
7
|
install(): Promise<void>;
|
|
5
8
|
beforeDisable(): Promise<void>;
|
|
6
9
|
beforeUnload(): Promise<void>;
|
|
7
10
|
}
|
|
11
|
+
export declare function ensureAutoReviewFlowSchema(app: any): Promise<void>;
|
|
8
12
|
export default PluginGitManagerServer;
|