patchnote 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchnote-0.1.1/.gitignore +43 -0
- patchnote-0.1.1/CHANGELOG.md +33 -0
- patchnote-0.1.1/LICENSE +21 -0
- patchnote-0.1.1/PKG-INFO +291 -0
- patchnote-0.1.1/README.md +249 -0
- patchnote-0.1.1/action.yml +123 -0
- patchnote-0.1.1/examples/config/patchnote.yaml +60 -0
- patchnote-0.1.1/examples/workflows/check-pr.yml +106 -0
- patchnote-0.1.1/examples/workflows/release-on-tag.yml +29 -0
- patchnote-0.1.1/examples/workflows/update-changelog.yml +40 -0
- patchnote-0.1.1/pyproject.toml +153 -0
- patchnote-0.1.1/src/patchnote/__init__.py +7 -0
- patchnote-0.1.1/src/patchnote/__main__.py +7 -0
- patchnote-0.1.1/src/patchnote/action_entry.py +148 -0
- patchnote-0.1.1/src/patchnote/ai/__init__.py +126 -0
- patchnote-0.1.1/src/patchnote/ai/cache.py +40 -0
- patchnote-0.1.1/src/patchnote/ai/client.py +89 -0
- patchnote-0.1.1/src/patchnote/ai/prompts.py +71 -0
- patchnote-0.1.1/src/patchnote/ai/validate.py +87 -0
- patchnote-0.1.1/src/patchnote/check.py +67 -0
- patchnote-0.1.1/src/patchnote/classify.py +353 -0
- patchnote-0.1.1/src/patchnote/cli.py +690 -0
- patchnote-0.1.1/src/patchnote/config.py +461 -0
- patchnote-0.1.1/src/patchnote/conventional.py +197 -0
- patchnote-0.1.1/src/patchnote/filters.py +199 -0
- patchnote-0.1.1/src/patchnote/github.py +388 -0
- patchnote-0.1.1/src/patchnote/gitlog.py +201 -0
- patchnote-0.1.1/src/patchnote/model.py +228 -0
- patchnote-0.1.1/src/patchnote/py.typed +0 -0
- patchnote-0.1.1/src/patchnote/render/__init__.py +17 -0
- patchnote-0.1.1/src/patchnote/render/changelog_file.py +138 -0
- patchnote-0.1.1/src/patchnote/render/github_release.py +36 -0
- patchnote-0.1.1/src/patchnote/render/json.py +12 -0
- patchnote-0.1.1/src/patchnote/render/markdown.py +185 -0
- patchnote-0.1.1/src/patchnote/render/templates/changelog.md.j2 +43 -0
- patchnote-0.1.1/tests/__init__.py +0 -0
- patchnote-0.1.1/tests/conftest.py +58 -0
- patchnote-0.1.1/tests/gitutil.py +54 -0
- patchnote-0.1.1/tests/golden/changelog.json +104 -0
- patchnote-0.1.1/tests/golden/conventional.md +13 -0
- patchnote-0.1.1/tests/golden/github-release.md +16 -0
- patchnote-0.1.1/tests/golden/keepachangelog.md +20 -0
- patchnote-0.1.1/tests/test_action.py +99 -0
- patchnote-0.1.1/tests/test_ai.py +240 -0
- patchnote-0.1.1/tests/test_changelog_file.py +96 -0
- patchnote-0.1.1/tests/test_check.py +82 -0
- patchnote-0.1.1/tests/test_classify.py +156 -0
- patchnote-0.1.1/tests/test_cli.py +127 -0
- patchnote-0.1.1/tests/test_cli_ai_and_release.py +167 -0
- patchnote-0.1.1/tests/test_config.py +74 -0
- patchnote-0.1.1/tests/test_conventional.py +129 -0
- patchnote-0.1.1/tests/test_extra.py +483 -0
- patchnote-0.1.1/tests/test_filters.py +135 -0
- patchnote-0.1.1/tests/test_github.py +166 -0
- patchnote-0.1.1/tests/test_gitlog.py +82 -0
- patchnote-0.1.1/tests/test_regressions.py +343 -0
- patchnote-0.1.1/tests/test_render.py +124 -0
- patchnote-0.1.1/tools/demo.py +217 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
.python-version
|
|
12
|
+
|
|
13
|
+
# Test / type / lint
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
htmlcov/
|
|
20
|
+
coverage.xml
|
|
21
|
+
|
|
22
|
+
# Patchnote
|
|
23
|
+
.patchnote-cache/
|
|
24
|
+
.cache/
|
|
25
|
+
|
|
26
|
+
# Editors / OS
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
|
|
33
|
+
# Secrets
|
|
34
|
+
.envrc
|
|
35
|
+
.direnv/
|
|
36
|
+
|
|
37
|
+
# Local review and generated demos
|
|
38
|
+
build-docs.txt
|
|
39
|
+
build-report.txt
|
|
40
|
+
PUBLISHING.md
|
|
41
|
+
VERIFICATION.md
|
|
42
|
+
demo-output/
|
|
43
|
+
.env.*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add CLI commands and the GitHub composite Action (Zahid Hasan; `202307f`)
|
|
15
|
+
- Optional LLM polish with validated, injectable-safe output (Zahid Hasan; `5025c5f`)
|
|
16
|
+
- Enrich commits from the GitHub REST and GraphQL APIs (Zahid Hasan; `f37853d`)
|
|
17
|
+
- Prepend releases into Keep a Changelog files (Zahid Hasan; `07cc14e`)
|
|
18
|
+
- Render markdown, JSON, and GitHub Release notes (Zahid Hasan; `696145a`)
|
|
19
|
+
- Classify commits into changelog sections (Zahid Hasan; `87af103`)
|
|
20
|
+
- Parse git history and Conventional Commits (Zahid Hasan; `5b56162`)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Add README, security policy, changelog, and CI workflows (Zahid Hasan; `fea0805`)
|
|
25
|
+
- Cover parsing, classification, rendering, AI, action, and CLI (Zahid Hasan; `ed852ad`)
|
|
26
|
+
- Scaffold package layout and tooling (Zahid Hasan; `0349eef`)
|
|
27
|
+
|
|
28
|
+
### Contributors
|
|
29
|
+
|
|
30
|
+
- Zahid Hasan
|
|
31
|
+
|
|
32
|
+
[Unreleased]: https://github.com/zahidhasann88/patchnote/compare/v0.1.0...HEAD
|
|
33
|
+
[0.1.0]: https://github.com/zahidhasann88/patchnote/releases/tag/v0.1.0
|
patchnote-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zahid Hasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
patchnote-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: patchnote
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Turn git commits and merged pull requests into clean changelogs and release notes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/zahidhasann88/patchnote
|
|
6
|
+
Project-URL: Documentation, https://github.com/zahidhasann88/patchnote#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/zahidhasann88/patchnote/issues
|
|
8
|
+
Project-URL: Source, https://github.com/zahidhasann88/patchnote
|
|
9
|
+
Project-URL: Changelog, https://github.com/zahidhasann88/patchnote/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Zahid Hasan <jahidhasann67@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: changelog,conventional-commits,git,github-actions,release-notes
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: jinja2>=3.1
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Requires-Dist: rich>=13.7
|
|
30
|
+
Requires-Dist: typer>=0.12
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
34
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
39
|
+
Requires-Dist: twine>=6; extra == 'dev'
|
|
40
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# Patchnote
|
|
44
|
+
|
|
45
|
+
**Turn git commits and merged pull requests into clean, human-readable changelogs and release notes.**
|
|
46
|
+
|
|
47
|
+
Patchnote is a Python CLI and GitHub Action. It groups history with Conventional Commits, PR labels, and conservative keyword heuristics, then writes [Keep a Changelog](https://keepachangelog.com/) (or conventional-changelog) markdown, JSON, or a GitHub Release body. An optional LLM step can polish wording. **The rule-based path is the product; AI is an enhancement, not a requirement.** AI is off by default. Generation reads local Git history; when a GitHub token is present, it also requests PR metadata.
|
|
48
|
+
|
|
49
|
+
## The problem
|
|
50
|
+
|
|
51
|
+
Release notes are either tedious (hand-written, then stale) or noisy (`git log --oneline` dumped into a GitHub Release). Tools that only understand labels miss local commits; tools that only understand Conventional Commits miss squash-merged PRs; tools that always call an LLM invent entries and leak private messages. Patchnote is the boring, inspectable middle: classify locally, enrich from GitHub when a token is present, rewrite with an LLM only if you ask.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
Python 3.10+ and a `git` binary on `PATH`. Until the first PyPI release, install from this checkout with `pip install .`.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# isolated CLI
|
|
59
|
+
pipx install patchnote
|
|
60
|
+
|
|
61
|
+
# or uv
|
|
62
|
+
uv tool install patchnote
|
|
63
|
+
|
|
64
|
+
# or pip
|
|
65
|
+
pip install patchnote
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
From a clone (this is also how the GitHub Action installs itself, before the first PyPI release):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## CLI quickstart
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd your-repo
|
|
78
|
+
patchnote init # commented patchnote.yaml
|
|
79
|
+
patchnote preview # HEAD since the latest tag, stdout only
|
|
80
|
+
patchnote generate --version 1.4.0 # markdown on stdout
|
|
81
|
+
patchnote generate --version 1.4.0 --output CHANGELOG.md --prepend
|
|
82
|
+
patchnote generate --format json
|
|
83
|
+
patchnote generate --format github-release
|
|
84
|
+
patchnote check --from v1.3.0 # CI linter for commit messages
|
|
85
|
+
patchnote release --version 1.4.0 --tag --push-release # draft GH release; never publishes without --publish
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Default range is *latest reachable tag matching `v*`* → `HEAD`. If there are no tags, Patchnote uses the full history. Empty ranges produce an empty section, not an error. With `--to vX.Y.Z`, the current tag is excluded from the default starting tag, so a tag-triggered release includes its changes.
|
|
89
|
+
|
|
90
|
+
Global flags: `--config`, `--repo`, `--no-color`, `--verbose`, `--version`. Place global flags before the command, for example `patchnote --repo path/to/repo generate`.
|
|
91
|
+
|
|
92
|
+
Exit codes: `0` success, `1` `check` found violations, `2` usage or config error, `3` runtime error (git or network).
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
## GitHub Action quickstart
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
# .github/workflows/release.yml
|
|
100
|
+
name: Release
|
|
101
|
+
on:
|
|
102
|
+
push:
|
|
103
|
+
tags: ["v*"]
|
|
104
|
+
permissions:
|
|
105
|
+
contents: write
|
|
106
|
+
jobs:
|
|
107
|
+
notes:
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@v4
|
|
111
|
+
with:
|
|
112
|
+
fetch-depth: 0
|
|
113
|
+
- uses: zahidhasann88/patchnote@v0.1.0
|
|
114
|
+
with:
|
|
115
|
+
version: ${{ github.ref_name }}
|
|
116
|
+
create-release: true
|
|
117
|
+
draft: true
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The action is **composite**: it sets up Python, `pip install`s Patchnote from the action's own checkout (so it works before the first PyPI release), and runs `python -m patchnote.action_entry`. Inputs are passed as environment variables, never interpolated into a shell command.
|
|
121
|
+
|
|
122
|
+
| Input | Default | Notes |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `from`, `to`, `version`, `style` | (latest tag / HEAD / unset / config) | Same meaning as the CLI |
|
|
125
|
+
| `ai` | `none` | `none` \| `polish` \| `summary` \| `both` |
|
|
126
|
+
| `ai-base-url`, `ai-model`, `ai-api-key` | (config / empty) | Key is a secret input, never echoed |
|
|
127
|
+
| `output-file`, `prepend` | empty / `false` | Prepend is idempotent per version |
|
|
128
|
+
| `create-release`, `draft`, `publish` | `false` / `true` / `false` | Publication requires `publish: true`, even if `draft: false` |
|
|
129
|
+
| `github-token` | `${{ github.token }}` | Used for PR enrichment and releases |
|
|
130
|
+
|
|
131
|
+
Outputs: `changelog`, `release-url`, `unreleased-entries`.
|
|
132
|
+
|
|
133
|
+
Ready-to-copy workflows live in [`examples/workflows/`](examples/workflows/):
|
|
134
|
+
|
|
135
|
+
- **Tag push** → GitHub Release with generated notes
|
|
136
|
+
- **Pull request** → `patchnote check` + preview comment (documents fork-PR limitations)
|
|
137
|
+
- **workflow_dispatch** → update `CHANGELOG.md` and open a PR
|
|
138
|
+
|
|
139
|
+
## Sample input, sample output
|
|
140
|
+
|
|
141
|
+
Given these commits since `v1.3.0`:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
feat(api)!: remove the v1 endpoint
|
|
145
|
+
|
|
146
|
+
BREAKING CHANGE: clients must call /v2
|
|
147
|
+
|
|
148
|
+
feat: add widget factory (#12)
|
|
149
|
+
fix: crash on empty input
|
|
150
|
+
fix the retry timer
|
|
151
|
+
chore: ignore me
|
|
152
|
+
chore(deps): bump requests from 2.31.0 to 2.32.0 (dependabot[bot])
|
|
153
|
+
Revert "feat: experimental flag"
|
|
154
|
+
This reverts commit abcdef1.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`patchnote generate --version 1.4.0` prints:
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
## [1.4.0] - 2026-10-10
|
|
161
|
+
|
|
162
|
+
### Breaking Changes
|
|
163
|
+
|
|
164
|
+
- **api:** Remove the v1 endpoint (`a1b2c3d`)
|
|
165
|
+
- **BREAKING CHANGE:** clients must call /v2
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
|
|
169
|
+
- Add widget factory ([#12](https://github.com/acme/widgets/pull/12); @ada; `d4e5f6a`)
|
|
170
|
+
|
|
171
|
+
### Fixed
|
|
172
|
+
|
|
173
|
+
- Crash on empty input (`aa11bb2`)
|
|
174
|
+
- Fix the retry timer (`cc22dd3`)
|
|
175
|
+
|
|
176
|
+
### Changed
|
|
177
|
+
|
|
178
|
+
- 2 dependency updates
|
|
179
|
+
|
|
180
|
+
[1.4.0]: https://github.com/acme/widgets/compare/v1.3.0...v1.4.0
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Chores are skipped, the revert cancels the original commit when both are in range, dependabot is collapsed, and the non-conventional "fix the retry timer" lands in **Fixed** marked as low-confidence internally (the marker is not shown in markdown).
|
|
184
|
+
|
|
185
|
+
## Classification rules
|
|
186
|
+
|
|
187
|
+
Precedence, first match wins:
|
|
188
|
+
|
|
189
|
+
1. **Breaking** — `!` after the type, a `BREAKING CHANGE:` footer, or a `breaking` / `breaking-change` label. Always rendered in a **Breaking Changes** section at the top, not duplicated elsewhere.
|
|
190
|
+
2. **PR labels** — mapped via `label_map` / `sections.*.labels` in config (e.g. `bug` → Fixed, `enhancement` → Added). Labels beat the conventional type so a `feat:` that was triaged as a bug still lands in Fixed.
|
|
191
|
+
3. **Conventional Commits** — `feat` `fix` `perf` `refactor` `docs` `test` `build` `ci` `chore` `revert`, with optional `(scope)` and `!`. Types are case-insensitive; a missing space after `:` is accepted.
|
|
192
|
+
4. **Keyword heuristics** — `fix`/`bug`/`hotfix`, `add`/`implement`, `remove`/`delete`, `deprecat`, `security`/`cve`/`xss`, `update`/`change`/`improve`. These are low-confidence.
|
|
193
|
+
5. **Other** — anything left. Patchnote does not guess wildly.
|
|
194
|
+
|
|
195
|
+
Filtering and cleanup:
|
|
196
|
+
|
|
197
|
+
- Skip `chore` / `ci` / `test` / `docs` / `build` / `style` by default (`--include-all` or config to keep them). A mapped PR label rescues a skipped type.
|
|
198
|
+
- Skip merge commits and `Merge branch` / `Merge pull request` subjects.
|
|
199
|
+
- Skip subjects matching `ignore_patterns` (defaults include `[skip changelog]`).
|
|
200
|
+
- Optionally skip bot authors, or collapse them into one "N dependency updates" line (default: collapse dependabot / renovate / `[bot]`).
|
|
201
|
+
- Dedupe commits that belong to the same PR (squash, merge, or `(#123)` in the subject).
|
|
202
|
+
- Collapse a revert with the commit it reverts when both are in range; a revert of something already released is kept.
|
|
203
|
+
|
|
204
|
+
Each entry can include a cleaned summary (sentence case, no trailing period by default), scope, PR number and link, author handle, and commit hashes. Contributors can be listed, with first-time contributors marked when GitHub data is available.
|
|
205
|
+
|
|
206
|
+
`--style conventional` groups as Features / Bug Fixes / Performance / … instead of Added / Fixed / ….
|
|
207
|
+
|
|
208
|
+
## GitHub enrichment
|
|
209
|
+
|
|
210
|
+
When `GITHUB_TOKEN` or `--token` is set and the remote (or `repository:` in config, or `GITHUB_REPOSITORY`) looks like GitHub, Patchnote resolves each commit to its merged pull request(s) via GraphQL (`associatedPullRequests`) with REST `/commits/{sha}/pulls` as fallback. Enrichment adds title, number, labels, author, and linked issues.
|
|
211
|
+
|
|
212
|
+
**If the API is missing, rate-limited, unauthorized, or on fire, Patchnote warns and continues with commits only.** Enrichment never fails the run.
|
|
213
|
+
|
|
214
|
+
## AI mode (optional)
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
export OPENAI_API_KEY=sk-... # or a local key; name is configurable
|
|
218
|
+
patchnote generate --ai polish --yes
|
|
219
|
+
patchnote generate --ai summary --yes
|
|
220
|
+
patchnote generate --ai both --yes \
|
|
221
|
+
--ai-include-bodies # opt-in; off by default
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Any OpenAI-compatible `/v1/chat/completions` endpoint works, including Ollama:
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
ai:
|
|
228
|
+
base_url: http://127.0.0.1:11434/v1
|
|
229
|
+
model: llama3.1
|
|
230
|
+
api_key_env: OPENAI_API_KEY # Ollama ignores the key; the env var may be unset
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The model **only rewrites entries that were already classified**. It is sent a JSON list and must return JSON with the same `id`s in the same order. Added, missing, reordered, or bullet-stuffed items are rejected; Patchnote falls back to the rule-based text and prints a warning. HTTP errors do the same.
|
|
234
|
+
|
|
235
|
+
`--ai polish` rewrites summaries for clarity and consistent tone. `--ai summary` adds a 2–3 sentence overview generated only from those entries.
|
|
236
|
+
|
|
237
|
+
Commit messages and PR titles are **untrusted**. They are placed in a delimited data block; PR bodies are not sent unless `--ai-include-bodies`. See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
238
|
+
|
|
239
|
+
Before the first call Patchnote prints the endpoint, model, and what will be sent, and waits, unless `--yes`. Non-interactive sessions without `--yes` refuse to call the LLM. Responses are cached by content hash under `.patchnote-cache/`; a matching validated response is reused. API keys are not included in prompts, caches, or generated notes. Reports still contain commit text and contributor details; review them before sharing.
|
|
240
|
+
|
|
241
|
+
## Configuration
|
|
242
|
+
|
|
243
|
+
`patchnote init` writes a commented `patchnote.yaml`. All keys are optional. See [`examples/config/patchnote.yaml`](examples/config/patchnote.yaml) and the comments in the generated file for the full reference.
|
|
244
|
+
|
|
245
|
+
A Jinja2 template can replace the markdown renderer (`template: path/to/file.j2`). The default template ships inside the package.
|
|
246
|
+
|
|
247
|
+
Config is validated with Pydantic v2; errors name the field and the problem. Custom templates are trusted code and resolve relative to the configuration file.
|
|
248
|
+
|
|
249
|
+
## Comparison
|
|
250
|
+
|
|
251
|
+
Related tools include [release-drafter](https://github.com/release-drafter/release-drafter),
|
|
252
|
+
[git-cliff](https://github.com/orhun/git-cliff), and
|
|
253
|
+
[conventional-changelog](https://github.com/conventional-changelog/conventional-changelog).
|
|
254
|
+
Patchnote focuses on local Git history, optional PR enrichment, a Python API, and optional
|
|
255
|
+
LLM rewriting with structural validation. Compare the tools against your release workflow;
|
|
256
|
+
Patchnote does not attempt to cover every template, integration, or ecosystem.
|
|
257
|
+
|
|
258
|
+
## Limitations
|
|
259
|
+
|
|
260
|
+
- GitLab / Gitea / Bitbucket remotes are parsed as "not GitHub"; enrichment is skipped. Local generation still works.
|
|
261
|
+
- Squash merges without `(#123)` in the subject need a token to attach the PR.
|
|
262
|
+
- Heuristics are English-centric.
|
|
263
|
+
- The LLM cannot be forced to be truthful; validation only stops it from adding or dropping entries.
|
|
264
|
+
- `patchnote release --push-tag` needs network access to `origin`; it will not force-push.
|
|
265
|
+
|
|
266
|
+
## Roadmap
|
|
267
|
+
|
|
268
|
+
- GitLab merge-request enrichment
|
|
269
|
+
- Built-in changelog fragments (`changes/*.md`) as an extra input
|
|
270
|
+
- `patchnote explain` — show why each entry landed in its section
|
|
271
|
+
- Action marketplace listing (needs a public repo and icon assets)
|
|
272
|
+
|
|
273
|
+
## Local verification
|
|
274
|
+
|
|
275
|
+
```powershell
|
|
276
|
+
python -m venv .venv
|
|
277
|
+
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
|
|
278
|
+
.\.venv\Scripts\python.exe tools/demo.py
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The demo creates a temporary Git repository, checks all formats and idempotent prepend,
|
|
282
|
+
and exercises valid and rejected responses from a local fake LLM. Its reports are saved
|
|
283
|
+
in `demo-output/`. No GitHub token or real LLM is used.
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Please follow the [Code of Conduct](CODE_OF_CONDUCT.md). Security reports: [SECURITY.md](SECURITY.md).
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
[MIT](LICENSE) © 2026 Zahid Hasan
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Patchnote
|
|
2
|
+
|
|
3
|
+
**Turn git commits and merged pull requests into clean, human-readable changelogs and release notes.**
|
|
4
|
+
|
|
5
|
+
Patchnote is a Python CLI and GitHub Action. It groups history with Conventional Commits, PR labels, and conservative keyword heuristics, then writes [Keep a Changelog](https://keepachangelog.com/) (or conventional-changelog) markdown, JSON, or a GitHub Release body. An optional LLM step can polish wording. **The rule-based path is the product; AI is an enhancement, not a requirement.** AI is off by default. Generation reads local Git history; when a GitHub token is present, it also requests PR metadata.
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
Release notes are either tedious (hand-written, then stale) or noisy (`git log --oneline` dumped into a GitHub Release). Tools that only understand labels miss local commits; tools that only understand Conventional Commits miss squash-merged PRs; tools that always call an LLM invent entries and leak private messages. Patchnote is the boring, inspectable middle: classify locally, enrich from GitHub when a token is present, rewrite with an LLM only if you ask.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Python 3.10+ and a `git` binary on `PATH`. Until the first PyPI release, install from this checkout with `pip install .`.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# isolated CLI
|
|
17
|
+
pipx install patchnote
|
|
18
|
+
|
|
19
|
+
# or uv
|
|
20
|
+
uv tool install patchnote
|
|
21
|
+
|
|
22
|
+
# or pip
|
|
23
|
+
pip install patchnote
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From a clone (this is also how the GitHub Action installs itself, before the first PyPI release):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CLI quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd your-repo
|
|
36
|
+
patchnote init # commented patchnote.yaml
|
|
37
|
+
patchnote preview # HEAD since the latest tag, stdout only
|
|
38
|
+
patchnote generate --version 1.4.0 # markdown on stdout
|
|
39
|
+
patchnote generate --version 1.4.0 --output CHANGELOG.md --prepend
|
|
40
|
+
patchnote generate --format json
|
|
41
|
+
patchnote generate --format github-release
|
|
42
|
+
patchnote check --from v1.3.0 # CI linter for commit messages
|
|
43
|
+
patchnote release --version 1.4.0 --tag --push-release # draft GH release; never publishes without --publish
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Default range is *latest reachable tag matching `v*`* → `HEAD`. If there are no tags, Patchnote uses the full history. Empty ranges produce an empty section, not an error. With `--to vX.Y.Z`, the current tag is excluded from the default starting tag, so a tag-triggered release includes its changes.
|
|
47
|
+
|
|
48
|
+
Global flags: `--config`, `--repo`, `--no-color`, `--verbose`, `--version`. Place global flags before the command, for example `patchnote --repo path/to/repo generate`.
|
|
49
|
+
|
|
50
|
+
Exit codes: `0` success, `1` `check` found violations, `2` usage or config error, `3` runtime error (git or network).
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
## GitHub Action quickstart
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
# .github/workflows/release.yml
|
|
58
|
+
name: Release
|
|
59
|
+
on:
|
|
60
|
+
push:
|
|
61
|
+
tags: ["v*"]
|
|
62
|
+
permissions:
|
|
63
|
+
contents: write
|
|
64
|
+
jobs:
|
|
65
|
+
notes:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
fetch-depth: 0
|
|
71
|
+
- uses: zahidhasann88/patchnote@v0.1.0
|
|
72
|
+
with:
|
|
73
|
+
version: ${{ github.ref_name }}
|
|
74
|
+
create-release: true
|
|
75
|
+
draft: true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The action is **composite**: it sets up Python, `pip install`s Patchnote from the action's own checkout (so it works before the first PyPI release), and runs `python -m patchnote.action_entry`. Inputs are passed as environment variables, never interpolated into a shell command.
|
|
79
|
+
|
|
80
|
+
| Input | Default | Notes |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `from`, `to`, `version`, `style` | (latest tag / HEAD / unset / config) | Same meaning as the CLI |
|
|
83
|
+
| `ai` | `none` | `none` \| `polish` \| `summary` \| `both` |
|
|
84
|
+
| `ai-base-url`, `ai-model`, `ai-api-key` | (config / empty) | Key is a secret input, never echoed |
|
|
85
|
+
| `output-file`, `prepend` | empty / `false` | Prepend is idempotent per version |
|
|
86
|
+
| `create-release`, `draft`, `publish` | `false` / `true` / `false` | Publication requires `publish: true`, even if `draft: false` |
|
|
87
|
+
| `github-token` | `${{ github.token }}` | Used for PR enrichment and releases |
|
|
88
|
+
|
|
89
|
+
Outputs: `changelog`, `release-url`, `unreleased-entries`.
|
|
90
|
+
|
|
91
|
+
Ready-to-copy workflows live in [`examples/workflows/`](examples/workflows/):
|
|
92
|
+
|
|
93
|
+
- **Tag push** → GitHub Release with generated notes
|
|
94
|
+
- **Pull request** → `patchnote check` + preview comment (documents fork-PR limitations)
|
|
95
|
+
- **workflow_dispatch** → update `CHANGELOG.md` and open a PR
|
|
96
|
+
|
|
97
|
+
## Sample input, sample output
|
|
98
|
+
|
|
99
|
+
Given these commits since `v1.3.0`:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
feat(api)!: remove the v1 endpoint
|
|
103
|
+
|
|
104
|
+
BREAKING CHANGE: clients must call /v2
|
|
105
|
+
|
|
106
|
+
feat: add widget factory (#12)
|
|
107
|
+
fix: crash on empty input
|
|
108
|
+
fix the retry timer
|
|
109
|
+
chore: ignore me
|
|
110
|
+
chore(deps): bump requests from 2.31.0 to 2.32.0 (dependabot[bot])
|
|
111
|
+
Revert "feat: experimental flag"
|
|
112
|
+
This reverts commit abcdef1.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`patchnote generate --version 1.4.0` prints:
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
## [1.4.0] - 2026-10-10
|
|
119
|
+
|
|
120
|
+
### Breaking Changes
|
|
121
|
+
|
|
122
|
+
- **api:** Remove the v1 endpoint (`a1b2c3d`)
|
|
123
|
+
- **BREAKING CHANGE:** clients must call /v2
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
|
|
127
|
+
- Add widget factory ([#12](https://github.com/acme/widgets/pull/12); @ada; `d4e5f6a`)
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
|
|
131
|
+
- Crash on empty input (`aa11bb2`)
|
|
132
|
+
- Fix the retry timer (`cc22dd3`)
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- 2 dependency updates
|
|
137
|
+
|
|
138
|
+
[1.4.0]: https://github.com/acme/widgets/compare/v1.3.0...v1.4.0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Chores are skipped, the revert cancels the original commit when both are in range, dependabot is collapsed, and the non-conventional "fix the retry timer" lands in **Fixed** marked as low-confidence internally (the marker is not shown in markdown).
|
|
142
|
+
|
|
143
|
+
## Classification rules
|
|
144
|
+
|
|
145
|
+
Precedence, first match wins:
|
|
146
|
+
|
|
147
|
+
1. **Breaking** — `!` after the type, a `BREAKING CHANGE:` footer, or a `breaking` / `breaking-change` label. Always rendered in a **Breaking Changes** section at the top, not duplicated elsewhere.
|
|
148
|
+
2. **PR labels** — mapped via `label_map` / `sections.*.labels` in config (e.g. `bug` → Fixed, `enhancement` → Added). Labels beat the conventional type so a `feat:` that was triaged as a bug still lands in Fixed.
|
|
149
|
+
3. **Conventional Commits** — `feat` `fix` `perf` `refactor` `docs` `test` `build` `ci` `chore` `revert`, with optional `(scope)` and `!`. Types are case-insensitive; a missing space after `:` is accepted.
|
|
150
|
+
4. **Keyword heuristics** — `fix`/`bug`/`hotfix`, `add`/`implement`, `remove`/`delete`, `deprecat`, `security`/`cve`/`xss`, `update`/`change`/`improve`. These are low-confidence.
|
|
151
|
+
5. **Other** — anything left. Patchnote does not guess wildly.
|
|
152
|
+
|
|
153
|
+
Filtering and cleanup:
|
|
154
|
+
|
|
155
|
+
- Skip `chore` / `ci` / `test` / `docs` / `build` / `style` by default (`--include-all` or config to keep them). A mapped PR label rescues a skipped type.
|
|
156
|
+
- Skip merge commits and `Merge branch` / `Merge pull request` subjects.
|
|
157
|
+
- Skip subjects matching `ignore_patterns` (defaults include `[skip changelog]`).
|
|
158
|
+
- Optionally skip bot authors, or collapse them into one "N dependency updates" line (default: collapse dependabot / renovate / `[bot]`).
|
|
159
|
+
- Dedupe commits that belong to the same PR (squash, merge, or `(#123)` in the subject).
|
|
160
|
+
- Collapse a revert with the commit it reverts when both are in range; a revert of something already released is kept.
|
|
161
|
+
|
|
162
|
+
Each entry can include a cleaned summary (sentence case, no trailing period by default), scope, PR number and link, author handle, and commit hashes. Contributors can be listed, with first-time contributors marked when GitHub data is available.
|
|
163
|
+
|
|
164
|
+
`--style conventional` groups as Features / Bug Fixes / Performance / … instead of Added / Fixed / ….
|
|
165
|
+
|
|
166
|
+
## GitHub enrichment
|
|
167
|
+
|
|
168
|
+
When `GITHUB_TOKEN` or `--token` is set and the remote (or `repository:` in config, or `GITHUB_REPOSITORY`) looks like GitHub, Patchnote resolves each commit to its merged pull request(s) via GraphQL (`associatedPullRequests`) with REST `/commits/{sha}/pulls` as fallback. Enrichment adds title, number, labels, author, and linked issues.
|
|
169
|
+
|
|
170
|
+
**If the API is missing, rate-limited, unauthorized, or on fire, Patchnote warns and continues with commits only.** Enrichment never fails the run.
|
|
171
|
+
|
|
172
|
+
## AI mode (optional)
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
export OPENAI_API_KEY=sk-... # or a local key; name is configurable
|
|
176
|
+
patchnote generate --ai polish --yes
|
|
177
|
+
patchnote generate --ai summary --yes
|
|
178
|
+
patchnote generate --ai both --yes \
|
|
179
|
+
--ai-include-bodies # opt-in; off by default
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Any OpenAI-compatible `/v1/chat/completions` endpoint works, including Ollama:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
ai:
|
|
186
|
+
base_url: http://127.0.0.1:11434/v1
|
|
187
|
+
model: llama3.1
|
|
188
|
+
api_key_env: OPENAI_API_KEY # Ollama ignores the key; the env var may be unset
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The model **only rewrites entries that were already classified**. It is sent a JSON list and must return JSON with the same `id`s in the same order. Added, missing, reordered, or bullet-stuffed items are rejected; Patchnote falls back to the rule-based text and prints a warning. HTTP errors do the same.
|
|
192
|
+
|
|
193
|
+
`--ai polish` rewrites summaries for clarity and consistent tone. `--ai summary` adds a 2–3 sentence overview generated only from those entries.
|
|
194
|
+
|
|
195
|
+
Commit messages and PR titles are **untrusted**. They are placed in a delimited data block; PR bodies are not sent unless `--ai-include-bodies`. See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
196
|
+
|
|
197
|
+
Before the first call Patchnote prints the endpoint, model, and what will be sent, and waits, unless `--yes`. Non-interactive sessions without `--yes` refuse to call the LLM. Responses are cached by content hash under `.patchnote-cache/`; a matching validated response is reused. API keys are not included in prompts, caches, or generated notes. Reports still contain commit text and contributor details; review them before sharing.
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
`patchnote init` writes a commented `patchnote.yaml`. All keys are optional. See [`examples/config/patchnote.yaml`](examples/config/patchnote.yaml) and the comments in the generated file for the full reference.
|
|
202
|
+
|
|
203
|
+
A Jinja2 template can replace the markdown renderer (`template: path/to/file.j2`). The default template ships inside the package.
|
|
204
|
+
|
|
205
|
+
Config is validated with Pydantic v2; errors name the field and the problem. Custom templates are trusted code and resolve relative to the configuration file.
|
|
206
|
+
|
|
207
|
+
## Comparison
|
|
208
|
+
|
|
209
|
+
Related tools include [release-drafter](https://github.com/release-drafter/release-drafter),
|
|
210
|
+
[git-cliff](https://github.com/orhun/git-cliff), and
|
|
211
|
+
[conventional-changelog](https://github.com/conventional-changelog/conventional-changelog).
|
|
212
|
+
Patchnote focuses on local Git history, optional PR enrichment, a Python API, and optional
|
|
213
|
+
LLM rewriting with structural validation. Compare the tools against your release workflow;
|
|
214
|
+
Patchnote does not attempt to cover every template, integration, or ecosystem.
|
|
215
|
+
|
|
216
|
+
## Limitations
|
|
217
|
+
|
|
218
|
+
- GitLab / Gitea / Bitbucket remotes are parsed as "not GitHub"; enrichment is skipped. Local generation still works.
|
|
219
|
+
- Squash merges without `(#123)` in the subject need a token to attach the PR.
|
|
220
|
+
- Heuristics are English-centric.
|
|
221
|
+
- The LLM cannot be forced to be truthful; validation only stops it from adding or dropping entries.
|
|
222
|
+
- `patchnote release --push-tag` needs network access to `origin`; it will not force-push.
|
|
223
|
+
|
|
224
|
+
## Roadmap
|
|
225
|
+
|
|
226
|
+
- GitLab merge-request enrichment
|
|
227
|
+
- Built-in changelog fragments (`changes/*.md`) as an extra input
|
|
228
|
+
- `patchnote explain` — show why each entry landed in its section
|
|
229
|
+
- Action marketplace listing (needs a public repo and icon assets)
|
|
230
|
+
|
|
231
|
+
## Local verification
|
|
232
|
+
|
|
233
|
+
```powershell
|
|
234
|
+
python -m venv .venv
|
|
235
|
+
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
|
|
236
|
+
.\.venv\Scripts\python.exe tools/demo.py
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The demo creates a temporary Git repository, checks all formats and idempotent prepend,
|
|
240
|
+
and exercises valid and rejected responses from a local fake LLM. Its reports are saved
|
|
241
|
+
in `demo-output/`. No GitHub token or real LLM is used.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
245
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Please follow the [Code of Conduct](CODE_OF_CONDUCT.md). Security reports: [SECURITY.md](SECURITY.md).
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
[MIT](LICENSE) © 2026 Zahid Hasan
|