From 09c6b3c0c8806bb58bb9654b469d0963419d0acf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 00:10:21 +0000 Subject: [PATCH 1/5] Add GitHub Action to auto-fix CI failures on Claude PRs When the Validate workflow fails on a PR from a Claude branch (claude/*), this workflow automatically: 1. Checks if the last commit was an auto-fix (prevents infinite loops) 2. Extracts the failure details and error logs 3. Invokes Claude Code Action to analyze and fix the issues 4. Commits fixes directly to the same branch with [auto-fix] tag 5. Comments on the PR with the fix status The action supports fixing: - ruff formatting errors - ruff linting errors - pytest test failures Requires ANTHROPIC_API_KEY secret to be configured. --- .github/workflows/auto-fix-ci.yml | 174 ++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 .github/workflows/auto-fix-ci.yml diff --git a/.github/workflows/auto-fix-ci.yml b/.github/workflows/auto-fix-ci.yml new file mode 100644 index 0000000..a691591 --- /dev/null +++ b/.github/workflows/auto-fix-ci.yml @@ -0,0 +1,174 @@ +name: Auto Fix CI Failures + +on: + workflow_run: + workflows: ["Validate"] + types: + - completed + +permissions: + contents: write + pull-requests: write + actions: read + issues: write + id-token: write + +jobs: + auto-fix: + # Only run when: + # 1. The Validate workflow failed + # 2. There is an associated PR + # 3. The branch was created by Claude (starts with 'claude/') + if: | + github.event.workflow_run.conclusion == 'failure' && + github.event.workflow_run.pull_requests[0] && + startsWith(github.event.workflow_run.head_branch, 'claude/') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_branch }} + fetch-depth: 10 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if last commit was auto-fix (prevent loops) + id: check_loop + run: | + LAST_COMMIT_MSG=$(git log -1 --pretty=%s) + if [[ "$LAST_COMMIT_MSG" == *"[auto-fix]"* ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "Last commit was an auto-fix, skipping to prevent loop" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Setup git identity + if: steps.check_loop.outputs.skip != 'true' + run: | + git config --global user.email "claude[bot]@users.noreply.github.com" + git config --global user.name "claude[bot]" + + - name: Get CI failure details + if: steps.check_loop.outputs.skip != 'true' + id: failure_details + uses: actions/github-script@v7 + with: + script: | + const run = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }} + }); + + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }} + }); + + const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure'); + + let errorLogs = []; + for (const job of failedJobs) { + try { + const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + job_id: job.id + }); + errorLogs.push({ + jobName: job.name, + logs: logs.data.substring(0, 50000) // Limit log size + }); + } catch (e) { + errorLogs.push({ + jobName: job.name, + logs: `Failed to retrieve logs: ${e.message}` + }); + } + } + + return { + runUrl: run.data.html_url, + failedJobs: failedJobs.map(j => j.name), + errorLogs: errorLogs + }; + + - name: Fix CI failures with Claude + if: steps.check_loop.outputs.skip != 'true' + id: claude + uses: anthropics/claude-code-action@v1 + with: + prompt: | + The CI workflow "Validate" has failed on a PR that you (Claude) contributed to. + Please analyze the failure and fix the issues. + + ## Failure Information + - Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} + - Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }} + - PR Number: ${{ github.event.workflow_run.pull_requests[0].number }} + - Branch: ${{ github.event.workflow_run.head_branch }} + - Repository: ${{ github.repository }} + + ## CI Workflow Steps + The Validate workflow runs these checks: + 1. `uv run ruff format --check src/ tests/` - Code formatting + 2. `uv run ruff check src/ tests/` - Linting + 3. `uv run pytest tests/ -v` - Tests + + ## Your Task + 1. Analyze the error logs below to understand what failed + 2. Fix the issues in the code: + - For formatting errors: Run `uv run ruff format src/ tests/` + - For linting errors: Fix the code issues reported by ruff + - For test failures: Fix the failing tests or the code they're testing + 3. Commit your fixes with a message that includes "[auto-fix]" tag + 4. Push the changes to the branch: ${{ github.event.workflow_run.head_branch }} + + IMPORTANT: Your commit message MUST include "[auto-fix]" to prevent infinite loops. + Example: "Fix linting errors in parser module [auto-fix]" + + ## Error Logs + ``` + ${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }} + ``` + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(uv:*),Bash(python:*),Bash(pytest:*),Bash(ruff:*)'" + + - name: Comment on PR with fix status + if: always() && steps.check_loop.outputs.skip != 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }}; + const conclusion = '${{ steps.claude.outcome }}'; + const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; + + let body; + if (conclusion === 'success') { + body = `## CI Auto-Fix Attempted + + Claude has analyzed the CI failure and attempted to fix the issues. + + - **Fix workflow run**: ${runUrl} + - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} + + Please review the changes pushed to this branch.`; + } else { + body = `## CI Auto-Fix Failed + + Claude attempted to fix the CI failure but encountered issues. + + - **Fix workflow run**: ${runUrl} + - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} + + Manual intervention may be required.`; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); From 7da6d7a6d2ab97ee16c053657df1d25fd5235a6b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 00:15:42 +0000 Subject: [PATCH 2/5] Restrict auto-fix to PR branches only [auto-fix] Add explicit checks to ensure the workflow doesn't run on main/master branches, only on PR branches from Claude. --- .github/workflows/auto-fix-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-fix-ci.yml b/.github/workflows/auto-fix-ci.yml index a691591..ff0dbe3 100644 --- a/.github/workflows/auto-fix-ci.yml +++ b/.github/workflows/auto-fix-ci.yml @@ -17,11 +17,13 @@ jobs: auto-fix: # Only run when: # 1. The Validate workflow failed - # 2. There is an associated PR + # 2. There is an associated PR (not main/master branch) # 3. The branch was created by Claude (starts with 'claude/') if: | github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.pull_requests[0] && + github.event.workflow_run.head_branch != 'main' && + github.event.workflow_run.head_branch != 'master' && startsWith(github.event.workflow_run.head_branch, 'claude/') runs-on: ubuntu-latest steps: From 34e16366a57624e077444d313ba1c55d2dba22e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 00:16:38 +0000 Subject: [PATCH 3/5] Remove redundant main/master branch checks [auto-fix] --- .github/workflows/auto-fix-ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/auto-fix-ci.yml b/.github/workflows/auto-fix-ci.yml index ff0dbe3..a691591 100644 --- a/.github/workflows/auto-fix-ci.yml +++ b/.github/workflows/auto-fix-ci.yml @@ -17,13 +17,11 @@ jobs: auto-fix: # Only run when: # 1. The Validate workflow failed - # 2. There is an associated PR (not main/master branch) + # 2. There is an associated PR # 3. The branch was created by Claude (starts with 'claude/') if: | github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.pull_requests[0] && - github.event.workflow_run.head_branch != 'main' && - github.event.workflow_run.head_branch != 'master' && startsWith(github.event.workflow_run.head_branch, 'claude/') runs-on: ubuntu-latest steps: From 8defd7ea2ac43671d27567fead71fc26472a87a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 00:26:32 +0000 Subject: [PATCH 4/5] Fix robustness issues in auto-fix-ci workflow [auto-fix] - Add PR open check before attempting fixes - Check last 3 commits for [auto-fix] tag (not just last one) - Handle case where workflow fails but no jobs failed - Fix comment step to only run when failure_details succeeded - Remove unnecessary id-token permission - Add note about fork PR limitation --- .github/workflows/auto-fix-ci.yml | 79 +++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/.github/workflows/auto-fix-ci.yml b/.github/workflows/auto-fix-ci.yml index a691591..2c770d2 100644 --- a/.github/workflows/auto-fix-ci.yml +++ b/.github/workflows/auto-fix-ci.yml @@ -1,5 +1,8 @@ name: Auto Fix CI Failures +# NOTE: This workflow only works for PRs from branches in the same repo. +# Fork PRs cannot be auto-fixed because GITHUB_TOKEN lacks push access to forks. + on: workflow_run: workflows: ["Validate"] @@ -11,7 +14,6 @@ permissions: pull-requests: write actions: read issues: write - id-token: write jobs: auto-fix: @@ -25,32 +27,51 @@ jobs: startsWith(github.event.workflow_run.head_branch, 'claude/') runs-on: ubuntu-latest steps: + - name: Check if PR is still open + id: pr_check + uses: actions/github-script@v7 + with: + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ github.event.workflow_run.pull_requests[0].number }} + }); + return { isOpen: pr.data.state === 'open' }; + - name: Checkout code + if: fromJSON(steps.pr_check.outputs.result).isOpen uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_branch }} fetch-depth: 10 token: ${{ secrets.GITHUB_TOKEN }} - - name: Check if last commit was auto-fix (prevent loops) + - name: Check for recent auto-fix commits (prevent loops) + if: fromJSON(steps.pr_check.outputs.result).isOpen id: check_loop run: | - LAST_COMMIT_MSG=$(git log -1 --pretty=%s) - if [[ "$LAST_COMMIT_MSG" == *"[auto-fix]"* ]]; then + # Check last 3 commits for auto-fix tag to be more robust + RECENT_COMMITS=$(git log -3 --pretty=%s) + if echo "$RECENT_COMMITS" | grep -q "\[auto-fix\]"; then echo "skip=true" >> $GITHUB_OUTPUT - echo "Last commit was an auto-fix, skipping to prevent loop" + echo "Recent commit was an auto-fix, skipping to prevent loop" else echo "skip=false" >> $GITHUB_OUTPUT fi - name: Setup git identity - if: steps.check_loop.outputs.skip != 'true' + if: | + fromJSON(steps.pr_check.outputs.result).isOpen && + steps.check_loop.outputs.skip != 'true' run: | git config --global user.email "claude[bot]@users.noreply.github.com" git config --global user.name "claude[bot]" - name: Get CI failure details - if: steps.check_loop.outputs.skip != 'true' + if: | + fromJSON(steps.pr_check.outputs.result).isOpen && + steps.check_loop.outputs.skip != 'true' id: failure_details uses: actions/github-script@v7 with: @@ -69,6 +90,15 @@ jobs: const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure'); + if (failedJobs.length === 0) { + return { + runUrl: run.data.html_url, + failedJobs: [], + errorLogs: [], + hasFailedJobs: false + }; + } + let errorLogs = []; for (const job of failedJobs) { try { @@ -79,7 +109,7 @@ jobs: }); errorLogs.push({ jobName: job.name, - logs: logs.data.substring(0, 50000) // Limit log size + logs: logs.data.substring(0, 50000) }); } catch (e) { errorLogs.push({ @@ -92,11 +122,15 @@ jobs: return { runUrl: run.data.html_url, failedJobs: failedJobs.map(j => j.name), - errorLogs: errorLogs + errorLogs: errorLogs, + hasFailedJobs: true }; - name: Fix CI failures with Claude - if: steps.check_loop.outputs.skip != 'true' + if: | + fromJSON(steps.pr_check.outputs.result).isOpen && + steps.check_loop.outputs.skip != 'true' && + fromJSON(steps.failure_details.outputs.result).hasFailedJobs id: claude uses: anthropics/claude-code-action@v1 with: @@ -137,22 +171,37 @@ jobs: claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(uv:*),Bash(python:*),Bash(pytest:*),Bash(ruff:*)'" - name: Comment on PR with fix status - if: always() && steps.check_loop.outputs.skip != 'true' + if: | + always() && + fromJSON(steps.pr_check.outputs.result).isOpen && + steps.check_loop.outputs.skip != 'true' && + steps.failure_details.outcome == 'success' uses: actions/github-script@v7 with: script: | const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }}; - const conclusion = '${{ steps.claude.outcome }}'; + const claudeOutcome = '${{ steps.claude.outcome }}'; + const hasFailedJobs = ${{ fromJSON(steps.failure_details.outputs.result).hasFailedJobs }}; const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; + const failureUrl = '${{ fromJSON(steps.failure_details.outputs.result).runUrl }}'; let body; - if (conclusion === 'success') { + if (!hasFailedJobs) { + body = `## CI Auto-Fix Skipped + + The CI workflow failed but no individual jobs failed (possibly a setup/infrastructure issue). + + - **Fix workflow run**: ${runUrl} + - **Original failure**: ${failureUrl} + + Manual investigation may be required.`; + } else if (claudeOutcome === 'success') { body = `## CI Auto-Fix Attempted Claude has analyzed the CI failure and attempted to fix the issues. - **Fix workflow run**: ${runUrl} - - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} + - **Original failure**: ${failureUrl} Please review the changes pushed to this branch.`; } else { @@ -161,7 +210,7 @@ jobs: Claude attempted to fix the CI failure but encountered issues. - **Fix workflow run**: ${runUrl} - - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} + - **Original failure**: ${failureUrl} Manual intervention may be required.`; } From ad5e9bd8974dd18e8fffb16701fc871f75882233 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:33:40 -0700 Subject: [PATCH 5/5] Address review feedback on auto-fix-ci workflow robustness and security (#30) * Initial plan * Address review comments on auto-fix-ci workflow Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Fix trailing spaces in workflow file Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Fix claude_args quoting and improve null check Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Use YAML folded scalar for claude_args to avoid quoting issues Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Fix context object consistency and claude_args formatting Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Add null safety check to PR validation step Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Add JSON parsing safety and quote claude_args properly Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> * Use validated PR number from pr_check step throughout workflow Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nhorton <204146+nhorton@users.noreply.github.com> --- .github/workflows/auto-fix-ci.yml | 109 +++++++++++++++++++----------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/.github/workflows/auto-fix-ci.yml b/.github/workflows/auto-fix-ci.yml index 2c770d2..cd547eb 100644 --- a/.github/workflows/auto-fix-ci.yml +++ b/.github/workflows/auto-fix-ci.yml @@ -32,12 +32,18 @@ jobs: uses: actions/github-script@v7 with: script: | + const pullRequests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; + if (!pullRequests || pullRequests.length === 0) { + console.log('No pull requests found'); + return { isOpen: false, prNumber: null }; + } + const prNumber = pullRequests[0].number; const pr = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: ${{ github.event.workflow_run.pull_requests[0].number }} + pull_number: prNumber }); - return { isOpen: pr.data.state === 'open' }; + return { isOpen: pr.data.state === 'open', prNumber: prNumber }; - name: Checkout code if: fromJSON(steps.pr_check.outputs.result).isOpen @@ -51,7 +57,9 @@ jobs: if: fromJSON(steps.pr_check.outputs.result).isOpen id: check_loop run: | - # Check last 3 commits for auto-fix tag to be more robust + # Check last 3 commits for auto-fix tag to prevent infinite loops. + # We look back 3 commits as a lightweight safeguard: this is enough to catch + # any recent auto-fix pushes triggered by this workflow without scanning the full history. RECENT_COMMITS=$(git log -3 --pretty=%s) if echo "$RECENT_COMMITS" | grep -q "\[auto-fix\]"; then echo "skip=true" >> $GITHUB_OUTPUT @@ -60,14 +68,6 @@ jobs: echo "skip=false" >> $GITHUB_OUTPUT fi - - name: Setup git identity - if: | - fromJSON(steps.pr_check.outputs.result).isOpen && - steps.check_loop.outputs.skip != 'true' - run: | - git config --global user.email "claude[bot]@users.noreply.github.com" - git config --global user.name "claude[bot]" - - name: Get CI failure details if: | fromJSON(steps.pr_check.outputs.result).isOpen && @@ -107,6 +107,8 @@ jobs: repo: context.repo.repo, job_id: job.id }); + // Truncate logs to 50000 characters to stay within Claude's context window + // and GitHub API response size limits errorLogs.push({ jobName: job.name, logs: logs.data.substring(0, 50000) @@ -141,7 +143,7 @@ jobs: ## Failure Information - Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} - Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }} - - PR Number: ${{ github.event.workflow_run.pull_requests[0].number }} + - PR Number: ${{ fromJSON(steps.pr_check.outputs.result).prNumber }} - Branch: ${{ github.event.workflow_run.head_branch }} - Repository: ${{ github.repository }} @@ -168,7 +170,7 @@ jobs: ${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }} ``` anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(uv:*),Bash(python:*),Bash(pytest:*),Bash(ruff:*)'" + claude_args: "--allowedTools Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git commit:*),Bash(git push:*),Bash(git status:*),Bash(git diff:*),Bash(uv:*),Bash(python:*),Bash(pytest:*),Bash(ruff:*)" - name: Comment on PR with fix status if: | @@ -179,40 +181,67 @@ jobs: uses: actions/github-script@v7 with: script: | - const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }}; + // Safely access pull request number with null check + const pullRequests = ${{ toJSON(github.event.workflow_run.pull_requests) }}; + if (!pullRequests || pullRequests.length === 0) { + console.log('No pull request found, skipping comment'); + return; + } + const prNumber = pullRequests[0].number; + + // Safely access failure details with null check and JSON validation + const failureDetails = '${{ steps.failure_details.outputs.result }}'; + if (!failureDetails || failureDetails.trim() === '' || failureDetails === 'undefined' || failureDetails === 'null') { + console.log('No failure details available, skipping comment'); + return; + } + let failureData; + try { + failureData = JSON.parse(failureDetails); + } catch (e) { + console.log('Failed to parse failure details:', e); + return; + } + const claudeOutcome = '${{ steps.claude.outcome }}'; - const hasFailedJobs = ${{ fromJSON(steps.failure_details.outputs.result).hasFailedJobs }}; + const hasFailedJobs = failureData.hasFailedJobs; const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; - const failureUrl = '${{ fromJSON(steps.failure_details.outputs.result).runUrl }}'; + const failureUrl = failureData.runUrl; let body; if (!hasFailedJobs) { - body = `## CI Auto-Fix Skipped - - The CI workflow failed but no individual jobs failed (possibly a setup/infrastructure issue). - - - **Fix workflow run**: ${runUrl} - - **Original failure**: ${failureUrl} - - Manual investigation may be required.`; + body = [ + '## CI Auto-Fix Skipped', + '', + 'The CI workflow failed but no individual jobs failed (possibly a setup/infrastructure issue).', + '', + `- **Fix workflow run**: ${runUrl}`, + `- **Original failure**: ${failureUrl}`, + '', + 'Manual investigation may be required.' + ].join('\n'); } else if (claudeOutcome === 'success') { - body = `## CI Auto-Fix Attempted - - Claude has analyzed the CI failure and attempted to fix the issues. - - - **Fix workflow run**: ${runUrl} - - **Original failure**: ${failureUrl} - - Please review the changes pushed to this branch.`; + body = [ + '## CI Auto-Fix Attempted', + '', + 'Claude has analyzed the CI failure and attempted to fix the issues.', + '', + `- **Fix workflow run**: ${runUrl}`, + `- **Original failure**: ${failureUrl}`, + '', + 'Please review the changes pushed to this branch.' + ].join('\n'); } else { - body = `## CI Auto-Fix Failed - - Claude attempted to fix the CI failure but encountered issues. - - - **Fix workflow run**: ${runUrl} - - **Original failure**: ${failureUrl} - - Manual intervention may be required.`; + body = [ + '## CI Auto-Fix Failed', + '', + 'Claude attempted to fix the CI failure but encountered issues.', + '', + `- **Fix workflow run**: ${runUrl}`, + `- **Original failure**: ${failureUrl}`, + '', + 'Manual intervention may be required.' + ].join('\n'); } await github.rest.issues.createComment({