@spaceflow/review 0.69.0 → 0.71.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.
- package/CHANGELOG.md +23 -0
- package/dist/index.js +95 -17
- package/package.json +2 -2
- package/src/review-spec/types.ts +2 -0
- package/src/review.config.ts +14 -0
- package/src/review.service.ts +130 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.70.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.69.0...@spaceflow/review@0.70.0) (2026-04-02)
|
|
4
|
+
|
|
5
|
+
### 新特性
|
|
6
|
+
|
|
7
|
+
* **review:** 新增自动批准功能,当所有问题解决时自动提交 APPROVE review ([4155342](https://github.com/Lydanne/spaceflow/commit/4155342511f76d6cd9001a14a144fce55a7a8946))
|
|
8
|
+
* **review:** 添加 workflow 重复运行检测,防止同一 PR 并发审查 ([a3777af](https://github.com/Lydanne/spaceflow/commit/a3777affb10b6fb4f88a83660ce2a81f9f2d6498))
|
|
9
|
+
|
|
10
|
+
### 其他修改
|
|
11
|
+
|
|
12
|
+
* **core:** released version 0.27.0 [no ci] ([053d624](https://github.com/Lydanne/spaceflow/commit/053d624997b04e47537dac4ca0c7a99ed4a4cd36))
|
|
13
|
+
* **publish:** released version 0.51.0 [no ci] ([1fbe22a](https://github.com/Lydanne/spaceflow/commit/1fbe22a538cdcb6264e3937c5706d75f67fa4f2a))
|
|
14
|
+
* **review-summary:** released version 0.37.0 [no ci] ([455c581](https://github.com/Lydanne/spaceflow/commit/455c58140def453d95c8fc85b1ea8fde39037abc))
|
|
15
|
+
|
|
16
|
+
## [0.69.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.68.0...@spaceflow/review@0.69.0) (2026-03-04)
|
|
17
|
+
|
|
18
|
+
### 代码重构
|
|
19
|
+
|
|
20
|
+
* **review:** 优化问题统计逻辑,区分 fixed 和 resolved 状态 ([c4dda30](https://github.com/Lydanne/spaceflow/commit/c4dda30fed17ce020fcce9af8874dfa89ccca20b))
|
|
21
|
+
|
|
22
|
+
### 其他修改
|
|
23
|
+
|
|
24
|
+
* **review-summary:** released version 0.36.0 [no ci] ([95c3d5c](https://github.com/Lydanne/spaceflow/commit/95c3d5cbac67c1ffa4c821faaffb476c502fe2c7))
|
|
25
|
+
|
|
3
26
|
## [0.68.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.67.0...@spaceflow/review@0.68.0) (2026-03-04)
|
|
4
27
|
|
|
5
28
|
### 代码重构
|
package/dist/index.js
CHANGED
|
@@ -1630,7 +1630,9 @@ class ReviewReportService {
|
|
|
1630
1630
|
timeout: z.number().optional(),
|
|
1631
1631
|
retries: z.number().default(0).optional(),
|
|
1632
1632
|
retryDelay: z.number().default(1000).optional(),
|
|
1633
|
-
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional()
|
|
1633
|
+
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
|
1634
|
+
skipDuplicateWorkflow: z.boolean().default(false).optional(),
|
|
1635
|
+
autoApprove: z.boolean().default(false).optional()
|
|
1634
1636
|
});
|
|
1635
1637
|
|
|
1636
1638
|
;// CONCATENATED MODULE: ./src/parse-title-options.ts
|
|
@@ -2022,7 +2024,9 @@ class ReviewService {
|
|
|
2022
2024
|
showAll: options.showAll ?? false,
|
|
2023
2025
|
flush: options.flush ?? false,
|
|
2024
2026
|
eventAction: options.eventAction,
|
|
2025
|
-
localMode
|
|
2027
|
+
localMode,
|
|
2028
|
+
skipDuplicateWorkflow: options.skipDuplicateWorkflow ?? reviewConf.skipDuplicateWorkflow ?? false,
|
|
2029
|
+
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false
|
|
2026
2030
|
};
|
|
2027
2031
|
}
|
|
2028
2032
|
/**
|
|
@@ -2116,7 +2120,7 @@ class ReviewService {
|
|
|
2116
2120
|
* @param context 审查上下文,包含 owner、repo、prNumber 等信息
|
|
2117
2121
|
* @returns 审查结果,包含发现的问题列表和统计信息
|
|
2118
2122
|
*/ async execute(context) {
|
|
2119
|
-
const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode } = context;
|
|
2123
|
+
const { owner, repo, prNumber, baseRef, headRef, specSources, dryRun, ci, verbose, includes, llmMode, files, commits: filterCommits, deletionOnly, localMode, skipDuplicateWorkflow, autoApprove } = context;
|
|
2120
2124
|
// 直接审查文件模式:指定了 -f 文件且 base=head
|
|
2121
2125
|
const isDirectFileMode = files && files.length > 0 && baseRef === headRef;
|
|
2122
2126
|
// 本地模式:审查未提交的代码(可能回退到分支比较)
|
|
@@ -2203,6 +2207,37 @@ class ReviewService {
|
|
|
2203
2207
|
console.log(` Commits: ${commits.length}`);
|
|
2204
2208
|
console.log(` Changed files: ${changedFiles.length}`);
|
|
2205
2209
|
}
|
|
2210
|
+
// 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
|
|
2211
|
+
// 需要显式启用 skipDuplicateWorkflow 配置
|
|
2212
|
+
if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
|
|
2213
|
+
const headSha = pr.head.sha;
|
|
2214
|
+
// 获取当前 PR 编号(从 CI 环境变量)
|
|
2215
|
+
// GitHub: GITHUB_REF = refs/pull/123/merge
|
|
2216
|
+
// Gitea: GITEA_REF = refs/pull/123/head
|
|
2217
|
+
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
2218
|
+
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
2219
|
+
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
|
|
2220
|
+
const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
|
|
2221
|
+
status: "in_progress"
|
|
2222
|
+
});
|
|
2223
|
+
// 获取当前 workflow 名称和 run ID
|
|
2224
|
+
const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
|
|
2225
|
+
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
2226
|
+
// 只检查同 PR 同名的其他 workflow run(排除当前 run)
|
|
2227
|
+
const duplicateReviewRuns = runningWorkflows.filter((w)=>w.sha === headSha && w.name === currentWorkflowName && (!currentRunId || String(w.id) !== currentRunId));
|
|
2228
|
+
if (duplicateReviewRuns.length > 0) {
|
|
2229
|
+
if (shouldLog(verbose, 1)) {
|
|
2230
|
+
console.log(`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`);
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
success: true,
|
|
2234
|
+
description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
|
|
2235
|
+
issues: [],
|
|
2236
|
+
summary: [],
|
|
2237
|
+
round: 1
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2206
2241
|
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
2207
2242
|
// 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
|
|
2208
2243
|
if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
|
|
@@ -2449,7 +2484,7 @@ class ReviewService {
|
|
|
2449
2484
|
await this.postOrUpdateReviewComment(owner, repo, prNumber, {
|
|
2450
2485
|
...result,
|
|
2451
2486
|
issues: allIssues
|
|
2452
|
-
}, verbose);
|
|
2487
|
+
}, verbose, autoApprove);
|
|
2453
2488
|
if (shouldLog(verbose, 1)) {
|
|
2454
2489
|
console.log(`✅ 评论已提交`);
|
|
2455
2490
|
}
|
|
@@ -2496,7 +2531,7 @@ class ReviewService {
|
|
|
2496
2531
|
* 仅收集 review 状态模式(用于 PR 关闭或 --flush 指令)
|
|
2497
2532
|
* 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
|
|
2498
2533
|
*/ async executeCollectOnly(context) {
|
|
2499
|
-
const { owner, repo, prNumber, verbose, ci, dryRun } = context;
|
|
2534
|
+
const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
|
|
2500
2535
|
if (shouldLog(verbose, 1)) {
|
|
2501
2536
|
console.log(`📊 仅收集 review 状态模式`);
|
|
2502
2537
|
}
|
|
@@ -2541,7 +2576,7 @@ class ReviewService {
|
|
|
2541
2576
|
if (shouldLog(verbose, 1)) {
|
|
2542
2577
|
console.log(`💬 更新 PR 评论...`);
|
|
2543
2578
|
}
|
|
2544
|
-
await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose);
|
|
2579
|
+
await this.postOrUpdateReviewComment(owner, repo, prNumber, existingResult, verbose, autoApprove);
|
|
2545
2580
|
if (shouldLog(verbose, 1)) {
|
|
2546
2581
|
console.log(`✅ 评论已更新`);
|
|
2547
2582
|
}
|
|
@@ -2621,11 +2656,12 @@ class ReviewService {
|
|
|
2621
2656
|
const fixed = validIssue.filter((i)=>i.fixed).length;
|
|
2622
2657
|
const resolved = validIssue.filter((i)=>i.resolved).length;
|
|
2623
2658
|
const invalid = total - validTotal;
|
|
2624
|
-
const pending = validTotal - fixed;
|
|
2659
|
+
const pending = validTotal - fixed - resolved;
|
|
2625
2660
|
const fixRate = validTotal > 0 ? Math.round(fixed / validTotal * 100 * 10) / 10 : 0;
|
|
2626
2661
|
const resolveRate = validTotal > 0 ? Math.round(resolved / validTotal * 100 * 10) / 10 : 0;
|
|
2627
2662
|
return {
|
|
2628
2663
|
total,
|
|
2664
|
+
validTotal,
|
|
2629
2665
|
fixed,
|
|
2630
2666
|
resolved,
|
|
2631
2667
|
invalid,
|
|
@@ -2637,7 +2673,7 @@ class ReviewService {
|
|
|
2637
2673
|
/**
|
|
2638
2674
|
* 仅执行删除代码分析模式
|
|
2639
2675
|
*/ async executeDeletionOnly(context) {
|
|
2640
|
-
const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } = context;
|
|
2676
|
+
const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } = context;
|
|
2641
2677
|
if (shouldLog(verbose, 1)) {
|
|
2642
2678
|
console.log(`🗑️ 仅执行删除代码分析模式`);
|
|
2643
2679
|
}
|
|
@@ -2688,7 +2724,7 @@ class ReviewService {
|
|
|
2688
2724
|
if (shouldLog(verbose, 1)) {
|
|
2689
2725
|
console.log(`💬 提交 PR 评论...`);
|
|
2690
2726
|
}
|
|
2691
|
-
await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
|
|
2727
|
+
await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
|
|
2692
2728
|
if (shouldLog(verbose, 1)) {
|
|
2693
2729
|
console.log(`✅ 评论已提交`);
|
|
2694
2730
|
}
|
|
@@ -3425,7 +3461,7 @@ ${fileChanges || "无"}`;
|
|
|
3425
3461
|
}
|
|
3426
3462
|
return this.reviewReportService.format(result, format);
|
|
3427
3463
|
}
|
|
3428
|
-
async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose) {
|
|
3464
|
+
async postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove) {
|
|
3429
3465
|
// 获取配置
|
|
3430
3466
|
const reviewConf = this.config.getPluginConfig("review");
|
|
3431
3467
|
// 如果配置启用且有 AI 生成的标题,只在第一轮审查时更新 PR 标题
|
|
@@ -3490,29 +3526,42 @@ ${fileChanges || "无"}`;
|
|
|
3490
3526
|
console.warn("⚠️ 发布/更新 AI Review 评论失败:", error);
|
|
3491
3527
|
}
|
|
3492
3528
|
// 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
|
|
3529
|
+
// 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
|
|
3493
3530
|
let lineIssues = [];
|
|
3494
3531
|
let comments = [];
|
|
3495
3532
|
if (reviewConf.lineComments) {
|
|
3496
3533
|
lineIssues = result.issues.filter((issue)=>issue.round === result.round && !issue.fixed && !issue.resolved && issue.valid !== "false");
|
|
3497
3534
|
comments = lineIssues.map((issue)=>this.issueToReviewComment(issue)).filter((comment)=>comment !== null);
|
|
3498
3535
|
}
|
|
3536
|
+
// 计算是否需要自动批准
|
|
3537
|
+
// 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
|
|
3538
|
+
const stats = this.calculateIssueStats(result.issues);
|
|
3539
|
+
const shouldAutoApprove = autoApprove && stats.pending === 0;
|
|
3499
3540
|
if (reviewConf.lineComments) {
|
|
3500
|
-
const
|
|
3541
|
+
const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
|
|
3542
|
+
// 如果需要自动批准,追加批准信息到 body
|
|
3543
|
+
const finalReviewBody = shouldAutoApprove ? lineReviewBody + `\n\n---\n\n✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。` : lineReviewBody;
|
|
3544
|
+
const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
|
|
3501
3545
|
if (comments.length > 0) {
|
|
3502
3546
|
try {
|
|
3503
3547
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
3504
|
-
event:
|
|
3505
|
-
body:
|
|
3548
|
+
event: reviewEvent,
|
|
3549
|
+
body: finalReviewBody,
|
|
3506
3550
|
comments,
|
|
3507
3551
|
commit_id: commitId
|
|
3508
3552
|
});
|
|
3509
|
-
|
|
3553
|
+
if (shouldAutoApprove) {
|
|
3554
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
3555
|
+
} else {
|
|
3556
|
+
console.log(`✅ 已发布 ${comments.length} 条行级评论`);
|
|
3557
|
+
}
|
|
3510
3558
|
} catch {
|
|
3511
3559
|
// 批量失败时逐条发布,跳过无法定位的评论
|
|
3512
3560
|
console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
|
|
3513
3561
|
let successCount = 0;
|
|
3514
3562
|
for (const comment of comments){
|
|
3515
3563
|
try {
|
|
3564
|
+
// 逐条发布时只用 COMMENT event,避免重复 APPROVE
|
|
3516
3565
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
3517
3566
|
event: REVIEW_STATE.COMMENT,
|
|
3518
3567
|
body: successCount === 0 ? reviewBody : undefined,
|
|
@@ -3528,6 +3577,19 @@ ${fileChanges || "无"}`;
|
|
|
3528
3577
|
}
|
|
3529
3578
|
if (successCount > 0) {
|
|
3530
3579
|
console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
|
|
3580
|
+
// 如果需要自动批准,单独发一个 APPROVE review
|
|
3581
|
+
if (shouldAutoApprove) {
|
|
3582
|
+
try {
|
|
3583
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
3584
|
+
event: REVIEW_STATE.APPROVE,
|
|
3585
|
+
body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
|
|
3586
|
+
commit_id: commitId
|
|
3587
|
+
});
|
|
3588
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
3589
|
+
} catch (error) {
|
|
3590
|
+
console.warn("⚠️ 自动批准失败:", error);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3531
3593
|
} else {
|
|
3532
3594
|
console.warn("⚠️ 所有行级评论均无法定位,已跳过");
|
|
3533
3595
|
}
|
|
@@ -3536,16 +3598,32 @@ ${fileChanges || "无"}`;
|
|
|
3536
3598
|
// 本轮无新问题,仍发布 Round 状态(含上轮回顾)
|
|
3537
3599
|
try {
|
|
3538
3600
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
3539
|
-
event:
|
|
3540
|
-
body:
|
|
3601
|
+
event: reviewEvent,
|
|
3602
|
+
body: finalReviewBody,
|
|
3541
3603
|
comments: [],
|
|
3542
3604
|
commit_id: commitId
|
|
3543
3605
|
});
|
|
3544
|
-
|
|
3606
|
+
if (shouldAutoApprove) {
|
|
3607
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
|
|
3608
|
+
} else {
|
|
3609
|
+
console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
|
|
3610
|
+
}
|
|
3545
3611
|
} catch (error) {
|
|
3546
3612
|
console.warn("⚠️ 发布审查状态失败:", error);
|
|
3547
3613
|
}
|
|
3548
3614
|
}
|
|
3615
|
+
} else if (shouldAutoApprove) {
|
|
3616
|
+
// 未启用 lineComments 但需要自动批准
|
|
3617
|
+
try {
|
|
3618
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
3619
|
+
event: REVIEW_STATE.APPROVE,
|
|
3620
|
+
body: `✅ **自动批准合并**\n\n${stats.validTotal > 0 ? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),` : "代码审查通过,未发现问题,"}自动批准此 PR。`,
|
|
3621
|
+
commit_id: commitId
|
|
3622
|
+
});
|
|
3623
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
console.warn("⚠️ 自动批准失败:", error);
|
|
3626
|
+
}
|
|
3549
3627
|
}
|
|
3550
3628
|
}
|
|
3551
3629
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spaceflow/review",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "Spaceflow 代码审查插件,使用 LLM 对 PR 代码进行自动审查",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Lydanne",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@spaceflow/cli": "0.40.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@spaceflow/core": "0.
|
|
31
|
+
"@spaceflow/core": "0.27.0"
|
|
32
32
|
},
|
|
33
33
|
"spaceflow": {
|
|
34
34
|
"type": "flow",
|
package/src/review-spec/types.ts
CHANGED
package/src/review.config.ts
CHANGED
|
@@ -79,6 +79,18 @@ export interface ReviewOptions {
|
|
|
79
79
|
* 在非 CI 和非 PR 模式下默认为 'uncommitted'
|
|
80
80
|
*/
|
|
81
81
|
local?: LocalReviewMode;
|
|
82
|
+
/**
|
|
83
|
+
* 跳过重复的 review workflow 检查
|
|
84
|
+
* - true: 启用检查,当检测到同名 workflow 正在运行时跳过审查
|
|
85
|
+
* - false: 禁用检查(默认)
|
|
86
|
+
*/
|
|
87
|
+
skipDuplicateWorkflow?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* 自动批准合并
|
|
90
|
+
* - true: 当所有问题都已解决时,自动提交 APPROVE review
|
|
91
|
+
* - false: 不自动批准(默认)
|
|
92
|
+
*/
|
|
93
|
+
autoApprove?: boolean;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/** review 命令配置 schema(LLM 敏感配置由系统 llm.config.ts 管理) */
|
|
@@ -100,6 +112,8 @@ export const reviewSchema = () =>
|
|
|
100
112
|
retries: z.number().default(0).optional(),
|
|
101
113
|
retryDelay: z.number().default(1000).optional(),
|
|
102
114
|
invalidateChangedFiles: invalidateChangedFilesSchema.default("invalidate").optional(),
|
|
115
|
+
skipDuplicateWorkflow: z.boolean().default(false).optional(),
|
|
116
|
+
autoApprove: z.boolean().default(false).optional(),
|
|
103
117
|
});
|
|
104
118
|
|
|
105
119
|
/** review 配置类型(从 schema 推导) */
|
package/src/review.service.ts
CHANGED
|
@@ -306,6 +306,9 @@ export class ReviewService {
|
|
|
306
306
|
flush: options.flush ?? false,
|
|
307
307
|
eventAction: options.eventAction,
|
|
308
308
|
localMode,
|
|
309
|
+
skipDuplicateWorkflow:
|
|
310
|
+
options.skipDuplicateWorkflow ?? reviewConf.skipDuplicateWorkflow ?? false,
|
|
311
|
+
autoApprove: options.autoApprove ?? reviewConf.autoApprove ?? false,
|
|
309
312
|
};
|
|
310
313
|
}
|
|
311
314
|
|
|
@@ -433,6 +436,8 @@ export class ReviewService {
|
|
|
433
436
|
commits: filterCommits,
|
|
434
437
|
deletionOnly,
|
|
435
438
|
localMode,
|
|
439
|
+
skipDuplicateWorkflow,
|
|
440
|
+
autoApprove,
|
|
436
441
|
} = context;
|
|
437
442
|
|
|
438
443
|
// 直接审查文件模式:指定了 -f 文件且 base=head
|
|
@@ -532,6 +537,46 @@ export class ReviewService {
|
|
|
532
537
|
console.log(` Commits: ${commits.length}`);
|
|
533
538
|
console.log(` Changed files: ${changedFiles.length}`);
|
|
534
539
|
}
|
|
540
|
+
|
|
541
|
+
// 检查是否有其他同名 review workflow 正在运行中(防止同一 PR 重复审查)
|
|
542
|
+
// 需要显式启用 skipDuplicateWorkflow 配置
|
|
543
|
+
if (skipDuplicateWorkflow && ci && pr?.head?.sha) {
|
|
544
|
+
const headSha = pr.head.sha;
|
|
545
|
+
// 获取当前 PR 编号(从 CI 环境变量)
|
|
546
|
+
// GitHub: GITHUB_REF = refs/pull/123/merge
|
|
547
|
+
// Gitea: GITEA_REF = refs/pull/123/head
|
|
548
|
+
const ref = process.env.GITHUB_REF || process.env.GITEA_REF || "";
|
|
549
|
+
const prMatch = ref.match(/refs\/pull\/(\d+)/);
|
|
550
|
+
const currentPrNumber = prMatch ? parseInt(prMatch[1], 10) : prNumber;
|
|
551
|
+
|
|
552
|
+
const runningWorkflows = await this.gitProvider.listWorkflowRuns(owner, repo, {
|
|
553
|
+
status: "in_progress",
|
|
554
|
+
});
|
|
555
|
+
// 获取当前 workflow 名称和 run ID
|
|
556
|
+
const currentWorkflowName = process.env.GITHUB_WORKFLOW || process.env.GITEA_WORKFLOW;
|
|
557
|
+
const currentRunId = process.env.GITHUB_RUN_ID || process.env.GITEA_RUN_ID;
|
|
558
|
+
// 只检查同 PR 同名的其他 workflow run(排除当前 run)
|
|
559
|
+
const duplicateReviewRuns = runningWorkflows.filter(
|
|
560
|
+
(w) =>
|
|
561
|
+
w.sha === headSha &&
|
|
562
|
+
w.name === currentWorkflowName &&
|
|
563
|
+
(!currentRunId || String(w.id) !== currentRunId),
|
|
564
|
+
);
|
|
565
|
+
if (duplicateReviewRuns.length > 0) {
|
|
566
|
+
if (shouldLog(verbose, 1)) {
|
|
567
|
+
console.log(
|
|
568
|
+
`⏭️ 跳过审查: 当前 PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
success: true,
|
|
573
|
+
description: `跳过审查: PR #${currentPrNumber} 有 ${duplicateReviewRuns.length} 个同名 workflow 正在运行中,等待完成后重新审查`,
|
|
574
|
+
issues: [],
|
|
575
|
+
summary: [],
|
|
576
|
+
round: 1,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
535
580
|
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
536
581
|
// 如果指定了 -f 文件且 base=head(无差异模式),直接审查指定文件
|
|
537
582
|
if (files && files.length > 0 && effectiveBaseRef === effectiveHeadRef) {
|
|
@@ -856,6 +901,7 @@ export class ReviewService {
|
|
|
856
901
|
issues: allIssues,
|
|
857
902
|
},
|
|
858
903
|
verbose,
|
|
904
|
+
autoApprove,
|
|
859
905
|
);
|
|
860
906
|
if (shouldLog(verbose, 1)) {
|
|
861
907
|
console.log(`✅ 评论已提交`);
|
|
@@ -916,7 +962,7 @@ export class ReviewService {
|
|
|
916
962
|
* 从现有的 AI review 评论中读取问题状态,同步已解决/无效状态,输出统计信息
|
|
917
963
|
*/
|
|
918
964
|
protected async executeCollectOnly(context: ReviewContext): Promise<ReviewResult> {
|
|
919
|
-
const { owner, repo, prNumber, verbose, ci, dryRun } = context;
|
|
965
|
+
const { owner, repo, prNumber, verbose, ci, dryRun, autoApprove } = context;
|
|
920
966
|
|
|
921
967
|
if (shouldLog(verbose, 1)) {
|
|
922
968
|
console.log(`📊 仅收集 review 状态模式`);
|
|
@@ -982,7 +1028,14 @@ export class ReviewService {
|
|
|
982
1028
|
if (shouldLog(verbose, 1)) {
|
|
983
1029
|
console.log(`💬 更新 PR 评论...`);
|
|
984
1030
|
}
|
|
985
|
-
await this.postOrUpdateReviewComment(
|
|
1031
|
+
await this.postOrUpdateReviewComment(
|
|
1032
|
+
owner,
|
|
1033
|
+
repo,
|
|
1034
|
+
prNumber,
|
|
1035
|
+
existingResult,
|
|
1036
|
+
verbose,
|
|
1037
|
+
autoApprove,
|
|
1038
|
+
);
|
|
986
1039
|
if (shouldLog(verbose, 1)) {
|
|
987
1040
|
console.log(`✅ 评论已更新`);
|
|
988
1041
|
}
|
|
@@ -1099,17 +1152,18 @@ export class ReviewService {
|
|
|
1099
1152
|
const fixed = validIssue.filter((i) => i.fixed).length;
|
|
1100
1153
|
const resolved = validIssue.filter((i) => i.resolved).length;
|
|
1101
1154
|
const invalid = total - validTotal;
|
|
1102
|
-
const pending = validTotal - fixed;
|
|
1155
|
+
const pending = validTotal - fixed - resolved;
|
|
1103
1156
|
const fixRate = validTotal > 0 ? Math.round((fixed / validTotal) * 100 * 10) / 10 : 0;
|
|
1104
1157
|
const resolveRate = validTotal > 0 ? Math.round((resolved / validTotal) * 100 * 10) / 10 : 0;
|
|
1105
|
-
return { total, fixed, resolved, invalid, pending, fixRate, resolveRate };
|
|
1158
|
+
return { total, validTotal, fixed, resolved, invalid, pending, fixRate, resolveRate };
|
|
1106
1159
|
}
|
|
1107
1160
|
|
|
1108
1161
|
/**
|
|
1109
1162
|
* 仅执行删除代码分析模式
|
|
1110
1163
|
*/
|
|
1111
1164
|
protected async executeDeletionOnly(context: ReviewContext): Promise<ReviewResult> {
|
|
1112
|
-
const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode } =
|
|
1165
|
+
const { owner, repo, prNumber, baseRef, headRef, dryRun, ci, verbose, llmMode, autoApprove } =
|
|
1166
|
+
context;
|
|
1113
1167
|
|
|
1114
1168
|
if (shouldLog(verbose, 1)) {
|
|
1115
1169
|
console.log(`🗑️ 仅执行删除代码分析模式`);
|
|
@@ -1174,7 +1228,7 @@ export class ReviewService {
|
|
|
1174
1228
|
if (shouldLog(verbose, 1)) {
|
|
1175
1229
|
console.log(`💬 提交 PR 评论...`);
|
|
1176
1230
|
}
|
|
1177
|
-
await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose);
|
|
1231
|
+
await this.postOrUpdateReviewComment(owner, repo, prNumber, result, verbose, autoApprove);
|
|
1178
1232
|
if (shouldLog(verbose, 1)) {
|
|
1179
1233
|
console.log(`✅ 评论已提交`);
|
|
1180
1234
|
}
|
|
@@ -2063,6 +2117,7 @@ ${fileChanges || "无"}`;
|
|
|
2063
2117
|
prNumber: number,
|
|
2064
2118
|
result: ReviewResult,
|
|
2065
2119
|
verbose?: VerboseLevel,
|
|
2120
|
+
autoApprove?: boolean,
|
|
2066
2121
|
): Promise<void> {
|
|
2067
2122
|
// 获取配置
|
|
2068
2123
|
const reviewConf = this.config.getPluginConfig<ReviewConfig>("review");
|
|
@@ -2137,6 +2192,7 @@ ${fileChanges || "无"}`;
|
|
|
2137
2192
|
}
|
|
2138
2193
|
|
|
2139
2194
|
// 2. 发布本轮新发现的行级评论(使用 PR Review API,不删除旧的 review,保留历史)
|
|
2195
|
+
// 如果启用 autoApprove 且所有问题已解决,使用 APPROVE event 合并发布
|
|
2140
2196
|
let lineIssues: ReviewIssue[] = [];
|
|
2141
2197
|
let comments: CreatePullReviewComment[] = [];
|
|
2142
2198
|
if (reviewConf.lineComments) {
|
|
@@ -2151,23 +2207,47 @@ ${fileChanges || "无"}`;
|
|
|
2151
2207
|
.map((issue) => this.issueToReviewComment(issue))
|
|
2152
2208
|
.filter((comment): comment is CreatePullReviewComment => comment !== null);
|
|
2153
2209
|
}
|
|
2210
|
+
|
|
2211
|
+
// 计算是否需要自动批准
|
|
2212
|
+
// 条件:启用 autoApprove 且没有待处理问题(包括从未发现问题的情况)
|
|
2213
|
+
const stats = this.calculateIssueStats(result.issues);
|
|
2214
|
+
const shouldAutoApprove = autoApprove && stats.pending === 0;
|
|
2215
|
+
|
|
2154
2216
|
if (reviewConf.lineComments) {
|
|
2155
|
-
const
|
|
2217
|
+
const lineReviewBody = this.buildLineReviewBody(lineIssues, result.round, result.issues);
|
|
2218
|
+
|
|
2219
|
+
// 如果需要自动批准,追加批准信息到 body
|
|
2220
|
+
const finalReviewBody = shouldAutoApprove
|
|
2221
|
+
? lineReviewBody +
|
|
2222
|
+
`\n\n---\n\n✅ **自动批准合并**\n\n${
|
|
2223
|
+
stats.validTotal > 0
|
|
2224
|
+
? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
|
|
2225
|
+
: "代码审查通过,未发现问题,"
|
|
2226
|
+
}自动批准此 PR。`
|
|
2227
|
+
: lineReviewBody;
|
|
2228
|
+
|
|
2229
|
+
const reviewEvent = shouldAutoApprove ? REVIEW_STATE.APPROVE : REVIEW_STATE.COMMENT;
|
|
2230
|
+
|
|
2156
2231
|
if (comments.length > 0) {
|
|
2157
2232
|
try {
|
|
2158
2233
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2159
|
-
event:
|
|
2160
|
-
body:
|
|
2234
|
+
event: reviewEvent,
|
|
2235
|
+
body: finalReviewBody,
|
|
2161
2236
|
comments,
|
|
2162
2237
|
commit_id: commitId,
|
|
2163
2238
|
});
|
|
2164
|
-
|
|
2239
|
+
if (shouldAutoApprove) {
|
|
2240
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
2241
|
+
} else {
|
|
2242
|
+
console.log(`✅ 已发布 ${comments.length} 条行级评论`);
|
|
2243
|
+
}
|
|
2165
2244
|
} catch {
|
|
2166
2245
|
// 批量失败时逐条发布,跳过无法定位的评论
|
|
2167
2246
|
console.warn("⚠️ 批量发布行级评论失败,尝试逐条发布...");
|
|
2168
2247
|
let successCount = 0;
|
|
2169
2248
|
for (const comment of comments) {
|
|
2170
2249
|
try {
|
|
2250
|
+
// 逐条发布时只用 COMMENT event,避免重复 APPROVE
|
|
2171
2251
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2172
2252
|
event: REVIEW_STATE.COMMENT,
|
|
2173
2253
|
body: successCount === 0 ? reviewBody : undefined,
|
|
@@ -2181,6 +2261,23 @@ ${fileChanges || "无"}`;
|
|
|
2181
2261
|
}
|
|
2182
2262
|
if (successCount > 0) {
|
|
2183
2263
|
console.log(`✅ 逐条发布成功 ${successCount}/${comments.length} 条行级评论`);
|
|
2264
|
+
// 如果需要自动批准,单独发一个 APPROVE review
|
|
2265
|
+
if (shouldAutoApprove) {
|
|
2266
|
+
try {
|
|
2267
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2268
|
+
event: REVIEW_STATE.APPROVE,
|
|
2269
|
+
body: `✅ **自动批准合并**\n\n${
|
|
2270
|
+
stats.validTotal > 0
|
|
2271
|
+
? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
|
|
2272
|
+
: "代码审查通过,未发现问题,"
|
|
2273
|
+
}自动批准此 PR。`,
|
|
2274
|
+
commit_id: commitId,
|
|
2275
|
+
});
|
|
2276
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
2277
|
+
} catch (error) {
|
|
2278
|
+
console.warn("⚠️ 自动批准失败:", error);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2184
2281
|
} else {
|
|
2185
2282
|
console.warn("⚠️ 所有行级评论均无法定位,已跳过");
|
|
2186
2283
|
}
|
|
@@ -2189,16 +2286,36 @@ ${fileChanges || "无"}`;
|
|
|
2189
2286
|
// 本轮无新问题,仍发布 Round 状态(含上轮回顾)
|
|
2190
2287
|
try {
|
|
2191
2288
|
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2192
|
-
event:
|
|
2193
|
-
body:
|
|
2289
|
+
event: reviewEvent,
|
|
2290
|
+
body: finalReviewBody,
|
|
2194
2291
|
comments: [],
|
|
2195
2292
|
commit_id: commitId,
|
|
2196
2293
|
});
|
|
2197
|
-
|
|
2294
|
+
if (shouldAutoApprove) {
|
|
2295
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(Round ${result.round},所有问题已解决)`);
|
|
2296
|
+
} else {
|
|
2297
|
+
console.log(`✅ 已发布 Round ${result.round} 审查状态(无新问题)`);
|
|
2298
|
+
}
|
|
2198
2299
|
} catch (error) {
|
|
2199
2300
|
console.warn("⚠️ 发布审查状态失败:", error);
|
|
2200
2301
|
}
|
|
2201
2302
|
}
|
|
2303
|
+
} else if (shouldAutoApprove) {
|
|
2304
|
+
// 未启用 lineComments 但需要自动批准
|
|
2305
|
+
try {
|
|
2306
|
+
await this.gitProvider.createPullReview(owner, repo, prNumber, {
|
|
2307
|
+
event: REVIEW_STATE.APPROVE,
|
|
2308
|
+
body: `✅ **自动批准合并**\n\n${
|
|
2309
|
+
stats.validTotal > 0
|
|
2310
|
+
? `所有问题都已解决 (${stats.fixed} 已修复, ${stats.resolved} 已解决),`
|
|
2311
|
+
: "代码审查通过,未发现问题,"
|
|
2312
|
+
}自动批准此 PR。`,
|
|
2313
|
+
commit_id: commitId,
|
|
2314
|
+
});
|
|
2315
|
+
console.log(`✅ 已自动批准 PR #${prNumber}(所有问题已解决)`);
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
console.warn("⚠️ 自动批准失败:", error);
|
|
2318
|
+
}
|
|
2202
2319
|
}
|
|
2203
2320
|
}
|
|
2204
2321
|
|