This is a multi-mode Python tool that rewrites GitHub's auto-generated release notes into polished, human-readable sentences using an LLM. It can be used as:
- CLI Tool - Traditional command-line workflow
- Python Library - Programmatic API for integration
- Web Backend - FastAPI service for web frontends or automation
It is tuned for ERPNext and the Frappe Framework by default, but can be adapted to other repositories by providing a custom prompt template.
- Rewrites GitHub release-note PR lines into clearer, user-facing summaries
- Supports provider-qualified LLM models through
any_llm(for exampleopenai:o3,openai:gpt-5,anthropic:claude-sonnet-4-5) - Filters non-user-facing changes by conventional commit type, label, or author
- Detects PRs that were reverted within the same release and removes both the original PR and the revert PR from output
- Reuses cached summaries for direct backports by using the original PR number as the summary key
- Falls back to commit-based generation when PR metadata is unavailable or
force_use_commitsis enabled - Loads reviewer information and credits authors/reviewers while excluding configured bots
- Supports optional grouping by conventional commit type, with breaking changes rendered first when present
- Stores generated summaries in CSV or SQLite caches
- Supports interactive TOML-based setup and one-time migration from legacy
.envfiles - Can infer the previous tag from the GitHub compare URL in existing release notes when one is not supplied manually
- Language: Python 3.11+
- Key Libraries:
typer- CLI frameworkrich- terminal output and interactive promptsrequests- GitHub REST and GraphQL communicationtenacity- retry logic for LLM callspython-dotenv- legacy.envloading and migration supportany-llm-sdk[all]- provider-agnostic LLM access
- Optional Web Stack:
fastapi- REST APIuvicorn- ASGI serverpydantic- request/response models
- External APIs:
- GitHub REST API - repositories, PRs, commits, reviews, releases
- GitHub GraphQL API - issues closed by a PR
- LLM provider APIs via
any_llm
The project follows a lightweight Hexagonal Architecture / Ports and Adapters approach. The core release-note generation logic is UI-agnostic and reusable across CLI, library, and web modes.
┌───────────────────────────────────────────────────────┐
│ Adapters (External) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CLI │ │ Library │ │ Web │ │
│ │ Adapter │ │ API │ │ API │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼───────────────────┘
│ │ │
┌───────┼─────────────┼─────────────┼───────────────────┐
│ │ Core Domain │ │
│ ┌────▼─────────────▼─────────────▼─────┐ │
│ │ ProgressReporter Interface │ │
│ │ (event-based progress reporting) │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ ReleaseNotesConfig / LLMConfig │ │
│ │ typed configuration with validation │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ ReleaseNotesGenerator │ │
│ │ core business logic, UI-free │ │
│ └──────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
pretty_release_notes/ # Main package
├── __init__.py # Public package exports
├── __main__.py # `python -m pretty_release_notes`
├── main.py # Typer CLI entrypoint
├── setup_command.py # Interactive setup / migration helpers
├── api.py # Library client and builder
├── generator.py # Core release note generation workflow
├── github_client.py # GitHub API wrapper
├── openai_client.py # LLM adapter (legacy module name)
├── database.py # CSV / SQLite cache backends
├── ui.py # Rich-based CLI UI
├── prompt.txt # Packaged default prompt
├── py.typed # PEP 561 marker
├── core/
│ ├── __init__.py
│ ├── config.py # Typed configuration models
│ ├── config_loader.py # TOML, dict, and .env loaders
│ ├── execution.py # Parallel execution strategies
│ └── interfaces.py # Progress interfaces and events
├── adapters/
│ ├── __init__.py
│ └── cli_progress.py # CLI ProgressReporter adapter
├── web/
│ ├── __init__.py
│ ├── app.py # FastAPI app and background jobs
│ └── server.py # Uvicorn runner
└── models/
├── __init__.py
├── _utils.py # Conventional commit helpers
├── change.py # Change protocol
├── commit.py # Commit model
├── issue.py # Linked issue model
├── pull_request.py # Pull request model
├── release_notes.py # Release notes container / serializer
├── release_notes_line.py # Parsed line model
└── repository.py # Repository model
tests/ # Pytest suite
├── __init__.py
├── pull_request.py # PR fixtures
├── test_api.py
├── test_core.py
├── test_database_threading.py
├── test_execution.py
├── test_models_utils.py
├── test_openai_client.py
├── test_pull_request.py
├── test_release_notes.py
├── test_setup_command.py
└── test_web_api.py
docs/adr/ # Architecture Decision Records
├── 001-multi-mode-architecture.md
├── 002-user-directory-database-storage.md
└── 003-toml-configuration.md
examples/
└── library_usage.py
.github/workflows/ # CI workflows
├── lint.yml
└── tests.yml
CHANGELOG.md
CLAUDE.md
CONTRIBUTING.md
README.md
config.toml.example # Example user config
prompt.txt # Root prompt file for development/reference
pyproject.toml # Packaging, dependencies, tool config
.pre-commit-config.yaml # Pre-commit hooks
pretty_release_notes/core/interfaces.py - Progress reporting protocol:
ProgressEvent: Dataclass withtype,message, and optionalmetadataProgressReporter: Protocol used by the generator and adaptersNullProgressReporter: No-op reporter for silent operationCompositeProgressReporter: Broadcasts progress to multiple reporters
pretty_release_notes/core/config.py - Typed configuration:
GitHubConfig: GitHub token and optional default ownerLLMConfig: Provider API key, model name, and max patch sizeOpenAIConfig: Backward-compatible alias forLLMConfigDatabaseConfig: Cache backend type, name, and enabled stateFilterConfig: Excluded change types, labels, and authorsGroupingConfig: Grouping toggle, default type headings,other_heading, andbreaking_changes_headingReleaseNotesConfig: Main config object; acceptsllm=or legacyopenai=inputs and validates required config on creation
pretty_release_notes/core/config_loader.py - Configuration loaders:
ConfigLoader: Abstract base loaderTomlConfigLoader: Primary CLI loader; reads~/.pretty-release-notes/config.tomlby defaultTomlConfigLoadermerges[llm]with legacy[openai], with[llm]taking precedenceDictConfigLoader: Programmatic loader; supportsllm_*keys and legacyopenai_*fallbacksEnvConfigLoader: Legacy.envloader kept for backward compatibility and migration workflows
pretty_release_notes/core/execution.py - Execution strategies:
ExecutionStrategy: Abstract interface for parallel workThreadPoolStrategy: Default strategy used byReleaseNotesGeneratorThreadingStrategy: Older direct-threading implementation kept for compatibilitySequentialStrategy: Useful for debugging or constrained environments
pretty_release_notes/adapters/cli_progress.py - CLI adapter:
CLIProgressReportertranslatesProgressEventobjects intoCLIoutput calls- Keeps the generator independent from Rich/terminal concerns
ReleaseNotesClient - High-level library interface:
generate_release_notes(): Generate release notes for a repo and tagupdate_github_release(): Write generated notes back to GitHub- Accepts a
ReleaseNotesConfigplus an optionalProgressReporter
ReleaseNotesBuilder - Fluent builder for library usage:
with_github_token(): Set GitHub authenticationwith_llm(): Primary LLM configuration methodwith_openai(): Backward-compatible alias forwith_llm()with_database(): Configure CSV/SQLite cachingwith_filters(): Configure excluded types, labels, and authorswith_grouping(): Enable and customize grouped output headingswith_prompt_file(): Override the prompt template filewith_force_commits(): Force commit-based processing even when PR data existswith_progress_reporter(): Attach custom progress reportingbuild(): Construct a configuredReleaseNotesClient
pretty_release_notes/web/app.py - FastAPI API:
POST /generate: Start a background generation jobGET /jobs/{job_id}: Poll job status, progress, result, or errorGET /health: Health check endpointGenerateRequestusesllm_key/llm_modelas primary fields and still acceptsopenai_key/openai_model- Request payload also supports
exclude_types,exclude_labels, andexclude_authors WebProgressReporterstores structured progress events in memory- Job storage is in-process and in-memory; a persistent store such as Redis would be needed for production
pretty_release_notes/__init__.py - Public package API:
- Re-exports
ReleaseNotesBuilder,ReleaseNotesClient, config classes, and progress interfaces - Exports both
LLMConfigand the aliasOpenAIConfig
pretty_release_notes/__main__.py - Module entrypoint:
- Enables
python -m pretty_release_notes - Delegates to
main.app()
pretty_release_notes/main.py - CLI:
- Typer app with
generateandsetupcommands - Loads TOML config from
~/.pretty-release-notes/config.tomlby default - Allows CLI overrides for owner, prompt path, database toggle, commit fallback, grouping, and config path
- Uses
CLIProgressReporterto display progress and results - Prompts the user before writing updated release notes back to GitHub
pretty_release_notes/setup_command.py - Interactive setup:
- Creates or updates user config interactively
- Reads existing TOML values to prefill prompts
- Can import legacy values from a project
.env - Writes a
[llm]-based TOML config file - Contains helper functions for flattening TOML, migrating
.envvalues, and rendering TOML output
- Constructor accepts
ReleaseNotesConfig, optionalProgressReporter, and optionalExecutionStrategy - Initializes a
GitHubClientand normalizes commonly used config fields initialize_repository(): Loads repository metadata before generation/update operationsgenerate()workflow:- Loads the current GitHub release body
- Infers
previous_tag_namefrom the compare URL when needed - Tries to regenerate release notes via GitHub
- Falls back to the existing release body on 403/404 errors
- Parses lines into
ReleaseNotes - Downloads PR details in parallel
- Falls back to commit lines when forced or when PR parsing produced no usable changes
- Processes lines in parallel with cache lookup + LLM summarization
- Loads reviewers
- Serializes final markdown with filters, attribution, grouping, and LLM disclosure
update_on_github()updates the release body and gracefully skips 403 errors
- Wraps a
requests.Sessionwith retry-enabled HTTP adapter - Uses GitHub REST APIs for repositories, releases, PRs, commits, patches, reviews, and diffs
- Uses GitHub GraphQL to load issues closed by a PR
- Returns typed model objects such as
Repository,PullRequest,Commit, andIssue
Despite the legacy module name, this file is now a generic LLM adapter.
- Uses
any_llm/any-llm-sdk[all]rather than theopenaiSDK directly - Default model is
openai:o3 - Supports provider-qualified models such as
openai:gpt-5oranthropic:claude-sonnet-4-5 - Unqualified model names still default to OpenAI for backward compatibility
- Applies OpenAI
service_tier="flex"for supportedo3,o4-mini, and selectedgpt-5*models, andautootherwise format_model_name()produces user-facing model labels for generated notesget_chat_response()is wrapped in retry logic with random exponential backoff and up to six attempts
Database: Base interface for cached sentence storageCSVDatabase: CSV-backed cacheSQLiteDatabase: Thread-safe SQLite cache- Uses thread-local connections/cursors
- Protects write transactions with a lock
- Creates the table and index lazily
get_db(): Factory that resolves relative database names into~/.pretty-release-notes/- Absolute database paths are honored as-is
- Rich-based terminal UI used by the CLI adapter
- Displays markdown, release notes, errors, success messages, and update confirmations
Change protocol (pretty_release_notes/models/change.py):
- Shared contract for
PullRequestandCommit - Defines summary, author, reviewer, and prompt-generation behavior
PullRequest (pretty_release_notes/models/pull_request.py):
- Detects direct backports from title suffixes like
(backport #12345) - Detects revert PRs from PR body patterns such as
Reverts owner/repo#12345 - Extracts conventional commit type and breaking-change markers from title
- Builds prompts from the template, linked issues, and either the PR patch or commit messages
- Resolves reviewers, merged-by users, and backport/original PR relationships
Commit (pretty_release_notes/models/commit.py):
- Uses commit message as the source text for conventional commit / breaking-change parsing
- Builds prompts from the template plus commit diff
- Truncates very large diffs with a
[TRUNCATED]marker
ReleaseNotesLine (pretty_release_notes/models/release_notes_line.py):
- Parses PR URLs out of GitHub-generated note lines
- Preserves "new contributor" lines without sending them to the LLM
- Renders summarized changes as markdown bullets with GitHub links
ReleaseNotes (pretty_release_notes/models/release_notes.py):
- Stores parsed lines
- Loads PR reviewers concurrently
- Filters excluded change types, labels, and reverted PRs
- Supports grouped or flat serialization
- Emits author and reviewer sections
- Appends an AI disclosure section that includes the model name
Utilities (pretty_release_notes/models/_utils.py):
get_conventional_type(): Extracts conventional commit typeis_breaking_change(): Detects!-style breaking changes from commit/PR titles
The tool supports several configuration paths, with TOML as the primary CLI format.
Use the setup command to create or update the default config:
pretty-release-notes setupThis command:
- Walks through GitHub, LLM, database, filter, and grouping settings
- Reads existing TOML values when present and uses them as defaults
- Writes the config file to
~/.pretty-release-notes/config.toml - Can migrate legacy values from a project
.env
To seed the prompts from a legacy .env file:
pretty-release-notes setup --migrate-envPrimary config location:
~/.pretty-release-notes/config.toml
The canonical LLM section is [llm]. The legacy [openai] section is still accepted for backward compatibility.
# Optional top-level settings
# prompt_path = "/absolute/path/to/custom_prompt.txt"
# force_use_commits = false
[github]
token = "ghp_xxxxx"
owner = "frappe"
[llm]
api_key = "sk-xxxxx"
model = "openai:o3"
max_patch_size = 10000
[database]
type = "sqlite"
name = "stored_lines"
enabled = true
[filters]
exclude_change_types = ["chore", "refactor", "ci", "style", "test"]
exclude_change_labels = ["skip-release-notes"]
exclude_authors = [
"mergify[bot]",
"copilot-pull-request-reviewer[bot]",
"coderabbitai[bot]",
"dependabot[bot]",
"cursor[bot]",
]
[grouping]
group_by_type = false
# type_headings = { feat = "Features", fix = "Bug Fixes" }
# other_heading = "Other Changes"Notes:
- The CLI loads this file through
TomlConfigLoader - If
prompt_pathis omitted in TOML, the packaged default prompt is used - Relative database names are stored under
~/.pretty-release-notes/ - The example config is in
config.toml.example
from pretty_release_notes import GitHubConfig, LLMConfig, ReleaseNotesConfig
config = ReleaseNotesConfig(
github=GitHubConfig(token="ghp_xxxxx", owner="frappe"),
llm=LLMConfig(api_key="sk-xxxxx", model="openai:o3"),
)Backward compatibility:
OpenAIConfigis an alias forLLMConfigReleaseNotesConfig(openai=...)still works, butllm=is the primary API
from pretty_release_notes.core.config_loader import DictConfigLoader
loader = DictConfigLoader(
{
"github_token": "ghp_xxxxx",
"github_owner": "frappe",
"llm_api_key": "sk-xxxxx",
"llm_model": "openai:o3",
"exclude_types": ["chore", "ci"],
}
)
config = loader.load()Notes:
llm_api_keyandllm_modelare the primary keysopenai_api_keyandopenai_modelare still accepted as fallbacks
All configuration paths validate required fields and raise clear errors for missing/invalid values.
# Interactive setup
pretty-release-notes setup
# Generate release notes
pretty-release-notes generate [OPTIONS] REPO TAG
# Using python -m
python -m pretty_release_notes setup
python -m pretty_release_notes generate [OPTIONS] REPO TAG
# Examples
pretty-release-notes generate erpnext v15.38.4
pretty-release-notes generate --owner alyf-de banking v0.0.1
pretty-release-notes generate erpnext v15.38.4 --previous-tag v15.38.0
pretty-release-notes generate --config-path /path/to/config.toml erpnext v15.38.4
pretty-release-notes generate --group-by-type erpnext v15.38.4
pretty-release-notes generate --no-database erpnext v15.38.4CLI parameters:
repo: Repository nametag: Git tag for the release--owner: Override the configured repository owner--previous-tag: Specify the previous tag manually--database/--no-database: Enable or disable caching for this run--prompt-path: Use a custom prompt file--force-use-commits: Force commit-based generation--group-by-type: Enable grouped output--config-path: Use a non-default TOML config file
from pathlib import Path
from pretty_release_notes import ReleaseNotesBuilder
client = (
ReleaseNotesBuilder()
.with_github_token("ghp_xxxxx")
.with_llm("sk-xxxxx", model="openai:o3")
.with_database("sqlite", enabled=True)
.with_filters(
exclude_types={"chore", "ci", "refactor"},
exclude_labels={"skip-release-notes"},
)
.with_grouping(
group_by_type=True,
type_headings={"feat": "New Features", "fix": "Fixes"},
other_heading="Other",
)
.with_prompt_file(Path("prompt.txt"))
.build()
)
notes = client.generate_release_notes(
owner="frappe",
repo="erpnext",
tag="v15.38.4",
previous_tag_name="v15.38.0",
)
client.update_github_release("frappe", "erpnext", "v15.38.4", notes)Notes:
with_llm()is the primary builder APIwith_openai()remains available as an aliaswith_force_commits()andwith_progress_reporter()are also available
When grouping is enabled, output is serialized in a consistent order:
- Breaking Changes
- Features
- Bug Fixes
- Performance Improvements
- Documentation
- Code Refactoring
- Tests
- Build System
- CI/CD
- Chores
- Style
- Reverts
- Other Changes
- New Contributors
Only sections with matching content are emitted.
Install the web dependencies:
pip install -e .[web]Start the server:
python -m uvicorn pretty_release_notes.web.app:app --host 0.0.0.0 --port 8000
# Or use the wrapper module
python -m pretty_release_notes.web.serverCreate a generation job:
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{
"owner": "frappe",
"repo": "erpnext",
"tag": "v15.38.4",
"previous_tag_name": "v15.38.0",
"github_token": "ghp_xxx",
"llm_key": "sk-xxx",
"llm_model": "openai:o3",
"exclude_types": ["chore", "ci", "refactor"],
"exclude_labels": ["skip-release-notes"],
"exclude_authors": ["dependabot[bot]"]
}'Notes:
openai_keyandopenai_modelare still accepted as request aliases- The API serves interactive docs at
http://localhost:8000/docs
Check job status:
curl http://localhost:8000/jobs/{job_id}Health check:
curl http://localhost:8000/health# Install core package
pip install -e .
# Install web extras
pip install -e .[web]
# Install dev tools
pip install -e .[dev]Pre-commit is configured in .pre-commit-config.yaml.
# Install hooks
pre-commit install
# Run on all files
pre-commit run --all-filesConfigured hooks include:
- Standard repository checks (trailing whitespace, merge conflicts, JSON/TOML/YAML, debug statements)
- Ruff import sorting
- Ruff linting
- Ruff formatting
- Mypy
- Commitlint on
commit-msgusing the conventional commits preset
# Format code
ruff format .
# Check formatting
ruff format --check .
# Lint code
ruff check .
# Auto-fix lint issues
ruff check --fix .Ruff is used for formatting, import sorting, and linting.
# Check the whole project
mypy .
# Check a specific file
mypy pretty_release_notes/generator.py# Run the full test suite
pytest
# Run with coverage
pytest --cov=pretty_release_notes --cov-report=html --cov-report=term-missing
# Run a specific test file
pytest tests/test_web_api.pyThe test suite covers configuration loaders, core abstractions, builder/API behavior, setup migration, release note serialization, revert/backport logic, web endpoints, threading, and the LLM adapter.
Core generation logic is kept separate from CLI, library, and web adapters.
ProgressReporter implementations receive ProgressEvent objects from the generator, allowing different frontends to display progress differently.
TomlConfigLoader, DictConfigLoader, and EnvConfigLoader all produce the same ReleaseNotesConfig shape.
ReleaseNotesGenerator delegates parallel work to an ExecutionStrategy, making threading behavior swappable and testable.
The Change protocol lets PullRequest and Commit participate in the same summarization pipeline.
get_db() returns either a CSV or SQLite backend based on configuration.
Summaries are cached by get_summary_key(), allowing direct backports to reuse the original PR summary.
The code uses any_llm with provider-qualified model names while preserving OpenAI-oriented backward compatibility in config and request fields.
Commit/PR titles are parsed for conventional commit types and ! breaking-change markers, which drive filtering and grouped output.
Revert PRs are detected from PR body text. When both the original PR and its revert are in the same release, both are excluded from the final notes.
The generator can fall back from GitHub regeneration to existing notes, from PR patches to commit messages, and from PR-based notes to raw commits when necessary.
SQLite writes are synchronized with a lock while each thread gets its own connection/cursor pair.
LLM calls are retried with randomized exponential backoff up to six attempts.
Most configuration and model objects are dataclasses, which keeps construction, validation, and typing straightforward.
- Initialize repository metadata from GitHub.
- Load the current GitHub release.
- Infer the previous tag from the compare URL when it is not provided explicitly.
- Ask GitHub to regenerate release notes for the requested range.
- Fall back to the existing release body if regeneration is unavailable.
- Parse the release notes into structured lines.
- Download PR details in parallel for lines that reference PR URLs.
- Fall back to commit-based lines when forced or when GitHub-generated PR parsing did not produce usable changes.
- Build prompts, consult the cache, and summarize remaining changes with the configured LLM.
- Load reviewers.
- Serialize the final markdown with filters, attribution, grouping, and AI disclosure.
- Display the result and optionally write it back to GitHub.
This codebase combines:
- A reusable release-note generation core
- Multiple delivery modes (CLI, library, web)
- Typed config with backward-compatible migration paths
- Provider-agnostic LLM integration
- Cache-backed summarization
- Parallel GitHub data collection
- Filtering, grouping, backport reuse, and revert suppression
It is structured for extension: new loaders, adapters, execution strategies, or storage backends can be added without reworking the core generator.
Key architecture decisions are documented in ADRs:
- ADR 001: Multi-Mode Architecture with Hexagonal Design
- Documents the move from a CLI-only workflow to reusable core logic with adapters
- ADR 002: User Directory Database Storage
- Documents storing cache files in
~/.pretty-release-notes/instead of the project directory
- Documents storing cache files in
- ADR 003: TOML Configuration in User Home Directory
- Documents the move from project-local
.envfiles to~/.pretty-release-notes/config.toml
- Documents the move from project-local