Skip to content

Create pr

create_pr

Create-PR hook action.

Creates a GitHub pull request with conventional commit title and auto-merge.

CreatePRHook dataclass

Create a GitHub PR with auto-merge squash.

Reads branch, commit_spec, and ticket_id from context. Runs gh pr create followed by gh pr merge --auto --squash. Skips gracefully when gh is not installed.

Source code in packages/axm-git/src/axm_git/hooks/create_pr.py
@dataclass
class CreatePRHook:
    """Create a GitHub PR with auto-merge squash.

    Reads ``branch``, ``commit_spec``, and ``ticket_id`` from *context*.
    Runs ``gh pr create`` followed by ``gh pr merge --auto --squash``.
    Skips gracefully when ``gh`` is not installed.
    """

    def execute(self, context: dict[str, Any], **params: Any) -> HookResult:
        """Execute the hook action.

        Args:
            context: Session context dictionary.
            **params: Optional ``enabled`` (default ``True``),
                ``base`` (default ``"main"``), ``commit_spec``,
                ``ticket_id``.  Params take precedence over context.

        Returns:
            HookResult with ``pr_url`` and ``pr_number`` in metadata.
        """
        if not params.get("enabled", True):
            return HookResult.ok(skipped=True, reason="git disabled")

        if not gh_available():
            return HookResult.ok(skipped=True, reason="gh not available")

        working_dir = _resolve_working_dir(params, context)

        commit_spec: dict[str, Any] = params.get(
            "commit_spec", context.get("commit_spec", {})
        )
        ticket_id: str = params.get("ticket_id", context.get("ticket_id", ""))
        base = params.get("base", "main")

        title = _format_pr_title(commit_spec, ticket_id)
        body = commit_spec.get("body", "")

        # Create the PR
        create_args = [
            "pr",
            "create",
            "--title",
            title,
            "--body",
            body,
            "--base",
            base,
        ]
        result = run_gh(create_args, working_dir)

        if result.returncode != 0:
            stderr = result.stderr.strip()
            if "already exists" in stderr:
                return _recover_existing_pr(working_dir)
            return HookResult.fail(f"gh pr create failed: {stderr}")

        pr_url = result.stdout.strip()

        # Extract PR number from URL
        pr_number = pr_url.rstrip("/").rsplit("/", maxsplit=1)[-1]

        # Enable auto-merge
        merge_result = run_gh(
            ["pr", "merge", pr_number, "--auto", "--squash"],
            working_dir,
        )
        if merge_result.returncode != 0:
            # Non-fatal: PR created but auto-merge not enabled
            return HookResult.ok(
                pr_url=pr_url,
                pr_number=pr_number,
                auto_merge=False,
                auto_merge_error=merge_result.stderr.strip(),
            )

        return HookResult.ok(
            pr_url=pr_url,
            pr_number=pr_number,
            auto_merge=True,
        )
execute(context, **params)

Execute the hook action.

Parameters:

Name Type Description Default
context dict[str, Any]

Session context dictionary.

required
**params Any

Optional enabled (default True), base (default "main"), commit_spec, ticket_id. Params take precedence over context.

{}

Returns:

Type Description
HookResult

HookResult with pr_url and pr_number in metadata.

Source code in packages/axm-git/src/axm_git/hooks/create_pr.py
def execute(self, context: dict[str, Any], **params: Any) -> HookResult:
    """Execute the hook action.

    Args:
        context: Session context dictionary.
        **params: Optional ``enabled`` (default ``True``),
            ``base`` (default ``"main"``), ``commit_spec``,
            ``ticket_id``.  Params take precedence over context.

    Returns:
        HookResult with ``pr_url`` and ``pr_number`` in metadata.
    """
    if not params.get("enabled", True):
        return HookResult.ok(skipped=True, reason="git disabled")

    if not gh_available():
        return HookResult.ok(skipped=True, reason="gh not available")

    working_dir = _resolve_working_dir(params, context)

    commit_spec: dict[str, Any] = params.get(
        "commit_spec", context.get("commit_spec", {})
    )
    ticket_id: str = params.get("ticket_id", context.get("ticket_id", ""))
    base = params.get("base", "main")

    title = _format_pr_title(commit_spec, ticket_id)
    body = commit_spec.get("body", "")

    # Create the PR
    create_args = [
        "pr",
        "create",
        "--title",
        title,
        "--body",
        body,
        "--base",
        base,
    ]
    result = run_gh(create_args, working_dir)

    if result.returncode != 0:
        stderr = result.stderr.strip()
        if "already exists" in stderr:
            return _recover_existing_pr(working_dir)
        return HookResult.fail(f"gh pr create failed: {stderr}")

    pr_url = result.stdout.strip()

    # Extract PR number from URL
    pr_number = pr_url.rstrip("/").rsplit("/", maxsplit=1)[-1]

    # Enable auto-merge
    merge_result = run_gh(
        ["pr", "merge", pr_number, "--auto", "--squash"],
        working_dir,
    )
    if merge_result.returncode != 0:
        # Non-fatal: PR created but auto-merge not enabled
        return HookResult.ok(
            pr_url=pr_url,
            pr_number=pr_number,
            auto_merge=False,
            auto_merge_error=merge_result.stderr.strip(),
        )

    return HookResult.ok(
        pr_url=pr_url,
        pr_number=pr_number,
        auto_merge=True,
    )