autopilot-code 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.autopilot/autopilot.json +5 -1
- package/README.md +16 -3
- package/package.json +1 -1
- package/scripts/run_autopilot.py +304 -3
- package/scripts/run_opencode_issue.sh +54 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
"enabled": true,
|
|
3
3
|
"repo": "bakkensoftware/autopilot",
|
|
4
4
|
"agent": "opencode",
|
|
5
|
+
"autoMerge": true,
|
|
6
|
+
"mergeMethod": "squash",
|
|
5
7
|
"allowedMergeUsers": [
|
|
6
8
|
"kellyelton"
|
|
7
9
|
],
|
|
@@ -26,5 +28,7 @@
|
|
|
26
28
|
"branchPrefix": "autopilot/",
|
|
27
29
|
"allowedBaseBranch": "main",
|
|
28
30
|
"autoResolveConflicts": true,
|
|
29
|
-
"conflictResolutionMaxAttempts": 3
|
|
31
|
+
"conflictResolutionMaxAttempts": 3,
|
|
32
|
+
"autoFixChecks": true,
|
|
33
|
+
"autoFixChecksMaxAttempts": 3
|
|
30
34
|
}
|
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ Example:
|
|
|
22
22
|
"enabled": true,
|
|
23
23
|
"repo": "bakkensoftware/autopilot",
|
|
24
24
|
"agent": "opencode",
|
|
25
|
-
"autoMerge":
|
|
25
|
+
"autoMerge": true,
|
|
26
|
+
"mergeMethod": "squash",
|
|
26
27
|
"allowedMergeUsers": ["github-username"],
|
|
27
28
|
"issueLabels": {
|
|
28
29
|
"queue": ["autopilot:todo"],
|
|
@@ -31,19 +32,31 @@ Example:
|
|
|
31
32
|
"done": "autopilot:done"
|
|
32
33
|
},
|
|
33
34
|
"priorityLabels": ["p0", "p1", "p2"],
|
|
35
|
+
"minPriority": null,
|
|
36
|
+
"ignoreIssueLabels": ["autopilot:backlog"],
|
|
34
37
|
"maxParallel": 1,
|
|
35
38
|
"heartbeatMaxAgeSecs": 3600,
|
|
36
39
|
"branchPrefix": "autopilot/",
|
|
37
|
-
"allowedBaseBranch": "main"
|
|
40
|
+
"allowedBaseBranch": "main",
|
|
41
|
+
"autoResolveConflicts": true,
|
|
42
|
+
"conflictResolutionMaxAttempts": 3,
|
|
43
|
+
"autoFixChecks": true,
|
|
44
|
+
"autoFixChecksMaxAttempts": 3
|
|
38
45
|
}
|
|
39
46
|
```
|
|
40
47
|
|
|
41
48
|
Notes:
|
|
42
49
|
- `repo` must be the GitHub `owner/name`.
|
|
43
50
|
- `agent` (optional, default `"none"`): set to `"opencode"` to enable OpenCode integration after claiming issues.
|
|
44
|
-
- `autoMerge` (optional, default `
|
|
51
|
+
- `autoMerge` (optional, default `true`): if `true`, autopilot will automatically merge PRs after checks pass.
|
|
52
|
+
- `mergeMethod` (optional, default `"squash"`): merge strategy to use. Options: `"squash"`, `"merge"`, or `"rebase"`.
|
|
45
53
|
- `allowedMergeUsers` (required when `autoMerge=true`): list of GitHub usernames allowed to auto-merge. The runner verifies the authenticated GitHub user is in this list before merging.
|
|
54
|
+
- `minPriority` (optional, default `null`): minimum priority to work on. For example, set to `"p1"` to only work on `p0` and `p1` issues. Uses `priorityLabels` array for priority order.
|
|
46
55
|
- `ignoreIssueLabels` (optional, default `["autopilot:backlog"]`): issues with any of these labels will be ignored by the runner.
|
|
56
|
+
- `autoResolveConflicts` (optional, default `true`): if `true`, autopilot will attempt to automatically resolve merge conflicts.
|
|
57
|
+
- `conflictResolutionMaxAttempts` (optional, default `3`): maximum number of attempts to resolve merge conflicts.
|
|
58
|
+
- `autoFixChecks` (optional, default `true`): if `true`, autopilot will attempt to automatically fix failing CI checks.
|
|
59
|
+
- `autoFixChecksMaxAttempts` (optional, default `3`): maximum number of attempts to fix failing checks.
|
|
47
60
|
- `heartbeatMaxAgeSecs` controls how long an in-progress issue can go without a heartbeat before it's considered stale.
|
|
48
61
|
|
|
49
62
|
## Workflow (labels)
|
package/package.json
CHANGED
package/scripts/run_autopilot.py
CHANGED
|
@@ -317,6 +317,286 @@ def list_in_progress_issues(cfg: RepoConfig, limit: int = 20) -> list[dict[str,
|
|
|
317
317
|
return json.loads(raw)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def check_pr_for_issue(cfg: RepoConfig, issue_number: int) -> dict[str, Any] | None:
|
|
321
|
+
"""Check if there's a PR for this issue and return its status."""
|
|
322
|
+
branch = f"autopilot/issue-{issue_number}"
|
|
323
|
+
cmd = [
|
|
324
|
+
"gh",
|
|
325
|
+
"pr",
|
|
326
|
+
"list",
|
|
327
|
+
"--repo",
|
|
328
|
+
cfg.repo,
|
|
329
|
+
"--head",
|
|
330
|
+
branch,
|
|
331
|
+
"--json",
|
|
332
|
+
"number,title,state,mergeable,mergeStateStatus,url,merged,closed,headRefName",
|
|
333
|
+
"--limit",
|
|
334
|
+
"1",
|
|
335
|
+
]
|
|
336
|
+
try:
|
|
337
|
+
raw = sh(cmd, check=False)
|
|
338
|
+
prs = json.loads(raw)
|
|
339
|
+
return prs[0] if prs else None
|
|
340
|
+
except Exception:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def resolve_merge_conflicts(cfg: RepoConfig, issue_number: int, pr: dict[str, Any], attempt: int) -> bool:
|
|
345
|
+
"""Attempt to resolve merge conflicts using the coding agent. Returns True if resolved."""
|
|
346
|
+
if cfg.agent != "opencode":
|
|
347
|
+
print(f"[{cfg.repo}] Cannot resolve conflicts: agent '{cfg.agent}' not supported (only 'opencode')", flush=True)
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
pr_number = pr["number"]
|
|
351
|
+
branch = pr.get("headRefName", f"autopilot/issue-{issue_number}")
|
|
352
|
+
worktree = Path(f"/tmp/autopilot-issue-{issue_number}")
|
|
353
|
+
|
|
354
|
+
print(f"[{cfg.repo}] Attempting to resolve merge conflicts for PR #{pr_number} (attempt {attempt}/{cfg.conflict_resolution_max_attempts})", flush=True)
|
|
355
|
+
|
|
356
|
+
# Ensure worktree exists and is up to date
|
|
357
|
+
if not worktree.exists():
|
|
358
|
+
print(f"[{cfg.repo}] Creating worktree at {worktree}", flush=True)
|
|
359
|
+
try:
|
|
360
|
+
sh(["git", "worktree", "add", str(worktree), branch], cwd=cfg.root)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"[{cfg.repo}] Failed to create worktree: {e}", flush=True)
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
# Fetch latest and attempt rebase/merge
|
|
366
|
+
try:
|
|
367
|
+
sh(["git", "fetch", "origin", "main"], cwd=worktree)
|
|
368
|
+
|
|
369
|
+
# Try to rebase onto main
|
|
370
|
+
rebase_result = subprocess.run(
|
|
371
|
+
["git", "rebase", "origin/main"],
|
|
372
|
+
cwd=str(worktree),
|
|
373
|
+
capture_output=True,
|
|
374
|
+
text=True
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if rebase_result.returncode == 0:
|
|
378
|
+
# No conflicts, push and we're done
|
|
379
|
+
sh(["git", "push", "-f", "origin", branch], cwd=worktree)
|
|
380
|
+
print(f"[{cfg.repo}] Rebase successful, no conflicts to resolve", flush=True)
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
# There are conflicts - abort the rebase first
|
|
384
|
+
subprocess.run(["git", "rebase", "--abort"], cwd=str(worktree), capture_output=True)
|
|
385
|
+
|
|
386
|
+
# Get list of files that would conflict
|
|
387
|
+
merge_base = sh(["git", "merge-base", "HEAD", "origin/main"], cwd=worktree).strip()
|
|
388
|
+
diff_output = sh(["git", "diff", "--name-only", merge_base, "origin/main"], cwd=worktree, check=False)
|
|
389
|
+
conflicting_files = diff_output.strip().split("\n") if diff_output.strip() else []
|
|
390
|
+
|
|
391
|
+
# Build prompt for the agent
|
|
392
|
+
conflict_prompt = f"""This PR has merge conflicts with main. Please resolve them.
|
|
393
|
+
|
|
394
|
+
Branch: {branch}
|
|
395
|
+
Files potentially conflicting: {', '.join(conflicting_files[:10])}
|
|
396
|
+
|
|
397
|
+
Steps:
|
|
398
|
+
1. Run `git fetch origin main`
|
|
399
|
+
2. Run `git rebase origin/main` to start the rebase
|
|
400
|
+
3. When conflicts occur, examine the conflicted files
|
|
401
|
+
4. Edit the files to resolve conflicts (remove conflict markers, keep correct code)
|
|
402
|
+
5. Run `git add <resolved-files>` for each resolved file
|
|
403
|
+
6. Run `git rebase --continue`
|
|
404
|
+
7. Repeat until rebase is complete
|
|
405
|
+
8. Run `git push -f origin {branch}` to update the PR
|
|
406
|
+
|
|
407
|
+
Work rules:
|
|
408
|
+
- Focus only on resolving conflicts, don't make unrelated changes
|
|
409
|
+
- Preserve the intent of both the PR changes and main branch changes
|
|
410
|
+
- If unsure about a conflict, prefer the PR's changes but ensure compatibility
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
print(f"[{cfg.repo}] Running opencode to resolve conflicts...", flush=True)
|
|
414
|
+
|
|
415
|
+
# Run opencode in the worktree
|
|
416
|
+
agent_result = subprocess.run(
|
|
417
|
+
["opencode", "run", conflict_prompt],
|
|
418
|
+
cwd=str(worktree),
|
|
419
|
+
capture_output=True,
|
|
420
|
+
text=True,
|
|
421
|
+
timeout=600 # 10 minute timeout
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if agent_result.returncode != 0:
|
|
425
|
+
print(f"[{cfg.repo}] Opencode failed: {agent_result.stderr[:500]}", flush=True)
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
# Check if conflicts are resolved by checking PR status
|
|
429
|
+
time.sleep(5) # Give GitHub time to update
|
|
430
|
+
cmd = ["gh", "pr", "view", str(pr_number), "--repo", cfg.repo, "--json", "mergeable,mergeStateStatus"]
|
|
431
|
+
pr_status = json.loads(sh(cmd))
|
|
432
|
+
|
|
433
|
+
if pr_status.get("mergeable") == "MERGEABLE":
|
|
434
|
+
print(f"[{cfg.repo}] Conflicts resolved successfully!", flush=True)
|
|
435
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
436
|
+
"--body", f"✅ Autopilot resolved merge conflicts (attempt {attempt})."])
|
|
437
|
+
return True
|
|
438
|
+
else:
|
|
439
|
+
print(f"[{cfg.repo}] Conflicts still present after resolution attempt", flush=True)
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
except subprocess.TimeoutExpired:
|
|
443
|
+
print(f"[{cfg.repo}] Conflict resolution timed out", flush=True)
|
|
444
|
+
return False
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print(f"[{cfg.repo}] Error resolving conflicts: {e}", flush=True)
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool:
|
|
451
|
+
"""Attempt to merge a PR if it's ready. Returns True if merged or closed."""
|
|
452
|
+
pr_number = pr["number"]
|
|
453
|
+
pr_url = pr["url"]
|
|
454
|
+
|
|
455
|
+
# If already merged or closed, mark issue as done
|
|
456
|
+
if pr.get("merged") or pr.get("closed"):
|
|
457
|
+
print(f"[{cfg.repo}] PR #{pr_number} is already merged/closed, marking issue #{issue_number} as done", flush=True)
|
|
458
|
+
sh(["gh", "issue", "edit", str(issue_number), "--repo", cfg.repo,
|
|
459
|
+
"--add-label", cfg.label_done, "--remove-label", cfg.label_in_progress])
|
|
460
|
+
sh(["gh", "issue", "close", str(issue_number), "--repo", cfg.repo, "--reason", "completed"])
|
|
461
|
+
return True
|
|
462
|
+
|
|
463
|
+
# Check if PR is mergeable
|
|
464
|
+
mergeable = pr.get("mergeable")
|
|
465
|
+
merge_state = pr.get("mergeStateStatus")
|
|
466
|
+
|
|
467
|
+
# Handle merge conflicts
|
|
468
|
+
if mergeable == "CONFLICTING" or merge_state == "DIRTY":
|
|
469
|
+
print(f"[{cfg.repo}] PR #{pr_number} has merge conflicts", flush=True)
|
|
470
|
+
|
|
471
|
+
if not cfg.auto_resolve_conflicts:
|
|
472
|
+
print(f"[{cfg.repo}] Auto-resolve conflicts disabled, skipping", flush=True)
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
# Load state to track resolution attempts
|
|
476
|
+
state = load_state(cfg.root)
|
|
477
|
+
conflict_key = f"conflict_attempts_{issue_number}"
|
|
478
|
+
attempts = state.get(conflict_key, 0)
|
|
479
|
+
|
|
480
|
+
if attempts >= cfg.conflict_resolution_max_attempts:
|
|
481
|
+
print(f"[{cfg.repo}] Max conflict resolution attempts ({cfg.conflict_resolution_max_attempts}) reached", flush=True)
|
|
482
|
+
if attempts == cfg.conflict_resolution_max_attempts:
|
|
483
|
+
# Only notify once when we hit the limit
|
|
484
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
485
|
+
"--body", f"❌ Autopilot failed to resolve merge conflicts after {attempts} attempts. Manual resolution required."])
|
|
486
|
+
state[conflict_key] = attempts + 1 # Increment to avoid repeat notifications
|
|
487
|
+
write_state(cfg.root, state)
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
# Attempt to resolve
|
|
491
|
+
attempts += 1
|
|
492
|
+
state[conflict_key] = attempts
|
|
493
|
+
write_state(cfg.root, state)
|
|
494
|
+
|
|
495
|
+
if resolve_merge_conflicts(cfg, issue_number, pr, attempts):
|
|
496
|
+
# Reset attempts on success
|
|
497
|
+
del state[conflict_key]
|
|
498
|
+
write_state(cfg.root, state)
|
|
499
|
+
# Re-check the PR - it might be ready to merge now
|
|
500
|
+
return False # Return False to re-check on next cycle
|
|
501
|
+
else:
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
if mergeable != "MERGEABLE":
|
|
505
|
+
print(f"[{cfg.repo}] PR #{pr_number} is not mergeable (state: {mergeable}/{merge_state}), skipping", flush=True)
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
# Get PR check status
|
|
509
|
+
cmd = [
|
|
510
|
+
"gh", "pr", "view", str(pr_number), "--repo", cfg.repo,
|
|
511
|
+
"--json", "statusCheckRollup",
|
|
512
|
+
]
|
|
513
|
+
try:
|
|
514
|
+
pr_details = json.loads(sh(cmd))
|
|
515
|
+
checks = pr_details.get("statusCheckRollup", [])
|
|
516
|
+
|
|
517
|
+
if not checks:
|
|
518
|
+
# No checks configured, can merge
|
|
519
|
+
check_status = "PASSED"
|
|
520
|
+
else:
|
|
521
|
+
# Check if any checks are pending or failed
|
|
522
|
+
has_failure = any(c.get("conclusion") == "FAILURE" for c in checks)
|
|
523
|
+
has_pending = any(c.get("conclusion") in ("PENDING", "QUEUED", None) for c in checks)
|
|
524
|
+
|
|
525
|
+
if has_failure:
|
|
526
|
+
check_status = "FAILED"
|
|
527
|
+
elif has_pending:
|
|
528
|
+
check_status = "PENDING"
|
|
529
|
+
else:
|
|
530
|
+
check_status = "PASSED"
|
|
531
|
+
|
|
532
|
+
if check_status != "PASSED":
|
|
533
|
+
print(f"[{cfg.repo}] PR #{pr_number} checks not ready (status: {check_status})", flush=True)
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
# Checks passed, attempt merge
|
|
537
|
+
print(f"[{cfg.repo}] PR #{pr_number} is ready, attempting merge...", flush=True)
|
|
538
|
+
|
|
539
|
+
merge_method = "squash" # Default, could be read from config if needed
|
|
540
|
+
merge_cmd = ["gh", "pr", "merge", pr_url, "--delete-branch", f"--{merge_method}"]
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
sh(merge_cmd)
|
|
544
|
+
print(f"[{cfg.repo}] Successfully merged PR #{pr_number}!", flush=True)
|
|
545
|
+
|
|
546
|
+
# Comment and close issue
|
|
547
|
+
success_msg = f"✅ Autopilot successfully merged PR #{pr_number} using {merge_method} method.\n\nThe fix has been merged to the main branch."
|
|
548
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo, "--body", success_msg])
|
|
549
|
+
sh(["gh", "issue", "edit", str(issue_number), "--repo", cfg.repo,
|
|
550
|
+
"--add-label", cfg.label_done, "--remove-label", cfg.label_in_progress])
|
|
551
|
+
sh(["gh", "issue", "close", str(issue_number), "--repo", cfg.repo, "--reason", "completed"])
|
|
552
|
+
|
|
553
|
+
return True
|
|
554
|
+
except Exception as e:
|
|
555
|
+
print(f"[{cfg.repo}] Failed to merge PR #{pr_number}: {e}", flush=True)
|
|
556
|
+
return False
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
print(f"[{cfg.repo}] Error checking PR #{pr_number}: {e}", flush=True)
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def clean_completed_issues_from_state(cfg: RepoConfig) -> None:
|
|
564
|
+
"""Remove completed issues from state.json."""
|
|
565
|
+
state = load_state(cfg.root)
|
|
566
|
+
active_issues = state.get("activeIssues", {})
|
|
567
|
+
|
|
568
|
+
if not isinstance(active_issues, dict):
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
# Check each issue to see if it's still open
|
|
572
|
+
issues_to_remove = []
|
|
573
|
+
for issue_num_str, issue_data in active_issues.items():
|
|
574
|
+
if not isinstance(issue_data, dict):
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
issue_num = int(issue_data.get("number", -1))
|
|
578
|
+
if issue_num <= 0:
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
# Check if issue is closed
|
|
582
|
+
try:
|
|
583
|
+
cmd = ["gh", "issue", "view", str(issue_num), "--repo", cfg.repo, "--json", "state,closed"]
|
|
584
|
+
result = json.loads(sh(cmd, check=False))
|
|
585
|
+
if result.get("state") == "CLOSED" or result.get("closed"):
|
|
586
|
+
print(f"[{cfg.repo}] Issue #{issue_num} is closed, removing from state", flush=True)
|
|
587
|
+
issues_to_remove.append(issue_num_str)
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
# Remove completed issues
|
|
592
|
+
for issue_num_str in issues_to_remove:
|
|
593
|
+
del active_issues[issue_num_str]
|
|
594
|
+
|
|
595
|
+
if issues_to_remove:
|
|
596
|
+
state["activeIssues"] = active_issues
|
|
597
|
+
write_state(cfg.root, state)
|
|
598
|
+
|
|
599
|
+
|
|
320
600
|
def maybe_mark_blocked(cfg: RepoConfig, issue: dict[str, Any]) -> None:
|
|
321
601
|
num = int(issue["number"])
|
|
322
602
|
if is_heartbeat_fresh(cfg, num):
|
|
@@ -358,12 +638,33 @@ def run_cycle(
|
|
|
358
638
|
|
|
359
639
|
for cfg in all_configs:
|
|
360
640
|
print(f"[{cfg.repo}] Scanning for issues...", flush=True)
|
|
361
|
-
|
|
641
|
+
|
|
642
|
+
# 0) Clean up completed issues from state
|
|
643
|
+
if not dry_run:
|
|
644
|
+
clean_completed_issues_from_state(cfg)
|
|
645
|
+
|
|
646
|
+
# 1) Check in-progress issues
|
|
362
647
|
inprog = list_in_progress_issues(cfg)
|
|
363
648
|
for it in inprog:
|
|
364
|
-
|
|
649
|
+
issue_num = int(it["number"])
|
|
650
|
+
|
|
651
|
+
# First check if we should mark as blocked due to stale heartbeat
|
|
365
652
|
if not dry_run:
|
|
366
|
-
|
|
653
|
+
# Only mark blocked if heartbeat is stale
|
|
654
|
+
if not is_heartbeat_fresh(cfg, issue_num):
|
|
655
|
+
maybe_mark_blocked(cfg, it)
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
# If heartbeat is fresh and autoMerge is enabled, try to merge PR
|
|
659
|
+
if cfg.auto_merge:
|
|
660
|
+
pr = check_pr_for_issue(cfg, issue_num)
|
|
661
|
+
if pr:
|
|
662
|
+
print(f"[{cfg.repo}] Found PR for issue #{issue_num}, checking if ready to merge...", flush=True)
|
|
663
|
+
if try_merge_pr(cfg, issue_num, pr):
|
|
664
|
+
print(f"[{cfg.repo}] Issue #{issue_num} completed!", flush=True)
|
|
665
|
+
else:
|
|
666
|
+
# Touch heartbeat to keep it alive while waiting for checks
|
|
667
|
+
touch_heartbeat(cfg, issue_num)
|
|
367
668
|
|
|
368
669
|
# 2) Count active issues with fresh heartbeats from local state
|
|
369
670
|
state = load_state(cfg.root)
|
|
@@ -15,6 +15,7 @@ cd "$REPO_DIR"
|
|
|
15
15
|
if command -v jq >/dev/null 2>&1; then
|
|
16
16
|
REPO=$(jq -r '.repo' < .autopilot/autopilot.json)
|
|
17
17
|
AUTO_MERGE=$(jq -r '.autoMerge // true' < .autopilot/autopilot.json)
|
|
18
|
+
MERGE_METHOD=$(jq -r '.mergeMethod // "squash"' < .autopilot/autopilot.json)
|
|
18
19
|
AUTO_RESOLVE_CONFLICTS=$(jq -r '.autoResolveConflicts // true' < .autopilot/autopilot.json)
|
|
19
20
|
CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(jq -r '.conflictResolutionMaxAttempts // 3' < .autopilot/autopilot.json)
|
|
20
21
|
AUTO_FIX_CHECKS=$(jq -r '.autoFixChecks // true' < .autopilot/autopilot.json)
|
|
@@ -23,6 +24,7 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
23
24
|
else
|
|
24
25
|
REPO=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["repo"])')
|
|
25
26
|
AUTO_MERGE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoMerge", True))')
|
|
27
|
+
MERGE_METHOD=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("mergeMethod", "squash"))')
|
|
26
28
|
AUTO_RESOLVE_CONFLICTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoResolveConflicts", True))')
|
|
27
29
|
CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("conflictResolutionMaxAttempts", 3))')
|
|
28
30
|
AUTO_FIX_CHECKS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecks", True))')
|
|
@@ -360,6 +362,58 @@ if [[ "$AUTO_MERGE" == "true" ]] && [[ -n "$PR_URL" ]]; then
|
|
|
360
362
|
sleep "$POLL_INTERVAL"
|
|
361
363
|
fi
|
|
362
364
|
done
|
|
365
|
+
|
|
366
|
+
# After polling loop: merge if checks passed
|
|
367
|
+
if [[ "$CHECK_STATUS" == "PASSED" ]]; then
|
|
368
|
+
echo "[run_opencode_issue.sh] Merging PR using method: $MERGE_METHOD"
|
|
369
|
+
|
|
370
|
+
# Build merge command based on method
|
|
371
|
+
MERGE_CMD="gh pr merge $PR_URL --delete-branch"
|
|
372
|
+
case "$MERGE_METHOD" in
|
|
373
|
+
squash)
|
|
374
|
+
MERGE_CMD="$MERGE_CMD --squash"
|
|
375
|
+
;;
|
|
376
|
+
merge)
|
|
377
|
+
MERGE_CMD="$MERGE_CMD --merge"
|
|
378
|
+
;;
|
|
379
|
+
rebase)
|
|
380
|
+
MERGE_CMD="$MERGE_CMD --rebase"
|
|
381
|
+
;;
|
|
382
|
+
*)
|
|
383
|
+
echo "[run_opencode_issue.sh] Unknown merge method '$MERGE_METHOD', defaulting to squash"
|
|
384
|
+
MERGE_CMD="$MERGE_CMD --squash"
|
|
385
|
+
;;
|
|
386
|
+
esac
|
|
387
|
+
|
|
388
|
+
# Attempt merge
|
|
389
|
+
if eval "$MERGE_CMD"; then
|
|
390
|
+
echo "[run_opencode_issue.sh] PR merged successfully!"
|
|
391
|
+
|
|
392
|
+
# Update issue with success message and mark as done
|
|
393
|
+
SUCCESS_MSG="✅ Autopilot successfully merged PR #${PR_URL##*/} using $MERGE_METHOD method.\n\nThe fix has been merged to the main branch."
|
|
394
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$SUCCESS_MSG" || true
|
|
395
|
+
|
|
396
|
+
# Get done label and mark issue as done
|
|
397
|
+
if command -v jq >/dev/null 2>&1; then
|
|
398
|
+
LABEL_DONE=$(jq -r '.issueLabels.done' < .autopilot/autopilot.json)
|
|
399
|
+
LABEL_IN_PROGRESS=$(jq -r '.issueLabels.inProgress' < .autopilot/autopilot.json)
|
|
400
|
+
else
|
|
401
|
+
LABEL_DONE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["done"])')
|
|
402
|
+
LABEL_IN_PROGRESS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["inProgress"])')
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_DONE" --remove-label "$LABEL_IN_PROGRESS" || true
|
|
406
|
+
gh issue close "$ISSUE_NUMBER" --repo "$REPO" --reason completed || true
|
|
407
|
+
else
|
|
408
|
+
echo "[run_opencode_issue.sh] Failed to merge PR"
|
|
409
|
+
MERGE_FAIL_MSG="❌ Autopilot failed to merge PR automatically.\n\nThe PR is ready (checks passed) but the merge operation failed. You may need to merge manually."
|
|
410
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$MERGE_FAIL_MSG" || true
|
|
411
|
+
fi
|
|
412
|
+
elif [[ $POLL_ATTEMPT -ge $MAX_POLL_ATTEMPTS ]]; then
|
|
413
|
+
echo "[run_opencode_issue.sh] Timed out waiting for checks to pass"
|
|
414
|
+
TIMEOUT_MSG="⏱️ Autopilot timed out waiting for PR checks to complete.\n\nThe PR will remain open for manual review."
|
|
415
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$TIMEOUT_MSG" || true
|
|
416
|
+
fi
|
|
363
417
|
else
|
|
364
418
|
BLOCKED_MSG="❌ Autopilot cannot auto-merge PR: authenticated user '$AUTH_USER' is not in the allowedMergeUsers list."
|
|
365
419
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BLOCKED_MSG" || true
|