build(release): make Git.checkout a context manager (#4239) Release commands that switch to older release branches left the working tree on the target branch when exiting. This caused the post-job cleanup for the local `.github/actions/setup-bazel` composite action to fail when the checked-out release branch predated that action. Convert `Git.checkout()` into a context manager that restores the prior branch on exit, and update all callers and agent Python conventions.
diff --git a/.agents/rules/python.md b/.agents/rules/python.md index fd7aef7..f2545ab 100644 --- a/.agents/rules/python.md +++ b/.agents/rules/python.md
@@ -58,4 +58,6 @@ * Module-level functions delegating to class methods should have a docstring referring to the class method (e.g. `"""Refer to \`Class.method\`."""`). - +## Context Managers +* Implement temporary state changes (e.g., `Git.checkout()`, `os.chdir()`) as + context managers that restore prior state on exit.
diff --git a/dev/release/backport_prepare.py b/dev/release/backport_prepare.py index 21d00b1..3f03bdb 100644 --- a/dev/release/backport_prepare.py +++ b/dev/release/backport_prepare.py
@@ -158,16 +158,13 @@ if self.git.status(): raise RuntimeError("Workspace is dirty. Aborting.") - current_branch = self.git.get_current_branch() verify_results = {} # branch -> (success, reason) version_map = {} # branch -> next_version - try: - for branch in target_branches: - minor_ver = branch.split("/")[1] - print(f"Verifying application on {branch}...") - self.git.checkout(branch, track_remote=args.remote) - + for branch in target_branches: + minor_ver = branch.split("/")[1] + print(f"Verifying application on {branch}...") + with self.git.checkout(branch, track_remote=args.remote): # Determine next version next_version = determine_next_version(branch) version_map[branch] = next_version @@ -200,10 +197,6 @@ except Exception: pass self.git.reset_hard(reset_to=f"{args.remote}/{branch}") - finally: - # Restore original branch - if current_branch: - self.git.checkout(current_branch) # Generate issue content body_lines = [
diff --git a/dev/release/create_rc.py b/dev/release/create_rc.py index fabf0c5..3ca50e5 100644 --- a/dev/release/create_rc.py +++ b/dev/release/create_rc.py
@@ -125,18 +125,17 @@ target_ref = f"{args.remote}/{branch_name}" commit_sha = self.git.get_commit_sha(target_ref) - self.git.checkout(target_ref) - - print("Checking for leftover VERSION_NEXT_ markers...") - markers = find_version_markers() - if markers: - for marker in markers: - print(marker) - print( - "Error: Found VERSION_NEXT markers indicating version needs to" - " be specified." - ) - return 1 + with self.git.checkout(target_ref): + print("Checking for leftover VERSION_NEXT_ markers...") + markers = find_version_markers() + if markers: + for marker in markers: + print(marker) + print( + "Error: Found VERSION_NEXT markers indicating version needs to" + " be specified." + ) + return 1 # Precheck: next RC number must exist and be unchecked in the checklist rc_tags = state.get("rc_tags", {})
diff --git a/dev/release/git.py b/dev/release/git.py index bab76a6..d4943dc 100644 --- a/dev/release/git.py +++ b/dev/release/git.py
@@ -1,6 +1,8 @@ """Git helper functions for the release tool.""" +import contextlib import subprocess +from collections.abc import Iterator from dev.release.shell import run_cmd @@ -50,20 +52,25 @@ output = self._run_git("tag") return output.splitlines() if output else [] + @contextlib.contextmanager def checkout( self, ref: str, create_branch: bool = False, track_remote: str | None = None, - ) -> None: - """Checks out a git reference (tag, branch, or commit). + ) -> Iterator[None]: + """Checks out a git reference and restores the previous branch on exit. Args: ref: The git reference (tag, branch, or commit) to checkout. create_branch: If True, creates the branch before checking it out. track_remote: If specified, checks out the branch tracking this remote's corresponding branch. + + Yields: + None. """ + previous_branch = self.get_current_branch() cmd = ["checkout"] if create_branch: cmd.append("-b") @@ -82,6 +89,12 @@ if should_reset_hard: self.reset_hard(reset_to=f"{track_remote}/{ref}") + try: + yield + finally: + if previous_branch: + self._run_git("checkout", previous_branch, capture_output=False) + def add(self, *files: str) -> None: """Stages files for commit.
diff --git a/dev/release/prepare.py b/dev/release/prepare.py index d2df2b0..2d11cff 100644 --- a/dev/release/prepare.py +++ b/dev/release/prepare.py
@@ -1,6 +1,7 @@ """Subcommand to prepare the release (updates changelog, placeholders).""" import argparse +import contextlib import datetime from dev.release import changelog_news @@ -100,101 +101,105 @@ f"[DRY RUN] Branch {branch_name} already exists. Would" " checkout existing branch." ) + checkout_cm = contextlib.nullcontext() else: print(f"Branch {branch_name} already exists. Checking it out...") - self.git.checkout(branch_name) + checkout_cm = self.git.checkout(branch_name) else: if args.dry_run: print(f"[DRY RUN] Would create and checkout branch {branch_name}") + checkout_cm = contextlib.nullcontext() else: - self.git.checkout(branch_name, create_branch=True) + checkout_cm = self.git.checkout(branch_name, create_branch=True) - # --- Update files --- - if args.dry_run: - print( - f"[DRY RUN] Would update CHANGELOG.md and version placeholders" - f" for {version}" - ) - else: - print("Updating changelog and placeholders...") - release_date = datetime.date.today().strftime("%Y-%m-%d") - changelog_news.update_changelog(version, release_date) - replace_version_next(version) - - # --- Commit and Push --- - if args.dry_run: - print(f"[DRY RUN] Would push branch {branch_name} to origin") - else: - modified_files = self.git.status() - if modified_files: - # Stage all modified and deleted tracked files - self.git.add_modified_and_deleted() - self.git.commit(f"Prepare release {version}") + with checkout_cm: + # --- Update files --- + if args.dry_run: + print( + f"[DRY RUN] Would update CHANGELOG.md and version placeholders" + f" for {version}" + ) else: - print("No files modified by the release tool. Nothing to commit.") + print("Updating changelog and placeholders...") + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(version, release_date) + replace_version_next(version) - print(f"Pushing branch {branch_name} to origin...") - # Force push to overwrite the remote branch if it already exists (e.g. from a previous run) - self.git.push("origin", branch_name, set_upstream=True, force=True) + # --- Commit and Push --- + if args.dry_run: + print(f"[DRY RUN] Would push branch {branch_name} to origin") + else: + modified_files = self.git.status() + if modified_files: + # Stage all modified and deleted tracked files + self.git.add_modified_and_deleted() + self.git.commit(f"Prepare release {version}") + else: + print("No files modified by the release tool. Nothing to commit.") - # --- Create PR --- - # Determine if we need to create a PR or reuse an existing one - open_pr = self.gh.get_open_pr(branch_name) - associated_pr = None + print(f"Pushing branch {branch_name} to origin...") + # Force push to overwrite the remote branch if it already + # exists (e.g. from a previous run) + self.git.push("origin", branch_name, set_upstream=True, force=True) - if not open_pr and issue_num: - body = self.gh.get_issue_body(issue_num) - state = parse_checklist_state(body) - associated_pr = state["prepare_release"].pr + # --- Create PR --- + # Determine if we need to create a PR or reuse an existing one + open_pr = self.gh.get_open_pr(branch_name) + associated_pr = None - if open_pr: - pr_num = open_pr["number"] - pr_url = open_pr["url"] - print(f"Open Pull Request already exists: {pr_url} (PR #{pr_num})") - elif associated_pr: - pr_num = associated_pr.lstrip("#") - pr_url = f"https://github.com/bazel-contrib/rules_python/pull/{pr_num}" - print( - f"PR #{pr_num} is already associated in tracking issue" - f" #{issue_num}. Using it." - ) - else: + if not open_pr and issue_num: + body = self.gh.get_issue_body(issue_num) + state = parse_checklist_state(body) + associated_pr = state["prepare_release"].pr + + if open_pr: + pr_num = open_pr["number"] + pr_url = open_pr["url"] + print(f"Open Pull Request already exists: {pr_url} (PR #{pr_num})") + elif associated_pr: + pr_num = associated_pr.lstrip("#") + pr_url = f"https://github.com/bazel-contrib/rules_python/pull/{pr_num}" + print( + f"PR #{pr_num} is already associated in tracking issue" + f" #{issue_num}. Using it." + ) + else: + if args.dry_run: + target_issue = f"#{issue_num}" if issue_num else "<NEW_ISSUE>" + print( + f"[DRY RUN] Would create Pull Request for branch" + f" {branch_name} targeting issue {target_issue}" + ) + pr_num = "<NEW_PR>" + else: + pr_url = self.gh.create_pr( + title=f"Prepare release v{version}", + body=f"Work towards #{issue_num}", + base="main", + labels=[RELEASE_PREPARED_LABEL], + ) + pr_num = pr_url.split("/")[-1] + print(f"Created Pull Request: {pr_url} (PR #{pr_num})") + + # --- Update checklist --- if args.dry_run: target_issue = f"#{issue_num}" if issue_num else "<NEW_ISSUE>" print( - f"[DRY RUN] Would create Pull Request for branch" - f" {branch_name} targeting issue {target_issue}" + f"[DRY RUN] Would update tracking issue {target_issue} checklist" + " 'Prepare Release' task status to PENDING" ) - pr_num = "<NEW_PR>" else: - pr_url = self.gh.create_pr( - title=f"Prepare release v{version}", - body=f"Work towards #{issue_num}", - base="main", - labels=[RELEASE_PREPARED_LABEL], + print( + f"Updating tracking issue #{issue_num} checklist 'Prepare" + " Release' task status to PENDING..." ) - pr_num = pr_url.split("/")[-1] - print(f"Created Pull Request: {pr_url} (PR #{pr_num})") - - # --- Update checklist --- - if args.dry_run: - target_issue = f"#{issue_num}" if issue_num else "<NEW_ISSUE>" - print( - f"[DRY RUN] Would update tracking issue {target_issue} checklist" - " 'Prepare Release' task status to PENDING" - ) - else: - print( - f"Updating tracking issue #{issue_num} checklist 'Prepare" - " Release' task status to PENDING..." - ) - body = self.gh.get_issue_body(issue_num) - metadata = {"status": "pending", "pr": f"#{pr_num}"} - updated_body = update_task_in_body( - body, "Prepare Release", checked=False, metadata=metadata - ) - self.gh.update_issue_body(issue_num, updated_body) - print("Preparation pipeline completed successfully!") + body = self.gh.get_issue_body(issue_num) + metadata = {"status": "pending", "pr": f"#{pr_num}"} + updated_body = update_task_in_body( + body, "Prepare Release", checked=False, metadata=metadata + ) + self.gh.update_issue_body(issue_num, updated_body) + print("Preparation pipeline completed successfully!") return 0
diff --git a/dev/release/process_backports.py b/dev/release/process_backports.py index 2b2a0aa..243f1ab 100644 --- a/dev/release/process_backports.py +++ b/dev/release/process_backports.py
@@ -380,31 +380,31 @@ sorted_shas = self.git.sort_commits_chronologically(shas) self.git.fetch(args.remote) - self.git.checkout(branch_name, track_remote=args.remote) - start_sha = self.git.get_commit_sha("HEAD") + with self.git.checkout(branch_name, track_remote=args.remote): + start_sha = self.git.get_commit_sha("HEAD") - try: - result = self._cherry_pick_and_update_prs( - sorted_shas, - sha_to_item, - body, - args.issue, - args.remote, - args.dry_run, - version, - branch_name, - next_rc_suffix, - ) - failed_prs.extend(result.failed_prs) - body = result.body - finally: - if args.dry_run: - logger.info( - "[DRY RUN] Resetting branch %s to %s", + try: + result = self._cherry_pick_and_update_prs( + sorted_shas, + sha_to_item, + body, + args.issue, + args.remote, + args.dry_run, + version, branch_name, - start_sha, + next_rc_suffix, ) - self.git.reset_hard(reset_to=start_sha) + failed_prs.extend(result.failed_prs) + body = result.body + finally: + if args.dry_run: + logger.info( + "[DRY RUN] Resetting branch %s to %s", + branch_name, + start_sha, + ) + self.git.reset_hard(reset_to=start_sha) if failed_prs: logger.error("One or more cherry-picks/resolutions failed:")
diff --git a/dev/release/sync_changelog.py b/dev/release/sync_changelog.py index d79722f..0376017 100644 --- a/dev/release/sync_changelog.py +++ b/dev/release/sync_changelog.py
@@ -172,105 +172,108 @@ sync_branch = f"sync-changelog-{version}-{prs_hash}" self.git.fetch(args.remote, refspec=main_branch) - self.git.checkout(main_branch, track_remote=args.remote) - - try: + with self.git.checkout(main_branch, track_remote=args.remote): if self.git.branch_exists(sync_branch): - self.git.checkout(sync_branch) - self.git.reset_hard(reset_to=main_branch) + checkout_cm = self.git.checkout(sync_branch) + should_reset = True else: - self.git.checkout(sync_branch, create_branch=True) + checkout_cm = self.git.checkout(sync_branch, create_branch=True) + should_reset = False - # Run ProcessNews to process news files and version markers - process_news_args = argparse.Namespace( - version=version, - targets=[str(pr) for pr in sorted_prs], - release_date=args.release_date, - ) - process_news_runner = ProcessNews(process_news_args, gh=self.gh) - ret = process_news_runner.run() - if ret != 0: - err = f"ProcessNews failed for targets: {sorted_prs}" - logger.error(err) - self._post_failure_comment(issue_num) - return 1 + with checkout_cm: + if should_reset: + self.git.reset_hard(reset_to=main_branch) - if not self.git.status(): - logger.info("No changes to sync after running process-news.") - return 0 - - self.git.add_modified_and_deleted() - self.git.commit(f"chore(release): sync changelog for v{version} backports") - self.git.push(args.remote, sync_branch, set_upstream=True, force=True) - - pr_title = f"chore(release): sync changelog for v{version} backports" - pr_body_lines = [ - "Updates CHANGELOG.md and removes news files for backports:", - ] - for pr_num in sorted_prs: - pr_body_lines.append(f"- #{pr_num}") - - pr_body_lines.append("") - pr_body_lines.append(f"Work towards #{issue_num}") - pr_body_lines.append(f"Release-Tracking-Issue: #{issue_num}") - pr_body = "\n".join(pr_body_lines) - - logger.info("Creating PR to %s...", main_branch) - pr_url = self.gh.create_pr( - title=pr_title, - body=pr_body, - base=main_branch, - labels=[SYNC_CHANGELOG_LABEL], - ) - logger.info("Created PR: %s", pr_url) - - pr_num = int(pr_url.split("/")[-1]) - try: - logger.info("Enabling auto-merge for PR #%s...", pr_num) - self.gh.enable_auto_merge(pr_num) - except Exception as e: - logger.warning( - "Failed to enable auto-merge on PR #%s: %s", - pr_num, - format_exception(e), + # Run ProcessNews to process news files and version markers + process_news_args = argparse.Namespace( + version=version, + targets=[str(pr) for pr in sorted_prs], + release_date=args.release_date, ) + process_news_runner = ProcessNews(process_news_args, gh=self.gh) + ret = process_news_runner.run() + if ret != 0: + err = f"ProcessNews failed for targets: {sorted_prs}" + logger.error(err) + self._post_failure_comment(issue_num) + return 1 - try: - logger.info( - "Updating tracking issue #%s checklist with" - " Sync Changelog tasks...", - issue_num, + if not self.git.status(): + logger.info("No changes to sync after running process-news.") + return 0 + + self.git.add_modified_and_deleted() + self.git.commit( + f"chore(release): sync changelog for v{version} backports" ) - issue_body = self.gh.get_issue_body(issue_num) - for pr in sorted_prs: - task_name = f"Sync Changelog #{pr}" - metadata = {"status": "pending", "pr": f"#{pr_num}"} - issue_body = update_task_in_body( - issue_body, - task_name, - checked=False, - metadata=metadata, + self.git.push(args.remote, sync_branch, set_upstream=True, force=True) + + pr_title = f"chore(release): sync changelog for v{version} backports" + pr_body_lines = [ + "Updates CHANGELOG.md and removes news files for backports:", + ] + for pr_num in sorted_prs: + pr_body_lines.append(f"- #{pr_num}") + + pr_body_lines.append("") + pr_body_lines.append(f"Work towards #{issue_num}") + pr_body_lines.append(f"Release-Tracking-Issue: #{issue_num}") + pr_body = "\n".join(pr_body_lines) + + logger.info("Creating PR to %s...", main_branch) + pr_url = self.gh.create_pr( + title=pr_title, + body=pr_body, + base=main_branch, + labels=[SYNC_CHANGELOG_LABEL], + ) + logger.info("Created PR: %s", pr_url) + + pr_num = int(pr_url.split("/")[-1]) + try: + logger.info("Enabling auto-merge for PR #%s...", pr_num) + self.gh.enable_auto_merge(pr_num) + except Exception as e: + logger.warning( + "Failed to enable auto-merge on PR #%s: %s", + pr_num, + format_exception(e), ) - self.gh.update_issue_body(issue_num, issue_body) - except Exception as e: - logger.warning( - "Failed to update tracking issue checklist: %s", - format_exception(e), - ) - try: - success_body = SYNC_CHANGELOG_SUCCESS_COMMENT_TEMPLATE.format( - pr_url=pr_url, - ) - self.gh.post_issue_comment(issue_num, success_body) - except Exception as e: - logger.warning( - "Failed to post success comment to issue #%s: %s", - issue_num, - format_exception(e), - ) - finally: - self.git.checkout(main_branch) + try: + logger.info( + "Updating tracking issue #%s checklist with" + " Sync Changelog tasks...", + issue_num, + ) + issue_body = self.gh.get_issue_body(issue_num) + for pr in sorted_prs: + task_name = f"Sync Changelog #{pr}" + metadata = {"status": "pending", "pr": f"#{pr_num}"} + issue_body = update_task_in_body( + issue_body, + task_name, + checked=False, + metadata=metadata, + ) + self.gh.update_issue_body(issue_num, issue_body) + except Exception as e: + logger.warning( + "Failed to update tracking issue checklist: %s", + format_exception(e), + ) + + try: + success_body = SYNC_CHANGELOG_SUCCESS_COMMENT_TEMPLATE.format( + pr_url=pr_url, + ) + self.gh.post_issue_comment(issue_num, success_body) + except Exception as e: + logger.warning( + "Failed to post success comment to issue #%s: %s", + issue_num, + format_exception(e), + ) return 0
diff --git a/tests/tools/private/release/backport_prepare_test.py b/tests/tools/private/release/backport_prepare_test.py index 99fac9d..cfd0e01 100644 --- a/tests/tools/private/release/backport_prepare_test.py +++ b/tests/tools/private/release/backport_prepare_test.py
@@ -60,9 +60,14 @@ mock_git.checkout.assert_has_calls( [ call("release/1.7", track_remote="my-remote"), + call().__enter__(), + call().__exit__(None, None, None), call("release/1.8", track_remote="my-remote"), + call().__enter__(), + call().__exit__(None, None, None), call("release/1.9", track_remote="my-remote"), - call("work-branch"), # Restored branch + call().__enter__(), + call().__exit__(None, None, None), ] )
diff --git a/tests/tools/private/release/create_rc_test.py b/tests/tools/private/release/create_rc_test.py index 70905be..b2f36d1 100644 --- a/tests/tools/private/release/create_rc_test.py +++ b/tests/tools/private/release/create_rc_test.py
@@ -48,7 +48,13 @@ mock_git.fetch.assert_has_calls( [call("my-remote"), call("my-remote", tags=True, force=True)] ) - mock_git.checkout.assert_called_once_with("my-remote/release/2.0") + mock_git.checkout.assert_has_calls( + [ + call("my-remote/release/2.0"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") @@ -452,7 +458,13 @@ # Assert assert result == 1 - mock_git.checkout.assert_called_once_with("my-remote/release/2.0") + mock_git.checkout.assert_has_calls( + [ + call("my-remote/release/2.0"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.tag.assert_not_called() mock_git.push.assert_not_called() assert mock_gh.get_issue_body(123) == initial_body
diff --git a/tests/tools/private/release/git_test.py b/tests/tools/private/release/git_test.py index b9ed755..7669f91 100644 --- a/tests/tools/private/release/git_test.py +++ b/tests/tools/private/release/git_test.py
@@ -1,4 +1,5 @@ import subprocess +from unittest.mock import call import pytest @@ -14,39 +15,73 @@ return git -def test_checkout_simple(git_obj): - git_obj.checkout("my-branch") - git_obj.mock_run_git.assert_called_once_with( - "checkout", "my-branch", capture_output=False +def test_checkout_simple(mocker, git_obj): + mocker.patch.object(git_obj, "get_current_branch", return_value="main") + with git_obj.checkout("my-branch"): + git_obj.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + git_obj.mock_run_git.assert_has_calls( + [ + call("checkout", "my-branch", capture_output=False), + call("checkout", "main", capture_output=False), + ] + ) + + +def test_checkout_restores_branch_on_exception(mocker, git_obj): + mocker.patch.object(git_obj, "get_current_branch", return_value="main") + with pytest.raises(RuntimeError, match="boom"): + with git_obj.checkout("my-branch"): + raise RuntimeError("boom") + git_obj.mock_run_git.assert_has_calls( + [ + call("checkout", "my-branch", capture_output=False), + call("checkout", "main", capture_output=False), + ] ) def test_checkout_track_remote_new_branch(mocker, git_obj): + mocker.patch.object(git_obj, "get_current_branch", return_value="main") mock_branch_exists = mocker.patch( "dev.release.git.Git.branch_exists", return_value=False ) - git_obj.checkout("my-branch", track_remote="origin") + with git_obj.checkout("my-branch", track_remote="origin"): + mock_branch_exists.assert_called_once_with("my-branch") + git_obj.mock_run_git.assert_called_once_with( + "checkout", "--track", "origin/my-branch", capture_output=False + ) - mock_branch_exists.assert_called_once_with("my-branch") - git_obj.mock_run_git.assert_called_once_with( - "checkout", "--track", "origin/my-branch", capture_output=False + git_obj.mock_run_git.assert_has_calls( + [ + call("checkout", "--track", "origin/my-branch", capture_output=False), + call("checkout", "main", capture_output=False), + ] ) def test_checkout_track_remote_existing_branch(mocker, git_obj): + mocker.patch.object(git_obj, "get_current_branch", return_value="main") mock_branch_exists = mocker.patch( "dev.release.git.Git.branch_exists", return_value=True ) mock_reset_hard = mocker.patch("dev.release.git.Git.reset_hard") - git_obj.checkout("my-branch", track_remote="origin") + with git_obj.checkout("my-branch", track_remote="origin"): + mock_branch_exists.assert_called_once_with("my-branch") + git_obj.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + mock_reset_hard.assert_called_once_with(reset_to="origin/my-branch") - mock_branch_exists.assert_called_once_with("my-branch") - git_obj.mock_run_git.assert_called_once_with( - "checkout", "my-branch", capture_output=False + git_obj.mock_run_git.assert_has_calls( + [ + call("checkout", "my-branch", capture_output=False), + call("checkout", "main", capture_output=False), + ] ) - mock_reset_hard.assert_called_once_with(reset_to="origin/my-branch") def test_fetch_default(git_obj):
diff --git a/tests/tools/private/release/prepare_test.py b/tests/tools/private/release/prepare_test.py index 62d2b62..655dba5 100644 --- a/tests/tools/private/release/prepare_test.py +++ b/tests/tools/private/release/prepare_test.py
@@ -1,4 +1,5 @@ import argparse +from unittest.mock import call from dev.release.gh import RELEASE_PREPARED_LABEL from dev.release.prepare import Prepare @@ -120,7 +121,13 @@ # Assert assert result == 0 - mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.checkout.assert_has_calls( + [ + call("prepare-2.0.0"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.commit.assert_not_called() mock_git.push.assert_called_once_with( "origin", "prepare-2.0.0", set_upstream=True, force=True @@ -150,7 +157,13 @@ # Assert assert result == 0 - mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.checkout.assert_has_calls( + [ + call("prepare-2.0.0"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.commit.assert_not_called() mock_git.push.assert_called_once_with( "origin", "prepare-2.0.0", set_upstream=True, force=True @@ -185,7 +198,13 @@ # Assert assert result == 0 - mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.checkout.assert_has_calls( + [ + call("prepare-2.0.0"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.commit.assert_not_called() mock_git.push.assert_called_once_with( "origin", "prepare-2.0.0", set_upstream=True, force=True
diff --git a/tests/tools/private/release/process_backports_test.py b/tests/tools/private/release/process_backports_test.py index 8733573..475ada4 100644 --- a/tests/tools/private/release/process_backports_test.py +++ b/tests/tools/private/release/process_backports_test.py
@@ -65,7 +65,13 @@ call("origin"), ] ) - mock_git.checkout.assert_called_once_with("release/2.0", track_remote="origin") + mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.cherry_pick.assert_called_once_with("abcdef12") mock_changelog.update_changelog.assert_called_once_with("2.0.0", "2026-07-01") mock_git.add_modified_and_deleted.assert_called_once() @@ -206,7 +212,13 @@ result = ProcessBackports(args, mock_git, mock_gh).run() assert result == 1 - mock_git.checkout.assert_called_once_with("release/2.0", track_remote="origin") + mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) mock_git.cherry_pick.assert_called_once_with("abcdef12") mock_git.cherry_pick_abort.assert_called_once()
diff --git a/tests/tools/private/release/sync_changelog_test.py b/tests/tools/private/release/sync_changelog_test.py index 2cdf202..d5b1b0e 100644 --- a/tests/tools/private/release/sync_changelog_test.py +++ b/tests/tools/private/release/sync_changelog_test.py
@@ -68,8 +68,11 @@ mock_git.checkout.assert_has_calls( [ call("main", track_remote="origin"), + call().__enter__(), call("sync-changelog-2.0.0-6affdae", create_branch=True), - call("main"), + call().__enter__(), + call().__exit__(None, None, None), + call().__exit__(None, None, None), ] ) mock_process_news_class.assert_called_once() @@ -160,8 +163,11 @@ mock_git.checkout.assert_has_calls( [ call("main", track_remote="origin"), + call().__enter__(), call("sync-changelog-2.0.0-6affdae"), - call("main"), + call().__enter__(), + call().__exit__(None, None, None), + call().__exit__(None, None, None), ] ) mock_git.reset_hard.assert_called_once_with(reset_to="main")