feat(push): re-sign and lease every mirror pushurl

pkgmgr push now signs unpushed commits like git-sign-push and pushes
each pushurl with its own --force-with-lease: the fetch URL against the
tracking ref, every mirror against its own fetched tip, each only after
git cherry shows no commit exists only there. --rewrite allows a
deliberate history rewrite. Without a repository selection it pushes
the current checkout, including worktrees outside the base dir.
pkgmgr release and release --retry sign before tagging and push the
branch and tag through the same path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-11-01 00:53:16 +01:00
1 parent 4a4bf137ac
commit 919e6a6434
8 files changed
+488 -70

No files matched your search

+4 -3
View File
@@ -10,7 +10,7 @@ the side effects downstream are incomplete.
`retry_release` re-runs the idempotent tail of that flow so a botched
release can be re-pushed without touching code or recreating tags:
* `git push origin <branch> <tag>` for the existing HEAD tag
* push the branch and the existing HEAD tag through `pkgmgr push`'s leases
* re-align the floating `latest` tag if HEAD tag is the highest
Publishing (PyPI etc.) stays the caller's responsibility — the publish
@@ -23,7 +23,8 @@ from __future__ import annotations
import os
from pkgmgr.actions.publish.git_tags import head_semver_tags
from pkgmgr.core.git import GitRunError, run
from pkgmgr.actions.repository.push import push_branch
from pkgmgr.core.git import GitRunError
from pkgmgr.core.git.queries import get_current_branch
from pkgmgr.core.version.semver import SemVer
@@ -50,7 +51,7 @@ def retry_release(
tag = max(tags, key=SemVer.parse)
print(f"Re-pushing existing tag: {tag}")
run(["push", "origin", branch, tag], preview=preview)
push_branch(".", [], refs=(tag,), preview=preview)
try:
if is_highest_version_tag(tag):
+6 -4
View File
@@ -4,7 +4,8 @@ import os
import sys
from pkgmgr.actions.branch import close_branch
from pkgmgr.core.git import GitRunError, run
from pkgmgr.actions.repository.push import push_branch, sign_unpushed
from pkgmgr.core.git import GitRunError
from pkgmgr.core.git.commands import add, commit, tag_annotated
from pkgmgr.core.git.queries import get_current_branch
from pkgmgr.core.repository.paths import resolve_repo_paths
@@ -136,8 +137,9 @@ def _release_impl(
if preview:
add(existing_files, preview=True)
commit(commit_msg, all=True, preview=True)
sign_unpushed(".", preview=True)
tag_annotated(new_tag, tag_msg, preview=True)
run(["push", "origin", branch, new_tag], preview=True)
push_branch(".", [], refs=(new_tag,), preview=True)
if is_highest_version_tag(new_tag):
update_latest_tag(new_tag, preview=True)
@@ -157,10 +159,10 @@ def _release_impl(
add(existing_files, preview=False)
commit(commit_msg, all=True, preview=False)
sign_unpushed(".", preview=False)
tag_annotated(new_tag, tag_msg, preview=False)
# Push branch and ONLY the newly created version tag in one command (no --tags)
run(["push", "origin", branch, new_tag], preview=False)
push_branch(".", [], refs=(new_tag,), preview=False)
# Update 'latest' only if this is the highest version tag
try:
+236 -10
View File
@@ -1,24 +1,249 @@
from __future__ import annotations
import os
import subprocess
import threading
from typing import Any
from pkgmgr.actions.repository._parallel import (
RepoRef,
resolve_repos,
run_on_repos,
)
from pkgmgr.core.git.commands import GitPushArgsError, push_args
from pkgmgr.core.git import GitRunError, run
Repository = dict[str, Any]
# Re-signing opens pinentry; parallel pushes must not ask for the passphrase at once.
_SIGN_LOCK = threading.Lock()
def _push_one(repo_dir: str, extra_args: list[str], preview: bool) -> tuple[bool, str]:
class PushRefusedError(RuntimeError):
"""Raised when a push would ship unsigned commits or overwrite a mirror."""
def _try_git(repo_dir: str, *args: str) -> str | None:
try:
push_args(extra_args, cwd=repo_dir, preview=preview)
return run(list(args), cwd=repo_dir)
except GitRunError:
return None
def _push_target(repo_dir: str) -> tuple[str, str]:
branch = run(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_dir)
if branch == "HEAD":
raise PushRefusedError(f"{repo_dir}: detached HEAD, nothing to push")
remote = (
_try_git(repo_dir, "config", "--get", f"branch.{branch}.pushRemote")
or _try_git(repo_dir, "config", "--get", "remote.pushDefault")
or _try_git(repo_dir, "config", "--get", f"branch.{branch}.remote")
or "origin"
)
return branch, remote
def _unpushed_signatures(repo_dir: str, remote: str) -> tuple[str, list[str]]:
for ref in ("@{u}", f"{remote}/main", f"{remote}/master"):
base = _try_git(repo_dir, "merge-base", "HEAD", ref)
if base:
return base, run(
["log", "--format=%G?", f"{base}..HEAD"], cwd=repo_dir
).split()
raise PushRefusedError(
f"{repo_dir}: no upstream and no {remote}/main or {remote}/master to sign against"
)
def sign_unpushed(repo_dir: str, *, preview: bool = False) -> None:
"""
GPG-sign every unpushed commit that is not signed yet, like git-sign-push.
Args:
repo_dir: checkout whose current branch is signed.
preview: print the fetch and rebase instead of running them.
"""
_branch, remote = _push_target(repo_dir)
run(["fetch", "--quiet", remote], cwd=repo_dir, preview=preview)
base, signatures = _unpushed_signatures(repo_dir, remote)
unsigned = sum(1 for s in signatures if s != "G")
if not unsigned:
return
if run(["status", "--porcelain", "--untracked-files=no"], cwd=repo_dir):
raise PushRefusedError(f"{repo_dir}: uncommitted changes, cannot re-sign")
cmd = ["git", "rebase", "--rebase-merges", "--force-rebase", "-S", base]
print(f"[push] Signing {unsigned} of {len(signatures)} commit(s) in {repo_dir}")
if preview:
print(f"[PREVIEW] Would run in {repo_dir!r}: {' '.join(cmd)}")
return
with _SIGN_LOCK:
result = subprocess.run(
cmd,
cwd=repo_dir,
env={**os.environ, "GIT_SEQUENCE_EDITOR": ":"},
check=False,
)
if result.returncode != 0:
raise PushRefusedError(
f"{repo_dir}: re-sign rebase failed; resolve or `git rebase --abort`"
)
def _lease(
repo_dir: str,
url: str,
branch_ref: str,
tracking: str | None,
keep: tuple[str, ...],
preview: bool,
) -> str:
if tracking is not None:
sha = _try_git(repo_dir, "rev-parse", "--verify", "--quiet", tracking) or ""
else:
listed = run(["ls-remote", url, branch_ref], cwd=repo_dir).splitlines()
sha = next(
(ln.split()[0] for ln in listed if ln.split()[1:] == [branch_ref]), ""
)
if sha and not preview:
run(["fetch", "--quiet", "--no-tags", url, branch_ref], cwd=repo_dir)
if not sha or preview:
return sha
lost: set[str] | None = None
for tip in keep:
plus = {
line[2:]
for line in run(["cherry", tip, sha], cwd=repo_dir).splitlines()
if line.startswith("+")
}
lost = plus if lost is None else lost & plus
if lost:
raise PushRefusedError(
f"{url}: {len(lost)} commit(s) exist only there, not overwritten: "
f"{' '.join(s[:10] for s in sorted(lost))}; "
"pass --rewrite for a deliberate history rewrite"
)
return sha
def push_branch(
repo_dir: str,
extra_args: list[str],
*,
refs: tuple[str, ...] = (),
rewrite: bool = False,
preview: bool = False,
) -> None:
"""
Push the current branch to every pushurl of its push remote, each with its own lease.
The fetch URL is leased against the remote-tracking ref, every other
pushurl against its own branch tip; each only after `git cherry` shows it
holds no commit the new tip lacks. A mirror failure is reported after the
others were pushed; a canonical failure stops before any mirror.
Args:
repo_dir: checkout whose current branch is pushed.
extra_args: flags appended to every `git push`.
refs: extra refs (e.g. a release tag) pushed to every URL.
rewrite: replace commits of the old canonical tip; the fetch URL skips
the cherry check, mirrors only refuse commits the old tip never had.
preview: print the pushes instead of running them.
"""
branch, remote = _push_target(repo_dir)
_base, signatures = _unpushed_signatures(repo_dir, remote)
if not preview and any(s != "G" for s in signatures):
raise PushRefusedError(
f"{repo_dir}: {sum(s != 'G' for s in signatures)} unsigned commit(s) "
"in the push range; run `pkgmgr push` to sign them"
)
head = run(["rev-parse", "HEAD"], cwd=repo_dir)
fetch_url = run(["remote", "get-url", remote], cwd=repo_dir)
push_urls = run(
["remote", "get-url", "--push", "--all", remote], cwd=repo_dir
).splitlines()
branch_ref = f"refs/heads/{branch}"
tracking = f"refs/remotes/{remote}/{branch}"
old_tip = _try_git(repo_dir, "rev-parse", "--verify", "--quiet", tracking)
if fetch_url not in push_urls:
print(
f"[push] WARN {remote}: fetch URL is no pushurl; every URL is leased "
"as a mirror, tracking ref and upstream stay untouched"
)
failed: list[str] = []
for url in sorted(push_urls, key=lambda u: u != fetch_url):
if url == fetch_url:
keep: tuple[str, ...] = () if rewrite else (head,)
else:
keep = (head, old_tip) if rewrite and old_tip else (head,)
try:
expect = _lease(
repo_dir,
url,
branch_ref,
tracking if url == fetch_url else None,
keep,
preview,
)
run(
[
"push",
"--follow-tags",
f"--force-with-lease={branch_ref}:{expect}",
*extra_args,
url,
branch_ref,
*refs,
],
cwd=repo_dir,
preview=preview,
)
except (GitRunError, PushRefusedError) as exc:
if url == fetch_url:
raise
print(f"[push] FAILED {url}\n{exc}")
failed.append(url)
continue
print(f"[push] {url} <- {branch} {head[:10]}")
if url == fetch_url and not preview:
run(["update-ref", tracking, head], cwd=repo_dir)
if not _try_git(repo_dir, "rev-parse", "--abbrev-ref", "@{u}"):
run(["branch", f"--set-upstream-to={remote}/{branch}"], cwd=repo_dir)
if failed:
raise PushRefusedError(f"{repo_dir}: push failed for {', '.join(failed)}")
def _push_one(
repo_dir: str, extra_args: list[str], rewrite: bool, preview: bool
) -> tuple[bool, str]:
try:
sign_unpushed(repo_dir, preview=preview)
push_branch(repo_dir, extra_args, rewrite=rewrite, preview=preview)
return (True, "")
except GitPushArgsError as exc:
except (GitRunError, PushRefusedError) as exc:
return (False, str(exc))
def push_repo_dirs(
repos: list[RepoRef],
extra_args: list[str],
preview: bool,
jobs: int = 1,
rewrite: bool = False,
) -> None:
"""
Sign and push each ``(identifier, repo_dir)``, optionally in parallel.
"""
run_on_repos(
repos,
lambda rd: _push_one(rd, extra_args, rewrite, preview),
jobs=jobs,
op_name="push",
)
def push_in_parallel(
selected_repos: list[Repository],
repositories_base_dir: str,
@@ -26,14 +251,15 @@ def push_in_parallel(
extra_args: list[str],
preview: bool,
jobs: int = 1,
rewrite: bool = False,
) -> None:
"""
Execute `git push` for each repository, optionally in parallel.
Sign and push each selected repository, optionally in parallel.
"""
repos = resolve_repos(selected_repos, repositories_base_dir, all_repos)
run_on_repos(
repos,
lambda rd: _push_one(rd, extra_args, preview),
push_repo_dirs(
resolve_repos(selected_repos, repositories_base_dir, all_repos),
extra_args,
preview,
jobs=jobs,
op_name="push",
rewrite=rewrite,
)
+28 -1
View File
@@ -8,8 +8,9 @@ from typing import Any
from pkgmgr.actions.proxy import exec_proxy_command
from pkgmgr.actions.repository.clone import clone_repos
from pkgmgr.actions.repository.pull import pull_with_verification
from pkgmgr.actions.repository.push import push_in_parallel
from pkgmgr.actions.repository.push import push_in_parallel, push_repo_dirs
from pkgmgr.cli.context import CLIContext
from pkgmgr.core.git import GitRunError, run
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.selected import get_selected_repos
@@ -185,6 +186,16 @@ def register_proxy_commands(
"(default: min(cpu_count, 8)). Use 1 for sequential."
),
)
if subcommand == "push":
parser.add_argument(
"--rewrite",
action="store_true",
default=False,
help=(
"Deliberate history rewrite: replace commits the remote "
"already has (amend, squash, drop); leases still apply."
),
)
if subcommand == "clone":
parser.add_argument(
"--clone-mode",
@@ -209,6 +220,21 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool:
if args.command not in all_proxy_subcommands:
return False
if args.command == "push" and not _proxy_has_explicit_selection(args):
try:
toplevel = run(["rev-parse", "--show-toplevel"])
except GitRunError:
print("[ERROR] Current directory is not inside a git checkout.")
sys.exit(1)
push_repo_dirs(
[(toplevel, toplevel)],
args.extra_args,
args.preview,
jobs=args.jobs,
rewrite=args.rewrite,
)
sys.exit(0)
# Default semantics: without explicit selection → repo of current folder.
if _proxy_has_explicit_selection(args):
selected = get_selected_repos(args, ctx.all_repositories)
@@ -252,6 +278,7 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool:
args.extra_args,
args.preview,
jobs=args.jobs,
rewrite=args.rewrite,
)
else:
exec_proxy_command(
-3
View File
@@ -19,7 +19,6 @@ from .pull import GitPullError, pull
from .pull_args import GitPullArgsError, pull_args
from .pull_ff_only import GitPullFfOnlyError, pull_ff_only
from .push import GitPushError, push
from .push_args import GitPushArgsError, push_args
from .push_upstream import GitPushUpstreamError, push_upstream
from .set_remote_url import GitSetRemoteUrlError, set_remote_url
from .tag_annotated import GitTagAnnotatedError, tag_annotated
@@ -43,7 +42,6 @@ __all__ = [
"GitPullArgsError",
"GitPullError",
"GitPullFfOnlyError",
"GitPushArgsError",
"GitPushError",
"GitPushUpstreamError",
"GitSetRemoteUrlError",
@@ -67,7 +65,6 @@ __all__ = [
"pull_args",
"pull_ff_only",
"push",
"push_args",
"push_upstream",
"set_remote_url",
"tag_annotated",
-37
View File
@@ -1,37 +0,0 @@
from __future__ import annotations
from ..errors import GitCommandError, GitRunError
from ..run import run
class GitPushArgsError(GitCommandError):
"""Raised when `git push` with arbitrary args fails."""
def push_args(
args: list[str] | None = None,
*,
cwd: str = ".",
preview: bool = False,
) -> None:
"""
Execute `git push` with caller-provided arguments.
Examples:
[] -> git push
["--force"] -> git push --force
["origin", "main"] -> git push origin main
["-u", "origin", "feature"] -> git push -u origin feature
"""
extra = args or []
try:
run(["push", *extra], cwd=cwd, preview=preview)
except GitRunError as exc:
details = getattr(exc, "output", None) or getattr(exc, "stderr", None) or ""
raise GitPushArgsError(
(
f"Failed to run `git push` with args={extra!r} "
f"in cwd={cwd!r}.\n{details}"
).rstrip(),
cwd=cwd,
) from exc
@@ -0,0 +1,206 @@
from __future__ import annotations
import io
import os
import shutil
import subprocess
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest.mock import patch
from pkgmgr.actions.release.retry import retry_release
from pkgmgr.actions.repository.push import (
PushRefusedError,
push_branch,
sign_unpushed,
)
from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.parser import create_parser
from pkgmgr.cli.proxy import maybe_handle_proxy
def git(cwd: str, *args: str) -> str:
return subprocess.run(
["git", *args], cwd=cwd, check=True, capture_output=True, text=True
).stdout.strip()
@unittest.skipUnless(shutil.which("ssh-keygen"), "ssh-keygen signs the commits")
class TestPushMirrorLeases(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
root = Path(os.path.realpath(self._tmp.name))
self.root = str(root)
key = root / "key"
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)],
check=True,
)
(root / "allowed").write_text(f"t@example.org {(root / 'key.pub').read_text()}")
(root / "gitconfig").write_text(
"[user]\n\tname = t\n\temail = t@example.org\n"
f"\tsigningkey = {key}\n"
"[gpg]\n\tformat = ssh\n"
f'[gpg "ssh"]\n\tallowedSignersFile = {root / "allowed"}\n'
"[commit]\n\tgpgsign = false\n"
"[init]\n\tdefaultBranch = main\n"
)
env = patch.dict(
os.environ,
{"GIT_CONFIG_GLOBAL": str(root / "gitconfig"), "GIT_CONFIG_NOSYSTEM": "1"},
)
env.start()
self.addCleanup(env.stop)
self.canonical = str(root / "canonical.git")
self.mirror = str(root / "mirror.git")
self.repo = str(root / "repo")
git(self.root, "init", "-q", "--bare", self.canonical)
git(self.root, "init", "-q", "--bare", self.mirror)
git(self.root, "init", "-q", self.repo)
git(self.repo, "remote", "add", "origin", self.canonical)
git(self.repo, "remote", "set-url", "--add", "--push", "origin", self.canonical)
git(self.repo, "remote", "set-url", "--add", "--push", "origin", self.mirror)
self.init = self._commit("init")
git(self.repo, "push", "-q", self.canonical, "main")
git(self.repo, "push", "-q", self.mirror, "main")
git(self.repo, "fetch", "-q", "origin")
git(self.repo, "branch", "-q", "--set-upstream-to=origin/main")
def _commit(self, name: str, cwd: str | None = None, *flags: str) -> str:
cwd = cwd or self.repo
Path(cwd, f"{name}.txt").write_text(name)
git(cwd, "add", f"{name}.txt")
git(cwd, "commit", "-q", *flags, "-m", name)
return git(cwd, "rev-parse", "HEAD")
def _push(self, rewrite: bool = False) -> str:
with redirect_stdout(io.StringIO()) as out:
sign_unpushed(self.repo)
push_branch(self.repo, [], rewrite=rewrite)
return out.getvalue()
def _tip(self, bare: str, ref: str = "refs/heads/main") -> str:
return subprocess.run(
["git", "rev-parse", "--verify", "--quiet", ref],
cwd=bare,
check=False,
capture_output=True,
text=True,
).stdout.strip()
def _assert_shipped_signed(self) -> None:
head = git(self.repo, "rev-parse", "HEAD")
self.assertEqual(self._tip(self.canonical), head)
self.assertEqual(self._tip(self.mirror), head)
signatures = git(self.repo, "log", "--format=%G?", f"{self.init}..HEAD")
self.assertEqual(set(signatures.split()), {"G"})
def test_mirror_equal_gets_the_signed_commit(self) -> None:
self._commit("feature")
self._push()
self._assert_shipped_signed()
def test_mirror_behind_is_fast_forwarded(self) -> None:
self._commit("first", None, "-S")
git(self.repo, "push", "-q", self.canonical, "main")
git(self.repo, "fetch", "-q", "origin")
self._commit("second")
self._push()
self._assert_shipped_signed()
def test_mirror_on_older_rewrite_is_replaced_after_resign(self) -> None:
unsigned = self._commit("feature")
git(self.repo, "push", "-q", self.mirror, "main")
self._push()
self._assert_shipped_signed()
self.assertNotEqual(self._tip(self.mirror), unsigned)
def test_mirror_only_commit_is_refused_and_kept(self) -> None:
other = os.path.join(self.root, "other")
git(self.root, "clone", "-q", self.mirror, other)
foreign = self._commit("foreign", other)
git(other, "push", "-q", "origin", "main")
self._commit("feature")
with self.assertRaises(PushRefusedError) as cm:
self._push()
with self.assertRaises(PushRefusedError):
self._push(rewrite=True)
self.assertIn(self.mirror, str(cm.exception))
self.assertEqual(self._tip(self.mirror), foreign)
self.assertEqual(self._tip(self.canonical), git(self.repo, "rev-parse", "HEAD"))
def test_content_rewrite_needs_the_rewrite_flag(self) -> None:
self._commit("feature")
self._push()
shipped = git(self.repo, "rev-parse", "HEAD")
Path(self.repo, "feature.txt").write_text("changed")
git(self.repo, "commit", "-q", "-a", "--amend", "--no-edit")
with self.assertRaises(PushRefusedError) as cm:
self._push()
self.assertIn(self.canonical, str(cm.exception))
self.assertEqual(self._tip(self.canonical), shipped)
self.assertEqual(self._tip(self.mirror), shipped)
self._push(rewrite=True)
self._assert_shipped_signed()
self.assertNotEqual(self._tip(self.mirror), shipped)
def test_worktree_outside_base_dir_is_pushed(self) -> None:
worktree = os.path.join(self.root, "wt")
git(self.repo, "worktree", "add", "-q", "-b", "feature", worktree)
head_before = self._commit("feature", worktree)
ctx = CLIContext(
config_merged={},
repositories_base_dir=os.path.join(self.root, "base"),
all_repositories=[],
binaries_dir=os.path.join(self.root, "bin"),
user_config_path=os.path.join(self.root, "config.yaml"),
)
args = create_parser("pkgmgr").parse_args(["push", "-j", "1"])
cwd = os.getcwd()
os.chdir(worktree)
self.addCleanup(os.chdir, cwd)
with redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as cm:
maybe_handle_proxy(args, ctx)
self.assertEqual(cm.exception.code, 0)
head = git(worktree, "rev-parse", "HEAD")
self.assertNotEqual(head, head_before)
self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head)
self.assertEqual(self._tip(self.mirror, "refs/heads/feature"), head)
self.assertEqual(
git(worktree, "rev-parse", "--abbrev-ref", "@{u}"), "origin/feature"
)
def test_release_retry_refuses_unsigned_range(self) -> None:
self._commit("release")
git(self.repo, "tag", "-a", "v1.0.0", "-m", "v1.0.0")
cwd = os.getcwd()
os.chdir(self.repo)
self.addCleanup(os.chdir, cwd)
with redirect_stdout(io.StringIO()), self.assertRaises(PushRefusedError):
retry_release(pyproject_path=os.path.join(self.repo, "pyproject.toml"))
self.assertEqual(self._tip(self.canonical), git(self.repo, "rev-parse", "@{u}"))
self.assertEqual(self._tip(self.canonical, "refs/tags/v1.0.0"), "")
if __name__ == "__main__":
unittest.main()
@@ -65,7 +65,7 @@ class TestReleaseRetry(unittest.TestCase):
class TestRetryRelease(unittest.TestCase):
@patch("pkgmgr.actions.release.retry.update_latest_tag")
@patch("pkgmgr.actions.release.retry.is_highest_version_tag", return_value=True)
@patch("pkgmgr.actions.release.retry.run")
@patch("pkgmgr.actions.release.retry.push_branch")
@patch("pkgmgr.actions.release.retry.head_semver_tags")
@patch(
"pkgmgr.actions.release.retry.get_current_branch",
@@ -75,7 +75,7 @@ class TestRetryRelease(unittest.TestCase):
self,
_mock_branch,
mock_head_tags,
mock_run,
mock_push,
_mock_highest,
mock_update_latest,
) -> None:
@@ -83,14 +83,12 @@ class TestRetryRelease(unittest.TestCase):
retry_release(pyproject_path="pyproject.toml", preview=False)
mock_run.assert_called_once_with(
["push", "origin", "release-fix", "v1.13.4"], preview=False
)
mock_push.assert_called_once_with(".", [], refs=("v1.13.4",), preview=False)
mock_update_latest.assert_called_once_with("v1.13.4", preview=False)
@patch("pkgmgr.actions.release.retry.update_latest_tag")
@patch("pkgmgr.actions.release.retry.is_highest_version_tag", return_value=False)
@patch("pkgmgr.actions.release.retry.run")
@patch("pkgmgr.actions.release.retry.push_branch")
@patch("pkgmgr.actions.release.retry.head_semver_tags")
@patch(
"pkgmgr.actions.release.retry.get_current_branch",
@@ -100,7 +98,7 @@ class TestRetryRelease(unittest.TestCase):
self,
_mock_branch,
mock_head_tags,
_mock_run,
_mock_push,
_mock_highest,
mock_update_latest,
) -> None:
@@ -127,7 +125,7 @@ class TestRetryRelease(unittest.TestCase):
@patch("pkgmgr.actions.release.retry.update_latest_tag")
@patch("pkgmgr.actions.release.retry.is_highest_version_tag", return_value=True)
@patch("pkgmgr.actions.release.retry.run")
@patch("pkgmgr.actions.release.retry.push_branch")
@patch("pkgmgr.actions.release.retry.head_semver_tags")
@patch(
"pkgmgr.actions.release.retry.get_current_branch",
@@ -137,7 +135,7 @@ class TestRetryRelease(unittest.TestCase):
self,
_mock_branch,
mock_head_tags,
mock_run,
mock_push,
_mock_highest,
_mock_update_latest,
) -> None:
@@ -145,9 +143,7 @@ class TestRetryRelease(unittest.TestCase):
retry_release(pyproject_path="pyproject.toml", preview=False)
mock_run.assert_called_once_with(
["push", "origin", "main", "v2.0.0"], preview=False
)
mock_push.assert_called_once_with(".", [], refs=("v2.0.0",), preview=False)
if __name__ == "__main__":