Skip to content

Claude Issue Implementation #2911

Claude Issue Implementation

Claude Issue Implementation #2911

Workflow file for this run

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