From 52591f9f9ccd1ce7c93b337257343c44c88ffe7e Mon Sep 17 00:00:00 2001 From: McKay H Davis Date: Thu, 24 Jul 2025 16:30:30 -0700 Subject: [PATCH] Skip CI on first push to avoid race condition Add [skip ci] to commit messages on first push, then remove it on second push. This ensures CI only runs on the final commits, avoiding the race condition where GitHub cancels the wrong workflow when both pushes happen within 1 second. --- src/stack_pr/cli.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/stack_pr/cli.py b/src/stack_pr/cli.py index 32afe35..c8c4557 100755 --- a/src/stack_pr/cli.py +++ b/src/stack_pr/cli.py @@ -534,7 +534,9 @@ def draft_bitmask_type(value: str) -> list[bool]: # ===----------------------------------------------------------------------=== # # SUBMIT # ===----------------------------------------------------------------------=== # -def add_or_update_metadata(e: StackEntry, *, needs_rebase: bool, verbose: bool) -> bool: +def add_or_update_metadata( + e: StackEntry, *, needs_rebase: bool, verbose: bool, skip_ci: bool = False +) -> bool: if needs_rebase: if not e.has_base() or not e.has_head(): error("Stack entry has no base or head branch") @@ -565,6 +567,8 @@ def add_or_update_metadata(e: StackEntry, *, needs_rebase: bool, verbose: bool) # Add the stack info metadata to the commit message commit_msg += f"\n\nstack-info: PR: {e.pr}, branch: {e.head}" + if skip_ci: + commit_msg += "\n\n[skip ci]" run_shell_command( ["git", "commit", "--amend", "-F", "-"], input=commit_msg.encode(), @@ -573,6 +577,25 @@ def add_or_update_metadata(e: StackEntry, *, needs_rebase: bool, verbose: bool) return True +def remove_skip_ci_from_commits(st: list[StackEntry], *, verbose: bool) -> None: + """Remove [skip ci] from all commit messages in the stack.""" + for e in st: + run_shell_command(["git", "checkout", e.head], quiet=not verbose) + commit_msg = e.commit.commit_msg() + if "[skip ci]" in commit_msg: + # Remove [skip ci] with various spacing patterns + commit_msg = ( + commit_msg.replace("\n\n[skip ci]", "") + .replace("\n[skip ci]", "") + .replace("[skip ci]", "") + ) + run_shell_command( + ["git", "commit", "--amend", "-F", "-"], + input=commit_msg.encode(), + quiet=not verbose, + ) + + def fix_branch_name_template(branch_name_template: str) -> str: if "$ID" not in branch_name_template: return f"{branch_name_template}/$ID" @@ -1000,13 +1023,13 @@ def command_submit( # Verify consistency in everything we have so far verify(st) - # Embed stack-info into commit messages + # Embed stack-info into commit messages with [skip ci] for first push log(h("Updating commit messages with stack metadata"), level=1) needs_rebase = False for e in st: try: needs_rebase = add_or_update_metadata( - e, needs_rebase=needs_rebase, verbose=args.verbose + e, needs_rebase=needs_rebase, verbose=args.verbose, skip_ci=True ) except Exception: error(ERROR_CANT_UPDATE_META.format(**locals())) @@ -1014,6 +1037,11 @@ def command_submit( push_branches(st, remote=args.remote, verbose=args.verbose) + # Remove [skip ci] from commit messages for final push + log(h("Removing [skip ci] from commit messages"), level=1) + remove_skip_ci_from_commits(st, verbose=args.verbose) + push_branches(st, remote=args.remote, verbose=args.verbose) + log(h("Adding cross-links to PRs"), level=1) add_cross_links(st, keep_body=keep_body, verbose=args.verbose)