Repository navigation
feat: add a changelog package that reads the git log #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tonyandrewmeyer
merged 31 commits into
canonical:main
from
tonyandrewmeyer:changelog-from-git-log
Sep 20, 2026
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
a231e35
feat: add a changelog package
claude 441b095
Apply suggestion from @tonyandrewmeyer
tonyandrewmeyer 1edb056
Apply suggestion from @tonyandrewmeyer
tonyandrewmeyer 9c9adb7
Apply suggestion from @tonyandrewmeyer
tonyandrewmeyer 7ddbee1
feat: infer the bump size and add a console script to changelog
claude f32ad50
feat: read the changelog off the git log, not off the release notes
claude 12850ef
Apply suggestion from @tonyandrewmeyer
tonyandrewmeyer 811c1d8
docs: split the subprocess call across lines, as ruff format wants
tonyandrewmeyer f3af1ac
docs: restore the deleted README section, and stop narrating this branch
tonyandrewmeyer 0b7beeb
refactor: make next_version's arguments keyword-only
tonyandrewmeyer f50aa7a
style: hold the package to the team's agreed ruff rule set
tonyandrewmeyer 5bbea9e
refactor!: read the git log and nothing else
tonyandrewmeyer b40d807
docs: tidy what removing the release-notes parser left in _constants
tonyandrewmeyer b762364
refactor: format_release_notes takes the compare URL, not the whole line
tonyandrewmeyer a34d387
docs: scopes are dropped by choice, not because nothing uses them
tonyandrewmeyer 4291824
docs: drop the release.py dependency inventory from pyproject
tonyandrewmeyer 8d48ada
build: adopt the team's ruff rule set at the root, not per package
tonyandrewmeyer 21131cf
docs: drop the README's last mention of the --input flag
tonyandrewmeyer b31dcd5
docs: don't point at a sibling package for how to run this one
tonyandrewmeyer bb4e2d2
docs: state the two things about the type map without the defensiveness
tonyandrewmeyer ffa7fd2
feat: surface unrecognised commit types, and drop the library API
tonyandrewmeyer 0f96d99
Apply batched suggestions from code review
tonyandrewmeyer c8f0f75
refactor: swap which output spells a pull request out in full
tonyandrewmeyer 7ec05f0
build: tell ruff the docstrings are Google-style
tonyandrewmeyer 46bd6bf
style: bring ai-failure-notifier up to the widened rule set
tonyandrewmeyer 1d227a2
docs: cut the _cli narration back to the rules it is keeping
tonyandrewmeyer c0624a5
test: name the fixture maintainers after Ubuntu releases
tonyandrewmeyer 9cdd3b7
docs: one sentence per rule in the README
tonyandrewmeyer bbad3ff
build: name the ignored ruff rules instead of coding them
tonyandrewmeyer 5925f9d
docs: link the pull request in CHANGES.md instead of printing its URL
tonyandrewmeyer 4dc0d1f
style: drop the inline suppressions the root ignores replace
tonyandrewmeyer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # changelog | ||
|
|
||
| Turns a range of commits into our changelog format. | ||
|
|
||
| The Charm Tech repositories have different release processes but aim for a consistent changelog style. The formatting and the version arithmetic are centralised here; the file rewriting and the GitHub calls stay in each repository. | ||
|
|
||
| ## What it produces | ||
|
|
||
| Four commits in a range, the first of them from a contributor outside the team: | ||
|
|
||
| ``` | ||
| fix: stop the framework mistaking two notices for twins (#2684) | ||
| docs: stop dressing cross-references up as quotations (#2666) | ||
| ci: crawl back to the upstream concierge presets (#2699) | ||
| chore: bump cryptography from 48.0.1 to 50.0.0 (#2682) | ||
| ``` | ||
|
|
||
| become one `CHANGES.md` entry: | ||
|
|
||
| ```markdown | ||
| # 3.8.2 - 31 August 2026 | ||
|
|
||
| ## Fixes | ||
|
|
||
| * Stop the framework mistaking two notices for twins by @ducky-debugger ([#2684](https://github.com/canonical/operator/pull/2684)) | ||
|
|
||
| ## Documentation | ||
|
|
||
| * Stop dressing cross-references up as quotations ([#2666](https://github.com/canonical/operator/pull/2666)) | ||
|
|
||
| ## CI | ||
|
|
||
| * Crawl back to the upstream concierge presets ([#2699](https://github.com/canonical/operator/pull/2699)) | ||
| ``` | ||
|
|
||
| Release notes are the same content with `###` headings and `in #2684` in place of the parenthesised link. The short form is GitHub's own: a release body renders `#2684` as a link to the pull request, where a `CHANGES.md` is read in an editor, on PyPI and in the docs as well, and only GitHub would make a link of it. | ||
|
|
||
| Most of the rules are visible there: | ||
|
|
||
| * **A contributor from outside the maintaining team is credited; a maintainer is not.** Pass the team to `--team`, comma-separated, as emails and/or handles; an empty team credits everyone, which is the safe way round, since over-crediting is visible in the draft release and crediting nobody is not. | ||
| * **A handle is only sometimes recoverable.** `46688206+ducky-debugger@users.noreply.github.com` gives `@ducky-debugger`, GitHub's default for an account with a private email; where the log has no handle, the person is credited by name. | ||
| * **`chore` is dropped on purpose.** Dependency bumps, charm pins and the release's own version bump are not what a reader came for, and `git log` still has them. | ||
| * **The headings, their order and the commit-type map are fixed.** The format is common across our repositories, so there is nothing for an adopting repository to supply. | ||
|
|
||
| And the rules it doesn't show: | ||
|
|
||
| * **A `!` means breaking.** The entry moves to a `Breaking Changes` section that renders first, keeping its real type as a prefix and carrying a sentence asking the reader to review carefully; it does not infer a *major* bump. | ||
| * **A revert of something in the same range cancels with it**, and neither appears; a revert of something already released goes under `Reverted`, or under `Breaking Changes` where it undoes a released `feat` or anything carrying a `!`. | ||
| * **Anything unplaceable is surfaced rather than dropped.** An unrecognised commit type, or a subject that is not conventional at all, lands under `Uncategorised` with the type kept, for a human to fix while reading the draft. | ||
| * **A commit with no `(#N)` in its subject** - one pushed straight to the branch - renders with no reference rather than a placeholder standing in for one. | ||
|
|
||
| ## Using it | ||
|
|
||
| Everything goes through the `changelog` console script. It reads a git log on stdin and prints one answer on stdout: | ||
|
|
||
| ```shell | ||
| git log --reverse --no-merges --format="$(changelog git-log-format)" "$LAST_TAG..$BRANCH" > log.txt | ||
| SIZE=$(changelog bump-size --team "$TEAM" < log.txt) | ||
| VERSION=$(changelog next-version --previous "$LAST_TAG" --team "$TEAM" < log.txt) | ||
| changelog release-notes --repo "$REPO" --team "$TEAM" \ | ||
| --compare-url "https://github.com/$REPO/compare/$LAST_TAG...$VERSION" < log.txt > release-notes.md | ||
| changelog changes-entry --repo "$REPO" --tag "$VERSION" --team "$TEAM" < log.txt > changes-entry.md | ||
| ``` | ||
|
|
||
| The two commands that print one word are for `$GITHUB_OUTPUT`; the two that print Markdown are for redirecting into a file, because a `$GITHUB_OUTPUT` line only takes a multi-line document through a heredoc delimiter the document must not itself contain. `git-log-format` prints the `--format` string the others expect, so the separators live in one place rather than in every workflow - copy them and it works until someone drops one, at which point the log stops parsing and the release goes out with an empty changelog rather than an error. | ||
|
|
||
| `--compare-url` is optional: a git log carries no compare link and the tags at either end are the workflow's to know. `--date` defaults to today in UTC, and the console script is the only part of the package that reads the clock. `changelog --help` has the rest. | ||
|
|
||
| From a workflow, pinned to a commit: | ||
|
|
||
| ```shell | ||
| uvx --from "git+https://github.com/canonical/charm-tech-code@<40-char-sha>#subdirectory=changelog" changelog bump-size < log.txt | ||
| ``` | ||
|
|
||
| ## Versions | ||
|
|
||
| `bump-size` says whether a range is a minor or a patch - a `feat` or a breaking change means minor, anything else means patch - and `next-version` applies that to a plain `X.Y.Z`. A major bump is never inferred, nor is a pre-release: `next-version` raises rather than guess at anything that isn't `X.Y.Z`, `3.9.0.dev0` included, since that is the guess the last post-release bump left in the version file rather than a version anyone shipped. | ||
|
|
||
| The repository decides what to count from, whether a `.dev0` goes on afterwards, and what any of it means for the other packages it ships. A `minor` on a branch where a feature has no business appearing, such as operator's `2.23-maintenance`, is an error rather than a patch - but which branches those are is the repository's to know, so that check belongs there. | ||
|
|
||
| ## Developing | ||
|
|
||
| ```shell | ||
| uv sync --group unit | ||
| uv run pytest | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| [project] | ||
| name = "charm-tech-code-changelog" | ||
| version = "0.1.0" | ||
| description = "Turn a range of commits into our changelog format." | ||
| readme = "README.md" | ||
| requires-python = ">=3.10" | ||
| authors = [ | ||
| {name = "The Charm Tech team at Canonical Ltd."}, | ||
| ] | ||
| license = "Apache-2.0" | ||
| # No runtime dependencies, and that is worth keeping. The package is pure | ||
| # text-to-text: it does not talk to GitHub, run git, or read the clock, so | ||
| # there is nothing for a dependency to do. | ||
| dependencies = [] | ||
|
|
||
| # The importable API is the package; this is the same thing wrapped for a | ||
| # workflow step, which cannot `import`. `argparse` is stdlib, so it stays a | ||
| # dependency-free package. | ||
| [project.scripts] | ||
| changelog = "charm_tech_code.changelog._cli:main" | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["src/charm_tech_code"] | ||
|
|
||
| [dependency-groups] | ||
| unit = ["pytest"] | ||
|
|
||
| [tool.pytest.ini_options] | ||
| testpaths = ["tests"] | ||
|
|
||
| # The real ruff configuration is at the root of the monorepo; extending it | ||
| # means a setting added here overrides one key rather than the whole config. | ||
| [tool.ruff] | ||
| extend = "../pyproject.toml" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Copyright 2026 Canonical Ltd. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
|
|
||
| """Turn a range of commits into our changelog format. | ||
|
|
||
| Text in, structured data and formatted strings out. Nothing here touches the | ||
| network, git, the filesystem or the clock: the `changelog` console script | ||
| (`_cli`) is the package's one I/O boundary, and is how a workflow step calls | ||
| this. See `_constants` for the format, and why `chore` commits do not appear | ||
| in a changelog. | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Copyright 2026 Canonical Ltd. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
|
|
||
| """Who gets credited in a changelog, and how. | ||
|
|
||
| **"Outside the team" is not "outside Canonical".** A contributor from | ||
| another Canonical team has an `@canonical.com` address, no GitHub handle | ||
| anyone can derive from it, and every bit as much claim to the credit as a | ||
| stranger does. They are credited by name. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Collection | ||
|
|
||
| from ._constants import NOREPLY_EMAIL_REGEX | ||
|
|
||
|
|
||
| def normalise_team(team: Collection[str]) -> frozenset[str]: | ||
| """Fold a caller's team list into something to compare against. | ||
|
|
||
| Entries may be email addresses or GitHub handles, with or without a | ||
| leading `@`, in any mixture: a caller assembling the list from a team | ||
| page has both to hand and should not have to decide which kind each one | ||
| is. Comparison is case-insensitive, since neither an email address nor a | ||
| GitHub handle distinguishes case. | ||
| """ | ||
| return frozenset(member.strip().lstrip('@').casefold() for member in team if member.strip()) | ||
|
|
||
|
|
||
| def derive_handle(email: str) -> str | None: | ||
| """Recover a GitHub handle from an author email, if it is in there. | ||
|
|
||
| Only a `users.noreply.github.com` address carries one. That is not the | ||
| narrow case it sounds like: it is what GitHub commits as by default when | ||
| an account keeps its email private, so it is the usual form for a | ||
| drive-by contributor, which is the author this most needs to name. | ||
|
|
||
| Returns: | ||
| The handle without its `@`, or `None` for any other address. | ||
| """ | ||
| match = NOREPLY_EMAIL_REGEX.match(email.strip()) | ||
| return match.group('handle') if match else None | ||
|
|
||
|
|
||
| def credit_for(name: str, email: str, team: Collection[str]) -> str | None: | ||
| """Work out how to credit the author of a commit, from a git log. | ||
|
|
||
| Args: | ||
| name: The author name, as `%an` gives it. | ||
| email: The author email, as `%ae` gives it. | ||
| team: The maintainers, as emails and/or handles. See `normalise_team`. | ||
| A parameter rather than a constant because it drifts and differs | ||
| per repository. An empty team credits everyone, which is the | ||
| right way for this to fail: over-crediting is visible in a draft | ||
| release and takes one edit, while crediting nobody is invisible | ||
| until a contributor notices. | ||
|
|
||
| Returns: | ||
| `@handle` when a handle can be recovered from the email, the name | ||
| when it cannot, or `None` when this author is one of `team` and so | ||
| is not a guest to be thanked. | ||
| """ | ||
| members = normalise_team(team) | ||
| handle = derive_handle(email) | ||
| if email.strip().casefold() in members: | ||
| return None | ||
| if handle is not None and handle.casefold() in members: | ||
| return None | ||
| return f'@{handle}' if handle is not None else name.strip() or None | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should just settle on one format for the team list, emails or Github users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's harder to match in that case. It's not about what the package gets passed, it's about what's available in the Git log.