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
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from typing import Any, Dict, List
|
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.cli.context import CLIContext
|
||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
from pkgmgr.actions.release import release as run_release
|
|
||||||
|
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
@@ -35,23 +21,10 @@ def handle_release(
|
|||||||
ctx: CLIContext,
|
ctx: CLIContext,
|
||||||
selected: List[Repository],
|
selected: List[Repository],
|
||||||
) -> None:
|
) -> 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:
|
if not selected:
|
||||||
print("[pkgmgr] No repositories selected for release.")
|
print("[pkgmgr] No repositories selected for release.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# List-only mode: show which repositories would be affected.
|
|
||||||
if getattr(args, "list", False):
|
if getattr(args, "list", False):
|
||||||
print("[pkgmgr] Repositories that would be affected by this release:")
|
print("[pkgmgr] Repositories that would be affected by this release:")
|
||||||
for repo in selected:
|
for repo in selected:
|
||||||
@@ -62,29 +35,22 @@ def handle_release(
|
|||||||
for repo in selected:
|
for repo in selected:
|
||||||
identifier = get_repo_identifier(repo, ctx.all_repositories)
|
identifier = get_repo_identifier(repo, ctx.all_repositories)
|
||||||
|
|
||||||
repo_dir = repo.get("directory")
|
try:
|
||||||
if not repo_dir:
|
repo_dir = repo.get("directory") or get_repo_dir(ctx.repositories_base_dir, repo)
|
||||||
try:
|
except Exception as exc:
|
||||||
repo_dir = get_repo_dir(ctx.repositories_base_dir, repo)
|
print(f"[WARN] Skipping repository {identifier}: failed to resolve directory: {exc}")
|
||||||
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."
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(
|
if not os.path.isdir(repo_dir):
|
||||||
f"[pkgmgr] Running release for repository {identifier} "
|
print(f"[WARN] Skipping repository {identifier}: directory missing.")
|
||||||
f"in '{repo_dir}'..."
|
continue
|
||||||
)
|
|
||||||
|
print(f"[pkgmgr] Running release for repository {identifier}...")
|
||||||
|
|
||||||
# Change to repo directory and invoke the helper.
|
|
||||||
cwd_before = os.getcwd()
|
cwd_before = os.getcwd()
|
||||||
try:
|
try:
|
||||||
os.chdir(repo_dir)
|
os.chdir(repo_dir)
|
||||||
|
|
||||||
run_release(
|
run_release(
|
||||||
pyproject_path="pyproject.toml",
|
pyproject_path="pyproject.toml",
|
||||||
changelog_path="CHANGELOG.md",
|
changelog_path="CHANGELOG.md",
|
||||||
@@ -94,5 +60,17 @@ def handle_release(
|
|||||||
force=getattr(args, "force", False),
|
force=getattr(args, "force", False),
|
||||||
close=getattr(args, "close", 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:
|
finally:
|
||||||
os.chdir(cwd_before)
|
os.chdir(cwd_before)
|
||||||
|
|||||||
@@ -21,22 +21,22 @@ def add_release_subparser(
|
|||||||
"and updating the changelog."
|
"and updating the changelog."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
release_parser.add_argument(
|
release_parser.add_argument(
|
||||||
"release_type",
|
"release_type",
|
||||||
choices=["major", "minor", "patch"],
|
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).",
|
||||||
)
|
)
|
||||||
|
|
||||||
release_parser.add_argument(
|
release_parser.add_argument(
|
||||||
"-m",
|
"-m",
|
||||||
"--message",
|
"--message",
|
||||||
default=None,
|
default=None,
|
||||||
help=(
|
help="Optional release message to add to the changelog and tag.",
|
||||||
"Optional release message to add to the changelog and tag."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
# Generic selection / preview / list / extra_args
|
|
||||||
add_identifier_arguments(release_parser)
|
add_identifier_arguments(release_parser)
|
||||||
# Close current branch after successful release
|
|
||||||
release_parser.add_argument(
|
release_parser.add_argument(
|
||||||
"--close",
|
"--close",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -45,7 +45,7 @@ def add_release_subparser(
|
|||||||
"repository, if it is not main/master."
|
"repository, if it is not main/master."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Force: skip preview+confirmation and run release directly
|
|
||||||
release_parser.add_argument(
|
release_parser.add_argument(
|
||||||
"-f",
|
"-f",
|
||||||
"--force",
|
"--force",
|
||||||
@@ -55,3 +55,9 @@ def add_release_subparser(
|
|||||||
"release directly."
|
"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