Compare commits

...
10 Commits
Author SHA1 Message Date
kevinveenbirkenbach 579ded3438 Release version 2.0.5
CI / security-codeql (push) Blocked by required conditions
CI / test-unit (push) Blocked by required conditions
CI / test-integration (push) Blocked by required conditions
CI / test-env-virtual (push) Blocked by required conditions
CI / test-env-nix (push) Blocked by required conditions
CI / test-e2e (push) Blocked by required conditions
CI / test-virgin-user (push) Blocked by required conditions
CI / test-virgin-root (push) Blocked by required conditions
CI / lint-shell (push) Blocked by required conditions
CI / lint-python (push) Blocked by required conditions
CI / lint-docker (push) Blocked by required conditions
CodeQL Advanced / Check security (none, actions) (push) Blocked by required conditions
CodeQL Advanced / Check security (none, python) (push) Blocked by required conditions
Test Units / test-unit (push) Blocked by required conditions
Test Code Integration / test-integration (push) Blocked by required conditions
Test OS Containers / test-env-virtual (arch) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (centos) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (debian) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (fedora) (push) Blocked by required conditions
Test OS Containers / test-env-virtual (ubuntu) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (arch) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (centos) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (debian) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (fedora) (push) Blocked by required conditions
Test Virgin Nix (flake only) / test-env-nix (ubuntu) (push) Blocked by required conditions
Test End-To-End / test-e2e (arch) (push) Blocked by required conditions
Test End-To-End / test-e2e (centos) (push) Blocked by required conditions
Test End-To-End / test-e2e (debian) (push) Blocked by required conditions
Test End-To-End / test-e2e (fedora) (push) Blocked by required conditions
Test End-To-End / test-e2e (ubuntu) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (arch) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (centos) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (debian) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (fedora) (push) Blocked by required conditions
Test Virgin User / test-virgin-user (ubuntu) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (arch) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (centos) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (debian) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (fedora) (push) Blocked by required conditions
Test Virgin Root / test-virgin-root (ubuntu) (push) Blocked by required conditions
ShellCheck / lint-shell (push) Blocked by required conditions
Ruff (Python code sniffer) / lint-python (push) Blocked by required conditions
Docker Linter / Lint Dockerfile (push) Blocked by required conditions
Mark stable commit / mark-stable (push) Waiting to run
2026-10-29 13:57:31 +01:00
kevinveenbirkenbachandClaude Opus 5.5 eb17cddd21 fix(config): open nano on the terminal in config edit
pkgmgr config edit ran nano through run_command, which pipes stdout and stderr to capture them, so nano aborted with 'Standard output is not a terminal'. The editor now runs with subprocess.call and the inherited terminal, and a failing editor exits with its code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:47:45 +01:00
kevinveenbirkenbach c7b070a62b Release version 2.0.4
CI / security-codeql (push) Has been cancelled
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / lint-shell (push) Has been cancelled
CI / lint-python (push) Has been cancelled
CI / lint-docker (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2026-10-19 10:06:02 +02:00
kevinveenbirkenbachandClaude Opus 5.5 20cd174a1f fix(release): date changelog entries without a ruff-version-dependent noqa
ruff 0.16 flags date.today() (DTZ011) while 0.17 drops that rule from its defaults and flags the noqa as unused (RUF100), so CI's unpinned ruff failed lint-python. datetime.now().astimezone().date() yields the same local date and passes both, matching debian.py and rpm_changelog.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 09:58:31 +02:00
kevinveenbirkenbach ae4bfb5965 Merge pull request #7 from kevinveenbirkenbach/dependabot/github_actions/actions/checkout-7
build(deps): bump actions/checkout from 4 to 7
2026-10-19 09:36:25 +02:00
kevinveenbirkenbach 4f71680bb1 Merge pull request #6 from kevinveenbirkenbach/dependabot/github_actions/hadolint/hadolint-action-3.5.0
build(deps): bump hadolint/hadolint-action from 3.3.0 to 3.5.0
2026-10-19 09:35:34 +02:00
kevinveenbirkenbach c78f5ac75e Release version 2.0.3
CI / security-codeql (push) Has been cancelled
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / lint-shell (push) Has been cancelled
CI / lint-python (push) Has been cancelled
CI / lint-docker (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Update flake.lock / Update flake.lock (push) Has been cancelled
2026-10-18 23:33:52 +02:00
kevinveenbirkenbachandClaude Opus 5.5 32e896fd09 fix(release): lint the changelog entry inside the file it lands in
The entry was linted alone, so rules that compare it with the rest of
CHANGELOG.md never fired. infinito-nexus 16.0.0 passed this check with
"-" bullets on top of older "*" entries, and the repository's own
markdown lint then reported 400 MD004 findings and refused the push.

update_changelog now also lints the merged file and reports only the
findings the existing file does not already produce
(lint_changelog_context), so pre-existing issues are not blamed on the
entry. markdownlint-cli2 runs with --no-globs, so a target config with
its own globs no longer lints the whole repository and blocks a release
on unrelated files. Without markdownlint-cli2 the built-in check now
requires "-" list markers (MD004 dash), the strictest style.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 23:31:22 +02:00
dependabot[bot] d24eb0155c build(deps): bump actions/checkout from 4 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 00:15:09 +00:00
dependabot[bot] c6e866b2c9 build(deps): bump hadolint/hadolint-action from 3.3.0 to 3.5.0
Bumps [hadolint/hadolint-action](https://github.com/hadolint/hadolint-action) from 3.3.0 to 3.5.0.
- [Release notes](https://github.com/hadolint/hadolint-action/releases)
- [Commits](https://github.com/hadolint/hadolint-action/compare/2332a7b74a6de0dda2e2221d575162eba76ba5e5...06be81baf89a55ffd0e24b8f04a4185738dd3387)

---
updated-dependencies:
- dependency-name: hadolint/hadolint-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 00:14:01 +00:00
26 changed files with 252 additions and 40 deletions

No files matched your search

+2 -2
View File
@@ -15,12 +15,12 @@ jobs:
security-events: write security-events: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Run hadolint (produce SARIF) - name: Run hadolint (produce SARIF)
id: hadolint id: hadolint
continue-on-error: true continue-on-error: true
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387
with: with:
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
format: sarif format: sarif
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v7 uses: actions/setup-python@v7
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
lint-shell: lint-shell:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install ShellCheck - name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck - name: Run ShellCheck
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true # We need tags and main history for version comparison fetch-tags: true # We need tags and main history for version comparison
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Checkout repository (with tags) - name: Checkout repository (with tags)
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v4
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show commit SHA - name: Show commit SHA
run: git rev-parse HEAD run: git rev-parse HEAD
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Show Docker version - name: Show Docker version
run: docker version run: docker version
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@v23 uses: DeterminateSystems/nix-installer-action@v23
+33
View File
@@ -1,5 +1,38 @@
# Changelog # Changelog
## [2.0.5] - 2026-10-29
Fixed:
- Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
- Config: *pkgmgr config edit* exits with the editor's code when it fails
## [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 ## [2.0.2] - 2026-10-01
Fixes a release flow that could reject every entry written for it. Fixes a release flow that could reject every entry written for it.
+1 -1
View File
@@ -32,7 +32,7 @@
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "2.0.2"; version = "2.0.5";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world> # Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager pkgname=package-manager
pkgver=2.0.2 pkgver=2.0.5
pkgrel=1 pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any') arch=('any')
+39
View File
@@ -1,3 +1,42 @@
package-manager (2.0.5-1) unstable; urgency=medium
Fixed:
- Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
- Config: *pkgmgr config edit* exits with the editor's code when it fails
-- Kevin Veen-Birkenbach <kevin@veen.world> Thu, 29 Oct 2026 13:57:31 +0100
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 package-manager (2.0.2-1) unstable; urgency=medium
Fixes a release flow that could reject every entry written for it. Fixes a release flow that could reject every entry written for it.
+31 -1
View File
@@ -1,5 +1,5 @@
Name: package-manager Name: package-manager
Version: 2.0.2 Version: 2.0.5
Release: 1%{?dist} Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -74,6 +74,36 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/ /usr/lib/package-manager/
%changelog %changelog
* Thu Oct 29 2026 Kevin Veen-Birkenbach <kevin@veen.world> - 2.0.5-1
- Fixed:
- - Config: *pkgmgr config edit* opens nano instead of aborting without a TTY
- - Config: *pkgmgr config edit* exits with the editor's code when it fails
* 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 * 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. - Fixes a release flow that could reject every entry written for it.
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kpmx" name = "kpmx"
version = "2.0.2" version = "2.0.5"
description = "Kevin's package-manager tool (pkgmgr)" description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
@@ -6,10 +6,13 @@ import re
import shutil import shutil
import subprocess import subprocess
import tempfile 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]+)`") _INLINE_CODE = re.compile(r"`([^`\n]+)`")
_FINDING = re.compile(r"\bMD\d{3}\b") _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,}") _MULTI_BLANK = re.compile(r"\n{3,}")
_ENTRY_LEVEL = 3 _ENTRY_LEVEL = 3
@@ -59,6 +62,35 @@ def lint_changelog_entry(reference_path: str, entry: str) -> list[str]:
return _builtin_lint(document) 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]: def _markdownlint(reference_path: str, document: str) -> list[str]:
directory = os.path.dirname(os.path.abspath(reference_path)) or "." directory = os.path.dirname(os.path.abspath(reference_path)) or "."
fd, tmp_path = tempfile.mkstemp( fd, tmp_path = tempfile.mkstemp(
@@ -69,7 +101,7 @@ def _markdownlint(reference_path: str, document: str) -> list[str]:
with os.fdopen(fd, "w", encoding="utf-8") as handle: with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(document) handle.write(document)
proc = subprocess.run( proc = subprocess.run(
["markdownlint-cli2", name], ["markdownlint-cli2", "--no-globs", name],
cwd=directory, cwd=directory,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -96,6 +128,8 @@ def _builtin_lint(document: str) -> list[str]:
errors.append(f"line {number}: trailing whitespace (MD009)") errors.append(f"line {number}: trailing whitespace (MD009)")
if "`" in line: if "`" in line:
errors.append(f"line {number}: backtick is not allowed (use *italic*)") 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: if "\n\n\n" in document:
errors.append("multiple consecutive blank lines (MD012)") errors.append("multiple consecutive blank lines (MD012)")
return errors return errors
@@ -3,10 +3,11 @@ from __future__ import annotations
import os import os
import re import re
import sys import sys
from datetime import date from datetime import datetime
from .changelog_lint import ( from .changelog_lint import (
ChangelogLintError, ChangelogLintError,
lint_changelog_context,
lint_changelog_entry, lint_changelog_entry,
transform_changelog_message, transform_changelog_message,
) )
@@ -68,7 +69,7 @@ def update_changelog(
missing) and above any existing release entries, so the result stays missing) and above any existing release entries, so the result stays
markdown-lint-clean (MD041 first-line-h1, MD012 no-multiple-blanks). 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]: def _entry_for(raw: str) -> tuple[str, str]:
body = transform_changelog_message(raw).strip() or f"Release {new_version}" body = transform_changelog_message(raw).strip() or f"Release {new_version}"
@@ -86,9 +87,22 @@ def update_changelog(
except EOFError: except EOFError:
print("[INFO] Re-opening the editor so you can fix the entry...") print("[INFO] Re-opening the editor so you can fix the entry...")
changelog = ""
if os.path.exists(changelog_path):
try:
with open(changelog_path, encoding="utf-8") as f:
changelog = f.read()
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: if message is not None:
body, entry = _entry_for(message) body, entry = _entry_for(message)
findings = lint_changelog_entry(changelog_path, entry) findings = _findings_for(entry)
if findings: if findings:
_print_findings(findings) _print_findings(findings)
raise ChangelogLintError( raise ChangelogLintError(
@@ -106,7 +120,7 @@ def update_changelog(
) )
raw = _open_editor_for_changelog(attempt, rejected) raw = _open_editor_for_changelog(attempt, rejected)
body, entry = _entry_for(raw or f"Release {new_version}") body, entry = _entry_for(raw or f"Release {new_version}")
findings = lint_changelog_entry(changelog_path, entry) findings = _findings_for(entry)
if not findings: if not findings:
break break
_print_findings(findings) _print_findings(findings)
@@ -114,14 +128,6 @@ def update_changelog(
rejected = findings rejected = findings
_wait_for_reader() _wait_for_reader()
changelog = ""
if os.path.exists(changelog_path):
try:
with open(changelog_path, encoding="utf-8") as f:
changelog = f.read()
except (OSError, UnicodeDecodeError) as exc:
print(f"[WARN] Could not read existing CHANGELOG.md: {exc}")
new_changelog = _insert_after_h1(changelog, entry) new_changelog = _insert_after_h1(changelog, entry)
print("\n================ CHANGELOG ENTRY ================") print("\n================ CHANGELOG ENTRY ================")
+4 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import os import os
import shutil import shutil
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -14,7 +15,6 @@ from pkgmgr.actions.config.add import interactive_add
from pkgmgr.actions.config.init import config_init from pkgmgr.actions.config.init import config_init
from pkgmgr.actions.config.show import show_config from pkgmgr.actions.config.show import show_config
from pkgmgr.cli.context import CLIContext from pkgmgr.cli.context import CLIContext
from pkgmgr.core.command.run import run_command
from pkgmgr.core.config.save import save_user_config from pkgmgr.core.config.save import save_user_config
from pkgmgr.core.repository.resolve import resolve_repos from pkgmgr.core.repository.resolve import resolve_repos
@@ -108,7 +108,9 @@ def handle_config(args, ctx: CLIContext) -> None:
return return
if args.subcommand == "edit": if args.subcommand == "edit":
run_command(f"nano {user_config_path}") returncode = subprocess.call(["nano", os.path.expanduser(user_config_path)])
if returncode != 0:
sys.exit(returncode)
return return
if args.subcommand == "init": if args.subcommand == "init":
@@ -1,11 +1,18 @@
from __future__ import annotations from __future__ import annotations
import shutil
import unittest import unittest
from unittest.mock import patch
from pkgmgr.actions.release.files import changelog_lint
from pkgmgr.actions.release.files.changelog_lint import ( from pkgmgr.actions.release.files.changelog_lint import (
lint_changelog_context,
lint_changelog_entry, lint_changelog_entry,
transform_changelog_message, 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): class TestTransformChangelogMessage(unittest.TestCase):
@@ -38,16 +45,42 @@ class TestTransformChangelogMessage(unittest.TestCase):
class TestLintChangelogEntry(unittest.TestCase): class TestLintChangelogEntry(unittest.TestCase):
def test_clean_entry_has_no_findings(self) -> None: 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), []) self.assertEqual(lint_changelog_entry("CHANGELOG.md", entry), [])
def test_a_structured_message_survives_its_own_transform(self) -> None: def test_a_structured_message_survives_its_own_transform(self) -> None:
body = transform_changelog_message( body = transform_changelog_message(
"# Security\n\n* a bullet\n\n## Fixed\n\n* another bullet" "# 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" entry = f"## [1.0.0] - 2026-01-01\n\n{body}\n\n"
self.assertEqual(lint_changelog_entry("CHANGELOG.md", entry), []) 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 if __name__ == "__main__": # pragma: no cover
unittest.main() unittest.main()
@@ -313,13 +313,13 @@ class TestUpdateChangelog(unittest.TestCase):
def test_update_changelog_does_not_inject_a_leading_bullet(self) -> None: def test_update_changelog_does_not_inject_a_leading_bullet(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "CHANGELOG.md") 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: with open(path, encoding="utf-8") as f:
content = f.read() content = f.read()
self.assertIn("\n\n* Provided bullet\n", content) self.assertIn("\n\n- Provided bullet\n", content)
self.assertNotIn("* * Provided bullet", content) self.assertNotIn("- - Provided bullet", content)
def test_update_changelog_transforms_heading_and_inline_code(self) -> None: def test_update_changelog_transforms_heading_and_inline_code(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
@@ -327,7 +327,7 @@ class TestUpdateChangelog(unittest.TestCase):
update_changelog( update_changelog(
path, path,
"1.2.3", "1.2.3",
message="# Summary\n\n* uses `foo` tool", message="# Summary\n\n- uses `foo` tool",
preview=False, preview=False,
) )
@@ -0,0 +1,35 @@
from __future__ import annotations
import types
import unittest
from unittest.mock import patch
from pkgmgr.cli.commands import config as config_cmd
class ConfigEditTests(unittest.TestCase):
def _edit(self, path: str, returncode: int):
with patch(
"subprocess.Popen",
side_effect=AssertionError("the editor's output must not be captured"),
), patch("subprocess.call", return_value=returncode) as call:
config_cmd.handle_config(
types.SimpleNamespace(subcommand="edit"),
types.SimpleNamespace(user_config_path=path),
)
return call
def test_nano_inherits_the_terminal(self):
call = self._edit("/cfg/config.yaml", 0)
call.assert_called_once_with(["nano", "/cfg/config.yaml"])
def test_a_failing_editor_exits_with_its_code(self):
with self.assertRaises(SystemExit) as raised:
self._edit("/cfg/config.yaml", 3)
self.assertEqual(raised.exception.code, 3)
if __name__ == "__main__":
unittest.main()