@raolin2025/claude-code-node 2.0.0 → 2.2.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/README.md +117 -0
- package/package.json +1 -1
- package/src/__tests__/git-tool.integration.test.js +89 -0
- package/src/__tests__/git-tool.test.js +144 -0
- package/src/channel/notify-daemon.js +16 -13
- package/src/core/cli.js +45 -5
- package/src/core/query-engine.js +57 -54
- package/src/core/session.js +3 -1
- package/src/git/github-api.js +360 -0
- package/src/git/index.js +37 -0
- package/src/git/llm-assistant.js +83 -0
- package/src/git/pr-merge-policy.js +367 -0
- package/src/git/pr-reviewer.js +533 -0
- package/src/git/utils/diff-parser.js +330 -0
- package/src/mcp/client.js +13 -3
- package/src/security/path-guard.js +11 -1
- package/src/tools/git-tool.js +308 -0
- package/src/tools/index.js +2 -0
- package/src/tools/web-fetch.js +3 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 合并策略引擎
|
|
3
|
+
* 根据项目的合并规则决定是否允许合并,以及如何合并
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GitHubAPI } from './github-api.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 合并策略配置
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MERGE_POLICY = {
|
|
12
|
+
requiredApprovals: 1, // 最少需要多少个 approve
|
|
13
|
+
requireCI: true, // 是否要求所有 CI 检查通过
|
|
14
|
+
requireNoChangesRequested: true, // 是否要求无 changes_requested
|
|
15
|
+
requireReview: true, // 是否要求至少一个 review 且是 approve
|
|
16
|
+
allowedMergeMethods: ['merge', 'squash', 'rebase'],
|
|
17
|
+
defaultMergeMethod: 'merge',
|
|
18
|
+
autoMergeLabels: ['auto-merge', 'ready-to-merge', 'automerge'], // 有这些标签则允许自动合并
|
|
19
|
+
ignoreLabels: ['do-not-merge', 'wip', 'work-in-progress'], // 忽略这些标签
|
|
20
|
+
bannedBranches: ['main', 'master', 'develop'], // 禁止合并到这些分支(可配置)
|
|
21
|
+
checkProtectedBranch: true, // 是否检查分支保护规则
|
|
22
|
+
allowSelfApprove: false, // 是否允许 PR 作者自己 approve
|
|
23
|
+
maxReviewDays: 30, // 审查时间上限(天)
|
|
24
|
+
requireDescription: true, // 是否要求 PR 描述不为空
|
|
25
|
+
minDescriptionLength: 20, // PR 描述最小长度
|
|
26
|
+
requireLinkedIssue: false, // 是否要求关联 issue
|
|
27
|
+
blockOnConflicts: true // 有冲突是否阻止合并
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* PR 合并策略检查器
|
|
32
|
+
*/
|
|
33
|
+
export class PRMergePolicy {
|
|
34
|
+
/**
|
|
35
|
+
* @param {GitHubAPI} github
|
|
36
|
+
* @param {Object} userPolicy - 用户自定义策略
|
|
37
|
+
*/
|
|
38
|
+
constructor(github, userPolicy = {}) {
|
|
39
|
+
this.github = github
|
|
40
|
+
this.policy = { ...DEFAULT_MERGE_POLICY, ...userPolicy }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 检查 PR 是否可合并
|
|
45
|
+
* @param {number} prNumber
|
|
46
|
+
* @returns {Promise<MergeCheckResult>}
|
|
47
|
+
*/
|
|
48
|
+
async checkMergeable(prNumber) {
|
|
49
|
+
const pr = await this.github.getPR(prNumber)
|
|
50
|
+
const result = {
|
|
51
|
+
prNumber,
|
|
52
|
+
mergeable: true,
|
|
53
|
+
checks: {},
|
|
54
|
+
violations: [],
|
|
55
|
+
warnings: [],
|
|
56
|
+
metadata: {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 1. 基础检查
|
|
60
|
+
await this.checkBasicState(pr, result)
|
|
61
|
+
if (!result.mergeable) return result
|
|
62
|
+
|
|
63
|
+
// 2. 审查状态检查
|
|
64
|
+
await this.checkReviewStatus(pr, result)
|
|
65
|
+
if (!result.mergeable) return result
|
|
66
|
+
|
|
67
|
+
// 3. CI 状态检查
|
|
68
|
+
if (this.policy.requireCI) {
|
|
69
|
+
await this.checkCIStatus(pr, result)
|
|
70
|
+
if (!result.mergeable) return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. 分支保护检查
|
|
74
|
+
if (this.policy.checkProtectedBranch) {
|
|
75
|
+
await this.checkBranchProtection(pr, result)
|
|
76
|
+
if (!result.mergeable) return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5. 自动合并标签检查
|
|
80
|
+
await this.checkAutoMergeLabels(pr, result)
|
|
81
|
+
|
|
82
|
+
// 6. 忽略标签检查
|
|
83
|
+
await this.checkIgnoreLabels(pr, result)
|
|
84
|
+
|
|
85
|
+
// 7. 合并冲突检查
|
|
86
|
+
if (this.policy.blockOnConflicts) {
|
|
87
|
+
const isMergeable = await this.github.isMergeable(prNumber)
|
|
88
|
+
if (isMergeable === false) {
|
|
89
|
+
result.violations.push({
|
|
90
|
+
code: 'MERGE_CONFLICT',
|
|
91
|
+
message: 'PR has merge conflicts that need to be resolved'
|
|
92
|
+
})
|
|
93
|
+
result.mergeable = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 执行合并(已通过检查)
|
|
102
|
+
* @param {number} prNumber
|
|
103
|
+
* @param {Object} options - 合并选项
|
|
104
|
+
* @returns {Promise<MergeResult>}
|
|
105
|
+
*/
|
|
106
|
+
async merge(prNumber, options = {}) {
|
|
107
|
+
const checkResult = await this.checkMergeable(prNumber)
|
|
108
|
+
if (!checkResult.mergeable) {
|
|
109
|
+
throw new Error(`Cannot merge PR ${prNumber}: ${checkResult.violations.map(v => v.message).join('; ')}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pr = await this.github.getPR(prNumber)
|
|
113
|
+
const mergeMethod = options.method || this.policy.defaultMergeMethod
|
|
114
|
+
|
|
115
|
+
if (!this.policy.allowedMergeMethods.includes(mergeMethod)) {
|
|
116
|
+
throw new Error(`Merge method '${mergeMethod}' not allowed. Allowed: ${this.policy.allowedMergeMethods.join(', ')}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.github.mergePR(prNumber, {
|
|
121
|
+
mergeMethod,
|
|
122
|
+
commitTitle: options.commitTitle || pr.title,
|
|
123
|
+
commitMessage: options.commitMessage || pr.body
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
prNumber,
|
|
128
|
+
merged: true,
|
|
129
|
+
method: mergeMethod,
|
|
130
|
+
message: result?.message || 'Merged successfully',
|
|
131
|
+
sha: result?.sha || null
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.status === 405) {
|
|
135
|
+
throw new Error(`Merge blocked by branch protection rules: ${error.message}`)
|
|
136
|
+
}
|
|
137
|
+
throw error
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============ 检查器 ============
|
|
142
|
+
|
|
143
|
+
async checkBasicState(pr, result) {
|
|
144
|
+
result.metadata.prTitle = pr.title
|
|
145
|
+
result.metadata.prBody = pr.body
|
|
146
|
+
result.metadata.baseBranch = pr.base.ref
|
|
147
|
+
result.metadata.headBranch = pr.head.ref
|
|
148
|
+
result.metadata.state = pr.state
|
|
149
|
+
|
|
150
|
+
// 关闭的 PR 无法合并
|
|
151
|
+
if (pr.state !== 'open') {
|
|
152
|
+
result.violations.push({
|
|
153
|
+
code: 'PR_CLOSED',
|
|
154
|
+
message: `PR is ${pr.state}, cannot merge`
|
|
155
|
+
})
|
|
156
|
+
result.mergeable = false
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 检查目标分支 — bannedBranches 只限制合并操作,不影响审查
|
|
161
|
+
// 但如果明确禁止,给出警告而非阻止
|
|
162
|
+
if (this.policy.bannedBranches.includes(pr.base.ref)) {
|
|
163
|
+
result.warnings.push({
|
|
164
|
+
code: 'BANNED_TARGET_BRANCH',
|
|
165
|
+
message: `Merge to '${pr.base.ref}' is not allowed. Set bannedBranches: [] to override.`
|
|
166
|
+
})
|
|
167
|
+
// 不 set mergeable = false,只警告
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 检查 PR 描述
|
|
171
|
+
if (this.policy.requireDescription && (!pr.body || pr.body.trim().length < this.policy.minDescriptionLength)) {
|
|
172
|
+
result.violations.push({
|
|
173
|
+
code: 'SHORT_DESCRIPTION',
|
|
174
|
+
message: `PR description too short (min ${this.policy.minDescriptionLength} chars)`
|
|
175
|
+
})
|
|
176
|
+
result.mergeable = false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async checkReviewStatus(pr, result) {
|
|
181
|
+
const reviews = await this.github.listReviews(pr.number)
|
|
182
|
+
result.metadata.reviews = reviews
|
|
183
|
+
|
|
184
|
+
// 计数:approved / changes_requested / commented
|
|
185
|
+
const approvedReviews = reviews.filter(r => r.state === 'APPROVED')
|
|
186
|
+
const changesRequestedReviews = reviews.filter(r => r.state === 'CHANGES_REQUESTED')
|
|
187
|
+
const commentedReviews = reviews.filter(r => r.state === 'COMMENTED')
|
|
188
|
+
|
|
189
|
+
result.checks.approvals = approvedReviews.length
|
|
190
|
+
result.checks.changesRequested = changesRequestedReviews.length
|
|
191
|
+
result.checks.reviews = reviews.length
|
|
192
|
+
|
|
193
|
+
// 检查 changes_requested
|
|
194
|
+
if (this.policy.requireNoChangesRequested && changesRequestedReviews.length > 0) {
|
|
195
|
+
result.violations.push({
|
|
196
|
+
code: 'CHANGES_REQUESTED',
|
|
197
|
+
message: `${changesRequestedReviews.length} review(s) requested changes`
|
|
198
|
+
})
|
|
199
|
+
result.mergeable = false
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 检查 approvals 数量
|
|
204
|
+
if (approvedReviews.length < this.policy.requiredApprovals) {
|
|
205
|
+
result.violations.push({
|
|
206
|
+
code: 'INSUFFICIENT_APPROVALS',
|
|
207
|
+
message: `Need ${this.policy.requiredApprovals} approval(s), have ${approvedReviews.length}`
|
|
208
|
+
})
|
|
209
|
+
result.mergeable = false
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 检查是否自 approve(如果禁止)
|
|
214
|
+
if (!this.policy.allowSelfApprove && pr?.user?.login) {
|
|
215
|
+
const selfApproved = approvedReviews.some(r => r.user?.login === pr.user.login)
|
|
216
|
+
if (selfApproved) {
|
|
217
|
+
result.warnings.push({
|
|
218
|
+
code: 'SELF_APPROVE',
|
|
219
|
+
message: 'PR author self-approved (allowSelfApprove is false)'
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async checkCIStatus(pr, result) {
|
|
226
|
+
try {
|
|
227
|
+
const prSha = pr.head?.sha
|
|
228
|
+
if (!prSha) throw new Error('PR has no head commit SHA')
|
|
229
|
+
const status = await this.github.getCombinedStatus(prSha)
|
|
230
|
+
result.metadata.ciStatus = status
|
|
231
|
+
|
|
232
|
+
if (!status) {
|
|
233
|
+
result.warnings.push({
|
|
234
|
+
code: 'CI_UNAVAILABLE',
|
|
235
|
+
message: 'Unable to fetch CI status'
|
|
236
|
+
})
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (status.state !== 'success') {
|
|
241
|
+
const failingContexts = (status.statuses || []).filter(s => s.state !== 'success' && s.state !== 'pending')
|
|
242
|
+
if (failingContexts.length > 0) {
|
|
243
|
+
result.violations.push({
|
|
244
|
+
code: 'CI_FAILED',
|
|
245
|
+
message: `CI checks failing: ${failingContexts.map(c => c.context).join(', ')}`,
|
|
246
|
+
details: failingContexts
|
|
247
|
+
})
|
|
248
|
+
result.mergeable = false
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
result.checks.ciPassed = true
|
|
252
|
+
result.checks.ciContexts = (status.statuses || []).map(s => s.context)
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
result.warnings.push({
|
|
256
|
+
code: 'CI_FAILED',
|
|
257
|
+
message: `CI check unavailable: ${error.message}`
|
|
258
|
+
})
|
|
259
|
+
// 不阻止合并,只告警
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async checkBranchProtection(pr, result) {
|
|
264
|
+
try {
|
|
265
|
+
const protection = await this.github.request(
|
|
266
|
+
`/repos/${this.github.owner}/${this.github.repo}/branches/${pr.base.ref}/protection`
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
result.metadata.branchProtection = protection
|
|
270
|
+
|
|
271
|
+
// 检查 required_pull_request_reviews
|
|
272
|
+
if (protection.required_pull_request_reviews) {
|
|
273
|
+
const req = protection.required_pull_request_reviews
|
|
274
|
+
if (req.required_approving_review_count > result.checks.approvals) {
|
|
275
|
+
result.violations.push({
|
|
276
|
+
code: 'BRANCH_PROTECTION_APPROVALS',
|
|
277
|
+
message: `Branch requires ${req.required_approving_review_count} approvals`
|
|
278
|
+
})
|
|
279
|
+
result.mergeable = false
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 检查 required_status_checks
|
|
284
|
+
if (protection.required_status_checks?.contexts?.length > 0) {
|
|
285
|
+
const requiredContexts = protection.required_status_checks.contexts
|
|
286
|
+
const ciContexts = result.checks.ciContexts || []
|
|
287
|
+
const missing = requiredContexts.filter(c => !ciContexts.includes(c))
|
|
288
|
+
if (missing.length > 0) {
|
|
289
|
+
result.violations.push({
|
|
290
|
+
code: 'MISSING_REQUIRED_CHECKS',
|
|
291
|
+
message: `Missing required status checks: ${missing.join(', ')}`
|
|
292
|
+
})
|
|
293
|
+
result.mergeable = false
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.status === 404) {
|
|
298
|
+
// 无分支保护规则,继续
|
|
299
|
+
result.metadata.branchProtection = null
|
|
300
|
+
} else {
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async checkAutoMergeLabels(pr, result) {
|
|
307
|
+
const labels = pr.labels?.map(l => l.name) || []
|
|
308
|
+
const hasAutoMerge = labels.some(l => this.policy.autoMergeLabels.includes(l))
|
|
309
|
+
|
|
310
|
+
if (hasAutoMerge) {
|
|
311
|
+
result.checks.autoMergeLabel = true
|
|
312
|
+
// 不自动合并,只是标记 Eligible
|
|
313
|
+
result.metadata.autoMergeEligible = true
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async checkIgnoreLabels(pr, result) {
|
|
318
|
+
const labels = pr.labels?.map(l => l.name) || []
|
|
319
|
+
const hasIgnore = labels.some(l => this.policy.ignoreLabels.includes(l))
|
|
320
|
+
|
|
321
|
+
if (hasIgnore) {
|
|
322
|
+
result.warnings.push({
|
|
323
|
+
code: 'IGNORED_LABEL',
|
|
324
|
+
message: 'PR has ignore label, consider not merging automatically'
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 快速检查:PR 是否可合并(简化版)
|
|
332
|
+
*/
|
|
333
|
+
export async function isPRMergeable(github, prNumber, policy = {}) {
|
|
334
|
+
const checker = new PRMergePolicy(github, policy)
|
|
335
|
+
const result = await checker.checkMergeable(prNumber)
|
|
336
|
+
return result.mergeable
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 批量检查:找出所有待处理可自动合并的 PR
|
|
341
|
+
*/
|
|
342
|
+
export async function findEligiblePRs(github, options = {}) {
|
|
343
|
+
const { label, limit = 50 } = options
|
|
344
|
+
if (!github || typeof github.listPRs !== 'function') {
|
|
345
|
+
throw new Error('Invalid GitHub API instance')
|
|
346
|
+
}
|
|
347
|
+
const prs = await github.listPRs({ state: 'open', per_page: limit })
|
|
348
|
+
const checker = new PRMergePolicy(github)
|
|
349
|
+
|
|
350
|
+
const eligible = []
|
|
351
|
+
for (const pr of (prs || [])) {
|
|
352
|
+
if (label && !pr.labels?.some(l => l.name === label)) {
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const result = await checker.checkMergeable(pr.number)
|
|
358
|
+
if (result.mergeable && result.metadata.autoMergeEligible) {
|
|
359
|
+
eligible.push({ pr, check: result })
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error(`Error checking PR ${pr.number}:`, error.message)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return eligible
|
|
367
|
+
}
|