feat(drone): add generic drone @git pr command — works from any branch including main
This commit is contained in:
@@ -6,10 +6,11 @@
|
||||
# Modified: 2026-05-12
|
||||
# =============================================
|
||||
|
||||
"""Dev branch PR handler — push dev and create PR to main."""
|
||||
"""Branch PR handlers — generic and dev-specific."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from aipass.prax import logger
|
||||
@@ -17,6 +18,101 @@ from aipass.drone.apps.handlers.json import json_handler
|
||||
from aipass.drone.apps.handlers.git.lock_handler import find_repo_root
|
||||
|
||||
|
||||
def _slugify(text: str) -> str:
|
||||
"""Convert text to a branch-safe slug."""
|
||||
slug = text.lower().strip().replace(" ", "-")
|
||||
slug = re.sub(r"[^a-z0-9-]", "", slug)
|
||||
slug = re.sub(r"-{2,}", "-", slug).strip("-")
|
||||
return slug[:40]
|
||||
|
||||
|
||||
def create_branch_pr(description: str, target_branch: str = "main") -> dict:
|
||||
"""Push current branch and create a PR to target.
|
||||
|
||||
Works from any branch. Detects HEAD, pushes to origin, creates PR via gh CLI.
|
||||
|
||||
Returns:
|
||||
Dict with success, message, and pr_url keys.
|
||||
"""
|
||||
repo_root = find_repo_root()
|
||||
|
||||
try:
|
||||
head = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
logger.error("Failed to detect current branch: %s", exc)
|
||||
return {"success": False, "message": f"Failed to detect current branch: {exc}", "pr_url": ""}
|
||||
|
||||
pr_branch = head.stdout.strip()
|
||||
|
||||
if pr_branch == target_branch:
|
||||
pr_branch = _slugify(description)
|
||||
if not pr_branch:
|
||||
return {"success": False, "message": "Description required to generate branch name.", "pr_url": ""}
|
||||
push_refspec = f"{target_branch}:{pr_branch}"
|
||||
else:
|
||||
push_refspec = pr_branch
|
||||
|
||||
try:
|
||||
push = subprocess.run(
|
||||
["git", "push", "-u", "origin", push_refspec],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
logger.error("git push failed: %s", exc)
|
||||
return {"success": False, "message": f"Push failed: {exc}", "pr_url": ""}
|
||||
|
||||
if push.returncode != 0:
|
||||
return {"success": False, "message": f"Push failed: {push.stderr.strip()}", "pr_url": ""}
|
||||
|
||||
try:
|
||||
pr = subprocess.run(
|
||||
["gh", "pr", "create", "--head", pr_branch, "--base", target_branch, "--title", description, "--body", ""],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo_root),
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
logger.warning("gh CLI not found: %s", exc)
|
||||
return {"success": False, "message": "gh CLI not found. Install: https://cli.github.com/", "pr_url": ""}
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
logger.error("gh pr create failed: %s", exc)
|
||||
return {"success": False, "message": f"PR creation failed: {exc}", "pr_url": ""}
|
||||
|
||||
if pr.returncode != 0:
|
||||
stderr = pr.stderr.strip()
|
||||
if "already exists" in stderr:
|
||||
existing_url = ""
|
||||
for line in stderr.splitlines():
|
||||
if "github.com" in line:
|
||||
existing_url = line.strip()
|
||||
break
|
||||
msg = (
|
||||
f"Pushed to {pr_branch}. PR already open: {existing_url}"
|
||||
if existing_url
|
||||
else f"Pushed to {pr_branch}. PR already open."
|
||||
)
|
||||
json_handler.log_operation(
|
||||
"branch_pr_push_existing", {"pr_url": existing_url, "branch": pr_branch, "description": description}
|
||||
)
|
||||
return {"success": True, "message": msg, "pr_url": existing_url}
|
||||
return {"success": False, "message": f"PR creation failed: {stderr}", "pr_url": ""}
|
||||
|
||||
pr_url = pr.stdout.strip()
|
||||
json_handler.log_operation(
|
||||
"create_branch_pr", {"pr_url": pr_url, "branch": pr_branch, "target": target_branch, "description": description}
|
||||
)
|
||||
logger.info("PR created from %s: %s", pr_branch, pr_url)
|
||||
|
||||
return {"success": True, "message": f"PR created: {pr_url}", "pr_url": pr_url}
|
||||
|
||||
|
||||
def create_dev_pr(description: str) -> dict:
|
||||
"""Push dev branch and create a PR to main.
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ _COMMANDS = (
|
||||
"unlock",
|
||||
"dev-pr",
|
||||
"delete-branch",
|
||||
"system-pr",
|
||||
"merge",
|
||||
"smart-sync",
|
||||
"fix",
|
||||
@@ -153,8 +152,6 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
|
||||
return _handle_sync(args)
|
||||
if command == "unlock":
|
||||
return _handle_unlock(args)
|
||||
if command == "system-pr":
|
||||
return _handle_system_pr(args, caller)
|
||||
if command == "merge":
|
||||
return _handle_merge(args, caller)
|
||||
if command == "smart-sync":
|
||||
@@ -162,7 +159,7 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
|
||||
if command == "fix":
|
||||
return _handle_fix(args, caller)
|
||||
if command == "pr":
|
||||
return {"stdout": "", "stderr": "Agent PRs are deprecated.", "exit_code": 1}
|
||||
return _handle_pr(args)
|
||||
|
||||
available = ", ".join(_COMMANDS)
|
||||
return {
|
||||
@@ -215,6 +212,21 @@ def _handle_branches() -> dict:
|
||||
return {"stdout": result["message"], "stderr": "", "exit_code": 0}
|
||||
|
||||
|
||||
def _handle_pr(args: list[str]) -> dict:
|
||||
"""Handle the pr subcommand — push current branch and create PR to main."""
|
||||
if not args:
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "Usage: drone @git pr <description>",
|
||||
"exit_code": 1,
|
||||
}
|
||||
description = " ".join(args)
|
||||
result = dev_pr_handler.create_branch_pr(description)
|
||||
if result["success"]:
|
||||
return {"stdout": result["message"], "stderr": "", "exit_code": 0}
|
||||
return {"stdout": "", "stderr": result["message"], "exit_code": 1}
|
||||
|
||||
|
||||
def _handle_dev_pr(args: list[str]) -> dict:
|
||||
"""Handle the dev-pr subcommand (owner tier)."""
|
||||
if not args:
|
||||
@@ -244,15 +256,6 @@ def _handle_delete_branch(args: list[str]) -> dict:
|
||||
return {"stdout": "", "stderr": result["message"], "exit_code": 1}
|
||||
|
||||
|
||||
def _handle_system_pr(_args: list[str], _caller: str) -> dict:
|
||||
"""Handle the system-pr subcommand — DEPRECATED."""
|
||||
return {
|
||||
"stdout": "",
|
||||
"stderr": "system-pr is deprecated. Use: drone @git dev-pr <description>",
|
||||
"exit_code": 1,
|
||||
}
|
||||
|
||||
|
||||
def _handle_merge(args: list[str], caller: str) -> dict:
|
||||
"""Handle the merge subcommand (owner-tier, auth pre-checked)."""
|
||||
if not args:
|
||||
@@ -542,7 +545,7 @@ def get_help(command: str | None = None) -> str:
|
||||
"git workflow [args] — Passthrough to gh workflow CLI [global]\n Examples: list, view <name>, run <name>\n"
|
||||
)
|
||||
if command == "pr":
|
||||
return "git pr — DEPRECATED. Agent PRs are no longer supported. Devpulse handles git.\n"
|
||||
return "git pr <description> — Push current branch and create PR to main [owner]\n"
|
||||
if command == "status":
|
||||
return "git status [--all] — Show git status filtered to your branch (--all for repo-wide) [global]\n"
|
||||
if command == "diff":
|
||||
@@ -585,8 +588,6 @@ def get_help(command: str | None = None) -> str:
|
||||
)
|
||||
if command == "unlock":
|
||||
return "git unlock --force — Force-release the PR lock [owner]\n"
|
||||
if command == "system-pr":
|
||||
return "git system-pr — DEPRECATED. Use: drone @git dev-pr <description>\n"
|
||||
if command == "merge":
|
||||
return (
|
||||
"git merge <PR#> — Merge a PR and sync local main [owner]\n"
|
||||
@@ -624,13 +625,13 @@ def get_help(command: str | None = None) -> str:
|
||||
"Owner (devpulse only):\n"
|
||||
" commit <msg> [--all | files] Commit changes (selective or --all)\n"
|
||||
" checkout <main|dev> Switch branches\n"
|
||||
" pr <desc> Push current branch and create PR to main\n"
|
||||
" dev-pr <desc> Push dev and create PR to main\n"
|
||||
" delete-branch <name> Delete a remote branch\n"
|
||||
" merge <PR#> Merge a PR\n"
|
||||
" sync [--autostash] Checkout main and pull\n"
|
||||
" smart-sync Fetch + rebase if behind\n"
|
||||
" unlock --force Force-release the PR lock\n"
|
||||
" system-pr DEPRECATED (use dev-pr)\n"
|
||||
" fix [--dry-run] Fix broken git states\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ GIT_ACCESS_TIERS: dict[str, dict] = {
|
||||
"checkout",
|
||||
"sync",
|
||||
"unlock",
|
||||
"system-pr",
|
||||
"merge",
|
||||
"smart-sync",
|
||||
"fix",
|
||||
"dev-pr",
|
||||
"pr",
|
||||
"delete-branch",
|
||||
],
|
||||
"allowed_callers": ["devpulse"],
|
||||
@@ -121,9 +121,6 @@ def verify_git_access(command: str) -> str:
|
||||
Raises:
|
||||
PermissionError: If the caller is not authorized for this command.
|
||||
"""
|
||||
if command == "pr":
|
||||
raise PermissionError("Agent PRs are deprecated. Build code, run tests, report results. Devpulse handles git.")
|
||||
|
||||
global_cmds = GIT_ACCESS_TIERS["global"]["commands"]
|
||||
owner_tier = GIT_ACCESS_TIERS["owner"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user