Release version 0.4.2
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
## [0.4.2] - 2025-12-09
|
||||||
|
|
||||||
|
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||||
|
|
||||||
|
|
||||||
## [0.4.1] - 2025-12-08
|
## [0.4.1] - 2025-12-08
|
||||||
|
|
||||||
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||||
|
|||||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||||
|
|
||||||
pkgname=package-manager
|
pkgname=package-manager
|
||||||
pkgver=0.4.1
|
pkgver=0.4.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
package-manager (0.4.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:03:46 +0100
|
||||||
|
|
||||||
package-manager (0.4.1-1) unstable; urgency=medium
|
package-manager (0.4.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "0.4.1";
|
version = "0.4.2";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: package-manager
|
Name: package-manager
|
||||||
Version: 0.4.1
|
Version: 0.4.2
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
|
# pkgmgr/cli_core/commands/release.py
|
||||||
|
#!/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_core.dispatch. It does not define its
|
||||||
|
own subparser; the CLI surface is configured in cli_core.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.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, Optional
|
|
||||||
|
|
||||||
from pkgmgr.cli_core.context import CLIContext
|
from pkgmgr.cli_core.context import CLIContext
|
||||||
from pkgmgr.get_repo_dir import get_repo_dir
|
from pkgmgr.get_repo_dir import get_repo_dir
|
||||||
from pkgmgr import release as rel
|
from pkgmgr.get_repo_identifier import get_repo_identifier
|
||||||
|
from pkgmgr.release import release as run_release
|
||||||
|
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
@@ -18,59 +37,63 @@ def handle_release(
|
|||||||
selected: List[Repository],
|
selected: List[Repository],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Handle the 'release' command.
|
Handle the `pkgmgr release` subcommand.
|
||||||
|
|
||||||
Creates a release by incrementing the version and updating the changelog
|
Flow:
|
||||||
in a single selected repository.
|
1) Use the `selected` repositories as computed by dispatch_command().
|
||||||
|
2) If --list is given, print the identifiers of the selected repos
|
||||||
Important:
|
and return without running any release.
|
||||||
- Releases are strictly limited to exactly ONE repository.
|
3) For each selected repository:
|
||||||
- Using --all or specifying multiple identifiers for release does
|
- Resolve its identifier and local directory.
|
||||||
not make sense and is therefore rejected.
|
- Change into that directory.
|
||||||
- The --preview flag is respected and passed through to the release
|
- Call pkgmgr.release.release(...) with the parsed options.
|
||||||
implementation so that no changes are made in preview mode.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not selected:
|
if not selected:
|
||||||
print("No repositories selected for release.")
|
print("[pkgmgr] No repositories selected for release.")
|
||||||
sys.exit(1)
|
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:
|
||||||
|
identifier = get_repo_identifier(repo, ctx.all_repositories)
|
||||||
|
print(f" - {identifier}")
|
||||||
|
return
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
if len(selected) > 1:
|
|
||||||
print(
|
print(
|
||||||
"[ERROR] Release operations are limited to a single repository.\n"
|
f"[pkgmgr] Running release for repository {identifier} "
|
||||||
"Do not use --all or multiple identifiers with 'pkgmgr release'."
|
f"in '{repo_dir}'..."
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
original_dir = os.getcwd()
|
# Change to repo directory and invoke the helper.
|
||||||
|
cwd_before = os.getcwd()
|
||||||
repo = selected[0]
|
try:
|
||||||
|
os.chdir(repo_dir)
|
||||||
repo_dir: Optional[str] = repo.get("directory")
|
run_release(
|
||||||
if not repo_dir:
|
pyproject_path="pyproject.toml",
|
||||||
repo_dir = get_repo_dir(ctx.repositories_base_dir, repo)
|
changelog_path="CHANGELOG.md",
|
||||||
|
release_type=args.release_type,
|
||||||
if not os.path.isdir(repo_dir):
|
message=args.message or None,
|
||||||
print(
|
preview=getattr(args, "preview", False),
|
||||||
f"[ERROR] Repository directory does not exist locally: {repo_dir}"
|
force=getattr(args, "force", False),
|
||||||
)
|
close=getattr(args, "close", False),
|
||||||
sys.exit(1)
|
)
|
||||||
|
finally:
|
||||||
pyproject_path = os.path.join(repo_dir, "pyproject.toml")
|
os.chdir(cwd_before)
|
||||||
changelog_path = os.path.join(repo_dir, "CHANGELOG.md")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Releasing repository '{repo.get('repository')}' in '{repo_dir}'..."
|
|
||||||
)
|
|
||||||
|
|
||||||
os.chdir(repo_dir)
|
|
||||||
try:
|
|
||||||
rel.release(
|
|
||||||
pyproject_path=pyproject_path,
|
|
||||||
changelog_path=changelog_path,
|
|
||||||
release_type=args.release_type,
|
|
||||||
message=args.message,
|
|
||||||
preview=getattr(args, "preview", False),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
os.chdir(original_dir)
|
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ def create_parser(description_text: str) -> argparse.ArgumentParser:
|
|||||||
help="Base branch to create the new branch from (default: main)",
|
help="Base branch to create the new branch from (default: main)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# release
|
# release
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -316,12 +315,32 @@ def create_parser(description_text: str) -> argparse.ArgumentParser:
|
|||||||
release_parser.add_argument(
|
release_parser.add_argument(
|
||||||
"-m",
|
"-m",
|
||||||
"--message",
|
"--message",
|
||||||
default="",
|
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(
|
||||||
|
"--close",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Close the current branch after a successful release in each "
|
||||||
|
"repository, if it is not main/master."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Force: skip preview+confirmation and run release directly
|
||||||
|
release_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Skip the interactive preview+confirmation step and run the "
|
||||||
|
"release directly."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# version
|
# version
|
||||||
|
|||||||
@@ -23,15 +23,14 @@ Additional behaviour:
|
|||||||
phases:
|
phases:
|
||||||
1) Preview-only run (dry-run).
|
1) Preview-only run (dry-run).
|
||||||
2) Interactive confirmation, then real release if confirmed.
|
2) Interactive confirmation, then real release if confirmed.
|
||||||
This confirmation can be skipped with the `-f/--force` flag.
|
This confirmation can be skipped with the `force=True` flag.
|
||||||
- If `-c/--close` is used and the current branch is not main/master,
|
- If `close=True` is used and the current branch is not main/master,
|
||||||
the branch will be closed via branch_commands.close_branch() after
|
the branch will be closed via branch_commands.close_branch() after
|
||||||
a successful release.
|
a successful release.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -142,7 +141,6 @@ def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
) as tmp:
|
) as tmp:
|
||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
# Prefill with instructions as comments
|
|
||||||
tmp.write(
|
tmp.write(
|
||||||
"# Write the changelog entry for this release.\n"
|
"# Write the changelog entry for this release.\n"
|
||||||
"# Lines starting with '#' will be ignored.\n"
|
"# Lines starting with '#' will be ignored.\n"
|
||||||
@@ -152,7 +150,6 @@ def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str:
|
|||||||
tmp.write(initial_message.strip() + "\n")
|
tmp.write(initial_message.strip() + "\n")
|
||||||
tmp.flush()
|
tmp.flush()
|
||||||
|
|
||||||
# Open editor (best-effort; fall back gracefully if not available)
|
|
||||||
try:
|
try:
|
||||||
subprocess.call([editor, tmp_path])
|
subprocess.call([editor, tmp_path])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -161,7 +158,6 @@ def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str:
|
|||||||
"interactive changelog message."
|
"interactive changelog message."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read back content
|
|
||||||
try:
|
try:
|
||||||
with open(tmp_path, "r", encoding="utf-8") as f:
|
with open(tmp_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
@@ -171,7 +167,6 @@ def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Filter out commented lines and return joined text
|
|
||||||
lines = [
|
lines = [
|
||||||
line for line in content.splitlines()
|
line for line in content.splitlines()
|
||||||
if not line.strip().startswith("#")
|
if not line.strip().startswith("#")
|
||||||
@@ -197,14 +192,6 @@ def update_pyproject_version(
|
|||||||
version = "X.Y.Z"
|
version = "X.Y.Z"
|
||||||
|
|
||||||
and replaces the version part with the given new_version string.
|
and replaces the version part with the given new_version string.
|
||||||
|
|
||||||
It does not try to parse the full TOML structure here. This keeps the
|
|
||||||
implementation small and robust as long as the version line follows
|
|
||||||
the standard pattern.
|
|
||||||
|
|
||||||
Behaviour:
|
|
||||||
- In normal mode: write the updated content back to the file.
|
|
||||||
- In preview mode: do NOT write, only report what would change.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(pyproject_path, "r", encoding="utf-8") as f:
|
with open(pyproject_path, "r", encoding="utf-8") as f:
|
||||||
@@ -242,13 +229,6 @@ def update_flake_version(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Update the version in flake.nix, if present.
|
Update the version in flake.nix, if present.
|
||||||
|
|
||||||
Looks for a line like:
|
|
||||||
version = "1.2.3";
|
|
||||||
|
|
||||||
and replaces the string inside the quotes. If the file does not
|
|
||||||
exist or no version line is found, this is treated as a non-fatal
|
|
||||||
condition and only a log message is printed.
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(flake_path):
|
if not os.path.exists(flake_path):
|
||||||
print("[INFO] flake.nix not found, skipping.")
|
print("[INFO] flake.nix not found, skipping.")
|
||||||
@@ -293,13 +273,6 @@ def update_pkgbuild_version(
|
|||||||
Expects:
|
Expects:
|
||||||
pkgver=1.2.3
|
pkgver=1.2.3
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
|
|
||||||
Behaviour:
|
|
||||||
- Set pkgver to the new_version (e.g. 1.2.3).
|
|
||||||
- Reset pkgrel to 1.
|
|
||||||
|
|
||||||
If the file does not exist, this is non-fatal and only a log
|
|
||||||
message is printed.
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(pkgbuild_path):
|
if not os.path.exists(pkgbuild_path):
|
||||||
print("[INFO] PKGBUILD not found, skipping.")
|
print("[INFO] PKGBUILD not found, skipping.")
|
||||||
@@ -312,7 +285,6 @@ def update_pkgbuild_version(
|
|||||||
print(f"[WARN] Could not read PKGBUILD: {exc}")
|
print(f"[WARN] Could not read PKGBUILD: {exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update pkgver
|
|
||||||
ver_pattern = r"^(pkgver\s*=\s*)(.+)$"
|
ver_pattern = r"^(pkgver\s*=\s*)(.+)$"
|
||||||
new_content, ver_count = re.subn(
|
new_content, ver_count = re.subn(
|
||||||
ver_pattern,
|
ver_pattern,
|
||||||
@@ -323,9 +295,8 @@ def update_pkgbuild_version(
|
|||||||
|
|
||||||
if ver_count == 0:
|
if ver_count == 0:
|
||||||
print("[WARN] No pkgver line found in PKGBUILD.")
|
print("[WARN] No pkgver line found in PKGBUILD.")
|
||||||
new_content = content # revert to original if we didn't change anything
|
new_content = content
|
||||||
|
|
||||||
# Reset pkgrel to 1
|
|
||||||
rel_pattern = r"^(pkgrel\s*=\s*)(.+)$"
|
rel_pattern = r"^(pkgrel\s*=\s*)(.+)$"
|
||||||
new_content, rel_count = re.subn(
|
new_content, rel_count = re.subn(
|
||||||
rel_pattern,
|
rel_pattern,
|
||||||
@@ -354,19 +325,6 @@ def update_spec_version(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Update the version in an RPM spec file, if present.
|
Update the version in an RPM spec file, if present.
|
||||||
|
|
||||||
Assumes a file like 'package-manager.spec' with lines:
|
|
||||||
|
|
||||||
Version: 1.2.3
|
|
||||||
Release: 1%{?dist}
|
|
||||||
|
|
||||||
Behaviour:
|
|
||||||
- Set 'Version:' to new_version.
|
|
||||||
- Reset 'Release:' to '1' while preserving any macro suffix,
|
|
||||||
e.g. '1%{?dist}'.
|
|
||||||
|
|
||||||
If the file does not exist, this is non-fatal and only a log
|
|
||||||
message is printed.
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(spec_path):
|
if not os.path.exists(spec_path):
|
||||||
print("[INFO] RPM spec file not found, skipping.")
|
print("[INFO] RPM spec file not found, skipping.")
|
||||||
@@ -379,7 +337,6 @@ def update_spec_version(
|
|||||||
print(f"[WARN] Could not read spec file: {exc}")
|
print(f"[WARN] Could not read spec file: {exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update Version:
|
|
||||||
ver_pattern = r"^(Version:\s*)(.+)$"
|
ver_pattern = r"^(Version:\s*)(.+)$"
|
||||||
new_content, ver_count = re.subn(
|
new_content, ver_count = re.subn(
|
||||||
ver_pattern,
|
ver_pattern,
|
||||||
@@ -391,12 +348,10 @@ def update_spec_version(
|
|||||||
if ver_count == 0:
|
if ver_count == 0:
|
||||||
print("[WARN] No 'Version:' line found in spec file.")
|
print("[WARN] No 'Version:' line found in spec file.")
|
||||||
|
|
||||||
# Reset Release:
|
|
||||||
rel_pattern = r"^(Release:\s*)(.+)$"
|
rel_pattern = r"^(Release:\s*)(.+)$"
|
||||||
|
|
||||||
def _release_repl(m: re.Match[str]) -> str: # type: ignore[name-defined]
|
def _release_repl(m: re.Match[str]) -> str: # type: ignore[name-defined]
|
||||||
rest = m.group(2).strip()
|
rest = m.group(2).strip()
|
||||||
# Reset numeric prefix to "1" and keep any suffix (e.g. % macros).
|
|
||||||
match = re.match(r"^(\d+)(.*)$", rest)
|
match = re.match(r"^(\d+)(.*)$", rest)
|
||||||
if match:
|
if match:
|
||||||
suffix = match.group(2)
|
suffix = match.group(2)
|
||||||
@@ -439,21 +394,11 @@ def update_changelog(
|
|||||||
"""
|
"""
|
||||||
Prepend a new release section to CHANGELOG.md with the new version,
|
Prepend a new release section to CHANGELOG.md with the new version,
|
||||||
current date, and a message.
|
current date, and a message.
|
||||||
|
|
||||||
Behaviour:
|
|
||||||
- If message is None and preview is False:
|
|
||||||
→ open $EDITOR (fallback 'nano') to let the user enter a message.
|
|
||||||
- If message is None and preview is True:
|
|
||||||
→ use a generic automated message.
|
|
||||||
- The resulting changelog entry is printed to stdout.
|
|
||||||
- Returns the final message text used.
|
|
||||||
"""
|
"""
|
||||||
today = date.today().isoformat()
|
today = date.today().isoformat()
|
||||||
|
|
||||||
# Resolve message
|
|
||||||
if message is None:
|
if message is None:
|
||||||
if preview:
|
if preview:
|
||||||
# Do not open editor in preview mode; keep it non-interactive.
|
|
||||||
message = "Automated release."
|
message = "Automated release."
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
@@ -481,7 +426,6 @@ def update_changelog(
|
|||||||
|
|
||||||
new_changelog = header + "\n" + changelog if changelog else header
|
new_changelog = header + "\n" + changelog if changelog else header
|
||||||
|
|
||||||
# Show the entry that will be written
|
|
||||||
print("\n================ CHANGELOG ENTRY ================")
|
print("\n================ CHANGELOG ENTRY ================")
|
||||||
print(header.rstrip())
|
print(header.rstrip())
|
||||||
print("=================================================\n")
|
print("=================================================\n")
|
||||||
@@ -506,8 +450,6 @@ def update_changelog(
|
|||||||
def _get_git_config_value(key: str) -> Optional[str]:
|
def _get_git_config_value(key: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Try to read a value from `git config --get <key>`.
|
Try to read a value from `git config --get <key>`.
|
||||||
|
|
||||||
Returns the stripped value or None if not set / on error.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -526,12 +468,6 @@ def _get_git_config_value(key: str) -> Optional[str]:
|
|||||||
def _get_debian_author() -> Tuple[str, str]:
|
def _get_debian_author() -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Determine the maintainer name/email for debian/changelog entries.
|
Determine the maintainer name/email for debian/changelog entries.
|
||||||
|
|
||||||
Priority:
|
|
||||||
1. DEBFULLNAME / DEBEMAIL
|
|
||||||
2. GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL
|
|
||||||
3. git config user.name / user.email
|
|
||||||
4. Fallback: 'Unknown Maintainer' / 'unknown@example.com'
|
|
||||||
"""
|
"""
|
||||||
name = os.environ.get("DEBFULLNAME")
|
name = os.environ.get("DEBFULLNAME")
|
||||||
email = os.environ.get("DEBEMAIL")
|
email = os.environ.get("DEBEMAIL")
|
||||||
@@ -563,12 +499,6 @@ def update_debian_changelog(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Prepend a new entry to debian/changelog, if it exists.
|
Prepend a new entry to debian/changelog, if it exists.
|
||||||
|
|
||||||
The first line typically looks like:
|
|
||||||
package-name (1.2.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
We generate a new stanza at the top with Debian-style version
|
|
||||||
'X.Y.Z-1'. If the file does not exist, this function does nothing.
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(debian_changelog_path):
|
if not os.path.exists(debian_changelog_path):
|
||||||
print("[INFO] debian/changelog not found, skipping.")
|
print("[INFO] debian/changelog not found, skipping.")
|
||||||
@@ -576,15 +506,12 @@ def update_debian_changelog(
|
|||||||
|
|
||||||
debian_version = f"{new_version}-1"
|
debian_version = f"{new_version}-1"
|
||||||
now = datetime.now().astimezone()
|
now = datetime.now().astimezone()
|
||||||
# Debian-like date string, e.g. "Mon, 08 Dec 2025 12:34:56 +0100"
|
|
||||||
date_str = now.strftime("%a, %d %b %Y %H:%M:%S %z")
|
date_str = now.strftime("%a, %d %b %Y %H:%M:%S %z")
|
||||||
|
|
||||||
author_name, author_email = _get_debian_author()
|
author_name, author_email = _get_debian_author()
|
||||||
|
|
||||||
first_line = f"{package_name} ({debian_version}) unstable; urgency=medium"
|
first_line = f"{package_name} ({debian_version}) unstable; urgency=medium"
|
||||||
body_line = (
|
body_line = message.strip() if message else f"Automated release {new_version}."
|
||||||
message.strip() if message else f"Automated release {new_version}."
|
|
||||||
)
|
|
||||||
stanza = (
|
stanza = (
|
||||||
f"{first_line}\n\n"
|
f"{first_line}\n\n"
|
||||||
f" * {body_line}\n\n"
|
f" * {body_line}\n\n"
|
||||||
@@ -628,22 +555,8 @@ def _release_impl(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Internal implementation that performs a single-phase release.
|
Internal implementation that performs a single-phase release.
|
||||||
|
|
||||||
If `preview` is True:
|
|
||||||
- No files are written.
|
|
||||||
- No git commands are executed.
|
|
||||||
- Planned actions are printed.
|
|
||||||
|
|
||||||
If `preview` is False:
|
|
||||||
- Files are updated.
|
|
||||||
- Git commit, tag, and push are executed.
|
|
||||||
- If `close` is True and the current branch is not main/master,
|
|
||||||
the branch will be closed after a successful release.
|
|
||||||
"""
|
"""
|
||||||
# 1) Determine the current version from Git tags.
|
|
||||||
current_ver = _determine_current_version()
|
current_ver = _determine_current_version()
|
||||||
|
|
||||||
# 2) Compute the next version.
|
|
||||||
new_ver = _bump_semver(current_ver, release_type)
|
new_ver = _bump_semver(current_ver, release_type)
|
||||||
new_ver_str = str(new_ver)
|
new_ver_str = str(new_ver)
|
||||||
new_tag = new_ver.to_tag(with_prefix=True)
|
new_tag = new_ver.to_tag(with_prefix=True)
|
||||||
@@ -653,12 +566,9 @@ def _release_impl(
|
|||||||
print(f"Current version: {current_ver}")
|
print(f"Current version: {current_ver}")
|
||||||
print(f"New version: {new_ver_str} ({release_type})")
|
print(f"New version: {new_ver_str} ({release_type})")
|
||||||
|
|
||||||
# Determine repository root based on pyproject location
|
|
||||||
repo_root = os.path.dirname(os.path.abspath(pyproject_path))
|
repo_root = os.path.dirname(os.path.abspath(pyproject_path))
|
||||||
|
|
||||||
# 2) Update files.
|
|
||||||
update_pyproject_version(pyproject_path, new_ver_str, preview=preview)
|
update_pyproject_version(pyproject_path, new_ver_str, preview=preview)
|
||||||
# Let update_changelog resolve or edit the message; capture it separately.
|
|
||||||
changelog_message = update_changelog(
|
changelog_message = update_changelog(
|
||||||
changelog_path,
|
changelog_path,
|
||||||
new_ver_str,
|
new_ver_str,
|
||||||
@@ -666,7 +576,6 @@ def _release_impl(
|
|||||||
preview=preview,
|
preview=preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Additional packaging files (non-fatal if missing)
|
|
||||||
flake_path = os.path.join(repo_root, "flake.nix")
|
flake_path = os.path.join(repo_root, "flake.nix")
|
||||||
update_flake_version(flake_path, new_ver_str, preview=preview)
|
update_flake_version(flake_path, new_ver_str, preview=preview)
|
||||||
|
|
||||||
@@ -676,14 +585,12 @@ def _release_impl(
|
|||||||
spec_path = os.path.join(repo_root, "package-manager.spec")
|
spec_path = os.path.join(repo_root, "package-manager.spec")
|
||||||
update_spec_version(spec_path, new_ver_str, preview=preview)
|
update_spec_version(spec_path, new_ver_str, preview=preview)
|
||||||
|
|
||||||
# Determine an effective message for tag & Debian changelog.
|
|
||||||
effective_message: Optional[str] = message
|
effective_message: Optional[str] = message
|
||||||
if effective_message is None and isinstance(changelog_message, str):
|
if effective_message is None and isinstance(changelog_message, str):
|
||||||
if changelog_message.strip():
|
if changelog_message.strip():
|
||||||
effective_message = changelog_message.strip()
|
effective_message = changelog_message.strip()
|
||||||
|
|
||||||
debian_changelog_path = os.path.join(repo_root, "debian", "changelog")
|
debian_changelog_path = os.path.join(repo_root, "debian", "changelog")
|
||||||
# Use repo directory name as a simple default for package name
|
|
||||||
package_name = os.path.basename(repo_root) or "package-manager"
|
package_name = os.path.basename(repo_root) or "package-manager"
|
||||||
update_debian_changelog(
|
update_debian_changelog(
|
||||||
debian_changelog_path,
|
debian_changelog_path,
|
||||||
@@ -693,7 +600,6 @@ def _release_impl(
|
|||||||
preview=preview,
|
preview=preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3) Git operations: stage, commit, tag, push.
|
|
||||||
commit_msg = f"Release version {new_ver_str}"
|
commit_msg = f"Release version {new_ver_str}"
|
||||||
tag_msg = effective_message or commit_msg
|
tag_msg = effective_message or commit_msg
|
||||||
|
|
||||||
@@ -703,7 +609,6 @@ def _release_impl(
|
|||||||
branch = "main"
|
branch = "main"
|
||||||
print(f"Releasing on branch: {branch}")
|
print(f"Releasing on branch: {branch}")
|
||||||
|
|
||||||
# Stage all relevant packaging files so they are included in the commit
|
|
||||||
files_to_add = [
|
files_to_add = [
|
||||||
pyproject_path,
|
pyproject_path,
|
||||||
changelog_path,
|
changelog_path,
|
||||||
@@ -725,11 +630,11 @@ def _release_impl(
|
|||||||
if close and branch not in ("main", "master"):
|
if close and branch not in ("main", "master"):
|
||||||
print(
|
print(
|
||||||
f"[PREVIEW] Would also close branch {branch} after the release "
|
f"[PREVIEW] Would also close branch {branch} after the release "
|
||||||
"(--close specified and branch is not main/master)."
|
"(close=True and branch is not main/master)."
|
||||||
)
|
)
|
||||||
elif close:
|
elif close:
|
||||||
print(
|
print(
|
||||||
f"[PREVIEW] --close specified but current branch is {branch}; "
|
f"[PREVIEW] close=True but current branch is {branch}; "
|
||||||
"no branch would be closed."
|
"no branch would be closed."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -746,27 +651,26 @@ def _release_impl(
|
|||||||
|
|
||||||
print(f"Release {new_ver_str} completed.")
|
print(f"Release {new_ver_str} completed.")
|
||||||
|
|
||||||
# Optional: close the current branch after a successful release.
|
|
||||||
if close:
|
if close:
|
||||||
if branch in ("main", "master"):
|
if branch in ("main", "master"):
|
||||||
print(
|
print(
|
||||||
f"[INFO] --close specified but current branch is {branch}; "
|
f"[INFO] close=True but current branch is {branch}; "
|
||||||
"nothing to close."
|
"nothing to close."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[INFO] Closing branch {branch} after successful release "
|
f"[INFO] Closing branch {branch} after successful release "
|
||||||
"(--close enabled and branch is not main/master)..."
|
"(close=True and branch is not main/master)..."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
close_branch(name=branch, base_branch="main", cwd=".")
|
close_branch(name=branch, base_branch="main", cwd=".")
|
||||||
except Exception as exc: # pragma: no cover - defensive
|
except Exception as exc: # pragma: no cover
|
||||||
print(f"[WARN] Failed to close branch {branch} automatically: {exc}")
|
print(f"[WARN] Failed to close branch {branch} automatically: {exc}")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public release entry point (with preview-first + confirmation logic)
|
# Public release entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@@ -786,29 +690,13 @@ def release(
|
|||||||
|
|
||||||
- preview=True:
|
- preview=True:
|
||||||
* Single-phase PREVIEW only.
|
* Single-phase PREVIEW only.
|
||||||
* No files are changed, no git commands are executed.
|
|
||||||
* `force` is ignored in this mode.
|
|
||||||
|
|
||||||
- preview=False, force=True:
|
- preview=False, force=True:
|
||||||
* Single-phase REAL release, no interactive preview.
|
* Single-phase REAL release, no interactive preview.
|
||||||
* Files are changed and git commands are executed immediately.
|
|
||||||
|
|
||||||
- preview=False, force=False:
|
- preview=False, force=False:
|
||||||
* Two-phase flow (intended default for interactive CLI use):
|
* Two-phase flow (intended default for interactive CLI use).
|
||||||
1) PREVIEW: dry-run, printing all planned actions.
|
|
||||||
2) Ask the user for confirmation:
|
|
||||||
"Proceed with the actual release? [y/N]: "
|
|
||||||
If confirmed, perform the REAL release.
|
|
||||||
Otherwise, abort without changes.
|
|
||||||
|
|
||||||
* In non-interactive environments (stdin not a TTY), the
|
|
||||||
confirmation step is skipped automatically and a single
|
|
||||||
REAL phase is executed, to avoid blocking on input().
|
|
||||||
|
|
||||||
The `close` flag controls whether the current branch should be
|
|
||||||
closed after a successful REAL release (only if it is not main/master).
|
|
||||||
"""
|
"""
|
||||||
# Explicit preview mode: just do a single PREVIEW phase and exit.
|
|
||||||
if preview:
|
if preview:
|
||||||
_release_impl(
|
_release_impl(
|
||||||
pyproject_path=pyproject_path,
|
pyproject_path=pyproject_path,
|
||||||
@@ -820,7 +708,6 @@ def release(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Non-preview, but forced: run REAL release directly.
|
|
||||||
if force:
|
if force:
|
||||||
_release_impl(
|
_release_impl(
|
||||||
pyproject_path=pyproject_path,
|
pyproject_path=pyproject_path,
|
||||||
@@ -832,7 +719,6 @@ def release(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Non-interactive environment? Skip confirmation to avoid blocking.
|
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
_release_impl(
|
_release_impl(
|
||||||
pyproject_path=pyproject_path,
|
pyproject_path=pyproject_path,
|
||||||
@@ -844,7 +730,6 @@ def release(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Interactive two-phase flow:
|
|
||||||
print("[INFO] Running preview before actual release...\n")
|
print("[INFO] Running preview before actual release...\n")
|
||||||
_release_impl(
|
_release_impl(
|
||||||
pyproject_path=pyproject_path,
|
pyproject_path=pyproject_path,
|
||||||
@@ -855,7 +740,6 @@ def release(
|
|||||||
close=close,
|
close=close,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ask for confirmation
|
|
||||||
try:
|
try:
|
||||||
answer = input("Proceed with the actual release? [y/N]: ").strip().lower()
|
answer = input("Proceed with the actual release? [y/N]: ").strip().lower()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
@@ -875,78 +759,3 @@ def release(
|
|||||||
preview=False,
|
preview=False,
|
||||||
close=close,
|
close=close,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# CLI entry point for standalone use
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="pkgmgr release helper")
|
|
||||||
parser.add_argument(
|
|
||||||
"release_type",
|
|
||||||
choices=["major", "minor", "patch"],
|
|
||||||
help="Type of release (major/minor/patch).",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-m",
|
|
||||||
"--message",
|
|
||||||
dest="message",
|
|
||||||
default=None,
|
|
||||||
help="Release message to use for changelog and tag.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pyproject",
|
|
||||||
dest="pyproject",
|
|
||||||
default="pyproject.toml",
|
|
||||||
help="Path to pyproject.toml (default: pyproject.toml)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--changelog",
|
|
||||||
dest="changelog",
|
|
||||||
default="CHANGELOG.md",
|
|
||||||
help="Path to CHANGELOG.md (default: CHANGELOG.md)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--preview",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Preview release changes without modifying files or running git. "
|
|
||||||
"This mode never executes the real release."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-f",
|
|
||||||
"--force",
|
|
||||||
dest="force",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Skip the interactive preview+confirmation step and run the "
|
|
||||||
"release directly."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--close",
|
|
||||||
dest="close",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Close the current branch after a successful release, "
|
|
||||||
"if it is not main/master."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return parser.parse_args(argv)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
args = _parse_args()
|
|
||||||
release(
|
|
||||||
pyproject_path=args.pyproject,
|
|
||||||
changelog_path=args.changelog,
|
|
||||||
release_type=args.release_type,
|
|
||||||
message=args.message,
|
|
||||||
preview=args.preview,
|
|
||||||
force=args.force,
|
|
||||||
close=args.close,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
description = "Kevin's package-manager tool (pkgmgr)"
|
description = "Kevin's package-manager tool (pkgmgr)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
0
tests/unit/pkgmgr/cli_core/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/commands/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/commands/__init__.py
Normal file
206
tests/unit/pkgmgr/cli_core/commands/test_release.py
Normal file
206
tests/unit/pkgmgr/cli_core/commands/test_release.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit tests for pkgmgr.cli_core.commands.release.
|
||||||
|
|
||||||
|
These tests focus on the wiring layer:
|
||||||
|
- Argument handling for the release command as defined by the
|
||||||
|
top-level parser (cli_core.parser.create_parser).
|
||||||
|
- Correct invocation of pkgmgr.release.release(...) for the
|
||||||
|
selected repositories.
|
||||||
|
- Behaviour of --preview, --list, --close, and -f/--force.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import List
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestReleaseCommand(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the `pkgmgr release` CLI wiring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _make_ctx(self, all_repos: List[dict]) -> SimpleNamespace:
|
||||||
|
"""
|
||||||
|
Create a minimal CLIContext-like object for tests.
|
||||||
|
|
||||||
|
Only the attributes that handle_release() uses are provided.
|
||||||
|
"""
|
||||||
|
return SimpleNamespace(
|
||||||
|
config_merged={},
|
||||||
|
repositories_base_dir="/base/dir",
|
||||||
|
all_repositories=all_repos,
|
||||||
|
binaries_dir="/bin",
|
||||||
|
user_config_path="/tmp/config.yaml",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_release_args(self, argv: List[str]) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Build a real top-level parser and parse the given argv list
|
||||||
|
to obtain the Namespace for the `release` command.
|
||||||
|
"""
|
||||||
|
from pkgmgr.cli_core.parser import create_parser
|
||||||
|
|
||||||
|
parser = create_parser("test parser")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
self.assertEqual(args.command, "release")
|
||||||
|
return args
|
||||||
|
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.path.isdir", return_value=True)
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.chdir")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.getcwd", return_value="/cwd")
|
||||||
|
def test_release_with_close_and_message(
|
||||||
|
self,
|
||||||
|
mock_getcwd,
|
||||||
|
mock_chdir,
|
||||||
|
mock_get_repo_identifier,
|
||||||
|
mock_get_repo_dir,
|
||||||
|
mock_run_release,
|
||||||
|
mock_isdir,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
The release handler should call pkgmgr.release.release() with:
|
||||||
|
- release_type (e.g. minor)
|
||||||
|
- provided message
|
||||||
|
- preview flag
|
||||||
|
- force flag
|
||||||
|
- close flag
|
||||||
|
|
||||||
|
It must change into the repository directory and then back.
|
||||||
|
"""
|
||||||
|
from pkgmgr.cli_core.commands.release import handle_release
|
||||||
|
|
||||||
|
repo = {"name": "dummy-repo"}
|
||||||
|
selected = [repo]
|
||||||
|
ctx = self._make_ctx(selected)
|
||||||
|
|
||||||
|
mock_get_repo_identifier.return_value = "dummy-id"
|
||||||
|
mock_get_repo_dir.return_value = "/repos/dummy"
|
||||||
|
|
||||||
|
argv = [
|
||||||
|
"release",
|
||||||
|
"minor",
|
||||||
|
"dummy-id",
|
||||||
|
"-m",
|
||||||
|
"Close branch after minor release",
|
||||||
|
"--close",
|
||||||
|
"-f",
|
||||||
|
]
|
||||||
|
args = self._parse_release_args(argv)
|
||||||
|
|
||||||
|
handle_release(args, ctx, selected)
|
||||||
|
|
||||||
|
# We should have changed into the repo dir and then back.
|
||||||
|
mock_chdir.assert_has_calls(
|
||||||
|
[call("/repos/dummy"), call("/cwd")]
|
||||||
|
)
|
||||||
|
|
||||||
|
# And run_release should be invoked once with the expected parameters.
|
||||||
|
mock_run_release.assert_called_once_with(
|
||||||
|
pyproject_path="pyproject.toml",
|
||||||
|
changelog_path="CHANGELOG.md",
|
||||||
|
release_type="minor",
|
||||||
|
message="Close branch after minor release",
|
||||||
|
preview=False,
|
||||||
|
force=True,
|
||||||
|
close=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.path.isdir", return_value=True)
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.chdir")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.os.getcwd", return_value="/cwd")
|
||||||
|
def test_release_preview_mode(
|
||||||
|
self,
|
||||||
|
mock_getcwd,
|
||||||
|
mock_chdir,
|
||||||
|
mock_get_repo_identifier,
|
||||||
|
mock_get_repo_dir,
|
||||||
|
mock_run_release,
|
||||||
|
mock_isdir,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
In preview mode, the handler should pass preview=True to the
|
||||||
|
release helper and force=False by default.
|
||||||
|
"""
|
||||||
|
from pkgmgr.cli_core.commands.release import handle_release
|
||||||
|
|
||||||
|
repo = {"name": "dummy-repo"}
|
||||||
|
selected = [repo]
|
||||||
|
ctx = self._make_ctx(selected)
|
||||||
|
|
||||||
|
mock_get_repo_identifier.return_value = "dummy-id"
|
||||||
|
mock_get_repo_dir.return_value = "/repos/dummy"
|
||||||
|
|
||||||
|
argv = [
|
||||||
|
"release",
|
||||||
|
"patch",
|
||||||
|
"dummy-id",
|
||||||
|
"--preview",
|
||||||
|
]
|
||||||
|
args = self._parse_release_args(argv)
|
||||||
|
|
||||||
|
handle_release(args, ctx, selected)
|
||||||
|
|
||||||
|
mock_run_release.assert_called_once_with(
|
||||||
|
pyproject_path="pyproject.toml",
|
||||||
|
changelog_path="CHANGELOG.md",
|
||||||
|
release_type="patch",
|
||||||
|
message=None,
|
||||||
|
preview=True,
|
||||||
|
force=False,
|
||||||
|
close=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||||
|
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||||
|
def test_release_list_mode_does_not_invoke_helper(
|
||||||
|
self,
|
||||||
|
mock_get_repo_identifier,
|
||||||
|
mock_get_repo_dir,
|
||||||
|
mock_run_release,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
When --list is provided, the handler should print the list of affected
|
||||||
|
repositories and must NOT invoke run_release().
|
||||||
|
"""
|
||||||
|
from pkgmgr.cli_core.commands.release import handle_release
|
||||||
|
|
||||||
|
repo1 = {"name": "repo-1"}
|
||||||
|
repo2 = {"name": "repo-2"}
|
||||||
|
selected = [repo1, repo2]
|
||||||
|
ctx = self._make_ctx(selected)
|
||||||
|
|
||||||
|
mock_get_repo_identifier.side_effect = ["id-1", "id-2"]
|
||||||
|
|
||||||
|
argv = [
|
||||||
|
"release",
|
||||||
|
"major",
|
||||||
|
"--list",
|
||||||
|
]
|
||||||
|
args = self._parse_release_args(argv)
|
||||||
|
|
||||||
|
handle_release(args, ctx, selected)
|
||||||
|
|
||||||
|
mock_run_release.assert_not_called()
|
||||||
|
self.assertEqual(
|
||||||
|
mock_get_repo_identifier.call_args_list,
|
||||||
|
[call(repo1, selected), call(repo2, selected)],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -365,6 +365,7 @@ class TestUpdateDebianChangelog(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestReleaseOrchestration(unittest.TestCase):
|
class TestReleaseOrchestration(unittest.TestCase):
|
||||||
|
@patch("pkgmgr.release.sys.stdin.isatty", return_value=False)
|
||||||
@patch("pkgmgr.release._run_git_command")
|
@patch("pkgmgr.release._run_git_command")
|
||||||
@patch("pkgmgr.release.update_debian_changelog")
|
@patch("pkgmgr.release.update_debian_changelog")
|
||||||
@patch("pkgmgr.release.update_spec_version")
|
@patch("pkgmgr.release.update_spec_version")
|
||||||
@@ -387,6 +388,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
|||||||
mock_update_spec,
|
mock_update_spec,
|
||||||
mock_update_debian_changelog,
|
mock_update_debian_changelog,
|
||||||
mock_run_git_command,
|
mock_run_git_command,
|
||||||
|
mock_isatty,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
||||||
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
||||||
@@ -449,6 +451,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
|||||||
self.assertIn("git push origin develop", git_calls)
|
self.assertIn("git push origin develop", git_calls)
|
||||||
self.assertIn("git push origin --tags", git_calls)
|
self.assertIn("git push origin --tags", git_calls)
|
||||||
|
|
||||||
|
@patch("pkgmgr.release.sys.stdin.isatty", return_value=False)
|
||||||
@patch("pkgmgr.release._run_git_command")
|
@patch("pkgmgr.release._run_git_command")
|
||||||
@patch("pkgmgr.release.update_debian_changelog")
|
@patch("pkgmgr.release.update_debian_changelog")
|
||||||
@patch("pkgmgr.release.update_spec_version")
|
@patch("pkgmgr.release.update_spec_version")
|
||||||
@@ -471,6 +474,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
|||||||
mock_update_spec,
|
mock_update_spec,
|
||||||
mock_update_debian_changelog,
|
mock_update_debian_changelog,
|
||||||
mock_run_git_command,
|
mock_run_git_command,
|
||||||
|
mock_isatty,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
||||||
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
||||||
|
|||||||
Reference in New Issue
Block a user