diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b1fbe..7024afa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.2.0] - 2025-12-08 + +* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630) + + ## [0.1.0] - 2025-12-08 * Updated to correct version diff --git a/PKGBUILD b/PKGBUILD index d31ae4e..0fd3b30 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kevin Veen-Birkenbach pkgname=package-manager -pkgver=0.1.0 +pkgver=0.2.0 pkgrel=1 pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." arch=('any') diff --git a/debian/changelog b/debian/changelog index 50a82a3..0911570 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +package-manager (0.2.0-1) unstable; urgency=medium + + * Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630) + + -- Kevin Veen-Birkenbach Mon, 08 Dec 2025 20:31:19 +0100 + package-manager (0.1.0-1) unstable; urgency=medium * Updated to correct version diff --git a/flake.nix b/flake.nix index 9cd78da..e83ead8 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ rec { pkgmgr = pyPkgs.buildPythonApplication { pname = "package-manager"; - version = "0.1.0"; + version = "0.2.0"; # Use the git repo as source src = ./.; diff --git a/package-manager.spec b/package-manager.spec index 9894281..49eab11 100644 --- a/package-manager.spec +++ b/package-manager.spec @@ -1,5 +1,5 @@ Name: package-manager -Version: 0.1.0 +Version: 0.2.0 Release: 1%{?dist} Summary: Wrapper that runs Kevin's package-manager via Nix flake diff --git a/pkgmgr/release.py b/pkgmgr/release.py index a54dd3c..863654e 100644 --- a/pkgmgr/release.py +++ b/pkgmgr/release.py @@ -18,6 +18,11 @@ Additional behaviour: - If `preview=True` (from --preview), no files are written and no Git commands are executed. Instead, a detailed summary of the planned changes and commands is printed. + - If `preview=False` and not forced, the release is executed in two + phases: + 1) Preview-only run (dry-run). + 2) Interactive confirmation, then real release if confirmed. + This confirmation can be skipped with the `-f/--force` flag. """ from __future__ import annotations @@ -598,11 +603,11 @@ def update_debian_changelog( # --------------------------------------------------------------------------- -# Public release entry point +# Internal implementation (single-phase, preview or real) # --------------------------------------------------------------------------- -def release( +def _release_impl( pyproject_path: str = "pyproject.toml", changelog_path: str = "CHANGELOG.md", release_type: str = "patch", @@ -610,22 +615,16 @@ def release( preview: bool = False, ) -> None: """ - Perform a release by: + Internal implementation that performs a single-phase release. - 1. Determining the current version from Git tags. - 2. Computing the next version (major/minor/patch). - 3. Updating pyproject.toml with the new version. - 4. Updating CHANGELOG.md with a new entry. - 5. Updating additional packaging files where present: - - flake.nix - - PKGBUILD - - debian/changelog - - package-manager.spec - 6. Staging all these files. - 7. Committing, tagging, and pushing the changes. + If `preview` is True: + - No files are written. + - No git commands are executed. + - Planned actions are printed. - If `preview` is True, no files are written and no Git commands - are executed. Instead, the planned actions are printed. + If `preview` is False: + - Files are updated. + - Git commit, tag, and push are executed. """ # 1) Determine the current version from Git tags. current_ver = _determine_current_version() @@ -716,6 +715,109 @@ def release( print(f"Release {new_ver_str} completed.") +# --------------------------------------------------------------------------- +# Public release entry point (with preview-first + confirmation logic) +# --------------------------------------------------------------------------- + + +def release( + pyproject_path: str = "pyproject.toml", + changelog_path: str = "CHANGELOG.md", + release_type: str = "patch", + message: Optional[str] = None, + preview: bool = False, + force: bool = False, +) -> None: + """ + High-level release entry point. + + Modes: + + - preview=True: + * Single-phase PREVIEW only. + * No files are changed, no git commands are executed. + * `force` is ignored in this mode. + + - preview=False, force=True: + * Single-phase REAL release, no interactive preview. + * Files are changed and git commands are executed immediately. + + - preview=False, force=False: + * 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(). + """ + # Explicit preview mode: just do a single PREVIEW phase and exit. + if preview: + _release_impl( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=release_type, + message=message, + preview=True, + ) + return + + # Non-preview, but forced: run REAL release directly. + if force: + _release_impl( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=release_type, + message=message, + preview=False, + ) + return + + # Non-interactive environment? Skip confirmation to avoid blocking. + if not sys.stdin.isatty(): + _release_impl( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=release_type, + message=message, + preview=False, + ) + return + + # Interactive two-phase flow: + print("[INFO] Running preview before actual release...\n") + _release_impl( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=release_type, + message=message, + preview=True, + ) + + # Ask for confirmation + try: + answer = input("Proceed with the actual release? [y/N]: ").strip().lower() + except (EOFError, KeyboardInterrupt): + print("\n[INFO] Release aborted (no confirmation).") + return + + if answer not in ("y", "yes"): + print("Release aborted by user. No changes were made.") + return + + print("\n[INFO] Running REAL release...\n") + _release_impl( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=release_type, + message=message, + preview=False, + ) + + # --------------------------------------------------------------------------- # CLI entry point for standalone use # --------------------------------------------------------------------------- @@ -750,7 +852,20 @@ def _parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace: parser.add_argument( "--preview", action="store_true", - help="Preview release changes without modifying files or running git.", + 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." + ), ) return parser.parse_args(argv) @@ -763,4 +878,5 @@ if __name__ == "__main__": release_type=args.release_type, message=args.message, preview=args.preview, + force=args.force, ) diff --git a/pyproject.toml b/pyproject.toml index 3485266..f267913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "package-manager" -version = "0.1.0" +version = "0.2.0" description = "Kevin's package-manager tool (pkgmgr)" readme = "README.md" requires-python = ">=3.11"