Skip to content

Commit f405c8f

Browse files
authored
fix: sync lifecycle workflow fixes from template
Syncs 3 workflow fixes from sigvardt/ai-lifecycle-workflows#1: - ai-pr-body-check.yml: enables auto-merge after adding close keyword (GAP 1+5) - ai-unstick.yml: scheduled runs + comprehensive stuck detection (GAP 2+3+4) - ai-success-cleanup.yml: explicit issue close on merge (GAP 6) Files are byte-identical to template repo.
1 parent f2f5594 commit f405c8f

File tree

3 files changed

+198
-45
lines changed

3 files changed

+198
-45
lines changed

.github/workflows/ai-pr-body-check.yml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
permissions:
99
pull-requests: write
10-
contents: read
10+
contents: write
1111

1212
concurrency:
1313
group: ai-pr-body-${{ github.event.pull_request.number }}
@@ -57,3 +57,33 @@ jobs:
5757
gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --body "$NEW_BODY"
5858
gh pr comment "$PR_NUM" --repo "${{ github.repository }}" \
5959
--body "<!-- ai-pr-body-fix --> Auto-added \"Closes #${ISSUE_NUM}\" to PR body (was missing)."
60+
61+
- name: Ensure auto-merge is enabled
62+
env:
63+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
run: |
65+
PR_NUM="${{ github.event.pull_request.number }}"
66+
REPO="${{ github.repository }}"
67+
68+
# Re-read the PR body (may have been updated by previous step)
69+
PR_BODY=$(gh pr view "$PR_NUM" --repo "$REPO" --json body --jq '.body // ""')
70+
if ! echo "$PR_BODY" | grep -iqE "(closes|fixes|resolves)\s+#[0-9]+"; then
71+
echo "No close keyword in PR body — skipping auto-merge"
72+
exit 0
73+
fi
74+
75+
# Check if auto-merge is already enabled
76+
AUTO_MERGE=$(gh pr view "$PR_NUM" --repo "$REPO" --json autoMergeRequest --jq '.autoMergeRequest // empty')
77+
if [ -n "$AUTO_MERGE" ]; then
78+
echo "Auto-merge already enabled — skipping"
79+
exit 0
80+
fi
81+
82+
# Enable auto-merge with squash strategy
83+
if gh pr merge --auto --squash "$PR_NUM" --repo "$REPO" 2>/dev/null; then
84+
echo "Auto-merge enabled for PR #$PR_NUM"
85+
gh pr comment "$PR_NUM" --repo "$REPO" \
86+
--body "<!-- ai-auto-merge-enabled --> Auto-merge enabled by body-check workflow."
87+
else
88+
echo "::warning::Failed to enable auto-merge for PR #$PR_NUM (may already be enabled or not eligible)"
89+
fi

.github/workflows/ai-success-cleanup.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ jobs:
5555
done
5656
echo "Labels cleaned for issue #$ISSUE_NUM"
5757
58+
# Explicitly close the issue (safety net if "Closes #N" auto-close didn't trigger)
59+
ISSUE_STATE=$(gh issue view "$ISSUE_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
60+
if [ "$ISSUE_STATE" = "OPEN" ]; then
61+
gh issue close "$ISSUE_NUM" --repo "$REPO" 2>/dev/null || true
62+
echo "Issue #$ISSUE_NUM explicitly closed"
63+
else
64+
echo "Issue #$ISSUE_NUM already closed (state: $ISSUE_STATE)"
65+
fi
66+
5867
# Delete the AI branch
5968
gh api "repos/$REPO/git/refs/heads/$BRANCH" --method DELETE 2>/dev/null || true
6069
echo "Branch $BRANCH deleted"

.github/workflows/ai-unstick.yml

Lines changed: 158 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,199 @@
11
name: AI Unstick Deadlocked Issues
22

33
on:
4+
schedule:
5+
- cron: '*/30 * * * *'
46
workflow_dispatch:
57
inputs:
68
issue_number:
79
description: 'Specific issue number to unstick (leave empty for all stuck issues)'
810
required: false
911
default: ''
1012

13+
concurrency:
14+
group: ai-unstick
15+
cancel-in-progress: false
16+
1117
permissions:
1218
issues: write
19+
pull-requests: read
20+
contents: read
1321

1422
jobs:
1523
unstick:
1624
runs-on: ubuntu-latest
1725
steps:
18-
- name: Find and unstick deadlocked issues
26+
- name: Find and unstick stalled issues
1927
env:
2028
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21-
INPUT_ISSUE: ${{ inputs.issue_number }}
29+
INPUT_ISSUE: ${{ github.event.inputs.issue_number || '' }}
2230
run: |
2331
REPO="${{ github.repository }}"
2432
UNSTUCK=0
33+
BLOCKED=0
2534
SKIPPED=0
35+
PROCESSED=""
36+
THRESHOLD_DATE=$(date -u -d '40 minutes ago' +%Y-%m-%dT%H:%M:%SZ)
37+
SINCE_DATE=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)
2638
27-
if [ -n "$INPUT_ISSUE" ]; then
28-
# Specific issue mode
29-
ISSUES="$INPUT_ISSUE"
30-
echo "Targeting specific issue: #$INPUT_ISSUE"
31-
else
32-
# Find all deadlocked issues: have both ai-review-ready AND ai-debugging
33-
ISSUES=$(gh issue list --repo "$REPO" \
34-
--label "ai-review-ready" --label "ai-debugging" \
35-
--state open --json number --jq '.[].number' 2>/dev/null)
36-
if [ -z "$ISSUES" ]; then
37-
echo "No stuck issues found — nothing to do"
38-
exit 0
39+
was_processed() {
40+
case " $PROCESSED " in
41+
*" $1 "*)
42+
return 0
43+
;;
44+
*)
45+
return 1
46+
;;
47+
esac
48+
}
49+
50+
mark_processed() {
51+
if ! was_processed "$1"; then
52+
PROCESSED="$PROCESSED $1"
3953
fi
40-
echo "Found stuck issues: $ISSUES"
41-
fi
54+
}
4255
43-
for ISSUE_NUM in $ISSUES; do
44-
echo ""
45-
echo "=== Processing issue #$ISSUE_NUM ==="
56+
reset_issue() {
57+
ISSUE_NUM="$1"
58+
REASON="$2"
4659
47-
# Verify the issue exists and check current labels
48-
CURRENT_LABELS=$(gh issue view "$ISSUE_NUM" --repo "$REPO" \
49-
--json labels --jq '[.labels[].name]' 2>/dev/null)
50-
if [ $? -ne 0 ]; then
51-
echo "Issue #$ISSUE_NUM not found — skipping"
60+
if ! gh issue view "$ISSUE_NUM" --repo "$REPO" --json number --jq '.number' >/dev/null 2>&1; then
61+
echo "Issue #$ISSUE_NUM not found - skipping"
5262
SKIPPED=$((SKIPPED + 1))
53-
continue
63+
return
5464
fi
55-
echo "Current labels: $CURRENT_LABELS"
5665
57-
# Remove ai-review-ready FIRST (critical: this is the exclude label in ai-task-auto-label)
58-
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
59-
--remove-label "ai-review-ready" 2>/dev/null || true
66+
RESET_COUNT=$(gh api "repos/${REPO}/issues/${ISSUE_NUM}/comments" \
67+
--paginate \
68+
--jq "[.[] | select(.created_at >= \"${SINCE_DATE}\") | select(.body | contains(\"<!-- ai-stall-reset -->\"))] | length" 2>/dev/null || printf "0")
6069
61-
# Remove ai-in-progress if present
62-
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
63-
--remove-label "ai-in-progress" 2>/dev/null || true
70+
if [ -z "$RESET_COUNT" ]; then
71+
RESET_COUNT=0
72+
fi
73+
74+
if [ "$RESET_COUNT" -ge 3 ]; then
75+
echo "Circuit breaker tripped for #$ISSUE_NUM ($RESET_COUNT reset attempts in last 24h)"
76+
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
77+
--remove-label "ai-task" --remove-label "ai-in-progress" \
78+
--remove-label "ai-review-ready" --remove-label "ai-debugging" \
79+
--add-label "ai-blocked" 2>/dev/null || true
80+
81+
gh issue comment "$ISSUE_NUM" --repo "$REPO" \
82+
--body "<!-- ai-stall-circuit-breaker --> ⛔ Circuit breaker tripped after $RESET_COUNT stall resets in the last 24 hours. Removed AI lifecycle labels and added \`ai-blocked\`. Reason: $REASON"
83+
84+
BLOCKED=$((BLOCKED + 1))
85+
return
86+
fi
6487
65-
# Add ai-task (the orchestrator pickup label)
66-
# Keep ai-debugging (signals this is a retry)
6788
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
68-
--add-label "ai-task" 2>/dev/null || true
89+
--remove-label "ai-review-ready" --remove-label "ai-in-progress" \
90+
--remove-label "ai-debugging" --add-label "ai-task" 2>/dev/null || true
6991
70-
# Post unstick comment
7192
gh issue comment "$ISSUE_NUM" --repo "$REPO" \
72-
--body "<!-- ai-unstick --> 🔧 Labels reset by ai-unstick workflow. Removed \`ai-review-ready\`, added \`ai-task\`. Issue is now eligible for orchestrator pickup."
73-
74-
# Verify final state
75-
FINAL_LABELS=$(gh issue view "$ISSUE_NUM" --repo "$REPO" \
76-
--json labels --jq '[.labels[].name]' 2>/dev/null)
77-
echo "Final labels: $FINAL_LABELS"
93+
--body "<!-- ai-stall-reset --> 🔧 Stall reset by ai-unstick. Reason: $REASON. Action: removed \`ai-review-ready\`, \`ai-in-progress\`, \`ai-debugging\`; added \`ai-task\`."
7894
7995
UNSTUCK=$((UNSTUCK + 1))
80-
done
96+
}
97+
98+
if [ -n "$INPUT_ISSUE" ]; then
99+
echo "Manual mode: targeting issue #$INPUT_ISSUE"
100+
reset_issue "$INPUT_ISSUE" "manual workflow_dispatch reset request"
101+
else
102+
echo "Automated mode: evaluating stuck-state strategies"
103+
104+
STRATEGY1=$(gh issue list --repo "$REPO" \
105+
--label "ai-review-ready" --label "ai-debugging" \
106+
--state open --json number --jq '.[].number' 2>/dev/null)
107+
108+
for ISSUE_NUM in $STRATEGY1; do
109+
if was_processed "$ISSUE_NUM"; then
110+
continue
111+
fi
112+
reset_issue "$ISSUE_NUM" "strategy1: issue has both ai-review-ready and ai-debugging"
113+
mark_processed "$ISSUE_NUM"
114+
done
115+
116+
STRATEGY2=$(gh issue list --repo "$REPO" \
117+
--label "ai-in-progress" --label "ai-debugging" \
118+
--state open --json number --jq '.[].number' 2>/dev/null)
119+
120+
for ISSUE_NUM in $STRATEGY2; do
121+
if was_processed "$ISSUE_NUM"; then
122+
continue
123+
fi
124+
reset_issue "$ISSUE_NUM" "strategy2: issue has both ai-in-progress and ai-debugging"
125+
mark_processed "$ISSUE_NUM"
126+
done
127+
128+
STRATEGY3=$(gh issue list --repo "$REPO" \
129+
--label "ai-in-progress" \
130+
--state open --json number --jq '.[].number' 2>/dev/null)
131+
132+
for ISSUE_NUM in $STRATEGY3; do
133+
if was_processed "$ISSUE_NUM"; then
134+
continue
135+
fi
136+
137+
LATEST_COMMENT_DATE=$(gh issue view "$ISSUE_NUM" --repo "$REPO" \
138+
--json comments --jq '.comments | map(.createdAt) | sort | last // ""' 2>/dev/null)
139+
140+
PR_NUMBER=$(gh pr list --repo "$REPO" \
141+
--search "head:ai/issue-${ISSUE_NUM}" --state open \
142+
--json number --jq '.[0].number // ""' 2>/dev/null)
143+
144+
LATEST_COMMIT_DATE=""
145+
if [ -n "$PR_NUMBER" ]; then
146+
LATEST_COMMIT_DATE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
147+
--json commits --jq '.commits | map(.committedDate) | sort | last // ""' 2>/dev/null)
148+
fi
149+
150+
RECENT_COMMENT=0
151+
RECENT_COMMIT=0
152+
153+
if [ -n "$LATEST_COMMENT_DATE" ] && [ "$LATEST_COMMENT_DATE" \> "$THRESHOLD_DATE" ]; then
154+
RECENT_COMMENT=1
155+
fi
156+
157+
if [ -n "$LATEST_COMMIT_DATE" ] && [ "$LATEST_COMMIT_DATE" \> "$THRESHOLD_DATE" ]; then
158+
RECENT_COMMIT=1
159+
fi
160+
161+
if [ "$RECENT_COMMENT" -eq 1 ] || [ "$RECENT_COMMIT" -eq 1 ]; then
162+
echo "Skipping #$ISSUE_NUM - recent activity detected"
163+
SKIPPED=$((SKIPPED + 1))
164+
continue
165+
fi
166+
167+
reset_issue "$ISSUE_NUM" "strategy3: ai-in-progress stalled over 40 minutes (no recent comment or PR commit activity)"
168+
mark_processed "$ISSUE_NUM"
169+
done
170+
171+
STRATEGY4=$(gh issue list --repo "$REPO" \
172+
--label "ai-review-ready" \
173+
--state open --json number --jq '.[].number' 2>/dev/null)
174+
175+
for ISSUE_NUM in $STRATEGY4; do
176+
if was_processed "$ISSUE_NUM"; then
177+
continue
178+
fi
179+
180+
PR_COUNT=$(gh pr list --repo "$REPO" \
181+
--search "head:ai/issue-${ISSUE_NUM}" --state open \
182+
--json number --jq 'length' 2>/dev/null || printf "0")
183+
184+
if [ "$PR_COUNT" -gt 0 ]; then
185+
echo "Skipping #$ISSUE_NUM - open PR found"
186+
SKIPPED=$((SKIPPED + 1))
187+
continue
188+
fi
189+
190+
reset_issue "$ISSUE_NUM" "strategy4: ai-review-ready issue has no associated open PR"
191+
mark_processed "$ISSUE_NUM"
192+
done
193+
fi
81194
82-
echo ""
195+
echo
83196
echo "=== Summary ==="
84197
echo "Unstuck: $UNSTUCK issues"
198+
echo "Blocked: $BLOCKED issues"
85199
echo "Skipped: $SKIPPED issues"

0 commit comments

Comments
 (0)