-
Notifications
You must be signed in to change notification settings - Fork 3
631 lines (517 loc) · 27.1 KB
/
Copy pathclaude-issue.yml
File metadata and controls
631 lines (517 loc) · 27.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
name: Claude Issue Implementation
# Serialized, durable runner for issue implementation.
#
# Requests are enqueued by claude-issue-enqueue.yml, which adds the
# `impl:queued` label (a durable queue entry). This runner drains that queue
# ONE issue at a time:
#
# * concurrency group `claude-impl` (cancel-in-progress: false) guarantees
# no two implementation runs — across this runner, claude-batch-fix, and
# claude-stale-check — ever execute at the same time, preventing
# conflicting branches/PRs against main.
# * The queue lives in labels, not in GitHub's pending-run slot, so nothing
# is ever silently dropped when many requests arrive at once (the failure
# that stranded issue #1039).
#
# Draining is driven by three triggers: the `labeled` event (fast path),
# a periodic `schedule` (backstop so the queue can never stall), and
# `workflow_dispatch` (self re-dispatch after each item + manual kick).
on:
issues:
types: [labeled]
schedule:
# Backstop: drain anything left in the queue even if a labeled/dispatch
# event was lost or evicted. Cheap no-op when the queue is empty.
- cron: "*/10 * * * *"
workflow_dispatch:
jobs:
claude:
# Only react to the queue label (or schedule / manual / self re-dispatch).
# Authorization is enforced at enqueue time; by the time `impl:queued`
# exists the request is already vetted.
if: |
github.event_name != 'issues' || github.event.label.name == 'impl:queued'
runs-on: ubuntu-latest
timeout-minutes: 75
# The single global implementation lock. Shared with claude-batch-fix and
# claude-stale-check so main-mutating automation is strictly serialized.
concurrency:
group: claude-impl
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
env:
MIX_ENV: test
# The "Pick next queued issue" step runs before checkout, so gh can't
# infer the repo from a local .git — GH_REPO targets it explicitly.
GH_REPO: ${{ github.repository }}
steps:
- name: Check PAT availability
run: |
if [ -z "${{ secrets.PAT_WORKFLOW_TRIGGER }}" ]; then
echo "::warning::PAT_WORKFLOW_TRIGGER secret not set - PRs won't trigger CI automatically"
fi
# Claim the oldest queued issue. Flips impl:queued -> impl:running so a
# concurrent enqueue can't double-process it. Exits cleanly (has_work=false)
# when the queue is empty, which makes schedule/spurious triggers no-ops.
- name: Pick next queued issue
id: pick
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
run: |
set -euo pipefail
# FIFO-ish: oldest by issue number among queued, not-yet-running issues.
NEXT=$(gh issue list \
--repo "${{ github.repository }}" \
--label "impl:queued" \
--state open \
--json number,labels \
--jq '[ .[] | select([.labels[].name] | index("impl:running") | not) ]
| sort_by(.number) | .[0].number // empty')
if [ -z "${NEXT:-}" ]; then
echo "Queue empty — nothing to implement."
echo "has_work=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Claiming issue #$NEXT"
gh issue edit "$NEXT" --add-label "impl:running" --remove-label "impl:queued"
echo "has_work=true" >> "$GITHUB_OUTPUT"
echo "issue=$NEXT" >> "$GITHUB_OUTPUT"
# Expose to all later steps as $ISSUE_NUMBER / env.ISSUE_NUMBER.
echo "ISSUE_NUMBER=$NEXT" >> "$GITHUB_ENV"
- name: Record implementation claim
id: claim
if: steps.pick.outputs.has_work == 'true'
continue-on-error: true
timeout-minutes: 3
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
run: |
set -euo pipefail
CLAIMED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
CURRENT_BODY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body // ""')
CLEAN_BODY=$(printf '%s\n' "$CURRENT_BODY" | sed '/^## Automation Claim/,$d')
cat > /tmp/issue-body.md << EOF
$CLEAN_BODY
## Automation Claim
<!-- automation-claim: {"status":"RUNNING","issue":${ISSUE_NUMBER},"run_id":${GITHUB_RUN_ID},"run_attempt":${GITHUB_RUN_ATTEMPT},"run_url":"${RUN_URL}","claimed_at":"${CLAIMED_AT}"} -->
| Field | Value |
|-------|-------|
| Status | \`RUNNING\` |
| Run | [${GITHUB_RUN_ID}](${RUN_URL}) |
| Attempt | ${GITHUB_RUN_ATTEMPT} |
| Claimed At | ${CLAIMED_AT} |
EOF
gh issue edit "$ISSUE_NUMBER" --body-file /tmp/issue-body.md
- name: Check issue still needs implementation
id: issue-state
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success'
continue-on-error: true
timeout-minutes: 3
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --json state --jq '.state')
if [ "$ISSUE_STATE" != "OPEN" ]; then
echo "Issue #$ISSUE_NUMBER is $ISSUE_STATE; skipping implementation."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=issue-${ISSUE_STATE}" >> "$GITHUB_OUTPUT"
exit 0
fi
MERGED_PRS=$(gh pr list \
--repo "${{ github.repository }}" \
--state merged \
--limit 100 \
--json number,body \
| jq --arg issue "$ISSUE_NUMBER" '[.[] | select((.body // "") | test("(?i)(closes|fixes|resolves)[[:space:]]+#" + $issue + "([^0-9]|$)"))] | length')
if [ "${MERGED_PRS:-0}" -gt 0 ]; then
echo "Issue #$ISSUE_NUMBER already has a merged closing PR; skipping implementation."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=merged-pr-exists" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "should_run=true" >> "$GITHUB_OUTPUT"
- name: Checkout repository
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
- name: Install git pre-commit hook
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
timeout-minutes: 3
run: |
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
- name: Setup Elixir environment
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
uses: ./.github/actions/setup-elixir
- name: Setup Claude Code
id: setup-claude
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
uses: ./.github/actions/setup-claude-code
- name: Run tests and capture failures
id: test-feedback
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
continue-on-error: true
timeout-minutes: 15
run: |
echo "Running tests to capture any failures..."
FAILURES=""
# Compile
echo "=== Compiling ===" | tee /tmp/test_output.txt
if ! mix compile --warnings-as-errors 2>&1 | tee -a /tmp/test_output.txt; then
FAILURES="${FAILURES}COMPILE_FAILED "
fi
# Tests (limit failures for manageable output)
echo "" >> /tmp/test_output.txt
echo "=== Running tests ===" | tee -a /tmp/test_output.txt
if ! mix test --max-failures 5 --warnings-as-errors 2>&1 | tee -a /tmp/test_output.txt; then
FAILURES="${FAILURES}TESTS_FAILED "
fi
# Format check
echo "" >> /tmp/test_output.txt
echo "=== Checking format ===" | tee -a /tmp/test_output.txt
if ! mix format --check-formatted 2>&1 | tee -a /tmp/test_output.txt; then
FAILURES="${FAILURES}FORMAT_FAILED "
fi
# Credo
echo "" >> /tmp/test_output.txt
echo "=== Running Credo ===" | tee -a /tmp/test_output.txt
if ! mix credo --strict 2>&1 | tee -a /tmp/test_output.txt; then
FAILURES="${FAILURES}CREDO_FAILED "
fi
# Capture output for Claude
if [ -n "$FAILURES" ]; then
echo "has_failures=true" >> "$GITHUB_OUTPUT"
echo "failures=$FAILURES" >> "$GITHUB_OUTPUT"
# Truncate output if too long (keep last 200 lines)
tail -200 /tmp/test_output.txt > /tmp/test_output_truncated.txt
# Store output in multiline format
{
echo 'output<<EOF'
cat /tmp/test_output_truncated.txt
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "::warning::Test failures detected: $FAILURES"
else
echo "has_failures=false" >> "$GITHUB_OUTPUT"
echo "All checks passed"
fi
- name: Run Claude Code
id: claude
if: steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
timeout-minutes: 30
uses: anthropics/claude-code-action@v1
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
# Used by E2E tests that call LLM APIs
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
with:
allowed_bots: "claude"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Use PAT for git operations to avoid GitHub App workflow permission issues
github_token: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
path_to_claude_code_executable: ${{ steps.setup-claude.outputs.executable-path }}
prompt: |
Implement issue #${{ steps.pick.outputs.issue }}.
## Step 1: Read Issue and Check Dependencies
```bash
gh issue view ${{ steps.pick.outputs.issue }} --comments
```
**Check for "Blocked by:" in the issue body.** If blocking issues exist:
1. Check if each blocking issue is closed: `gh issue view BLOCKER_NUMBER --json state`
2. If ANY blocker is still open:
- Post status comment (see Step 5) with result: BLOCKED
- Remove label: `gh issue edit ${{ steps.pick.outputs.issue }} --remove-label "ready-for-implementation"`
- Stop here - do not implement
## Step 2: Get Epic Context (if part of an epic)
Check issue labels for `epic:*` label. If present:
```bash
# Find the active epic
gh issue list --label "type:epic" --label "status:active" --state open --json number,title,body --limit 1
```
Read the epic to understand the broader context and what's been completed.
## Step 3: Scope Check (IMPORTANT)
Before implementing, assess whether the issue scope is appropriate:
**Signs of scope creep (STOP and report):**
- Changes spanning multiple unrelated modules or subsystems
- Discovering substantial prerequisite work not mentioned in the issue
- The implementation feels like 2-3 separate issues bundled together
- Non-mechanical changes touching areas unrelated to the issue's focus
**NOT scope creep (proceed normally):**
- Mechanical changes across many files (renames, import updates, type fixes)
- Related changes that naturally flow from the core implementation
- Test files matching the implementation scope
When stopping for scope issues:
1. Post status (see Step 6) with result: INCOMPLETE
2. Add `needs-breakdown` label and explain how to split
3. If discovered blocker:
- Create issue with `discovered-blocker` AND `needs-review` labels
- Update this issue's "Blocked by:" section
- Remove `ready-for-implementation` from this issue
## Step 4: Check for Existing PR (Duplicate Detection)
Before creating a new PR, check if one already exists:
```bash
EXISTING_PR=$(gh pr list --head "claude/${{ steps.pick.outputs.issue }}-" --state open --json number,url --jq '.[0]')
```
If a PR exists:
- Update the existing PR instead of creating a new one
- Push to the existing branch
- Post a comment noting continued work
## Step 5: Implement
${{ steps.test-feedback.outputs.has_failures == 'true' && format('### Current Test Failures
The following checks are currently failing: {0}
```
{1}
```
**Be aware of these existing failures. If related to your changes, fix them.**
', steps.test-feedback.outputs.failures, steps.test-feedback.outputs.output) || '' }}
**Guidelines:**
- Keep it simple - only implement what's requested
- Run `mix precommit` before committing - NEVER use `--no-verify`
- Create PR with "Closes #${{ steps.pick.outputs.issue }}" in body
- Use branch naming: `claude/${{ steps.pick.outputs.issue }}-short-description`
**Protected files - changes require justification:**
- `docs/specs/*.md` - Specifications (source of truth)
- `docs/guidelines/*.md` - Process documentation
- `.github/workflows/*.yml` - Automation workflows
- `.credo.exs`, `.formatter.exs` - Linter configs
If you MUST change these, explain why in the PR description.
## TODO and Skip Tag Protocol (IMPORTANT)
When you need to add TODOs, skip tests, or disable linting:
**Step 1: Search for existing issues first**
```bash
gh issue list --search "keyword describing the problem" --state open --limit 5
```
**Step 2: Create issue if none exists**
```bash
gh issue create --title "[Tech Debt] Brief description" \
--label "tech-debt" --label "from-pr-review" \
--body "Source: \`path/to/file.ex:LINE\`
Description of what needs to be fixed/cleaned up.
Found during implementation of #${{ steps.pick.outputs.issue }}."
```
**Step 3: Reference issue in code (REQUIRED)**
```elixir
# TODO(#123): Explanation of what needs to be done
# FIXME(#123): Explanation of the bug
@tag :skip # Skipped: #123 - reason why test is skipped
# credo:disable-for-next-line Credo.Check.Name - #123
```
**NEVER add TODO/FIXME/@tag :skip without an issue reference.**
## Step 6: MANDATORY Status Update (ALWAYS DO THIS)
You MUST update the issue body with automation state. This is required
even if you encounter errors or cannot complete the implementation.
First, get the current issue body, then append/update the automation state section:
```bash
# Get current body and update automation state
CURRENT_BODY=$(gh issue view ${{ steps.pick.outputs.issue }} --json body --jq '.body')
# Remove old automation state if present
CLEAN_BODY=$(echo "$CURRENT_BODY" | sed '/^## Automation State/,/^## /{ /^## Automation State/d; /^## /!d; }' | sed '/<!-- automation-state:/d')
# Create new body with automation state
cat > /tmp/issue-body.md << 'ISSUE_EOF'
$CLEAN_BODY
## Automation State
<!-- automation-state: {"status":"[SUCCESS|INCOMPLETE|BLOCKED]","pr":null,"branch":"claude/${{ steps.pick.outputs.issue }}-xxx","attempts":1} -->
| Field | Value |
|-------|-------|
| Status | `[SUCCESS\|INCOMPLETE\|BLOCKED]` |
| PR | #NNN or None |
| Branch | `claude/${{ steps.pick.outputs.issue }}-xxx` |
| Attempts | 1 |
**Details:** [What was done, what remains, blockers discovered]
**Next Steps:** [If not SUCCESS: what needs to happen]
ISSUE_EOF
gh issue edit ${{ steps.pick.outputs.issue }} --body-file /tmp/issue-body.md
```
**Status meanings:**
- SUCCESS: PR created, all work complete
- INCOMPLETE: Stopped due to scope/complexity/errors (explain why)
- BLOCKED: Open dependencies prevent implementation
NEVER include "@claude" in comments.
claude_args: '--model claude-opus-4-8 --max-turns 300 --allowed-tools "Read,Write,Edit,MultiEdit,Glob,Grep,LS,Bash,WebSearch,WebFetch,Task,TodoWrite,TodoRead"'
show_full_output: true
# Verify status was updated in issue body
- name: Verify implementation status
if: always() && steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
id: verify-status
timeout-minutes: 3
env:
GH_TOKEN: ${{ github.token }}
run: |
# Check if automation state exists in issue body
ISSUE_BODY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body')
if echo "$ISSUE_BODY" | grep -q "<!-- automation-state:"; then
echo "status_posted=true" >> "$GITHUB_OUTPUT"
# Extract status from JSON
STATUS_JSON=$(echo "$ISSUE_BODY" | grep -o '<!-- automation-state: {[^}]*}' | sed 's/<!-- automation-state: //')
RESULT=$(echo "$STATUS_JSON" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
echo "result=$RESULT" >> "$GITHUB_OUTPUT"
echo "Implementation result: $RESULT"
else
echo "status_posted=false" >> "$GITHUB_OUTPUT"
echo "::warning::No automation state found in issue body"
fi
# Post fallback status if Claude didn't update issue body
- name: Post fallback status
if: always() && steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true' && steps.verify-status.outputs.status_posted == 'false'
timeout-minutes: 5
env:
GH_TOKEN: ${{ github.token }}
run: |
# Check for branch and PR
BRANCH_NAME="claude/${ISSUE_NUMBER}-"
BRANCH=$(git branch -r --list "origin/${BRANCH_NAME}*" | head -1 | tr -d ' ')
BRANCH_SHORT=$(echo "$BRANCH" | sed 's|origin/||' | tr -d ' ')
PR_NUMBER=""
if [ -n "$BRANCH_SHORT" ]; then
PR_NUMBER=$(gh pr list --head "$BRANCH_SHORT" --json number --jq '.[0].number' 2>/dev/null || echo "")
fi
# Get current issue body
CURRENT_BODY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body')
# Remove any partial automation state
CLEAN_BODY=$(echo "$CURRENT_BODY" | sed '/^## Automation State/,$d')
# Create updated body with fallback status
cat > /tmp/issue-body.md << EOF
$CLEAN_BODY
## Automation State
<!-- automation-state: {"status":"INCOMPLETE","pr":${PR_NUMBER:-null},"branch":"${BRANCH_SHORT:-null}","attempts":1,"interrupted":true} -->
| Field | Value |
|-------|-------|
| Status | \`INCOMPLETE\` |
| PR | ${PR_NUMBER:-None} |
| Branch | \`${BRANCH_SHORT:-None}\` |
| Attempts | 1 |
**Details:** Workflow interrupted before completion (timeout or error). Check workflow logs.
**Next Steps:** Re-trigger with \`@claude Please continue implementing this issue.\`
EOF
gh issue edit "$ISSUE_NUMBER" --body-file /tmp/issue-body.md
# Add needs-attention label
gh issue edit "$ISSUE_NUMBER" --add-label "needs-attention" || true
# Safety net: push any unpushed commits and remove workflow files
- name: Push unpushed commits
if: always() && steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
timeout-minutes: 5
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
run: |
ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --json state --jq '.state')
if [ "$ISSUE_STATE" != "OPEN" ]; then
echo "Issue #$ISSUE_NUMBER is $ISSUE_STATE; skipping fallback push."
exit 0
fi
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
[ "$CURRENT_BRANCH" = "main" ] && exit 0
# Re-configure git auth (Claude may have changed it)
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
# Remove workflow files from any unpushed commits (GitHub App tokens can't push these)
REWROTE_COMMIT=false
if git log origin/main..HEAD --name-only --pretty=format: 2>/dev/null | grep -q "^\.github/workflows/"; then
echo "Removing workflow files from commit..."
COMMIT_MSG=$(git log -1 --format=%s)
git reset HEAD~1 --soft
git checkout origin/main -- .github/workflows/ 2>/dev/null || true
git add -A
git reset HEAD -- .github/workflows/ 2>/dev/null || true
if git commit -m "$COMMIT_MSG" 2>/dev/null; then
REWROTE_COMMIT=true
else
echo "No changes after removing workflow files"
fi
fi
# Push if there are unpushed commits
if git log origin/main..HEAD --oneline 2>/dev/null | grep -q .; then
echo "Pushing unpushed commits..."
if [ "$REWROTE_COMMIT" = "true" ]; then
echo "Using force-with-lease (commit was rewritten to remove workflow files)"
git push --force-with-lease -u origin HEAD
else
git push -u origin HEAD
fi
echo "branch_pushed=true" >> "$GITHUB_OUTPUT"
echo "branch_name=$CURRENT_BRANCH" >> "$GITHUB_OUTPUT"
echo "::notice::Pushed commits"
fi
- name: Create PR if branch was pushed but no PR exists
if: always() && steps.pick.outputs.has_work == 'true' && steps.claim.outcome == 'success' && steps.issue-state.outcome == 'success' && steps.issue-state.outputs.should_run == 'true'
timeout-minutes: 5
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
run: |
ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --json state --jq '.state')
if [ "$ISSUE_STATE" != "OPEN" ]; then
echo "Issue #$ISSUE_NUMBER is $ISSUE_STATE; skipping fallback PR creation."
exit 0
fi
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
[ "$CURRENT_BRANCH" = "main" ] && exit 0
# Check if PR already exists for this branch
EXISTING_PR=$(gh pr list --head "$CURRENT_BRANCH" --state all --json number --jq '.[0].number')
MERGED_CLOSING_PR=$(gh pr list \
--repo "${{ github.repository }}" \
--state merged \
--limit 100 \
--json number,body \
| jq -r --arg issue "$ISSUE_NUMBER" '[.[] | select((.body // "") | test("(?i)(closes|fixes|resolves)[[:space:]]+#" + $issue + "([^0-9]|$)"))][0].number // empty')
if [ -n "$MERGED_CLOSING_PR" ]; then
echo "Merged closing PR #$MERGED_CLOSING_PR already exists; skipping fallback PR creation."
exit 0
fi
if [ -z "$EXISTING_PR" ]; then
echo "No PR exists for branch $CURRENT_BRANCH, creating one..."
# Get issue title for PR title
ISSUE_TITLE=$(gh issue view ${{ env.ISSUE_NUMBER }} --json title --jq '.title')
gh pr create \
--head "$CURRENT_BRANCH" \
--base main \
--title "$ISSUE_TITLE" \
--body "$(cat <<EOF
Closes #${{ env.ISSUE_NUMBER }}
This PR was created by the fallback mechanism after Claude's push succeeded
but PR creation failed (likely due to GitHub App permission issues).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)" || echo "::warning::Could not create PR"
else
echo "PR #$EXISTING_PR already exists for branch $CURRENT_BRANCH"
fi
# Release the claim and keep the queue draining. Runs even on failure so a
# crashed item never stays stuck as impl:running. Re-dispatch is a fast
# path; the */10 schedule is the guaranteed backstop.
- name: Release claim and drain next
if: always() && steps.pick.outputs.has_work == 'true'
timeout-minutes: 5
env:
GH_TOKEN: ${{ secrets.PAT_WORKFLOW_TRIGGER || github.token }}
run: |
set -euo pipefail
ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --json state --jq '.state')
if [ "${{ steps.claim.outcome }}" != "success" ] || [ "${{ steps.issue-state.outcome }}" != "success" ]; then
if [ "$ISSUE_STATE" = "OPEN" ]; then
echo "Pre-implementation setup failed; requeueing issue #$ISSUE_NUMBER."
gh issue edit "$ISSUE_NUMBER" \
--remove-label "impl:running" \
--add-label "impl:queued" 2>/dev/null || true
else
echo "Pre-implementation setup failed, but issue #$ISSUE_NUMBER is $ISSUE_STATE; removing running claim."
gh issue edit "$ISSUE_NUMBER" --remove-label "impl:running" 2>/dev/null || true
fi
else
gh issue edit "$ISSUE_NUMBER" --remove-label "impl:running" 2>/dev/null || true
fi
REMAINING=$(gh issue list \
--repo "${{ github.repository }}" \
--label "impl:queued" --state open \
--json number --jq 'length')
echo "Released issue #$ISSUE_NUMBER. Queue depth now: ${REMAINING:-0}."
if [ "${REMAINING:-0}" -gt 0 ]; then
echo "Re-dispatching runner to drain next queued issue..."
gh workflow run claude-issue.yml --repo "${{ github.repository }}" \
|| echo "::warning::Self re-dispatch failed; the */10 schedule will drain the queue."
fi