Claude Auto-Triage #1114
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Claude Auto-Triage | |
| on: | |
| workflow_run: | |
| workflows: ["Claude Code Review"] | |
| types: [completed] | |
| jobs: | |
| triage: | |
| if: > | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: claude-automation | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| actions: read | |
| statuses: write | |
| id-token: write | |
| steps: | |
| - name: Get PR from workflow run | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}" | |
| HEAD_SHA="${{ github.event.workflow_run.head_sha }}" | |
| PR_NUMBER=$(gh pr list \ | |
| --repo "${{ github.repository }}" \ | |
| --state open \ | |
| --head "$HEAD_BRANCH" \ | |
| --json number \ | |
| --jq '.[0].number') | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "No open PR found for branch $HEAD_BRANCH" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "Found PR #$PR_NUMBER" | |
| echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT | |
| # Get PR details | |
| PR_DATA=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json headRefName,title,headRepositoryOwner) | |
| echo "head_ref=$(echo "$PR_DATA" | jq -r '.headRefName')" >> $GITHUB_OUTPUT | |
| echo "title=$(echo "$PR_DATA" | jq -r '.title')" >> $GITHUB_OUTPUT | |
| # Check if fork | |
| HEAD_OWNER=$(echo "$PR_DATA" | jq -r '.headRepositoryOwner.login') | |
| if [ "$HEAD_OWNER" != "${{ github.repository_owner }}" ]; then | |
| echo "is_fork=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_fork=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Check skip labels | |
| if: steps.pr.outputs.skip != 'true' | |
| id: labels | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| LABELS=$(gh pr view ${{ steps.pr.outputs.number }} --repo "${{ github.repository }}" --json labels --jq '.labels[].name') | |
| if echo "$LABELS" | grep -q "do-not-auto-merge\|needs-human-review"; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "::notice::PR has skip label, skipping triage" | |
| else | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Report pending status | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api repos/${{ github.repository }}/statuses/${{ steps.pr.outputs.head_sha }} \ | |
| -f state=pending \ | |
| -f context="triage" \ | |
| -f description="Triage in progress..." | |
| - name: Checkout repository | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.pr.outputs.head_ref }} | |
| - name: Setup Claude Code | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| id: setup-claude | |
| uses: ./.github/actions/setup-claude-code | |
| - name: Run Claude Auto-Triage | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| id: triage | |
| uses: anthropics/claude-code-action@v1 | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }} | |
| with: | |
| allowed_bots: "claude" | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| path_to_claude_code_executable: ${{ steps.setup-claude.outputs.executable-path }} | |
| prompt: | | |
| # Auto-Triage PR #${{ steps.pr.outputs.number }} | |
| **Title**: ${{ steps.pr.outputs.title }} | |
| **Is Fork**: ${{ steps.pr.outputs.is_fork }} | |
| ## Task | |
| 1. Read review comments: `gh pr view ${{ steps.pr.outputs.number }} --comments` | |
| 2. For each issue found, decide: **FIX_NOW**, **DEFER_ISSUE**, or **IGNORE** | |
| 3. Execute actions and post summary | |
| ## Decision Rules | |
| **FIX_NOW** (max 3 items, batch in one comment): | |
| - Mechanical fixes (rename, remove debug code, add null checks) | |
| - In-scope incomplete work | |
| - If fork PR: convert to DEFER_ISSUE instead | |
| **DEFER_ISSUE** (create GitHub issue): | |
| - Complex changes, architectural suggestions | |
| - Out-of-scope improvements | |
| - Use `quick-fix` label for trivial out-of-scope fixes | |
| - Use `from-pr-review` label for all deferred issues | |
| **IGNORE**: False positives, existing issues cover it, pure style preference | |
| ## Execute Actions | |
| **For FIX_NOW** (batch all in ONE comment): | |
| ```bash | |
| gh pr comment ${{ steps.pr.outputs.number }} --body "@claude please fix: | |
| 1. [issue]: [fix instructions] | |
| 2. [issue]: [fix instructions] | |
| ..." | |
| ``` | |
| **For DEFER_ISSUE**: | |
| ```bash | |
| gh issue create --title "[From PR #${{ steps.pr.outputs.number }}] [desc]" \ | |
| --label "from-pr-review" --body "..." | |
| ``` | |
| **Post summary** (no @claude prefix): | |
| ```bash | |
| gh pr comment ${{ steps.pr.outputs.number }} --body "## Auto-Triage Summary | |
| | Issue | Decision | Action | | |
| |-------|----------|--------| | |
| | ... | FIX_NOW | In fix request | | |
| | ... | DEFER_ISSUE | Created #N |" | |
| ``` | |
| **If no FIX_NOW items**, add ready-to-merge label: | |
| ```bash | |
| gh pr edit ${{ steps.pr.outputs.number }} --add-label "ready-to-merge" | |
| ``` | |
| claude_args: '--model claude-sonnet-4-6 --max-turns 30 --allowed-tools "Read,Glob,Grep,Bash,Task"' | |
| show_full_output: true | |
| - name: Check for pending fixes | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| id: check-fixes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Get all comments | |
| COMMENTS=$(gh pr view ${{ steps.pr.outputs.number }} \ | |
| --repo "${{ github.repository }}" \ | |
| --json comments) | |
| # Count fix requests (@claude.*fix) | |
| FIX_REQUESTS=$(echo "$COMMENTS" | jq '[.comments[] | select(.body | test("@claude.*fix"; "i"))] | length') | |
| # Count fix completions (## Fix Summary) | |
| FIX_COMPLETIONS=$(echo "$COMMENTS" | jq '[.comments[] | select(.body | test("## Fix Summary"))] | length') | |
| echo "fix_requests=$FIX_REQUESTS" >> $GITHUB_OUTPUT | |
| echo "fix_completions=$FIX_COMPLETIONS" >> $GITHUB_OUTPUT | |
| # Pending fixes = requests that don't have corresponding completions | |
| if [ "$FIX_REQUESTS" -gt "$FIX_COMPLETIONS" ]; then | |
| echo "has_pending_fixes=true" >> $GITHUB_OUTPUT | |
| echo "Pending fixes: $FIX_REQUESTS requests, $FIX_COMPLETIONS completions" | |
| else | |
| echo "has_pending_fixes=false" >> $GITHUB_OUTPUT | |
| echo "All fixes resolved: $FIX_REQUESTS requests, $FIX_COMPLETIONS completions" | |
| fi | |
| - name: Enable auto-merge | |
| if: steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' && steps.check-fixes.outputs.has_pending_fixes != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER }} | |
| run: | | |
| gh pr merge ${{ steps.pr.outputs.number }} \ | |
| --repo "${{ github.repository }}" \ | |
| --auto --squash --delete-branch || true | |
| - name: Report status | |
| if: always() && steps.pr.outputs.skip != 'true' && steps.labels.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "${{ steps.check-fixes.outputs.has_pending_fixes }}" = "true" ]; then | |
| STATE="failure" | |
| DESC="Fixes pending" | |
| else | |
| STATE="success" | |
| DESC="Triage complete" | |
| fi | |
| gh api repos/${{ github.repository }}/statuses/${{ steps.pr.outputs.head_sha }} \ | |
| -f state="$STATE" -f context="triage" -f description="$DESC" |