From 9b255aada02e0c823ec7b3fd18e35188a26c6ba9 Mon Sep 17 00:00:00 2001 From: Laszlo Kindrat Date: Tue, 2 Dec 2025 18:33:40 -0500 Subject: [PATCH] Mark PRs temporarily draft while submitting When running `stack-pr submit`, patches in a stack need to have their base branches temporarily changed to avoid accidental closures. This has the unfortunate side effect that in this transient state, the PRs seem like multi-commit PRs pointing to main, resulting in GH notifying reviewers for every commit on the branch (as opposed to just the one that corresponds to the stacked PR). This patch fixes this by marking PRs temporarily draft while we manipulate their bases. stack-info: PR: https://github.com/modular/stack-pr/pull/111, branch: laszlokindrat/stack/2 --- src/stack_pr/cli.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/stack_pr/cli.py b/src/stack_pr/cli.py index b5f4023..9ec2e1f 100755 --- a/src/stack_pr/cli.py +++ b/src/stack_pr/cli.py @@ -294,7 +294,7 @@ class StackEntry: _pr: str | None = None _base: str | None = None _head: str | None = None - need_update: bool = False + is_tmp_draft: bool = False @property def pr(self) -> str: @@ -783,7 +783,7 @@ def toc_entry(se: StackEntry) -> str: return f"Stacked PRs:\n{''.join(entries)}\n" -def get_current_pr_body(e: StackEntry) -> str: +def get_pr_body(e: StackEntry) -> str: out = get_command_output( ["gh", "pr", "view", e.pr, "--json", "body"], ) @@ -815,7 +815,7 @@ def add_cross_links(st: list[StackEntry], *, keep_body: bool, verbose: bool) -> if keep_body: # Keep current body of the PR after the cross links component - current_pr_body = get_current_pr_body(e) + current_pr_body = get_pr_body(e) body_content = current_pr_body.split(CROSS_LINKS_DELIMETER, 1)[-1].lstrip() pr_body = [*header, body_content] @@ -831,6 +831,13 @@ def add_cross_links(st: list[StackEntry], *, keep_body: bool, verbose: bool) -> raise RuntimeError +def is_draft_pr(e: StackEntry) -> bool: + out = get_command_output( + ["gh", "pr", "view", e.pr, "--json", "isDraft"], + ) + return json.loads(out)["isDraft"] + + # Temporarily set base branches of existing PRs to the bottom of the stack. # This needs to be done to avoid PRs getting closed when commits are # rearranged. @@ -852,14 +859,21 @@ def add_cross_links(st: list[StackEntry], *, keep_body: bool, verbose: bool) -> # stack/2. If we push stack/1, then PR #2 gets automatically closed, since its # head branch will contain all the commits from its base branch. # -# To avoid this, we temporarily set all base branches to point to 'main' - once -# all the branches are pushed we can set the actual base branches. +# To avoid this, we temporarily set all base branches to point to 'main'. To ensure +# we don't accidentally notify reviewers in this transient state (where the PRs are +# pointing to 'main'), we mark the PRs as draft - once all the branches are pushed +# we can set the actual base branches and undraft the PRs. def reset_remote_base_branches( st: list[StackEntry], target: str, *, verbose: bool ) -> None: log(h("Resetting remote base branches"), level=2) for e in filter(lambda e: e.has_pr(), st): + # We need to check if the PR is already draft, otherwise we would + # unintentionally undo the draft status later. + if not is_draft_pr(e): + run_shell_command(["gh", "pr", "ready", e.pr, "--undo"], quiet=not verbose) + e.is_tmp_draft = True run_shell_command(["gh", "pr", "edit", e.pr, "-B", target], quiet=not verbose) @@ -1117,6 +1131,12 @@ def command_submit( log(h("Adding cross-links to PRs"), level=1) add_cross_links(st, keep_body=keep_body, verbose=args.verbose) + # Undraft the PRs if they were marked as temporary drafts. + for e in st: + if e.is_tmp_draft: + run_shell_command(["gh", "pr", "ready", e.pr], quiet=not args.verbose) + e.is_tmp_draft = False + if need_to_rebase_current: log(h(f"Rebasing the original branch '{current_branch}'"), level=2) run_shell_command(