feat(release): add retry mode to re-deploy existing release without re-tagging
Recovers from a release whose tag+commit landed cleanly but whose post-tag steps (git push, latest-tag bump, twine upload) failed mid-flight. pkgmgr release --retry skips the version bump, file rewrites, commit, and tag-creation steps and re-runs only the idempotent tail: re-push the existing HEAD tag, re-align the floating latest tag, and (unless --no-publish) re-invoke publish. The retry logic lives in its own module pkgmgr.actions.release.retry so the workflow.py orchestrator stays focused on the forward path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6c40451fe
commit
43fbcfb227
5 files changed
+259
-3
No files matched your search
@@ -0,0 +1,67 @@
|
||||
"""Re-deploy an existing release without modifying files or creating a new tag.
|
||||
|
||||
The release workflow normally bumps versions, rewrites packaging
|
||||
manifests, commits, tags, pushes, and uploads to PyPI in one shot.
|
||||
When a post-tag step fails mid-flight (typical examples: `git push`
|
||||
rejected, `twine upload` aborted by a broken venv, `update-latest`
|
||||
rejected by branch protection) the local tag still exists on HEAD but
|
||||
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
|
||||
* re-align the floating `latest` tag if HEAD tag is the highest
|
||||
|
||||
Publishing (PyPI etc.) stays the caller's responsibility — the publish
|
||||
workflow is already idempotent (twine rejects duplicates per spec) and
|
||||
can be invoked independently via the `publish` subcommand.
|
||||
"""
|
||||
|
||||
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.core.git.queries import get_current_branch
|
||||
from pkgmgr.core.version.semver import SemVer
|
||||
|
||||
from .git_ops import is_highest_version_tag, update_latest_tag
|
||||
|
||||
|
||||
def retry_release(
|
||||
pyproject_path: str = "pyproject.toml",
|
||||
preview: bool = False,
|
||||
) -> None:
|
||||
"""Re-push the HEAD release without re-tagging or modifying any files."""
|
||||
try:
|
||||
branch = get_current_branch() or "main"
|
||||
except GitRunError:
|
||||
branch = "main"
|
||||
print(f"Retrying release push on branch: {branch}")
|
||||
|
||||
tags = head_semver_tags(cwd=os.path.dirname(os.path.abspath(pyproject_path)))
|
||||
if not tags:
|
||||
raise RuntimeError(
|
||||
"No version tag on HEAD. Nothing to retry — "
|
||||
"run `pkgmgr release <type>` first to create a release."
|
||||
)
|
||||
tag = max(tags, key=SemVer.parse)
|
||||
print(f"Re-pushing existing tag: {tag}")
|
||||
|
||||
run(["push", "origin", branch, tag], preview=preview)
|
||||
|
||||
try:
|
||||
if is_highest_version_tag(tag):
|
||||
update_latest_tag(tag, preview=preview)
|
||||
else:
|
||||
print(f"[INFO] Skipping 'latest' update (tag {tag} is not the highest).")
|
||||
except GitRunError as exc:
|
||||
print(f"[WARN] Failed to update floating 'latest' tag for {tag}: {exc}")
|
||||
|
||||
if preview:
|
||||
print(f"[PREVIEW] Retry push for {tag} would now complete.")
|
||||
return
|
||||
|
||||
print(f"Retry push completed for {tag}.")
|
||||
@@ -26,6 +26,7 @@ from .git_ops import (
|
||||
)
|
||||
from .package_name import resolve_package_name
|
||||
from .prompts import confirm_proceed_release, should_delete_branch
|
||||
from .retry import retry_release
|
||||
from .versioning import bump_semver, determine_current_version
|
||||
|
||||
|
||||
@@ -199,7 +200,12 @@ def release(
|
||||
preview: bool = False,
|
||||
force: bool = False,
|
||||
close: bool = False,
|
||||
retry: bool = False,
|
||||
) -> None:
|
||||
if retry:
|
||||
retry_release(pyproject_path=pyproject_path, preview=preview)
|
||||
return
|
||||
|
||||
if preview:
|
||||
_release_impl(
|
||||
pyproject_path=pyproject_path,
|
||||
|
||||
@@ -49,7 +49,19 @@ def handle_release(
|
||||
print(f"[WARN] Skipping repository {identifier}: directory missing.")
|
||||
continue
|
||||
|
||||
print(f"[pkgmgr] Running release for repository {identifier}...")
|
||||
retry = bool(getattr(args, "retry", False))
|
||||
if retry and args.release_type:
|
||||
print(
|
||||
f"[WARN] Ignoring release_type '{args.release_type}' for {identifier} — --retry skips version bumps."
|
||||
)
|
||||
if not retry and not args.release_type:
|
||||
print(
|
||||
f"[WARN] Skipping {identifier}: release_type is required unless --retry is set."
|
||||
)
|
||||
continue
|
||||
|
||||
action_label = "retry-release" if retry else "release"
|
||||
print(f"[pkgmgr] Running {action_label} for repository {identifier}...")
|
||||
|
||||
cwd_before = os.getcwd()
|
||||
try:
|
||||
@@ -58,11 +70,12 @@ def handle_release(
|
||||
run_release(
|
||||
pyproject_path="pyproject.toml",
|
||||
changelog_path="CHANGELOG.md",
|
||||
release_type=args.release_type,
|
||||
release_type=args.release_type or "patch",
|
||||
message=args.message or None,
|
||||
preview=getattr(args, "preview", False),
|
||||
force=getattr(args, "force", False),
|
||||
close=getattr(args, "close", False),
|
||||
retry=retry,
|
||||
)
|
||||
|
||||
if not getattr(args, "no_publish", False):
|
||||
|
||||
@@ -24,8 +24,12 @@ def add_release_subparser(
|
||||
|
||||
release_parser.add_argument(
|
||||
"release_type",
|
||||
nargs="?",
|
||||
choices=["major", "minor", "patch"],
|
||||
help="Type of version increment for the release (major, minor, patch).",
|
||||
help=(
|
||||
"Type of version increment for the release (major, minor, patch). "
|
||||
"Omit when `--retry` is set."
|
||||
),
|
||||
)
|
||||
|
||||
release_parser.add_argument(
|
||||
@@ -61,3 +65,15 @@ def add_release_subparser(
|
||||
action="store_true",
|
||||
help="Do not run publish automatically after a successful release.",
|
||||
)
|
||||
|
||||
release_parser.add_argument(
|
||||
"--retry",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Re-deploy the existing HEAD release without re-tagging or "
|
||||
"modifying any files: re-push the existing version tag, "
|
||||
"re-align the floating `latest` tag, and (unless --no-publish) "
|
||||
"re-run publish. Use this to recover from a release whose "
|
||||
"post-tag push or PyPI upload failed mid-flight."
|
||||
),
|
||||
)
|
||||
Reference in new issue
Block a user