Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions handlers/tag-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const MAX_FILE_SIZE = 20000; // 20K 문자 제한 (OpenAI 토큰 안전장치)
* @param {object} prData - PR 객체 (draft, labels 포함)
* @param {string} appToken - GitHub App installation token
* @param {string} openaiApiKey
* @param {string[]|null} [changedFilenames=null] - synchronize 시 변경된 파일명 목록 (null이면 전체 분석)
*/
export async function tagPatterns(
repoOwner,
Expand All @@ -31,7 +32,8 @@ export async function tagPatterns(
headSha,
prData,
appToken,
openaiApiKey
openaiApiKey,
changedFilenames = null
) {
// 2-1. Skip 조건
if (prData.draft === true) {
Expand All @@ -58,22 +60,34 @@ export async function tagPatterns(
}

const allFiles = await filesResponse.json();
const solutionFiles = allFiles.filter(
let solutionFiles = allFiles.filter(
(f) =>
(f.status === "added" || f.status === "modified") &&
SOLUTION_PATH_REGEX.test(f.filename)
);

// changedFilenames가 제공되면 해당 파일만 대상으로 좁힘 (synchronize 최적화)
if (changedFilenames !== null) {
const changedSet = new Set(changedFilenames);
solutionFiles = solutionFiles.filter((f) => changedSet.has(f.filename));
console.log(
`[tagPatterns] PR #${prNumber}: narrowed to ${solutionFiles.length} changed solution files`
);
}

console.log(
`[tagPatterns] PR #${prNumber}: ${allFiles.length} files, ${solutionFiles.length} solution files`
`[tagPatterns] PR #${prNumber}: ${allFiles.length} total files, ${solutionFiles.length} solution files to analyze`
);

if (solutionFiles.length === 0) {
return { skipped: "no-solution-files" };
}

// 2-3. 기존 Bot 패턴 태그 코멘트 삭제
await deletePreviousPatternComments(repoOwner, repoName, prNumber, appToken);
// 2-3. 기존 Bot 패턴 태그 코멘트 삭제 (변경 파일만)
const targetFilenames = solutionFiles.map((f) => f.filename);
await deletePreviousPatternComments(
repoOwner, repoName, prNumber, appToken, targetFilenames
);

// 2-4. 파일별 OpenAI 분석 + 코멘트 작성 (각 파일 try/catch 래핑)
const results = [];
Expand Down Expand Up @@ -101,13 +115,16 @@ export async function tagPatterns(
}

/**
* 기존 Bot 패턴 태그 코멘트 삭제 (다른 사용자 코멘트는 절대 건드리지 않음)
* 기존 Bot 패턴 태그 코멘트 삭제 (대상 파일만, 다른 사용자 코멘트는 절대 건드리지 않음)
*
* @param {string[]} targetFilenames - 삭제 대상 파일명 목록
*/
async function deletePreviousPatternComments(
repoOwner,
repoName,
prNumber,
appToken
appToken,
targetFilenames
) {
const response = await fetch(
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/comments?per_page=100`,
Expand All @@ -122,8 +139,12 @@ async function deletePreviousPatternComments(
}

const comments = await response.json();
const targetSet = new Set(targetFilenames);
const botPatternComments = comments.filter(
(c) => c.user?.type === "Bot" && c.body?.includes(COMMENT_MARKER)
(c) =>
c.user?.type === "Bot" &&
c.body?.includes(COMMENT_MARKER) &&
targetSet.has(c.path)
);

for (const comment of botPatternComments) {
Expand All @@ -149,7 +170,7 @@ async function deletePreviousPatternComments(
}

console.log(
`[tagPatterns] Deleted ${botPatternComments.length} previous pattern comments`
`[tagPatterns] Deleted ${botPatternComments.length} previous pattern comments for ${targetFilenames.length} files`
);
}

Expand Down
47 changes: 44 additions & 3 deletions handlers/webhooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,35 @@ async function handleProjectsV2ItemEvent(payload, env) {
});
}

/**
* Compare API로 두 커밋 사이에 변경된 파일명 목록 조회
*
* @param {string} repoOwner
* @param {string} repoName
* @param {string} baseSha - 이전 커밋 SHA
* @param {string} headSha - 새 커밋 SHA
* @param {string} appToken
* @returns {Promise<string[]|null>} 변경된 파일명 배열 (실패 시 null → 전체 분석 fallback)
*/
async function getChangedFilenames(repoOwner, repoName, baseSha, headSha, appToken) {
const response = await fetch(
`https://api.github.com/repos/${repoOwner}/${repoName}/compare/${baseSha}...${headSha}`,
{ headers: getGitHubHeaders(appToken) }
);

if (!response.ok) {
console.error(`[getChangedFilenames] Compare API failed: ${response.status}`);
return null;
}

const data = await response.json();
return (data.files || []).map((f) => f.filename);
}

/**
* Pull Request 이벤트 처리
* - opened/reopened: Week 설정 체크 + 알고리즘 패턴 태깅
* - synchronize: 알고리즘 패턴 태깅만 (Week 체크 스킵 - 이미 설정됐을 가능성 높음)
* - opened/reopened: Week 설정 체크 + 알고리즘 패턴 태깅 (전체 파일)
* - synchronize: 알고리즘 패턴 태깅만 (변경된 파일만, Week 체크 스킵)
*/
async function handlePullRequestEvent(payload, env) {
const action = payload.action;
Expand Down Expand Up @@ -256,14 +281,30 @@ async function handlePullRequestEvent(payload, env) {
// 알고리즘 패턴 태깅 (OPENAI_API_KEY 있을 때만)
if (env.OPENAI_API_KEY) {
try {
// synchronize일 때만 변경 파일 목록 추출 (최적화: #7)
let changedFilenames = null;
if (action === "synchronize" && payload.before && payload.after) {
changedFilenames = await getChangedFilenames(
repoOwner,
repoName,
payload.before,
payload.after,
appToken
);
console.log(
`[handlePullRequestEvent] synchronize: ${changedFilenames?.length ?? "fallback(all)"} files changed between ${payload.before.slice(0, 7)}...${payload.after.slice(0, 7)}`
);
}

await tagPatterns(
repoOwner,
repoName,
prNumber,
pr.head.sha,
pr,
appToken,
env.OPENAI_API_KEY
env.OPENAI_API_KEY,
changedFilenames
);
} catch (error) {
console.error(`[handlePullRequestEvent] tagPatterns failed: ${error.message}`);
Expand Down