CI Doctor #8
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: CI Doctor | |
| # When a core CI workflow fails on a mainline branch, capture the failure logs | |
| # and dispatch Cursor's Cloud Agent to inspect and open a minimal fix PR (or an | |
| # issue if it isn't safely auto-fixable). Reuses the same Cursor API pattern as | |
| # the other agent-*.yml workflows. | |
| # | |
| # Scope is deliberately narrow to avoid fix loops: | |
| # - only reacts to FAILED runs (conclusion == failure) | |
| # - only on push/schedule to a mainline branch (not pull_request runs) | |
| # - skips cursor/* branches, so it never doctors an agent's own fix PR | |
| # - one run per branch+workflow at a time (concurrency guard) | |
| on: | |
| workflow_run: | |
| workflows: ["CI", "E2E Tests", "Security"] | |
| types: [completed] | |
| concurrency: | |
| group: ci-doctor-${{ github.event.workflow_run.head_branch }}-${{ github.event.workflow_run.name }} | |
| cancel-in-progress: false | |
| jobs: | |
| # Gate on CURSOR_API_KEY being present. Secrets can't be used in job-level | |
| # `if:`, so we surface presence as an output here and skip cleanly (neutral, | |
| # not failed) on repos that haven't set the key. | |
| guard: | |
| name: Check Cursor API key | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_key: ${{ steps.check.outputs.has_key }} | |
| steps: | |
| - id: check | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| run: | | |
| if [ -n "$CURSOR_API_KEY" ]; then | |
| echo "has_key=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_key=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice title=CI Doctor skipped::CURSOR_API_KEY is not set on this repo — skipping. Set it with: gh secret set CURSOR_API_KEY --repo <owner>/<repo>" | |
| fi | |
| diagnose: | |
| name: Diagnose & dispatch fix agent | |
| needs: guard | |
| if: > | |
| needs.guard.outputs.has_key == 'true' && | |
| github.event.workflow_run.conclusion == 'failure' && | |
| github.event.workflow_run.event != 'pull_request' && | |
| !startsWith(github.event.workflow_run.head_branch, 'cursor/') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| issues: write | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_branch }} | |
| - name: Capture failure logs | |
| id: logs | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| run: | | |
| set -euo pipefail | |
| # Grab the failed-step logs, strip ANSI + timestamps, keep the tail. | |
| gh run view "$RUN_ID" --repo "$REPO" --log-failed 2>/dev/null \ | |
| | sed -E 's/\x1b\[[0-9;]*m//g' \ | |
| | sed -E 's/^[0-9T:.Z+-]+ //' \ | |
| | tail -n 300 > /tmp/ci-fail.log || true | |
| if [ ! -s /tmp/ci-fail.log ]; then | |
| echo "(no failed-step logs could be retrieved)" > /tmp/ci-fail.log | |
| fi | |
| echo "Captured $(wc -l < /tmp/ci-fail.log) log lines." | |
| - name: Dispatch Cursor CI Doctor agent | |
| id: trigger | |
| env: | |
| CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} | |
| REPO: ${{ github.repository }} | |
| RUN_NAME: ${{ github.event.workflow_run.name }} | |
| RUN_URL: ${{ github.event.workflow_run.html_url }} | |
| BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| RUN_EVENT: ${{ github.event.workflow_run.event }} | |
| run: | | |
| set -euo pipefail | |
| LOGS=$(cat /tmp/ci-fail.log) | |
| PROMPT=$(jq -Rs \ | |
| --arg repo "$REPO" \ | |
| --arg name "$RUN_NAME" \ | |
| --arg url "$RUN_URL" \ | |
| --arg branch "$BRANCH" \ | |
| --arg event "$RUN_EVENT" \ | |
| --arg logs "$LOGS" \ | |
| ' | |
| gsub("\\{\\{repo\\}\\}"; $repo) | |
| | gsub("\\{\\{run.name\\}\\}"; $name) | |
| | gsub("\\{\\{run.url\\}\\}"; $url) | |
| | gsub("\\{\\{branch\\}\\}"; $branch) | |
| | gsub("\\{\\{run.event\\}\\}"; $event) | |
| | gsub("\\{\\{logs\\}\\}"; $logs) | |
| ' .cursor/agent-prompts/ci-doctor.md) | |
| PAYLOAD=$(jq -n \ | |
| --arg prompt "$PROMPT" \ | |
| --arg repo "$REPO" \ | |
| --arg ref "$BRANCH" \ | |
| '{ | |
| prompt: { text: $prompt }, | |
| source: { repository: ("github.com/" + $repo), ref: $ref }, | |
| target: { autoCreatePr: true, openAsCursorGithubApp: true }, | |
| model: "composer-2.5" | |
| }') | |
| response=$(curl -sS -X POST "https://api.cursor.com/v0/agents" \ | |
| -H "Authorization: Bearer $CURSOR_API_KEY" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD") | |
| echo "$response" | jq . | |
| AGENT_ID=$(echo "$response" | jq -er '.id') | |
| echo "agent_id=$AGENT_ID" >> "$GITHUB_OUTPUT" | |
| - name: Record dispatch in run summary | |
| if: always() && steps.trigger.outputs.agent_id != '' | |
| env: | |
| AGENT_ID: ${{ steps.trigger.outputs.agent_id }} | |
| RUN_NAME: ${{ github.event.workflow_run.name }} | |
| RUN_URL: ${{ github.event.workflow_run.html_url }} | |
| BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| run: | | |
| { | |
| echo "## 🔧 CI Doctor dispatched" | |
| echo "" | |
| echo "- **Failed workflow:** \`$RUN_NAME\` on \`$BRANCH\`" | |
| echo "- **Failed run:** $RUN_URL" | |
| echo "- **Cursor agent:** [\`$AGENT_ID\`](https://cursor.com/agents/$AGENT_ID)" | |
| echo "" | |
| echo "The agent will open a fix PR (or an issue if it isn't safely auto-fixable)." | |
| } >> "$GITHUB_STEP_SUMMARY" |