Skip to content

CI Doctor

CI Doctor #8

Workflow file for this run

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"