diff --git a/src/bugbotMonitor.ts b/src/bugbotMonitor.ts index 7c6ffe4..de558f8 100644 --- a/src/bugbotMonitor.ts +++ b/src/bugbotMonitor.ts @@ -229,8 +229,8 @@ export class BugbotMonitor { // ============================================================ private computeSinceForRepo(repo: string): string | undefined { - if (this.state.hasFailedBugsForRepo(repo)) { - logger.debug("Skipping since filter to retry failed bugs.", { repo }); + if (this.state.hasRetryableBugsForRepo(repo)) { + logger.debug("Skipping since filter to retry failed/skipped bugs.", { repo }); return undefined; } const lookbackMs = DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1000; diff --git a/src/githubClient.ts b/src/githubClient.ts index d0812e3..ef6a284 100644 --- a/src/githubClient.ts +++ b/src/githubClient.ts @@ -393,6 +393,39 @@ export class GitHubClient { return data.id; } + + async hasIssueCommentContaining( + owner: string, + repo: string, + prNumber: number, + marker: string + ): Promise { + logger.debug("Checking for existing issue comment with marker.", { + owner, + repo, + prNumber, + }); + + const octokit = await this.getOctokitForOwner(owner); + + for await (const response of octokit.paginate.iterator( + octokit.rest.issues.listComments, + { + owner, + repo, + issue_number: prNumber, + per_page: 100, + } + )) { + for (const comment of response.data) { + if (comment.body?.includes(marker)) { + return true; + } + } + } + + return false; + } } // ============================================================ diff --git a/src/main.ts b/src/main.ts index 1dd1840..1cd8d4c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,7 @@ import { StateStore } from "./state.js"; import type { Config, FixResult, PrBugReport } from "./types.js"; const AUTOFIX_COMMENT_MARKER = ""; +const AUTOFIX_NO_CHANGES_MARKER = ""; class FixoolyDaemon { private config: Config; @@ -211,17 +212,19 @@ class FixoolyDaemon { } ); } else { + await this.postNoChangesComment(pr, bugs); + this.state.recordProcessedBugs( bugs.map((b) => ({ bugId: b.bugId, repo: repoFullName, prNumber: pr.number, })), - null + "SKIPPED_NO_CHANGES" ); logger.info( - `No changes made for PR #${pr.number}. Bugs recorded as processed.`, + `No changes made for PR #${pr.number}. Bugs recorded as skipped.`, { prNumber: pr.number, repo: repoFullName } ); } @@ -289,6 +292,59 @@ class FixoolyDaemon { } } + // ============================================================ + // Post no-changes comment on the PR + // ============================================================ + + private async postNoChangesComment( + pr: { owner: string; repo: string; number: number }, + bugs: import("./types.js").BugbotBug[] + ): Promise { + try { + const alreadyPosted = await this.github.hasIssueCommentContaining( + pr.owner, + pr.repo, + pr.number, + AUTOFIX_NO_CHANGES_MARKER + ); + if (alreadyPosted) { + logger.debug("No-changes comment already exists on PR, skipping.", { + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + return; + } + + const bugList = bugs + .map((b) => `- ⏭️ Skipped: **${b.title}**`) + .join("\n"); + + const body = + `${AUTOFIX_COMMENT_MARKER}\n${AUTOFIX_NO_CHANGES_MARKER}\n` + + `[Fixooly](https://github.com/Senna46/fixooly) ` + + `analyzed ${bugs.length} bug(s) but determined no code changes were needed.\n\n` + + bugList; + + await this.github.createIssueComment( + pr.owner, + pr.repo, + pr.number, + body + ); + logger.debug("Posted no-changes comment on PR.", { + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn("Failed to post no-changes comment on PR.", { + error: message, + prNumber: pr.number, + repo: `${pr.owner}/${pr.repo}`, + }); + } + } + // ============================================================ // Shutdown // ============================================================ diff --git a/src/state.ts b/src/state.ts index 2fb417b..00b8d7e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -50,11 +50,11 @@ export class StateStore { const row = this.db .prepare("SELECT fix_commit_sha FROM processed_bugs WHERE bug_id = ?") .get(bugId) as { fix_commit_sha: string | null } | undefined; - // Bugs marked as FAILED should be retried + // Only FAILED bugs should be retried; SKIPPED_NO_CHANGES is terminal return row !== undefined && row.fix_commit_sha !== "FAILED"; } - hasFailedBugsForRepo(repo: string): boolean { + hasRetryableBugsForRepo(repo: string): boolean { const row = this.db .prepare( "SELECT 1 FROM processed_bugs WHERE fix_commit_sha = 'FAILED' AND repo = ? LIMIT 1"