-
Notifications
You must be signed in to change notification settings - Fork 0
[NO JIRA] Moving claude code bot reaction and comments collection job from claude-code-action repo #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NO JIRA] Moving claude code bot reaction and comments collection job from claude-code-action repo #30
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,344 @@ | ||||||||||||||||||||||||||||||
| name: Claude Bot Feedback Summary | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||||||||||
| # Run every night at 1:00 AM UTC | ||||||||||||||||||||||||||||||
| - cron: '0 1 * * *' | ||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||
| hours_lookback: | ||||||||||||||||||||||||||||||
| description: 'Hours to look back for comments (default: 24)' | ||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||
| default: '24' | ||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||
| GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||
| gather-feedback: | ||||||||||||||||||||||||||||||
| runs-on: gha-production-medium | ||||||||||||||||||||||||||||||
| container: ci-images-release.arti.tw.ee/actions_base_24_04 | ||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||
| - name: Gather Claude Bot Feedback | ||||||||||||||||||||||||||||||
| uses: actions/github-script@v7 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| github-token: ${{ secrets.GH_ACCESS_TOKEN }} | ||||||||||||||||||||||||||||||
| script: | | ||||||||||||||||||||||||||||||
| const org = 'transferwise'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| async function fetchRepositoriesWithWorkflowFile() { | ||||||||||||||||||||||||||||||
| const query = `filename:call-ci-ai-agents.yml path:.github/workflows org:${org}`; | ||||||||||||||||||||||||||||||
| const repositories = new Set(); | ||||||||||||||||||||||||||||||
| const perPage = 100; | ||||||||||||||||||||||||||||||
| const maxResults = 1000; // GitHub search API cap | ||||||||||||||||||||||||||||||
| let page = 1; | ||||||||||||||||||||||||||||||
| let fetched = 0; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| while (fetched < maxResults) { | ||||||||||||||||||||||||||||||
| const { data } = await github.rest.search.code({ | ||||||||||||||||||||||||||||||
| q: query, | ||||||||||||||||||||||||||||||
| per_page: perPage, | ||||||||||||||||||||||||||||||
| page | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (const item of data.items) { | ||||||||||||||||||||||||||||||
| if (item.repository?.full_name) { | ||||||||||||||||||||||||||||||
| repositories.add(item.repository.full_name); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fetched += data.items.length; | ||||||||||||||||||||||||||||||
| if (data.items.length < perPage) break; | ||||||||||||||||||||||||||||||
| page++; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return Array.from(repositories); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const repositories = await fetchRepositoriesWithWorkflowFile(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const hoursLookback = parseInt('${{ github.event.inputs.hours_lookback }}' || '24'); | ||||||||||||||||||||||||||||||
| const cutoffTime = new Date(Date.now() - hoursLookback * 60 * 60 * 1000); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log(`Looking for Claude bot feedback since: ${cutoffTime.toISOString()}`); | ||||||||||||||||||||||||||||||
| console.log(`Scanning ${repositories.length} repositories...`); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const results = { | ||||||||||||||||||||||||||||||
| totalClaudeComments: 0, | ||||||||||||||||||||||||||||||
| totalReactions: 0, | ||||||||||||||||||||||||||||||
| totalReplies: 0, | ||||||||||||||||||||||||||||||
| reactionBreakdown: {}, | ||||||||||||||||||||||||||||||
| feedbackEntries: [], // Single list for all feedback (reactions + replies) | ||||||||||||||||||||||||||||||
| claudeUsageEntries: [] | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const reactionEmojis = { | ||||||||||||||||||||||||||||||
| '+1': '👍', | ||||||||||||||||||||||||||||||
| '-1': '👎', | ||||||||||||||||||||||||||||||
| 'laugh': '😄', | ||||||||||||||||||||||||||||||
| 'hooray': '🎉', | ||||||||||||||||||||||||||||||
| 'confused': '😕', | ||||||||||||||||||||||||||||||
| 'heart': '❤️', | ||||||||||||||||||||||||||||||
| 'rocket': '🚀', | ||||||||||||||||||||||||||||||
| 'eyes': '👀' | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (const repo of repositories) { | ||||||||||||||||||||||||||||||
| const [owner, repoName] = repo.split('/'); | ||||||||||||||||||||||||||||||
| console.log(`\nProcessing ${repo}...`); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| // Get PRs updated recently - use manual pagination with early exit | ||||||||||||||||||||||||||||||
| const recentPRs = []; | ||||||||||||||||||||||||||||||
| let page = 1; | ||||||||||||||||||||||||||||||
| let hasMore = true; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| while (hasMore) { | ||||||||||||||||||||||||||||||
| const { data: prs } = await github.rest.pulls.list({ | ||||||||||||||||||||||||||||||
| owner, | ||||||||||||||||||||||||||||||
| repo: repoName, | ||||||||||||||||||||||||||||||
| state: 'all', | ||||||||||||||||||||||||||||||
| sort: 'updated', | ||||||||||||||||||||||||||||||
| direction: 'desc', | ||||||||||||||||||||||||||||||
| per_page: 100, | ||||||||||||||||||||||||||||||
| page | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (prs.length === 0) { | ||||||||||||||||||||||||||||||
| hasMore = false; | ||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (const pr of prs) { | ||||||||||||||||||||||||||||||
| if (new Date(pr.updated_at) >= cutoffTime) { | ||||||||||||||||||||||||||||||
| recentPRs.push(pr); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| // PRs are sorted by updated_at desc, so we can stop once we hit old PRs | ||||||||||||||||||||||||||||||
| hasMore = false; | ||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If we processed all PRs on this page and they're all recent, get next page | ||||||||||||||||||||||||||||||
| if (hasMore && prs.length === 100) { | ||||||||||||||||||||||||||||||
| page++; | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| hasMore = false; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| console.log(`Found ${recentPRs.length} recently updated PRs in ${repo}`); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (const pr of recentPRs) { | ||||||||||||||||||||||||||||||
| console.log(` Checking PR #${pr.number}...`); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get issue comments with since filter to limit results | ||||||||||||||||||||||||||||||
| const { data: issueComments } = await github.rest.issues.listComments({ | ||||||||||||||||||||||||||||||
| owner, | ||||||||||||||||||||||||||||||
| repo: repoName, | ||||||||||||||||||||||||||||||
| issue_number: pr.number, | ||||||||||||||||||||||||||||||
| since: cutoffTime.toISOString(), | ||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get review comments with since filter | ||||||||||||||||||||||||||||||
| const { data: reviewComments } = await github.rest.pulls.listReviewComments({ | ||||||||||||||||||||||||||||||
| owner, | ||||||||||||||||||||||||||||||
| repo: repoName, | ||||||||||||||||||||||||||||||
| pull_number: pr.number, | ||||||||||||||||||||||||||||||
| since: cutoffTime.toISOString(), | ||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const allComments = [...issueComments, ...reviewComments]; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Find Claude bot comments (checking for common bot usernames) | ||||||||||||||||||||||||||||||
| const claudeComments = allComments.filter(comment => { | ||||||||||||||||||||||||||||||
| const username = comment.user?.login?.toLowerCase() || ''; | ||||||||||||||||||||||||||||||
| return username.includes('claude') || | ||||||||||||||||||||||||||||||
| (username === 'github-actions[bot]' && comment.body?.includes('Claude')) || | ||||||||||||||||||||||||||||||
| username.includes('claude-bot') || | ||||||||||||||||||||||||||||||
| username.includes('claudebot'); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (const claudeComment of claudeComments) { | ||||||||||||||||||||||||||||||
| results.totalClaudeComments++; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Capture usage details | ||||||||||||||||||||||||||||||
| const lines = (claudeComment.body || '').split('\n'); | ||||||||||||||||||||||||||||||
| // Find first non-empty line | ||||||||||||||||||||||||||||||
| const titleLine = lines.find(l => l.trim().length > 0) || 'No content'; | ||||||||||||||||||||||||||||||
| // Clean title: remove markdown headers, links, and URLs, then truncate | ||||||||||||||||||||||||||||||
| let cleanTitle = titleLine.replace(/^#+\s*/, ''); | ||||||||||||||||||||||||||||||
| cleanTitle = cleanTitle.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); // Remove markdown links | ||||||||||||||||||||||||||||||
| cleanTitle = cleanTitle.replace(/https?:\/\/\S+/g, ''); // Remove raw URLs | ||||||||||||||||||||||||||||||
| cleanTitle = cleanTitle.replace(/\s+/g, ' '); // Normalize spaces | ||||||||||||||||||||||||||||||
| const title = cleanTitle.trim().substring(0, 80); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| results.claudeUsageEntries.push({ | ||||||||||||||||||||||||||||||
| repo, | ||||||||||||||||||||||||||||||
| prNumber: pr.number, | ||||||||||||||||||||||||||||||
| prUrl: pr.html_url, | ||||||||||||||||||||||||||||||
| commentTitle: title, | ||||||||||||||||||||||||||||||
| commentUrl: claudeComment.html_url, | ||||||||||||||||||||||||||||||
| dateTime: claudeComment.created_at | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get reactions for this comment (no pagination needed, reactions are typically few) | ||||||||||||||||||||||||||||||
| let reactions = []; | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| if (claudeComment.pull_request_review_id) { | ||||||||||||||||||||||||||||||
| // This is a review comment | ||||||||||||||||||||||||||||||
| const { data } = await github.rest.reactions.listForPullRequestReviewComment({ | ||||||||||||||||||||||||||||||
| owner, | ||||||||||||||||||||||||||||||
| repo: repoName, | ||||||||||||||||||||||||||||||
| comment_id: claudeComment.id, | ||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| reactions = data; | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| // This is an issue comment | ||||||||||||||||||||||||||||||
| const { data } = await github.rest.reactions.listForIssueComment({ | ||||||||||||||||||||||||||||||
| owner, | ||||||||||||||||||||||||||||||
| repo: repoName, | ||||||||||||||||||||||||||||||
| comment_id: claudeComment.id, | ||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| reactions = data; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||
| console.log(`Could not fetch reactions for comment ${claudeComment.id}: ${e.message}`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Filter reactions to only those in our time window | ||||||||||||||||||||||||||||||
| const recentReactions = reactions.filter(r => new Date(r.created_at) >= cutoffTime); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Add each reaction as a feedback entry | ||||||||||||||||||||||||||||||
| for (const reaction of recentReactions) { | ||||||||||||||||||||||||||||||
| results.totalReactions++; | ||||||||||||||||||||||||||||||
| results.reactionBreakdown[reaction.content] = (results.reactionBreakdown[reaction.content] || 0) + 1; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| results.feedbackEntries.push({ | ||||||||||||||||||||||||||||||
| dateTime: reaction.created_at, | ||||||||||||||||||||||||||||||
| repo, | ||||||||||||||||||||||||||||||
| prNumber: pr.number, | ||||||||||||||||||||||||||||||
| prUrl: pr.html_url, | ||||||||||||||||||||||||||||||
| comment: null, | ||||||||||||||||||||||||||||||
| reaction: reactionEmojis[reaction.content] || reaction.content, | ||||||||||||||||||||||||||||||
| user: reaction.user?.login, | ||||||||||||||||||||||||||||||
| claudeCommentUrl: claudeComment.html_url | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Find replies to Claude's comments (comments created after Claude's comment that might be replies) | ||||||||||||||||||||||||||||||
| const potentialReplies = allComments.filter(comment => { | ||||||||||||||||||||||||||||||
| if (comment.id === claudeComment.id) return false; | ||||||||||||||||||||||||||||||
| const commentTime = new Date(comment.created_at); | ||||||||||||||||||||||||||||||
| const claudeTime = new Date(claudeComment.created_at); | ||||||||||||||||||||||||||||||
| // Check if comment is after Claude's and within our time window | ||||||||||||||||||||||||||||||
| return commentTime > claudeTime && | ||||||||||||||||||||||||||||||
| commentTime >= cutoffTime && | ||||||||||||||||||||||||||||||
| comment.user?.login?.toLowerCase() !== 'claude' && | ||||||||||||||||||||||||||||||
| !comment.user?.login?.toLowerCase().includes('claude'); | ||||||||||||||||||||||||||||||
|
Comment on lines
+239
to
+243
|
||||||||||||||||||||||||||||||
| // Check if comment is after Claude's and within our time window | |
| return commentTime > claudeTime && | |
| commentTime >= cutoffTime && | |
| comment.user?.login?.toLowerCase() !== 'claude' && | |
| !comment.user?.login?.toLowerCase().includes('claude'); | |
| const login = comment.user?.login?.toLowerCase() || ''; | |
| // Check if comment is after Claude's and within our time window and not from a bot user | |
| return commentTime > claudeTime && | |
| commentTime >= cutoffTime && | |
| login !== 'claude' && | |
| login !== 'claude-bot' && | |
| login !== 'claudebot' && | |
| login !== 'github-actions[bot]' && | |
| !login.endsWith('[bot]'); |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic checks if truncatedComment.length === 100 but should check if the original reply.body?.substring(0, 100) operation actually truncated content by comparing against reply.body?.length. A comment with exactly 100 characters won't get the ellipsis, while one with 101+ characters will, but this check doesn't accurately reflect whether truncation occurred.
| const commentDisplay = truncatedComment.length === 100 ? truncatedComment + '...' : truncatedComment; | |
| const commentDisplay = reply.body && reply.body.length > 100 ? truncatedComment + '...' : truncatedComment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Claude bot detection logic is duplicated in two places (lines 158-163 and lines 242-243). Consider extracting this into a helper function to ensure consistency and easier maintenance.