Claude Test Fixer #10
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 Test Fixer | |
| on: | |
| workflow_run: | |
| workflows: ["Test all (Linux)", "Test all (MacOS)"] | |
| types: [completed] | |
| jobs: | |
| determine-runner: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event.workflow_run.conclusion == 'failure' }} | |
| outputs: | |
| runner: ${{ steps.pick.outputs.runner }} | |
| steps: | |
| - id: pick | |
| run: | | |
| if [[ "${{ github.event.workflow_run.name }}" == *"MacOS"* ]]; then | |
| echo "runner=macos-latest" >> $GITHUB_OUTPUT | |
| else | |
| echo "runner=ubuntu-latest" >> $GITHUB_OUTPUT | |
| fi | |
| fix-on-failure: | |
| needs: determine-runner | |
| runs-on: ${{ needs.determine-runner.outputs.runner }} | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| actions: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_branch }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| fetch-depth: 0 | |
| - name: Use Node.js 24 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24.x' | |
| cache: 'npm' | |
| - name: Enable linger (Linux only) | |
| if: runner.os == 'Linux' | |
| run: loginctl enable-linger $(whoami) | |
| - run: npm ci | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Generate fix branch name | |
| id: branch | |
| run: echo "name=fix/claude-auto-$(date +%s)" >> $GITHUB_OUTPUT | |
| - name: Fetch and filter failure logs from all shards | |
| id: fetch-logs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| REPO="${GITHUB_REPOSITORY}" | |
| RUN_ID="${{ github.event.workflow_run.id }}" | |
| # Write all temp files to /tmp so the working tree stays clean for Claude's changes | |
| gh api "repos/${REPO}/actions/runs/${RUN_ID}/jobs" --paginate > /tmp/all_jobs.json | |
| FAILED_JOB_IDS=$(jq -r '.jobs[] | select(.conclusion == "failure") | .id' /tmp/all_jobs.json) | |
| : > /tmp/failed_logs.txt | |
| FAILED_TEST_FILES="" | |
| for job_id in $FAILED_JOB_IDS; do | |
| JOB_NAME=$(jq -r ".jobs[] | select(.id == $job_id) | .name" /tmp/all_jobs.json) | |
| echo "=== Failed Shard: ${JOB_NAME} ===" >> /tmp/failed_logs.txt | |
| gh api "repos/${REPO}/actions/jobs/${job_id}/logs" > /tmp/raw_log.txt 2>/dev/null || { | |
| echo "[Could not fetch logs for job ${job_id}]" >> /tmp/failed_logs.txt | |
| continue | |
| } | |
| # "Failed Tests" is always the last section in vitest output — | |
| # take from its line to EOF so we capture all failures without a hard limit. | |
| START_LINE=$(grep -n "Failed Tests" /tmp/raw_log.txt | tail -1 | cut -d: -f1) | |
| if [ -n "$START_LINE" ]; then | |
| CONTEXT_START=$((START_LINE > 5 ? START_LINE - 5 : 1)) | |
| tail -n +"$CONTEXT_START" /tmp/raw_log.txt >> /tmp/failed_logs.txt | |
| else | |
| echo "[No 'Failed Tests' section found — showing last 100 lines]" >> /tmp/failed_logs.txt | |
| tail -100 /tmp/raw_log.txt >> /tmp/failed_logs.txt | |
| fi | |
| echo "" >> /tmp/failed_logs.txt | |
| NEW_FILES=$(grep " FAIL test/" /tmp/raw_log.txt | grep -o "test/[^[:space:]>]*\.test\.ts" | sort -u | tr '\n' ' ') | |
| FAILED_TEST_FILES="${FAILED_TEST_FILES}${NEW_FILES}" | |
| done | |
| FAILED_TEST_FILES=$(echo "${FAILED_TEST_FILES}" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | xargs) | |
| echo "failed_test_files=${FAILED_TEST_FILES}" >> "$GITHUB_OUTPUT" | |
| - name: Claude Fix Failed Tests | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| claude_args: "--allowedTools Bash,Read,Edit,Write --max-turns 100" | |
| prompt: | | |
| The "${{ github.event.workflow_run.name }}" workflow failed on branch "${{ github.event.workflow_run.head_branch }}". | |
| Failing test files: ${{ steps.fetch-logs.outputs.failed_test_files }} | |
| Filtered failure logs from all failed shards are in `/tmp/failed_logs.txt`. Read it first. | |
| Follow these steps carefully: | |
| 1. **Diagnose the root cause**: Read `/tmp/failed_logs.txt` thoroughly. Before writing a single line of code, understand WHY the tests are failing — not just the symptom. Read the relevant source files to understand the existing architecture and patterns. | |
| 2. **Implement an architecturally sound fix**: The fix must address the underlying root cause, match existing code patterns in this codebase, and introduce no workarounds or hacks. Do not mask symptoms. | |
| 3. **Verify each fix**: For each failing test file, run: | |
| ``` | |
| npm run test -- <test_file_path> --no-file-parallelism --disable-console-intercept | |
| ``` | |
| On macOS, prepend `CI=true` and ensure `/opt/homebrew/bin` is in PATH. | |
| 4. **Iterate until all tests pass**: If tests still fail after a fix, re-read the output, deepen your understanding of the root cause, and refine the fix. Do not give up and do not apply increasingly speculative patches. Each iteration must be grounded in analysis. | |
| 5. **Stop once all tests pass** — do NOT run any git commands. The workflow will handle committing and opening the PR. | |
| additional_permissions: | | |
| actions: read | |
| - name: Restore git remote auth | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git | |
| - name: Commit fixes | |
| id: auto-commit | |
| uses: stefanzweifel/git-auto-commit-action@v5 | |
| with: | |
| commit_message: "fix: auto-fix test failures from ${{ github.event.workflow_run.name }}" | |
| branch: ${{ steps.branch.outputs.name }} | |
| create_branch: true | |
| - name: Open pull request and post failure logs | |
| if: steps.auto-commit.outputs.changes_detected == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: 'fix: auto-fix ${{ github.event.workflow_run.name }} failures on ${{ github.event.workflow_run.head_branch }}', | |
| body: [ | |
| 'Failures detected in `${{ github.event.workflow_run.name }}` on branch `${{ github.event.workflow_run.head_branch }}`.', | |
| '', | |
| 'This PR was automatically generated by the Claude Test Fixer workflow.', | |
| '', | |
| '> ⚠️ AI-generated fix. Please review carefully before merging.', | |
| ].join('\n'), | |
| head: '${{ steps.branch.outputs.name }}', | |
| base: '${{ github.event.workflow_run.head_branch }}', | |
| }); | |
| const fs = require('fs'); | |
| let logs = fs.readFileSync('/tmp/failed_logs.txt', 'utf8'); | |
| if (logs.length > 60000) { | |
| logs = logs.substring(0, 60000) + '\n... (truncated — see full logs in the failed workflow run)'; | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: [ | |
| '## Test Failure Logs', | |
| '', | |
| '<details>', | |
| '<summary>Click to expand failure logs from the failed CI run</summary>', | |
| '', | |
| '```', | |
| logs, | |
| '```', | |
| '', | |
| '</details>', | |
| ].join('\n'), | |
| }); |