diff --git a/.github/scripts/cleanup-claude-comments.js b/.github/scripts/cleanup-claude-comments.js new file mode 100644 index 00000000..634d4bd7 --- /dev/null +++ b/.github/scripts/cleanup-claude-comments.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node + +/** + * Script to clean up multiple Claude bot comments on a PR + * Keeps only the most recent successful review and collapses others + */ + +async function cleanupClaudeComments({ github, context, core }) { + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + try { + // Get all comments on the PR + const allComments = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const { data } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + per_page: 100, + page + }); + + allComments.push(...data); + hasMore = data.length === 100; + page++; + } + + // Filter Claude bot comments + const claudeComments = allComments + .filter(comment => + comment.user.login === 'claude[bot]' || + comment.user.login === 'claude' || + (comment.user.type === 'Bot' && comment.body.includes('Claude')) + ) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + if (claudeComments.length <= 1) { + core.info(`Found ${claudeComments.length} Claude comments, no cleanup needed`); + return; + } + + core.info(`Found ${claudeComments.length} Claude comments, cleaning up...`); + + // Categorize comments + const successfulReviews = []; + const errorComments = []; + const statusComments = []; + + for (const comment of claudeComments) { + if (comment.body.includes('Claude finished') && comment.body.includes('## 📋 Summary')) { + successfulReviews.push(comment); + } else if (comment.body.includes('Claude encountered an error')) { + errorComments.push(comment); + } else if (comment.body.includes('Claude Code is analyzing')) { + statusComments.push(comment); + } + } + + // Keep the most recent successful review visible + const commentsToCollapse = []; + let keptReview = false; + + if (successfulReviews.length > 0) { + // Keep the first (most recent) successful review + keptReview = true; + commentsToCollapse.push(...successfulReviews.slice(1)); + } + + // Collapse all error and status comments + commentsToCollapse.push(...errorComments, ...statusComments); + + // If no successful review, keep the most recent comment of any type + if (!keptReview && claudeComments.length > 0) { + commentsToCollapse.push(...claudeComments.slice(1)); + } + + // Process comments to collapse + for (const comment of commentsToCollapse) { + try { + const timestamp = new Date(comment.created_at).toLocaleString(); + const commentType = + comment.body.includes('encountered an error') ? 'error' : + comment.body.includes('is analyzing') ? 'status' : + comment.body.includes('finished') ? 'review' : 'comment'; + + // Collapse the comment + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: comment.id, + body: `
Claude ${commentType} from ${timestamp} (outdated - click to expand)\n\n${comment.body}\n
` + }); + + core.info(`Collapsed Claude ${commentType} comment ${comment.id} from ${timestamp}`); + } catch (error) { + // If update fails, try to delete (might not have permission) + try { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id + }); + core.info(`Deleted Claude comment ${comment.id}`); + } catch (deleteError) { + core.warning(`Failed to update or delete comment ${comment.id}: ${error.message}`); + } + } + } + + core.info(`Cleanup complete. Collapsed ${commentsToCollapse.length} comments`); + + } catch (error) { + core.error(`Failed to cleanup Claude comments: ${error.message}`); + throw error; + } +} + +// Export for use in GitHub Actions +module.exports = cleanupClaudeComments; \ No newline at end of file diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 165a43cf..54614624 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -148,4 +148,17 @@ jobs: HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} CHANGED_FILES: ${{ github.event.pull_request.changed_files }} ADDITIONS: ${{ github.event.pull_request.additions }} - DELETIONS: ${{ github.event.pull_request.deletions }} \ No newline at end of file + DELETIONS: ${{ github.event.pull_request.deletions }} + + - name: Clean up old Claude comments + if: steps.check-review.outputs.skip != 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // Wait a bit to ensure the new comment is posted + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Use the cleanup script + const cleanup = require('./.github/scripts/cleanup-claude-comments.js'); + await cleanup({ github, context, core }); \ No newline at end of file diff --git a/.github/workflows/cleanup-claude-comments.yml b/.github/workflows/cleanup-claude-comments.yml new file mode 100644 index 00000000..e935b3e4 --- /dev/null +++ b/.github/workflows/cleanup-claude-comments.yml @@ -0,0 +1,89 @@ +name: Cleanup Claude Comments + +on: + # Manual trigger + workflow_dispatch: + inputs: + pr_number: + description: 'Pull Request number to clean up' + required: true + type: number + + # Also run when PR is closed or merged + pull_request: + types: [closed] + + # Run on a schedule to clean up old PRs + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout scripts + uses: actions/checkout@v4 + with: + sparse-checkout: | + .github/scripts/cleanup-claude-comments.js + + - name: Cleanup Claude comments on specific PR + if: github.event_name == 'workflow_dispatch' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const cleanup = require('./.github/scripts/cleanup-claude-comments.js'); + + // Override context for manual trigger + const customContext = { + ...context, + issue: { number: ${{ github.event.inputs.pr_number }} } + }; + + await cleanup({ github, context: customContext, core }); + + - name: Cleanup Claude comments on closed PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const cleanup = require('./.github/scripts/cleanup-claude-comments.js'); + await cleanup({ github, context, core }); + + - name: Cleanup Claude comments on all recent PRs + if: github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const cleanup = require('./.github/scripts/cleanup-claude-comments.js'); + + // Get recent PRs + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + sort: 'updated', + direction: 'desc', + per_page: 50 + }); + + for (const pr of prs) { + core.info(`Checking PR #${pr.number}: ${pr.title}`); + + const customContext = { + ...context, + issue: { number: pr.number } + }; + + try { + await cleanup({ github, context: customContext, core }); + } catch (error) { + core.warning(`Failed to cleanup PR #${pr.number}: ${error.message}`); + } + } \ No newline at end of file