Compare commits

...

4 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
7bc7259988 Release version 0.7.4 2025-12-09 16:22:03 +01:00
Kevin Veen-Birkenbach
66b96ac3a5 Refactor CI workflows and Makefile to unify container builds and simplify test execution
This commit updates all GitHub Actions workflows and the Makefile to ensure
consistent behavior across unit, integration, end-to-end, and OS-container
tests.

Changes include:

CI Workflows:
  - Rename workflows for clearer, more professional naming:
        * "Test Distribution Containers" → "Test OS Containers"
        * "Test package-manager (e2e)" → "Test End-To-End"
        * "Test package-manager (unit)" → "Test Units"
        * "Test package-manager (integration)" → "Test Code Integration"
  - Remove explicit build steps from workflows; container creation is now
    delegated to the Makefile via build-missing.
  - Restrict test jobs to only build the Arch test container by setting:
        DISTROS="arch"

Makefile:
  - Add build-missing as a dependency to all test targets:
        test-unit, test-integration, test-e2e, test-container
  - Remove redundant build-missing call from the combined 'test' target,
    since Make now ensures build-missing runs exactly once per invocation.
  - Preserve existing target structure while ensuring container images are
    built automatically on demand.

This makes the CI pipeline faster, more predictable, and removes duplicated
container build logic. All tests now use the same unified mechanism for
building missing images.
2025-12-09 16:18:15 +01:00
Kevin Veen-Birkenbach
f974e0b14a Release version 0.7.3 2025-12-09 16:08:34 +01:00
Kevin Veen-Birkenbach
de8c3f768d feat(repository): integrate ignore filtering into selection pipeline + add unit tests
This commit introduces proper handling of the `ignore: true` flag in the
repository selection mechanism and adds comprehensive unit tests for both
`ignored.py` and `selected.py`.

- `get_selected_repos()` now filters ignored repositories in all implicit
  selection modes:
    • filter-only mode (string/category/tag)
    • `--all` mode
    • CWD-based selection

- Explicit identifiers (e.g. `pkgmgr install ignored-repo`) **bypass**
  ignore filtering, so the user can still operate directly on ignored
  repositories if they ask for them explicitly.

- Added `_maybe_filter_ignored()` helper to handle logic cleanly and allow
  future extension (e.g. integrating a CLI flag `--include-ignored`).

Under `tests/unit/pkgmgr/core/repository`:

1. **test_ignored.py**
   • Ensures `filter_ignored()` removes repos with `ignore: true`
   • Ensures empty lists are handled correctly

2. **test_selected.py**
   Comprehensive coverage of the selection logic:
   • Explicit identifiers bypass ignore filtering
   • Filter-only mode excludes ignored repos unless `include_ignored=True`
   • `--all` mode excludes ignored repos unless explicitly overridden
   • CWD-based detection filters ignored repos unless explicitly overridden

Before this change, ignored repositories still appeared in `pkgmgr list`,
`pkgmgr status`, and other commands using `get_selected_repos()`.
This was unintuitive and broke the expected semantics of the `ignore`
attribute.
The new logic ensures ignored repositories are truly invisible unless
explicitly requested.

https://chatgpt.com/share/69383b41-50a0-800f-a2b9-c680cd96d9e9
2025-12-09 16:07:39 +01:00
15 changed files with 292 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
name: Test Distribution Containers
name: Test OS Containers
on:
push:

View File

@@ -1,4 +1,4 @@
name: Test package-manager (e2e)
name: Test End-To-End
on:
push:

View File

@@ -1,4 +1,4 @@
name: Test package-manager (integration)
name: Test Code Integration
on:
push:
@@ -21,9 +21,5 @@ jobs:
- name: Show Docker version
run: docker version
# Build Arch test image (same as used in test-unit and test-e2e)
- name: Build test images
run: make build
- name: Run integration tests via make (Arch container)
run: make test-integration
run: make test-integration DISTROS="arch"

View File

@@ -1,4 +1,4 @@
name: Test package-manager (unit)
name: Test Units
on:
push:
@@ -22,4 +22,4 @@ jobs:
run: docker version
- name: Run unit tests via make (Arch container)
run: make test-unit
run: make test-unit DISTROS="arch"

View File

@@ -1,3 +1,13 @@
## [0.7.4] - 2025-12-09
* Fixed missing build in test workflow -> Tests pass now
## [0.7.3] - 2025-12-09
* Fixed bug: Ignored packages are now ignored
## [0.7.2] - 2025-12-09
* Implemented Changelog Support for Fedora and Debian

View File

@@ -46,16 +46,16 @@ build:
# Test targets (delegated to scripts/test)
# ------------------------------------------------------------
test-unit:
test-unit: build-missing
@bash scripts/test/test-unit.sh
test-integration:
test-integration: build-missing
@bash scripts/test/test-integration.sh
test-e2e:
test-e2e: build-missing
@bash scripts/test/test-e2e.sh
test-container:
test-container: build-missing
@bash scripts/test/test-container.sh
# ------------------------------------------------------------
@@ -65,7 +65,7 @@ build-missing:
@bash scripts/build/build-image-missing.sh
# Combined test target for local + CI (unit + e2e + integration)
test: build-missing test-container test-unit test-e2e test-integration
test: test-container test-unit test-e2e test-integration
# ------------------------------------------------------------
# System install (native packages, calls scripts/installation/run-package.sh)

View File

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

12
debian/changelog vendored
View File

@@ -1,3 +1,15 @@
package-manager (0.7.4-1) unstable; urgency=medium
* Fixed missing build in test workflow -> Tests pass now
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:22:00 +0100
package-manager (0.7.3-1) unstable; urgency=medium
* Fixed bug: Ignored packages are now ignored
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:08:31 +0100
package-manager (0.7.2-1) unstable; urgency=medium
* Implemented Changelog Support for Fedora and Debian

View File

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

View File

@@ -1,5 +1,5 @@
Name: package-manager
Version: 0.7.2
Version: 0.7.4
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -77,6 +77,12 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/
%changelog
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.4-1
- Fixed missing build in test workflow -> Tests pass now
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.3-1
- Fixed bug: Ignored packages are now ignored
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.2-1
- Implemented Changelog Support for Fedora and Debian

View File

@@ -8,6 +8,7 @@ import re
from typing import Any, Dict, List, Sequence
from pkgmgr.core.repository.resolve import resolve_repos
from pkgmgr.core.repository.ignored import filter_ignored
Repository = Dict[str, Any]
@@ -88,7 +89,7 @@ def _apply_filters(
if not _match_pattern(ident_str, string_pattern):
continue
# Category filter: nur echte Kategorien, KEINE Tags
# Category filter: only real categories, NOT tags
if category_patterns:
cats: List[str] = []
cats.extend(map(str, repo.get("category_files", [])))
@@ -106,7 +107,7 @@ def _apply_filters(
if not ok:
continue
# Tag filter: ausschließlich YAML-Tags
# Tag filter: YAML tags only
if tag_patterns:
tags: List[str] = list(map(str, repo.get("tags", [])))
if not tags:
@@ -124,16 +125,38 @@ def _apply_filters(
return filtered
def _maybe_filter_ignored(args, repos: List[Repository]) -> List[Repository]:
"""
Apply ignore filtering unless the caller explicitly opted to include ignored
repositories (via args.include_ignored).
Note: this helper is used only for *implicit* selections (all / filters /
by-directory). For *explicit* identifiers we do NOT filter ignored repos,
so the user can still target them directly if desired.
"""
include_ignored: bool = bool(getattr(args, "include_ignored", False))
if include_ignored:
return repos
return filter_ignored(repos)
def get_selected_repos(args, all_repositories: List[Repository]) -> List[Repository]:
"""
Compute the list of repositories selected by CLI arguments.
Modes:
- If identifiers are given: select via resolve_repos() from all_repositories.
- Else if any of --category/--string/--tag is used: start from all_repositories
and apply filters.
- Else if --all is set: select all_repositories.
- Else: try to select the repository of the current working directory.
Ignored repositories are *not* filtered here, so explicit identifiers
always win.
- Else if any of --category/--string/--tag is used: start from
all_repositories, apply filters and then drop ignored repos.
- Else if --all is set: select all_repositories and then drop ignored repos.
- Else: try to select the repository of the current working directory
and then drop it if it is ignored.
The ignore filter can be bypassed by setting args.include_ignored = True
(e.g. via a CLI flag --include-ignored).
"""
identifiers: List[str] = getattr(args, "identifiers", []) or []
use_all: bool = bool(getattr(args, "all", False))
@@ -143,18 +166,25 @@ def get_selected_repos(args, all_repositories: List[Repository]) -> List[Reposit
has_filters = bool(category_patterns or string_pattern or tag_patterns)
# 1) Explicit identifiers win
# 1) Explicit identifiers win and bypass ignore filtering
if identifiers:
base = resolve_repos(identifiers, all_repositories)
return _apply_filters(base, string_pattern, category_patterns, tag_patterns)
# 2) Filter-only mode: start from all repositories
if has_filters:
return _apply_filters(list(all_repositories), string_pattern, category_patterns, tag_patterns)
base = _apply_filters(
list(all_repositories),
string_pattern,
category_patterns,
tag_patterns,
)
return _maybe_filter_ignored(args, base)
# 3) --all (no filters): all repos
if use_all:
return list(all_repositories)
base = list(all_repositories)
return _maybe_filter_ignored(args, base)
# 4) Fallback: try to select repository of current working directory
cwd = os.path.abspath(os.getcwd())
@@ -164,7 +194,7 @@ def get_selected_repos(args, all_repositories: List[Repository]) -> List[Reposit
if os.path.abspath(str(repo.get("directory", ""))) == cwd
]
if by_dir:
return by_dir
return _maybe_filter_ignored(args, by_dir)
# No specific match -> empty list
return []

View File

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

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import unittest
from pkgmgr.core.repository.ignored import filter_ignored
class TestFilterIgnored(unittest.TestCase):
def test_filter_ignored_removes_repos_with_ignore_true(self) -> None:
repos = [
{"provider": "github.com", "account": "user", "repository": "a", "ignore": True},
{"provider": "github.com", "account": "user", "repository": "b", "ignore": False},
{"provider": "github.com", "account": "user", "repository": "c"},
]
result = filter_ignored(repos)
identifiers = {(r["provider"], r["account"], r["repository"]) for r in result}
self.assertNotIn(("github.com", "user", "a"), identifiers)
self.assertIn(("github.com", "user", "b"), identifiers)
self.assertIn(("github.com", "user", "c"), identifiers)
def test_filter_ignored_empty_list_returns_empty_list(self) -> None:
self.assertEqual(filter_ignored([]), [])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from pkgmgr.core.repository.selected import get_selected_repos
def _repo(
provider: str,
account: str,
repository: str,
ignore: bool | None = None,
directory: str | None = None,
):
repo = {
"provider": provider,
"account": account,
"repository": repository,
}
if ignore is not None:
repo["ignore"] = ignore
if directory is not None:
repo["directory"] = directory
return repo
class TestGetSelectedRepos(unittest.TestCase):
def setUp(self) -> None:
self.repo_ignored = _repo(
"github.com",
"user",
"ignored-repo",
ignore=True,
directory="/repos/github.com/user/ignored-repo",
)
self.repo_visible = _repo(
"github.com",
"user",
"visible-repo",
ignore=False,
directory="/repos/github.com/user/visible-repo",
)
self.all_repos = [self.repo_ignored, self.repo_visible]
# ------------------------------------------------------------------
# 1) Explizite Identifier ignorierte Repos dürfen ausgewählt werden
# ------------------------------------------------------------------
def test_identifiers_bypass_ignore_filter(self) -> None:
args = SimpleNamespace(
identifiers=["ignored-repo"], # matches by repository name
all=False,
category=[],
string="",
tag=[],
include_ignored=False, # should be ignored for explicit identifiers
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_ignored)
# ------------------------------------------------------------------
# 2) Filter-only Modus ignorierte Repos werden rausgefiltert
# ------------------------------------------------------------------
def test_filter_mode_excludes_ignored_by_default(self) -> None:
# string-Filter, der beide Repos matchen würde
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="repo", # substring in beiden Namen
tag=[],
include_ignored=False,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_visible)
def test_filter_mode_can_include_ignored_when_flag_set(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="repo",
tag=[],
include_ignored=True,
)
selected = get_selected_repos(args, self.all_repos)
# Beide Repos sollten erscheinen, weil include_ignored=True
self.assertEqual({r["repository"] for r in selected}, {"ignored-repo", "visible-repo"})
# ------------------------------------------------------------------
# 3) --all Modus ignorierte Repos werden per Default entfernt
# ------------------------------------------------------------------
def test_all_mode_excludes_ignored_by_default(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=True,
category=[],
string="",
tag=[],
include_ignored=False,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_visible)
def test_all_mode_can_include_ignored_when_flag_set(self) -> None:
args = SimpleNamespace(
identifiers=[],
all=True,
category=[],
string="",
tag=[],
include_ignored=True,
)
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 2)
self.assertCountEqual(
[r["repository"] for r in selected],
["ignored-repo", "visible-repo"],
)
# ------------------------------------------------------------------
# 4) CWD-Modus Repo anhand des aktuellen Verzeichnisses auswählen
# ------------------------------------------------------------------
def test_cwd_selection_excludes_ignored_by_default(self) -> None:
# Wir lassen CWD auf das Verzeichnis des ignorierten Repos zeigen.
cwd = os.path.abspath(self.repo_ignored["directory"])
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="",
tag=[],
include_ignored=False,
)
with patch("os.getcwd", return_value=cwd):
selected = get_selected_repos(args, self.all_repos)
# Da das einzige Repo für dieses Verzeichnis ignoriert ist,
# sollte die Auswahl leer sein.
self.assertEqual(selected, [])
def test_cwd_selection_can_include_ignored_when_flag_set(self) -> None:
cwd = os.path.abspath(self.repo_ignored["directory"])
args = SimpleNamespace(
identifiers=[],
all=False,
category=[],
string="",
tag=[],
include_ignored=True,
)
with patch("os.getcwd", return_value=cwd):
selected = get_selected_repos(args, self.all_repos)
self.assertEqual(len(selected), 1)
self.assertIs(selected[0], self.repo_ignored)
if __name__ == "__main__":
unittest.main()