Skip to content
Open
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
139 changes: 139 additions & 0 deletions .github/workflows/coderabbit-label-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: CodeRabbit Label Sync

on:
pull_request_target:
types: [opened, reopened, synchronize, edited]
Comment on lines +3 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

pull_request_target runs the workflow from the base branch with write permissions — ensure this is intentional.

Using pull_request_target is appropriate here since the workflow only reads PR metadata via GraphQL and manages labels (no checkout of untrusted code). Just be aware that if someone later adds a checkout step, this becomes a significant security risk for fork-based PRs.

Consider adding a comment in the workflow to document why pull_request_target is used.

🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 3 - 5, Add an
inline comment above the "pull_request_target" trigger documenting that using
pull_request_target is intentional because the workflow only queries PR metadata
and manages labels (no checkout of PR code), note the security risk if a
checkout or other untrusted-code steps are added later (especially for forked
PRs), and optionally document any permission restrictions (e.g., no
write/contents permissions or safe token usage) to make the rationale and
constraints explicit next to the pull_request_target declaration.


pull_request_review:
types: [submitted, edited, dismissed]

pull_request_review_thread:
types: [resolved, unresolved]
Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

GitHub Actions pull_request_review_thread webhook event

💡 Result:

pull_request_review_thread is a GitHub webhook event that fires when there is activity on a pull request review comment thread (i.e., a diff comment “thread”), specifically when the thread is marked as resolved (action: resolved). The webhook payload includes (at least) action, pull_request, thread, and sender. [1]

It is not one of the events that can directly trigger GitHub Actions workflows (it does not appear in the “Events that trigger workflows” list). [2]

If you need an Actions workflow to react to PR review activity, you typically use pull_request_review or pull_request_review_comment (and issue_comment for non-diff PR comments), depending on what you’re targeting. [1]

Sources: [1] GitHub Docs – “Webhook events and payloads” (pull_request_review_thread) ; [2] GitHub Docs – “Events that trigger workflows” (GitHub Actions)


pull_request_review_thread is not supported as a GitHub Actions workflow trigger.

While pull_request_review_thread exists as a GitHub webhook event, it cannot trigger workflows. The workflow will never fire when review threads are resolved or unresolved, defeating this workflow's core purpose.

Use one of these alternatives instead:

  • pull_request_review (already present) combined with issue_comment, or
  • A schedule trigger to periodically poll for resolution state, or
  • workflow_dispatch for manual invocation.
🧰 Tools
🪛 actionlint (1.7.10)

[error] 10-10: unknown Webhook event "pull_request_review_thread". see https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names

(events)

🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 10 - 11, The
workflow is using the unsupported trigger name pull_request_review_thread;
replace it with supported triggers so the workflow can run—remove
pull_request_review_thread and instead rely on pull_request_review (already
present) combined with issue_comment for comment-based thread updates, or add a
schedule trigger to poll thread resolution, or add workflow_dispatch for manual
runs; update the triggers block to use one of these supported events and ensure
any logic that relied on pull_request_review_thread is invoked from the chosen
supported event handlers.


permissions:
contents: read
pull-requests: write
issues: write

jobs:
sync-coderabbit-label:
runs-on: ubuntu-latest
name: Update CodeRabbit Review Label
steps:
- name: Check for unresolved CodeRabbit threads
id: check-threads
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.issue.number;

console.log('Checking PR:', prNumber);

// Query for review threads using GraphQL API
const query = `
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
isResolved
isOutdated
comments(first: 10) {
nodes {
author {
login
}
body
}
}
}
Comment on lines +38 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pagination not handled — PRs with >100 review threads will silently miss unresolved threads.

The GraphQL query fetches at most 100 threads and 10 comments per thread. For large PRs with extensive review activity, some threads will be silently skipped, potentially causing the label to be removed prematurely (false negative).

At minimum, consider checking reviewThreads.pageInfo.hasNextPage and logging a warning when results are truncated. Ideally, paginate through all threads.

🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 38 - 50, The
GraphQL query limits reviewThreads and comments (reviewThreads(first: 100) and
comments(first: 10)) so PRs with >100 threads or >10 comments per thread can be
truncated; update the query-handling to check reviewThreads.pageInfo.hasNextPage
and comments.pageInfo.hasNextPage and at minimum log a warning when either is
true, and ideally implement cursor-based pagination to fetch additional pages
(using endCursor and subsequent queries) to fully iterate all reviewThreads and
their comments before deciding label changes.

}
}
}
}
`;

const variables = {
owner: context.repo.owner,
repo: context.repo.repo,
prNumber: parseInt(prNumber)
};

const result = await github.graphql(query, variables);
const threads = result.repository.pullRequest.reviewThreads.nodes;

console.log(`Total review threads: ${threads.length}`);

// Filter for unresolved CodeRabbit threads
const unresolvedCodeRabbitThreads = threads.filter(thread => {
// Skip resolved or outdated threads
if (thread.isResolved || thread.isOutdated) return false;

// Check if any comment in the thread is from CodeRabbit
const hasCodeRabbitComment = thread.comments.nodes.some(comment =>
comment.author?.login === 'coderabbitai' ||
comment.author?.login === 'coderabbitai[bot]'
);

return hasCodeRabbitComment;
});

console.log('Unresolved CodeRabbit threads:', unresolvedCodeRabbitThreads.length);

core.setOutput('has-unresolved', unresolvedCodeRabbitThreads.length > 0);
core.setOutput('unresolved-count', unresolvedCodeRabbitThreads.length);
core.setOutput('pr-number', prNumber);
Comment on lines +84 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider consolidating into a single step.

The outputs from step 1 are only consumed by step 2 within the same job. You could combine both steps into a single github-script step, which would eliminate the output-passing mechanism entirely (and the injection concern above). The filtering logic and label management are simple enough to coexist in one script.

🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 84 - 86, The
workflow currently writes intermediate outputs with core.setOutput
('has-unresolved', 'unresolved-count', 'pr-number') to pass data to a subsequent
step; instead, merge the producer and consumer into one github-script step so
you can directly call the filtering/labeling logic without
core.setOutput/steps.<...>.outputs: move the unresolvedCodeRabbitThreads
filtering and label management into the same github-script invocation (use the
existing unresolvedCodeRabbitThreads and prNumber variables directly), remove
the separate step that reads those outputs, and delete the now-unneeded
core.setOutput calls to eliminate the output-passing and injection surface.


- name: Update unresolved-code-rabbit-review label
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = parseInt('${{ steps.check-threads.outputs.pr-number }}');
const hasUnresolved = '${{ steps.check-threads.outputs.has-unresolved }}' === 'true';
const unresolvedCount = parseInt('${{ steps.check-threads.outputs.unresolved-count }}');
Comment on lines +92 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid ${{ }} interpolation inside script: blocks — use environment variables instead.

Directly embedding ${{ steps.*.outputs.* }} in JavaScript strings is a known injection anti-pattern in GitHub Actions. While these particular outputs are set by your own prior step and are currently safe, this pattern is fragile and flagged by security linters. Pass values through env instead:

Proposed fix
       uses: actions/github-script@v7
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
+        env:
+          PR_NUMBER: ${{ steps.check-threads.outputs.pr-number }}
+          HAS_UNRESOLVED: ${{ steps.check-threads.outputs.has-unresolved }}
+          UNRESOLVED_COUNT: ${{ steps.check-threads.outputs.unresolved-count }}
         script: |
-          const prNumber = parseInt('${{ steps.check-threads.outputs.pr-number }}');
-          const hasUnresolved = '${{ steps.check-threads.outputs.has-unresolved }}' === 'true';
-          const unresolvedCount = parseInt('${{ steps.check-threads.outputs.unresolved-count }}');
+          const prNumber = parseInt(process.env.PR_NUMBER);
+          const hasUnresolved = process.env.HAS_UNRESOLVED === 'true';
+          const unresolvedCount = parseInt(process.env.UNRESOLVED_COUNT);

Note: env should be a sibling of script under with, or placed at the step level. At step level:

      - name: Update unresolved-code-rabbit-review label
        uses: actions/github-script@v7
        env:
          PR_NUMBER: ${{ steps.check-threads.outputs.pr-number }}
          HAS_UNRESOLVED: ${{ steps.check-threads.outputs.has-unresolved }}
          UNRESOLVED_COUNT: ${{ steps.check-threads.outputs.unresolved-count }}
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const prNumber = parseInt(process.env.PR_NUMBER);
            const hasUnresolved = process.env.HAS_UNRESOLVED === 'true';
            const unresolvedCount = parseInt(process.env.UNRESOLVED_COUNT);
🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 92 - 95, The script
currently interpolates GitHub Actions outputs directly into JS strings
(prNumber, hasUnresolved, unresolvedCount), which is insecure; instead, expose
those outputs as environment variables on the step (e.g., PR_NUMBER,
HAS_UNRESOLVED, UNRESOLVED_COUNT) and read them via process.env inside the
script (parseInt(process.env.PR_NUMBER), process.env.HAS_UNRESOLVED === 'true',
parseInt(process.env.UNRESOLVED_COUNT')); set the env either at the step level
or as a sibling of script under with when using actions/github-script@v7.


console.log(`PR #${prNumber}: Has unresolved threads: ${hasUnresolved} (${unresolvedCount})`);

// Get current labels
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});

const existingLabel = currentLabels.find(
label => label.name === 'unresolved-code-rabbit-review'
);

// Apply label if unresolved comments exist
if (hasUnresolved && !existingLabel) {
console.log('Applying unresolved-code-rabbit-review label');

await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['unresolved-code-rabbit-review']
});
}

// Remove label if all comments resolved
if (!hasUnresolved && existingLabel) {
console.log('Removing unresolved-code-rabbit-review label');

await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
name: 'unresolved-code-rabbit-review'
});
}
Comment on lines +110 to +132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No error handling around label API calls.

If addLabels or removeLabel fails (e.g., due to a race condition where another workflow run modifies labels concurrently, or the label was already removed), the workflow will fail with an unhandled exception. Consider wrapping these calls in try/catch blocks and logging the error gracefully, since label sync is not a critical-path operation.

Also note that addLabels will auto-create the unresolved-code-rabbit-review label with a default color if it doesn't already exist in the repository. You may want to pre-create the label with a specific color/description, or add a setup step.

🤖 Prompt for AI Agents
In @.github/workflows/coderabbit-label-sync.yml around lines 110 - 132, Wrap the
GitHub Issues API calls github.rest.issues.addLabels and
github.rest.issues.removeLabel (the blocks guarded by
hasUnresolved/existingLabel) in try/catch so API failures don't abort the
workflow: on failure log a descriptive message including prNumber, the action
attempted and the caught error (use console.error or processLogger), and do not
rethrow; for removeLabel consider suppressing 404 errors (label not found) while
still logging other errors. This keeps label sync best-effort while preserving
the existing hasUnresolved/existingLabel logic.


if (
(!hasUnresolved && !existingLabel) ||
(hasUnresolved && existingLabel)
) {
console.log('Label already in correct state. No changes needed.');
}
Loading