claude-teammate 0.1.37 → 0.1.39
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/package.json +1 -1
- package/src/claude.js +127 -0
- package/src/commands/worker.js +98 -1
- package/src/github.js +96 -1
- package/src/repo.js +60 -0
- package/src/runtime.js +2 -1
package/package.json
CHANGED
package/src/claude.js
CHANGED
|
@@ -139,6 +139,34 @@ const GITHUB_PR_COMMENT_REVIEW_SCHEMA = {
|
|
|
139
139
|
required: ["decision", "comment_body", "epic_facts", "epic_guardrails"]
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
const GITHUB_PR_REVIEW_SCHEMA = {
|
|
143
|
+
type: "object",
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
properties: {
|
|
146
|
+
summary: {
|
|
147
|
+
type: "string"
|
|
148
|
+
},
|
|
149
|
+
suggestions: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: {
|
|
152
|
+
type: "object",
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
file: { type: "string" },
|
|
156
|
+
line: { type: "number" },
|
|
157
|
+
side: { type: "string" },
|
|
158
|
+
body: { type: "string" },
|
|
159
|
+
classification: { type: "string", enum: ["Minor", "Major"] },
|
|
160
|
+
committable: { type: "boolean" },
|
|
161
|
+
suggestion: { type: "string" }
|
|
162
|
+
},
|
|
163
|
+
required: ["file", "line", "side", "body", "classification", "committable"]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
required: ["summary", "suggestions"]
|
|
168
|
+
};
|
|
169
|
+
|
|
142
170
|
const EPIC_MEMORY_CLEANUP_SCHEMA = {
|
|
143
171
|
type: "object",
|
|
144
172
|
additionalProperties: false,
|
|
@@ -502,6 +530,50 @@ export async function runClaudePullRequestCommentReview(input) {
|
|
|
502
530
|
return validateGitHubPullRequestCommentReviewResult(parseClaudeOutput(stdout));
|
|
503
531
|
}
|
|
504
532
|
|
|
533
|
+
export async function runClaudePrReview(input) {
|
|
534
|
+
if (!input.repoPath) {
|
|
535
|
+
throw new Error("PR review requires a local repository path.");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const repoPaths = [input.repoPath];
|
|
539
|
+
await Promise.all(repoPaths.map((repoPath) => clearLocalPlaywrightMcpConfig(repoPath)));
|
|
540
|
+
|
|
541
|
+
const args = [
|
|
542
|
+
"--print",
|
|
543
|
+
"--model",
|
|
544
|
+
input.model || DEFAULT_MODEL,
|
|
545
|
+
"--permission-mode",
|
|
546
|
+
"default",
|
|
547
|
+
"--strict-mcp-config",
|
|
548
|
+
"--tools",
|
|
549
|
+
"Read,Grep,Glob",
|
|
550
|
+
"--effort",
|
|
551
|
+
"medium",
|
|
552
|
+
"--output-format",
|
|
553
|
+
"json",
|
|
554
|
+
"--json-schema",
|
|
555
|
+
JSON.stringify(GITHUB_PR_REVIEW_SCHEMA),
|
|
556
|
+
"--append-system-prompt",
|
|
557
|
+
buildGitHubPRReviewSystemPrompt(),
|
|
558
|
+
buildGitHubPRReviewUserPrompt(input)
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
let stdout;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
({ stdout } = await runClaudeCommand("claude", args, {
|
|
565
|
+
cwd: input.repoPath,
|
|
566
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
567
|
+
timeout: input.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
568
|
+
onSpawn: input.onSpawn
|
|
569
|
+
}));
|
|
570
|
+
} catch (error) {
|
|
571
|
+
throw new Error(formatClaudeInvocationError(error, input.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return validateGitHubPrReviewResult(parseClaudeOutput(stdout));
|
|
575
|
+
}
|
|
576
|
+
|
|
505
577
|
export function parseClaudeOutput(output) {
|
|
506
578
|
const trimmed = output.trim();
|
|
507
579
|
if (!trimmed) {
|
|
@@ -522,6 +594,10 @@ export function parseClaudeOutput(output) {
|
|
|
522
594
|
return direct;
|
|
523
595
|
}
|
|
524
596
|
|
|
597
|
+
if ("summary" in direct && "suggestions" in direct) {
|
|
598
|
+
return direct;
|
|
599
|
+
}
|
|
600
|
+
|
|
525
601
|
if ("result" in direct && typeof direct.result === "string") {
|
|
526
602
|
return JSON.parse(direct.result);
|
|
527
603
|
}
|
|
@@ -634,6 +710,27 @@ function validateGitHubPullRequestCommentReviewResult(result) {
|
|
|
634
710
|
};
|
|
635
711
|
}
|
|
636
712
|
|
|
713
|
+
function validateGitHubPrReviewResult(result) {
|
|
714
|
+
if (!result || typeof result !== "object") {
|
|
715
|
+
throw new Error("Claude CLI returned an invalid GitHub PR review payload.");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
summary: String(result.summary ?? "").trim(),
|
|
720
|
+
suggestions: Array.isArray(result.suggestions)
|
|
721
|
+
? result.suggestions.map((s) => ({
|
|
722
|
+
file: String(s.file ?? "").trim(),
|
|
723
|
+
line: Number(s.line) || 1,
|
|
724
|
+
side: String(s.side ?? "RIGHT").trim(),
|
|
725
|
+
body: String(s.body ?? "").trim(),
|
|
726
|
+
classification: ["Minor", "Major"].includes(s.classification) ? s.classification : "Minor",
|
|
727
|
+
committable: Boolean(s.committable),
|
|
728
|
+
suggestion: s.suggestion != null ? String(s.suggestion).trim() : undefined
|
|
729
|
+
}))
|
|
730
|
+
: []
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
637
734
|
function validateEpicMemoryCleanupResult(result) {
|
|
638
735
|
if (!result || typeof result !== "object") {
|
|
639
736
|
throw new Error("Claude CLI returned an invalid epic memory cleanup payload.");
|
|
@@ -931,6 +1028,20 @@ function buildGitHubPRCommentReviewSystemPrompt() {
|
|
|
931
1028
|
].join(" ");
|
|
932
1029
|
}
|
|
933
1030
|
|
|
1031
|
+
function buildGitHubPRReviewSystemPrompt() {
|
|
1032
|
+
return [
|
|
1033
|
+
"You are reviewing a GitHub pull request as a code reviewer.",
|
|
1034
|
+
"The primary input is the unified diff provided inline.",
|
|
1035
|
+
"You may also use Read, Grep, and Glob to read related code when the diff is ambiguous.",
|
|
1036
|
+
"Do not edit files, do not run mutating commands, and do not implement anything.",
|
|
1037
|
+
"Skip trivial style-only issues unless they introduce a real risk.",
|
|
1038
|
+
"Classify each suggestion as Minor (non-blocking) or Major (should be addressed before merge).",
|
|
1039
|
+
"Where possible, produce a committable suggestion with the corrected code.",
|
|
1040
|
+
"Return a summary paragraph describing what the PR does, followed by a summary of all suggestions.",
|
|
1041
|
+
"Return only structured output matching the provided schema."
|
|
1042
|
+
].join(" ");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
934
1045
|
function buildEpicMemoryCleanupSystemPrompt() {
|
|
935
1046
|
return [
|
|
936
1047
|
"You are updating epic memory for an autonomous engineering workflow.",
|
|
@@ -1082,6 +1193,22 @@ Instructions:
|
|
|
1082
1193
|
- If blocked, return result=stuck with that summary explaining why it did not work.`;
|
|
1083
1194
|
}
|
|
1084
1195
|
|
|
1196
|
+
function buildGitHubPRReviewUserPrompt(input) {
|
|
1197
|
+
return `Review this GitHub pull request diff.
|
|
1198
|
+
|
|
1199
|
+
Pull request number: ${input.pr?.number ?? ""}
|
|
1200
|
+
Pull request title:
|
|
1201
|
+
${input.pr?.title ?? ""}
|
|
1202
|
+
|
|
1203
|
+
Repository path:
|
|
1204
|
+
${input.repoPath}
|
|
1205
|
+
|
|
1206
|
+
Unified diff:
|
|
1207
|
+
${input.diff || "(empty)"}
|
|
1208
|
+
|
|
1209
|
+
Review the diff carefully. Read related code files with Read, Grep, or Glob only when the diff alone is insufficient to judge correctness. Return a summary describing the PR and its suggestions.`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1085
1212
|
function buildEpicMemoryCleanupUserPrompt(input) {
|
|
1086
1213
|
return `Update this epic memory facts and guardrails and clean it up.
|
|
1087
1214
|
|
package/src/commands/worker.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runClaudeEpicMemoryCleanup,
|
|
9
9
|
runClaudeEpicMemorySummarize,
|
|
10
10
|
runClaudeImplementation,
|
|
11
|
+
runClaudePrReview,
|
|
11
12
|
runClaudePullRequestCommentReview,
|
|
12
13
|
runClaudeGitHubIssueReview
|
|
13
14
|
} from "../claude.js";
|
|
@@ -26,8 +27,10 @@ import {
|
|
|
26
27
|
saveIssueMemory
|
|
27
28
|
} from "../memory.js";
|
|
28
29
|
import {
|
|
30
|
+
checkoutPullRequestBranch,
|
|
29
31
|
commitAndPushRepoChanges,
|
|
30
32
|
ensureBranchFromDefault,
|
|
33
|
+
ensureReviewRepo,
|
|
31
34
|
hasUsableGitCheckout,
|
|
32
35
|
listMissingRepoPaths,
|
|
33
36
|
parseGitHubRepoUrl,
|
|
@@ -116,7 +119,12 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
116
119
|
draftPrCount: 0,
|
|
117
120
|
draftPrs: [],
|
|
118
121
|
prCommentReview: buildPrSubtaskState(previousState.prCommentReview),
|
|
119
|
-
prImplementation: buildPrSubtaskState(previousState.prImplementation)
|
|
122
|
+
prImplementation: buildPrSubtaskState(previousState.prImplementation),
|
|
123
|
+
lastReviewPollAt: null,
|
|
124
|
+
lastReviewSuccessAt: null,
|
|
125
|
+
lastReviewError: null,
|
|
126
|
+
reviewPrCount: 0,
|
|
127
|
+
reviewPrs: []
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
const updatePrSubtaskState = async (key, updates) => {
|
|
@@ -136,6 +144,7 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
136
144
|
let jiraPolling = false;
|
|
137
145
|
let githubPolling = false;
|
|
138
146
|
let prPolling = false;
|
|
147
|
+
let reviewPolling = false;
|
|
139
148
|
|
|
140
149
|
const shutdown = async (signal) => {
|
|
141
150
|
if (stopping) {
|
|
@@ -344,9 +353,94 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
344
353
|
}
|
|
345
354
|
};
|
|
346
355
|
|
|
356
|
+
const pollReviewPrs = async () => {
|
|
357
|
+
if (reviewPolling || stopping) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
reviewPolling = true;
|
|
362
|
+
state.lastReviewPollAt = new Date().toISOString();
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const processedPrs = [];
|
|
366
|
+
const prs = await github.listPrsNeedingReview();
|
|
367
|
+
let reviewedPrCount = prs.length;
|
|
368
|
+
|
|
369
|
+
for (const pr of prs) {
|
|
370
|
+
if (!pr.repoUrl) {
|
|
371
|
+
await logger.error("PR review skipped because repository URL is missing", {
|
|
372
|
+
pr: pr.number,
|
|
373
|
+
title: pr.title
|
|
374
|
+
});
|
|
375
|
+
reviewedPrCount -= 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const repoPath = await ensureReviewRepo(pr.repoUrl, runtimePaths.reviewReposDir);
|
|
381
|
+
const prDetail = await github.fetchPullRequest(pr.repoUrl, pr.number);
|
|
382
|
+
await checkoutPullRequestBranch(repoPath, prDetail.headRef, prDetail.headRepoCloneUrl);
|
|
383
|
+
|
|
384
|
+
const diff = await github.fetchPullRequestDiff(pr.repoUrl, pr.number);
|
|
385
|
+
const result = await runClaudePrReview({
|
|
386
|
+
diff,
|
|
387
|
+
repoPath,
|
|
388
|
+
pr: prDetail,
|
|
389
|
+
timeoutMs: parseOptionalInt(values.CLAUDE_TIMEOUT_MS)
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await github.createPullRequestReview(
|
|
393
|
+
pr.repoUrl,
|
|
394
|
+
pr.number,
|
|
395
|
+
result.summary,
|
|
396
|
+
result.suggestions,
|
|
397
|
+
"COMMENT"
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
await github.addLabelsToPullRequest(pr.repoUrl, pr.number, ["AI-reviewed"]);
|
|
401
|
+
|
|
402
|
+
processedPrs.push({
|
|
403
|
+
repoUrl: pr.repoUrl,
|
|
404
|
+
pullRequestNumber: String(pr.number),
|
|
405
|
+
pullRequestUrl: prDetail.url,
|
|
406
|
+
suggestionsCount: result.suggestions.length
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await logger.info("PR review submitted", {
|
|
410
|
+
repo: pr.repoUrl,
|
|
411
|
+
pr: pr.number,
|
|
412
|
+
suggestions: result.suggestions.length
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
await logger.error("PR review failed", {
|
|
416
|
+
repo: pr.repoUrl,
|
|
417
|
+
pr: pr.number,
|
|
418
|
+
error
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
state.lastReviewSuccessAt = new Date().toISOString();
|
|
424
|
+
state.lastReviewError = null;
|
|
425
|
+
state.reviewPrCount = reviewedPrCount;
|
|
426
|
+
state.reviewPrs = processedPrs.slice(0, 20);
|
|
427
|
+
await writeState(runtimePaths.stateFile, state);
|
|
428
|
+
await logger.info("PR review poll complete", {
|
|
429
|
+
reviewed: reviewedPrCount
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
state.lastReviewError = error instanceof Error ? error.message : String(error);
|
|
433
|
+
await writeState(runtimePaths.stateFile, state);
|
|
434
|
+
await logger.error("PR review poll failed", { error });
|
|
435
|
+
} finally {
|
|
436
|
+
reviewPolling = false;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
347
440
|
await pollJira();
|
|
348
441
|
await pollGitHub();
|
|
349
442
|
await pollDraftPrs();
|
|
443
|
+
await pollReviewPrs();
|
|
350
444
|
setInterval(() => {
|
|
351
445
|
void pollJira();
|
|
352
446
|
}, POLL_INTERVAL_MS);
|
|
@@ -356,6 +450,9 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
356
450
|
setInterval(() => {
|
|
357
451
|
void pollDraftPrs();
|
|
358
452
|
}, POLL_INTERVAL_MS);
|
|
453
|
+
setInterval(() => {
|
|
454
|
+
void pollReviewPrs();
|
|
455
|
+
}, POLL_INTERVAL_MS);
|
|
359
456
|
}
|
|
360
457
|
|
|
361
458
|
async function processJiraIssue({ issue, jira, github, botUser, config, projectRoot, runtimePaths, logger }) {
|
package/src/github.js
CHANGED
|
@@ -290,6 +290,85 @@ export function createGitHubClient(config) {
|
|
|
290
290
|
return payload.default_branch;
|
|
291
291
|
},
|
|
292
292
|
|
|
293
|
+
async listPrsNeedingReview() {
|
|
294
|
+
const query = "is:pr+is:open+review-requested:@me+-label:AI-reviewed";
|
|
295
|
+
const url = new URL("https://api.github.com/search/issues");
|
|
296
|
+
url.searchParams.set("q", query);
|
|
297
|
+
url.searchParams.set("per_page", "10");
|
|
298
|
+
|
|
299
|
+
const payload = await requestGitHub(url, config, { method: "GET" });
|
|
300
|
+
return Array.isArray(payload.items)
|
|
301
|
+
? payload.items.map((item) => ({
|
|
302
|
+
repoUrl: mapRepositoryApiUrlToHtmlUrl(item.repository_url),
|
|
303
|
+
number: item.number,
|
|
304
|
+
title: item.title ?? ""
|
|
305
|
+
}))
|
|
306
|
+
: [];
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async addLabelsToPullRequest(repoUrl, prNumber, labels) {
|
|
310
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
311
|
+
return requestGitHub(
|
|
312
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${prNumber}/labels`,
|
|
313
|
+
config,
|
|
314
|
+
{
|
|
315
|
+
method: "POST",
|
|
316
|
+
body: JSON.stringify({ labels })
|
|
317
|
+
},
|
|
318
|
+
repo
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
async createPullRequestReview(repoUrl, prNumber, body, suggestions, event) {
|
|
323
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
324
|
+
const comments = Array.isArray(suggestions)
|
|
325
|
+
? suggestions.map((s) => {
|
|
326
|
+
let commentBody = String(s.body || "").trim();
|
|
327
|
+
if (s.committable && s.suggestion) {
|
|
328
|
+
commentBody = `${commentBody}\n\n\`\`\`suggestion\n${s.suggestion}\n\`\`\``.trim();
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
path: String(s.file || ""),
|
|
332
|
+
line: Number(s.line) || 1,
|
|
333
|
+
side: String(s.side || "RIGHT").toUpperCase(),
|
|
334
|
+
body: commentBody
|
|
335
|
+
};
|
|
336
|
+
})
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
return requestGitHub(
|
|
340
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/reviews`,
|
|
341
|
+
config,
|
|
342
|
+
{
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: JSON.stringify({ body: String(body || "").trim(), event: event || "COMMENT", comments })
|
|
345
|
+
},
|
|
346
|
+
repo
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
async fetchPullRequestDiff(repoUrl, prNumber) {
|
|
351
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
352
|
+
const response = await fetch(
|
|
353
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`,
|
|
354
|
+
{
|
|
355
|
+
headers: {
|
|
356
|
+
Accept: "application/vnd.github.v3.diff",
|
|
357
|
+
Authorization: `Bearer ${config.GITHUB_PAT}`,
|
|
358
|
+
"User-Agent": "claude-teammate"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
const errorBody = await response.text();
|
|
364
|
+
throw new Error(buildGitHubRequestError(response.status, repo, errorBody, {
|
|
365
|
+
method: "GET",
|
|
366
|
+
path: `/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
return response.text();
|
|
370
|
+
},
|
|
371
|
+
|
|
293
372
|
async createPullRequest(repoUrl, pullRequest) {
|
|
294
373
|
const repo = parseGitHubRepoUrl(repoUrl);
|
|
295
374
|
const payload = await requestGitHub(
|
|
@@ -316,6 +395,21 @@ export function createGitHubClient(config) {
|
|
|
316
395
|
};
|
|
317
396
|
}
|
|
318
397
|
|
|
398
|
+
function mapRepositoryApiUrlToHtmlUrl(repositoryApiUrl) {
|
|
399
|
+
const value = String(repositoryApiUrl || "").trim();
|
|
400
|
+
|
|
401
|
+
if (!value) {
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const match = value.match(/^https:\/\/api\.github\.com\/repos\/([^/]+)\/([^/]+)$/u);
|
|
406
|
+
if (!match) {
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return `https://github.com/${match[1]}/${match[2]}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
319
413
|
function mapGitHubIssue(payload, comments = []) {
|
|
320
414
|
return {
|
|
321
415
|
number: payload.number,
|
|
@@ -347,7 +441,8 @@ function mapGitHubPullRequestSummary(payload) {
|
|
|
347
441
|
id: payload.user?.id ?? null
|
|
348
442
|
},
|
|
349
443
|
headRef: payload.head?.ref ?? "",
|
|
350
|
-
baseRef: payload.base?.ref ?? ""
|
|
444
|
+
baseRef: payload.base?.ref ?? "",
|
|
445
|
+
headRepoCloneUrl: payload.head?.repo?.clone_url ?? null
|
|
351
446
|
};
|
|
352
447
|
}
|
|
353
448
|
|
package/src/repo.js
CHANGED
|
@@ -54,6 +54,66 @@ export async function validateOrEnsureLocalRepo(repoUrl, reposDir, localPath = "
|
|
|
54
54
|
return ensureLocalRepo(repoUrl, reposDir);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export async function ensureReviewRepo(repoUrl, reviewReposDir) {
|
|
58
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
59
|
+
const checkoutPath = path.join(reviewReposDir, repo.owner, repo.name);
|
|
60
|
+
|
|
61
|
+
if (await isUsableGitCheckout(checkoutPath)) {
|
|
62
|
+
await execGit(checkoutPath, ["checkout", "--", "."]);
|
|
63
|
+
await execGit(checkoutPath, ["clean", "-fd"]);
|
|
64
|
+
return checkoutPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await mkdir(path.dirname(checkoutPath), { recursive: true });
|
|
68
|
+
await execFileAsync("git", ["clone", repo.cloneUrl, checkoutPath], {
|
|
69
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
70
|
+
env: buildGitEnv(repo.cloneUrl)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return checkoutPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function checkoutPullRequestBranch(repoPath, branchName, headRepoCloneUrl = null) {
|
|
77
|
+
const originUrl = await execGitOutput(repoPath, ["remote", "get-url", "origin"]);
|
|
78
|
+
const normalizedOrigin = originUrl.trim().replace(/\.git$/u, "").toLowerCase();
|
|
79
|
+
const normalizedHead = headRepoCloneUrl
|
|
80
|
+
? headRepoCloneUrl.trim().replace(/\.git$/u, "").toLowerCase()
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
const isFork = normalizedHead && normalizedHead !== normalizedOrigin;
|
|
84
|
+
|
|
85
|
+
if (isFork) {
|
|
86
|
+
const remoteName = "pr-head";
|
|
87
|
+
let remoteExists = false;
|
|
88
|
+
try {
|
|
89
|
+
await execGitOutput(repoPath, ["remote", "get-url", remoteName]);
|
|
90
|
+
remoteExists = true;
|
|
91
|
+
} catch {
|
|
92
|
+
remoteExists = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (remoteExists) {
|
|
96
|
+
await execGit(repoPath, ["remote", "set-url", remoteName, headRepoCloneUrl]);
|
|
97
|
+
} else {
|
|
98
|
+
await execFileAsync("git", ["remote", "add", remoteName, headRepoCloneUrl], {
|
|
99
|
+
cwd: repoPath,
|
|
100
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
101
|
+
env: buildGitEnv(headRepoCloneUrl)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await execFileAsync("git", ["fetch", remoteName], {
|
|
106
|
+
cwd: repoPath,
|
|
107
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
108
|
+
env: buildGitEnv(headRepoCloneUrl)
|
|
109
|
+
});
|
|
110
|
+
await execGit(repoPath, ["checkout", "-B", branchName, `${remoteName}/${branchName}`]);
|
|
111
|
+
} else {
|
|
112
|
+
await execGit(repoPath, ["fetch", "origin"]);
|
|
113
|
+
await execGit(repoPath, ["checkout", "-B", branchName, `origin/${branchName}`]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
57
117
|
export async function ensureBranchFromDefault(repoPath, branchName, baseBranch) {
|
|
58
118
|
try {
|
|
59
119
|
await execGit(repoPath, ["fetch", "origin", baseBranch, "--prune"]);
|
package/src/runtime.js
CHANGED
|
@@ -11,7 +11,8 @@ export function getRuntimePaths(projectRoot) {
|
|
|
11
11
|
pidFile: path.join(runtimeDir, "worker.pid"),
|
|
12
12
|
stateFile: path.join(runtimeDir, "state.json"),
|
|
13
13
|
logFile: path.join(runtimeDir, "worker.log"),
|
|
14
|
-
reposDir: path.join(runtimeDir, "repos")
|
|
14
|
+
reposDir: path.join(runtimeDir, "repos"),
|
|
15
|
+
reviewReposDir: path.join(runtimeDir, "review-repos")
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|