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.
Files changed (58) hide show
  1. patchnote-0.1.1/.gitignore +43 -0
  2. patchnote-0.1.1/CHANGELOG.md +33 -0
  3. patchnote-0.1.1/LICENSE +21 -0
  4. patchnote-0.1.1/PKG-INFO +291 -0
  5. patchnote-0.1.1/README.md +249 -0
  6. patchnote-0.1.1/action.yml +123 -0
  7. patchnote-0.1.1/examples/config/patchnote.yaml +60 -0
  8. patchnote-0.1.1/examples/workflows/check-pr.yml +106 -0
  9. patchnote-0.1.1/examples/workflows/release-on-tag.yml +29 -0
  10. patchnote-0.1.1/examples/workflows/update-changelog.yml +40 -0
  11. patchnote-0.1.1/pyproject.toml +153 -0
  12. patchnote-0.1.1/src/patchnote/__init__.py +7 -0
  13. patchnote-0.1.1/src/patchnote/__main__.py +7 -0
  14. patchnote-0.1.1/src/patchnote/action_entry.py +148 -0
  15. patchnote-0.1.1/src/patchnote/ai/__init__.py +126 -0
  16. patchnote-0.1.1/src/patchnote/ai/cache.py +40 -0
  17. patchnote-0.1.1/src/patchnote/ai/client.py +89 -0
  18. patchnote-0.1.1/src/patchnote/ai/prompts.py +71 -0
  19. patchnote-0.1.1/src/patchnote/ai/validate.py +87 -0
  20. patchnote-0.1.1/src/patchnote/check.py +67 -0
  21. patchnote-0.1.1/src/patchnote/classify.py +353 -0
  22. patchnote-0.1.1/src/patchnote/cli.py +690 -0
  23. patchnote-0.1.1/src/patchnote/config.py +461 -0
  24. patchnote-0.1.1/src/patchnote/conventional.py +197 -0
  25. patchnote-0.1.1/src/patchnote/filters.py +199 -0
  26. patchnote-0.1.1/src/patchnote/github.py +388 -0
  27. patchnote-0.1.1/src/patchnote/gitlog.py +201 -0
  28. patchnote-0.1.1/src/patchnote/model.py +228 -0
  29. patchnote-0.1.1/src/patchnote/py.typed +0 -0
  30. patchnote-0.1.1/src/patchnote/render/__init__.py +17 -0
  31. patchnote-0.1.1/src/patchnote/render/changelog_file.py +138 -0
  32. patchnote-0.1.1/src/patchnote/render/github_release.py +36 -0
  33. patchnote-0.1.1/src/patchnote/render/json.py +12 -0
  34. patchnote-0.1.1/src/patchnote/render/markdown.py +185 -0
  35. patchnote-0.1.1/src/patchnote/render/templates/changelog.md.j2 +43 -0
  36. patchnote-0.1.1/tests/__init__.py +0 -0
  37. patchnote-0.1.1/tests/conftest.py +58 -0
  38. patchnote-0.1.1/tests/gitutil.py +54 -0
  39. patchnote-0.1.1/tests/golden/changelog.json +104 -0
  40. patchnote-0.1.1/tests/golden/conventional.md +13 -0
  41. patchnote-0.1.1/tests/golden/github-release.md +16 -0
  42. patchnote-0.1.1/tests/golden/keepachangelog.md +20 -0
  43. patchnote-0.1.1/tests/test_action.py +99 -0
  44. patchnote-0.1.1/tests/test_ai.py +240 -0
  45. patchnote-0.1.1/tests/test_changelog_file.py +96 -0
  46. patchnote-0.1.1/tests/test_check.py +82 -0
  47. patchnote-0.1.1/tests/test_classify.py +156 -0
  48. patchnote-0.1.1/tests/test_cli.py +127 -0
  49. patchnote-0.1.1/tests/test_cli_ai_and_release.py +167 -0
  50. patchnote-0.1.1/tests/test_config.py +74 -0
  51. patchnote-0.1.1/tests/test_conventional.py +129 -0
  52. patchnote-0.1.1/tests/test_extra.py +483 -0
  53. patchnote-0.1.1/tests/test_filters.py +135 -0
  54. patchnote-0.1.1/tests/test_github.py +166 -0
  55. patchnote-0.1.1/tests/test_gitlog.py +82 -0
  56. patchnote-0.1.1/tests/test_regressions.py +343 -0
  57. patchnote-0.1.1/tests/test_render.py +124 -0
  58. 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
@@ -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.
@@ -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
+ ![Patchnote generating release notes from local Git history](docs/screenshots/demo.png)
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
+ ![Patchnote generating release notes from local Git history](docs/screenshots/demo.png)
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