feat(push): add --no-sign to push unsigned commits
pkgmgr push --no-sign skips the re-sign rebase and the unsigned-range refusal, so a checkout without a usable signing key can still sync the fetch URL and every mirror pushurl. Leases and the git cherry guard still apply. push_branch takes sign as a required keyword; pkgmgr release and release --retry pass sign=True, so releases stay signed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
944b4cb663
commit
f40cc5cfde
6 files changed
+67
-30
No files matched your search
@@ -51,7 +51,7 @@ def retry_release(
|
|||||||
tag = max(tags, key=SemVer.parse)
|
tag = max(tags, key=SemVer.parse)
|
||||||
print(f"Re-pushing existing tag: {tag}")
|
print(f"Re-pushing existing tag: {tag}")
|
||||||
|
|
||||||
push_branch(".", [], refs=(tag,), preview=preview)
|
push_branch(".", [], sign=True, refs=(tag,), preview=preview)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_highest_version_tag(tag):
|
if is_highest_version_tag(tag):
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ def _release_impl(
|
|||||||
commit(commit_msg, all=True, preview=True)
|
commit(commit_msg, all=True, preview=True)
|
||||||
sign_unpushed(".", preview=True)
|
sign_unpushed(".", preview=True)
|
||||||
tag_annotated(new_tag, tag_msg, preview=True)
|
tag_annotated(new_tag, tag_msg, preview=True)
|
||||||
push_branch(".", [], refs=(new_tag,), preview=True)
|
push_branch(".", [], sign=True, refs=(new_tag,), preview=True)
|
||||||
|
|
||||||
if is_highest_version_tag(new_tag):
|
if is_highest_version_tag(new_tag):
|
||||||
update_latest_tag(new_tag, preview=True)
|
update_latest_tag(new_tag, preview=True)
|
||||||
@@ -162,7 +162,7 @@ def _release_impl(
|
|||||||
sign_unpushed(".", preview=False)
|
sign_unpushed(".", preview=False)
|
||||||
tag_annotated(new_tag, tag_msg, preview=False)
|
tag_annotated(new_tag, tag_msg, preview=False)
|
||||||
|
|
||||||
push_branch(".", [], refs=(new_tag,), preview=False)
|
push_branch(".", [], sign=True, refs=(new_tag,), preview=False)
|
||||||
|
|
||||||
# Update 'latest' only if this is the highest version tag
|
# Update 'latest' only if this is the highest version tag
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ def push_branch(
|
|||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
extra_args: list[str],
|
extra_args: list[str],
|
||||||
*,
|
*,
|
||||||
|
sign: bool,
|
||||||
refs: tuple[str, ...] = (),
|
refs: tuple[str, ...] = (),
|
||||||
rewrite: bool = False,
|
rewrite: bool = False,
|
||||||
preview: bool = False,
|
preview: bool = False,
|
||||||
@@ -144,18 +145,20 @@ def push_branch(
|
|||||||
Args:
|
Args:
|
||||||
repo_dir: checkout whose current branch is pushed.
|
repo_dir: checkout whose current branch is pushed.
|
||||||
extra_args: flags appended to every `git push`.
|
extra_args: flags appended to every `git push`.
|
||||||
|
sign: refuse unsigned commits in the push range; False pushes them as they are.
|
||||||
refs: extra refs (e.g. a release tag) pushed to every URL.
|
refs: extra refs (e.g. a release tag) pushed to every URL.
|
||||||
rewrite: replace commits of the old canonical tip; the fetch URL skips
|
rewrite: replace commits of the old canonical tip; the fetch URL skips
|
||||||
the cherry check, mirrors only refuse commits the old tip never had.
|
the cherry check, mirrors only refuse commits the old tip never had.
|
||||||
preview: print the pushes instead of running them.
|
preview: print the pushes instead of running them.
|
||||||
"""
|
"""
|
||||||
branch, remote = _push_target(repo_dir)
|
branch, remote = _push_target(repo_dir)
|
||||||
_base, signatures = _unpushed_signatures(repo_dir, remote)
|
if sign:
|
||||||
if not preview and any(s != "G" for s in signatures):
|
_base, signatures = _unpushed_signatures(repo_dir, remote)
|
||||||
raise PushRefusedError(
|
if not preview and any(s != "G" for s in signatures):
|
||||||
f"{repo_dir}: {sum(s != 'G' for s in signatures)} unsigned commit(s) "
|
raise PushRefusedError(
|
||||||
"in the push range; run `pkgmgr push` to sign them"
|
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)
|
head = run(["rev-parse", "HEAD"], cwd=repo_dir)
|
||||||
fetch_url = run(["remote", "get-url", remote], cwd=repo_dir)
|
fetch_url = run(["remote", "get-url", remote], cwd=repo_dir)
|
||||||
@@ -216,11 +219,12 @@ def push_branch(
|
|||||||
|
|
||||||
|
|
||||||
def _push_one(
|
def _push_one(
|
||||||
repo_dir: str, extra_args: list[str], rewrite: bool, preview: bool
|
repo_dir: str, extra_args: list[str], sign: bool, rewrite: bool, preview: bool
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
try:
|
try:
|
||||||
sign_unpushed(repo_dir, preview=preview)
|
if sign:
|
||||||
push_branch(repo_dir, extra_args, rewrite=rewrite, preview=preview)
|
sign_unpushed(repo_dir, preview=preview)
|
||||||
|
push_branch(repo_dir, extra_args, sign=sign, rewrite=rewrite, preview=preview)
|
||||||
return (True, "")
|
return (True, "")
|
||||||
except (GitRunError, PushRefusedError) as exc:
|
except (GitRunError, PushRefusedError) as exc:
|
||||||
return (False, str(exc))
|
return (False, str(exc))
|
||||||
@@ -230,6 +234,7 @@ def push_repo_dirs(
|
|||||||
repos: list[RepoRef],
|
repos: list[RepoRef],
|
||||||
extra_args: list[str],
|
extra_args: list[str],
|
||||||
preview: bool,
|
preview: bool,
|
||||||
|
sign: bool,
|
||||||
jobs: int = 1,
|
jobs: int = 1,
|
||||||
rewrite: bool = False,
|
rewrite: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -238,7 +243,7 @@ def push_repo_dirs(
|
|||||||
"""
|
"""
|
||||||
run_on_repos(
|
run_on_repos(
|
||||||
repos,
|
repos,
|
||||||
lambda rd: _push_one(rd, extra_args, rewrite, preview),
|
lambda rd: _push_one(rd, extra_args, sign, rewrite, preview),
|
||||||
jobs=jobs,
|
jobs=jobs,
|
||||||
op_name="push",
|
op_name="push",
|
||||||
)
|
)
|
||||||
@@ -250,6 +255,7 @@ def push_in_parallel(
|
|||||||
all_repos: list[Repository],
|
all_repos: list[Repository],
|
||||||
extra_args: list[str],
|
extra_args: list[str],
|
||||||
preview: bool,
|
preview: bool,
|
||||||
|
sign: bool,
|
||||||
jobs: int = 1,
|
jobs: int = 1,
|
||||||
rewrite: bool = False,
|
rewrite: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -260,6 +266,7 @@ def push_in_parallel(
|
|||||||
resolve_repos(selected_repos, repositories_base_dir, all_repos),
|
resolve_repos(selected_repos, repositories_base_dir, all_repos),
|
||||||
extra_args,
|
extra_args,
|
||||||
preview,
|
preview,
|
||||||
|
sign,
|
||||||
jobs=jobs,
|
jobs=jobs,
|
||||||
rewrite=rewrite,
|
rewrite=rewrite,
|
||||||
)
|
)
|
||||||
@@ -196,6 +196,15 @@ def register_proxy_commands(
|
|||||||
"already has (amend, squash, drop); leases still apply."
|
"already has (amend, squash, drop); leases still apply."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-sign",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Push unsigned commits as they are instead of re-signing "
|
||||||
|
"them; leases still apply."
|
||||||
|
),
|
||||||
|
)
|
||||||
if subcommand == "clone":
|
if subcommand == "clone":
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--clone-mode",
|
"--clone-mode",
|
||||||
@@ -230,6 +239,7 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool:
|
|||||||
[(toplevel, toplevel)],
|
[(toplevel, toplevel)],
|
||||||
args.extra_args,
|
args.extra_args,
|
||||||
args.preview,
|
args.preview,
|
||||||
|
not args.no_sign,
|
||||||
jobs=args.jobs,
|
jobs=args.jobs,
|
||||||
rewrite=args.rewrite,
|
rewrite=args.rewrite,
|
||||||
)
|
)
|
||||||
@@ -277,6 +287,7 @@ def maybe_handle_proxy(args: argparse.Namespace, ctx: CLIContext) -> bool:
|
|||||||
ctx.all_repositories,
|
ctx.all_repositories,
|
||||||
args.extra_args,
|
args.extra_args,
|
||||||
args.preview,
|
args.preview,
|
||||||
|
not args.no_sign,
|
||||||
jobs=args.jobs,
|
jobs=args.jobs,
|
||||||
rewrite=args.rewrite,
|
rewrite=args.rewrite,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,9 +80,27 @@ class TestPushMirrorLeases(unittest.TestCase):
|
|||||||
def _push(self, rewrite: bool = False) -> str:
|
def _push(self, rewrite: bool = False) -> str:
|
||||||
with redirect_stdout(io.StringIO()) as out:
|
with redirect_stdout(io.StringIO()) as out:
|
||||||
sign_unpushed(self.repo)
|
sign_unpushed(self.repo)
|
||||||
push_branch(self.repo, [], rewrite=rewrite)
|
push_branch(self.repo, [], sign=True, rewrite=rewrite)
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|
||||||
|
def _cli_push(self, cwd: str, *argv: str) -> None:
|
||||||
|
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", *argv])
|
||||||
|
prev = os.getcwd()
|
||||||
|
os.chdir(cwd)
|
||||||
|
self.addCleanup(os.chdir, prev)
|
||||||
|
|
||||||
|
with redirect_stdout(io.StringIO()), self.assertRaises(SystemExit) as cm:
|
||||||
|
maybe_handle_proxy(args, ctx)
|
||||||
|
|
||||||
|
self.assertEqual(cm.exception.code, 0)
|
||||||
|
|
||||||
def _tip(self, bare: str, ref: str = "refs/heads/main") -> str:
|
def _tip(self, bare: str, ref: str = "refs/heads/main") -> str:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["git", "rev-parse", "--verify", "--quiet", ref],
|
["git", "rev-parse", "--verify", "--quiet", ref],
|
||||||
@@ -164,22 +182,9 @@ class TestPushMirrorLeases(unittest.TestCase):
|
|||||||
worktree = os.path.join(self.root, "wt")
|
worktree = os.path.join(self.root, "wt")
|
||||||
git(self.repo, "worktree", "add", "-q", "-b", "feature", worktree)
|
git(self.repo, "worktree", "add", "-q", "-b", "feature", worktree)
|
||||||
head_before = self._commit("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:
|
self._cli_push(worktree)
|
||||||
maybe_handle_proxy(args, ctx)
|
|
||||||
|
|
||||||
self.assertEqual(cm.exception.code, 0)
|
|
||||||
head = git(worktree, "rev-parse", "HEAD")
|
head = git(worktree, "rev-parse", "HEAD")
|
||||||
self.assertNotEqual(head, head_before)
|
self.assertNotEqual(head, head_before)
|
||||||
self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head)
|
self.assertEqual(self._tip(self.canonical, "refs/heads/feature"), head)
|
||||||
@@ -188,6 +193,16 @@ class TestPushMirrorLeases(unittest.TestCase):
|
|||||||
git(worktree, "rev-parse", "--abbrev-ref", "@{u}"), "origin/feature"
|
git(worktree, "rev-parse", "--abbrev-ref", "@{u}"), "origin/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_no_sign_pushes_unsigned_commits_to_every_url(self) -> None:
|
||||||
|
unsigned = self._commit("feature")
|
||||||
|
|
||||||
|
self._cli_push(self.repo, "--no-sign")
|
||||||
|
|
||||||
|
self.assertEqual(git(self.repo, "rev-parse", "HEAD"), unsigned)
|
||||||
|
self.assertEqual(self._tip(self.canonical), unsigned)
|
||||||
|
self.assertEqual(self._tip(self.mirror), unsigned)
|
||||||
|
self.assertEqual(git(self.repo, "log", "-1", "--format=%G?"), "N")
|
||||||
|
|
||||||
def test_release_retry_refuses_unsigned_range(self) -> None:
|
def test_release_retry_refuses_unsigned_range(self) -> None:
|
||||||
self._commit("release")
|
self._commit("release")
|
||||||
git(self.repo, "tag", "-a", "v1.0.0", "-m", "v1.0.0")
|
git(self.repo, "tag", "-a", "v1.0.0", "-m", "v1.0.0")
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ class TestRetryRelease(unittest.TestCase):
|
|||||||
|
|
||||||
retry_release(pyproject_path="pyproject.toml", preview=False)
|
retry_release(pyproject_path="pyproject.toml", preview=False)
|
||||||
|
|
||||||
mock_push.assert_called_once_with(".", [], refs=("v1.13.4",), preview=False)
|
mock_push.assert_called_once_with(
|
||||||
|
".", [], sign=True, refs=("v1.13.4",), preview=False
|
||||||
|
)
|
||||||
mock_update_latest.assert_called_once_with("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.update_latest_tag")
|
||||||
@@ -143,7 +145,9 @@ class TestRetryRelease(unittest.TestCase):
|
|||||||
|
|
||||||
retry_release(pyproject_path="pyproject.toml", preview=False)
|
retry_release(pyproject_path="pyproject.toml", preview=False)
|
||||||
|
|
||||||
mock_push.assert_called_once_with(".", [], refs=("v2.0.0",), preview=False)
|
mock_push.assert_called_once_with(
|
||||||
|
".", [], sign=True, refs=("v2.0.0",), preview=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user