ci: Fix several robustness issues in claude-review workflow (#41115)

- Use github.paginate() for listComments to handle PRs with 100+
comments
- Make line optional in review schema to allow file-level comments
- Skip createReviewComment for comments without a line number
- Fix failed count to exclude skipped file-level comments
- Pass review result via env var instead of expression injection
- Use core.warning() instead of console.log() for JSON parse failures
- Fix MARKER insertion for single-line summaries that have no newline
- Require "@claude review" instead of just "@claude" to trigger

Co-developed-by: Claude <claude@anthropic.com>
This commit is contained in:
Zbigniew Jędrzejewski-Szmek
2026-03-16 09:55:50 +01:00
committed by GitHub
+23 -18
View File
@@ -32,13 +32,13 @@ jobs:
github.repository_owner == 'systemd' &&
((github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@claude') &&
contains(github.event.comment.body, '@claude review') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(github.event.comment.body, '@claude review') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
contains(github.event.review.body, '@claude review') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.review.author_association)))
permissions:
@@ -71,12 +71,10 @@ jobs:
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const MARKER = "<!-- claude-pr-review -->";
const {data: issueComments} = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const issueComments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: prNumber, per_page: 100 },
);
const existing = issueComments.find((c) => c.body && c.body.includes(MARKER));
@@ -146,7 +144,7 @@ jobs:
"type": "array",
"items": {
"type": "object",
"required": ["file", "line", "severity", "body"],
"required": ["file", "severity", "body"],
"properties": {
"file": {
"type": "string",
@@ -176,6 +174,7 @@ jobs:
# so it cannot post comments or modify the PR.
github_token: ${{ secrets.GITHUB_TOKEN }}
track_progress: false
show_full_output: "true"
additional_permissions: |
actions: read
claude_args: |
@@ -330,6 +329,7 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
env:
STRUCTURED_OUTPUT: ${{ needs.review.outputs.structured_output }}
REVIEW_RESULT: ${{ needs.review.result }}
PR_NUMBER: ${{ needs.setup.outputs.pr_number }}
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
COMMENT_ID: ${{ needs.setup.outputs.comment_id }}
@@ -345,7 +345,7 @@ jobs:
/* If the review job failed or was cancelled, update the tracking
* comment to reflect that and bail out. */
if ("${{ needs.review.result }}" !== "success") {
if (process.env.REVIEW_RESULT !== "success") {
await github.rest.issues.updateComment({
owner,
repo,
@@ -371,7 +371,7 @@ jobs:
if (typeof review.summary === "string")
summary = review.summary;
} catch (e) {
console.log(`Failed to parse structured output: ${e.message}`);
core.warning(`Failed to parse structured output: ${e.message}`);
}
}
@@ -381,8 +381,13 @@ jobs:
* comments is handled by Claude in the prompt, so we just post whatever
* it returns. Using individual comments (rather than a review) means
* re-runs only add new comments instead of creating a whole new review. */
const inlineComments = comments.filter((c) => c.line);
const skipped = comments.length - inlineComments.length;
if (skipped > 0)
console.log(`Skipping ${skipped} file-level comment(s) (no line number).`);
let posted = 0;
for (const c of comments) {
for (const c of inlineComments) {
console.log(` Posting comment on ${c.file}:${c.line}`);
try {
await github.rest.pulls.createReviewComment({
@@ -403,19 +408,19 @@ jobs:
}
if (posted > 0)
console.log(`Posted ${posted}/${comments.length} inline comment(s).`);
else if (comments.length > 0)
console.log(`Could not post any of ${comments.length} inline comment(s) — see warnings above.`);
console.log(`Posted ${posted}/${inlineComments.length} inline comment(s).`);
else if (inlineComments.length > 0)
console.log(`Could not post any of ${inlineComments.length} inline comment(s) — see warnings above.`);
else
console.log("No inline comments to post.");
const failed = comments.length > 0 && posted < comments.length;
const failed = inlineComments.length > 0 && posted < inlineComments.length;
/* Update the tracking comment with Claude's summary. */
if (!summary)
summary = "Claude review: no issues found :tada:\n\n" + MARKER;
else if (!summary.includes(MARKER))
summary = summary.replace(/\n/, `\n${MARKER}\n`);
summary += "\n\n" + MARKER;
summary += `\n\n[Workflow run](${runUrl})`;
await github.rest.issues.updateComment({