11name : Auto Fix CI Failures
22
3+ # NOTE: This workflow only works for PRs from branches in the same repo.
4+ # Fork PRs cannot be auto-fixed because GITHUB_TOKEN lacks push access to forks.
5+
36on :
47 workflow_run :
58 workflows : ["Validate"]
@@ -11,7 +14,6 @@ permissions:
1114 pull-requests : write
1215 actions : read
1316 issues : write
14- id-token : write
1517
1618jobs :
1719 auto-fix :
@@ -25,32 +27,51 @@ jobs:
2527 startsWith(github.event.workflow_run.head_branch, 'claude/')
2628 runs-on : ubuntu-latest
2729 steps :
30+ - name : Check if PR is still open
31+ id : pr_check
32+ uses : actions/github-script@v7
33+ with :
34+ script : |
35+ const pr = await github.rest.pulls.get({
36+ owner: context.repo.owner,
37+ repo: context.repo.repo,
38+ pull_number: ${{ github.event.workflow_run.pull_requests[0].number }}
39+ });
40+ return { isOpen: pr.data.state === 'open' };
41+
2842 - name : Checkout code
43+ if : fromJSON(steps.pr_check.outputs.result).isOpen
2944 uses : actions/checkout@v4
3045 with :
3146 ref : ${{ github.event.workflow_run.head_branch }}
3247 fetch-depth : 10
3348 token : ${{ secrets.GITHUB_TOKEN }}
3449
35- - name : Check if last commit was auto-fix (prevent loops)
50+ - name : Check for recent auto-fix commits (prevent loops)
51+ if : fromJSON(steps.pr_check.outputs.result).isOpen
3652 id : check_loop
3753 run : |
38- LAST_COMMIT_MSG=$(git log -1 --pretty=%s)
39- if [[ "$LAST_COMMIT_MSG" == *"[auto-fix]"* ]]; then
54+ # Check last 3 commits for auto-fix tag to be more robust
55+ RECENT_COMMITS=$(git log -3 --pretty=%s)
56+ if echo "$RECENT_COMMITS" | grep -q "\[auto-fix\]"; then
4057 echo "skip=true" >> $GITHUB_OUTPUT
41- echo "Last commit was an auto-fix, skipping to prevent loop"
58+ echo "Recent commit was an auto-fix, skipping to prevent loop"
4259 else
4360 echo "skip=false" >> $GITHUB_OUTPUT
4461 fi
4562
4663 - name : Setup git identity
47- if : steps.check_loop.outputs.skip != 'true'
64+ if : |
65+ fromJSON(steps.pr_check.outputs.result).isOpen &&
66+ steps.check_loop.outputs.skip != 'true'
4867 run : |
4968 git config --global user.email "claude[bot]@users.noreply.github.com"
5069 git config --global user.name "claude[bot]"
5170
5271 - name : Get CI failure details
53- if : steps.check_loop.outputs.skip != 'true'
72+ if : |
73+ fromJSON(steps.pr_check.outputs.result).isOpen &&
74+ steps.check_loop.outputs.skip != 'true'
5475 id : failure_details
5576 uses : actions/github-script@v7
5677 with :
6990
7091 const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
7192
93+ if (failedJobs.length === 0) {
94+ return {
95+ runUrl: run.data.html_url,
96+ failedJobs: [],
97+ errorLogs: [],
98+ hasFailedJobs: false
99+ };
100+ }
101+
72102 let errorLogs = [];
73103 for (const job of failedJobs) {
74104 try {
79109 });
80110 errorLogs.push({
81111 jobName: job.name,
82- logs: logs.data.substring(0, 50000) // Limit log size
112+ logs: logs.data.substring(0, 50000)
83113 });
84114 } catch (e) {
85115 errorLogs.push({
@@ -92,11 +122,15 @@ jobs:
92122 return {
93123 runUrl: run.data.html_url,
94124 failedJobs: failedJobs.map(j => j.name),
95- errorLogs: errorLogs
125+ errorLogs: errorLogs,
126+ hasFailedJobs: true
96127 };
97128
98129 - name : Fix CI failures with Claude
99- if : steps.check_loop.outputs.skip != 'true'
130+ if : |
131+ fromJSON(steps.pr_check.outputs.result).isOpen &&
132+ steps.check_loop.outputs.skip != 'true' &&
133+ fromJSON(steps.failure_details.outputs.result).hasFailedJobs
100134 id : claude
101135 uses : anthropics/claude-code-action@v1
102136 with :
@@ -137,22 +171,37 @@ jobs:
137171 claude_args : " --allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(uv:*),Bash(python:*),Bash(pytest:*),Bash(ruff:*)'"
138172
139173 - name : Comment on PR with fix status
140- if : always() && steps.check_loop.outputs.skip != 'true'
174+ if : |
175+ always() &&
176+ fromJSON(steps.pr_check.outputs.result).isOpen &&
177+ steps.check_loop.outputs.skip != 'true' &&
178+ steps.failure_details.outcome == 'success'
141179 uses : actions/github-script@v7
142180 with :
143181 script : |
144182 const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
145- const conclusion = '${{ steps.claude.outcome }}';
183+ const claudeOutcome = '${{ steps.claude.outcome }}';
184+ const hasFailedJobs = ${{ fromJSON(steps.failure_details.outputs.result).hasFailedJobs }};
146185 const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}';
186+ const failureUrl = '${{ fromJSON(steps.failure_details.outputs.result).runUrl }}';
147187
148188 let body;
149- if (conclusion === 'success') {
189+ if (!hasFailedJobs) {
190+ body = `## CI Auto-Fix Skipped
191+
192+ The CI workflow failed but no individual jobs failed (possibly a setup/infrastructure issue).
193+
194+ - **Fix workflow run**: ${runUrl}
195+ - **Original failure**: ${failureUrl}
196+
197+ Manual investigation may be required.`;
198+ } else if (claudeOutcome === 'success') {
150199 body = `## CI Auto-Fix Attempted
151200
152201 Claude has analyzed the CI failure and attempted to fix the issues.
153202
154203 - **Fix workflow run**: ${runUrl}
155- - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl } }
204+ - **Original failure**: ${failureUrl }
156205
157206 Please review the changes pushed to this branch.`;
158207 } else {
@@ -161,7 +210,7 @@ jobs:
161210 Claude attempted to fix the CI failure but encountered issues.
162211
163212 - **Fix workflow run**: ${runUrl}
164- - **Original failure**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl } }
213+ - **Original failure**: ${failureUrl }
165214
166215 Manual intervention may be required.`;
167216 }
0 commit comments