Compare commits

...

3 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
7cfd7e8d5c Release version 1.6.3
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 / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-14 13:39:52 +01:00
Kevin Veen-Birkenbach
84b6c71748 test(integration): add unittest-based repository layout contract test
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 / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Add integration test using unittest to verify canonical repository paths
- Assert pkgmgr repository satisfies template layout (packaging, changelog, metadata)
- Use real filesystem without mocks or pytest dependencies

https://chatgpt.com/share/693eaa75-98f0-800f-adca-439555f84154
2025-12-14 13:26:18 +01:00
Kevin Veen-Birkenbach
db9aaf920e refactor(release,version): centralize repository path resolution and validate template layout
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 / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Introduce RepoPaths resolver as single source of truth for repository file locations
- Update release workflow to use resolved packaging and changelog paths
- Update version readers to rely on the shared path resolver
- Add integration test asserting pkgmgr repository satisfies canonical template layout

https://chatgpt.com/share/693eaa75-98f0-800f-adca-439555f84154
2025-12-14 13:15:41 +01:00
10 changed files with 296 additions and 62 deletions

View File

@@ -1,3 +1,8 @@
## [1.6.3] - 2025-12-14
* ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
## [1.6.2] - 2025-12-14
* **pkgmgr version** now also shows the installed pkgmgr version when run outside a repository.

View File

@@ -32,7 +32,7 @@
rec {
pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager";
version = "1.6.2";
version = "1.6.3";
# Use the git repo as source
src = ./.;

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager
pkgver=0.9.1
pkgver=1.6.3
pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any')

View File

@@ -1,3 +1,9 @@
package-manager (1.6.3-1) unstable; urgency=medium
* ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 14 Dec 2025 13:39:52 +0100
package-manager (0.9.1-1) unstable; urgency=medium
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.

View File

@@ -1,5 +1,5 @@
Name: package-manager
Version: 0.9.1
Version: 1.6.3
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -74,6 +74,9 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/
%changelog
* Sun Dec 14 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 1.6.3-1
- ***Fixed:*** Corrected repository path resolution so release and version logic consistently use the canonical packaging/* layout, preventing changelog and packaging files from being read or updated from incorrect locations.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.1-1
- * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "package-manager"
version = "1.6.2"
version = "1.6.3"
description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md"
requires-python = ">=3.9"

View File

@@ -1,10 +1,13 @@
# src/pkgmgr/actions/release/workflow.py
from __future__ import annotations
from typing import Optional
import os
import sys
from typing import Optional
from pkgmgr.actions.branch import close_branch
from pkgmgr.core.git import get_current_branch, GitError
from pkgmgr.core.repository.paths import resolve_repo_paths
from .files import (
update_changelog,
@@ -55,8 +58,12 @@ def _release_impl(
print(f"New version: {new_ver_str} ({release_type})")
repo_root = os.path.dirname(os.path.abspath(pyproject_path))
paths = resolve_repo_paths(repo_root)
# --- Update versioned files ------------------------------------------------
update_pyproject_version(pyproject_path, new_ver_str, preview=preview)
changelog_message = update_changelog(
changelog_path,
new_ver_str,
@@ -64,38 +71,46 @@ def _release_impl(
preview=preview,
)
flake_path = os.path.join(repo_root, "flake.nix")
update_flake_version(flake_path, new_ver_str, preview=preview)
update_flake_version(paths.flake_nix, new_ver_str, preview=preview)
pkgbuild_path = os.path.join(repo_root, "PKGBUILD")
update_pkgbuild_version(pkgbuild_path, new_ver_str, preview=preview)
if paths.arch_pkgbuild:
update_pkgbuild_version(paths.arch_pkgbuild, new_ver_str, preview=preview)
else:
print("[INFO] No PKGBUILD found (packaging/arch/PKGBUILD or PKGBUILD). Skipping.")
spec_path = os.path.join(repo_root, "package-manager.spec")
update_spec_version(spec_path, new_ver_str, preview=preview)
if paths.rpm_spec:
update_spec_version(paths.rpm_spec, new_ver_str, preview=preview)
else:
print("[INFO] No RPM spec file found. Skipping spec version update.")
effective_message: Optional[str] = message
if effective_message is None and isinstance(changelog_message, str):
if changelog_message.strip():
effective_message = changelog_message.strip()
debian_changelog_path = os.path.join(repo_root, "debian", "changelog")
package_name = os.path.basename(repo_root) or "package-manager"
update_debian_changelog(
debian_changelog_path,
package_name=package_name,
new_version=new_ver_str,
message=effective_message,
preview=preview,
)
if paths.debian_changelog:
update_debian_changelog(
paths.debian_changelog,
package_name=package_name,
new_version=new_ver_str,
message=effective_message,
preview=preview,
)
else:
print("[INFO] No debian changelog found. Skipping debian/changelog update.")
update_spec_changelog(
spec_path=spec_path,
package_name=package_name,
new_version=new_ver_str,
message=effective_message,
preview=preview,
)
if paths.rpm_spec:
update_spec_changelog(
spec_path=paths.rpm_spec,
package_name=package_name,
new_version=new_ver_str,
message=effective_message,
preview=preview,
)
# --- Git commit / tag / push ----------------------------------------------
commit_msg = f"Release version {new_ver_str}"
tag_msg = effective_message or commit_msg
@@ -103,12 +118,12 @@ def _release_impl(
files_to_add = [
pyproject_path,
changelog_path,
flake_path,
pkgbuild_path,
spec_path,
debian_changelog_path,
paths.flake_nix,
paths.arch_pkgbuild,
paths.rpm_spec,
paths.debian_changelog,
]
existing_files = [p for p in files_to_add if p and os.path.exists(p)]
existing_files = [p for p in files_to_add if isinstance(p, str) and p and os.path.exists(p)]
if preview:
for path in existing_files:

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Central repository path resolver.
Goal:
- Provide ONE place to define where packaging / changelog / metadata files live.
- Prefer modern layout (packaging/*) but stay backwards-compatible with legacy
root-level paths.
Both:
- readers (pkgmgr.core.version.source)
- writers (pkgmgr.actions.release.workflow)
should use this module instead of hardcoding paths.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Iterable, Optional
@dataclass(frozen=True)
class RepoPaths:
repo_dir: str
pyproject_toml: str
flake_nix: str
# Human changelog (typically Markdown)
changelog_md: Optional[str]
# Packaging-related files
arch_pkgbuild: Optional[str]
debian_changelog: Optional[str]
rpm_spec: Optional[str]
def _first_existing(candidates: Iterable[str]) -> Optional[str]:
for p in candidates:
if p and os.path.isfile(p):
return p
return None
def _find_first_spec_in_dir(dir_path: str) -> Optional[str]:
if not os.path.isdir(dir_path):
return None
try:
for fn in sorted(os.listdir(dir_path)):
if fn.endswith(".spec"):
p = os.path.join(dir_path, fn)
if os.path.isfile(p):
return p
except OSError:
return None
return None
def resolve_repo_paths(repo_dir: str) -> RepoPaths:
"""
Resolve canonical file locations for a repository.
Preferences (new layout first, legacy fallback second):
- PKGBUILD: packaging/arch/PKGBUILD -> PKGBUILD
- Debian changelog: packaging/debian/changelog -> debian/changelog
- RPM spec: packaging/fedora/package-manager.spec
-> first *.spec in packaging/fedora
-> first *.spec in repo root
- CHANGELOG.md: CHANGELOG.md -> packaging/CHANGELOG.md (optional fallback)
Notes:
- This resolver only returns paths; it does not read/parse files.
- Callers should treat Optional paths as "may not exist".
"""
repo_dir = os.path.abspath(repo_dir)
pyproject_toml = os.path.join(repo_dir, "pyproject.toml")
flake_nix = os.path.join(repo_dir, "flake.nix")
changelog_md = _first_existing(
[
os.path.join(repo_dir, "CHANGELOG.md"),
os.path.join(repo_dir, "packaging", "CHANGELOG.md"),
]
)
arch_pkgbuild = _first_existing(
[
os.path.join(repo_dir, "packaging", "arch", "PKGBUILD"),
os.path.join(repo_dir, "PKGBUILD"),
]
)
debian_changelog = _first_existing(
[
os.path.join(repo_dir, "packaging", "debian", "changelog"),
os.path.join(repo_dir, "debian", "changelog"),
]
)
# RPM spec: prefer the canonical file, else first spec in packaging/fedora, else first spec in repo root.
rpm_spec = _first_existing(
[
os.path.join(repo_dir, "packaging", "fedora", "package-manager.spec"),
]
)
if rpm_spec is None:
rpm_spec = _find_first_spec_in_dir(os.path.join(repo_dir, "packaging", "fedora"))
if rpm_spec is None:
rpm_spec = _find_first_spec_in_dir(repo_dir)
return RepoPaths(
repo_dir=repo_dir,
pyproject_toml=pyproject_toml,
flake_nix=flake_nix,
changelog_md=changelog_md,
arch_pkgbuild=arch_pkgbuild,
debian_changelog=debian_changelog,
rpm_spec=rpm_spec,
)

View File

@@ -1,3 +1,4 @@
# src/pkgmgr/core/version/source.py
from __future__ import annotations
import os
@@ -6,6 +7,8 @@ from typing import Optional
import yaml
from pkgmgr.core.repository.paths import resolve_repo_paths
def read_pyproject_version(repo_dir: str) -> Optional[str]:
"""
@@ -13,7 +16,8 @@ def read_pyproject_version(repo_dir: str) -> Optional[str]:
Expects a PEP 621-style [project] table with a 'version' field.
"""
path = os.path.join(repo_dir, "pyproject.toml")
paths = resolve_repo_paths(repo_dir)
path = paths.pyproject_toml
if not os.path.isfile(path):
return None
@@ -39,7 +43,8 @@ def read_pyproject_project_name(repo_dir: str) -> Optional[str]:
This is required to correctly resolve installed Python package
versions via importlib.metadata.
"""
path = os.path.join(repo_dir, "pyproject.toml")
paths = resolve_repo_paths(repo_dir)
path = paths.pyproject_toml
if not os.path.isfile(path):
return None
@@ -65,7 +70,8 @@ def read_flake_version(repo_dir: str) -> Optional[str]:
Looks for:
version = "X.Y.Z";
"""
path = os.path.join(repo_dir, "flake.nix")
paths = resolve_repo_paths(repo_dir)
path = paths.flake_nix
if not os.path.isfile(path):
return None
@@ -84,15 +90,16 @@ def read_flake_version(repo_dir: str) -> Optional[str]:
def read_pkgbuild_version(repo_dir: str) -> Optional[str]:
"""
Read the version from PKGBUILD in repo_dir.
Read the version from PKGBUILD (preferring packaging/arch/PKGBUILD).
Combines pkgver and pkgrel if both exist:
pkgver=1.2.3
pkgrel=1
-> 1.2.3-1
"""
path = os.path.join(repo_dir, "PKGBUILD")
if not os.path.isfile(path):
paths = resolve_repo_paths(repo_dir)
path = paths.arch_pkgbuild
if not path or not os.path.isfile(path):
return None
try:
@@ -117,13 +124,19 @@ def read_pkgbuild_version(repo_dir: str) -> Optional[str]:
def read_debian_changelog_version(repo_dir: str) -> Optional[str]:
"""
Read the latest version from debian/changelog.
Read the latest version from debian changelog.
Preferred path:
packaging/debian/changelog
Fallback:
debian/changelog
Expected format:
package (1.2.3-1) unstable; urgency=medium
"""
path = os.path.join(repo_dir, "debian", "changelog")
if not os.path.isfile(path):
paths = resolve_repo_paths(repo_dir)
path = paths.debian_changelog
if not path or not os.path.isfile(path):
return None
try:
@@ -146,37 +159,40 @@ def read_spec_version(repo_dir: str) -> Optional[str]:
"""
Read the version from an RPM spec file.
Preferred paths:
packaging/fedora/package-manager.spec
packaging/fedora/*.spec
repo_root/*.spec
Combines:
Version: 1.2.3
Release: 1%{?dist}
-> 1.2.3-1
"""
for fn in os.listdir(repo_dir):
if not fn.endswith(".spec"):
continue
paths = resolve_repo_paths(repo_dir)
path = paths.rpm_spec
if not path or not os.path.isfile(path):
return None
path = os.path.join(repo_dir, fn)
try:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
except Exception:
return None
try:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
except Exception:
return None
ver_match = re.search(r"^Version:\s*(.+)$", text, re.MULTILINE)
if not ver_match:
return None
version = ver_match.group(1).strip()
ver_match = re.search(r"^Version:\s*(.+)$", text, re.MULTILINE)
if not ver_match:
return None
version = ver_match.group(1).strip()
rel_match = re.search(r"^Release:\s*(.+)$", text, re.MULTILINE)
if rel_match:
release_raw = rel_match.group(1).strip()
release = release_raw.split("%", 1)[0].split(" ", 1)[0].strip()
if release:
return f"{version}-{release}"
rel_match = re.search(r"^Release:\s*(.+)$", text, re.MULTILINE)
if rel_match:
release_raw = rel_match.group(1).strip()
release = release_raw.split("%", 1)[0].split(" ", 1)[0].strip()
if release:
return f"{version}-{release}"
return version or None
return None
return version or None
def read_ansible_galaxy_version(repo_dir: str) -> Optional[str]:

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
import os
import unittest
from pathlib import Path
from pkgmgr.core.repository.paths import resolve_repo_paths
def _find_repo_root() -> Path:
"""
Locate the pkgmgr repository root from the test location.
Assumes:
repo_root/
src/pkgmgr/...
tests/integration/...
"""
here = Path(__file__).resolve()
for parent in here.parents:
if (parent / "pyproject.toml").is_file() and (parent / "src" / "pkgmgr").is_dir():
return parent
raise RuntimeError("Could not determine repository root for pkgmgr integration test")
class TestRepositoryPathsExist(unittest.TestCase):
"""
Integration test: pkgmgr is the TEMPLATE repository.
All canonical paths resolved for pkgmgr must exist.
"""
def test_pkgmgr_repository_paths_exist(self) -> None:
repo_root = _find_repo_root()
paths = resolve_repo_paths(str(repo_root))
missing: list[str] = []
def require(path: str | None, description: str) -> None:
if not path:
missing.append(f"{description}: <not resolved>")
return
if not os.path.isfile(path):
missing.append(f"{description}: {path} (missing)")
# Core metadata
require(paths.pyproject_toml, "pyproject.toml")
require(paths.flake_nix, "flake.nix")
# Human changelog
require(paths.changelog_md, "CHANGELOG.md")
# Packaging files (pkgmgr defines the template)
require(paths.arch_pkgbuild, "Arch PKGBUILD")
require(paths.debian_changelog, "Debian changelog")
require(paths.rpm_spec, "RPM spec file")
if missing:
self.fail(
"pkgmgr repository does not satisfy the canonical repository layout:\n"
+ "\n".join(f" - {item}" for item in missing)
)
if __name__ == "__main__":
unittest.main()