The tenzir-changelog harness curates
changelog entries, ships release notes, and assembles the public changelog
across Tenzir repositories. Use this page as a reference for concepts,
configuration, and CLI details. For step-by-step walkthroughs, see the guide for
maintaining a changelog.
Install
Section titled “Install”tenzir-changelog ships as a Python package that requires Python 3.12 or later.
Install it with uv (or pip) and verify the
console script:
uv add tenzir-changeloguvx tenzir-changelog --helpPython API
Section titled “Python API”Drive the CLI flows from another Python process by importing the
Changelog helper:
from pathlib import Pathfrom tenzir_changelog import Changelog
client = Changelog(root=Path("changelog"))client.add( title="API entry", entry_type="feature", description="Body text", co_authors=["claude"],)client.show(identifiers=["unreleased"], view="markdown", explicit_links=True)client.release_notes("v1.0.0", explicit_links=True)
# Get the latest versionversion = client.release_version() # Returns "v1.0.0"bare_version = client.release_version(bare=True) # Returns "1.0.0"
# Publish defaults to latest releaseclient.release_publish(create_tag=True, assume_yes=True)For advanced scenarios, reuse tenzir_changelog.create_cli_context and call
the helper functions from tenzir_changelog.cli directly.
Core concepts
Section titled “Core concepts”- Project – Identifies which documentation stream the changelog belongs to. Every entry and release references the same project string.
- Entry – A changelog consists of entries. Each entry uses one of four
hard-coded types:
breaking,feature,bugfix, orchange. - Unreleased bucket – Pending entries live in
unreleased/until you move them to a release. - Release – Versioned milestone under
releases/<version>/containingmanifest.yaml,notes.md, and archived entry files inentries/. - Configuration file – Settings live in
config.yamlby default, or you can store them inpackage.yamlalongside thechangelog/directory.config.yamltakes precedence when both exist. - Export style – Controls whether release notes use a detailed card layout
or a compact bullet-list layout. Set
export_style: compactin configuration to prefer the bullet-list format without passing--compacteach time. - Explicit links – Controls whether @mentions and #PR references render as
explicit Markdown links. Set
explicit_links: trueto produce portable output for documentation sites or Markdown renderers that don’t auto-link GitHub references. CLI flags--explicit-links/--no-explicit-linksoverride this setting. - Components – You may constrain the optional
componentfield on entries by declaring allowed labels in configuration. Use a dict mapping component names to descriptions. Component filters apply to the CLI table, exports, and validation. - Modules – Independent changelog projects discovered via a glob pattern. Each module has its own configuration and versioning. The parent project acts as a workspace that provides discovery, aggregated views, and typically handles distribution of the bundled modules.
A typical project layout looks like this:
Directoryproject-root/
Directorychangelog/
- config.yaml
Directoryunreleased/
- add-feature.md
- fix-bug.md
Directoryreleases/
Directoryv1.0.0/
- manifest.yaml
- notes.md
Directoryentries/
- add-feature.md
- fix-bug.md
For a package layout (with package.yaml), the
structure may look like:
Directorymy-package/
- package.yaml
Directorychangelog/
Directoryunreleased/
- …
Directoryreleases/
- …
Commands
Section titled “Commands”Run changelog commands from the project root or the changelog/ directory:
uvx tenzir-changelog [command] [options]All commands accept --config to point at an explicit configuration file (YAML
format, defaulting to config.yaml) and --root to operate on another
repository. When you omit both options, the CLI looks for config.yaml inside
the changelog root or, failing that, for a package.yaml one directory above
the changelog folder. The CLI also automatically uses a changelog/
subdirectory as the project root when running from a parent directory that
contains one. This package mode lets you run commands from either the package
root or the changelog/ directory without repeating --root.
Display changelog entries in multiple views.
tenzir-changelog show [identifiers...] [options]| Option | Description |
|---|---|
identifiers | Row numbers, entry IDs, release versions, or - (optional) |
-c/--card | Show detailed cards for matching entries |
-m/--markdown | Export as Markdown |
-j/--json | Export as JSON |
--compact | Use compact bullet-list layout |
--no-compact | Use detailed card layout |
--no-emoji | Remove type emoji from output |
--explicit-links | Render @mentions and PR refs as explicit Markdown links |
--project <id> | Filter to specific project (repeatable) |
--component <label> | Filter to specific component (repeatable) |
--banner | Display project banner above table output |
The table view (default) lists entries with ID, title, type, project, PRs, and
authors. Row numbers count backward from the newest entry, so #1 always
targets the latest change.
Create a new changelog entry in unreleased/.
tenzir-changelog add [options]| Option | Description |
|---|---|
--title <text> | Entry title |
--type <type> | breaking, feature, bugfix, or change |
--description <text> | Entry body |
--author <name> | Contributor name (repeatable) |
--co-author <name> | Additional contributor (repeatable) |
--pr <number> | Pull request number (repeatable) |
--component <label> | Component label (repeatable) |
--web | Open prefilled GitHub file creation URL |
The command prompts for any information you do not pass explicitly. The first
invocation scaffolds the project automatically, creating a changelog/
subdirectory with config.yaml and unreleased/. When you provide an explicit
--root flag, the CLI uses that directory directly instead of creating a
subdirectory. The CLI names entry files using the slugified title (e.g.,
my-feature.md).
By default, the CLI infers the primary author from environment variables
(TENZIR_CHANGELOG_AUTHOR, GH_USERNAME) or the GitHub CLI (gh api user).
Using --author overrides this inference entirely. The --co-author option
adds to the inferred or explicit author list without replacing it, making it
ideal for AI-assisted development, pair programming, or collaborative
contributions. Duplicates are removed automatically while preserving order.
release create
Section titled “release create”Stage a release under releases/<version>/.
tenzir-changelog release create [version] [options]| Option | Description |
|---|---|
version | Release version (e.g., v1.0.0) |
--patch | Bump patch version from latest release |
--minor | Bump minor version from latest release |
--major | Bump major version from latest release |
--yes | Commit changes (default is dry run) |
--title <text> | Custom title for release heading |
--intro <text> | Inline intro text (mutually exclusive with --intro-file) |
--intro-file <path> | Path to intro file (mutually exclusive with --intro) |
--compact | Use bullet-list layout for notes.md |
--explicit-links | Render @mentions and PR refs as explicit Markdown links |
--date <YYYY-MM-DD> | Override release date |
The command renders notes.md, updates manifest.yaml, and moves entry files
into entries/. It performs a dry run by default. When the release already
exists, the CLI appends additional unreleased entries.
release version
Section titled “release version”Print the latest released version.
tenzir-changelog release version [options]| Option | Description |
|---|---|
--bare | Print version without v prefix |
Use this command in scripts to query the current version without parsing output from other commands:
# Get the latest versiontenzir-changelog release version# Output: v1.2.0
# Get version without prefix (for semver tools)tenzir-changelog release version --bare# Output: 1.2.0
# Use in commit messagesgit commit -m "Release $(tenzir-changelog release version)"release notes
Section titled “release notes”Re-export release notes without modifying files.
tenzir-changelog release notes <identifier> [options]| Option | Description |
|---|---|
identifier | Release version or - for unreleased |
-m | Export as Markdown (default) |
-j | Export as JSON |
--compact | Use bullet-list layout |
--no-emoji | Drop type icons |
--explicit-links | Render @mentions and PR refs as explicit Markdown links |
Use --explicit-links when rendering Markdown outside of GitHub. By default,
the output uses @username and #123 references that GitHub auto-links. With
--explicit-links, these become explicit Markdown links like
[@username](https://github.com/username) and
[#123](https://github.com/owner/repo/pull/123), making the output portable to
documentation sites, blogs, or other Markdown renderers.
release publish
Section titled “release publish”Publish a release to GitHub via gh.
tenzir-changelog release publish [version] [options]| Option | Description |
|---|---|
version | Release version (defaults to latest) |
--yes | Skip confirmation prompts |
--draft | Mark as draft |
--prerelease | Mark as prerelease |
--no-latest | Prevent GitHub from marking as latest release |
--tag | Create and push annotated Git tag |
--commit | Commit staged changes before tagging (requires --tag) |
--commit-message | Custom commit message (default: Release {version}) |
The command reads project metadata from config.yaml or package.yaml for the
repository slug and uses notes.md as the release body. Projects without a
repository field cannot publish—this is intentional for changelogs that track
changes without producing GitHub releases (e.g., modules in a workspace).
How it works
Section titled “How it works”The release publish command executes the following steps:
-
Validate configuration: Checks that the
repositoryfield is set inconfig.yamlorpackage.yaml. This field determines the GitHub repository to publish to (e.g.,tenzir/tenzir). -
Check for
ghCLI: Verifies that the GitHub CLI is installed and available inPATH. The command usesghfor all GitHub operations. -
Find release manifest: Locates the release manifest at
releases/<version>/manifest.yaml. Fails if the specified version doesn’t exist. -
Verify release notes: Checks that
releases/<version>/notes.mdexists and is non-empty. If missing, prompts you to runrelease createfirst. -
Commit staged changes (if
--commit): Creates a git commit with all staged changes. Requires--tagto be set. Uses the commit message from--commit-message, therelease.commit_messageconfig field, or defaults toRelease {version}. -
Create and push git tag (if
--tag): Creates an annotated git tag named after the version with messageRelease {version}. If the tag already exists, skips creation but continues. Pushes the current branch to its upstream remote, then pushes the tag to the remote matching the configured repository. -
Check for existing GitHub release: Queries GitHub to determine if a release with this version already exists.
-
Create or update GitHub release:
- If the release exists, runs
gh release editto update the release notes and title. - If the release doesn’t exist, runs
gh release createwith the version tag, release notes fromnotes.md, and optional--draft,--prerelease, or--latest=falseflags.
- If the release exists, runs
-
Confirm and execute: Unless
--yesis provided, prompts for confirmation before running theghcommand. Shows the exact action (gh release createorgh release edit) that will run.
Typical workflow
Section titled “Typical workflow”A full release workflow with commit and tag:
# 1. Create the release (generates manifest.yaml and notes.md)tenzir-changelog release create --minor --yes
# 2. Review the generated notestenzir-changelog release notes
# 3. Stage release filesgit add changelog/releases/
# 4. Publish with commit and tag (defaults to latest release)tenzir-changelog release publish --commit --tag --yesAlternatively, commit manually to customize the message using release version:
# 3. Stage and commit with custom messagegit add changelog/releases/git commit -m "Release $(tenzir-changelog release version)"
# 4. Publish (defaults to latest release)tenzir-changelog release publish --tag --yesBoth workflows are version-agnostic after step 1. The release publish command
defaults to the latest release when no version is specified.
validate
Section titled “validate”Run structural checks across entry files, release manifests, and exported documentation.
tenzir-changelog validateThe validator reports missing metadata, unused entries, duplicate entry IDs, and configuration drift across repositories. When modules are configured, validation runs against the parent project and all discovered modules. Issues from modules are prefixed with the module ID.
modules
Section titled “modules”List discovered modules when a modules glob pattern is configured.
tenzir-changelog modulesThe command displays a table with module ID, name, relative path, and count of
unreleased entries. Use this to discover module paths for --root operations.
Configuration
Section titled “Configuration”Configuration settings live in config.yaml by default, or you can store them
in package.yaml alongside the changelog/ directory. config.yaml takes
precedence when both exist.
Configuration fields:
| Field | Description |
|---|---|
id | Canonical project slug written into entry frontmatter (required) |
name | Human-friendly label surfaced in release titles and CLI output |
description | Optional project description included in release manifests |
repository | GitHub slug (e.g., owner/repo) required by release publish |
export_style | Default layout: compact (bullet-list) or omit for detailed cards |
explicit_links | Render @mentions and #PR references as explicit Markdown links (boolean) |
omit_pr | Suppress PR numbers in entries (boolean, default false) |
omit_author | Suppress author attribution in entries (boolean, default false) |
components | Optional dict mapping component names to descriptions |
modules | Optional glob pattern for discovering nested changelog projects |
release | Release settings block (see below) |
Example:
id: tenzir-corename: Tenzir Coredescription: Core pipeline enginerepository: tenzir/tenzirexport_style: compactexplicit_links: truecomponents: cli: Command-line interface and user commands engine: Core pipeline engine internals operators: Built-in pipeline operatorsrelease: commit_message: "Release {version}"The release block supports:
| Field | Default | Description |
|---|---|---|
commit_message | Release {version} | Template for --commit flag; {version} expands |
The omit_pr and omit_author options suppress PR numbers and author
attribution in generated entries. When enabled, the CLI skips auto-detection and
ignores any --pr, --author, or --co-author flags (with a warning). Use
these options for projects that don’t use GitHub pull requests or prefer
anonymous changelog entries.
The first invocation of tenzir-changelog add scaffolds a changelog/
subdirectory with config.yaml, inferring defaults from the parent directory
name. When you provide an explicit --root flag, the CLI uses that directory
directly. Projects with package.yaml next to changelog/ reuse the package
id and name automatically.
Entry file format
Section titled “Entry file format”Entry files live in unreleased/ or releases/<version>/entries/ as Markdown
files with YAML frontmatter. The CLI names entry files using the slugified title
(e.g., my-feature.md, fix-bug.md).
Example entry:
---title: Add pipeline buildertype: featureauthor: alicecreated: 2025-10-16T14:30:00Zpr: 101component: cli---
Introduces the new pipeline builder UI with drag-and-drop support.You can use either singular (author, pr, component) or plural (authors,
prs, components) keys. The singular form is shorthand for single values and
is normalized to the plural form internally.
Frontmatter fields:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Entry title |
type | string | yes | breaking, feature, bugfix, or change |
author / authors | list[string] | no | Contributor names (singular or plural form) |
created | string | yes | Creation datetime in ISO 8601 UTC format |
pr / prs | list[int] | no | Pull request numbers (singular or plural form) |
component / components | list[string] | no | Labels matching configured components |
Release manifest format
Section titled “Release manifest format”Release manifests live at releases/<version>/manifest.yaml and record metadata
about a release.
Example manifest:
created: 2025-10-18title: Big Releaseintro: | Welcome to version 1.0.0!
This release includes significant performance improvements.Manifest fields:
| Field | Type | Required | Description |
|---|---|---|---|
created | string | yes | Release date in YYYY-MM-DD format |
title | string | no | Custom title for the release heading |
intro | string | no | Introductory content (supports Markdown) |
modules | dict | no | Module versions at release time (auto-populated) |
The modules field is automatically populated when creating a release for a
parent project with configured modules. It maps module IDs to their latest
versions, enabling incremental module summaries in subsequent releases.
Example manifest with modules:
created: 2025-10-18title: Marketplace v1.0.0intro: | Initial release with all plugins.modules: git: v1.0.0 docs: v1.1.0 changelog: v1.2.0The CLI generates notes.md with an H1 heading followed by the intro and
grouped entry sections. The heading format depends on whether a custom title is
set:
- With custom title:
# {title}(e.g.,# Big Release) - Without title:
# {name} {version}(e.g.,# My Project v1.0.0)
Intro content comes from either --intro (inline text) or --intro-file (file
path) when creating a release.
JSON export format
Section titled “JSON export format”JSON exports (-j flag) return structured objects for programmatic consumption.
Entries include prs and authors fields as structured objects with URLs.
PR objects
Section titled “PR objects”Each PR is an object with number and optional url:
{ "prs": [ {"number": 123, "url": "https://github.com/owner/repo/pull/123"}, {"number": 456, "url": "https://github.com/owner/repo/pull/456"} ]}The url field is included when the project’s repository setting is
configured. Without a repository, PRs contain only the number field.
Author objects
Section titled “Author objects”Each author is an object with either handle + url (for GitHub handles) or
name (for full names):
{ "authors": [ {"handle": "alice", "url": "https://github.com/alice"}, {"name": "Bob Smith"} ]}GitHub handles (names without spaces) include a url field linking to the
user’s profile. Full names (containing spaces) include only the name field.
Environment variables
Section titled “Environment variables”The CLI recognizes these environment variables:
| Variable | Description |
|---|---|
GITHUB_TOKEN | GitHub token with repo scope for private repos |
Modules
Section titled “Modules”Modules enable monorepo support by discovering nested changelog projects via a glob pattern. Each module is a fully independent project with its own configuration, versioning, and release cycle. The parent project serves as a workspace that provides discovery and aggregated views.
Configuration
Section titled “Configuration”Add a modules field to your parent project’s config.yaml:
id: libraryname: Tenzir Librarymodules: "../packages/*/changelog"The glob pattern resolves relative to the changelog root directory. Each matched
directory must contain a valid config.yaml with its own id and name:
id: amazon_vpc_flowname: Amazon VPC Flow PackageParent as workspace
Section titled “Parent as workspace”When a parent project defines modules, it typically acts as a workspace
coordinator rather than a versioned project itself. The parent provides:
- Discovery: Automatically finds modules matching the glob pattern
- Aggregation: Shows entries from all modules in unified views
- Validation: Checks module ID uniqueness and configuration validity
The parent may or may not have its own releases. A parent without releases exists purely for coordination.
Aggregated views
Section titled “Aggregated views”When modules is configured, show includes entries from all modules. The
table displays a Project column showing which project (parent or module)
each entry belongs to:
# Show all entries (parent + modules)tenzir-changelog show
# Show only parent entriestenzir-changelog show --project parentAggregated release summaries
Section titled “Aggregated release summaries”When creating a release for a parent project, release create automatically
appends a summary of module changes to the release notes. Each module with new
entries since the previous parent release gets a section showing:
- The module name and version as a heading
- A compact bullet list of entries with emoji prefix, title, and byline
---
## Git Plugin v1.1.0
- 🚀 Add commit message templates — *@alice*- 🐞 Fix branch detection — *@bob and @claude*
## Docs Plugin v1.2.0
- 🔧 Improve search indexing — *@alice*The release manifest records each module’s version at release time in the
modules field. Subsequent releases compare against this baseline and only
include entries from module releases newer than the recorded version. This
prevents duplicate entries across parent releases.
The release notes command also renders module summaries when viewing a
specific release, using the same incremental logic based on the manifest’s
recorded module versions.
Operating on modules
Section titled “Operating on modules”Each module is a standalone changelog project. Use --root to operate on a
specific module:
# Add entry to a moduletenzir-changelog --root ../packages/amazon_vpc_flow/changelog add --title "Add parser"
# Create a release for a moduletenzir-changelog --root ../packages/amazon_vpc_flow/changelog release create v1.0.0The modules command lists discovered modules with their paths for convenient
copy-paste into --root flags.
Independent versioning
Section titled “Independent versioning”Each module maintains its own version history and releases on its own schedule. There is no coordinated release mechanism—modules evolve independently.
Typically, modules do not publish GitHub releases themselves. Instead, the
parent workspace handles distribution (e.g., bundling modules into a product
release or publishing to a package registry). Modules that need to publish
standalone GitHub releases can add a repository field to their configuration.
Validation
Section titled “Validation”When modules is configured, tenzir-changelog validate checks:
- Module ID uniqueness across all discovered modules
- Valid configuration in each module directory
Troubleshooting
Section titled “Troubleshooting”- Validation errors – Run
tenzir-changelog validateto identify missing metadata, unused entries, or duplicate IDs. - Component mismatch – When
componentsis configured, ensure every entry either omitscomponentor uses an allowed label. - Configuration not found – Ensure
config.yamlexists in the changelog root orpackage.yamlsits next to thechangelog/directory. Runtenzir-changelog addonce to scaffold the configuration. - Version bump fails – Bump flags read the latest release manifest on disk.
Create an initial release with an explicit version before using
--patch/--minor/--major.