Opinionated Commitizen release tooling for Python projects — works for single-package repos and monorepos with per-package changelogs, with optional Slack notifications on tag pushes.
The headline feature is the impacts_cz Commitizen plugin: in a monorepo,
only commits tagged with Impacts: <package> count toward a package's
version bump and changelog. In a single-package repo it collapses to plain
Conventional Commits.
Requires Python ≥ 3.11 and uv — rt release
runs uv sync / uv run cz bump, rt init writes [dependency-groups].dev
(PEP 735), and the generated GitHub workflow uses astral-sh/setup-uv. Other
package managers (Poetry, plain pip) are not supported out of the box.
Install the CLI once on your PATH:
uv tool install release-toolkitBootstrap a repo (the CLI also writes a matching GitHub workflow under
.github/workflows/ and adds release-toolkit to [dependency-groups].dev
so cz bump has the plugin available in CI):
# single-package repo
rt init single ./pyproject.toml
# monorepo: PATH NAME pairs
rt init monorepo \
packages/client/pyproject.toml client \
packages/service/pyproject.toml service
uv sync --group devCut a release from the package directory:
rt release # monorepo (uses the Impacts: filter)
rt release --no-filter # single-package report is a short alias for release-toolkit; both names work everywhere.
In a monorepo, declare which packages a commit affects via an Impacts:
footer:
feat: add streaming endpoint
Impacts: client, commons
- the footer is matched case-insensitively, on its own line
- tags split on commas/whitespace, matched with word boundaries
- a commit without
Impacts:is invisible to every package - if you want shared-code commits to bump everyone, add a tag like
commonsto every package'simpactslist - the footer name is configurable via
impacts_footer(e.g.Affects)
In a single-package repo, just write Conventional Commits — the plugin has
nothing to filter and behaves like cz_conventional_commits.
Vanilla cz bump reads every commit between the last matching tag and
HEAD when computing the next version. In a monorepo that is wrong — a
feat: for service would still bump client. Commitizen's
changelog_pattern only filters the rendered changelog, not the
increment.
impacts_cz rebuilds changelog_pattern from the package's impacts
list, and rt release applies the same filter when picking
MAJOR / MINOR / PATCH / NONE before invoking cz bump --increment.
Drives the full flow from one command, run from the package directory:
uv sync --group dev- refuses a dirty worktree
- fast-forwards
--master-branch(defaultmaster); aborts on other branches unless--forceis passed - computes the filtered increment, aborts if
NONE(skip with--no-filter) - shows
cz bump --dry-runand asks[y/N] - runs
cz bump, thengit push --follow-tags
Anything after -- is forwarded to cz bump
(e.g. rt release -- --prerelease beta). Exit 1 with a stderr ERROR: on
abort.
Inserts a default [tool.commitizen] section, appends release-toolkit to
[dependency-groups].dev (with a major-version cap derived from the
running CLI), and writes a caller workflow at
<repo-root>/.github/workflows/release[-<name>].yml.
PATH may be a pyproject.toml file or the directory containing one. Each
file is processed independently — warnings keep exit 0, hard errors
(missing file, TOML parse failure, no .git found) flip to exit 1.
If [tool.commitizen] is already present with name = "impacts_cz" or
release-toolkit is already in dev, the step is a no-op. If the section
exists with a different name, the file is left untouched with a warning.
Override the version source with --version-provider <name> (default
pep621); any built-in or third-party
provider
name is written verbatim.
rt increment [--config pyproject.toml]Prints MAJOR / MINOR / PATCH / NONE. Useful for wiring into other
release pipelines: capture stdout, feed to cz bump --increment.
init single writes:
[tool.commitizen]
name = "impacts_cz"
version_provider = "pep621"
tag_format = "v$version"
annotated_tag = true
changelog_file = "CHANGELOG.md"
update_changelog_on_bump = true
changelog_merge_prerelease = trueinit monorepo adds per-package fields:
tag_format = "client-v$version"
bump_message = "bump: client $current_version -> $new_version"
impacts = ["client"]Tune by hand afterwards if needed:
- add a shared tag (e.g.
commons) toimpactsif shared-code commits should bump this package - override the footer name:
impacts_footer = "Affects"
Reusable workflow. Triggered on a release tag push; generates notes from
cz changelog, creates a GitHub Release, optionally posts to Slack. The
caller is generated by rt init; the example below is for reference:
name: CD Client
on:
push:
tags: ['client-v*']
permissions:
contents: read
jobs:
release:
uses: your-org/release-toolkit/.github/workflows/make-github-release.yml@v1
with:
package_dir: pylon_client
tag_prefix: client-v
python_version: '3.11'
secrets:
SLACK_NOTIFICATION_WEBHOOK_URL: ${{ secrets.SLACK_NOTIFICATION_WEBHOOK_URL }} # omit to disable
permissions:
contents: write| input | required | default | meaning |
|---|---|---|---|
package_dir |
yes | — | path to the package's pyproject.toml |
tag_prefix |
yes | — | tag prefix preceding the version (e.g. client-v, v) |
python_version |
no | 3.11 |
Python used by cz changelog |
slack_message_prefix |
no | Released |
prefix for the Slack message |
Slack is fully optional: provide SLACK_NOTIFICATION_WEBHOOK_URL to enable, omit to
disable. No extra plugin to install.
Composite action that installs Python, uv, and nox at pinned versions.
Inputs: python-version (required), uv-version, nox-version.
examples/monorepo/—pyproject.tomland caller workflow for a monorepo package usinghatch-vcs.examples/single-package/— same idea, noimpacts.
Copy what you need; they are intentionally minimal.
uv sync --group dev
uv run pytest
uv run ruff check .
uv run pyrightLayout:
src/release_toolkit/
cz_plugin.py # ImpactsCz, build_changelog_pattern
helpers.py # find_filtered_increment, load_config
installer.py # CommitizenConfig, install_into_document
workflow_installer.py # WorkflowConfig, render_workflow
cli.py # argparse + I/O (entry point)
release_runner.py # run_release (subprocess orchestration)
github/
actions/setup-python-env/
workflows/make-github-release.yml
The plugin is registered through the commitizen.plugin entry point in
pyproject.toml; do not import it from release_toolkit/__init__.py —
that would re-enter the package during Commitizen's entry-point discovery
and deadlock.
rt increment prints NONE but I just merged something. No commits
match the package's Impacts: filter since the last package tag. Either
the commit is missing the footer, or it lists tags this package does not
subscribe to.
cz bump wants to bump but increment says NONE. That is the bug
this toolkit fixes — vanilla cz bump ignores changelog_pattern for
increment computation. Use rt release (or feed --increment to cz bump
yourself) so the filter is honored.
Is the plugin compatible with bump_pattern / breaking-change detection?
Yes — ImpactsCz is a thin subclass of ConventionalCommitsCz that only
rewrites changelog_pattern. Everything else is inherited as-is.