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
82 changes: 76 additions & 6 deletions skills/automate-github-issues/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ An Agent Skill that sets up your repository to automatically triage and fix GitH

When activated, this skill **bootstraps your repository** with a 5-phase automated pipeline:

1. **Analyze**: Fetches all open GitHub issues and formats them as a structured markdown document for analysis (`fleet-analyze.ts`).
2. **Plan**: A Jules session performs deep code-level triage — diagnosing root causes, proposing implementations, and producing self-contained task prompts.
3. **Validate**: Ownership validation ensures no two tasks modify the same file, preventing merge conflicts.
4. **Dispatch**: Spawns parallel Jules sessions — one per task — each with a code-rich, self-contained prompt (`fleet-dispatch.ts`).
5. **Merge**: A sequential merge workflow waits for CI, updates branches, and merges PRs in risk order (lowest first).

## Example Prompt

```text
Expand Down Expand Up @@ -49,6 +43,82 @@ scripts/fleet/ # Pipeline scripts (committed to your repo)
- A [Jules API key](https://jules.google.com/)
- GitHub token with repo access

### Pipeline Overview

```mermaid
flowchart LR
A["📊 Analyze"] --> B["🧠 Plan"]
B --> C["✅ Validate"]
C --> D["🚀 Dispatch"]
D --> E["🔀 Merge"]
```

| Phase | Script | What it does |
|-------|--------|-------------|
| **Analyze** | `fleet-analyze.ts` | Fetches open issues → structured markdown |
| **Plan** | `fleet-plan.ts` | Jules diagnoses root causes, builds File Ownership Matrix |
| **Validate** | `fleet-dispatch.ts` | Checks no two tasks claim the same file |
| **Dispatch** | `fleet-dispatch.ts` | Spawns parallel Jules sessions via `jules.all()` |
| **Merge** | `fleet-merge.ts` | Sequential merge: update branch → CI → squash |

### Detailed Flow

```mermaid
flowchart TD
subgraph analyze ["Phase 1: Analyze"]
A1["Fetch open GitHub issues"] --> A2["Format as structured markdown"]
end

subgraph plan ["Phase 2: Plan"]
A2 --> B1["Create Jules planning session"]
B1 --> B2["Investigate: trace issues to source code"]
B2 --> B3["Architect: design solutions with diffs"]
B3 --> B4["Build File Ownership Matrix"]
B4 --> B5{"Any file in 2+ tasks?"}
B5 -- Yes --> B6["Merge overlapping tasks"]
B6 --> B4
B5 -- No --> B7["Write task plan to .fleet/"]
end

subgraph validate ["Phase 3: Validate"]
B7 --> C1["Read issue_tasks.json"]
C1 --> C2{"Ownership conflict?"}
C2 -- Yes --> C3["❌ Abort before dispatch"]
C2 -- No --> C4["✅ Safe to parallelize"]
end

subgraph dispatch ["Phase 4: Dispatch"]
C4 --> D1["jules.all — spawn parallel sessions"]
D1 --> D2["Each session targets same base branch"]
D2 --> D3["Sessions produce PRs"]
end

subgraph merge ["Phase 5: Merge"]
D3 --> E1["Process PRs sequentially by risk"]
E1 --> E2["Update branch from base"]
E2 --> E3{"Merge conflict?"}
E3 -- No --> E4["Wait for CI"]
E4 --> E5{"CI passed?"}
E5 -- Yes --> E6["Squash merge"]
E5 -- No --> E7["❌ Abort"]
E6 --> E8{"More PRs?"}
E8 -- Yes --> E1
E8 -- No --> E9["✅ All merged"]
E3 -- Yes --> E10{"Retries left?"}
E10 -- No --> E11["❌ Escalate to human"]
E10 -- Yes --> E12["Close old PR"]
E12 --> E13["Re-dispatch: new Jules session\nagainst current base"]
E13 --> E14["Wait for new PR"]
E14 --> E2
end

style analyze fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style plan fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style validate fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style dispatch fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
style merge fill:#1a2332,stroke:#2a4a6b,color:#e0e0e0
```

## Manual Usage

After setup, run the pipeline locally:
Expand Down
192 changes: 129 additions & 63 deletions skills/automate-github-issues/assets/fleet-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,46 @@
# .github/workflows/fleet-merge.yml
#
# Sequentially merges Jules-authored PRs: update branch → wait for CI → squash merge.
# This workflow is self-contained — it uses only the gh CLI (pre-installed on runners).
# On merge conflict, re-dispatches the task as a new Jules session against current base.
name: Fleet Sequential Merge

on:
# Trigger when a Jules-created PR is opened
pull_request:
types: [opened]

# Allow manual trigger
# Allow manual trigger (recommended: review PRs first, then trigger)
workflow_dispatch:
inputs:
base_branch:
description: "Base branch for merge"
type: string
default: "main"

concurrency:
group: fleet-merge
cancel-in-progress: false

jobs:
sequential-merge:
# Only run for Jules-authored PRs or manual triggers
if: >
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.user.login == 'google-labs-jules' ||
endsWith(github.event.pull_request.user.login, '[bot]')
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Find and merge Jules PRs sequentially
- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install fleet dependencies
run: cd scripts/fleet && bun install

- name: Find and merge fleet PRs sequentially
run: |
set -euo pipefail

BASE_BRANCH="${{ github.event.pull_request.base.ref || 'main' }}"
MAX_CI_WAIT=600 # 10 minutes per PR
BASE_BRANCH="${{ inputs.base_branch || 'main' }}"
MAX_CI_WAIT=600 # 10 minutes per PR
MAX_RETRIES=2 # Max re-dispatch attempts per PR
PR_POLL_TIMEOUT=900 # 15 minutes to wait for re-dispatched PR

echo "🔍 Finding open Jules-authored PRs targeting ${BASE_BRANCH}..."

Expand All @@ -71,58 +76,119 @@ jobs:
for PR_NUM in $PRS; do
echo ""
echo "📦 Processing PR #${PR_NUM}..."

# Update branch from base
echo " 🔄 Updating branch from ${BASE_BRANCH}..."
gh pr update-branch "$PR_NUM" --rebase 2>/dev/null || true

# Wait for branch update to propagate
sleep 5

# Wait for CI checks to pass
echo " 🧪 Waiting for CI checks..."
ELAPSED=0
CI_PASSED=false

while [ $ELAPSED -lt $MAX_CI_WAIT ]; do
STATUS=$(gh pr checks "$PR_NUM" --json name,state --jq '[.[].state] | if length == 0 then "none" elif all(. == "SUCCESS" or . == "SKIPPED") then "pass" elif any(. == "FAILURE") then "fail" else "pending" end')

case "$STATUS" in
"pass")
CI_PASSED=true
break
;;
"none")
echo " ℹ️ No CI checks configured. Proceeding."
CI_PASSED=true
break
;;
"fail")
echo " ❌ CI failed for PR #${PR_NUM}. Skipping."
break
;;
*)
echo " ⏳ CI pending... (${ELAPSED}s/${MAX_CI_WAIT}s)"
RETRY_COUNT=0

while true; do
# Update branch from base
echo " 🔄 Updating branch from ${BASE_BRANCH}..."
UPDATE_OUTPUT=$(gh pr update-branch "$PR_NUM" --rebase 2>&1) || true

# Check for merge conflict
if echo "$UPDATE_OUTPUT" | grep -qi "conflict\|cannot be rebased"; then
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo " ❌ Conflict persists after ${MAX_RETRIES} retries. Human intervention required."
echo " PR: $(gh pr view "$PR_NUM" --json url --jq '.url')"
exit 1
fi

echo " ⚠️ Merge conflict detected. Re-dispatching task..."

# Get the PR's task prompt from the fleet data
TASK_PROMPT=$(gh pr view "$PR_NUM" --json body --jq '.body')

# Close the conflicting PR
echo " 🔒 Closing conflicting PR #${PR_NUM}..."
gh pr close "$PR_NUM" --comment "⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched as a new session."

# Re-dispatch via Jules SDK
echo " 🚀 Re-dispatching against current ${BASE_BRANCH}..."
REPO_FULL="${{ github.repository }}"
NEW_SESSION_ID=$(bun -e "
import { jules } from '@google/jules-sdk';
const session = await jules.createSession({
prompt: process.env.TASK_PROMPT,
source: { github: '${REPO_FULL}', baseBranch: '${BASE_BRANCH}' },
});
console.log(session.id);
" 2>/dev/null)
echo " 📝 New session: ${NEW_SESSION_ID}"

# Poll for new PR
echo " ⏳ Waiting for new PR from session ${NEW_SESSION_ID}..."
POLL_ELAPSED=0
NEW_PR_NUM=""
while [ $POLL_ELAPSED -lt $PR_POLL_TIMEOUT ]; do
sleep 30
ELAPSED=$((ELAPSED + 30))
;;
esac
POLL_ELAPSED=$((POLL_ELAPSED + 30))
NEW_PR_NUM=$(gh pr list --state open --base "$BASE_BRANCH" --json number,headRefName,body \
--jq "[.[] | select(.headRefName | contains(\"${NEW_SESSION_ID}\")) // select(.body | contains(\"${NEW_SESSION_ID}\"))] | .[0].number // empty")
if [ -n "$NEW_PR_NUM" ]; then
echo " ✅ New PR #${NEW_PR_NUM} found."
break
fi
echo " ⏳ No PR yet... (${POLL_ELAPSED}s/${PR_POLL_TIMEOUT}s)"
done

if [ -z "$NEW_PR_NUM" ]; then
echo " ❌ Timed out waiting for re-dispatched PR. Human intervention required."
exit 1
fi

PR_NUM="$NEW_PR_NUM"
RETRY_COUNT=$((RETRY_COUNT + 1))
continue
fi

# Wait for branch update to propagate
sleep 5

# Wait for CI checks to pass
echo " 🧪 Waiting for CI checks..."
ELAPSED=0
CI_PASSED=false

while [ $ELAPSED -lt $MAX_CI_WAIT ]; do
STATUS=$(gh pr checks "$PR_NUM" --json name,state --jq '[.[].state] | if length == 0 then "none" elif all(. == "SUCCESS" or . == "SKIPPED") then "pass" elif any(. == "FAILURE") then "fail" else "pending" end')

case "$STATUS" in
"pass")
CI_PASSED=true
break
;;
"none")
echo " ℹ️ No CI checks configured. Proceeding."
CI_PASSED=true
break
;;
"fail")
echo " ❌ CI failed for PR #${PR_NUM}. Skipping."
break
;;
*)
echo " ⏳ CI pending... (${ELAPSED}s/${MAX_CI_WAIT}s)"
sleep 30
ELAPSED=$((ELAPSED + 30))
;;
esac
done

if [ "$CI_PASSED" = false ]; then
echo " ⏭️ Skipping PR #${PR_NUM} (CI did not pass)"
else
# Squash merge
echo " ✅ CI passed. Merging PR #${PR_NUM}..."
if gh pr merge "$PR_NUM" --squash; then
echo " 🎉 PR #${PR_NUM} merged successfully."
else
echo " ❌ Failed to merge PR #${PR_NUM}. Stopping sequential merge."
exit 1
fi
fi

# Exit the retry loop for this PR
break
done

if [ "$CI_PASSED" = false ]; then
echo " ⏭️ Skipping PR #${PR_NUM} (CI did not pass)"
continue
fi

# Squash merge
echo " ✅ CI passed. Merging PR #${PR_NUM}..."
if gh pr merge "$PR_NUM" --squash --auto; then
echo " 🎉 PR #${PR_NUM} merged successfully."
else
echo " ❌ Failed to merge PR #${PR_NUM}. Stopping sequential merge."
exit 1
fi

# Brief pause for merge to propagate before next PR
sleep 5
done
Expand Down
Loading