|
33 | 33 | * with a `review_bot: true` flag — they are NOT placed in the `bot` bucket. |
34 | 34 | * - Info bots (Codecov, Dependabot, Renovate, etc.) post status reports and are |
35 | 35 | * placed in the `bot` bucket for silent skipping. |
| 36 | + * - `github-actions` is decided by content, since claude-code-action posts as |
| 37 | + * it when given `github_token`: its inline threads, and its top-level |
| 38 | + * comments carrying a claude-code-action signature, are review feedback. |
36 | 39 | * |
37 | 40 | * Self-review feedback: |
38 | 41 | * - You can't formally "Request changes" on your own PR, so a PR author's own |
@@ -150,6 +153,45 @@ const isReviewBot = (username) => |
150 | 153 | const isInfoBot = (username) => |
151 | 154 | INFO_BOT_PATTERNS.some((pattern) => pattern.test(username ?? "")); |
152 | 155 |
|
| 156 | +/** |
| 157 | + * `github-actions` is a SHARED identity, so its login alone cannot decide a |
| 158 | + * bucket. claude-code-action posts as it whenever a workflow passes |
| 159 | + * `github_token: ${{ github.token }}` (this repo's review and focus workflows |
| 160 | + * do), and the login is spelled `github-actions[bot]` over REST and |
| 161 | + * `github-actions` over GraphQL. Filing those as info-bot noise is how |
| 162 | + * taskless/marketing#73 reported `needs_attention: 0` over four open findings. |
| 163 | + */ |
| 164 | +const SHARED_ACTIONS_BOT = /^github-actions(\[bot\])?$/i; |
| 165 | + |
| 166 | +/** |
| 167 | + * Openings and links that only claude-code-action's comments carry. The |
| 168 | + * in-progress placeholders count too: a placeholder posted as `github-actions` |
| 169 | + * must still hold the wait loop open. |
| 170 | + */ |
| 171 | +const ACTION_SIGNATURES = [ |
| 172 | + /^[*_]*\s*claude finished @/i, |
| 173 | + /\[View job(?: run)?\]\(https:\/\/[^)\s]+\/actions\/runs\/\d+/i, |
| 174 | +]; |
| 175 | + |
| 176 | +/** |
| 177 | + * Whether a comment is a review bot's, deciding `github-actions` by content. |
| 178 | + * |
| 179 | + * An INLINE thread from `github-actions` is review feedback by default: |
| 180 | + * informational bots post top-level status comments, not line-anchored review |
| 181 | + * threads. A top-level `github-actions` comment counts only when it carries a |
| 182 | + * claude-code-action signature; every other one stays informational. |
| 183 | + */ |
| 184 | +const isReviewComment = (author, body, { inline = false } = {}) => { |
| 185 | + if (isReviewBot(author)) return true; |
| 186 | + if (!SHARED_ACTIONS_BOT.test(author ?? "")) return false; |
| 187 | + if (inline) return true; |
| 188 | + const opening = body.trimStart(); |
| 189 | + return ( |
| 190 | + isReviewInProgress(opening) || |
| 191 | + ACTION_SIGNATURES.some((signature) => signature.test(opening)) |
| 192 | + ); |
| 193 | +}; |
| 194 | + |
153 | 195 | /** |
154 | 196 | * The placeholders a review bot posts before it has anything to say. |
155 | 197 | * |
@@ -328,8 +370,17 @@ const matchesUnnegated = (patterns, body) => |
328 | 370 | const categorizeComment = (comment, body) => { |
329 | 371 | const author = comment?.author?.login || comment?.user?.login || ""; |
330 | 372 |
|
331 | | - if (isInfoBot(author) && !isReviewBot(author)) return "bot"; |
| 373 | + if (isInfoBot(author) && !isReviewComment(author, body)) return "bot"; |
332 | 374 |
|
| 375 | + return categorizeContent(comment, body); |
| 376 | +}; |
| 377 | + |
| 378 | +/** |
| 379 | + * The content half of `categorizeComment`, for a caller that has already |
| 380 | + * decided the author is not an info bot (`bucketByAuthor`), so a review |
| 381 | + * comment posted as `github-actions` is not sent back to `bot` by its login. |
| 382 | + */ |
| 383 | +const categorizeContent = (_comment, body) => { |
333 | 384 | const logaf = detectLogaf(body); |
334 | 385 | if (logaf) return logaf; |
335 | 386 |
|
@@ -382,28 +433,30 @@ const categorizeReviewSummary = (_comment, body) => |
382 | 433 | * an info bot's noise (`bot`), or ordinary human feedback, from ANY of the |
383 | 434 | * three sources. |
384 | 435 | * |
385 | | - * `categorize` defaults to the inline-comment classifier; the review-summary |
386 | | - * call site passes `categorizeReviewSummary` instead, so the bot/info-bot |
387 | | - * split and the in-progress check stay one rule in one place while what |
388 | | - * happens to the *content* differs by source. |
| 436 | + * `categorize` defaults to the inline-comment content classifier; the |
| 437 | + * review-summary call site passes `categorizeReviewSummary` instead, so the |
| 438 | + * bot/info-bot split and the in-progress check stay one rule in one place while |
| 439 | + * what happens to the *content* differs by source. `inline` marks a review |
| 440 | + * thread, which decides a `github-actions` author (see `isReviewComment`). |
389 | 441 | */ |
390 | 442 | const bucketByAuthor = ( |
391 | 443 | feedback, |
392 | 444 | item, |
393 | 445 | comment, |
394 | 446 | body, |
395 | 447 | author, |
396 | | - categorize = categorizeComment |
| 448 | + { categorize = categorizeContent, inline = false } = {} |
397 | 449 | ) => { |
| 450 | + const reviewer = isReviewComment(author, body, { inline }); |
398 | 451 | // The author gate is load-bearing, not belt and braces. A human writing |
399 | 452 | // "Review in progress on my end, back by EOD" would otherwise be filed as an |
400 | 453 | // unfinished review, vanish from `needs_attention`, and hang the wait loop |
401 | 454 | // forever: a person's comment never gets edited into a finished form the way |
402 | 455 | // the bot's placeholder does, so the count never drops. Only a review bot has |
403 | 456 | // the lifecycle this bucket describes. |
404 | | - if (isReviewBot(author) && isReviewInProgress(body)) { |
| 457 | + if (reviewer && isReviewInProgress(body)) { |
405 | 458 | feedback.review_in_progress.push(item); |
406 | | - } else if (isReviewBot(author)) { |
| 459 | + } else if (reviewer) { |
407 | 460 | item.review_bot = true; |
408 | 461 | feedback[categorize(comment, body)].push(item); |
409 | 462 | } else if (isInfoBot(author)) { |
@@ -628,14 +681,9 @@ const buildFeedback = (client, { owner, repo, prInfo }) => { |
628 | 681 | ) { |
629 | 682 | feedback.high.push(item); |
630 | 683 | } else { |
631 | | - bucketByAuthor( |
632 | | - feedback, |
633 | | - item, |
634 | | - review, |
635 | | - body, |
636 | | - author, |
637 | | - categorizeReviewSummary |
638 | | - ); |
| 684 | + bucketByAuthor(feedback, item, review, body, author, { |
| 685 | + categorize: categorizeReviewSummary, |
| 686 | + }); |
639 | 687 | } |
640 | 688 | } |
641 | 689 |
|
@@ -671,7 +719,7 @@ const buildFeedback = (client, { owner, repo, prInfo }) => { |
671 | 719 | if (isResolved) { |
672 | 720 | feedback.resolved.push(item); |
673 | 721 | } else { |
674 | | - bucketByAuthor(feedback, item, first, body, author); |
| 722 | + bucketByAuthor(feedback, item, first, body, author, { inline: true }); |
675 | 723 | } |
676 | 724 | } |
677 | 725 |
|
@@ -822,6 +870,7 @@ module.exports = { |
822 | 870 | extractFeedbackItem, |
823 | 871 | isInfoBot, |
824 | 872 | isReviewBot, |
| 873 | + isReviewComment, |
825 | 874 | isReviewInProgress, |
826 | 875 | main, |
827 | 876 | }; |
0 commit comments