@@ -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
150486150485const 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};
150512150513const REVIEW_ON_LGTM = process.env.REVIEW_ON_LGTM === "true" ? true : false;
150513150514const 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);
0 commit comments