Repository navigation
Auto Fix CI Failures #2259
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: Auto Fix CI Failures | |
| on: | |
| workflow_run: | |
| workflows: | |
| # Include all CI workflows that could benefit from auto-fix | |
| # Excludes: Auto Fix CI Failures (self), Issue Triage/Deduplication (not fixable) | |
| - "Auto Tag Release" | |
| - "Bump Plugin Versions" | |
| - "Bump Website Version" | |
| - "Claude Code" | |
| - "Claude Code Review" | |
| - "Claude Plugin Validate" | |
| - "Coverage" | |
| - "Deploy Han Website" | |
| - "Manual Release" | |
| - "Release Binaries" | |
| - "Test Failure Analysis" | |
| - "Test Website" | |
| - "Validate Browse Client" | |
| - "Validate Han CLI" | |
| - "Validate Han Native" | |
| - "Validate Han Website" | |
| types: | |
| - completed | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| actions: read | |
| issues: write | |
| id-token: write | |
| # Use concurrency to prevent multiple auto-fix runs on the same branch | |
| concurrency: | |
| group: auto-fix-${{ github.event.workflow_run.head_branch }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Check if we should attempt auto-fix | |
| should-fix: | |
| if: github.event.workflow_run.conclusion == 'failure' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_run: ${{ steps.check.outputs.should_run }} | |
| attempt_count: ${{ steps.check.outputs.attempt_count }} | |
| steps: | |
| - name: Check if auto-fix should run | |
| id: check | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const branch = '${{ github.event.workflow_run.head_branch }}'; | |
| const workflowName = '${{ github.event.workflow_run.name }}'; | |
| const runId = ${{ github.event.workflow_run.id }}; | |
| // Skip if triggered by bot to prevent loops | |
| if ('${{ github.event.workflow_run.actor.login }}' === 'github-actions[bot]') { | |
| console.log('Skipping: triggered by github-actions[bot]'); | |
| core.setOutput('should_run', 'false'); | |
| core.setOutput('attempt_count', '0'); | |
| return; | |
| } | |
| // Get recent commits to count auto-fix attempts | |
| const commits = await github.rest.repos.listCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: branch, | |
| per_page: 10 | |
| }); | |
| // Count consecutive auto-fix commits | |
| let attemptCount = 0; | |
| for (const commit of commits.data) { | |
| if (commit.commit.message.includes('[auto-fix]')) { | |
| attemptCount++; | |
| } else { | |
| break; | |
| } | |
| } | |
| // Max 3 auto-fix attempts per failure chain | |
| if (attemptCount >= 3) { | |
| console.log(`Skipping: already ${attemptCount} auto-fix attempts`); | |
| core.setOutput('should_run', 'false'); | |
| core.setOutput('attempt_count', attemptCount.toString()); | |
| return; | |
| } | |
| console.log(`Proceeding with auto-fix attempt ${attemptCount + 1}`); | |
| core.setOutput('should_run', 'true'); | |
| core.setOutput('attempt_count', attemptCount.toString()); | |
| auto-fix: | |
| needs: should-fix | |
| if: needs.should-fix.outputs.should_run == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_branch }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup git identity | |
| run: | | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --global user.name "github-actions[bot]" | |
| - name: Get CI failure details | |
| 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'); | |
| // Categorize failure type | |
| let failureType = 'unknown'; | |
| 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 | |
| }); | |
| // Get more context - last 15000 chars | |
| const truncatedLogs = logs.data.slice(-15000); | |
| errorLogs.push({ | |
| jobName: job.name, | |
| logs: truncatedLogs | |
| }); | |
| // Detect failure type from logs | |
| const logLower = truncatedLogs.toLowerCase(); | |
| if (logLower.includes('biome') || logLower.includes('format')) { | |
| failureType = 'formatting'; | |
| } else if (logLower.includes('lint') || logLower.includes('eslint')) { | |
| failureType = 'linting'; | |
| } else if (logLower.includes('type') && logLower.includes('error')) { | |
| failureType = 'typescript'; | |
| } else if (logLower.includes('test') || logLower.includes('expect')) { | |
| failureType = 'test'; | |
| } else if (logLower.includes('markdownlint')) { | |
| failureType = 'markdown'; | |
| } else if (logLower.includes('plugin validate') || logLower.includes('claudelint')) { | |
| failureType = 'plugin-validation'; | |
| } else if (logLower.includes('network') || logLower.includes('timeout') || logLower.includes('econnrefused')) { | |
| failureType = 'network'; | |
| } else if (logLower.includes('build')) { | |
| failureType = 'build'; | |
| } | |
| } catch (e) { | |
| errorLogs.push({ | |
| jobName: job.name, | |
| logs: 'Could not fetch logs: ' + e.message | |
| }); | |
| } | |
| } | |
| return { | |
| runUrl: run.data.html_url, | |
| failedJobs: failedJobs.map(j => j.name), | |
| errorLogs: errorLogs, | |
| workflowName: run.data.name, | |
| failureType: failureType | |
| }; | |
| - name: Skip unfixable failures | |
| id: skip_check | |
| run: | | |
| FAILURE_TYPE="${{ fromJSON(steps.failure_details.outputs.result).failureType }}" | |
| if [ "$FAILURE_TYPE" = "network" ]; then | |
| echo "Skipping: network failure is not fixable by code changes" | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Setup tools for validation | |
| if: steps.skip_check.outputs.skip != 'true' | |
| run: | | |
| # Install Biome for formatting validation | |
| npm install -g @biomejs/biome | |
| - name: Fix CI failures with Claude | |
| if: steps.skip_check.outputs.skip != 'true' | |
| id: claude | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| prompt: | | |
| A CI workflow has failed. Please analyze the error and fix it. | |
| ## Failure Information | |
| - **Workflow**: ${{ fromJSON(steps.failure_details.outputs.result).workflowName }} | |
| - **Failed Jobs**: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }} | |
| - **Failure Type**: ${{ fromJSON(steps.failure_details.outputs.result).failureType }} | |
| - **Branch**: ${{ github.event.workflow_run.head_branch }} | |
| - **Auto-fix Attempt**: ${{ needs.should-fix.outputs.attempt_count }} (max 3) | |
| - **Run URL**: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} | |
| ## Error Logs | |
| ${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }} | |
| ## Fix Instructions by Failure Type | |
| **For formatting failures:** | |
| - Run: `biome format --write .` in the affected directory (website/, packages/han/, etc.) | |
| - The project uses Biome with line-width: 80, single quotes, ES5 trailing commas | |
| **For linting failures:** | |
| - Run: `biome lint --write .` for auto-fixable issues | |
| - For other lint errors, fix the specific code issues mentioned in the logs | |
| **For TypeScript errors:** | |
| - Fix type errors in the specific files mentioned | |
| - Common issues: missing types, incorrect generics, null checks | |
| **For test failures:** | |
| - Read the failing test file and the code it tests | |
| - Fix the code or update the test as appropriate | |
| **For markdown lint failures:** | |
| - Fix the markdown issues in the specific files mentioned | |
| - Common issues: line length, heading levels, trailing whitespace | |
| **For plugin validation failures:** | |
| - Check plugin.json, hooks.json structure | |
| - Ensure command frontmatter has required fields (description) | |
| - Validate paths exist | |
| **For build failures:** | |
| - Check for import errors, missing dependencies | |
| - Fix syntax errors in the mentioned files | |
| ## Critical Rules | |
| 1. Make MINIMAL changes to fix the issue | |
| 2. Do NOT commit or push - the workflow handles that | |
| 3. After making changes, validate by running the relevant command: | |
| - Formatting: `biome format --files-ignore-unknown=true <directory>` | |
| - Lint: `biome lint --files-ignore-unknown=true <directory>` | |
| 4. If you can't determine the fix, don't make random changes | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| timeout_minutes: 15 | |
| - name: Validate fixes | |
| if: steps.skip_check.outputs.skip != 'true' | |
| id: validate | |
| run: | | |
| FAILURE_TYPE="${{ fromJSON(steps.failure_details.outputs.result).failureType }}" | |
| VALID="true" | |
| # Run validation based on failure type | |
| case "$FAILURE_TYPE" in | |
| formatting) | |
| echo "Validating formatting..." | |
| biome format --files-ignore-unknown=true . || VALID="false" | |
| ;; | |
| linting) | |
| echo "Validating linting..." | |
| biome lint --files-ignore-unknown=true . || VALID="false" | |
| ;; | |
| *) | |
| echo "No pre-commit validation for failure type: $FAILURE_TYPE" | |
| ;; | |
| esac | |
| echo "valid=$VALID" >> $GITHUB_OUTPUT | |
| - name: Commit and push fixes | |
| if: steps.skip_check.outputs.skip != 'true' | |
| run: | | |
| # Pull latest changes first to avoid conflicts | |
| git pull --rebase origin ${{ github.event.workflow_run.head_branch }} || true | |
| # Check if there are any changes to commit | |
| if git diff --quiet && git diff --cached --quiet; then | |
| echo "No changes to commit" | |
| exit 0 | |
| fi | |
| # Stage all changes | |
| git add -A | |
| # Get the workflow name for the commit message | |
| WORKFLOW_NAME="${{ fromJSON(steps.failure_details.outputs.result).workflowName }}" | |
| FAILURE_TYPE="${{ fromJSON(steps.failure_details.outputs.result).failureType }}" | |
| ATTEMPT="${{ needs.should-fix.outputs.attempt_count }}" | |
| NEXT_ATTEMPT=$((ATTEMPT + 1)) | |
| # Commit with markers for loop detection | |
| git commit -m "fix(ci): auto-fix ${WORKFLOW_NAME} ${FAILURE_TYPE} failures [auto-fix] | |
| Auto-fixed by Claude Code action (attempt ${NEXT_ATTEMPT}/3). | |
| Failure type: ${FAILURE_TYPE} | |
| Original failure: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }} | |
| Co-Authored-By: Claude <noreply@anthropic.com>" | |
| # Push the changes | |
| git push origin ${{ github.event.workflow_run.head_branch }} | |
| # Notify when max attempts reached | |
| notify-max-attempts: | |
| needs: should-fix | |
| if: | | |
| needs.should-fix.outputs.should_run == 'false' && | |
| needs.should-fix.outputs.attempt_count >= '3' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Create issue for manual intervention | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const branch = '${{ github.event.workflow_run.head_branch }}'; | |
| const workflow = '${{ github.event.workflow_run.name }}'; | |
| const runUrl = '${{ github.event.workflow_run.html_url }}'; | |
| // Check if issue already exists | |
| const existingIssues = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'ci-auto-fix-failed' | |
| }); | |
| const title = `CI Auto-Fix Failed: ${workflow} on ${branch}`; | |
| const existingIssue = existingIssues.data.find(i => i.title === title); | |
| if (existingIssue) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existingIssue.number, | |
| body: `Auto-fix attempted again but still failing.\n\nLatest failure: ${runUrl}` | |
| }); | |
| } else { | |
| const body = [ | |
| '## CI Auto-Fix Exhausted', | |
| '', | |
| 'The automatic CI fix system has attempted **3 fixes** without success.', | |
| '', | |
| `**Workflow:** ${workflow}`, | |
| `**Branch:** ${branch}`, | |
| `**Latest Run:** ${runUrl}`, | |
| '', | |
| '### Manual intervention required', | |
| '', | |
| 'Please review the failing workflow and fix the issue manually.', | |
| '', | |
| 'After fixing, close this issue.' | |
| ].join('\n'); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: title, | |
| labels: ['ci-auto-fix-failed', 'needs-manual-fix'], | |
| body: body | |
| }); | |
| } |