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
252 changes: 252 additions & 0 deletions .github/workflows/auto-fix-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
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"]
types:
- completed

permissions:
contents: write
pull-requests: write
actions: read
issues: 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: Check if PR is still open
id: pr_check
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: prNumber
});
return { isOpen: pr.data.state === 'open', prNumber: prNumber };

- 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 for recent auto-fix commits (prevent loops)
if: fromJSON(steps.pr_check.outputs.result).isOpen
id: check_loop
run: |
# 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
echo "Recent commit was an auto-fix, skipping to prevent loop"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Get CI failure details
if: |
fromJSON(steps.pr_check.outputs.result).isOpen &&
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');

if (failedJobs.length === 0) {
return {
runUrl: run.data.html_url,
failedJobs: [],
errorLogs: [],
hasFailedJobs: false
};
}

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
});
// 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)
});
} 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,
hasFailedJobs: true
};

- name: Fix CI failures with Claude
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:
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: ${{ fromJSON(steps.pr_check.outputs.result).prNumber }}
- 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 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: |
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: |
// 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 = failureData.hasFailedJobs;
const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}';
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.'
].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.'
].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.'
].join('\n');
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: body
});
Loading