cloudflare/vinext

Public

mirrored fromhttps://github.com/cloudflare/vinextAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
23f9e1e13decb3ee024adc6a091b9252427f8e2e

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/autopilot.sh

259lines · modecode

1#!/usr/bin/env bash
2set -euo pipefail
3
4# ─────────────────────────────────────────────────────────────
5# autopilot.sh — Full issue-to-merge workflow via OpenCode
6#
7# Usage:
8# ./scripts/autopilot.sh <issue-number> [options]
9#
10# Options:
11# --server <url> Attach to an existing server instead of starting one
12# --port <number> Port for the auto-started server (default: 4096)
13# --skip-fix Skip the fix step (PR already exists)
14# --skip-review Skip the review step
15# --pr <number> Use existing PR number (with --skip-fix)
16# --dry-run Print what would be done without running
17#
18# The script auto-starts an OpenCode server and tears it down
19# on exit. Watch progress in another terminal:
20#
21# opencode attach http://localhost:4096
22#
23# Each step creates its own OpenCode session visible in the TUI.
24# ─────────────────────────────────────────────────────────────
25
26ISSUE=""
27SERVER_URL=""
28SERVER_PORT="4096"
29SKIP_FIX=false
30SKIP_REVIEW=false
31PR_NUMBER=""
32DRY_RUN=false
33STARTED_SERVER=false
34SERVER_PID=""
35
36# ── Parse args ────────────────────────────────────────────────
37while [[ $# -gt 0 ]]; do
38 case "$1" in
39 --server) SERVER_URL="$2"; shift 2 ;;
40 --port) SERVER_PORT="$2"; shift 2 ;;
41 --skip-fix) SKIP_FIX=true; shift ;;
42 --skip-review) SKIP_REVIEW=true; shift ;;
43 --pr) PR_NUMBER="$2"; shift 2 ;;
44 --dry-run) DRY_RUN=true; shift ;;
45 --help|-h)
46 sed -n '3,/^# ──────/p' "$0" | head -n -1 | sed 's/^# \?//'
47 exit 0
48 ;;
49 -*) echo "Unknown flag: $1"; exit 1 ;;
50 *)
51 if [[ -z "$ISSUE" ]]; then
52 ISSUE="$1"; shift
53 else
54 echo "Unexpected argument: $1"; exit 1
55 fi
56 ;;
57 esac
58done
59
60if [[ -z "$ISSUE" ]]; then
61 echo "Usage: autopilot.sh <issue-number> [options]"
62 echo "Run with --help for full usage."
63 exit 1
64fi
65
66REPO_ROOT="$(git rev-parse --show-toplevel)"
67WORKTREE_DIR="${REPO_ROOT}/../vinext-fix-${ISSUE}"
68
69# ── Server lifecycle ──────────────────────────────────────────
70cleanup() {
71 if [[ "$STARTED_SERVER" == "true" && -n "$SERVER_PID" ]]; then
72 echo ""
73 echo " Stopping OpenCode server (pid ${SERVER_PID})..."
74 kill "$SERVER_PID" 2>/dev/null || true
75 wait "$SERVER_PID" 2>/dev/null || true
76 fi
77}
78trap cleanup EXIT
79
80discover_server() {
81 # Find a running opencode process listening on a TCP port
82 local ports
83 ports=$(lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null \
84 | awk '/opencode/ {print $9}' \
85 | grep -o '[0-9]*$' \
86 | sort -u)
87
88 for port in $ports; do
89 local url="http://127.0.0.1:${port}"
90 if curl -sf "${url}/global/health" > /dev/null 2>&1; then
91 echo "${url}"
92 return 0
93 fi
94 done
95 return 1
96}
97
98ensure_server() {
99 # If user gave --server, just verify it's reachable
100 if [[ -n "$SERVER_URL" ]]; then
101 if ! curl -sf "${SERVER_URL}/global/health" > /dev/null 2>&1; then
102 echo "ERROR: No OpenCode server at ${SERVER_URL}"
103 exit 1
104 fi
105 echo " Using server at ${SERVER_URL}"
106 return
107 fi
108
109 # Try to discover a running server (e.g. desktop app)
110 local discovered
111 discovered=$(discover_server || true)
112 if [[ -n "$discovered" ]]; then
113 SERVER_URL="$discovered"
114 echo " Found running OpenCode server at ${SERVER_URL}"
115 return
116 fi
117
118 # Nothing running — start our own
119 SERVER_URL="http://localhost:${SERVER_PORT}"
120
121 echo " Starting OpenCode server on port ${SERVER_PORT}..."
122 opencode serve --port "${SERVER_PORT}" > /dev/null 2>&1 &
123 SERVER_PID=$!
124 STARTED_SERVER=true
125
126 # Wait for it to be ready (up to 30s)
127 local attempts=0
128 while ! curl -sf "${SERVER_URL}/global/health" > /dev/null 2>&1; do
129 if [[ $attempts -ge 30 ]]; then
130 echo "ERROR: Server failed to start within 30s"
131 exit 1
132 fi
133 sleep 1
134 attempts=$((attempts + 1))
135 done
136
137 echo " Server ready at ${SERVER_URL}"
138 echo " Watch progress: opencode attach ${SERVER_URL}"
139}
140
141if [[ "$DRY_RUN" == "false" ]]; then
142 ensure_server
143fi
144
145# ── Helpers ───────────────────────────────────────────────────
146step() {
147 local step_num="$1"
148 local step_name="$2"
149 echo ""
150 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
151 echo " Step ${step_num}: ${step_name}"
152 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
153 echo ""
154}
155
156run_opencode() {
157 local title="$1"
158 local command="$2"
159 local args="$3"
160
161 if [[ "$DRY_RUN" == "true" ]]; then
162 echo "[dry-run] opencode run --attach ${SERVER_URL} --title \"${title}\" --command ${command} ${args}"
163 return 0
164 fi
165
166 opencode run \
167 --attach "${SERVER_URL}" \
168 --title "${title}" \
169 --command "${command}" "${args}"
170}
171
172find_pr_number() {
173 # Try to find the PR from the worktree's branch
174 if [[ -d "${WORKTREE_DIR}" ]]; then
175 local branch
176 branch=$(git -C "${WORKTREE_DIR}" branch --show-current 2>/dev/null || true)
177 if [[ -n "${branch}" ]]; then
178 local pr
179 pr=$(gh pr list --repo cloudflare/vinext --head "${branch}" --json number -q '.[0].number' 2>/dev/null || true)
180 if [[ -n "${pr}" ]]; then
181 echo "${pr}"
182 return 0
183 fi
184 fi
185 fi
186
187 # Fallback: search by issue reference
188 local pr
189 pr=$(gh pr list --repo cloudflare/vinext --search "Fixes #${ISSUE}" --json number -q '.[0].number' 2>/dev/null || true)
190 if [[ -n "${pr}" ]]; then
191 echo "${pr}"
192 return 0
193 fi
194
195 return 1
196}
197
198# ── Main ──────────────────────────────────────────────────────
199echo "╔═══════════════════════════════════════════════════════╗"
200echo "║ Autopilot: Issue #${ISSUE}"
201echo "║ Server: ${SERVER_URL}"
202echo "║ Worktree: ${WORKTREE_DIR}"
203echo "╚═══════════════════════════════════════════════════════╝"
204
205# ── Step 1: Fix the issue ─────────────────────────────────────
206if [[ "$SKIP_FIX" == "false" ]]; then
207 step 1 "Fix issue #${ISSUE}"
208 run_opencode "Fix #${ISSUE}" "fix-issue" "${ISSUE}"
209else
210 echo ""
211 echo " Skipping fix step."
212fi
213
214# ── Resolve PR number ─────────────────────────────────────────
215if [[ -z "$PR_NUMBER" ]]; then
216 echo ""
217 echo " Resolving PR number..."
218 PR_NUMBER=$(find_pr_number || true)
219
220 if [[ -z "$PR_NUMBER" ]]; then
221 echo "ERROR: Could not find a PR for issue #${ISSUE}."
222 echo "If the PR already exists, re-run with: --pr <number>"
223 exit 1
224 fi
225fi
226
227echo " PR #${PR_NUMBER}"
228
229# ── Step 2: Review the PR ────────────────────────────────────
230if [[ "$SKIP_REVIEW" == "false" ]]; then
231 step 2 "Review PR #${PR_NUMBER}"
232 run_opencode "Review PR #${PR_NUMBER}" "review-pr" "${PR_NUMBER}"
233else
234 echo ""
235 echo " Skipping review step."
236fi
237
238# ── Step 3: Address review comments ──────────────────────────
239step 3 "Address review for PR #${PR_NUMBER}"
240run_opencode "Address review PR #${PR_NUMBER}" "address-review" "${PR_NUMBER}"
241
242# ── Done ──────────────────────────────────────────────────────
243echo ""
244echo "╔═══════════════════════════════════════════════════════╗"
245echo "║ Done!"
246echo "║"
247echo "║ Sessions created:"
248if [[ "$SKIP_FIX" == "false" ]]; then
249echo "║ 1. Fix #${ISSUE}"
250fi
251if [[ "$SKIP_REVIEW" == "false" ]]; then
252echo "║ 2. Review PR #${PR_NUMBER}"
253fi
254echo "║ 3. Address review PR #${PR_NUMBER}"
255echo "║"
256echo "║ View sessions: opencode attach ${SERVER_URL}"
257echo "║ Worktree: ${WORKTREE_DIR}"
258echo "║ Clean up: git worktree remove ${WORKTREE_DIR}"
259echo "╚═══════════════════════════════════════════════════════╝"
260