Compare commits

...
2 Commits
Author SHA1 Message Date
Kevin Veen-Birkenbach a6c40451fe Release version 1.13.4
Mark stable commit / mark-stable (push) Canceled after 0s
2026-05-27 20:32:39 +02:00
Kevin Veen-BirkenbachandClaude Opus 4.7 5fa2709a84 feat(release): resolve package name from packaging files, not folder
Previously the release workflow derived the distro-package name from
`os.path.basename(repo_root)`. Renaming the repo folder (e.g.
`infinito-nexus` → `infinito-nexus-core`) silently rewrote
`debian/changelog`'s top entry to the new folder name while
`debian/control` still pinned the legacy `Package:` value. dpkg-source
refuses to build a source package when the two disagree.

Add `resolve_package_name(paths)` that consults the existing packaging
files in priority order (debian/control `Package:` → PKGBUILD
`pkgname=` → RPM `.spec` `Name:`) and only falls back to the folder
basename when no packaging metadata is present. Extend `RepoPaths`
with a `debian_control` slot so `resolve_repo_paths` can discover the
file under both `packaging/debian/control` and the legacy `debian/`
location.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:27:01 +02:00
12 changed files with 304 additions and 13 deletions

No files matched your search

+31
View File
@@ -1,3 +1,34 @@
## [1.13.4] - 2026-05-27
* Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
## [1.13.3] - 2026-03-26 ## [1.13.3] - 2026-03-26
* CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations * CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
+1 -1
View File
@@ -32,7 +32,7 @@
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "1.13.3"; version = "1.13.4";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;
+1 -1
View File
@@ -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=1.13.3 pkgver=1.13.4
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')
+32
View File
@@ -1,3 +1,35 @@
package-manager (1.13.4-1) unstable; urgency=medium
* Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 27 May 2026 20:32:39 +0200
package-manager (1.13.3-1) unstable; urgency=medium package-manager (1.13.3-1) unstable; urgency=medium
* CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations * CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
+30 -1
View File
@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 1.13.3 Version: 1.13.4
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
@@ -74,6 +74,35 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Wed May 27 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.4-1
- Changed
* pkgmgr release now derives the distro-package name from existing
packaging metadata instead of the repository folder name. The lookup
order is packaging/debian/control Package field, then
packaging/arch/PKGBUILD pkgname value, then RPM spec Name field, then
folder basename as legacy fallback. Renaming a repository folder (for
example infinito-nexus to infinito-nexus-core) no longer silently
flips the debian/changelog top entry and the RPM changelog stanza to
a new identifier. Those keep matching the authoritative Package,
pkgname, or Name value in the packaging files, which is what apt,
pacman, and dnf index against.
Added
* RepoPaths gains a debian_control slot that is discovered alongside
debian_changelog under both packaging/debian (new layout) and debian
(legacy layout).
* pkgmgr.actions.release.package_name.resolve_package_name centralises
the priority chain and is unit-tested under
tests/unit/pkgmgr/actions/release/test_package_name.py.
Fixed
* dpkg-source --before-build no longer fails with the message about
source package having two conflicting values after a repo-folder
rename, because the changelog and control file stay in agreement.
* Thu Mar 26 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.3-1 * Thu Mar 26 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.13.3-1
- CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations - CI pipelines now include automated security scanning (CodeQL, Docker lint), increasing detection of vulnerabilities and misconfigurations
* Workflow permissions were tightened and fixed, ensuring secure and reliable execution of reusable workflows * Workflow permissions were tightened and fixed, ensuring secure and reliable execution of reusable workflows
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kpmx" name = "kpmx"
version = "1.13.3" version = "1.13.4"
description = "Kevin's package-manager tool (pkgmgr)" description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -0,0 +1,75 @@
"""Resolve the distro-package name for a release.
The release flow writes the package identifier into `debian/changelog`,
the RPM `%changelog` stanza, etc. Historically pkgmgr derived this
identifier from the repository folder name (`os.path.basename(repo_root)`),
which silently breaks when the repo is renamed but the existing packaging
files still ship the legacy name. Renaming the folder must not change the
distro-package identity — `apt`, `pacman`, `dnf`, and every downstream
manifest pin the old name.
The resolver therefore walks the existing packaging files in priority
order and only falls back to the folder name when none of them ship an
explicit name.
Priority:
1. `debian/control` `Package:` field (most authoritative — dpkg-source
refuses to build if changelog and control disagree)
2. `packaging/arch/PKGBUILD` `pkgname=` value
3. RPM spec `Name:` field
4. Repository folder basename (legacy fallback)
"""
from __future__ import annotations
import os
import re
from typing import Optional
from pkgmgr.core.repository.paths import RepoPaths
_DEBIAN_PACKAGE_RE = re.compile(r"^Package:\s*(\S+)\s*$", re.MULTILINE)
_PKGBUILD_NAME_RE = re.compile(r"^pkgname=([^\s#]+)\s*$", re.MULTILINE)
_RPM_NAME_RE = re.compile(r"^Name:\s*(\S+)\s*$", re.MULTILINE)
def _read(path: Optional[str]) -> str:
if not path or not os.path.isfile(path):
return ""
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except OSError:
return ""
def _extract(pattern: re.Pattern[str], text: str) -> Optional[str]:
if not text:
return None
match = pattern.search(text)
if not match:
return None
value = match.group(1).strip().strip('"').strip("'")
return value or None
def resolve_package_name(paths: RepoPaths) -> str:
"""Return the distro-package name for the repo, with a folder fallback.
The fallback uses `os.path.basename(paths.repo_dir)` so behaviour is
backwards-compatible for repos that ship no packaging metadata yet.
"""
debian_name = _extract(_DEBIAN_PACKAGE_RE, _read(paths.debian_control))
if debian_name:
return debian_name
pkgbuild_name = _extract(_PKGBUILD_NAME_RE, _read(paths.arch_pkgbuild))
if pkgbuild_name:
return pkgbuild_name
rpm_name = _extract(_RPM_NAME_RE, _read(paths.rpm_spec))
if rpm_name:
return rpm_name
return os.path.basename(paths.repo_dir) or "package"
+2 -1
View File
@@ -24,6 +24,7 @@ from .git_ops import (
is_highest_version_tag, is_highest_version_tag,
update_latest_tag, update_latest_tag,
) )
from .package_name import resolve_package_name
from .prompts import confirm_proceed_release, should_delete_branch from .prompts import confirm_proceed_release, should_delete_branch
from .versioning import bump_semver, determine_current_version from .versioning import bump_semver, determine_current_version
@@ -90,7 +91,7 @@ def _release_impl(
if changelog_message.strip(): if changelog_message.strip():
effective_message = changelog_message.strip() effective_message = changelog_message.strip()
package_name = os.path.basename(repo_root) or "package-manager" package_name = resolve_package_name(paths)
if paths.debian_changelog: if paths.debian_changelog:
update_debian_changelog( update_debian_changelog(
+1 -3
View File
@@ -83,9 +83,7 @@ def run_on_repos(
if failed: if failed:
if effective_jobs > 1: if effective_jobs > 1:
print( print(f"\n[SUMMARY] {len(failed)} of {len(repos)} {op_name}(s) failed:")
f"\n[SUMMARY] {len(failed)} of {len(repos)} {op_name}(s) failed:"
)
for ident, _msg in failed: for ident, _msg in failed:
print(f" - {ident}") print(f" - {ident}")
sys.exit(1) sys.exit(1)
+11 -5
View File
@@ -28,7 +28,10 @@ def _verify_one(
) -> Tuple[bool, bool, List[str]]: ) -> Tuple[bool, bool, List[str]]:
"""Returns (has_verified_info, verified_ok, errors).""" """Returns (has_verified_info, verified_ok, errors)."""
verified_ok, errors, _commit, _key = verify_repository( verified_ok, errors, _commit, _key = verify_repository(
repo, repo_dir, mode="pull", no_verification=no_verification, repo,
repo_dir,
mode="pull",
no_verification=no_verification,
) )
return (bool(repo.get("verified")), verified_ok, errors) return (bool(repo.get("verified")), verified_ok, errors)
@@ -56,9 +59,7 @@ def _verify_all(
for repo, _ident, rd in candidates for repo, _ident, rd in candidates
] ]
results = [f.result() for f in futures] results = [f.result() for f in futures]
return [ return [(ident, rd, *res) for (_repo, ident, rd), res in zip(candidates, results)]
(ident, rd, *res) for (_repo, ident, rd), res in zip(candidates, results)
]
def pull_with_verification( def pull_with_verification(
@@ -95,7 +96,12 @@ def pull_with_verification(
approved: List[RepoRef] = [] approved: List[RepoRef] = []
for ident, rd, has_verified_info, verified_ok, errors in verify_results: for ident, rd, has_verified_info, verified_ok, errors in verify_results:
if not preview and not no_verification and has_verified_info and not verified_ok: if (
not preview
and not no_verification
and has_verified_info
and not verified_ok
):
print(f"Warning: Verification failed for {ident}:") print(f"Warning: Verification failed for {ident}:")
for err in errors: for err in errors:
print(f" - {err}") print(f" - {err}")
+9
View File
@@ -36,6 +36,7 @@ class RepoPaths:
# Packaging-related files # Packaging-related files
arch_pkgbuild: Optional[str] arch_pkgbuild: Optional[str]
debian_changelog: Optional[str] debian_changelog: Optional[str]
debian_control: Optional[str]
rpm_spec: Optional[str] rpm_spec: Optional[str]
@@ -102,6 +103,13 @@ def resolve_repo_paths(repo_dir: str) -> RepoPaths:
] ]
) )
debian_control = _first_existing(
[
os.path.join(repo_dir, "packaging", "debian", "control"),
os.path.join(repo_dir, "debian", "control"),
]
)
# RPM spec: prefer the canonical file, else first spec in packaging/fedora, else first spec in repo root. # RPM spec: prefer the canonical file, else first spec in packaging/fedora, else first spec in repo root.
rpm_spec = _first_existing( rpm_spec = _first_existing(
[ [
@@ -122,5 +130,6 @@ def resolve_repo_paths(repo_dir: str) -> RepoPaths:
changelog_md=changelog_md, changelog_md=changelog_md,
arch_pkgbuild=arch_pkgbuild, arch_pkgbuild=arch_pkgbuild,
debian_changelog=debian_changelog, debian_changelog=debian_changelog,
debian_control=debian_control,
rpm_spec=rpm_spec, rpm_spec=rpm_spec,
) )
@@ -0,0 +1,110 @@
"""Unit tests for `pkgmgr.actions.release.package_name.resolve_package_name`.
The resolver must prefer the explicit name from existing packaging files
over the repository folder name so that renaming the folder does not
silently rename the distro package.
"""
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from typing import Optional
from pkgmgr.actions.release.package_name import resolve_package_name
from pkgmgr.core.repository.paths import RepoPaths
def _paths(
repo_dir: str,
*,
debian_control: Optional[str] = None,
arch_pkgbuild: Optional[str] = None,
rpm_spec: Optional[str] = None,
) -> RepoPaths:
return RepoPaths(
repo_dir=repo_dir,
pyproject_toml=os.path.join(repo_dir, "pyproject.toml"),
flake_nix=os.path.join(repo_dir, "flake.nix"),
changelog_md=None,
arch_pkgbuild=arch_pkgbuild,
debian_changelog=None,
debian_control=debian_control,
rpm_spec=rpm_spec,
)
class TestResolvePackageName(unittest.TestCase):
def test_debian_control_wins_over_folder(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
control = Path(repo) / "control"
control.write_text(
"Source: infinito-nexus\nPackage: infinito-nexus\n",
encoding="utf-8",
)
self.assertEqual(
resolve_package_name(_paths(repo, debian_control=str(control))),
"infinito-nexus",
)
def test_pkgbuild_used_when_no_control(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text(
"pkgname=infinito-nexus\npkgver=1.0\n", encoding="utf-8"
)
self.assertEqual(
resolve_package_name(_paths(repo, arch_pkgbuild=str(pkgbuild))),
"infinito-nexus",
)
def test_rpm_spec_used_when_no_control_no_pkgbuild(self) -> None:
with tempfile.TemporaryDirectory(prefix="infinito-nexus-core_") as repo:
spec = Path(repo) / "pkg.spec"
spec.write_text("Name: infinito-nexus\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(_paths(repo, rpm_spec=str(spec))),
"infinito-nexus",
)
def test_folder_fallback_when_no_packaging_metadata(self) -> None:
with tempfile.TemporaryDirectory(prefix="solo-tool_") as repo:
self.assertEqual(
resolve_package_name(_paths(repo)),
os.path.basename(repo),
)
def test_priority_debian_over_pkgbuild_over_spec(self) -> None:
with tempfile.TemporaryDirectory() as repo:
control = Path(repo) / "control"
control.write_text("Package: deb-name\n", encoding="utf-8")
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text("pkgname=arch-name\n", encoding="utf-8")
spec = Path(repo) / "x.spec"
spec.write_text("Name: rpm-name\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(
_paths(
repo,
debian_control=str(control),
arch_pkgbuild=str(pkgbuild),
rpm_spec=str(spec),
)
),
"deb-name",
)
def test_strips_quotes_in_pkgbuild(self) -> None:
with tempfile.TemporaryDirectory() as repo:
pkgbuild = Path(repo) / "PKGBUILD"
pkgbuild.write_text("pkgname='quoted-name'\n", encoding="utf-8")
self.assertEqual(
resolve_package_name(_paths(repo, arch_pkgbuild=str(pkgbuild))),
"quoted-name",
)
if __name__ == "__main__":
unittest.main()