|
| 1 | +name: AI Fix |
| 2 | +on: |
| 3 | + issues: |
| 4 | + types: [labeled] |
| 5 | + |
| 6 | +jobs: |
| 7 | + apply-fix: |
| 8 | + if: github.event.label.name == 'ai-fix' |
| 9 | + runs-on: ubuntu-latest |
| 10 | + timeout-minutes: 60 |
| 11 | + permissions: |
| 12 | + contents: write |
| 13 | + issues: write |
| 14 | + env: |
| 15 | + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
| 16 | + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} |
| 17 | + EMERGENCY_ANTHROPIC_API_KEY: ${{ secrets.EMERGENCY_ANTHROPIC_API_KEY }} |
| 18 | + outputs: |
| 19 | + branch: ${{ steps.push.outputs.branch }} |
| 20 | + issue_num: ${{ steps.meta.outputs.issue_num }} |
| 21 | + issue_title: ${{ steps.meta.outputs.issue_title }} |
| 22 | + steps: |
| 23 | + - uses: actions/checkout@v4 |
| 24 | + with: |
| 25 | + ref: dev |
| 26 | + fetch-depth: 0 |
| 27 | + |
| 28 | + - name: Verify ANTHROPIC_API_KEY is NOT set |
| 29 | + run: | |
| 30 | + if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then |
| 31 | + echo "::error::ANTHROPIC_API_KEY is set; would override Pro subscription auth and incur PAYG charges" |
| 32 | + exit 1 |
| 33 | + fi |
| 34 | +
|
| 35 | + - name: Extract issue metadata |
| 36 | + id: meta |
| 37 | + run: | |
| 38 | + echo "issue_num=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")" >> "$GITHUB_OUTPUT" |
| 39 | + echo "issue_title=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH")" >> "$GITHUB_OUTPUT" |
| 40 | +
|
| 41 | + - name: Install agent tooling if missing |
| 42 | + run: | |
| 43 | + command -v claude || npm install -g @anthropic-ai/claude-code |
| 44 | + command -v opencode || curl -fsSL https://opencode.ai/install | bash |
| 45 | +
|
| 46 | + - name: Checkout or create fix branch |
| 47 | + id: branch_setup |
| 48 | + run: | |
| 49 | + BRANCH="ai-fix/issue-${{ steps.meta.outputs.issue_num }}" |
| 50 | + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" |
| 51 | +
|
| 52 | + if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then |
| 53 | + echo "is_retry=true" >> "$GITHUB_OUTPUT" |
| 54 | + echo "Branch $BRANCH exists — checking out for retry" |
| 55 | + git checkout -b "$BRANCH" "origin/$BRANCH" |
| 56 | + else |
| 57 | + echo "is_retry=false" >> "$GITHUB_OUTPUT" |
| 58 | + echo "Branch $BRANCH does not exist — creating fresh from dev" |
| 59 | + git checkout -b "$BRANCH" |
| 60 | + fi |
| 61 | +
|
| 62 | + - name: Build agent prompt |
| 63 | + run: | |
| 64 | + ISSUE_BODY=$(jq -r '.issue.body' "$GITHUB_EVENT_PATH") |
| 65 | +
|
| 66 | + # If resuming an existing branch, include prior commit context so the |
| 67 | + # agent knows what was already attempted and can build on or correct it |
| 68 | + RETRY_CONTEXT="" |
| 69 | + if [[ "${{ steps.branch_setup.outputs.is_retry }}" == "true" ]]; then |
| 70 | + PREV_COMMITS=$(git log "origin/dev..HEAD" --oneline 2>/dev/null || echo "none") |
| 71 | + RETRY_CONTEXT=" |
| 72 | + NOTE: A previous fix attempt exists on this branch. Prior commits: |
| 73 | + ${PREV_COMMITS} |
| 74 | +
|
| 75 | + Review the existing changes and either build on them or correct them. |
| 76 | + Do not revert work that is already correct." |
| 77 | + fi |
| 78 | +
|
| 79 | + cat > /tmp/fix-prompt.txt << PROMPT_EOF |
| 80 | + You are resolving GitHub issue #${{ steps.meta.outputs.issue_num }} in jay9297/PathOfBuilding-PoE2. |
| 81 | +
|
| 82 | + Title: ${{ steps.meta.outputs.issue_title }} |
| 83 | +
|
| 84 | + Body: |
| 85 | + ${ISSUE_BODY} |
| 86 | + ${RETRY_CONTEXT} |
| 87 | +
|
| 88 | + Follow the rules in CLAUDE.md. Make the minimal correct change. |
| 89 | + Add or update tests to cover your fix. Do NOT run tests yourself. |
| 90 | + PROMPT_EOF |
| 91 | +
|
| 92 | + - name: Run AI agent |
| 93 | + run: | |
| 94 | + source scripts/agent-lib.sh |
| 95 | + run_agent_with_fallback /tmp/fix-prompt.txt fix 80 |
| 96 | +
|
| 97 | + - name: Commit and push branch |
| 98 | + id: push |
| 99 | + run: | |
| 100 | + BRANCH="${{ steps.branch_setup.outputs.branch }}" |
| 101 | + git config user.name "ai-agent[bot]" |
| 102 | + git config user.email "ai-agent@users.noreply.github.com" |
| 103 | + git add -A |
| 104 | +
|
| 105 | + if git diff --cached --quiet; then |
| 106 | + # Agent made no new changes — check if prior work already exists on the branch |
| 107 | + AHEAD=$(git rev-list --count "origin/dev..HEAD" 2>/dev/null || echo "0") |
| 108 | + if [[ "${{ steps.branch_setup.outputs.is_retry }}" == "true" && "$AHEAD" -gt 0 ]]; then |
| 109 | + echo "Agent made no new changes but branch has $AHEAD existing commit(s) — proceeding with existing work" |
| 110 | + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" |
| 111 | + exit 0 |
| 112 | + fi |
| 113 | + echo "::error::No changes produced by agent and no existing commits on branch" |
| 114 | + exit 1 |
| 115 | + fi |
| 116 | +
|
| 117 | + git commit -m "fix: resolve issue #${{ steps.meta.outputs.issue_num }} |
| 118 | +
|
| 119 | + Generated by AI agent. Requires human review before merge." |
| 120 | +
|
| 121 | + source scripts/agent-lib.sh |
| 122 | + retry_op 5 10 git push -u origin "$BRANCH" |
| 123 | + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" |
| 124 | +
|
| 125 | + - name: Post failure comment |
| 126 | + if: failure() |
| 127 | + env: |
| 128 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 129 | + run: | |
| 130 | + source scripts/agent-lib.sh |
| 131 | + retry_op 3 10 gh issue comment ${{ steps.meta.outputs.issue_num }} \ |
| 132 | + --body "⚠️ **AI Fix Failed** |
| 133 | +
|
| 134 | + The \`apply-fix\` job failed after exhausting all agent tiers and retry cycles. |
| 135 | + This likely indicates an API outage or expired credentials. |
| 136 | +
|
| 137 | + **What to check:** |
| 138 | + - \`CLAUDE_CODE_OAUTH_TOKEN\` — is the Pro subscription active? |
| 139 | + - \`OPENCODE_API_KEY\` — is the key valid? |
| 140 | + - \`EMERGENCY_ANTHROPIC_API_KEY\` — set this secret for last-resort fallback. |
| 141 | +
|
| 142 | + Re-add the \`ai-fix\` label once credentials are restored to retry." || true |
| 143 | +
|
| 144 | + verify-and-fix: |
| 145 | + needs: apply-fix |
| 146 | + runs-on: ubuntu-latest |
| 147 | + timeout-minutes: 90 |
| 148 | + permissions: |
| 149 | + contents: write |
| 150 | + env: |
| 151 | + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
| 152 | + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} |
| 153 | + EMERGENCY_ANTHROPIC_API_KEY: ${{ secrets.EMERGENCY_ANTHROPIC_API_KEY }} |
| 154 | + steps: |
| 155 | + - uses: actions/checkout@v4 |
| 156 | + with: |
| 157 | + ref: ${{ needs.apply-fix.outputs.branch }} |
| 158 | + fetch-depth: 0 |
| 159 | + |
| 160 | + - name: Configure git |
| 161 | + run: | |
| 162 | + git config user.name "ai-agent[bot]" |
| 163 | + git config user.email "ai-agent@users.noreply.github.com" |
| 164 | +
|
| 165 | + - name: Install agent tooling if missing |
| 166 | + run: | |
| 167 | + command -v claude || npm install -g @anthropic-ai/claude-code |
| 168 | + command -v opencode || curl -fsSL https://opencode.ai/install | bash |
| 169 | +
|
| 170 | + - name: Cache test image |
| 171 | + id: image-cache |
| 172 | + uses: actions/cache@v4 |
| 173 | + with: |
| 174 | + path: /tmp/pob-test-image.tar |
| 175 | + key: pob-test-image-${{ hashFiles('docker-compose.yml') }} |
| 176 | + restore-keys: pob-test-image- |
| 177 | + |
| 178 | + - name: Pull or restore image |
| 179 | + run: | |
| 180 | + if [[ "${{ steps.image-cache.outputs.cache-hit }}" == "true" ]]; then |
| 181 | + docker load < /tmp/pob-test-image.tar |
| 182 | + else |
| 183 | + docker pull ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest |
| 184 | + docker save ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest \ |
| 185 | + -o /tmp/pob-test-image.tar |
| 186 | + fi |
| 187 | +
|
| 188 | + # Separated into its own step — no ${{ }} expressions here so the heredoc |
| 189 | + # delimiter is never mangled by GitHub Actions' format() evaluator |
| 190 | + - name: Build verify-fix prompt template |
| 191 | + run: | |
| 192 | + cat > /tmp/verify-fix-prompt-template.txt << 'PROMPT_EOF' |
| 193 | + The AI agent previously made changes to fix a GitHub issue, but the test |
| 194 | + suite is now failing. Fix the failing tests without reverting the original |
| 195 | + fix. Make the minimal change needed. Follow CLAUDE.md. |
| 196 | +
|
| 197 | + Failing test output: |
| 198 | + PROMPT_EOF |
| 199 | +
|
| 200 | + - name: Verify and auto-fix loop |
| 201 | + run: | |
| 202 | + source scripts/agent-lib.sh |
| 203 | +
|
| 204 | + MAX_RETRIES=3 |
| 205 | + ATTEMPT=0 |
| 206 | +
|
| 207 | + run_tests() { |
| 208 | + local output_file="$1" |
| 209 | + local failed=0 |
| 210 | + echo "--- unit ---" | tee -a "$output_file" |
| 211 | + docker compose run --rm busted-tests --exclude-tags builds,data \ |
| 212 | + 2>&1 | tee -a "$output_file" || failed=1 |
| 213 | + echo "--- builds ---" | tee -a "$output_file" |
| 214 | + docker compose run --rm busted-tests --tags builds \ |
| 215 | + 2>&1 | tee -a "$output_file" || failed=1 |
| 216 | + echo "--- data ---" | tee -a "$output_file" |
| 217 | + docker compose run --rm busted-tests --tags data \ |
| 218 | + 2>&1 | tee -a "$output_file" || failed=1 |
| 219 | + return $failed |
| 220 | + } |
| 221 | +
|
| 222 | + while [ $ATTEMPT -lt $MAX_RETRIES ]; do |
| 223 | + ATTEMPT=$((ATTEMPT + 1)) |
| 224 | + OUTPUT_FILE="/tmp/test-output-attempt-${ATTEMPT}.txt" |
| 225 | + > "$OUTPUT_FILE" |
| 226 | +
|
| 227 | + echo "::group::Test attempt $ATTEMPT of $MAX_RETRIES" |
| 228 | + if run_tests "$OUTPUT_FILE"; then |
| 229 | + echo "✅ Tests passed on attempt $ATTEMPT" |
| 230 | + echo "::endgroup::" |
| 231 | + exit 0 |
| 232 | + fi |
| 233 | + echo "::endgroup::" |
| 234 | + echo "::warning::Tests failed on attempt $ATTEMPT" |
| 235 | +
|
| 236 | + cp "$OUTPUT_FILE" "/tmp/test-failures-attempt-${ATTEMPT}.txt" |
| 237 | +
|
| 238 | + if [ $ATTEMPT -ge $MAX_RETRIES ]; then |
| 239 | + echo "::error::Tests still failing after $MAX_RETRIES fix attempts" |
| 240 | + exit 1 |
| 241 | + fi |
| 242 | +
|
| 243 | + # Rebuild prompt fresh each iteration with the latest failure output |
| 244 | + cp /tmp/verify-fix-prompt-template.txt /tmp/verify-fix-prompt-current.txt |
| 245 | + cat "$OUTPUT_FILE" >> /tmp/verify-fix-prompt-current.txt |
| 246 | +
|
| 247 | + echo "🤖 Running agent to fix test failures (attempt $ATTEMPT)..." |
| 248 | + run_agent_with_fallback /tmp/verify-fix-prompt-current.txt fix 20 |
| 249 | +
|
| 250 | + if git diff --quiet && git diff --cached --quiet; then |
| 251 | + echo "::error::Agent produced no changes — cannot recover from test failures" |
| 252 | + exit 1 |
| 253 | + fi |
| 254 | +
|
| 255 | + git add -A |
| 256 | + retry_op 5 10 git commit -m "fix(tests): address failures — attempt $ATTEMPT [ai-agent] |
| 257 | +
|
| 258 | + Issue: #${{ needs.apply-fix.outputs.issue_num }}" |
| 259 | + retry_op 5 10 git push |
| 260 | + done |
| 261 | +
|
| 262 | + - name: Upload test failure logs |
| 263 | + if: failure() |
| 264 | + uses: actions/upload-artifact@v4 |
| 265 | + with: |
| 266 | + name: test-failures-issue-${{ needs.apply-fix.outputs.issue_num }} |
| 267 | + path: /tmp/test-failures-attempt-*.txt |
| 268 | + retention-days: 7 |
| 269 | + |
| 270 | + open-pr: |
| 271 | + needs: [apply-fix, verify-and-fix] |
| 272 | + runs-on: ubuntu-latest |
| 273 | + timeout-minutes: 10 |
| 274 | + permissions: |
| 275 | + pull-requests: write |
| 276 | + steps: |
| 277 | + - uses: actions/checkout@v4 |
| 278 | + with: |
| 279 | + ref: ${{ needs.apply-fix.outputs.branch }} |
| 280 | + |
| 281 | + - name: Create pull request |
| 282 | + env: |
| 283 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 284 | + run: | |
| 285 | + source scripts/agent-lib.sh |
| 286 | +
|
| 287 | + # If a PR already exists for this branch (from a previous run), skip creation |
| 288 | + EXISTING_PR=$(gh pr list \ |
| 289 | + --head "${{ needs.apply-fix.outputs.branch }}" \ |
| 290 | + --json number --jq '.[0].number' 2>/dev/null || echo "") |
| 291 | +
|
| 292 | + if [[ -n "$EXISTING_PR" ]]; then |
| 293 | + echo "PR #${EXISTING_PR} already exists for this branch — skipping creation" |
| 294 | + exit 0 |
| 295 | + fi |
| 296 | +
|
| 297 | + retry_op 5 15 gh pr create \ |
| 298 | + --base dev \ |
| 299 | + --head "${{ needs.apply-fix.outputs.branch }}" \ |
| 300 | + --title "${{ needs.apply-fix.outputs.issue_title }}" \ |
| 301 | + --body "Closes #${{ needs.apply-fix.outputs.issue_num }} |
| 302 | +
|
| 303 | + 🤖 Generated by AI agent. Automated review will follow. |
| 304 | + Final approval required from @jay9297." \ |
| 305 | + --label "ai-generated" |
0 commit comments