Skip to content

feat: add package changelog viewer#73

Open
patrykcodex-del wants to merge 5 commits into
cxlinux-ai:mainfrom
patrykcodex-del:feature/changelog-viewer-35
Open

feat: add package changelog viewer#73
patrykcodex-del wants to merge 5 commits into
cxlinux-ai:mainfrom
patrykcodex-del:feature/changelog-viewer-35

Conversation

@patrykcodex-del
Copy link
Copy Markdown

@patrykcodex-del patrykcodex-del commented Jun 1, 2026

Summary

  • Adds an offline package changelog viewer at scripts/changelog.py for local Debian package changelogs under packages/<name>/debian/changelog
  • Supports formatted display, text/version search, security-fix filtering, version-range comparison, and JSON export
  • Adds focused unit tests plus a make test-changelog target and README usage examples

Test plan

  • python3 -m py_compile scripts/changelog.py tests/test_changelog.py
  • python3 -m unittest tests/test_changelog.py
  • make test-changelog
  • python3 scripts/changelog.py cx-core --security --export /tmp/cx-core-security.json
  • git diff --check

Refs #35

Summary by CodeRabbit

  • New Features

    • Offline changelog viewer: inspect Debian package changelogs with search, filtering, version comparison, and JSON export.
    • Optional security-only filtering/highlighting for entries and bullets.
  • Chores

    • Added public make target test-changelog and documented it in help/.PHONY.
    • Updated test target to run changelog unit tests.
  • Documentation

    • README updated with usage and build-target docs for the changelog viewer.
  • Tests

    • Added unit/integration tests covering parsing, security detection, search/compare, export, and CLI error cases.

@patrykcodex-del patrykcodex-del requested a review from a team as a code owner June 1, 2026 19:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0c96497-0b98-49a0-b665-bdbd37e6df90

📥 Commits

Reviewing files that changed from the base of the PR and between 1933e62 and bf7c364.

📒 Files selected for processing (2)
  • scripts/changelog.py
  • tests/test_changelog.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/changelog.py
  • tests/test_changelog.py

📝 Walkthrough

Walkthrough

Adds an offline Python CLI (scripts/changelog.py) to parse Debian changelogs into structured entries, search and filter (including security-only), compare version ranges, and export JSON. Adds tests/test_changelog.py with unit and CLI tests and integrates the tests into Makefile and README via a new test-changelog target.

Changes

Changelog Viewer Feature

Layer / File(s) Summary
Changelog data model and parsing
scripts/changelog.py
Defines ChangelogEntry dataclass with has_security_fix; adds parsing regexes and parse_changelog to extract versions, metadata, and bullets into structured entries.
Loading, filtering, and compare logic
scripts/changelog.py
load_entries resolves default changelog path and raises FileNotFoundError when missing; filter_entries performs case-insensitive keyword matching; compare_entries returns a contiguous range between newer and older exact version bounds.
Formatting and JSON export
scripts/changelog.py
format_entries emits human-readable text with per-entry security markers and per-bullet security labels; export_entries writes selected entries to JSON including computed has_security_fix, creating parent directories when needed.
CLI interface and orchestration
scripts/changelog.py
build_parser defines package/file/search/security/export args; main orchestrates load → optional compare → filter → format → optional export, prints to stdout, and exits with code 2 on file/compare/export errors.
Unit and integration tests
tests/test_changelog.py
Adds fixture SAMPLE_CHANGELOG and tests for parsing, dash-bullet handling, maintainer/date extraction, filtering, compare behavior (including invalid bounds), CLI integration with --security and --export, and export path creation.
Build system and documentation
Makefile, README.md
Adds test-changelog to .PHONY and help; integrates changelog unit tests into the main test target; documents make test-changelog and usage of scripts/changelog.py in README.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI entry
  participant Loader as load_entries
  participant Filter as filter_entries
  participant Compare as compare_entries
  participant Formatter as format_entries
  participant Exporter as export_entries
  participant IO as stdout/file

  CLI->>Loader: parse args, resolve/load changelog
  Loader->>Filter: return parsed entries
  Filter->>Compare: apply --older/--newer if provided
  Compare->>Formatter: select and order entries
  Formatter->>IO: print formatted output
  opt --export set
    Formatter->>Exporter: prepare JSON payload
    Exporter->>IO: write JSON file
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through changelogs, line by careful line,
Parsed dates and bullets, and flagged each CVE I find.
Search, compare, export — neat JSON on the shelf,
A tiny CLI helper to inspect changelog health.
Hooray for small scripts that make changelogs shine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add package changelog viewer' accurately summarizes the main change: introducing a new package changelog viewer tool.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Debian package changelog viewer utility (scripts/changelog.py) that allows users to view, search, compare, and export package changelogs. It also adds a corresponding test suite (tests/test_changelog.py), updates the Makefile with a test-changelog target, and documents the tool in the README.md. The code reviewer provided valuable feedback to enhance the utility's robustness, such as improving parsing regexes, making the package argument optional when a file is explicitly provided, automatically creating parent directories on export, and adding validation for version comparison inputs.

Comment thread scripts/changelog.py Outdated
r"^(?P<package>[\w.+-]+) \((?P<version>[^)]+)\) (?P<distribution>[^;]+); urgency=(?P<urgency>\S+)"
)
SECURITY_RE = re.compile(r"\b(CVE-\d{4}-\d{4,}|security|vulnerab|exploit|privilege|auth)\b", re.I)
BULLET_RE = re.compile(r"^\s{2,}\*\s?(?P<text>.*)$")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Debian changelogs conventionally use * for bullet points, but some maintainers or tools use - as well. Updating the regex to support both * and - makes the parser more robust.

Suggested change
BULLET_RE = re.compile(r"^\s{2,}\*\s?(?P<text>.*)$")
BULLET_RE = re.compile(r"^\s{2,}[*-]\s?(?P<text>.*)$")

Comment thread scripts/changelog.py Outdated
)
SECURITY_RE = re.compile(r"\b(CVE-\d{4}-\d{4,}|security|vulnerab|exploit|privilege|auth)\b", re.I)
BULLET_RE = re.compile(r"^\s{2,}\*\s?(?P<text>.*)$")
MAINTAINER_RE = re.compile(r"^ -- (?P<maintainer>.+?) (?P<date>.+)$")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using \s{2,} instead of two literal spaces is more flexible and handles cases where there might be extra spaces before the date, while still preventing the non-greedy .+? from matching spaces within the maintainer's name.

Suggested change
MAINTAINER_RE = re.compile(r"^ -- (?P<maintainer>.+?) (?P<date>.+)$")
MAINTAINER_RE = re.compile(r"^ -- (?P<maintainer>.+?)\s{2,}(?P<date>.+)$")

Comment thread scripts/changelog.py Outdated
Comment on lines +96 to +100
def load_entries(package: str, changelog: Path | None = None) -> list[ChangelogEntry]:
path = changelog or default_changelog_path(package)
if not path.exists():
raise FileNotFoundError(f"No changelog found for {package!r}: {path}")
return parse_changelog(path.read_text(encoding="utf-8"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Allow package to be optional when changelog is explicitly provided via --file. This avoids requiring a dummy package name on the CLI when auditing a specific file.

Suggested change
def load_entries(package: str, changelog: Path | None = None) -> list[ChangelogEntry]:
path = changelog or default_changelog_path(package)
if not path.exists():
raise FileNotFoundError(f"No changelog found for {package!r}: {path}")
return parse_changelog(path.read_text(encoding="utf-8"))
def load_entries(package: str | None, changelog: Path | None = None) -> list[ChangelogEntry]:
path = changelog or (default_changelog_path(package) if package else None)
if not path:
raise ValueError("Either package or changelog file must be specified")
if not path.exists():
msg = f"No changelog found for {package!r}: {path}" if package else f"No changelog found: {path}"
raise FileNotFoundError(msg)
return parse_changelog(path.read_text(encoding="utf-8"))

Comment thread scripts/changelog.py
Comment on lines +136 to +142
def export_entries(entries: Iterable[ChangelogEntry], output: Path) -> None:
data = []
for entry in entries:
item = asdict(entry)
item["has_security_fix"] = entry.has_security_fix
data.append(item)
output.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the parent directory of the export path does not exist, write_text will raise a FileNotFoundError. Creating the parent directories automatically improves usability.

Suggested change
def export_entries(entries: Iterable[ChangelogEntry], output: Path) -> None:
data = []
for entry in entries:
item = asdict(entry)
item["has_security_fix"] = entry.has_security_fix
data.append(item)
output.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def export_entries(entries: Iterable[ChangelogEntry], output: Path) -> None:
data = []
for entry in entries:
item = asdict(entry)
item["has_security_fix"] = entry.has_security_fix
data.append(item)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")

Comment thread scripts/changelog.py Outdated
Comment on lines +145 to +147
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="View/search package changelogs")
parser.add_argument("package", help="package directory name under packages/")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Make the package positional argument optional (nargs='?') so that it is not required when --file is specified.

Suggested change
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="View/search package changelogs")
parser.add_argument("package", help="package directory name under packages/")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="View/search package changelogs")
parser.add_argument("package", nargs="?", help="package directory name under packages/")

Comment thread scripts/changelog.py Outdated
Comment on lines +157 to +169
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
entries = load_entries(args.package, args.file)
except FileNotFoundError as exc:
print(exc, file=sys.stderr)
return 2

if args.older or args.newer:
if not (args.older and args.newer):
print("compare mode requires both older and newer versions", file=sys.stderr)
return 2
entries = compare_entries(entries, args.older, args.newer)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add validation to ensure that:

  1. Either package or --file is provided.
  2. Both older and newer versions exist in the changelog when using compare mode.
  3. The newer version is actually newer than (or appears before) the older version in the changelog, preventing silent empty outputs due to incorrect ordering or typos.
def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    if not args.package and not args.file:
        parser.error("Either package or --file must be specified.")
    try:
        entries = load_entries(args.package, args.file)
    except FileNotFoundError as exc:
        print(exc, file=sys.stderr)
        return 2

    if args.older or args.newer:
        if not (args.older and args.newer):
            print("compare mode requires both older and newer versions", file=sys.stderr)
            return 2
        versions = [e.version for e in entries]
        if args.older not in versions:
            print(f"Error: older version {args.older!r} not found in changelog", file=sys.stderr)
            return 2
        if args.newer not in versions:
            print(f"Error: newer version {args.newer!r} not found in changelog", file=sys.stderr)
            return 2
        if versions.index(args.newer) > versions.index(args.older):
            print(f"Error: version {args.newer!r} is older than {args.older!r} in the changelog", file=sys.stderr)
            return 2
        entries = compare_entries(entries, args.older, args.newer)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/changelog.py`:
- Around line 111-121: The compare_entries function currently returns
partial/empty results when the provided newer/older versions are missing or
reversed; update compare_entries to validate that both newer and older exist in
the entries and that newer is encountered before older—if validation fails,
print a clear error message and exit non-zero (or raise SystemExit) instead of
returning a silent result; apply the same validation/failure behavior to the
other compare logic referenced around the 165-170 block so all compare paths
consistently fail fast when bounds are invalid.
- Around line 175-177: Wrap the export_entries(entries, args.export) call in
main with an OSError handler so filesystem/write errors produce a controlled CLI
failure: catch OSError as e, print or log a concise error message including
str(e) to stderr, and exit with a non-zero CLI code (e.g., sys.exit(2)); ensure
sys is imported if not present. This change should live around the
export_entries invocation in main (the args.export branch) so invalid
paths/permissions don't raise an unhandled traceback.

In `@tests/test_changelog.py`:
- Around line 49-75: The test test_export_json_and_cli_output calls
subprocess.run(...) to invoke scripts/changelog.py which still prints formatted
entries after calling export_entries (because scripts/changelog.py calls
format_entries(entries) and print(output)), and Ruff flags S603 on that
subprocess.run; fix by either adding a linter exemption comment "# noqa: S603"
to the subprocess.run(...) invocation in test_export_json_and_cli_output (to
silence the Ruff warning) or, better, remove/guard the post-export print in
scripts/changelog.py (where format_entries(entries) and print(output) are
called) so that when export_entries(...) is used no CLI output is printed;
reference symbols: test_export_json_and_cli_output, subprocess.run,
scripts/changelog.py, export_entries, format_entries, and print(output).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f79d144-c1e6-49c9-92f2-bc2597e4a2cb

📥 Commits

Reviewing files that changed from the base of the PR and between cfd0627 and d038b5a.

📒 Files selected for processing (4)
  • Makefile
  • README.md
  • scripts/changelog.py
  • tests/test_changelog.py

Comment thread scripts/changelog.py
Comment thread scripts/changelog.py
Comment thread tests/test_changelog.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/test_changelog.py (1)

106-128: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silence Ruff S603 on the subprocess.run invocation.

Ruff flags S603 on this subprocess.run(...) call (error-level), which can break a lint-gated pipeline. The inputs are static/trusted (sys.executable + repo path + literal flags), so suppress it inline. Apply the same treatment to the analogous call at Line 78 for consistency.

🧰 Proposed fix
             result = subprocess.run(
                 [
                     sys.executable,
                     str(ROOT / "scripts" / "changelog.py"),
                     "docker",
                     "24.0.5-1",
                     "24.0.7-1",
                     "--file",
                     str(source),
                 ],
                 check=False,
                 text=True,
                 capture_output=True,
-            )
+            )  # noqa: S603
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_changelog.py` around lines 106 - 128, Add an inline Ruff
suppression for S603 on the subprocess.run invocation in
test_cli_rejects_missing_or_invalid_compare_bounds by appending a "# noqa: S603"
comment to the subprocess.run(...) line to silence the warning for this
trusted/static input; also apply the same inline "# noqa: S603" suppression to
the analogous subprocess.run call earlier in the same test module (the other
test that runs the changelog script) so both calls consistently suppress Ruff
S603.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@tests/test_changelog.py`:
- Around line 106-128: Add an inline Ruff suppression for S603 on the
subprocess.run invocation in test_cli_rejects_missing_or_invalid_compare_bounds
by appending a "# noqa: S603" comment to the subprocess.run(...) line to silence
the warning for this trusted/static input; also apply the same inline "# noqa:
S603" suppression to the analogous subprocess.run call earlier in the same test
module (the other test that runs the changelog script) so both calls
consistently suppress Ruff S603.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa1d3110-54af-4264-8c6b-acd7a1a824c9

📥 Commits

Reviewing files that changed from the base of the PR and between d038b5a and 1933e62.

📒 Files selected for processing (2)
  • scripts/changelog.py
  • tests/test_changelog.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/changelog.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant