Compare commits
32
Commits
ec7b959893
..
v2.0.4
No files matched your search
@@ -0,0 +1,23 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "00:00"
|
||||
rebase-strategy: "auto"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "00:00"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "ci"
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Run hadolint (produce SARIF)
|
||||
id: hadolint
|
||||
continue-on-error: true
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5
|
||||
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387
|
||||
with:
|
||||
dockerfile: ./Dockerfile
|
||||
format: sarif
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
lint-shell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install ShellCheck
|
||||
run: sudo apt-get update && sudo apt-get install -y shellcheck
|
||||
- name: Run ShellCheck
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true # We need tags and main history for version comparison
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository (with tags)
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ steps.info.outputs.should_publish == 'true' }}
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003
|
||||
with:
|
||||
use: true
|
||||
|
||||
- name: Login to GHCR
|
||||
if: ${{ steps.info.outputs.should_publish == 'true' }}
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show commit SHA
|
||||
run: git rev-parse HEAD
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
@@ -19,15 +19,13 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
- name: Build virgin container (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||
- name: Pull base image (${{ matrix.distro }})
|
||||
run: docker pull "ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest"
|
||||
|
||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
||||
run: |
|
||||
@@ -39,7 +37,7 @@ jobs:
|
||||
-v pkgmgr_pip_cache:/root/.cache/pip \
|
||||
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||
-w /opt/src/pkgmgr \
|
||||
"pkgmgr-${{ matrix.distro }}-virgin" \
|
||||
"ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest" \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -19,15 +19,13 @@ jobs:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Show Docker version
|
||||
run: docker version
|
||||
|
||||
- name: Build virgin container (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||
- name: Pull base image (${{ matrix.distro }})
|
||||
run: docker pull "ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest"
|
||||
|
||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
||||
run: |
|
||||
@@ -37,7 +35,7 @@ jobs:
|
||||
-v "$PWD":/opt/src/pkgmgr \
|
||||
-e NIX_CONFIG="${NIX_CONFIG}" \
|
||||
-w /opt/src/pkgmgr \
|
||||
"pkgmgr-${{ matrix.distro }}-virgin" \
|
||||
"ghcr.io/kevinveenbirkenbach/base-${{ matrix.distro }}:latest" \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Update flake.lock
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
name: Update flake.lock
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v23
|
||||
|
||||
- name: Update flake.lock and open a pull request
|
||||
uses: DeterminateSystems/update-flake-lock@v29
|
||||
with:
|
||||
pr-title: "chore(deps): update flake.lock"
|
||||
pr-labels: |
|
||||
dependencies
|
||||
nix
|
||||
+106
@@ -1,5 +1,111 @@
|
||||
# Changelog
|
||||
|
||||
## [2.0.4] - 2026-10-19
|
||||
|
||||
Fixed:
|
||||
|
||||
- CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||
- CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||
|
||||
Upgrade notes:
|
||||
|
||||
- 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
## [2.0.3] - 2026-10-18
|
||||
|
||||
Fixed:
|
||||
|
||||
- Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||
- Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||
- Release: findings the file already has are not blamed on the entry
|
||||
- Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||
- Release: without markdownlint-cli2, list items must use dash bullets
|
||||
|
||||
Upgrade notes:
|
||||
|
||||
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
## [2.0.2] - 2026-10-01
|
||||
|
||||
Fixes a release flow that could reject every entry written for it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A heading in a changelog message became bold, which markdown-lint reads
|
||||
back as a heading and refuses with MD036
|
||||
- Since the editor reopens on failure, no structured entry could ever pass
|
||||
- Headings now move below the release heading they are filed under
|
||||
- The lint findings reached nobody: the editor took over the terminal in the
|
||||
same breath they were printed
|
||||
- They are held until Enter and repeated inside the editor, above the text
|
||||
- The Makefile installer accepted install-* as proof of an install target but
|
||||
always ran plain make install
|
||||
- A repository with install-python and no install died on "No rule to make
|
||||
target" instead of being skipped
|
||||
|
||||
### Tests
|
||||
|
||||
- transform and lint were covered separately, so a transform producing lint
|
||||
errors went unseen; one test now runs the first through the second
|
||||
- Three tests were checking behaviour the code had already left behind
|
||||
|
||||
### Dependencies
|
||||
|
||||
- docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||
- actions/setup-python 5 to 7
|
||||
- DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||
|
||||
### Upgrading
|
||||
|
||||
- Nothing to do; a changelog entry written with headings now keeps them
|
||||
|
||||
## [2.0.1] - 2026-09-18
|
||||
|
||||
* A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||
* The entry's separator only belongs there when another entry follows it.
|
||||
* Affects the first entry a file receives, so it shows on an opening release.
|
||||
* Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||
|
||||
## [2.0.0] - 2026-09-18
|
||||
|
||||
Breaking
|
||||
|
||||
* The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||
* Building an image now requires the base images to exist in the registry.
|
||||
* *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||
* The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||
* *scripts/build/image.sh* loses its *--target* axis.
|
||||
* A repository is installed by one hook, no longer by pip and *make* both.
|
||||
|
||||
Changed
|
||||
|
||||
* *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||
* A failed hook falls back to the next; all failing exits with what was tried.
|
||||
* The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||
* Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||
* *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||
* The virgin workflows pull the base image instead of building it.
|
||||
* The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||
* It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||
* *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||
* The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||
* The README drops the *Virgin* type and the Nix claim the image never met.
|
||||
* It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||
* Dependabot covers pip and github-actions weekly on Monday.
|
||||
* A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||
* *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||
* *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||
|
||||
## [1.16.1] - 2026-07-29
|
||||
|
||||
* *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||
* Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||
* Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||
* The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||
* New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||
* New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||
|
||||
## [1.16.0] - 2026-06-28
|
||||
|
||||
* New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||
|
||||
+5
-47
@@ -1,67 +1,25 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Base image selector — overridden by build args / Makefile
|
||||
# ------------------------------------------------------------
|
||||
# hadolint ignore=DL3006,InvalidDefaultArgInFrom
|
||||
# BASE_IMAGE carries no default on purpose: a default would build one
|
||||
# distribution's image under another distribution's tag whenever the build arg
|
||||
# is forgotten. scripts/build/base.sh is the only place that resolves it.
|
||||
ARG BASE_IMAGE
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# ============================================================
|
||||
# Target: virgin
|
||||
# - installs distro deps (incl. make)
|
||||
# - no pkgmgr build
|
||||
# - no entrypoint
|
||||
# ============================================================
|
||||
FROM ${BASE_IMAGE} AS virgin
|
||||
SHELL ["/bin/bash", "-lc"]
|
||||
|
||||
RUN echo "BASE_IMAGE=${BASE_IMAGE}" && cat /etc/os-release || true
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy scripts first so dependency installation can be cached
|
||||
COPY scripts/installation/ scripts/installation/
|
||||
|
||||
# Install distro-specific build dependencies (including make)
|
||||
RUN bash scripts/installation/dependencies.sh
|
||||
|
||||
# Virgin default
|
||||
CMD ["bash"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Target: full
|
||||
# - inherits from virgin
|
||||
# - builds + installs pkgmgr
|
||||
# - sets entrypoint + default cmd
|
||||
# - NOTE: does NOT run slim.sh (that is done in slim stage)
|
||||
# ============================================================
|
||||
FROM virgin AS full
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full repository for build
|
||||
COPY . .
|
||||
|
||||
# Build and install distro-native package-manager package
|
||||
RUN set -eu; \
|
||||
echo "Building and installing package-manager via make install..."; \
|
||||
make install; \
|
||||
rm -rf /build
|
||||
|
||||
# Entry point
|
||||
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
|
||||
|
||||
WORKDIR /opt/src/pkgmgr
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
|
||||
CMD ["pkgmgr", "--help"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Target: slim
|
||||
# - based on full
|
||||
# - runs slim.sh
|
||||
# ============================================================
|
||||
FROM full AS slim
|
||||
|
||||
COPY scripts/docker/slim.sh /usr/local/bin/slim.sh
|
||||
RUN chmod +x /usr/local/bin/slim.sh && /usr/local/bin/slim.sh
|
||||
@@ -17,20 +17,9 @@ export NIX_CONFIG
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Base images
|
||||
# (kept for documentation/reference; actual build logic is in scripts/build)
|
||||
# ------------------------------------------------------------
|
||||
BASE_IMAGE_ARCH := archlinux:latest
|
||||
BASE_IMAGE_DEBIAN := debian:stable-slim
|
||||
BASE_IMAGE_UBUNTU := ubuntu:latest
|
||||
BASE_IMAGE_FEDORA := fedora:latest
|
||||
BASE_IMAGE_CENTOS := quay.io/centos/centos:stream9
|
||||
|
||||
# Make them available in scripts
|
||||
export BASE_IMAGE_ARCH
|
||||
export BASE_IMAGE_DEBIAN
|
||||
export BASE_IMAGE_UBUNTU
|
||||
export BASE_IMAGE_FEDORA
|
||||
export BASE_IMAGE_CENTOS
|
||||
BASE_IMAGES_TAG ?= latest
|
||||
export BASE_IMAGES_TAG
|
||||
|
||||
# PYthon Unittest Pattern
|
||||
TEST_PATTERN := test_*.py
|
||||
@@ -63,17 +52,12 @@ setup-nix:
|
||||
# Docker build targets (delegated to scripts/build)
|
||||
# ------------------------------------------------------------
|
||||
build:
|
||||
@bash scripts/build/image.sh --target virgin
|
||||
@bash scripts/build/image.sh
|
||||
|
||||
build-missing-virgin:
|
||||
@bash scripts/build/image.sh --target virgin --missing
|
||||
|
||||
build-missing: build-missing-virgin
|
||||
build-missing:
|
||||
@bash scripts/build/image.sh --missing
|
||||
|
||||
build-no-cache:
|
||||
@bash scripts/build/image.sh --target virgin --no-cache
|
||||
@bash scripts/build/image.sh --no-cache
|
||||
|
||||
build-no-cache-all:
|
||||
|
||||
@@ -169,26 +169,31 @@ nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
||||
|
||||
PKGMGR can be executed **inside Docker containers** for CI, testing and isolated
|
||||
workflows.
|
||||
|
||||
---
|
||||
|
||||
#### Container types
|
||||
#### Container images
|
||||
|
||||
Two container types are available.
|
||||
One image per distribution, each installing PKGMGR as a distro-native package
|
||||
on top of the matching base image from
|
||||
[base-images](https://github.com/kevinveenbirkenbach/base-images). Every image
|
||||
is a manifest list covering `linux/amd64` and `linux/arm64`.
|
||||
|
||||
All images live under `ghcr.io/kevinveenbirkenbach/`:
|
||||
|
||||
| Image type | Contains | Typical use |
|
||||
| ---------- | ----------------------------- | ----------------------- |
|
||||
| **Virgin** | Base OS + system dependencies | Clean test environments |
|
||||
| **Stable** | PKGMGR + Nix (flakes enabled) | Ready-to-use workflows |
|
||||
| Tag | Contains |
|
||||
| --------------------------- | ---------------------------------- |
|
||||
| `pkgmgr-<distro>:latest` | newest build for that distribution |
|
||||
| `pkgmgr-<distro>:<version>` | one release |
|
||||
| `pkgmgr-<distro>:stable` | newest release marked stable |
|
||||
|
||||
Example images:
|
||||
`<distro>` is one of `arch`, `debian`, `ubuntu`, `fedora`, `centos`. The Arch
|
||||
image is published without the suffix as well, so `pkgmgr:stable` and
|
||||
`pkgmgr-arch:stable` are the same image.
|
||||
|
||||
* Virgin: `pkgmgr-arch-virgin`
|
||||
* Stable: `ghcr.io/kevinveenbirkenbach/pkgmgr:stable`
|
||||
|
||||
|
||||
Use **virgin images** for isolated test runs,
|
||||
use the **stable image** for fast, reproducible execution.
|
||||
For a clean environment that carries the system dependencies but no PKGMGR,
|
||||
pull `base-<distro>` from
|
||||
[base-images](https://github.com/kevinveenbirkenbach/base-images) directly.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
python = pkgs.python311;
|
||||
pyPkgs = pkgs.python311Packages;
|
||||
python = pkgs.python312;
|
||||
pyPkgs = pkgs.python312Packages;
|
||||
in
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "1.16.0";
|
||||
version = "2.0.4";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
@@ -79,8 +79,8 @@
|
||||
if pkgs ? ansible-core then pkgs.ansible-core
|
||||
else pkgs.ansible;
|
||||
|
||||
# Use the same Python version as the package (3.11)
|
||||
python = pkgs.python311;
|
||||
# Use the same Python version as the package
|
||||
python = pkgs.python312;
|
||||
|
||||
pythonWithDeps = python.withPackages (ps: [
|
||||
ps.pip
|
||||
@@ -95,6 +95,9 @@
|
||||
pkgs.git
|
||||
pkgs.gnupg
|
||||
ansiblePkg
|
||||
pkgs.pkg-config
|
||||
pkgs.openldap
|
||||
pkgs.cyrus_sasl
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=1.16.0
|
||||
pkgver=2.0.4
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
@@ -1,3 +1,121 @@
|
||||
package-manager (2.0.4-1) unstable; urgency=medium
|
||||
|
||||
Fixed:
|
||||
|
||||
- CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||
- CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||
|
||||
Upgrade notes:
|
||||
|
||||
- 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 19 Oct 2026 10:06:02 +0200
|
||||
|
||||
package-manager (2.0.3-1) unstable; urgency=medium
|
||||
|
||||
Fixed:
|
||||
|
||||
- Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||
- Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||
- Release: findings the file already has are not blamed on the entry
|
||||
- Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||
- Release: without markdownlint-cli2, list items must use dash bullets
|
||||
|
||||
Upgrade notes:
|
||||
|
||||
- Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Sun, 18 Oct 2026 23:33:52 +0200
|
||||
|
||||
package-manager (2.0.2-1) unstable; urgency=medium
|
||||
|
||||
Fixes a release flow that could reject every entry written for it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A heading in a changelog message became bold, which markdown-lint reads
|
||||
back as a heading and refuses with MD036
|
||||
- Since the editor reopens on failure, no structured entry could ever pass
|
||||
- Headings now move below the release heading they are filed under
|
||||
- The lint findings reached nobody: the editor took over the terminal in the
|
||||
same breath they were printed
|
||||
- They are held until Enter and repeated inside the editor, above the text
|
||||
- The Makefile installer accepted install-* as proof of an install target but
|
||||
always ran plain make install
|
||||
- A repository with install-python and no install died on "No rule to make
|
||||
target" instead of being skipped
|
||||
|
||||
### Tests
|
||||
|
||||
- transform and lint were covered separately, so a transform producing lint
|
||||
errors went unseen; one test now runs the first through the second
|
||||
- Three tests were checking behaviour the code had already left behind
|
||||
|
||||
### Dependencies
|
||||
|
||||
- docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||
- actions/setup-python 5 to 7
|
||||
- DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||
|
||||
### Upgrading
|
||||
|
||||
- Nothing to do; a changelog entry written with headings now keeps them
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 01 Oct 2026 17:47:12 +0200
|
||||
|
||||
package-manager (2.0.1-1) unstable; urgency=medium
|
||||
|
||||
* A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||
* The entry's separator only belongs there when another entry follows it.
|
||||
* Affects the first entry a file receives, so it shows on an opening release.
|
||||
* Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 18 Sep 2026 17:15:35 +0200
|
||||
|
||||
package-manager (2.0.0-1) unstable; urgency=medium
|
||||
|
||||
Breaking
|
||||
|
||||
* The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||
* Building an image now requires the base images to exist in the registry.
|
||||
* *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||
* The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||
* *scripts/build/image.sh* loses its *--target* axis.
|
||||
* A repository is installed by one hook, no longer by pip and *make* both.
|
||||
|
||||
Changed
|
||||
|
||||
* *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||
* A failed hook falls back to the next; all failing exits with what was tried.
|
||||
* The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||
* Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||
* *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||
* The virgin workflows pull the base image instead of building it.
|
||||
* The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||
* It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||
* *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||
* The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||
* The README drops the *Virgin* type and the Nix claim the image never met.
|
||||
* It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||
* Dependabot covers pip and github-actions weekly on Monday.
|
||||
* A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||
* *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||
* *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Fri, 18 Sep 2026 16:29:32 +0200
|
||||
|
||||
package-manager (1.16.1-1) unstable; urgency=medium
|
||||
|
||||
* *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||
* Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||
* Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||
* The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||
* New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||
* New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 29 Jul 2026 16:41:21 +0200
|
||||
|
||||
package-manager (1.16.0-1) unstable; urgency=medium
|
||||
|
||||
* New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 1.16.0
|
||||
Version: 2.0.4
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -74,6 +74,106 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
||||
/usr/lib/package-manager/
|
||||
|
||||
%changelog
|
||||
* Mon Oct 19 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.4-1
|
||||
- Fixed:
|
||||
|
||||
- - CI: Python lint passes again on ruff 0.17 and still on 0.16
|
||||
- - CI: actions bumped: *actions/checkout* v7, *hadolint-action* 3.5.0
|
||||
|
||||
- Upgrade notes:
|
||||
|
||||
- - 2.0.3 was never marked *stable* nor published to GHCR: its CI failed
|
||||
- - Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
* Sun Oct 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.3-1
|
||||
- Fixed:
|
||||
|
||||
- - Release: the entry is linted inside the CHANGELOG.md it lands in
|
||||
- - Release: a list marker that breaks the file's consistency is caught (MD004)
|
||||
- - Release: findings the file already has are not blamed on the entry
|
||||
- - Release: *--no-globs* keeps the target repo's other files out of the lint
|
||||
- - Release: without markdownlint-cli2, list items must use dash bullets
|
||||
|
||||
- Upgrade notes:
|
||||
|
||||
- - Release: write list items with dash bullets when markdownlint-cli2 is absent
|
||||
|
||||
* Thu Oct 01 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.2-1
|
||||
- Fixes a release flow that could reject every entry written for it.
|
||||
|
||||
- ### Fixed
|
||||
|
||||
- - A heading in a changelog message became bold, which markdown-lint reads
|
||||
- back as a heading and refuses with MD036
|
||||
- - Since the editor reopens on failure, no structured entry could ever pass
|
||||
- - Headings now move below the release heading they are filed under
|
||||
- - The lint findings reached nobody: the editor took over the terminal in the
|
||||
- same breath they were printed
|
||||
- - They are held until Enter and repeated inside the editor, above the text
|
||||
- - The Makefile installer accepted install-* as proof of an install target but
|
||||
- always ran plain make install
|
||||
- - A repository with install-python and no install died on "No rule to make
|
||||
- target" instead of being skipped
|
||||
|
||||
- ### Tests
|
||||
|
||||
- - transform and lint were covered separately, so a transform producing lint
|
||||
- errors went unseen; one test now runs the first through the second
|
||||
- - Three tests were checking behaviour the code had already left behind
|
||||
|
||||
- ### Dependencies
|
||||
|
||||
- - docker/login-action 3.7.0 to 4.6.0, setup-buildx-action 3.12.0 to 4.4.0
|
||||
- - actions/setup-python 5 to 7
|
||||
- - DeterminateSystems nix-installer-action 16 to 23, update-flake-lock 24 to 29
|
||||
|
||||
- ### Upgrading
|
||||
|
||||
- - Nothing to do; a changelog entry written with headings now keeps them
|
||||
|
||||
* Fri Sep 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.1-1
|
||||
- * A release no longer ends *CHANGELOG.md* on a blank line, which is MD012.
|
||||
- * The entry's separator only belongs there when another entry follows it.
|
||||
- * Affects the first entry a file receives, so it shows on an opening release.
|
||||
- * Four tests cover the placements in *_insert_after_h1*, one per branch.
|
||||
|
||||
* Fri Sep 18 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.0-1
|
||||
- Breaking
|
||||
|
||||
- * The *virgin* and *slim* image variants are gone; both tag suffixes with them.
|
||||
- * Building an image now requires the base images to exist in the registry.
|
||||
- * *make build-missing-virgin* is removed; *build* targets build the one image.
|
||||
- * The five *BASE_IMAGE_** overrides are gone; *BASE_IMAGES_TAG* replaces them.
|
||||
- * *scripts/build/image.sh* loses its *--target* axis.
|
||||
- * A repository is installed by one hook, no longer by pip and *make* both.
|
||||
|
||||
- Changed
|
||||
|
||||
- * *INSTALLERS* is a preference order led by *make install*, first success wins.
|
||||
- * A failed hook falls back to the next; all failing exits with what was tried.
|
||||
- * The *Dockerfile* builds *FROM ${BASE_IMAGE}*, resolved by *base.sh*.
|
||||
- * Publishing pushes *--platform* manifest lists for amd64 and arm64.
|
||||
- * *manjaro* can be built; *os_resolver.sh* already maps it onto arch.
|
||||
- * The virgin workflows pull the base image instead of building it.
|
||||
- * The Nix dev shell moves to Python 3.12 for *portfolio*'s *>=3.12* floor.
|
||||
- * It gains *pkg-config*, *openldap* and *cyrus_sasl* for *python-ldap*.
|
||||
- * *requires-python* for pkgmgr itself is unchanged at *>=3.9*.
|
||||
- * The e2e sweep drops *--silent*, which hid the non-zero exit, not the output.
|
||||
- * The README drops the *Virgin* type and the Nix claim the image never met.
|
||||
- * It now lists the tags *compute_publish_tags()* pushes, alias included.
|
||||
- * Dependabot covers pip and github-actions weekly on Monday.
|
||||
- * A new *update-flake-lock* workflow opens a PR for *flake.lock* the same day.
|
||||
- * *test_distro_dependency_scripts_install_gpg_tools* moves to base-images.
|
||||
- * *scripts/docker/slim.sh* is deleted with the stage that ran it.
|
||||
|
||||
* Wed Jul 29 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.16.1-1
|
||||
- * *pkgmgr repos create* now creates *main*-based repositories: *git init* is run with an explicit *-b main* instead of inheriting the host's *init.defaultBranch*, and a failed initial push no longer renames the branch back to *master*. *resolve_base_branch()* keeps its main → master fallback for existing repositories.
|
||||
- * Failures are no longer swallowed: 46 blind *except Exception* handlers were narrowed to the exception types the guarded code can actually raise (*OSError*, *UnicodeDecodeError*, TOML/YAML decode errors, *ValueError*, *ImportError*, *subprocess.SubprocessError*, *GitRunError*, *PackageNotFoundError*). Three handlers stay open by design — the per-repository loops in *install* and *update* are batch boundaries where one failing repository must not abort the run.
|
||||
- * Keyring backend errors now surface as the concrete *KeyringOperationError*, raised at the boundary in *KeyringTokenProvider.get/set* with the original exception preserved as *__cause__*, and kept distinct from *KeyringUnavailableError* ("no usable backend" vs. "backend present, call failed"). Both degradation paths are unchanged.
|
||||
- * The *code-scanning* report stamps *generated_at* and its fallback output directory in UTC instead of a naive local timestamp.
|
||||
- * New *make lint* target runs *ruff* on *src* and *tests* with the same invocation as the *lint-python* workflow, and *make test* now depends on it. Repository-wide formatting and typing modernisation (PEP 604 unions with *from __future__ import annotations*, builtin generics, shebangs stripped from 72 importable modules) — *ruff check src tests* passes.
|
||||
- * New *tests/integration/test_error_path_degradation.py* drives 44 of the narrowed handlers into their except branch with real injected faults and asserts the documented degradation.
|
||||
|
||||
* Sun Jun 28 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 1.16.0-1
|
||||
- * New *code-scanning* command: *pkgmgr code-scanning* downloads a repository's GitHub code scanning alerts and analysis metadata via the *gh* CLI into a timestamped directory (default */tmp/<repo>/code-scanner/<timestamp>*), together with a readable summary, for offline analysis.
|
||||
- * The release flow now lints and normalises the changelog message before writing it: a leading heading becomes bold, inline code becomes italic, and the entry is validated against the repository's markdown rules, re-prompting interactively until it is clean.
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "kpmx"
|
||||
version = "1.16.0"
|
||||
version = "2.0.4"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
+22
-10
@@ -1,20 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BASE_IMAGE_ARCH:=archlinux:latest}"
|
||||
: "${BASE_IMAGE_DEBIAN:=debian:stable-slim}"
|
||||
: "${BASE_IMAGE_UBUNTU:=ubuntu:latest}"
|
||||
: "${BASE_IMAGE_FEDORA:=fedora:latest}"
|
||||
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:latest}"
|
||||
# The distribution bases this repository builds on. They are published by
|
||||
# https://github.com/kevinveenbirkenbach/base-images, one multi-platform image
|
||||
# per distribution, and carry the build dependencies the package build needs.
|
||||
#
|
||||
# Env overrides exist for testing an unpublished base; the registry namespace
|
||||
# follows the images repository, not this one.
|
||||
|
||||
: "${BASE_IMAGES_REGISTRY:=ghcr.io}"
|
||||
: "${BASE_IMAGES_OWNER:=kevinveenbirkenbach}"
|
||||
: "${BASE_IMAGES_TAG:=latest}"
|
||||
|
||||
resolve_base_image() {
|
||||
local PKGMGR_DISTRO="$1"
|
||||
case "$PKGMGR_DISTRO" in
|
||||
arch) echo "$BASE_IMAGE_ARCH" ;;
|
||||
debian) echo "$BASE_IMAGE_DEBIAN" ;;
|
||||
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
|
||||
fedora) echo "$BASE_IMAGE_FEDORA" ;;
|
||||
centos) echo "$BASE_IMAGE_CENTOS" ;;
|
||||
arch|manjaro|debian|ubuntu|fedora|centos)
|
||||
echo "${BASE_IMAGES_REGISTRY}/${BASE_IMAGES_OWNER}/base-${PKGMGR_DISTRO}:${BASE_IMAGES_TAG}"
|
||||
;;
|
||||
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Platforms each distribution's base offers; see base-images/scripts/build/distros.sh.
|
||||
resolve_platforms() {
|
||||
local PKGMGR_DISTRO="$1"
|
||||
case "$PKGMGR_DISTRO" in
|
||||
arch|manjaro|debian|ubuntu|fedora|centos) echo "linux/amd64,linux/arm64" ;;
|
||||
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
+3
-26
@@ -10,7 +10,6 @@ source "${SCRIPT_DIR}/base.sh"
|
||||
|
||||
NO_CACHE=0
|
||||
MISSING_ONLY=0
|
||||
TARGET=""
|
||||
IMAGE_TAG="" # local image name or base tag (without registry)
|
||||
PUSH=0 # if 1 -> use buildx and push (requires docker buildx)
|
||||
PUBLISH=0 # if 1 -> push with semantic tags (latest/version/stable + arch aliases)
|
||||
@@ -23,9 +22,6 @@ DEFAULT_DISTRO="arch"
|
||||
|
||||
usage() {
|
||||
local default_tag="pkgmgr-${PKGMGR_DISTRO}"
|
||||
if [[ -n "${TARGET:-}" ]]; then
|
||||
default_tag="${default_tag}-${TARGET}"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
Usage: PKGMGR_DISTRO=<distro> $0 [options]
|
||||
@@ -33,7 +29,6 @@ Usage: PKGMGR_DISTRO=<distro> $0 [options]
|
||||
Build options:
|
||||
--missing Build only if the image does not already exist (local build only)
|
||||
--no-cache Build with --no-cache
|
||||
--target <name> Build a specific Dockerfile target (e.g. virgin, slim)
|
||||
--tag <image> Override the output image tag (default: ${default_tag})
|
||||
|
||||
Publish options:
|
||||
@@ -47,7 +42,7 @@ Publish options:
|
||||
|
||||
Notes:
|
||||
- --publish implies --push and requires --registry, --owner, and --version.
|
||||
- Local build (no --push) uses "docker build" and creates local images like "pkgmgr-arch" / "pkgmgr-arch-virgin" / "pkgmgr-arch-slim".
|
||||
- Local build (no --push) uses "docker build" and creates a local image like "pkgmgr-arch".
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -55,11 +50,6 @@ while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-cache) NO_CACHE=1; shift ;;
|
||||
--missing) MISSING_ONLY=1; shift ;;
|
||||
--target)
|
||||
TARGET="${2:-}"
|
||||
[[ -n "${TARGET}" ]] || { echo "ERROR: --target requires a value (e.g. virgin|slim)"; exit 2; }
|
||||
shift 2
|
||||
;;
|
||||
--tag)
|
||||
IMAGE_TAG="${2:-}"
|
||||
[[ -n "${IMAGE_TAG}" ]] || { echo "ERROR: --tag requires a value"; exit 2; }
|
||||
@@ -104,9 +94,6 @@ done
|
||||
# Derive default local tag if not provided
|
||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||
IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
|
||||
@@ -143,7 +130,7 @@ echo "------------------------------------------------------------"
|
||||
echo "[build] Building image"
|
||||
echo "distro = ${PKGMGR_DISTRO}"
|
||||
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
||||
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
|
||||
echo "platforms = $(resolve_platforms "$PKGMGR_DISTRO")"
|
||||
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
||||
if [[ "${PUSH}" == "1" ]]; then echo "push = enabled"; fi
|
||||
if [[ "${PUBLISH}" == "1" ]]; then
|
||||
@@ -162,23 +149,12 @@ if [[ "${NO_CACHE}" == "1" ]]; then
|
||||
build_args+=(--no-cache)
|
||||
fi
|
||||
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
build_args+=(--target "${TARGET}")
|
||||
fi
|
||||
|
||||
compute_publish_tags() {
|
||||
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||
local alias_tag_base=""
|
||||
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
distro_tag_base="${distro_tag_base}-${TARGET}"
|
||||
fi
|
||||
|
||||
if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
|
||||
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
alias_tag_base="${alias_tag_base}-${TARGET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
local tags=()
|
||||
@@ -211,6 +187,7 @@ if [[ "${PUSH}" == "1" ]]; then
|
||||
bx_args+=(-t "${IMAGE_TAG}")
|
||||
fi
|
||||
|
||||
bx_args+=(--platform "$(resolve_platforms "$PKGMGR_DISTRO")")
|
||||
bx_args+=("${build_args[@]}")
|
||||
bx_args+=(.)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Publish all distro images (full + virgin + slim) to a registry via image.sh --publish
|
||||
# Publish one image per distro, each covering every platform its base offers.
|
||||
#
|
||||
# Required env:
|
||||
# OWNER (e.g. GITHUB_REPOSITORY_OWNER)
|
||||
@@ -11,9 +11,6 @@ set -euo pipefail
|
||||
# REGISTRY (default: ghcr.io)
|
||||
# IS_STABLE (default: false)
|
||||
# DISTROS (default: "arch debian ubuntu fedora centos")
|
||||
#
|
||||
# Notes:
|
||||
# - This expects Dockerfile targets: virgin, full (default), slim
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
@@ -36,41 +33,12 @@ for d in ${DISTROS}; do
|
||||
echo "[publish] PKGMGR_DISTRO=${d}"
|
||||
echo "============================================================"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# virgin
|
||||
# -> ghcr.io/<owner>/pkgmgr-<distro>-virgin:{latest,<version>,stable?}
|
||||
# ----------------------------------------------------------
|
||||
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
--publish \
|
||||
--registry "${REGISTRY}" \
|
||||
--owner "${OWNER}" \
|
||||
--version "${VERSION}" \
|
||||
--stable "${IS_STABLE}" \
|
||||
--target virgin
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# full (default target)
|
||||
# -> ghcr.io/<owner>/pkgmgr-<distro>:{latest,<version>,stable?}
|
||||
# ----------------------------------------------------------
|
||||
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
--publish \
|
||||
--registry "${REGISTRY}" \
|
||||
--owner "${OWNER}" \
|
||||
--version "${VERSION}" \
|
||||
--stable "${IS_STABLE}"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# slim
|
||||
# -> ghcr.io/<owner>/pkgmgr-<distro>-slim:{latest,<version>,stable?}
|
||||
# + alias for default distro: ghcr.io/<owner>/pkgmgr-slim:{...}
|
||||
# ----------------------------------------------------------
|
||||
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
--publish \
|
||||
--registry "${REGISTRY}" \
|
||||
--owner "${OWNER}" \
|
||||
--version "${VERSION}" \
|
||||
--stable "${IS_STABLE}" \
|
||||
--target slim
|
||||
done
|
||||
|
||||
echo
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() { echo "[cleanup] $*"; }
|
||||
warn() { echo "[cleanup][WARN] $*" >&2; }
|
||||
|
||||
MODE="${MODE:-safe}" # safe | aggressive
|
||||
# safe: caches/logs/tmp only
|
||||
# aggressive: safe + docs/man/info (optional)
|
||||
|
||||
ID="unknown"
|
||||
if [ -f /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
ID="${ID:-unknown}"
|
||||
fi
|
||||
|
||||
log "Starting image cleanup"
|
||||
log "Mode: ${MODE}"
|
||||
log "Detected OS: ${ID}"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Package manager caches (SAFE)
|
||||
# ------------------------------------------------------------
|
||||
case "${ID}" in
|
||||
alpine)
|
||||
log "Cleaning apk cache"
|
||||
if [ -d /var/cache/apk ]; then
|
||||
du -sh /var/cache/apk || true
|
||||
rm -rvf /var/cache/apk/* || true
|
||||
else
|
||||
log "apk cache directory not present (already clean)"
|
||||
fi
|
||||
;;
|
||||
arch)
|
||||
log "Cleaning pacman cache"
|
||||
du -sh /var/cache/pacman/pkg 2>/dev/null || true
|
||||
pacman -Scc --noconfirm || true
|
||||
rm -rvf /var/cache/pacman/pkg/* || true
|
||||
;;
|
||||
debian|ubuntu)
|
||||
log "Cleaning apt cache"
|
||||
du -sh /var/lib/apt/lists 2>/dev/null || true
|
||||
apt-get clean || true
|
||||
rm -rvf /var/lib/apt/lists/* || true
|
||||
;;
|
||||
fedora)
|
||||
log "Cleaning dnf cache"
|
||||
du -sh /var/cache/dnf 2>/dev/null || true
|
||||
dnf clean all || true
|
||||
rm -rvf /var/cache/dnf/* || true
|
||||
;;
|
||||
centos|rhel)
|
||||
log "Cleaning yum/dnf cache"
|
||||
du -sh /var/cache/yum /var/cache/dnf 2>/dev/null || true
|
||||
(command -v dnf >/dev/null 2>&1 && dnf clean all) || true
|
||||
(command -v yum >/dev/null 2>&1 && yum clean all) || true
|
||||
rm -rvf /var/cache/yum/* /var/cache/dnf/* || true
|
||||
;;
|
||||
*)
|
||||
warn "Unknown distro '${ID}' — skipping package manager cleanup"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Python caches (SAFE)
|
||||
# ------------------------------------------------------------
|
||||
log "Cleaning pip cache"
|
||||
du -sh /root/.cache/pip 2>/dev/null || true
|
||||
rm -rvf /root/.cache/pip 2>/dev/null || true
|
||||
rm -rvf /home/*/.cache/pip 2>/dev/null || true
|
||||
|
||||
log "Cleaning __pycache__ directories"
|
||||
find /opt /usr /root /home -type d -name "__pycache__" -print -prune 2>/dev/null || true
|
||||
find /opt /usr /root /home -type d -name "__pycache__" -prune -exec rm -rvf {} + 2>/dev/null || true
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Logs (SAFE)
|
||||
# ------------------------------------------------------------
|
||||
log "Truncating log files (keeping paths intact)"
|
||||
if [ -d /var/log ]; then
|
||||
find /var/log -type f -name "*.log" -print 2>/dev/null || true
|
||||
find /var/log -type f -name "*.log" -exec sh -lc ': > "$1" 2>/dev/null || true' _ {} \; 2>/dev/null || true
|
||||
|
||||
find /var/log -type f -name "*.out" -print 2>/dev/null || true
|
||||
find /var/log -type f -name "*.out" -exec sh -lc ': > "$1" 2>/dev/null || true' _ {} \; 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
log "Vacuuming journald logs"
|
||||
journalctl --disk-usage || true
|
||||
journalctl --vacuum-size=10M || true
|
||||
journalctl --vacuum-time=1s || true
|
||||
journalctl --disk-usage || true
|
||||
else
|
||||
log "journald not present (skipping)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Temporary files (SAFE)
|
||||
# ------------------------------------------------------------
|
||||
log "Cleaning temporary directories"
|
||||
if [ -d /tmp ]; then
|
||||
du -sh /tmp 2>/dev/null || true
|
||||
rm -rvf /tmp/* || true
|
||||
fi
|
||||
|
||||
if [ -d /var/tmp ]; then
|
||||
du -sh /var/tmp 2>/dev/null || true
|
||||
rm -rvf /var/tmp/* || true
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Generic caches (SAFE)
|
||||
# ------------------------------------------------------------
|
||||
log "Cleaning generic caches"
|
||||
du -sh /root/.cache 2>/dev/null || true
|
||||
rm -rvf /root/.cache/* 2>/dev/null || true
|
||||
rm -rvf /home/*/.cache/* 2>/dev/null || true
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Optional aggressive extras (still safe for runtime)
|
||||
# ------------------------------------------------------------
|
||||
if [[ "${MODE}" == "aggressive" ]]; then
|
||||
log "Aggressive mode enabled: removing docs/man/info"
|
||||
du -sh /usr/share/doc /usr/share/man /usr/share/info 2>/dev/null || true
|
||||
rm -rvf /usr/share/doc/* /usr/share/man/* /usr/share/info/* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "Cleanup finished successfully"
|
||||
@@ -38,13 +38,14 @@ from pkgmgr.core.repository.verify import verify_repository
|
||||
Repository = dict[str, Any]
|
||||
|
||||
INSTALLERS = [
|
||||
MakefileInstaller(),
|
||||
ArchPkgbuildInstaller(),
|
||||
DebianControlInstaller(),
|
||||
RpmSpecInstaller(),
|
||||
NixFlakeInstaller(),
|
||||
PythonInstaller(),
|
||||
MakefileInstaller(),
|
||||
]
|
||||
"""Preference order: the pipeline runs the first supported hook and stops."""
|
||||
|
||||
|
||||
def _ensure_repo_dir(
|
||||
|
||||
@@ -31,9 +31,7 @@ class MakefileInstaller(BaseInstaller):
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
if re.search(r"^install\s*:", content, flags=re.MULTILINE):
|
||||
return True
|
||||
return bool(re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE))
|
||||
return bool(re.search(r"^install\s*:", content, flags=re.MULTILINE))
|
||||
|
||||
def run(self, ctx: RepoContext) -> None:
|
||||
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
|
||||
|
||||
@@ -81,13 +81,15 @@ class InstallationPipeline:
|
||||
else:
|
||||
repo.pop("command", None)
|
||||
|
||||
provided_capabilities: set[str] = set()
|
||||
attempted: list[str] = []
|
||||
|
||||
for installer in self._installers:
|
||||
layer_name = getattr(installer, "layer", None)
|
||||
|
||||
if layer_name is None:
|
||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||
if self._run_installer(installer, ctx, identifier, repo_dir, quiet):
|
||||
return
|
||||
attempted.append(installer.__class__.__name__)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -120,15 +122,6 @@ class InstallationPipeline:
|
||||
if not installer.supports(ctx):
|
||||
continue
|
||||
|
||||
caps = installer.discover_capabilities(ctx)
|
||||
if caps and caps.issubset(provided_capabilities):
|
||||
if not quiet:
|
||||
print(
|
||||
f"Skipping installer {installer.__class__.__name__} "
|
||||
f"for {identifier} – capabilities {caps} already provided."
|
||||
)
|
||||
continue
|
||||
|
||||
if not quiet:
|
||||
if (
|
||||
ctx.force_update
|
||||
@@ -142,13 +135,17 @@ class InstallationPipeline:
|
||||
else:
|
||||
print(
|
||||
f"[pkgmgr] Running installer {installer.__class__.__name__} "
|
||||
f"for {identifier} in '{repo_dir}' "
|
||||
f"(new capabilities: {caps or set()})..."
|
||||
f"for {identifier} in '{repo_dir}'..."
|
||||
)
|
||||
|
||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||
|
||||
provided_capabilities.update(caps)
|
||||
if not self._run_installer(installer, ctx, identifier, repo_dir, quiet):
|
||||
attempted.append(installer.__class__.__name__)
|
||||
if not quiet:
|
||||
print(
|
||||
f"[pkgmgr] {installer.__class__.__name__} installed nothing "
|
||||
f"for {identifier}; falling back to the next hook."
|
||||
)
|
||||
continue
|
||||
|
||||
new_state = resolver.resolve()
|
||||
if new_state.command:
|
||||
@@ -165,6 +162,13 @@ class InstallationPipeline:
|
||||
repo.pop("command", None)
|
||||
|
||||
state = new_state
|
||||
return
|
||||
|
||||
if attempted:
|
||||
raise SystemExit(
|
||||
f"every installation hook failed for {identifier}: "
|
||||
f"{', '.join(attempted)}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _run_installer(
|
||||
@@ -173,9 +177,11 @@ class InstallationPipeline:
|
||||
identifier: str,
|
||||
repo_dir: str,
|
||||
quiet: bool,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Run one hook. Returns whether it installed anything."""
|
||||
try:
|
||||
installer.run(ctx)
|
||||
return True
|
||||
except SystemExit as exc:
|
||||
exit_code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
print(
|
||||
@@ -193,4 +199,4 @@ class InstallationPipeline:
|
||||
f" pkgmgr install {identifier} "
|
||||
"--clone-mode shallow --no-verification"
|
||||
)
|
||||
raise
|
||||
return False
|
||||
@@ -6,11 +6,15 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import Counter
|
||||
|
||||
_HEADING = re.compile(r"^\s*#{1,6}\s+(.*?)\s*#*\s*$")
|
||||
_HEADING = re.compile(r"^\s*(#{1,6})\s+(.*?)\s*#*\s*$")
|
||||
_INLINE_CODE = re.compile(r"`([^`\n]+)`")
|
||||
_FINDING = re.compile(r"\bMD\d{3}\b")
|
||||
_LOCATION = re.compile(r"^.*?:\d+(?::\d+)?\s+")
|
||||
_NON_DASH_ITEM = re.compile(r"^\s*[*+]\s")
|
||||
_MULTI_BLANK = re.compile(r"\n{3,}")
|
||||
_ENTRY_LEVEL = 3
|
||||
|
||||
|
||||
class ChangelogLintError(RuntimeError):
|
||||
@@ -20,10 +24,10 @@ class ChangelogLintError(RuntimeError):
|
||||
def transform_changelog_message(text: str) -> str:
|
||||
"""Normalise a free-form release message into the changelog house style.
|
||||
|
||||
A leading ``#`` heading becomes a bold line of its own (markdown
|
||||
headings inside an entry body would collide with the ``## [version]``
|
||||
structure), and inline ``code`` spans become ``*italic*`` so the entry
|
||||
stays free of backticks.
|
||||
Headings are pushed below the ``## [version]`` line the entry is filed
|
||||
under, so they nest instead of competing with it. Bolding them instead
|
||||
would read as a heading to markdown-lint and fail MD036. Inline ``code``
|
||||
spans become ``*italic*`` so the entry stays free of backticks.
|
||||
"""
|
||||
text = _INLINE_CODE.sub(r"*\1*", text)
|
||||
|
||||
@@ -31,9 +35,10 @@ def transform_changelog_message(text: str) -> str:
|
||||
for line in text.split("\n"):
|
||||
heading = _HEADING.match(line)
|
||||
if heading:
|
||||
level = max(len(heading.group(1)), _ENTRY_LEVEL)
|
||||
if out and out[-1].strip():
|
||||
out.append("")
|
||||
out.append(f"**{heading.group(1).strip()}**")
|
||||
out.append(f"{'#' * level} {heading.group(2).strip()}")
|
||||
out.append("")
|
||||
else:
|
||||
out.append(line)
|
||||
@@ -57,6 +62,35 @@ def lint_changelog_entry(reference_path: str, entry: str) -> list[str]:
|
||||
return _builtin_lint(document)
|
||||
|
||||
|
||||
def lint_changelog_context(reference_path: str, existing: str, merged: str) -> list[str]:
|
||||
"""Return the findings *merged* has beyond those *existing* already has.
|
||||
|
||||
Args:
|
||||
reference_path: path of the target CHANGELOG.md; its directory supplies
|
||||
the markdownlint config.
|
||||
existing: the target file before the entry is inserted.
|
||||
merged: the target file with the entry inserted.
|
||||
|
||||
Returns:
|
||||
Findings of *merged* that *existing* does not produce. Empty when
|
||||
``markdownlint-cli2`` is unavailable.
|
||||
"""
|
||||
if not shutil.which("markdownlint-cli2"):
|
||||
return []
|
||||
known = Counter(
|
||||
_LOCATION.sub("", finding)
|
||||
for finding in (_markdownlint(reference_path, existing) if existing.strip() else [])
|
||||
)
|
||||
new: list[str] = []
|
||||
for finding in _markdownlint(reference_path, merged):
|
||||
key = _LOCATION.sub("", finding)
|
||||
if known[key]:
|
||||
known[key] -= 1
|
||||
else:
|
||||
new.append(finding)
|
||||
return new
|
||||
|
||||
|
||||
def _markdownlint(reference_path: str, document: str) -> list[str]:
|
||||
directory = os.path.dirname(os.path.abspath(reference_path)) or "."
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
@@ -67,7 +101,7 @@ def _markdownlint(reference_path: str, document: str) -> list[str]:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
handle.write(document)
|
||||
proc = subprocess.run(
|
||||
["markdownlint-cli2", name],
|
||||
["markdownlint-cli2", "--no-globs", name],
|
||||
cwd=directory,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -94,6 +128,8 @@ def _builtin_lint(document: str) -> list[str]:
|
||||
errors.append(f"line {number}: trailing whitespace (MD009)")
|
||||
if "`" in line:
|
||||
errors.append(f"line {number}: backtick is not allowed (use *italic*)")
|
||||
if _NON_DASH_ITEM.match(line):
|
||||
errors.append(f"line {number}: list items use '-' (MD004)")
|
||||
if "\n\n\n" in document:
|
||||
errors.append("multiple consecutive blank lines (MD012)")
|
||||
return errors
|
||||
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
from .changelog_lint import (
|
||||
ChangelogLintError,
|
||||
lint_changelog_context,
|
||||
lint_changelog_entry,
|
||||
transform_changelog_message,
|
||||
)
|
||||
@@ -16,6 +17,11 @@ H1_RE = re.compile(r"^#\s+\S", re.MULTILINE)
|
||||
H2_RE = re.compile(r"^##\s+\S", re.MULTILINE)
|
||||
|
||||
|
||||
def _at_end_of_file(entry: str) -> str:
|
||||
"""Return *entry* without the blank line that separates it from a next one."""
|
||||
return entry.rstrip("\n") + "\n"
|
||||
|
||||
|
||||
def _insert_after_h1(existing: str, entry: str) -> str:
|
||||
"""Place *entry* after the H1 (and any intro prose), above the first H2.
|
||||
|
||||
@@ -26,7 +32,7 @@ def _insert_after_h1(existing: str, entry: str) -> str:
|
||||
``## ``) is preserved: *entry* is prepended unchanged.
|
||||
"""
|
||||
if not existing.strip():
|
||||
return f"# Changelog\n\n{entry}"
|
||||
return f"# Changelog\n\n{_at_end_of_file(entry)}"
|
||||
|
||||
if not H1_RE.search(existing):
|
||||
# Legacy layout: file starts with `## [version]` and has no H1.
|
||||
@@ -43,7 +49,7 @@ def _insert_after_h1(existing: str, entry: str) -> str:
|
||||
if existing.endswith("\n\n")
|
||||
else ("\n" if existing.endswith("\n") else "\n\n")
|
||||
)
|
||||
return f"{existing}{suffix}{entry}"
|
||||
return f"{existing}{suffix}{_at_end_of_file(entry)}"
|
||||
|
||||
# Insert new entry just before the first H2.
|
||||
head = existing[: h2_match.start()].rstrip("\n") + "\n\n"
|
||||
@@ -63,7 +69,7 @@ def update_changelog(
|
||||
missing) and above any existing release entries, so the result stays
|
||||
markdown-lint-clean (MD041 first-line-h1, MD012 no-multiple-blanks).
|
||||
"""
|
||||
today = date.today().isoformat() # noqa: DTZ011 - changelog dates are local calendar dates
|
||||
today = datetime.now().astimezone().date().isoformat()
|
||||
|
||||
def _entry_for(raw: str) -> tuple[str, str]:
|
||||
body = transform_changelog_message(raw).strip() or f"Release {new_version}"
|
||||
@@ -75,30 +81,10 @@ def update_changelog(
|
||||
print(f" - {finding}")
|
||||
print()
|
||||
|
||||
if message is not None:
|
||||
body, entry = _entry_for(message)
|
||||
findings = lint_changelog_entry(changelog_path, entry)
|
||||
if findings:
|
||||
_print_findings(findings)
|
||||
raise ChangelogLintError(
|
||||
"Provided changelog message is not markdown-lint clean."
|
||||
)
|
||||
elif preview or not sys.stdin.isatty():
|
||||
body, entry = _entry_for(message or f"Release {new_version}")
|
||||
else:
|
||||
attempt: str | None = None
|
||||
while True:
|
||||
print(
|
||||
"\n[INFO] Provide the changelog entry — a leading '#' becomes "
|
||||
"bold, `code` becomes italic.\n"
|
||||
)
|
||||
raw = _open_editor_for_changelog(attempt)
|
||||
body, entry = _entry_for(raw or f"Release {new_version}")
|
||||
findings = lint_changelog_entry(changelog_path, entry)
|
||||
if not findings:
|
||||
break
|
||||
_print_findings(findings)
|
||||
attempt = body
|
||||
def _wait_for_reader() -> None:
|
||||
try:
|
||||
input("[INFO] Press Enter to re-open the editor and fix the entry...")
|
||||
except EOFError:
|
||||
print("[INFO] Re-opening the editor so you can fix the entry...")
|
||||
|
||||
changelog = ""
|
||||
@@ -109,6 +95,39 @@ def update_changelog(
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
print(f"[WARN] Could not read existing CHANGELOG.md: {exc}")
|
||||
|
||||
def _findings_for(entry: str) -> list[str]:
|
||||
return lint_changelog_entry(changelog_path, entry) or lint_changelog_context(
|
||||
changelog_path, changelog, _insert_after_h1(changelog, entry)
|
||||
)
|
||||
|
||||
if message is not None:
|
||||
body, entry = _entry_for(message)
|
||||
findings = _findings_for(entry)
|
||||
if findings:
|
||||
_print_findings(findings)
|
||||
raise ChangelogLintError(
|
||||
"Provided changelog message is not markdown-lint clean."
|
||||
)
|
||||
elif preview or not sys.stdin.isatty():
|
||||
body, entry = _entry_for(message or f"Release {new_version}")
|
||||
else:
|
||||
attempt: str | None = None
|
||||
rejected: list[str] | None = None
|
||||
while True:
|
||||
print(
|
||||
"\n[INFO] Provide the changelog entry - a leading '#' becomes "
|
||||
"a sub-heading, `code` becomes italic.\n"
|
||||
)
|
||||
raw = _open_editor_for_changelog(attempt, rejected)
|
||||
body, entry = _entry_for(raw or f"Release {new_version}")
|
||||
findings = _findings_for(entry)
|
||||
if not findings:
|
||||
break
|
||||
_print_findings(findings)
|
||||
attempt = body
|
||||
rejected = findings
|
||||
_wait_for_reader()
|
||||
|
||||
new_changelog = _insert_after_h1(changelog, entry)
|
||||
|
||||
print("\n================ CHANGELOG ENTRY ================")
|
||||
|
||||
@@ -6,7 +6,14 @@ import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def _open_editor_for_changelog(initial_message: str | None = None) -> str:
|
||||
def _open_editor_for_changelog(
|
||||
initial_message: str | None = None,
|
||||
findings: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Args:
|
||||
initial_message: the previous attempt, pre-loaded for editing.
|
||||
findings: why that attempt was rejected, shown above it.
|
||||
"""
|
||||
editor = os.environ.get("EDITOR", "nano")
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
@@ -18,9 +25,15 @@ def _open_editor_for_changelog(initial_message: str | None = None) -> str:
|
||||
tmp.write(
|
||||
"; Write the changelog entry for this release.\n"
|
||||
"; Lines starting with ';' are ignored.\n"
|
||||
"; A leading '#' becomes bold; `code` becomes italic.\n"
|
||||
"; Empty result will fall back to a generic message.\n\n"
|
||||
"; A leading '#' becomes a sub-heading; `code` becomes italic.\n"
|
||||
"; Empty result will fall back to a generic message.\n"
|
||||
)
|
||||
if findings:
|
||||
tmp.write(";\n; markdown-lint rejected the previous entry:\n")
|
||||
for finding in findings:
|
||||
for line in str(finding).splitlines() or [""]:
|
||||
tmp.write(f"; {line}\n")
|
||||
tmp.write("\n")
|
||||
if initial_message:
|
||||
tmp.write(initial_message.strip() + "\n")
|
||||
tmp.flush()
|
||||
|
||||
@@ -93,7 +93,6 @@ class TestIntegrationUpdateAllshallowNoSystem(unittest.TestCase):
|
||||
"--clone-mode",
|
||||
"shallow",
|
||||
"--no-verification",
|
||||
"--silent",
|
||||
]
|
||||
self._run_cmd(["pkgmgr", *args], label="pkgmgr", env=env)
|
||||
pkgmgr_help_debug()
|
||||
@@ -108,7 +107,6 @@ class TestIntegrationUpdateAllshallowNoSystem(unittest.TestCase):
|
||||
"--clone-mode",
|
||||
"shallow",
|
||||
"--no-verification",
|
||||
"--silent",
|
||||
]
|
||||
self._run_cmd(
|
||||
["nix", "run", ".#pkgmgr", "--", *args],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -25,33 +24,6 @@ class TestGitVerificationRuntimeDependencies(unittest.TestCase):
|
||||
self.assertIn("pkgs.git", flake_text)
|
||||
self.assertIn("pkgs.gnupg", flake_text)
|
||||
|
||||
def test_distro_dependency_scripts_install_gpg_tools(self) -> None:
|
||||
repo_root = _find_repo_root()
|
||||
expected_packages = {
|
||||
"arch": "gnupg",
|
||||
"debian": "gnupg",
|
||||
"ubuntu": "gnupg",
|
||||
"fedora": "gnupg2",
|
||||
"centos": "gnupg2",
|
||||
}
|
||||
|
||||
missing: list[str] = []
|
||||
for distro, package_name in expected_packages.items():
|
||||
script_path = (
|
||||
repo_root / "scripts" / "installation" / distro / "dependencies.sh"
|
||||
)
|
||||
content = script_path.read_text(encoding="utf-8")
|
||||
if not re.search(rf"\b{re.escape(package_name)}\b", content):
|
||||
missing.append(
|
||||
f"{distro}: expected package {package_name} in {script_path}"
|
||||
)
|
||||
|
||||
if missing:
|
||||
self.fail(
|
||||
"Git signature verification runtime dependencies are incomplete:\n"
|
||||
+ "\n".join(f" - {item}" for item in missing)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,9 +1,5 @@
|
||||
"""
|
||||
Integration tests for recursive capability resolution and installer shadowing.
|
||||
|
||||
These tests verify that, given different repository layouts (Makefile, pyproject,
|
||||
flake.nix, PKGBUILD), only the expected installers are executed based on the
|
||||
capabilities provided by higher layers.
|
||||
Integration tests for installer selection across repository layouts.
|
||||
|
||||
Layer order (strongest → weakest):
|
||||
|
||||
@@ -129,10 +125,8 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase):
|
||||
"With only a Makefile, the MakefileInstaller should run exactly once.",
|
||||
)
|
||||
|
||||
def test_python_and_makefile_both_run_when_caps_disjoint(self) -> None:
|
||||
"""
|
||||
If Python and Makefile have disjoint capabilities, both installers run.
|
||||
"""
|
||||
def test_only_the_first_supported_hook_runs(self) -> None:
|
||||
"""A repository with two usable hooks is installed by the first one."""
|
||||
repo_dir = self._new_repo()
|
||||
|
||||
# pyproject.toml without any explicit "make install" hint
|
||||
@@ -153,9 +147,8 @@ class TestRecursiveCapabilitiesIntegration(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
called,
|
||||
["python", "makefile"],
|
||||
"PythonInstaller and MakefileInstaller should both run when their "
|
||||
"capabilities are disjoint.",
|
||||
["python"],
|
||||
"Only the first supported hook may install the repository.",
|
||||
)
|
||||
|
||||
def test_python_shadows_makefile_when_pyproject_mentions_make_install(self) -> None:
|
||||
|
||||
@@ -72,6 +72,8 @@ class TestDebianControlInstaller(unittest.TestCase):
|
||||
return "/usr/bin/dpkg-buildpackage"
|
||||
if name == "apt-get":
|
||||
return "/usr/bin/apt-get"
|
||||
if name == "sudo":
|
||||
return "/usr/bin/sudo"
|
||||
return None
|
||||
|
||||
mock_which.side_effect = which_side_effect
|
||||
|
||||
@@ -81,6 +81,19 @@ class TestMakefileInstaller(unittest.TestCase):
|
||||
)
|
||||
mock_run_command.assert_not_called()
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.makefile.run_command")
|
||||
@patch(
|
||||
"builtins.open",
|
||||
new_callable=mock_open,
|
||||
read_data="install-python:\n\tpip install -r requirements.txt\n",
|
||||
)
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_run_skips_a_makefile_that_only_has_a_prefixed_target(
|
||||
self, mock_exists, mock_file, mock_run_command
|
||||
):
|
||||
self.installer.run(self.ctx)
|
||||
mock_run_command.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -21,11 +21,13 @@ class DummyInstaller(BaseInstaller):
|
||||
layer: str | None = None,
|
||||
supports_result: bool = True,
|
||||
capabilities: set[str] | None = None,
|
||||
fails: bool = False,
|
||||
) -> None:
|
||||
self._name = name
|
||||
self.layer = layer # type: ignore[assignment]
|
||||
self._supports_result = supports_result
|
||||
self._capabilities = capabilities or set()
|
||||
self._fails = fails
|
||||
self.ran = False
|
||||
|
||||
def supports(self, ctx: RepoContext) -> bool: # type: ignore[override]
|
||||
@@ -33,6 +35,8 @@ class DummyInstaller(BaseInstaller):
|
||||
|
||||
def run(self, ctx: RepoContext) -> None: # type: ignore[override]
|
||||
self.ran = True
|
||||
if self._fails:
|
||||
raise SystemExit(2)
|
||||
|
||||
def discover_capabilities(self, ctx: RepoContext) -> set[str]: # type: ignore[override]
|
||||
return set(self._capabilities)
|
||||
@@ -117,15 +121,12 @@ class TestInstallationPipeline(unittest.TestCase):
|
||||
|
||||
@patch("pkgmgr.actions.install.pipeline.create_ink")
|
||||
@patch("pkgmgr.actions.install.pipeline.resolve_command_for_repo")
|
||||
def test_capabilities_prevent_duplicate_installers(
|
||||
def test_only_one_installation_hook_runs(
|
||||
self,
|
||||
mock_resolve_command_for_repo: MagicMock,
|
||||
mock_create_ink: MagicMock,
|
||||
) -> None:
|
||||
"""
|
||||
If one installer has already provided a set of capabilities,
|
||||
a second installer advertising the same capabilities should be skipped.
|
||||
"""
|
||||
"""A repository is installed once, by the first hook that supports it."""
|
||||
mock_resolve_command_for_repo.return_value = None # no CLI initially
|
||||
|
||||
ctx = _minimal_context()
|
||||
@@ -148,9 +149,103 @@ class TestInstallationPipeline(unittest.TestCase):
|
||||
self.assertTrue(first.ran, "First installer should run.")
|
||||
self.assertFalse(
|
||||
second.ran,
|
||||
"Second installer with identical capabilities must be skipped.",
|
||||
"A second installation hook must not run after the first one did.",
|
||||
)
|
||||
|
||||
@patch("pkgmgr.actions.install.pipeline.create_ink")
|
||||
@patch("pkgmgr.actions.install.pipeline.resolve_command_for_repo")
|
||||
def test_a_differing_second_hook_is_not_run_either(
|
||||
self,
|
||||
mock_resolve_command_for_repo: MagicMock,
|
||||
mock_create_ink: MagicMock,
|
||||
) -> None:
|
||||
"""The rule is one hook, not one hook per capability."""
|
||||
mock_resolve_command_for_repo.return_value = None
|
||||
|
||||
ctx = _minimal_context()
|
||||
first = DummyInstaller(
|
||||
"python-installer",
|
||||
layer=CliLayer.PYTHON.value,
|
||||
supports_result=True,
|
||||
capabilities={"python-runtime"},
|
||||
)
|
||||
second = DummyInstaller(
|
||||
"makefile-installer",
|
||||
layer=CliLayer.MAKEFILE.value,
|
||||
supports_result=True,
|
||||
capabilities={"make-install"},
|
||||
)
|
||||
|
||||
pipeline = InstallationPipeline([first, second])
|
||||
pipeline.run(ctx)
|
||||
|
||||
self.assertTrue(first.ran, "The first supported hook should run.")
|
||||
self.assertFalse(
|
||||
second.ran,
|
||||
"A hook advertising a different capability must not run either: "
|
||||
"the project is already installed.",
|
||||
)
|
||||
|
||||
@patch("pkgmgr.actions.install.pipeline.create_ink")
|
||||
@patch("pkgmgr.actions.install.pipeline.resolve_command_for_repo")
|
||||
def test_a_failed_hook_falls_back_to_the_next(
|
||||
self,
|
||||
mock_resolve_command_for_repo: MagicMock,
|
||||
mock_create_ink: MagicMock,
|
||||
) -> None:
|
||||
"""A hook that fails installed nothing, so the next one gets its turn."""
|
||||
mock_resolve_command_for_repo.return_value = None
|
||||
|
||||
ctx = _minimal_context()
|
||||
broken = DummyInstaller(
|
||||
"makefile-installer",
|
||||
layer=CliLayer.MAKEFILE.value,
|
||||
supports_result=True,
|
||||
fails=True,
|
||||
)
|
||||
working = DummyInstaller(
|
||||
"python-installer",
|
||||
layer=CliLayer.PYTHON.value,
|
||||
supports_result=True,
|
||||
)
|
||||
|
||||
pipeline = InstallationPipeline([broken, working])
|
||||
pipeline.run(ctx)
|
||||
|
||||
self.assertTrue(broken.ran, "The preferred hook should be tried first.")
|
||||
self.assertTrue(
|
||||
working.ran,
|
||||
"The next hook must run when the preferred one installed nothing.",
|
||||
)
|
||||
|
||||
@patch("pkgmgr.actions.install.pipeline.create_ink")
|
||||
@patch("pkgmgr.actions.install.pipeline.resolve_command_for_repo")
|
||||
def test_a_repository_whose_hooks_all_fail_is_reported(
|
||||
self,
|
||||
mock_resolve_command_for_repo: MagicMock,
|
||||
mock_create_ink: MagicMock,
|
||||
) -> None:
|
||||
"""Falling back must not turn a broken repository into a silent pass."""
|
||||
mock_resolve_command_for_repo.return_value = None
|
||||
|
||||
ctx = _minimal_context()
|
||||
first = DummyInstaller(
|
||||
"makefile-installer",
|
||||
layer=CliLayer.MAKEFILE.value,
|
||||
supports_result=True,
|
||||
fails=True,
|
||||
)
|
||||
second = DummyInstaller(
|
||||
"python-installer",
|
||||
layer=CliLayer.PYTHON.value,
|
||||
supports_result=True,
|
||||
fails=True,
|
||||
)
|
||||
|
||||
pipeline = InstallationPipeline([first, second])
|
||||
with self.assertRaises(SystemExit):
|
||||
pipeline.run(ctx)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,23 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.release.files import changelog_lint
|
||||
from pkgmgr.actions.release.files.changelog_lint import (
|
||||
lint_changelog_context,
|
||||
lint_changelog_entry,
|
||||
transform_changelog_message,
|
||||
)
|
||||
from pkgmgr.actions.release.files.changelog_md import _insert_after_h1
|
||||
|
||||
_OLD_ASTERISK_FILE = "# Changelog\n\n## [1.0.0] - 2026-01-01\n\n* old bullet\n"
|
||||
|
||||
|
||||
class TestTransformChangelogMessage(unittest.TestCase):
|
||||
def test_heading_becomes_bold(self) -> None:
|
||||
def test_heading_is_pushed_below_the_release_heading(self) -> None:
|
||||
out = transform_changelog_message("# Title\n\nbody")
|
||||
self.assertIn("**Title**", out)
|
||||
self.assertNotIn("# Title", out)
|
||||
self.assertIn("### Title", out)
|
||||
self.assertNotIn("**Title**", out)
|
||||
|
||||
def test_multi_hash_heading_becomes_bold(self) -> None:
|
||||
self.assertEqual(
|
||||
transform_changelog_message("### Sub heading"), "**Sub heading**"
|
||||
)
|
||||
def test_a_heading_already_deep_enough_keeps_its_level(self) -> None:
|
||||
self.assertEqual(transform_changelog_message("### Sub heading"), "### Sub heading")
|
||||
|
||||
def test_a_deeper_heading_is_left_alone(self) -> None:
|
||||
self.assertEqual(transform_changelog_message("#### Deeper"), "#### Deeper")
|
||||
|
||||
def test_inline_code_becomes_italic(self) -> None:
|
||||
out = transform_changelog_message("use `pkgmgr release` now")
|
||||
@@ -27,18 +35,52 @@ class TestTransformChangelogMessage(unittest.TestCase):
|
||||
def test_plain_message_is_unchanged(self) -> None:
|
||||
self.assertEqual(transform_changelog_message("just text"), "just text")
|
||||
|
||||
def test_no_heading_or_backtick_survives(self) -> None:
|
||||
def test_no_backtick_survives_and_no_heading_competes(self) -> None:
|
||||
out = transform_changelog_message("# Heading\n\n* item with `code`")
|
||||
self.assertNotIn("`", out)
|
||||
for line in out.split("\n"):
|
||||
self.assertFalse(line.lstrip().startswith("#"))
|
||||
if line.lstrip().startswith("#"):
|
||||
self.assertTrue(line.startswith("###"), line)
|
||||
|
||||
|
||||
class TestLintChangelogEntry(unittest.TestCase):
|
||||
def test_clean_entry_has_no_findings(self) -> None:
|
||||
entry = "## [1.0.0] - 2026-01-01\n\n* a clean bullet\n\n"
|
||||
entry = "## [1.0.0] - 2026-01-01\n\n- a clean bullet\n\n"
|
||||
self.assertEqual(lint_changelog_entry("CHANGELOG.md", entry), [])
|
||||
|
||||
def test_a_structured_message_survives_its_own_transform(self) -> None:
|
||||
body = transform_changelog_message(
|
||||
"# Security\n\n- a bullet\n\n## Fixed\n\n- another bullet"
|
||||
)
|
||||
entry = f"## [1.0.0] - 2026-01-01\n\n{body}\n\n"
|
||||
self.assertEqual(lint_changelog_entry("CHANGELOG.md", entry), [])
|
||||
|
||||
def test_the_builtin_check_refuses_a_non_dash_list_marker(self) -> None:
|
||||
entry = "## [1.0.0] - 2026-01-01\n\n* a bullet\n\n"
|
||||
with patch.object(changelog_lint.shutil, "which", return_value=None):
|
||||
findings = lint_changelog_entry("CHANGELOG.md", entry)
|
||||
self.assertTrue(any("MD004" in finding for finding in findings), findings)
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("markdownlint-cli2"), "markdownlint-cli2 is not installed")
|
||||
class TestLintChangelogContext(unittest.TestCase):
|
||||
def test_a_marker_that_breaks_the_files_consistency_is_reported(self) -> None:
|
||||
entry = "## [1.1.0] - 2026-02-01\n\n- new bullet\n\n"
|
||||
merged = _insert_after_h1(_OLD_ASTERISK_FILE, entry)
|
||||
findings = lint_changelog_context("CHANGELOG.md", _OLD_ASTERISK_FILE, merged)
|
||||
self.assertTrue(any("MD004" in finding for finding in findings), findings)
|
||||
|
||||
def test_a_finding_the_file_already_has_is_not_blamed_on_the_entry(self) -> None:
|
||||
existing = "# Changelog\n\n## [1.0.0] - 2026-01-01\n\n* old bullet \n"
|
||||
entry = "## [1.1.0] - 2026-02-01\n\n* new bullet\n\n"
|
||||
merged = _insert_after_h1(existing, entry)
|
||||
self.assertEqual(lint_changelog_context("CHANGELOG.md", existing, merged), [])
|
||||
|
||||
def test_a_consistent_entry_has_no_findings(self) -> None:
|
||||
entry = "## [1.1.0] - 2026-02-01\n\n* new bullet\n\n"
|
||||
merged = _insert_after_h1(_OLD_ASTERISK_FILE, entry)
|
||||
self.assertEqual(lint_changelog_context("CHANGELOG.md", _OLD_ASTERISK_FILE, merged), [])
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from pkgmgr.actions.release.files.changelog_md import _insert_after_h1
|
||||
|
||||
ENTRY = "## [1.0.0] - 2026-09-18\n\nOfficial Release\n\n"
|
||||
|
||||
|
||||
class TestInsertAfterH1(unittest.TestCase):
|
||||
def _assert_lint_clean(self, document: str) -> None:
|
||||
self.assertNotIn(
|
||||
"\n\n\n",
|
||||
document,
|
||||
f"MD012: multiple consecutive blank lines in\n{document!r}",
|
||||
)
|
||||
self.assertTrue(
|
||||
document.endswith("\n") and not document.endswith("\n\n"),
|
||||
f"MD012: blank line at end of file in\n{document!r}",
|
||||
)
|
||||
|
||||
def test_an_empty_changelog_gets_one_trailing_newline(self) -> None:
|
||||
self._assert_lint_clean(_insert_after_h1("", ENTRY))
|
||||
|
||||
def test_a_first_entry_under_a_bare_h1_gets_one_trailing_newline(self) -> None:
|
||||
self._assert_lint_clean(_insert_after_h1("# Changelog\n", ENTRY))
|
||||
|
||||
def test_a_second_entry_stays_separated_from_the_first(self) -> None:
|
||||
existing = "# Changelog\n\n## [0.9.0] - 2026-09-01\n\nOlder\n"
|
||||
document = _insert_after_h1(existing, ENTRY)
|
||||
self._assert_lint_clean(document)
|
||||
self.assertIn("Official Release\n\n## [0.9.0]", document)
|
||||
|
||||
def test_a_legacy_headerless_changelog_gains_an_h1(self) -> None:
|
||||
document = _insert_after_h1("## [0.9.0] - 2026-09-01\n\nOlder\n", ENTRY)
|
||||
self._assert_lint_clean(document)
|
||||
self.assertTrue(document.startswith("# Changelog\n\n## [1.0.0]"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from pkgmgr.actions.release.files.editor import _open_editor_for_changelog
|
||||
|
||||
|
||||
class TestOpenEditorForChangelog(unittest.TestCase):
|
||||
def _buffer(self, **kwargs: object) -> str:
|
||||
"""Returns: what the editor was handed, with the editor itself a no-op."""
|
||||
seen: dict[str, str] = {}
|
||||
|
||||
def record(argv: list[str]) -> int:
|
||||
with open(argv[1], encoding="utf-8") as handle:
|
||||
seen["text"] = handle.read()
|
||||
return 0
|
||||
|
||||
with mock.patch(
|
||||
"pkgmgr.actions.release.files.editor.subprocess.call", side_effect=record
|
||||
):
|
||||
_open_editor_for_changelog(**kwargs)
|
||||
return seen["text"]
|
||||
|
||||
def test_the_rejection_reaches_the_editor_the_entry_is_fixed_in(self) -> None:
|
||||
text = self._buffer(
|
||||
initial_message="### Security\n\n- a bullet",
|
||||
findings=["changelog entry:3 error MD036/no-emphasis-as-heading"],
|
||||
)
|
||||
self.assertIn("MD036/no-emphasis-as-heading", text)
|
||||
self.assertIn("### Security", text)
|
||||
|
||||
def test_every_finding_line_is_commented_out(self) -> None:
|
||||
text = self._buffer(findings=["first line\nsecond line"])
|
||||
for line in text.splitlines():
|
||||
if "line" in line and "ignored" not in line:
|
||||
self.assertTrue(line.startswith(";"), line)
|
||||
|
||||
def test_a_finding_never_lands_in_the_entry(self) -> None:
|
||||
seen: dict[str, str] = {}
|
||||
|
||||
def rewrite(argv: list[str]) -> int:
|
||||
with open(argv[1], encoding="utf-8") as handle:
|
||||
seen["text"] = handle.read()
|
||||
return 0
|
||||
|
||||
with mock.patch(
|
||||
"pkgmgr.actions.release.files.editor.subprocess.call", side_effect=rewrite
|
||||
):
|
||||
kept = _open_editor_for_changelog(
|
||||
initial_message="- a bullet", findings=["MD036 somewhere"]
|
||||
)
|
||||
self.assertEqual(kept, "- a bullet")
|
||||
|
||||
def test_without_findings_the_header_stays_as_it_was(self) -> None:
|
||||
self.assertNotIn("markdown-lint rejected", self._buffer())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
@@ -313,13 +313,13 @@ class TestUpdateChangelog(unittest.TestCase):
|
||||
def test_update_changelog_does_not_inject_a_leading_bullet(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = os.path.join(tmpdir, "CHANGELOG.md")
|
||||
update_changelog(path, "1.2.3", message="* Provided bullet", preview=False)
|
||||
update_changelog(path, "1.2.3", message="- Provided bullet", preview=False)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
self.assertIn("\n\n* Provided bullet\n", content)
|
||||
self.assertNotIn("* * Provided bullet", content)
|
||||
self.assertIn("\n\n- Provided bullet\n", content)
|
||||
self.assertNotIn("- - Provided bullet", content)
|
||||
|
||||
def test_update_changelog_transforms_heading_and_inline_code(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -327,16 +327,17 @@ class TestUpdateChangelog(unittest.TestCase):
|
||||
update_changelog(
|
||||
path,
|
||||
"1.2.3",
|
||||
message="# Summary\n\n* uses `foo` tool",
|
||||
message="# Summary\n\n- uses `foo` tool",
|
||||
preview=False,
|
||||
)
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
self.assertIn("**Summary**", content)
|
||||
self.assertIn("### Summary", content)
|
||||
self.assertIn("*foo*", content)
|
||||
self.assertNotIn("# Summary", content)
|
||||
self.assertNotIn("\n# Summary", content)
|
||||
self.assertNotIn("\n## Summary", content)
|
||||
self.assertNotIn("`foo`", content)
|
||||
|
||||
def test_update_changelog_preview_does_not_write(self) -> None:
|
||||
|
||||
@@ -29,22 +29,19 @@ class FindDefaultsSourceDirTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(Path(found).resolve(), (pkg_root / "config").resolve())
|
||||
|
||||
def test_falls_back_to_project_root_config(self):
|
||||
def test_a_config_dir_beside_the_package_is_not_taken(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
pkg_root = root / "site-packages" / "pkgmgr"
|
||||
pkg_root.mkdir(parents=True)
|
||||
|
||||
# only project_root config exists
|
||||
(pkg_root.parent / "config").mkdir(parents=True)
|
||||
|
||||
fake_pkgmgr = types.SimpleNamespace(__file__=str(pkg_root / "__init__.py"))
|
||||
with patch.dict(sys.modules, {"pkgmgr": fake_pkgmgr}):
|
||||
found = config_cmd._find_defaults_source_dir()
|
||||
|
||||
self.assertEqual(
|
||||
Path(found).resolve(), (pkg_root.parent / "config").resolve()
|
||||
)
|
||||
self.assertIsNone(found)
|
||||
|
||||
def test_returns_none_when_no_config_dirs_exist(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
@@ -88,9 +85,8 @@ class UpdateDefaultConfigsTests(unittest.TestCase):
|
||||
self.assertTrue((dest_cfg_dir / "a.yaml").is_file())
|
||||
self.assertTrue((dest_cfg_dir / "b.yml").is_file())
|
||||
self.assertFalse(
|
||||
(dest_cfg_dir / "config.yaml")
|
||||
.read_text(encoding="utf-8")
|
||||
.startswith("should_not_copy")
|
||||
(dest_cfg_dir / "config.yaml").exists(),
|
||||
"the source config.yaml must not be copied over the user's own",
|
||||
)
|
||||
|
||||
# Ensure config.yaml was not overwritten (it may exist, but should remain original if we create it)
|
||||
|
||||
@@ -242,9 +242,7 @@ class LoadConfigIntegrationUnitTests(unittest.TestCase):
|
||||
# Provide package defaults via fake pkgmgr + pkg_root/config
|
||||
root = Path(td) / "site-packages"
|
||||
pkg_root = root / "pkgmgr"
|
||||
cfg_dir = (
|
||||
root / "config"
|
||||
) # NOTE: load.py checks multiple roots, including pkg_root.parent (=site-packages)
|
||||
cfg_dir = pkg_root / "config"
|
||||
pkg_root.mkdir(parents=True)
|
||||
cfg_dir.mkdir(parents=True)
|
||||
|
||||
|
||||
Reference in new issue
Block a user