From f6a2e2bc91ad9912c349b6531dcbc493e0657de5 Mon Sep 17 00:00:00 2001 From: Vaibhav Rathore Date: Tue, 22 Sep 2026 14:25:48 +0530 Subject: [PATCH] fix(code-review): post the review comment from the workflow, not the model The action auto-detects "agent" mode on pull_request events. Agent mode posts nothing to the PR on its own, and use_sticky_comment is inert there. The review only reached the PR when the model happened to call `gh pr comment` itself, which it did on 4 of the last 14 runs. The other 10 finished green with the full review sitting in the job summary and no comment on the PR. Take the model out of the delivery path. A new step reads the action's execution_file output, pulls the closing message, and posts it. The prompt now tells the model to make its final message the review and to post nothing, and `gh pr comment` is off the default tool list. The step also honours use_sticky_comment for real by updating its own earlier comment, matched on a hidden marker and a bot author. It uses GITHUB_TOKEN because the action revokes its app token in its own final step. Also fix guidelines_dir, which has never reached the model. It was interpolated inside the default value of another workflow_call input, where GitHub cannot resolve it, so every prompt so far has read "Check if the directory `` exists". The default prompt now carries a {{GUIDELINES_DIR}} placeholder that a job step substitutes. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/claude-code-review.yml | 87 ++++++++++++++++++++++-- automated-code-review/README.md | 2 +- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 16cfd57..06b1ce4 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -19,7 +19,7 @@ on: ## Instructions 1. **Read Project Guidelines First** - Check if the directory `${{ inputs.guidelines_dir }}` exists. + Check if the directory `{{GUIDELINES_DIR}}` exists. If it does, read ALL `.md` files in that directory - these contain the project's specific standards. Prioritize these guidelines over generic advice. @@ -30,7 +30,9 @@ on: - If you see issues that should be separate PRs, note them briefly at the end 3. **Review Output Format - IMPORTANT** - Create ONE single comment with your complete review using this structure: + Do NOT post anything to GitHub yourself. This workflow takes your final + message and posts it as the PR comment. Make your final message the + complete review and nothing else, using this structure: ## Code Review Summary @@ -46,7 +48,8 @@ on: ### What Looks Good - Brief positive notes - Do NOT create multiple inline comments. Put everything in ONE comment. + Do NOT create multiple inline comments, and do NOT run `gh pr comment`. + Everything belongs in that one final message. 4. **Focus Areas** (priority order) - Security vulnerabilities @@ -129,6 +132,24 @@ jobs: sanitized_tools="${sanitized_tools//\`/\\\`}" echo "allowed_tools=${sanitized_tools}" >> $GITHUB_OUTPUT + - name: Build review prompt + id: prompt + env: + RAW_PROMPT: ${{ inputs.prompt }} + GUIDELINES_DIR: ${{ inputs.guidelines_dir }} + run: | + # `inputs.guidelines_dir` cannot be referenced from the default value of + # another workflow_call input - GitHub resolves input defaults before the + # caller's inputs exist, so it renders empty. The default prompt therefore + # carries a literal {{GUIDELINES_DIR}} placeholder that we substitute here. + prompt="${RAW_PROMPT//\{\{GUIDELINES_DIR\}\}/${GUIDELINES_DIR}}" + delimiter="EOF_PROMPT_$(openssl rand -hex 8)" + { + echo "text<<${delimiter}" + echo "${prompt}" + echo "${delimiter}" + } >> "$GITHUB_OUTPUT" + - name: Run Claude Code Review id: claude-review uses: anthropics/claude-code-action@v1.0.8 @@ -139,10 +160,66 @@ jobs: actions: read # Use input parameters or defaults - prompt: ${{ inputs.prompt }} + prompt: ${{ steps.prompt.outputs.text }} use_sticky_comment: ${{ inputs.use_sticky_comment }} # track_progress: ${{ inputs.track_progress }} # DISABLED: See comment above in inputs section for details claude_args: | --model ${{ inputs.model || 'claude-sonnet-4-5-20250929' }} - --allowed-tools "${{ steps.sanitize.outputs.allowed_tools != '' && steps.sanitize.outputs.allowed_tools || 'Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)' }}" \ No newline at end of file + --allowed-tools "${{ steps.sanitize.outputs.allowed_tools != '' && steps.sanitize.outputs.allowed_tools || 'Read,Bash(gh pr diff:*),Bash(gh pr view:*)' }}" + + - name: Post review comment + # The action runs in "agent" mode on pull_request events, and agent mode + # posts nothing to the PR by itself - use_sticky_comment is inert there. + # Before this step the review only reached the job summary unless the model + # happened to call `gh pr comment` on its own, which it did on 4 of 14 runs. + if: ${{ !cancelled() && steps.claude-review.outputs.execution_file != '' }} + env: + # The action revokes its own app token in its final step, so that token is + # already dead here. Post as github-actions[bot] instead. + GH_TOKEN: ${{ github.token }} + EXECUTION_FILE: ${{ steps.claude-review.outputs.execution_file }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + STICKY: ${{ inputs.use_sticky_comment }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + MARKER: '' + run: | + set -euo pipefail + + if [ ! -f "$EXECUTION_FILE" ]; then + echo "::warning::No execution file at ${EXECUTION_FILE}; nothing to post." + exit 0 + fi + + # The execution file is a JSON array of turns. The final turn carries the + # model's closing message in .result - that is the review. + review=$(jq -r '[.[] | select(.type == "result")] | last | .result // ""' "$EXECUTION_FILE") + + if [ -z "${review//[[:space:]]/}" ]; then + echo "::warning::Claude produced no review text; nothing to post." + exit 0 + fi + + body_file=$(mktemp) + { + printf '%s\n\n' "$MARKER" + printf '%s\n' "$review" + printf '\nAutomated review by Claude. View run.\n' "$RUN_URL" + } > "$body_file" + + existing='' + if [ "$STICKY" = 'true' ]; then + existing=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq "[.[] | select(.user.type == \"Bot\") | select((.body // \"\") | contains(\"${MARKER}\"))] | last | .id // empty" || true) + fi + + if [ -n "$existing" ]; then + jq -n --rawfile body "$body_file" '{body: $body}' \ + | gh api --method PATCH "repos/${REPO}/issues/comments/${existing}" --input - --silent + echo "Updated review comment ${existing}." + else + jq -n --rawfile body "$body_file" '{body: $body}' \ + | gh api --method POST "repos/${REPO}/issues/${PR_NUMBER}/comments" --input - --silent + echo "Posted review comment on PR #${PR_NUMBER}." + fi diff --git a/automated-code-review/README.md b/automated-code-review/README.md index 8449ae9..367de11 100644 --- a/automated-code-review/README.md +++ b/automated-code-review/README.md @@ -100,4 +100,4 @@ You can re-trigger a review by removing and re-adding the label. * Reviews are **comment-only** * Merges are **not blocked** * Review runs only when the label is added -* Teams can customize `docs/code-review-guidelines.md` +* Teams can customize the guidelines in `docs/code-review/` - Claude reads every `.md` file in that directory