Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/ai-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: AI Auto-Merge

on:
check_suite:
types: [completed]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:

permissions:
pull-requests: write
contents: write
checks: read

concurrency:
group: ai-auto-merge-${{ github.event.pull_request.number || github.event.check_suite.id || github.run_id }}
cancel-in-progress: false

jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
- name: Identify PR
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" = "check_suite" ]; then
BRANCH="${{ github.event.check_suite.head_branch }}"
if [[ ! "$BRANCH" =~ ^ai/ ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Not an AI branch: $BRANCH"
exit 0
fi
# Find PR for this branch
PR_NUM=$(gh pr list --repo "${{ github.repository }}" \
--head "$BRANCH" --state open --json number --jq '.[0].number // empty')
if [ -z "$PR_NUM" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No open PR for branch: $BRANCH"
exit 0
fi
elif [ "${{ github.event_name }}" = "pull_request" ]; then
BRANCH="${{ github.event.pull_request.head.ref }}"
if [[ ! "$BRANCH" =~ ^ai/ ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Not an AI branch: $BRANCH"
exit 0
fi
PR_NUM="${{ github.event.pull_request.number }}"
else
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Manual dispatch — use with gh workflow run"
exit 0
fi

echo "skip=false" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Found AI PR #$PR_NUM on branch $BRANCH"

- name: Check workflow file guard
if: steps.pr.outputs.skip != 'true'
id: guard
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ steps.pr.outputs.pr_number }}
run: |
# Block PRs that modify workflow files
CHANGED_WORKFLOWS=$(gh pr diff "$PR_NUM" --repo "${{ github.repository }}" \
--name-only | grep -c '^\.github/workflows/' || true)
if [ "$CHANGED_WORKFLOWS" -gt 0 ]; then
echo "blocked=true" >> "$GITHUB_OUTPUT"
echo "::error::AI PRs cannot modify workflow files"
gh pr comment "$PR_NUM" --repo "${{ github.repository }}" \
--body "<!-- ai-auto-merge-blocked --> ⛔ Auto-merge blocked: This PR modifies \`.github/workflows/\` files. AI PRs are not allowed to change workflow files. A human must review and merge this PR manually."
exit 0
fi
echo "blocked=false" >> "$GITHUB_OUTPUT"

- name: Check PR body for close keyword
if: steps.pr.outputs.skip != 'true' && steps.guard.outputs.blocked != 'true'
id: body
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ steps.pr.outputs.pr_number }}
run: |
PR_BODY=$(gh pr view "$PR_NUM" --repo "${{ github.repository }}" \
--json body --jq '.body // ""')
ISSUE_NUM=$(echo "${{ steps.pr.outputs.branch }}" | sed -E 's|^ai/issue-([0-9]+).*|\1|')

if [ -z "$ISSUE_NUM" ] || [ "$ISSUE_NUM" = "${{ steps.pr.outputs.branch }}" ]; then
echo "valid=true" >> "$GITHUB_OUTPUT"
echo "Could not extract issue number — skipping body check"
exit 0
fi

if echo "$PR_BODY" | grep -iE "(Closes|Fixes|Resolves)\s+#${ISSUE_NUM}" > /dev/null 2>&1; then
echo "valid=true" >> "$GITHUB_OUTPUT"
echo "PR body contains close keyword for issue #$ISSUE_NUM"
else
echo "valid=false" >> "$GITHUB_OUTPUT"
echo "Missing close keyword — ai-pr-body-check should fix this"
fi

- name: Enable auto-merge
if: >-
steps.pr.outputs.skip != 'true' &&
steps.guard.outputs.blocked != 'true' &&
steps.body.outputs.valid != 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ steps.pr.outputs.pr_number }}
run: |
# Check if auto-merge is already enabled
EXISTING=$(gh pr view "$PR_NUM" --repo "${{ github.repository }}" \
--json autoMergeRequest --jq '.autoMergeRequest != null')
if [ "$EXISTING" = "true" ]; then
echo "Auto-merge already enabled for PR #$PR_NUM"
exit 0
fi

gh pr merge "$PR_NUM" \
--repo "${{ github.repository }}" \
--squash --auto

gh pr comment "$PR_NUM" --repo "${{ github.repository }}" \
--body "<!-- ai-auto-merge --> 🤖 Auto-merge enabled (squash). Will merge when all required checks pass."

echo "Auto-merge enabled for PR #$PR_NUM"
34 changes: 33 additions & 1 deletion .github/workflows/ai-auto-rebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ jobs:
echo "$prs" | while read -r number branch; do
echo "::group::Rebasing PR #$number ($branch)"

# Skip PRs with auto-merge pending (force-push would cancel it)
AUTO_MERGE=$(gh pr view "$number" --repo "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')
if [ "$AUTO_MERGE" = "true" ]; then
echo "⏭️ PR #$number has auto-merge pending — skipping rebase"
echo "::endgroup::"
continue
fi

git fetch origin "$branch"
git checkout -B "$branch" "origin/$branch"

Expand All @@ -60,6 +68,28 @@ jobs:
issue_num=$(echo "$branch" | sed -E 's|^ai/issue-([0-9]+).*|\1|')

if [ -n "$issue_num" ] && [ "$issue_num" != "$branch" ]; then
REBASE_ATTEMPTS=$(gh api "repos/${{ github.repository }}/issues/${issue_num}/comments" \
--paginate --jq '[.[] | select(.body | contains("<!-- ai-rebase-attempt -->"))] | length' \
2>/dev/null | awk '{sum+=$1} END {print sum+0}')

if [ "$REBASE_ATTEMPTS" -ge 10 ]; then
echo "🛑 Circuit breaker: issue #$issue_num has $REBASE_ATTEMPTS rebase failures"
gh issue edit "$issue_num" \
--remove-label "ai-task,ai-in-progress,ai-review-ready,ai-debugging" \
--add-label "ai-blocked" 2>/dev/null || true

{
echo "<!-- circuit-breaker-tripped -->"
echo "## 🛑 Circuit Breaker Tripped — Human Intervention Required"
echo ""
echo "This issue has failed **$REBASE_ATTEMPTS rebase attempts** and has been blocked."
echo "A human must investigate the persistent merge conflicts and remove \`ai-blocked\` to resume."
} > /tmp/circuit-breaker.md
gh issue comment "$issue_num" --body-file /tmp/circuit-breaker.md
echo "::endgroup::"
continue
fi

# Keep the PR open — the AI will resolve conflicts on the existing branch
# Just relabel the issue so the orchestrator picks it up
gh issue edit "$issue_num" \
Expand All @@ -73,7 +103,9 @@ jobs:

# Post context so the AI knows exactly what to fix
{
echo "## ⚠️ Merge Conflicts — Fix Needed"
echo "<!-- ai-rebase-attempt: $((REBASE_ATTEMPTS + 1))/10 -->"
echo "<!-- ai-rebase-attempt -->"
echo "## ⚠️ Merge Conflicts — Fix Needed (Attempt $((REBASE_ATTEMPTS + 1))/10)"
echo ""
echo "**Branch:** \`$branch\` (PR #$number still open — resolve conflicts on existing branch, don't start fresh)"
echo ""
Expand Down
65 changes: 58 additions & 7 deletions .github/workflows/ai-ci-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ name: AI CI Failure Recovery

on:
workflow_run:
workflows: ["CI"]
workflows: ["CI", "Secret Scan"]
types: [completed]

permissions:
issues: write
contents: read
pull-requests: write

concurrency:
group: ai-recovery-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false

jobs:
recover:
if: >
Expand Down Expand Up @@ -48,8 +52,53 @@ jobs:
# Write to file to avoid shell escaping issues
echo "$ERROR_LOG" > /tmp/error_log.txt

- name: Post failure context and relabel for retry
- name: Check circuit breaker
id: circuit_breaker
if: steps.extract.outputs.issue_number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUM="${{ steps.extract.outputs.issue_number }}"
REPO="${{ github.repository }}"

# Count recovery attempt markers in issue comments (awk sums across pages)
ATTEMPT_COUNT=$(gh api "repos/${REPO}/issues/${ISSUE_NUM}/comments" \
--paginate --jq '[.[] | select(.body | contains("<!-- ai-recovery-attempt -->"))] | length' \
2>/dev/null | awk '{sum+=$1} END {print sum+0}')

echo "attempt_count=$ATTEMPT_COUNT" >> "$GITHUB_OUTPUT"

if [ "$ATTEMPT_COUNT" -ge 10 ]; then
echo "tripped=true" >> "$GITHUB_OUTPUT"
echo "🛑 Circuit breaker tripped: $ATTEMPT_COUNT attempts on issue #$ISSUE_NUM"

# Remove ALL AI lifecycle labels, add ai-blocked
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
--remove-label "ai-task" \
--remove-label "ai-in-progress" \
--remove-label "ai-review-ready" \
--remove-label "ai-debugging" \
--add-label "ai-blocked" 2>/dev/null || true

# Post circuit breaker comment
cat > /tmp/circuit-breaker.md << 'CB_EOF'
<!-- circuit-breaker-tripped -->
## 🛑 Circuit Breaker Tripped — Human Intervention Required

This issue has failed **10+ recovery attempts** and has been blocked.

The AI was unable to resolve the CI/scan failures after repeated retries.
A human must investigate, fix the underlying issue, and remove the `ai-blocked` label to resume.
CB_EOF
sed -i '' 's/^ //' /tmp/circuit-breaker.md
gh issue comment "$ISSUE_NUM" --repo "$REPO" --body-file /tmp/circuit-breaker.md
else
echo "tripped=false" >> "$GITHUB_OUTPUT"
echo "Recovery attempt $((ATTEMPT_COUNT + 1))/10 for issue #$ISSUE_NUM"
fi

- name: Post failure context and relabel for retry
if: steps.extract.outputs.issue_number != '' && steps.circuit_breaker.outputs.tripped != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -60,17 +109,19 @@ jobs:
REPO="${{ github.repository }}"
RUN_URL="${{ github.event.workflow_run.html_url }}"
ERROR_LOG=$(cat /tmp/error_log.txt)
ATTEMPT_NUM=$(( ${{ steps.circuit_breaker.outputs.attempt_count }} + 1 ))

# Keep the PR open — the AI will fix the issue on the existing branch
# Just relabel the issue so the orchestrator picks it up
# Atomic label swap: remove ai-review-ready BEFORE adding ai-task to prevent deadlock
gh issue edit "$ISSUE_NUM" --repo "$REPO" \
--remove-label "ai-in-progress" \
--remove-label "ai-review-ready" \
--add-label "ai-task,ai-debugging" 2>/dev/null || true

# Post detailed failure context so the AI knows exactly what to fix
# Post detailed failure context with recovery attempt marker
cat > /tmp/comment.md << COMMENT_EOF
## ⚠️ CI Failure — Fix Needed
<!-- ai-recovery-attempt: ${ATTEMPT_NUM}/10 -->
<!-- ai-recovery-attempt -->
## ⚠️ CI/Scan Failure — Fix Needed (Attempt ${ATTEMPT_NUM}/10)

**Branch:** \`$BRANCH\` (PR #$PR_NUM still open — fix on existing branch, don't start fresh)
**Failed jobs:** $FAILED_JOBS
Expand All @@ -85,6 +136,6 @@ jobs:
COMMENT_EOF

# Trim leading whitespace from heredoc
sed -i 's/^ //' /tmp/comment.md
sed -i '' 's/^ //' /tmp/comment.md

gh issue comment "$ISSUE_NUM" --repo "$REPO" --body-file /tmp/comment.md
59 changes: 59 additions & 0 deletions .github/workflows/ai-pr-body-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: AI PR Body Check

on:
pull_request:
types: [opened, edited, reopened]
workflow_dispatch:

permissions:
pull-requests: write
contents: read

concurrency:
group: ai-pr-body-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
check-pr-body:
if: startsWith(github.event.pull_request.head.ref, 'ai/issue-')
runs-on: ubuntu-latest

steps:
- name: Extract issue number from branch
id: extract
run: |
ISSUE_NUM=$(echo "${{ github.event.pull_request.head.ref }}" | sed -E 's|^ai/issue-([0-9]+).*|\1|')
echo "issue_num=$ISSUE_NUM" >> $GITHUB_OUTPUT

- name: Check PR body for close keywords
id: check
env:
PR_BODY: ${{ github.event.pull_request.body }}
ISSUE_NUM: ${{ steps.extract.outputs.issue_num }}
run: |
if [ -z "$PR_BODY" ]; then
echo "has_close_keyword=false" >> $GITHUB_OUTPUT
elif echo "$PR_BODY" | grep -iqE "(closes|fixes|resolves)\s+#${ISSUE_NUM}"; then
echo "has_close_keyword=true" >> $GITHUB_OUTPUT
else
echo "has_close_keyword=false" >> $GITHUB_OUTPUT
fi

- name: Prepend close keyword if missing
if: steps.check.outputs.has_close_keyword == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
PR_NUM="${{ github.event.pull_request.number }}"
ISSUE_NUM="${{ steps.extract.outputs.issue_num }}"

if [ -z "$PR_BODY" ]; then
NEW_BODY="Closes #${ISSUE_NUM}"
else
NEW_BODY=$(printf "Closes #%s\n\n%s" "$ISSUE_NUM" "$PR_BODY")
fi

gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --body "$NEW_BODY"
gh pr comment "$PR_NUM" --repo "${{ github.repository }}" \
--body "<!-- ai-pr-body-fix --> Auto-added \"Closes #${ISSUE_NUM}\" to PR body (was missing)."
Loading
Loading