Skip to content

Commit 393f849

Browse files
committed
feat: 인라인 코멘트 위치 불일치 시 본문 코멘트로 대체하는 로직 추가 및 오류 처리 개선
1 parent e0a0e1b commit 393f849

2 files changed

Lines changed: 109 additions & 23 deletions

File tree

action/index.cjs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150482,32 +150482,33 @@ const buildReviewInput = (filename, content, patch, globalContext) => {
150482150482
}
150483150483
return parts.join("\n");
150484150484
};
150485-
// Heuristic: anchor comment near first changed line
150486150485
const firstChangedPosition = (patch) => {
150486+
if (!patch)
150487+
return 1;
150487150488
const lines = patch.split("\n");
150488150489
const start = lines.findIndex((l) => l.startsWith("@@ "));
150489-
if (start === -1)
150490-
return Math.max(1, lines.length - 1);
150491-
// Prefer the first real added line (exclude file headers like +++ a/b)
150490+
if (start === -1) {
150491+
const firstNonEmpty = lines.findIndex((l) => l.trim().length > 0);
150492+
return Math.max(1, (firstNonEmpty === -1 ? 0 : firstNonEmpty) + 1);
150493+
}
150492150494
for (let i = start + 1; i < lines.length; i++) {
150493150495
const line = lines[i];
150494150496
if (line.startsWith("+") && !line.startsWith("+++")) {
150495-
return i + 1; // 1-based for GitHub API
150497+
return Math.min(Math.max(1, i + 1), lines.length);
150496150498
}
150499+
if (line.startsWith("@@ "))
150500+
break; // stop at next hunk header
150497150501
}
150498-
// If there were no additions (e.g., deletions-only change),
150499150502
// fall back to the first context line within the first hunk.
150500150503
for (let i = start + 1; i < lines.length; i++) {
150501150504
const line = lines[i];
150502150505
if (line.startsWith(" ")) {
150503-
return i + 1;
150506+
return Math.min(Math.max(1, i + 1), lines.length);
150504150507
}
150505-
// Stop at the next hunk header
150506150508
if (line.startsWith("@@ "))
150507150509
break;
150508150510
}
150509-
// Final fallback: the line immediately after the first hunk header.
150510-
return Math.min(lines.length, start + 2);
150511+
return Math.min(Math.max(1, start + 2), lines.length);
150511150512
};
150512150513
const REVIEW_ON_LGTM = process.env.REVIEW_ON_LGTM === "true" ? true : false;
150513150514
const robot = (app) => {
@@ -150625,6 +150626,8 @@ const robot = (app) => {
150625150626
const isFileCountWithinLimit = changedFiles.length <= MAX_FILE_COUNT;
150626150627
const ress = [];
150627150628
let overallReview = null;
150629+
// 인라인 코멘트가 위치 불일치(예: 422)로 실패할 경우, 본문 코멘트로 대체하기 위해 누적하는 버퍼
150630+
let inlineFallback = [];
150628150631
if (isFileCountWithinLimit) {
150629150632
const aggregatedPatch = changedFiles
150630150633
.filter((file) => (file.status === "modified" || file.status === "added") &&
@@ -150720,11 +150723,20 @@ const robot = (app) => {
150720150723
if (!res.lgtm || (res.lgtm && REVIEW_ON_LGTM)) {
150721150724
if (!commentablePaths.has(file.filename)) {
150722150725
loglevel_1.default.info(`PR 파일에 patch가 없어 ${file.filename}에 대한 코멘트 생략`);
150726+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
150723150727
continue;
150724150728
}
150725150729
// Compute position against the PR-wide patch to avoid misalignment
150726150730
const prWidePatch = prFilePatchByPath.get(file.filename) || patch;
150731+
if (!prWidePatch || !prWidePatch.includes('@@')) {
150732+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
150733+
continue;
150734+
}
150727150735
const position = firstChangedPosition(prWidePatch);
150736+
if (!Number.isSafeInteger(position) || position <= 0) {
150737+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
150738+
continue;
150739+
}
150728150740
ress.push({
150729150741
path: file.filename,
150730150742
body: res.review_comment,
@@ -150777,7 +150789,38 @@ const robot = (app) => {
150777150789
}
150778150790
catch { }
150779150791
}
150780-
loglevel_1.default.info(`Failed to create review`, e);
150792+
if (status === 422) {
150793+
try {
150794+
let body = "LGTM 👍";
150795+
if (overallReview && overallReview.summary) {
150796+
body = `<!-- chatgpt-summary:v1 -->\n${overallReview.summary}`;
150797+
}
150798+
if (typeof inlineFallback !== "undefined" && inlineFallback.length) {
150799+
body += `\n\n---\n⚠️ 인라인 주석을 diff에 고정하지 못해 본문에 첨부합니다 (422).` + inlineFallback.join("\n");
150800+
}
150801+
else if (ress.length) {
150802+
body += `\n\n(인라인 코멘트가 위치 불일치로 생략되었습니다.)`;
150803+
}
150804+
if (!isFileCountWithinLimit) {
150805+
body += `\n\nNote: 파일 개수가 ${MAX_FILE_COUNT}개를 초과하여 개별 리뷰는 생략되었습니다.`;
150806+
}
150807+
await context.octokit.pulls.createReview({
150808+
repo: repo.repo,
150809+
owner: repo.owner,
150810+
pull_number: context.pullRequest().pull_number,
150811+
body,
150812+
event: "COMMENT",
150813+
commit_id: commits[commits.length - 1].sha,
150814+
});
150815+
loglevel_1.default.info("Fell back to body-only review after 422.");
150816+
}
150817+
catch (e2) {
150818+
loglevel_1.default.info(`Fallback review creation also failed`, e2);
150819+
}
150820+
}
150821+
else {
150822+
loglevel_1.default.info(`Failed to create review`, e);
150823+
}
150781150824
}
150782150825
console.timeEnd("gpt cost");
150783150826
loglevel_1.default.info("successfully reviewed", context.payload.pull_request.html_url);

src/bot.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,33 +116,34 @@ const buildReviewInput = (
116116
return parts.join("\n");
117117
};
118118

119-
// Heuristic: anchor comment near first changed line
120119
const firstChangedPosition = (patch: string): number => {
120+
if (!patch) return 1;
121121
const lines = patch.split("\n");
122+
122123
const start = lines.findIndex((l) => l.startsWith("@@ "));
123-
if (start === -1) return Math.max(1, lines.length - 1);
124+
if (start === -1) {
125+
const firstNonEmpty = lines.findIndex((l) => l.trim().length > 0);
126+
return Math.max(1, (firstNonEmpty === -1 ? 0 : firstNonEmpty) + 1);
127+
}
124128

125-
// Prefer the first real added line (exclude file headers like +++ a/b)
126129
for (let i = start + 1; i < lines.length; i++) {
127130
const line = lines[i];
128131
if (line.startsWith("+") && !line.startsWith("+++")) {
129-
return i + 1; // 1-based for GitHub API
132+
return Math.min(Math.max(1, i + 1), lines.length);
130133
}
134+
if (line.startsWith("@@ ")) break; // stop at next hunk header
131135
}
132136

133-
// If there were no additions (e.g., deletions-only change),
134137
// fall back to the first context line within the first hunk.
135138
for (let i = start + 1; i < lines.length; i++) {
136139
const line = lines[i];
137140
if (line.startsWith(" ")) {
138-
return i + 1;
141+
return Math.min(Math.max(1, i + 1), lines.length);
139142
}
140-
// Stop at the next hunk header
141143
if (line.startsWith("@@ ")) break;
142144
}
143145

144-
// Final fallback: the line immediately after the first hunk header.
145-
return Math.min(lines.length, start + 2);
146+
return Math.min(Math.max(1, start + 2), lines.length);
146147
};
147148

148149
const REVIEW_ON_LGTM = process.env.REVIEW_ON_LGTM === "true" ? true : false;
@@ -305,6 +306,8 @@ export const robot = (app: Probot) => {
305306

306307
const ress = [];
307308
let overallReview: { summary: string } | null = null;
309+
// 인라인 코멘트가 위치 불일치(예: 422)로 실패할 경우, 본문 코멘트로 대체하기 위해 누적하는 버퍼
310+
let inlineFallback: string[] = [];
308311

309312
if (isFileCountWithinLimit) {
310313
const aggregatedPatch = changedFiles
@@ -418,11 +421,21 @@ export const robot = (app: Probot) => {
418421
if (!res.lgtm || (res.lgtm && REVIEW_ON_LGTM)) {
419422
if (!commentablePaths.has(file.filename)) {
420423
log.info(`PR 파일에 patch가 없어 ${file.filename}에 대한 코멘트 생략`);
424+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
421425
continue;
422426
}
423427
// Compute position against the PR-wide patch to avoid misalignment
424428
const prWidePatch = prFilePatchByPath.get(file.filename) || patch;
429+
if (!prWidePatch || !prWidePatch.includes('@@')) {
430+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
431+
continue;
432+
}
433+
425434
const position = firstChangedPosition(prWidePatch);
435+
if (!Number.isSafeInteger(position) || position <= 0) {
436+
inlineFallback.push(`\n### ${file.filename}\n${res.review_comment}`);
437+
continue;
438+
}
426439
ress.push({
427440
path: file.filename,
428441
body: res.review_comment,
@@ -469,11 +482,41 @@ export const robot = (app: Probot) => {
469482
const data = e?.response?.data;
470483
log.error(`리뷰 생성 실패 (status=${status})`);
471484
if (data) {
485+
try { log.error(`GitHub API error payload: ${JSON.stringify(data, null, 2)}`); } catch {}
486+
}
487+
488+
if (status === 422) {
472489
try {
473-
log.error(`GitHub API error payload: ${JSON.stringify(data, null, 2)}`);
474-
} catch {}
490+
let body = "LGTM 👍";
491+
if (overallReview && overallReview.summary) {
492+
body = `<!-- chatgpt-summary:v1 -->\n${overallReview.summary}`;
493+
}
494+
495+
if (typeof inlineFallback !== "undefined" && inlineFallback.length) {
496+
body += `\n\n---\n⚠️ 인라인 주석을 diff에 고정하지 못해 본문에 첨부합니다 (422).` + inlineFallback.join("\n");
497+
} else if (ress.length) {
498+
body += `\n\n(인라인 코멘트가 위치 불일치로 생략되었습니다.)`;
499+
}
500+
501+
if (!isFileCountWithinLimit) {
502+
body += `\n\nNote: 파일 개수가 ${MAX_FILE_COUNT}개를 초과하여 개별 리뷰는 생략되었습니다.`;
503+
}
504+
505+
await context.octokit.pulls.createReview({
506+
repo: repo.repo,
507+
owner: repo.owner,
508+
pull_number: context.pullRequest().pull_number,
509+
body,
510+
event: "COMMENT",
511+
commit_id: commits[commits.length - 1].sha,
512+
});
513+
log.info("Fell back to body-only review after 422.");
514+
} catch (e2: any) {
515+
log.info(`Fallback review creation also failed`, e2);
516+
}
517+
} else {
518+
log.info(`Failed to create review`, e);
475519
}
476-
log.info(`Failed to create review`, e);
477520
}
478521

479522
console.timeEnd("gpt cost");

0 commit comments

Comments
 (0)