Project Setup

Managing CLI Versioning & Changelogs

Build a dependable foundation with packaging, dependency resolution, versioning, virtual environments, and release workflows for Python CLI projects.

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.toml to prevent drift.
  • Use dynamic versioning via importlib.metadata for 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.md from git history using towncrier or git-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 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.

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 --version flags in Typer/Click entry points.
  • Add optional update-checking logic with configurable registry endpoints.