Managing CLI Versioning & Changelogs
Introduction to CLI Release Strategy
Establishing a predictable release cadence is critical for maintaining trust in internal tooling and public CLIs. Before implementing versioning mechanics, ensure your foundational Project Setup & Dependency Management workflow is standardized across your team. This guide outlines how to enforce semantic versioning, automate changelog generation, and integrate release pipelines without disrupting developer velocity.
- Define clear versioning policies for breaking, feature, and patch releases.
- Align release cadence with CI/CD pipeline capabilities.
- Standardize commit conventions to enable automated changelog parsing.
Semantic Versioning & Package Metadata Sync
Python CLIs rely on pyproject.toml for authoritative version tracking. Modern toolchains streamline metadata synchronization across isolated environments.
When utilizing fast dependency resolvers like uv for Python CLI Dependency Management, you can script version bumps that automatically update lockfiles and trigger downstream CI pipelines.
For teams preferring declarative workflows, Poetry Workflows for CLI Development offer built-in version commands that seamlessly sync package metadata with CLI entry points.
# pyproject.toml
[project]
name = "my-cli-tool"
version = "1.2.0"
requires-python = ">=3.10"
dynamic = ["dependencies"]
[project.scripts]
my-cli = "my_cli_tool.cli:main"
Centralize version strings in pyproject.toml to prevent drift. Use dynamic versioning via importlib.metadata for runtime accuracy. This approach guarantees python cli semantic versioning compliance across all execution contexts.
- Adopt SemVer (MAJOR.MINOR.PATCH) for predictable dependency resolution.
- Centralize version strings in
pyproject.tomlto prevent drift. - Use dynamic versioning via
importlib.metadatafor runtime accuracy.
Automated Changelog Generation
Manual changelog maintenance introduces documentation drift and release bottlenecks. Implement Conventional Commits paired with tools like towncrier, commitizen, or git-cliff. Configure pre-commit hooks to enforce commit message standards. This automation guarantees that your CHANGELOG.md reflects actual code changes rather than developer memory.
# towncrier.toml
[tool.towncrier]
package = "my_cli_tool"
filename = "CHANGELOG.md"
directory = "changelog.d"
title_format = "## {version} ({project_date})"
issue_format = "[#{issue}](https://github.com/owner/repo/issues/{issue})"
[[tool.towncrier.type]]
directory = "feature"
name = "Features"
showcontent = true
[[tool.towncrier.type]]
directory = "fix"
name = "Bug Fixes"
showcontent = true
Execute automated changelog generation python workflows via terminal commands. Run towncrier build --version 1.3.0 to compile fragments into the final document. Commit the generated file alongside a git tag.
- Enforce Conventional Commits via pre-commit validation.
- Generate
CHANGELOG.mdfrom git history usingtowncrierorgit-cliff. - Map commit types to changelog categories (Features, Fixes, Breaking Changes).
CI/CD Integration & Release Automation
Integrate version bumping and changelog generation into GitHub Actions or GitLab CI. Use conditional triggers on main branch merges or explicit tag pushes. The pipeline should validate tests via pytest, generate the changelog, bump the version in pyproject.toml, publish to PyPI, and create a GitHub Release. Isolate these steps to prevent failed deployments from corrupting version history.
# .github/workflows/release.yml
name: Release Pipeline
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install uv && uv pip install -e ".[dev]"
- run: pytest tests/ --cov=my_cli_tool
- run: uv pip install build twine
- run: python -m build
- run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
Automate version bumps using bump2version or semantic-release. Gate releases on passing pytest suites and linting checks. Publish to PyPI and create GitHub Releases in a single atomic workflow.
- Automate version bumps using
bump2versionorsemantic-release. - Gate releases on passing
pytestsuites and linting checks. - Publish to PyPI and create GitHub Releases in a single atomic workflow.
Runtime Version Exposure & Update Checks
Expose the current version via --version and --help flags using Typer or Click. Dynamically read the version from importlib.metadata.version() to avoid hardcoding values in source files. Implement an optional --check-updates flag that queries PyPI or an internal registry. This provides users with actionable upgrade paths and deprecation warnings.
# src/my_cli_tool/cli.py
import sys
from importlib.metadata import version, PackageNotFoundError
import typer
import httpx
app = typer.Typer()
def get_version() -> str:
try:
return version("my-cli-tool")
except PackageNotFoundError:
return "0.0.0-dev"
@app.command()
def main(
version_flag: bool = typer.Option(False, "--version", "-v", help="Show version"),
check_updates: bool = typer.Option(False, "--check-updates", help="Check for newer releases"),
) -> None:
if version_flag:
print(f"my-cli-tool {get_version()}")
raise typer.Exit()
if check_updates:
latest = check_pypi_latest()
current = get_version()
if latest != current:
typer.echo(f"Update available: {current} -> {latest}")
else:
typer.echo("You are running the latest version.")
raise typer.Exit()
def check_pypi_latest() -> str:
url = f"https://pypi.org/pypi/my-cli-tool/json"
with httpx.Client() as client:
response = client.get(url, timeout=5.0)
response.raise_for_status()
return response.json()["info"]["version"]
if __name__ == "__main__":
app()
Use importlib.metadata.version() for runtime version resolution. Implement --version flags in Typer/Click entry points. Add optional update-checking logic with configurable registry endpoints.
- Use
importlib.metadata.version()for runtime version resolution. - Implement
--versionflags in Typer/Click entry points. - Add optional update-checking logic with configurable registry endpoints.