Skip to content

gh pr merge --auto -d does not delete the branch after merge #9073

Description

@arcivanov

Describe the bug

A successful gh pr merge --auto -d does not delete the branch after a successful merge. It's unclear whether it's GH CLI problem or GH API.

Steps to reproduce the behavior

The PR in question that didn't have a branch auto-deleted is here: karellen/karellen-llvm#40
The branch was marked for auto-merge and auto-delete here: https://github.com/karellen/karellen-llvm/actions/runs/9048046223/job/24860694266#step:4:62 (line 61)

If this is not something that can be fixed in GH CLI and GH API the documentation update is necessary to note that it's not an expected behavior.

Expected vs actual behavior

Expected: PR auto-merge will be followed by auto-delete if delete is requested.
Actual: The PR is automatically merged but the branch remains.

Logs

Above in the action items.

Activity

  1. williammartin commented on May 12, 2024

    @williammartin
    Member

    Reading the code this is intentional behaviour when you provide --auto because as far as I know there is no way to tell the API to remember to delete the branch when the PR is automatically merged.

    if !m.deleteBranch || m.crossRepoPR || m.autoMerge {
    return nil
    }

    However, it's not clear to me why we allow both of these flags to be provided at the same time if this is the behaviour, instead of erroring and saying "you can't do that". This seems to have been the behaviour since the --auto flag was introduced. I agree with you that it is confusing.

  2. andyfeller commented on May 16, 2024

    @andyfeller
    Contributor

    Is the suggestion that we need to fail early if these confliction options are specified?

    Reviewing the code, I see we have other places we're checking for mutually exclusive options:

    if err := cmdutil.MutuallyExclusive(
    "specify only one of `--auto`, `--disable-auto`, or `--admin`",
    opts.AutoMergeEnable,
    opts.AutoMergeDisable,
    opts.UseAdmin,
    ); err != nil {
    return err
    }

    Reading through the rest of the code, m.deleteBranch is not derived directly from options:

    func deleteBranchSurvey(opts *MergeOptions, crossRepoPR, localBranchExists bool) (bool, error) {
    if !opts.IsDeleteBranchIndicated {
    var message string
    if opts.CanDeleteLocalBranch && localBranchExists {
    if crossRepoPR {
    message = "Delete the branch locally?"
    } else {
    message = "Delete the branch locally and on GitHub?"
    }
    } else if !crossRepoPR {
    message = "Delete the branch on GitHub?"
    }
    return opts.Prompter.Confirm(message, false)
    }
    return opts.DeleteBranch, nil
    }

  3. added
    priority-3Affects a small number of users or is largely cosmetic
    and removed on May 16, 2024
  4. williammartin commented on May 21, 2024

    @williammartin
    Member

    Is the suggestion that we need to fail early if these confliction options are specified?

    Yes, I can't see any reason that we would want to proceed in this case. I can't see any code path that would allow for the branch to be deleted if --auto is provided.

  5. AriehSchneier commented on Jun 27, 2024

    @AriehSchneier

    Is it possible to set the branch to be auto-deleted after it is auto-merged? Using the 'Merge when ready' button in the UI seems to allow that?

  6. scarf005 commented on Aug 20, 2024

    @scarf005
    Contributor

    Yes, I can't see any reason that we would want to proceed in this case.

    I think passing both --auto and --delete-branch should work like this:

    branch is not merged branch is already merged
    same as --auto same as --delete-branch
  7. electriquo commented on Jan 14, 2025

    @electriquo

    the issue occurs even when merge queue is enabled and --auto --delete-branch are used together

    $ gh pr merge --auto --delete-branch https://github.com/foo/baz/pull/123
    X Cannot use `-d` or `--delete-branch` when merge queue enabled
    Error: Process completed with exit code 1.
  8. BagToad commented on Jan 14, 2025

    @BagToad
    Member

    Hey @electriquo 👋

    That error message is actually intentional and a feature to make gh pr merge more intelligent and compatible with merge queues. We cannot allow branch deletion with a merge queue because the branch must exist while it is processed through the merge queue. If we allow branch deletion with merge queues, usually the PR gets quietly kicked out of the merge queue and the PR quietly closed - which is a poor experience that we don't want to allow.

    For more information, this feature was implemented in #10074 and discussed in #7011

    ☝ This is likely the same solution we'll apply to this issue - early exit when the user requests branch deletions that would result in undesirable behavior.

  9. electriquo commented on Jan 15, 2025

    @electriquo

    @BagToad: thanks. i was hoping that the request to delete branch will take happen asynchronously and only after if the merge was successful.

  10. BagToad commented on Jan 15, 2025

    @BagToad
    Member

    @BagToad: thanks. i was hoping that the request to delete branch will take happen asynchronously and only after if the merge was successful.

    @electriquo I think the best way to achieve that async branch deletion is to enable the automatic branch deletion repo setting. This works more elegantly with merge queue.

    Does that setting work for you?

  11. rafaelpirolla commented on Feb 20, 2025

    @rafaelpirolla

    I have enabled the automatic behavior but I have some 600 pull requests before I've enable it. Is there any CLI option to delete the branch like in the pull request page?

  12. AriehSchneier commented on Feb 20, 2025

    @AriehSchneier

    I have enabled the automatic behavior but I have some 600 pull requests before I've enable it. Is there any CLI option to delete the branch like in the pull request page?

    @rafaelpirolla
    A shell script I wrote to go back and delete branches from closed pull requests using the gh cli, note there is a commented out line that you can switch if you want to go back through the full history, otherwise there is a limit to check the last 20 days/100 prs (you can change either limit):

    #!/bin/bash
    
    # Check if the correct number of arguments are provided
    if [ "$#" -ne 1 ]; then
        echo "Usage: $0 owner/repo"
        exit 1
    fi
    
    # Variables
    REPO="$1"
    
    # limit the number of closed pull requests to check
    recent=$(date -v -20d +%Y-%m-%d)
    limit=100
    
    # switch to check all
    # prs=$(gh pr list --repo $REPO --state closed --json number,headRefName)
    prs=$(gh pr list --repo $REPO --state closed --limit $limit --search "closed:>=$recent" --json number,headRefName)
    
    # Iterate over each pull request
    echo "$prs" | jq -c '.[]' | while read -r pr; do
        pr_number=$(echo "$pr" | jq -r '.number')
        branch_name=$(echo "$pr" | jq -r '.headRefName')
    
        # Check if the branch exists
        if gh api "repos/$REPO/branches/$branch_name" > /dev/null 2>&1; then
            gh api -X DELETE "repos/$REPO/git/refs/heads/$branch_name"
            echo "PR #$pr_number deleted branch: $branch_name"
        fi
    done
    
  13. arcivanov commented on Feb 22, 2025

    @arcivanov
    Author

    @AriehSchneier Thanks for the above script. I have improved on it considerably as follows:

    The below does not require checking out the repository at all and does not iterate over branches checking each one but has a set subtraction, i.e. you have one operation to retrieve all branches, one iteration to retrieve all PR records and then one operation each to delete only PRs that have matching branch found.

    1. Get all the branches that match auto-update.* and retrieve their heads putting them into a JSON looking like {"branches": ["branch1", "branch2"]}
    2. Get all PRs putting them into JSON {"prs": [{}, {}]}
    3. Eliminate all PRS with headRefName found in BRANCHES.
    4. Delete the results branches.

    This script filters by auto-update-* but it's easy to eliminate.

    BRANCHES=$(git ls-remote $GITHUB_SERVER_URL/$GITHUB_REPOSITORY 'auto-update-*' 2>/dev/null | { grep -o 'refs/heads/auto-update-.*' || true; } | sed 's/refs\/heads\///' | jq -Rn '{branches: [inputs]}')
    
    PRS=$(gh -R $GITHUB_REPOSITORY pr list --state closed --limit 100000 --state closed --json number,headRefName --jq '{prs: .}')
    
    CLOSED_PRS_WITH_BRANCHES=$(jq -s 'add' <(echo "$BRANCHES") <(echo "$PRS") | jq -c '[.branches[] as $branches | .prs[] | select(.headRefName | IN($branches))]')
    
    echo "$CLOSED_PRS_WITH_BRANCHES" | jq -r '.[] | .headRefName' | while read -r branch_name; do
        gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/$branch_name"
        echo "Deleted branch $branch_name"
    done

    You can see how it worked here: https://github.com/karellen/karellen-llvm/actions/runs/13469255221/job/37640624891#step:2:28

  14. AriehSchneier commented on Feb 24, 2025

    @AriehSchneier

    @arcivanov Nice update to remove the requirement to test each branch individually (noting that it wasn't required to check the repo out in my original script).
    I have tweaked my original script with your changes with a couple of minor tweaks.

    • I removed the auto-update- filter (that seems specific to your case)
    • Its a script that takes the repo as an input
    • I sorted the results so they run in order (minor change, just for cleanliness of the output really)
    • It outputs the PR number and branch name (minor again)
    #!/bin/bash
    
    # Check if the correct number of arguments are provided
    if [ "$#" -ne 1 ]; then
        echo "Usage: $0 owner/repo"
        exit 1
    fi
    
    # Variables
    REPO="$1"
    GITHUB_SERVER_URL="https://github.com"
    
    # Get all branches
    BRANCHES=$(git ls-remote $GITHUB_SERVER_URL/$REPO 2>/dev/null | { grep -o 'refs/heads/.*' || true; } | sed 's/refs\/heads\///' | jq -Rn '{branches: [inputs]}')
    
    # Get all closed pull requests
    PRS=$(gh pr list --repo $REPO --state closed --limit 100000 --json number,headRefName --jq '{prs: .}')
    
    # Calculate difference
    CLOSED_PRS_WITH_BRANCHES=$(jq -s 'add' <(echo "$BRANCHES") <(echo "$PRS") | jq -c '[.branches[] as $branches | .prs[] | select(.headRefName | IN($branches))] | sort_by(.number)')
    
    # Iterate over each pull request
    echo "$CLOSED_PRS_WITH_BRANCHES" | jq -c '.[]' | while read -r pr; do
        pr_number=$(echo "$pr" | jq -r '.number')
        branch_name=$(echo "$pr" | jq -r '.headRefName')
    
        gh api -X DELETE "repos/$REPO/git/refs/heads/$branch_name"
        echo "PR #$pr_number deleted branch: $branch_name"
    done
    
  15. electriquo commented on Feb 25, 2025

    @electriquo

    @AriehSchneier @arcivanov

    What do you think of:

    1. Instead of asking users for inputs, why not pull the necessary values from GitHub context or environment variables? At the very least, you could use them to set default values. The Context Dump action might come in handy for this.
    2. Have you thought about turning this into a public GitHub Action? That way, anyone could use it easily without extra setup. It could make things much more convenient for others.
  16. khatchad commented on Sep 17, 2025

    @khatchad

    I just hit this problem on gh version 2.50.0 (2024-05-29). I would think that this is a pretty difficult command because I'm unsure how gh would know when the PR was merged successfully (passes the checks) to delete the local branch. Perhaps the remote branch deletion is easier, but there's also a repo setting for that. My suggestion is to actually not allow this combination of commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggh-prrelating to the gh pr commandpriority-3Affects a small number of users or is largely cosmetic

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions