autopilot-code 0.0.14 → 0.0.16
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 +161 -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,143 @@ 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",
|
|
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 try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool:
|
|
345
|
+
"""Attempt to merge a PR if it's ready. Returns True if merged or closed."""
|
|
346
|
+
pr_number = pr["number"]
|
|
347
|
+
pr_url = pr["url"]
|
|
348
|
+
|
|
349
|
+
# If already merged or closed, mark issue as done
|
|
350
|
+
if pr.get("merged") or pr.get("closed"):
|
|
351
|
+
print(f"[{cfg.repo}] PR #{pr_number} is already merged/closed, marking issue #{issue_number} as done", flush=True)
|
|
352
|
+
sh(["gh", "issue", "edit", str(issue_number), "--repo", cfg.repo,
|
|
353
|
+
"--add-label", cfg.label_done, "--remove-label", cfg.label_in_progress])
|
|
354
|
+
sh(["gh", "issue", "close", str(issue_number), "--repo", cfg.repo, "--reason", "completed"])
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
# Check if PR is mergeable
|
|
358
|
+
mergeable = pr.get("mergeable")
|
|
359
|
+
merge_state = pr.get("mergeStateStatus")
|
|
360
|
+
|
|
361
|
+
if mergeable != "MERGEABLE":
|
|
362
|
+
print(f"[{cfg.repo}] PR #{pr_number} is not mergeable (state: {mergeable}/{merge_state}), skipping", flush=True)
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
# Get PR check status
|
|
366
|
+
cmd = [
|
|
367
|
+
"gh", "pr", "view", str(pr_number), "--repo", cfg.repo,
|
|
368
|
+
"--json", "statusCheckRollup",
|
|
369
|
+
]
|
|
370
|
+
try:
|
|
371
|
+
pr_details = json.loads(sh(cmd))
|
|
372
|
+
checks = pr_details.get("statusCheckRollup", [])
|
|
373
|
+
|
|
374
|
+
if not checks:
|
|
375
|
+
# No checks configured, can merge
|
|
376
|
+
check_status = "PASSED"
|
|
377
|
+
else:
|
|
378
|
+
# Check if any checks are pending or failed
|
|
379
|
+
has_failure = any(c.get("conclusion") == "FAILURE" for c in checks)
|
|
380
|
+
has_pending = any(c.get("conclusion") in ("PENDING", "QUEUED", None) for c in checks)
|
|
381
|
+
|
|
382
|
+
if has_failure:
|
|
383
|
+
check_status = "FAILED"
|
|
384
|
+
elif has_pending:
|
|
385
|
+
check_status = "PENDING"
|
|
386
|
+
else:
|
|
387
|
+
check_status = "PASSED"
|
|
388
|
+
|
|
389
|
+
if check_status != "PASSED":
|
|
390
|
+
print(f"[{cfg.repo}] PR #{pr_number} checks not ready (status: {check_status})", flush=True)
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
# Checks passed, attempt merge
|
|
394
|
+
print(f"[{cfg.repo}] PR #{pr_number} is ready, attempting merge...", flush=True)
|
|
395
|
+
|
|
396
|
+
merge_method = "squash" # Default, could be read from config if needed
|
|
397
|
+
merge_cmd = ["gh", "pr", "merge", pr_url, "--delete-branch", f"--{merge_method}"]
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
sh(merge_cmd)
|
|
401
|
+
print(f"[{cfg.repo}] Successfully merged PR #{pr_number}!", flush=True)
|
|
402
|
+
|
|
403
|
+
# Comment and close issue
|
|
404
|
+
success_msg = f"✅ Autopilot successfully merged PR #{pr_number} using {merge_method} method.\n\nThe fix has been merged to the main branch."
|
|
405
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo, "--body", success_msg])
|
|
406
|
+
sh(["gh", "issue", "edit", str(issue_number), "--repo", cfg.repo,
|
|
407
|
+
"--add-label", cfg.label_done, "--remove-label", cfg.label_in_progress])
|
|
408
|
+
sh(["gh", "issue", "close", str(issue_number), "--repo", cfg.repo, "--reason", "completed"])
|
|
409
|
+
|
|
410
|
+
return True
|
|
411
|
+
except Exception as e:
|
|
412
|
+
print(f"[{cfg.repo}] Failed to merge PR #{pr_number}: {e}", flush=True)
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
except Exception as e:
|
|
416
|
+
print(f"[{cfg.repo}] Error checking PR #{pr_number}: {e}", flush=True)
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def clean_completed_issues_from_state(cfg: RepoConfig) -> None:
|
|
421
|
+
"""Remove completed issues from state.json."""
|
|
422
|
+
state = load_state(cfg.root)
|
|
423
|
+
active_issues = state.get("activeIssues", {})
|
|
424
|
+
|
|
425
|
+
if not isinstance(active_issues, dict):
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
# Check each issue to see if it's still open
|
|
429
|
+
issues_to_remove = []
|
|
430
|
+
for issue_num_str, issue_data in active_issues.items():
|
|
431
|
+
if not isinstance(issue_data, dict):
|
|
432
|
+
continue
|
|
433
|
+
|
|
434
|
+
issue_num = int(issue_data.get("number", -1))
|
|
435
|
+
if issue_num <= 0:
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
# Check if issue is closed
|
|
439
|
+
try:
|
|
440
|
+
cmd = ["gh", "issue", "view", str(issue_num), "--repo", cfg.repo, "--json", "state,closed"]
|
|
441
|
+
result = json.loads(sh(cmd, check=False))
|
|
442
|
+
if result.get("state") == "CLOSED" or result.get("closed"):
|
|
443
|
+
print(f"[{cfg.repo}] Issue #{issue_num} is closed, removing from state", flush=True)
|
|
444
|
+
issues_to_remove.append(issue_num_str)
|
|
445
|
+
except Exception:
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
# Remove completed issues
|
|
449
|
+
for issue_num_str in issues_to_remove:
|
|
450
|
+
del active_issues[issue_num_str]
|
|
451
|
+
|
|
452
|
+
if issues_to_remove:
|
|
453
|
+
state["activeIssues"] = active_issues
|
|
454
|
+
write_state(cfg.root, state)
|
|
455
|
+
|
|
456
|
+
|
|
320
457
|
def maybe_mark_blocked(cfg: RepoConfig, issue: dict[str, Any]) -> None:
|
|
321
458
|
num = int(issue["number"])
|
|
322
459
|
if is_heartbeat_fresh(cfg, num):
|
|
@@ -358,12 +495,33 @@ def run_cycle(
|
|
|
358
495
|
|
|
359
496
|
for cfg in all_configs:
|
|
360
497
|
print(f"[{cfg.repo}] Scanning for issues...", flush=True)
|
|
361
|
-
|
|
498
|
+
|
|
499
|
+
# 0) Clean up completed issues from state
|
|
500
|
+
if not dry_run:
|
|
501
|
+
clean_completed_issues_from_state(cfg)
|
|
502
|
+
|
|
503
|
+
# 1) Check in-progress issues
|
|
362
504
|
inprog = list_in_progress_issues(cfg)
|
|
363
505
|
for it in inprog:
|
|
364
|
-
|
|
506
|
+
issue_num = int(it["number"])
|
|
507
|
+
|
|
508
|
+
# First check if we should mark as blocked due to stale heartbeat
|
|
365
509
|
if not dry_run:
|
|
366
|
-
|
|
510
|
+
# Only mark blocked if heartbeat is stale
|
|
511
|
+
if not is_heartbeat_fresh(cfg, issue_num):
|
|
512
|
+
maybe_mark_blocked(cfg, it)
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
# If heartbeat is fresh and autoMerge is enabled, try to merge PR
|
|
516
|
+
if cfg.auto_merge:
|
|
517
|
+
pr = check_pr_for_issue(cfg, issue_num)
|
|
518
|
+
if pr:
|
|
519
|
+
print(f"[{cfg.repo}] Found PR for issue #{issue_num}, checking if ready to merge...", flush=True)
|
|
520
|
+
if try_merge_pr(cfg, issue_num, pr):
|
|
521
|
+
print(f"[{cfg.repo}] Issue #{issue_num} completed!", flush=True)
|
|
522
|
+
else:
|
|
523
|
+
# Touch heartbeat to keep it alive while waiting for checks
|
|
524
|
+
touch_heartbeat(cfg, issue_num)
|
|
367
525
|
|
|
368
526
|
# 2) Count active issues with fresh heartbeats from local state
|
|
369
527
|
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
|