Scripting and CI Integration
This page covers machine-readable output, exit codes, jq patterns, and complete CI workflow
examples. It is aimed at shell scripts, GitHub Actions jobs, and any automation that consumes the
CLI non-interactively.
For day-to-day interactive use, see Key workflows. For the full option reference, see CLI reference.
Exit codes
The CLI uses two exit codes:
| Code | Meaning |
|---|---|
0 |
Success — or a non-fatal info/warning diagnostic |
1 |
Error — command failed, validation found errors, or a precondition was not met |
validate exits 1 when there are errors; warnings alone still produce 0. All other commands
exit 1 only on hard failures (missing [Unreleased], bad version, API error, etc.).
There is no exit code 2 for "usage error" — a bad flag causes argparse to print help and exit 2
via the standard library, not via the CLI dispatch.
Machine-readable flags
| Flag | Effect |
|---|---|
--json |
Emit a single JSON object to stdout instead of human text |
--quiet |
Suppress non-error human output; diagnostics and exit codes still work |
--json and --quiet are global flags that go before the command name:
changelogmanager --json version --reference future
changelogmanager --quiet validate
Runtime logs (--info, --verbose) always go to stderr and never pollute stdout.
JSON output shapes
version
changelogmanager --json version --reference future
{
"version": "1.4.0",
"reference": "future"
}
validate
changelogmanager --json validate
{
"valid": true,
"errors": [],
"warnings": []
}
On failure the array contains diagnostic objects with file, line, col, and message.
release
changelogmanager --json release --yes
{
"released": "1.4.0"
}
With --bump-versions:
{
"released": "1.4.0",
"bumped_version": "1.4.0",
"bumped_files": ["pyproject.toml", "mypackage/__about__.py"]
}
remove --list
changelogmanager --json remove --list
{
"entries": [
{"change_type": "added", "index": 0, "message": "Support dark mode"},
{"change_type": "fixed", "index": 0, "message": "Crash on empty input"}
]
}
remove --count
Prints the total number of unreleased entries as a bare integer — no jq needed:
changelogmanager remove --count # prints e.g. "2"
With --json:
changelogmanager --json remove --count
{ "count": 2 }
github-release
changelogmanager --json github-release --repository owner/repo
{
"release_state": "draft",
"version": "1.4.0",
"html_url": "https://github.com/owner/repo/releases/tag/v1.4.0"
}
from-commits
changelogmanager --json from-commits
{
"added": 3,
"skipped": 1,
"since": "v1.3.0"
}
backfill
changelogmanager --json backfill --source tags --dry-run
{
"unreleased_added": 0,
"since": null
}
Extracting values with jq
Capture the next version into a shell variable:
NEXT_VERSION=$(changelogmanager --json version --reference future | jq -r '.version')
echo "Next release: $NEXT_VERSION"
Check whether validation passed:
RESULT=$(changelogmanager --json validate)
if echo "$RESULT" | jq -e '.valid == false' > /dev/null; then
echo "Changelog has errors:" >&2
echo "$RESULT" | jq -r '.errors[] | "\(.file):\(.line): \(.message)"' >&2
exit 1
fi
List all unreleased entries:
changelogmanager --json remove --list \
| jq -r '.entries[] | "[\(.change_type)] \(.message)"'
Check whether there are any unreleased entries before attempting a release:
COUNT=$(changelogmanager remove --count)
if [ "$COUNT" -eq 0 ]; then
echo "Nothing to release." >&2
exit 0
fi
Get all files that were version-bumped by release --bump-versions:
BUMPED=$(changelogmanager --json release --bump-versions --yes \
| jq -r '.bumped_files[]')
git add CHANGELOG.md $BUMPED
Typical release.yml patterns
The most common automation context is a GitHub Actions release job. The examples below assume
changelogmanager is installed into the job's virtualenv via uv.
Minimal: validate on every PR
- name: Validate changelog
run: uv run changelogmanager --error-format github validate
--error-format github makes errors appear as inline annotations on the pull request diff.
The job fails automatically on exit code 1.
Release triggered by publishing a GitHub draft release
This is the pattern used in this repository. A separate workflow keeps a draft release in sync
with [Unreleased] on every push to main. When you publish that draft, a release event fires
and the following job runs.
jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Install with jiggle extra
run: uv sync --frozen --extra jiggle
- name: Release changelog and bump versions
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}" # strip leading v
uv run changelogmanager release \
--override-version "$VERSION" \
--bump-versions \
--yes
- name: Commit and push bump branch
run: |
VERSION="${{ github.event.release.tag_name }}"
BRANCH="release/bump-${{ github.run_id }}"
git checkout -b "$BRANCH"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md pyproject.toml
git commit -m "chore: release ${VERSION}"
git push origin "$BRANCH"
- name: Open or update release PR
run: |
VERSION="${{ github.event.release.tag_name }}"
BRANCH="release/bump-${{ github.run_id }}"
uv run changelogmanager github-pr \
--repository "${{ github.repository }}" \
--head "$BRANCH" \
--base "${{ github.event.release.target_commitish }}" \
--title "chore: release ${VERSION}"
build:
needs: bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: "release/bump-${{ github.run_id }}"
- uses: astral-sh/setup-uv@v5
- run: uv build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
Keep a draft GitHub release in sync with [Unreleased]
Run this on every push to main so reviewers always see the current draft before publishing:
- name: Update draft release
run: |
uv run changelogmanager github-release \
--repository "${{ github.repository }}"
Validate and auto-fix in CI (opt-in)
Preview what --fix would change without rewriting tracked files:
- name: Preview changelog fixes
run: uv run changelogmanager validate --fix --dry-run
If you want CI to commit autofixes automatically:
- name: Autofix changelog
run: |
uv run changelogmanager validate --fix
if ! git diff --quiet CHANGELOG.md; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "chore: autofix changelog"
git push
fi
Scripting guard patterns
Skip gracefully when [Unreleased] is empty
release already exits 0 with a skip notice when [Unreleased] has no entries, so most CI
jobs need no guard at all:
changelogmanager release --yes # safe to call unconditionally
If your script needs to branch on whether a release actually happened, check the --json output:
RESULT=$(changelogmanager --json release --yes)
if echo "$RESULT" | jq -e '.skipped' > /dev/null 2>&1; then
echo "Nothing to release."
else
VERSION=$(echo "$RESULT" | jq -r '.released')
echo "Released $VERSION"
fi
Or use remove --count to check up front without jq:
if [ "$(changelogmanager remove --count)" -eq 0 ]; then
echo "Nothing to release." >&2
exit 0
fi
changelogmanager release --yes
Gate on validation before releasing
changelogmanager validate || { echo "Fix changelog errors before releasing." >&2; exit 1; }
changelogmanager release --yes
Capture next version before it is promoted
NEXT=$(changelogmanager --json version --reference future | jq -r '.version')
changelogmanager release --yes
echo "Released $NEXT"
Non-interactive add in a commit hook
# In .git/hooks/post-commit or a CI step
SUBJECT=$(git log -1 --format=%s)
changelogmanager add \
--change-type changed \
--message "$SUBJECT" \
--quiet
Scripting behaviors to be aware of
releaserequires--yesin non-interactive contexts. Without it the command detects no TTY and refuses. This is intentional — accidental releases in CI are hard to undo.releaseexits0when[Unreleased]is empty. If the[Unreleased]heading exists but has no entries,releaseprints a skip notice and exits0— so a "nothing to release" run does not fail a CI job. It still exits1if there is no[Unreleased]section at all.--dry-runwrites nothing. All commands support--dry-run; use it to validate inputs before committing to a destructive change in CI.--jsondoes not suppress stderr diagnostics. Validation errors printed byvalidatein LLVM/GitHub format still go to stderr even with--json. Redirect stderr if you want a completely clean stdout pipe.- Exit code
0on warnings.validateexits0for warnings. If your script needs to distinguish warnings from a clean run, parse the--jsonoutput and inspect.warnings.