Repository navigation
Fix CI #1431
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: Fix CI | |
| # Auto-fix failing CI on PRs. Budgeted: at most MAX_ATTEMPTS auto-fix | |
| # commits per PR branch. Opt in by adding the `auto-fix` label to the | |
| # PR (flip from opt-out to opt-in so a hostile PR can't summon a | |
| # Claude-authored commit onto its own branch just by failing CI). | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| actions: read | |
| # anthropics/claude-code-action@v1 mints an OIDC token to identify the | |
| # runner to Anthropic's API; without id-token: write it fails with | |
| # "Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable". | |
| id-token: write | |
| concurrency: | |
| # Serialize attempts per PR so we never have two fix-ci jobs racing | |
| # to push to the same branch. | |
| group: fix-ci-${{ github.event.workflow_run.head_branch }} | |
| cancel-in-progress: false | |
| env: | |
| # Ceiling on how many times the bot will try to fix the same PR. | |
| MAX_ATTEMPTS: "3" | |
| # Commit-subject prefix used both to author and to count auto-fix | |
| # commits. Changing this breaks the attempt counter. | |
| COMMIT_MARKER: "fix(ci-auto)" | |
| jobs: | |
| fix: | |
| # Only run on real CI failures that came from a PR. | |
| if: >- | |
| github.event.workflow_run.conclusion == 'failure' && | |
| github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve PR number and branch | |
| id: pr | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| # workflow_run doesn't directly carry the PR number; look it up | |
| # from the head SHA. | |
| pr_json=$(gh api "repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" --jq '.[0] // empty') | |
| if [ -z "$pr_json" ]; then | |
| echo "No PR associated with ${HEAD_SHA}; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| pr_number=$(echo "$pr_json" | jq -r '.number') | |
| pr_branch=$(echo "$pr_json" | jq -r '.head.ref') | |
| pr_repo=$(echo "$pr_json" | jq -r '.head.repo.full_name') | |
| pr_labels=$(echo "$pr_json" | jq -r '[.labels[].name] | join(",")') | |
| echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" | |
| echo "pr_branch=${pr_branch}" >> "$GITHUB_OUTPUT" | |
| echo "pr_repo=${pr_repo}" >> "$GITHUB_OUTPUT" | |
| echo "pr_labels=${pr_labels}" >> "$GITHUB_OUTPUT" | |
| # Opt-in: only run when a maintainer has explicitly labeled the | |
| # PR with `auto-fix`. Without this, any PR whose CI fails would | |
| # summon the agent — and the agent pushes to the PR branch. | |
| if ! echo "${pr_labels}" | tr ',' '\n' | grep -qx 'auto-fix'; then | |
| echo "PR #${pr_number} is not labeled auto-fix; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Don't auto-fix PRs from forks — pushing to a fork's branch | |
| # requires the forker's token, which we don't have. | |
| if [ "${pr_repo}" != "${{ github.repository }}" ]; then | |
| echo "PR #${pr_number} comes from fork ${pr_repo}; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| - name: Checkout PR branch | |
| if: steps.pr.outputs.skip != 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.pr.outputs.pr_branch }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check attempt budget | |
| if: steps.pr.outputs.skip != 'true' | |
| id: budget | |
| run: | | |
| set -euo pipefail | |
| # Count auto-fix commits already on this branch since it | |
| # diverged from main. | |
| base=$(git merge-base origin/main HEAD) | |
| attempts=$(git log --format='%s' "${base}..HEAD" \ | |
| | grep -c "^${COMMIT_MARKER}" || true) | |
| echo "Found ${attempts} prior auto-fix commits on this branch." | |
| echo "attempts=${attempts}" >> "$GITHUB_OUTPUT" | |
| if [ "${attempts}" -ge "${MAX_ATTEMPTS}" ]; then | |
| echo "over_budget=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "over_budget=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Comment and stop (over budget) | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh pr comment "${{ steps.pr.outputs.pr_number }}" --repo "${{ github.repository }}" --body \ | |
| "🤖 auto-fix budget exhausted (${{ steps.budget.outputs.attempts }}/${{ env.MAX_ATTEMPTS }} attempts). Next step is manual — push a fix or re-run after resolving the failure root cause." | |
| - name: Fetch failed CI logs | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .ci-fix | |
| # `--log-failed` grabs stderr of only the failed steps, which | |
| # keeps the payload small enough to fit in a prompt. | |
| gh run view "${RUN_ID}" --repo "${{ github.repository }}" --log-failed > .ci-fix/failed.log || true | |
| gh run view "${RUN_ID}" --repo "${{ github.repository }}" --json name,displayTitle,conclusion,jobs > .ci-fix/run.json | |
| wc -l .ci-fix/failed.log | |
| - name: Sibling repos (mirror ci.yml) | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| run: | | |
| git clone --depth 1 https://github.com/ecto/tang.git ../tang | |
| git clone --depth 1 https://github.com/ecto/phyz.git ../phyz | |
| git clone --depth 1 https://github.com/ecto/loon.git ../loon | |
| - uses: dtolnay/rust-toolchain@master | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| with: | |
| toolchain: stable | |
| targets: wasm32-unknown-unknown | |
| components: rustfmt, clippy | |
| - uses: Swatinem/rust-cache@v2 | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| - uses: actions/setup-node@v6 | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install wasm-pack | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| run: cargo install wasm-pack --locked --version 0.13.1 | |
| - name: Install apt deps | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y \ | |
| libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev | |
| - name: npm ci | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| run: npm ci | |
| - name: Configure git | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| run: | | |
| git config user.name "vcad-agent[bot]" | |
| git config user.email "vcad-agent@users.noreply.github.com" | |
| - name: Run agent | |
| if: steps.pr.outputs.skip != 'true' && steps.budget.outputs.over_budget != 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| prompt: | | |
| You are fixing a failing CI run on pull request #${{ steps.pr.outputs.pr_number }} | |
| (branch `${{ steps.pr.outputs.pr_branch }}`). Attempt ${{ steps.budget.outputs.attempts }}+1 of ${{ env.MAX_ATTEMPTS }}. | |
| Read CLAUDE.md first for project conventions (Z-up coordinates, | |
| workspace layout, commands CI runs). | |
| The raw log of failed CI steps is at `.ci-fix/failed.log`. | |
| The run metadata is at `.ci-fix/run.json`. | |
| Your job: | |
| 1. Read `.ci-fix/failed.log` and identify the root cause of | |
| every failed job. Multiple jobs can fail for unrelated | |
| reasons — handle each. | |
| 2. Fix the root cause in source. Don't paper over real test | |
| failures by disabling tests, loosening assertions, or adding | |
| `--no-verify`. If a test is flaky, say so in the commit | |
| message but still try a real fix first. | |
| 3. Run the exact commands that failed, locally, and confirm | |
| they now pass. Use the sibling repos already cloned at | |
| ../tang, ../phyz, ../loon. Apt deps are installed. | |
| Typical commands to try: | |
| cargo fmt -p <crate> --check | |
| cargo clippy --workspace --exclude vcad-desktop --features vcad-kernel-text/no-builtin-font -- -D warnings | |
| cargo test --workspace --exclude vcad-desktop --features vcad-kernel-text/no-builtin-font | |
| cargo build --workspace --exclude vcad-desktop --examples --features vcad-kernel-text/no-builtin-font | |
| cargo doc --workspace --exclude vcad-desktop --no-deps --features vcad-kernel-text/no-builtin-font | |
| npm run build --workspaces | |
| npm test --workspaces --if-present | |
| 4. Delete `.ci-fix/` before committing so it never lands in | |
| the tree. | |
| 5. Commit with subject prefix `${{ env.COMMIT_MARKER }}:` so | |
| the attempt counter can find it. Include a short | |
| description of what broke and what you changed. | |
| 6. Push to branch `${{ steps.pr.outputs.pr_branch }}`. | |
| If you cannot fix the failure — it's genuinely ambiguous, the | |
| test needs product input, or the fix is larger than a CI-fix | |
| ought to be — do NOT commit. Instead leave a `gh pr comment` | |
| on PR #${{ steps.pr.outputs.pr_number }} explaining what | |
| broke, what you tried, and why it needs human attention. |