diff --git a/src/pkgmgr/actions/release/files.py b/src/pkgmgr/actions/release/files.py deleted file mode 100644 index 17dddee..0000000 --- a/src/pkgmgr/actions/release/files.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -File and metadata update helpers for the release workflow. - -Responsibilities: - - Update pyproject.toml with the new version. - - Update flake.nix, PKGBUILD, RPM spec files where present. - - Prepend release entries to CHANGELOG.md. - - Maintain distribution-specific changelog files: - * debian/changelog - * RPM spec %changelog section - including maintainer metadata where applicable. -""" - -from __future__ import annotations - -import os -import re -import subprocess -import tempfile -from datetime import date, datetime -from typing import Optional, Tuple - -from pkgmgr.core.git.queries import get_config_value - - -# --------------------------------------------------------------------------- -# Editor helper for interactive changelog messages -# --------------------------------------------------------------------------- - - -def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str: - """ - Open $EDITOR (fallback 'nano') so the user can enter a changelog message. - - The temporary file is pre-filled with commented instructions and an - optional initial_message. Lines starting with '#' are ignored when the - message is read back. - - Returns the final message (may be empty string if user leaves it blank). - """ - editor = os.environ.get("EDITOR", "nano") - - with tempfile.NamedTemporaryFile( - mode="w+", - delete=False, - encoding="utf-8", - ) as tmp: - tmp_path = tmp.name - tmp.write( - "# Write the changelog entry for this release.\n" - "# Lines starting with '#' will be ignored.\n" - "# Empty result will fall back to a generic message.\n\n" - ) - if initial_message: - tmp.write(initial_message.strip() + "\n") - tmp.flush() - - try: - subprocess.call([editor, tmp_path]) - except FileNotFoundError: - print( - f"[WARN] Editor {editor!r} not found; proceeding without " - "interactive changelog message." - ) - - try: - with open(tmp_path, "r", encoding="utf-8") as f: - content = f.read() - finally: - try: - os.remove(tmp_path) - except OSError: - pass - - lines = [line for line in content.splitlines() if not line.strip().startswith("#")] - return "\n".join(lines).strip() - - -# --------------------------------------------------------------------------- -# File update helpers (pyproject + extra packaging + changelog) -# --------------------------------------------------------------------------- - - -def update_pyproject_version(pyproject_path: str, new_version: str, preview: bool = False) -> None: - if not os.path.exists(pyproject_path): - print(f"[INFO] pyproject.toml not found at: {pyproject_path}, skipping version update.") - return - - try: - with open(pyproject_path, "r", encoding="utf-8") as f: - content = f.read() - except OSError as exc: - print(f"[WARN] Could not read pyproject.toml at {pyproject_path}: {exc}. Skipping version update.") - return - - # Find [project] block (PEP 621) - m = re.search(r"(?ms)^\s*\[project\]\s*$.*?(?=^\s*\[|\Z)", content) - if not m: - print("[ERROR] Could not find [project] section in pyproject.toml") - raise RuntimeError("Missing [project] section in pyproject.toml") - - project_block = m.group(0) - - # Replace version line inside that block (allow leading whitespace) - ver_pat = r'(?m)^(\s*version\s*=\s*")([^"]+)(")\s*$' - new_project_block, count = re.subn( - ver_pat, - lambda mm: f"{mm.group(1)}{new_version}{mm.group(3)}", - project_block, - ) - - if count == 0: - print("[ERROR] Could not find version = \"...\" in [project] section of pyproject.toml") - raise RuntimeError("Missing version key in [project] section") - - new_content = content[: m.start()] + new_project_block + content[m.end() :] - - if preview: - print(f"[PREVIEW] Would update pyproject.toml [project].version to {new_version}") - return - - with open(pyproject_path, "w", encoding="utf-8") as f: - f.write(new_content) - - print(f"Updated pyproject.toml version to {new_version}") - -def update_flake_version( - flake_path: str, - new_version: str, - preview: bool = False, -) -> None: - """ - Update the version in flake.nix, if present. - """ - if not os.path.exists(flake_path): - print("[INFO] flake.nix not found, skipping.") - return - - try: - with open(flake_path, "r", encoding="utf-8") as f: - content = f.read() - except Exception as exc: - print(f"[WARN] Could not read flake.nix: {exc}") - return - - pattern = r'(version\s*=\s*")([^"]+)(")' - new_content, count = re.subn( - pattern, - lambda m: f"{m.group(1)}{new_version}{m.group(3)}", - content, - ) - - if count == 0: - print("[WARN] No version assignment found in flake.nix, skipping.") - return - - if preview: - print(f"[PREVIEW] Would update flake.nix version to {new_version}") - return - - with open(flake_path, "w", encoding="utf-8") as f: - f.write(new_content) - - print(f"Updated flake.nix version to {new_version}") - - -def update_pkgbuild_version( - pkgbuild_path: str, - new_version: str, - preview: bool = False, -) -> None: - """ - Update the version in PKGBUILD, if present. - - Expects: - pkgver=1.2.3 - pkgrel=1 - """ - if not os.path.exists(pkgbuild_path): - print("[INFO] PKGBUILD not found, skipping.") - return - - try: - with open(pkgbuild_path, "r", encoding="utf-8") as f: - content = f.read() - except Exception as exc: - print(f"[WARN] Could not read PKGBUILD: {exc}") - return - - ver_pattern = r"^(pkgver\s*=\s*)(.+)$" - new_content, ver_count = re.subn( - ver_pattern, - lambda m: f"{m.group(1)}{new_version}", - content, - flags=re.MULTILINE, - ) - - if ver_count == 0: - print("[WARN] No pkgver line found in PKGBUILD.") - new_content = content - - rel_pattern = r"^(pkgrel\s*=\s*)(.+)$" - new_content, rel_count = re.subn( - rel_pattern, - lambda m: f"{m.group(1)}1", - new_content, - flags=re.MULTILINE, - ) - - if rel_count == 0: - print("[WARN] No pkgrel line found in PKGBUILD.") - - if preview: - print(f"[PREVIEW] Would update PKGBUILD to pkgver={new_version}, pkgrel=1") - return - - with open(pkgbuild_path, "w", encoding="utf-8") as f: - f.write(new_content) - - print(f"Updated PKGBUILD to pkgver={new_version}, pkgrel=1") - - -def update_spec_version( - spec_path: str, - new_version: str, - preview: bool = False, -) -> None: - """ - Update the version in an RPM spec file, if present. - """ - if not os.path.exists(spec_path): - print("[INFO] RPM spec file not found, skipping.") - return - - try: - with open(spec_path, "r", encoding="utf-8") as f: - content = f.read() - except Exception as exc: - print(f"[WARN] Could not read spec file: {exc}") - return - - ver_pattern = r"^(Version:\s*)(.+)$" - new_content, ver_count = re.subn( - ver_pattern, - lambda m: f"{m.group(1)}{new_version}", - content, - flags=re.MULTILINE, - ) - - if ver_count == 0: - print("[WARN] No 'Version:' line found in spec file.") - - rel_pattern = r"^(Release:\s*)(.+)$" - - def _release_repl(m: re.Match[str]) -> str: # type: ignore[name-defined] - rest = m.group(2).strip() - match = re.match(r"^(\d+)(.*)$", rest) - if match: - suffix = match.group(2) - else: - suffix = "" - return f"{m.group(1)}1{suffix}" - - new_content, rel_count = re.subn( - rel_pattern, - _release_repl, - new_content, - flags=re.MULTILINE, - ) - - if rel_count == 0: - print("[WARN] No 'Release:' line found in spec file.") - - if preview: - print( - "[PREVIEW] Would update spec file " - f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..." - ) - return - - with open(spec_path, "w", encoding="utf-8") as f: - f.write(new_content) - - print( - f"Updated spec file {os.path.basename(spec_path)} " - f"to Version: {new_version}, Release: 1..." - ) - - -def update_changelog( - changelog_path: str, - new_version: str, - message: Optional[str] = None, - preview: bool = False, -) -> str: - """ - Prepend a new release section to CHANGELOG.md with the new version, - current date, and a message. - """ - today = date.today().isoformat() - - if message is None: - if preview: - message = "Automated release." - else: - print( - "\n[INFO] No release message provided, opening editor for " - "changelog entry...\n" - ) - editor_message = _open_editor_for_changelog() - if not editor_message: - message = "Automated release." - else: - message = editor_message - - header = f"## [{new_version}] - {today}\n" - header += f"\n* {message}\n\n" - - if os.path.exists(changelog_path): - try: - with open(changelog_path, "r", encoding="utf-8") as f: - changelog = f.read() - except Exception as exc: - print(f"[WARN] Could not read existing CHANGELOG.md: {exc}") - changelog = "" - else: - changelog = "" - - new_changelog = header + "\n" + changelog if changelog else header - - print("\n================ CHANGELOG ENTRY ================") - print(header.rstrip()) - print("=================================================\n") - - if preview: - print(f"[PREVIEW] Would prepend new entry for {new_version} to CHANGELOG.md") - return message - - with open(changelog_path, "w", encoding="utf-8") as f: - f.write(new_changelog) - - print(f"Updated CHANGELOG.md with version {new_version}") - - return message - - -# --------------------------------------------------------------------------- -# Debian changelog helpers (with Git config fallback for maintainer) -# --------------------------------------------------------------------------- - - -def _get_debian_author() -> Tuple[str, str]: - """ - Determine the maintainer name/email for debian/changelog entries. - """ - name = os.environ.get("DEBFULLNAME") - email = os.environ.get("DEBEMAIL") - - if not name: - name = os.environ.get("GIT_AUTHOR_NAME") - if not email: - email = os.environ.get("GIT_AUTHOR_EMAIL") - - if not name: - name = get_config_value("user.name") - if not email: - email = get_config_value("user.email") - - if not name: - name = "Unknown Maintainer" - if not email: - email = "unknown@example.com" - - return name, email - - -def update_debian_changelog( - debian_changelog_path: str, - package_name: str, - new_version: str, - message: Optional[str] = None, - preview: bool = False, -) -> None: - """ - Prepend a new entry to debian/changelog, if it exists. - """ - if not os.path.exists(debian_changelog_path): - print("[INFO] debian/changelog not found, skipping.") - return - - debian_version = f"{new_version}-1" - now = datetime.now().astimezone() - date_str = now.strftime("%a, %d %b %Y %H:%M:%S %z") - - author_name, author_email = _get_debian_author() - - first_line = f"{package_name} ({debian_version}) unstable; urgency=medium" - body_line = message.strip() if message else f"Automated release {new_version}." - stanza = ( - f"{first_line}\n\n" - f" * {body_line}\n\n" - f" -- {author_name} <{author_email}> {date_str}\n\n" - ) - - if preview: - print( - "[PREVIEW] Would prepend the following stanza to debian/changelog:\n" - f"{stanza}" - ) - return - - try: - with open(debian_changelog_path, "r", encoding="utf-8") as f: - existing = f.read() - except Exception as exc: - print(f"[WARN] Could not read debian/changelog: {exc}") - existing = "" - - new_content = stanza + existing - - with open(debian_changelog_path, "w", encoding="utf-8") as f: - f.write(new_content) - - print(f"Updated debian/changelog with version {debian_version}") - - -# --------------------------------------------------------------------------- -# Fedora / RPM spec %changelog helper -# --------------------------------------------------------------------------- - - -def update_spec_changelog( - spec_path: str, - package_name: str, - new_version: str, - message: Optional[str] = None, - preview: bool = False, -) -> None: - """ - Prepend a new entry to the %changelog section of an RPM spec file, - if present. - - Typical RPM-style entry: - - * Tue Dec 09 2025 John Doe - 0.5.1-1 - - Your changelog message - """ - if not os.path.exists(spec_path): - print("[INFO] RPM spec file not found, skipping spec changelog update.") - return - - try: - with open(spec_path, "r", encoding="utf-8") as f: - content = f.read() - except Exception as exc: - print(f"[WARN] Could not read spec file for changelog update: {exc}") - return - - debian_version = f"{new_version}-1" - now = datetime.now().astimezone() - date_str = now.strftime("%a %b %d %Y") - - # Reuse Debian maintainer discovery for author name/email. - author_name, author_email = _get_debian_author() - - body_line = message.strip() if message else f"Automated release {new_version}." - - stanza = ( - f"* {date_str} {author_name} <{author_email}> - {debian_version}\n" - f"- {body_line}\n\n" - ) - - marker = "%changelog" - idx = content.find(marker) - - if idx == -1: - # No %changelog section yet: append one at the end. - new_content = content.rstrip() + "\n\n%changelog\n" + stanza - else: - # Insert stanza right after the %changelog line. - before = content[: idx + len(marker)] - after = content[idx + len(marker) :] - new_content = before + "\n" + stanza + after.lstrip("\n") - - if preview: - print( - "[PREVIEW] Would update RPM %changelog section with the following " - "stanza:\n" - f"{stanza}" - ) - return - - try: - with open(spec_path, "w", encoding="utf-8") as f: - f.write(new_content) - except Exception as exc: - print(f"[WARN] Failed to write updated spec changelog section: {exc}") - return - - print( - f"Updated RPM %changelog section in {os.path.basename(spec_path)} " - f"for {package_name} {debian_version}" - ) diff --git a/src/pkgmgr/actions/release/files/__init__.py b/src/pkgmgr/actions/release/files/__init__.py new file mode 100644 index 0000000..7556384 --- /dev/null +++ b/src/pkgmgr/actions/release/files/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Backwards-compatible facade for the release file update helpers. + +Implementations live in this package: + pkgmgr.actions.release.files.* + +Keep this package stable so existing imports continue to work, e.g.: + from pkgmgr.actions.release.files import update_pyproject_version +""" + +from __future__ import annotations + +from .editor import _open_editor_for_changelog +from .pyproject import update_pyproject_version +from .flake import update_flake_version +from .pkgbuild import update_pkgbuild_version +from .rpm_spec import update_spec_version +from .changelog_md import update_changelog +from .debian import _get_debian_author, update_debian_changelog +from .rpm_changelog import update_spec_changelog + +__all__ = [ + "_open_editor_for_changelog", + "update_pyproject_version", + "update_flake_version", + "update_pkgbuild_version", + "update_spec_version", + "update_changelog", + "_get_debian_author", + "update_debian_changelog", + "update_spec_changelog", +] diff --git a/src/pkgmgr/actions/release/files/changelog_md.py b/src/pkgmgr/actions/release/files/changelog_md.py new file mode 100644 index 0000000..80db2ed --- /dev/null +++ b/src/pkgmgr/actions/release/files/changelog_md.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import os +from datetime import date +from typing import Optional + +from .editor import _open_editor_for_changelog + + +def update_changelog( + changelog_path: str, + new_version: str, + message: Optional[str] = None, + preview: bool = False, +) -> str: + """ + Prepend a new release section to CHANGELOG.md with the new version, + current date, and a message. + """ + today = date.today().isoformat() + + if message is None: + if preview: + message = "Automated release." + else: + print( + "\n[INFO] No release message provided, opening editor for changelog entry...\n" + ) + editor_message = _open_editor_for_changelog() + if not editor_message: + message = "Automated release." + else: + message = editor_message + + header = f"## [{new_version}] - {today}\n" + header += f"\n* {message}\n\n" + + if os.path.exists(changelog_path): + try: + with open(changelog_path, "r", encoding="utf-8") as f: + changelog = f.read() + except Exception as exc: + print(f"[WARN] Could not read existing CHANGELOG.md: {exc}") + changelog = "" + else: + changelog = "" + + new_changelog = header + "\n" + changelog if changelog else header + + print("\n================ CHANGELOG ENTRY ================") + print(header.rstrip()) + print("=================================================\n") + + if preview: + print(f"[PREVIEW] Would prepend new entry for {new_version} to CHANGELOG.md") + return message + + with open(changelog_path, "w", encoding="utf-8") as f: + f.write(new_changelog) + + print(f"Updated CHANGELOG.md with version {new_version}") + return message diff --git a/src/pkgmgr/actions/release/files/debian.py b/src/pkgmgr/actions/release/files/debian.py new file mode 100644 index 0000000..e6765c1 --- /dev/null +++ b/src/pkgmgr/actions/release/files/debian.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import os +from datetime import datetime +from typing import Optional, Tuple + +from pkgmgr.core.git.queries import get_config_value + + +def _get_debian_author() -> Tuple[str, str]: + name = os.environ.get("DEBFULLNAME") + email = os.environ.get("DEBEMAIL") + + if not name: + name = os.environ.get("GIT_AUTHOR_NAME") + if not email: + email = os.environ.get("GIT_AUTHOR_EMAIL") + + if not name: + name = get_config_value("user.name") + if not email: + email = get_config_value("user.email") + + if not name: + name = "Unknown Maintainer" + if not email: + email = "unknown@example.com" + + return name, email + + +def update_debian_changelog( + debian_changelog_path: str, + package_name: str, + new_version: str, + message: Optional[str] = None, + preview: bool = False, +) -> None: + if not os.path.exists(debian_changelog_path): + print("[INFO] debian/changelog not found, skipping.") + return + + debian_version = f"{new_version}-1" + now = datetime.now().astimezone() + date_str = now.strftime("%a, %d %b %Y %H:%M:%S %z") + + author_name, author_email = _get_debian_author() + + first_line = f"{package_name} ({debian_version}) unstable; urgency=medium" + body_line = message.strip() if message else f"Automated release {new_version}." + stanza = ( + f"{first_line}\n\n" + f" * {body_line}\n\n" + f" -- {author_name} <{author_email}> {date_str}\n\n" + ) + + if preview: + print( + "[PREVIEW] Would prepend the following stanza to debian/changelog:\n" + f"{stanza}" + ) + return + + try: + with open(debian_changelog_path, "r", encoding="utf-8") as f: + existing = f.read() + except Exception as exc: + print(f"[WARN] Could not read debian/changelog: {exc}") + existing = "" + + with open(debian_changelog_path, "w", encoding="utf-8") as f: + f.write(stanza + existing) + + print(f"Updated debian/changelog with version {debian_version}") diff --git a/src/pkgmgr/actions/release/files/editor.py b/src/pkgmgr/actions/release/files/editor.py new file mode 100644 index 0000000..3f28686 --- /dev/null +++ b/src/pkgmgr/actions/release/files/editor.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import os +import subprocess +import tempfile +from typing import Optional + + +def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str: + editor = os.environ.get("EDITOR", "nano") + + with tempfile.NamedTemporaryFile( + mode="w+", + delete=False, + encoding="utf-8", + ) as tmp: + tmp_path = tmp.name + tmp.write( + "# Write the changelog entry for this release.\n" + "# Lines starting with '#' will be ignored.\n" + "# Empty result will fall back to a generic message.\n\n" + ) + if initial_message: + tmp.write(initial_message.strip() + "\n") + tmp.flush() + + try: + subprocess.call([editor, tmp_path]) + except FileNotFoundError: + print( + f"[WARN] Editor {editor!r} not found; proceeding without " + "interactive changelog message." + ) + + try: + with open(tmp_path, "r", encoding="utf-8") as f: + content = f.read() + finally: + try: + os.remove(tmp_path) + except OSError: + pass + + lines = [line for line in content.splitlines() if not line.strip().startswith("#")] + return "\n".join(lines).strip() diff --git a/src/pkgmgr/actions/release/files/flake.py b/src/pkgmgr/actions/release/files/flake.py new file mode 100644 index 0000000..a3207a0 --- /dev/null +++ b/src/pkgmgr/actions/release/files/flake.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import os +import re + + +def update_flake_version( + flake_path: str, new_version: str, preview: bool = False +) -> None: + if not os.path.exists(flake_path): + print("[INFO] flake.nix not found, skipping.") + return + + try: + with open(flake_path, "r", encoding="utf-8") as f: + content = f.read() + except Exception as exc: + print(f"[WARN] Could not read flake.nix: {exc}") + return + + pattern = r'(version\s*=\s*")([^"]+)(")' + new_content, count = re.subn( + pattern, + lambda m: f"{m.group(1)}{new_version}{m.group(3)}", + content, + ) + + if count == 0: + print("[WARN] No version found in flake.nix.") + return + + if preview: + print(f"[PREVIEW] Would update flake.nix version to {new_version}") + return + + with open(flake_path, "w", encoding="utf-8") as f: + f.write(new_content) + + print(f"Updated flake.nix version to {new_version}") diff --git a/src/pkgmgr/actions/release/files/pkgbuild.py b/src/pkgmgr/actions/release/files/pkgbuild.py new file mode 100644 index 0000000..b28556e --- /dev/null +++ b/src/pkgmgr/actions/release/files/pkgbuild.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import os +import re + + +def update_pkgbuild_version( + pkgbuild_path: str, new_version: str, preview: bool = False +) -> None: + if not os.path.exists(pkgbuild_path): + print("[INFO] PKGBUILD not found, skipping.") + return + + try: + with open(pkgbuild_path, "r", encoding="utf-8") as f: + content = f.read() + except Exception as exc: + print(f"[WARN] Could not read PKGBUILD: {exc}") + return + + content, _ = re.subn( + r"^(pkgver\s*=\s*)(.+)$", + lambda m: f"{m.group(1)}{new_version}", + content, + flags=re.MULTILINE, + ) + content, _ = re.subn( + r"^(pkgrel\s*=\s*)(.+)$", + lambda m: f"{m.group(1)}1", + content, + flags=re.MULTILINE, + ) + + if preview: + print(f"[PREVIEW] Would update PKGBUILD to pkgver={new_version}, pkgrel=1") + return + + with open(pkgbuild_path, "w", encoding="utf-8") as f: + f.write(content) + + print(f"Updated PKGBUILD to pkgver={new_version}, pkgrel=1") diff --git a/src/pkgmgr/actions/release/files/pyproject.py b/src/pkgmgr/actions/release/files/pyproject.py new file mode 100644 index 0000000..afefb39 --- /dev/null +++ b/src/pkgmgr/actions/release/files/pyproject.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import os +import re + + +def update_pyproject_version( + pyproject_path: str, new_version: str, preview: bool = False +) -> None: + if not os.path.exists(pyproject_path): + print(f"[INFO] pyproject.toml not found at: {pyproject_path}, skipping.") + return + + try: + with open(pyproject_path, "r", encoding="utf-8") as f: + content = f.read() + except OSError as exc: + print(f"[WARN] Could not read pyproject.toml: {exc}") + return + + m = re.search(r"(?ms)^\s*\[project\]\s*$.*?(?=^\s*\[|\Z)", content) + if not m: + raise RuntimeError("Missing [project] section in pyproject.toml") + + project_block = m.group(0) + ver_pat = r'(?m)^(\s*version\s*=\s*")([^"]+)(")\s*$' + + new_block, count = re.subn( + ver_pat, + lambda mm: f"{mm.group(1)}{new_version}{mm.group(3)}", + project_block, + ) + if count == 0: + raise RuntimeError("Missing version key in [project] section") + + new_content = content[: m.start()] + new_block + content[m.end() :] + + if preview: + print(f"[PREVIEW] Would update pyproject.toml version to {new_version}") + return + + with open(pyproject_path, "w", encoding="utf-8") as f: + f.write(new_content) + + print(f"Updated pyproject.toml version to {new_version}") diff --git a/src/pkgmgr/actions/release/files/rpm_changelog.py b/src/pkgmgr/actions/release/files/rpm_changelog.py new file mode 100644 index 0000000..eda7599 --- /dev/null +++ b/src/pkgmgr/actions/release/files/rpm_changelog.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import os +from datetime import datetime +from typing import Optional + +from .debian import _get_debian_author + + +def update_spec_changelog( + spec_path: str, + package_name: str, + new_version: str, + message: Optional[str] = None, + preview: bool = False, +) -> None: + if not os.path.exists(spec_path): + print("[INFO] RPM spec file not found, skipping spec changelog update.") + return + + try: + with open(spec_path, "r", encoding="utf-8") as f: + content = f.read() + except Exception as exc: + print(f"[WARN] Could not read spec file for changelog update: {exc}") + return + + debian_version = f"{new_version}-1" + now = datetime.now().astimezone() + date_str = now.strftime("%a %b %d %Y") + + author_name, author_email = _get_debian_author() + body_line = message.strip() if message else f"Automated release {new_version}." + + stanza = ( + f"* {date_str} {author_name} <{author_email}> - {debian_version}\n" + f"- {body_line}\n\n" + ) + + marker = "%changelog" + idx = content.find(marker) + + if idx == -1: + new_content = content.rstrip() + "\n\n%changelog\n" + stanza + else: + before = content[: idx + len(marker)] + after = content[idx + len(marker) :] + new_content = before + "\n" + stanza + after.lstrip("\n") + + if preview: + print( + "[PREVIEW] Would update RPM %changelog section with the following stanza:\n" + f"{stanza}" + ) + return + + try: + with open(spec_path, "w", encoding="utf-8") as f: + f.write(new_content) + except Exception as exc: + print(f"[WARN] Failed to write updated spec changelog section: {exc}") + return + + print( + f"Updated RPM %changelog section in {os.path.basename(spec_path)} " + f"for {package_name} {debian_version}" + ) diff --git a/src/pkgmgr/actions/release/files/rpm_spec.py b/src/pkgmgr/actions/release/files/rpm_spec.py new file mode 100644 index 0000000..b687cec --- /dev/null +++ b/src/pkgmgr/actions/release/files/rpm_spec.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import os +import re + + +def update_spec_version( + spec_path: str, new_version: str, preview: bool = False +) -> None: + """ + Update the version in an RPM spec file, if present. + """ + if not os.path.exists(spec_path): + print("[INFO] RPM spec file not found, skipping.") + return + + try: + with open(spec_path, "r", encoding="utf-8") as f: + content = f.read() + except Exception as exc: + print(f"[WARN] Could not read spec file: {exc}") + return + + ver_pattern = r"^(Version:\s*)(.+)$" + new_content, ver_count = re.subn( + ver_pattern, + lambda m: f"{m.group(1)}{new_version}", + content, + flags=re.MULTILINE, + ) + + if ver_count == 0: + print("[WARN] No 'Version:' line found in spec file.") + + rel_pattern = r"^(Release:\s*)(.+)$" + + def _release_repl(m: re.Match[str]) -> str: + rest = m.group(2).strip() + match = re.match(r"^(\d+)(.*)$", rest) + suffix = match.group(2) if match else "" + return f"{m.group(1)}1{suffix}" + + new_content, rel_count = re.subn( + rel_pattern, + _release_repl, + new_content, + flags=re.MULTILINE, + ) + + if rel_count == 0: + print("[WARN] No 'Release:' line found in spec file.") + + if preview: + print( + "[PREVIEW] Would update spec file " + f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..." + ) + return + + with open(spec_path, "w", encoding="utf-8") as f: + f.write(new_content) + + print( + f"Updated spec file {os.path.basename(spec_path)} " + f"to Version: {new_version}, Release: 1..." + )