feat(release): auto-run publish after release with --no-publish opt-out
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / linter-shell (push) Has been cancelled
Mark stable commit / linter-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / linter-shell (push) Has been cancelled
Mark stable commit / linter-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Run publish automatically after successful release - Add --no-publish flag to disable auto-publish - Respect TTY for interactive/credential prompts - Harden repo directory resolution - Add integration and unit tests for release→publish hook https://chatgpt.com/share/693f335b-b820-800f-8666-68355f3c938f
This commit is contained in:
@@ -1,31 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Release command wiring for the pkgmgr CLI.
|
||||
|
||||
This module implements the `pkgmgr release` subcommand on top of the
|
||||
generic selection logic from cli.dispatch. It does not define its
|
||||
own subparser; the CLI surface is configured in cli.parser.
|
||||
|
||||
Responsibilities:
|
||||
- Take the parsed argparse.Namespace for the `release` command.
|
||||
- Use the list of selected repositories provided by dispatch_command().
|
||||
- Optionally list affected repositories when --list is set.
|
||||
- For each selected repository, run pkgmgr.actions.release.release(...) in
|
||||
the context of that repository directory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pkgmgr.actions.publish import publish as run_publish
|
||||
from pkgmgr.actions.release import release as run_release
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
from pkgmgr.actions.release import release as run_release
|
||||
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
@@ -35,23 +21,10 @@ def handle_release(
|
||||
ctx: CLIContext,
|
||||
selected: List[Repository],
|
||||
) -> None:
|
||||
"""
|
||||
Handle the `pkgmgr release` subcommand.
|
||||
|
||||
Flow:
|
||||
1) Use the `selected` repositories as computed by dispatch_command().
|
||||
2) If --list is given, print the identifiers of the selected repos
|
||||
and return without running any release.
|
||||
3) For each selected repository:
|
||||
- Resolve its identifier and local directory.
|
||||
- Change into that directory.
|
||||
- Call pkgmgr.actions.release.release(...) with the parsed options.
|
||||
"""
|
||||
if not selected:
|
||||
print("[pkgmgr] No repositories selected for release.")
|
||||
return
|
||||
|
||||
# List-only mode: show which repositories would be affected.
|
||||
if getattr(args, "list", False):
|
||||
print("[pkgmgr] Repositories that would be affected by this release:")
|
||||
for repo in selected:
|
||||
@@ -62,29 +35,22 @@ def handle_release(
|
||||
for repo in selected:
|
||||
identifier = get_repo_identifier(repo, ctx.all_repositories)
|
||||
|
||||
repo_dir = repo.get("directory")
|
||||
if not repo_dir:
|
||||
try:
|
||||
repo_dir = get_repo_dir(ctx.repositories_base_dir, repo)
|
||||
except Exception:
|
||||
repo_dir = None
|
||||
|
||||
if not repo_dir or not os.path.isdir(repo_dir):
|
||||
print(
|
||||
f"[WARN] Skipping repository {identifier}: "
|
||||
"local directory does not exist."
|
||||
)
|
||||
try:
|
||||
repo_dir = repo.get("directory") or get_repo_dir(ctx.repositories_base_dir, repo)
|
||||
except Exception as exc:
|
||||
print(f"[WARN] Skipping repository {identifier}: failed to resolve directory: {exc}")
|
||||
continue
|
||||
|
||||
print(
|
||||
f"[pkgmgr] Running release for repository {identifier} "
|
||||
f"in '{repo_dir}'..."
|
||||
)
|
||||
if not os.path.isdir(repo_dir):
|
||||
print(f"[WARN] Skipping repository {identifier}: directory missing.")
|
||||
continue
|
||||
|
||||
print(f"[pkgmgr] Running release for repository {identifier}...")
|
||||
|
||||
# Change to repo directory and invoke the helper.
|
||||
cwd_before = os.getcwd()
|
||||
try:
|
||||
os.chdir(repo_dir)
|
||||
|
||||
run_release(
|
||||
pyproject_path="pyproject.toml",
|
||||
changelog_path="CHANGELOG.md",
|
||||
@@ -94,5 +60,17 @@ def handle_release(
|
||||
force=getattr(args, "force", False),
|
||||
close=getattr(args, "close", False),
|
||||
)
|
||||
|
||||
if not getattr(args, "no_publish", False):
|
||||
print(f"[pkgmgr] Running publish for repository {identifier}...")
|
||||
is_tty = sys.stdin.isatty()
|
||||
run_publish(
|
||||
repo=repo,
|
||||
repo_dir=repo_dir,
|
||||
preview=getattr(args, "preview", False),
|
||||
interactive=is_tty,
|
||||
allow_prompt=is_tty,
|
||||
)
|
||||
|
||||
finally:
|
||||
os.chdir(cwd_before)
|
||||
|
||||
@@ -21,22 +21,22 @@ def add_release_subparser(
|
||||
"and updating the changelog."
|
||||
),
|
||||
)
|
||||
|
||||
release_parser.add_argument(
|
||||
"release_type",
|
||||
choices=["major", "minor", "patch"],
|
||||
help="Type of version increment for the release (major, minor, patch).",
|
||||
)
|
||||
|
||||
release_parser.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
help=(
|
||||
"Optional release message to add to the changelog and tag."
|
||||
),
|
||||
help="Optional release message to add to the changelog and tag.",
|
||||
)
|
||||
# Generic selection / preview / list / extra_args
|
||||
|
||||
add_identifier_arguments(release_parser)
|
||||
# Close current branch after successful release
|
||||
|
||||
release_parser.add_argument(
|
||||
"--close",
|
||||
action="store_true",
|
||||
@@ -45,7 +45,7 @@ def add_release_subparser(
|
||||
"repository, if it is not main/master."
|
||||
),
|
||||
)
|
||||
# Force: skip preview+confirmation and run release directly
|
||||
|
||||
release_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
@@ -55,3 +55,9 @@ def add_release_subparser(
|
||||
"release directly."
|
||||
),
|
||||
)
|
||||
|
||||
release_parser.add_argument(
|
||||
"--no-publish",
|
||||
action="store_true",
|
||||
help="Do not run publish automatically after a successful release.",
|
||||
)
|
||||
|
||||
66
tests/integration/test_release_publish_hook.py
Normal file
66
tests/integration/test_release_publish_hook.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestIntegrationReleasePublishHook(unittest.TestCase):
|
||||
def _ctx(self) -> SimpleNamespace:
|
||||
# Minimal CLIContext shape used by handle_release()
|
||||
return SimpleNamespace(
|
||||
repositories_base_dir="/tmp",
|
||||
all_repositories=[],
|
||||
)
|
||||
|
||||
def _parse(self, argv: list[str]):
|
||||
from pkgmgr.cli.parser import create_parser
|
||||
|
||||
parser = create_parser("pkgmgr test")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
def test_release_runs_publish_by_default_and_respects_tty(self) -> None:
|
||||
from pkgmgr.cli.commands.release import handle_release
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
selected = [{"directory": td}]
|
||||
|
||||
# Go through real parser to ensure CLI surface is wired correctly
|
||||
args = self._parse(["release", "patch"])
|
||||
|
||||
with patch("pkgmgr.cli.commands.release.run_release") as m_release, patch(
|
||||
"pkgmgr.cli.commands.release.run_publish"
|
||||
) as m_publish, patch(
|
||||
"pkgmgr.cli.commands.release.sys.stdin.isatty", return_value=False
|
||||
):
|
||||
handle_release(args=args, ctx=self._ctx(), selected=selected)
|
||||
|
||||
m_release.assert_called_once()
|
||||
m_publish.assert_called_once()
|
||||
|
||||
_, kwargs = m_publish.call_args
|
||||
self.assertEqual(kwargs["repo"], selected[0])
|
||||
self.assertEqual(kwargs["repo_dir"], td)
|
||||
self.assertFalse(kwargs["interactive"])
|
||||
self.assertFalse(kwargs["allow_prompt"])
|
||||
|
||||
def test_release_skips_publish_when_no_publish_flag_set(self) -> None:
|
||||
from pkgmgr.cli.commands.release import handle_release
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
selected = [{"directory": td}]
|
||||
|
||||
args = self._parse(["release", "patch", "--no-publish"])
|
||||
|
||||
with patch("pkgmgr.cli.commands.release.run_release") as m_release, patch(
|
||||
"pkgmgr.cli.commands.release.run_publish"
|
||||
) as m_publish:
|
||||
handle_release(args=args, ctx=self._ctx(), selected=selected)
|
||||
|
||||
m_release.assert_called_once()
|
||||
m_publish.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
76
tests/unit/pkgmgr/cli/commands/test_release_publish_hook.py
Normal file
76
tests/unit/pkgmgr/cli/commands/test_release_publish_hook.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestCLIReleasePublishHook(unittest.TestCase):
|
||||
def _ctx(self) -> SimpleNamespace:
|
||||
# Minimal CLIContext shape used by handle_release
|
||||
return SimpleNamespace(
|
||||
repositories_base_dir="/tmp",
|
||||
all_repositories=[],
|
||||
)
|
||||
|
||||
def test_release_runs_publish_by_default_and_respects_tty(self) -> None:
|
||||
from pkgmgr.cli.commands.release import handle_release
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
repo = {"directory": td}
|
||||
|
||||
args = SimpleNamespace(
|
||||
list=False,
|
||||
release_type="patch",
|
||||
message=None,
|
||||
preview=False,
|
||||
force=False,
|
||||
close=False,
|
||||
no_publish=False,
|
||||
)
|
||||
|
||||
with patch("pkgmgr.cli.commands.release.run_release") as m_release, patch(
|
||||
"pkgmgr.cli.commands.release.run_publish"
|
||||
) as m_publish, patch(
|
||||
"pkgmgr.cli.commands.release.sys.stdin.isatty", return_value=False
|
||||
):
|
||||
handle_release(args=args, ctx=self._ctx(), selected=[repo])
|
||||
|
||||
m_release.assert_called_once()
|
||||
m_publish.assert_called_once()
|
||||
|
||||
_, kwargs = m_publish.call_args
|
||||
self.assertEqual(kwargs["repo"], repo)
|
||||
self.assertEqual(kwargs["repo_dir"], td)
|
||||
self.assertFalse(kwargs["interactive"])
|
||||
self.assertFalse(kwargs["allow_prompt"])
|
||||
|
||||
def test_release_skips_publish_when_no_publish_flag_set(self) -> None:
|
||||
from pkgmgr.cli.commands.release import handle_release
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
repo = {"directory": td}
|
||||
|
||||
args = SimpleNamespace(
|
||||
list=False,
|
||||
release_type="patch",
|
||||
message=None,
|
||||
preview=False,
|
||||
force=False,
|
||||
close=False,
|
||||
no_publish=True,
|
||||
)
|
||||
|
||||
with patch("pkgmgr.cli.commands.release.run_release") as m_release, patch(
|
||||
"pkgmgr.cli.commands.release.run_publish"
|
||||
) as m_publish:
|
||||
handle_release(args=args, ctx=self._ctx(), selected=[repo])
|
||||
|
||||
m_release.assert_called_once()
|
||||
m_publish.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user