relnote docs · CHANGELOG.md · Next version · standard-version alternative · semantic-release alternative · git-cliff alternative · release-please alternative · GitHub Action · Single-file zipapp
A conventional-changelog and commitizen alternative without Node plugins
conventional-changelog (and the conventional-changelog-* presets) turns conventional commits into CHANGELOG.md. commitizen and cz-cli prompt for the commit message. Neither command bumps a version file and tags. If you want that changelog — and the bump, commit, and tag — from commits you already wrote, without Node plugins for the release tooling, the relnote zipapp and examples/release.sh do it offline.
conventional-changelog + commitizen, or this?
| conventional-changelog + commitizen | relnote + release.sh | |
|---|---|---|
| Runtime for release tooling | Node.js and npm packages | bash, git, Python 3.11 stdlib (zipapp) |
| Plugins | conventional-changelog-cli, a preset (conventional-changelog-conventionalcommits, conventional-changelog-angular, …), cz-cli / commitizen, an adapter (cz-conventional-changelog) | None; flags on release.sh |
| Commit prompt | git cz asks type, scope, and subject | None. Commits are already conventional |
| Changelog | conventional-changelog -p angular -i CHANGELOG.md -s (preset plugin; changelog only) | relnote --changelog CHANGELOG.md via release.sh |
| Version bump, commit, tag | Not those two CLIs. conventional-recommended-bump prints a bump type; standard-version / commit-and-tag-version do the full release | One command: optional --bump-file, CHANGELOG.md, chore(release) commit, annotated tag |
| Network while writing the changelog | npm install of plugins; presets resolve from the registry | Offline until you push or gh release create |
| Config | package.json config.commitizen, .czrc, -p preset | None |
Keep commitizen if you want the questions. Keep a conventional-changelog-* preset if you depend on its writer options. This page replaces the changelog command and the separate bumper, not the prompt.
Pin the zipapp
Python 3.11+ and git on PATH. The download is the v0.3.7 zipapp from the GitHub Release (same pin as the single-file install), plus examples/release.sh from that tag. No npm packages.
mkdir -p ~/.local/bin
curl -fsSL -o ~/.local/bin/relnote https://github.com/loki-inu/relnote/releases/download/v0.3.7/relnote
chmod +x ~/.local/bin/relnote
curl -fsSL -o release.sh https://raw.githubusercontent.com/loki-inu/relnote/v0.3.7/examples/release.sh
chmod +x release.sh
Put ~/.local/bin on PATH. Commit release.sh into the repo (for example scripts/release.sh) so the release command does not depend on the raw URL next time. It is plain bash; read it before you run it.
One local command
The working tree must be clean. ./release.sh reads conventional commits since the last tag, prepends CHANGELOG.md, commits chore(release): vX.Y.Z, and creates an annotated tag. Nothing is pushed. The version rule is relnote --next-version: breaking → major, feat → minor, otherwise patch (in 0.x a breaking change bumps minor). See next version and the CHANGELOG.md guide.
Omit --bump-file when you have no version file. Pass it (repeatable) to set the first "version" / version = line in package.json, pyproject.toml, or Cargo.toml. --dry-run prints the plan and the notes and writes nothing. These are three separate invocations, not one script to run top to bottom:
./release.sh --dry-run
./release.sh
./release.sh --bump-file package.json
Afterwards: git push --follow-tags. If HEAD is already tagged, or the next tag already exists, the script exits 1 instead of guessing.
Changelog only, the shape of conventional-changelog -i CHANGELOG.md -s without a preset package — you choose the title, and nothing is committed:
relnote --next-version
relnote --changelog CHANGELOG.md --changelog-title v1.4.0
Use the tag --next-version printed. ./release.sh fills that title in for you and then commits and tags.
Optional GitHub Actions release
When you also want a GitHub Release, save this as .github/workflows/release.yml and use Actions → Release → Run workflow. It is the same job as the semantic-release alternative and the release-please alternative: a workflow_dispatch button, a has-changes gate, then release.sh. Drop --bump-file pyproject.toml or point it at another file. No Node install for the tooling. Action inputs: GitHub Action guide.
name: Release
on:
workflow_dispatch:
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: relnote
uses: loki-inu/relnote@v0.3.7
- name: Install relnote and release.sh
if: steps.relnote.outputs.has-changes == 'true'
env:
V: v0.3.7
run: |
mkdir -p "$HOME/.local/bin"
curl -fsSL -o "$HOME/.local/bin/relnote" "https://github.com/loki-inu/relnote/releases/download/$V/relnote"
chmod +x "$HOME/.local/bin/relnote"
curl -fsSL -o "$RUNNER_TEMP/release.sh" "https://raw.githubusercontent.com/loki-inu/relnote/$V/examples/release.sh"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Bump, changelog, tag, publish
if: steps.relnote.outputs.has-changes == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ -n "$(git tag --points-at HEAD --list 'v*')" ]; then
echo "HEAD is already tagged; nothing to release."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
bash "$RUNNER_TEMP/release.sh" --bump-file pyproject.toml
tag=$(git describe --tags --exact-match)
git push origin HEAD "refs/tags/$tag"
relnote --output "$RUNNER_TEMP/notes.md" --quiet
gh release create "$tag" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md"
The first relnote step only checks whether anything is unreleased (has-changes). Notes go through a file and --notes-file. Never interpolate the body inside run: (backticks in commit subjects break the YAML). Pin the action and the curl URLs to the same tag.
A tag-push workflow can publish the same notes after you tag locally: checkout with fetch-depth: 0, run the action, write notes.md, then gh release create with --notes-file. Same rule: do not put the notes in run:. See the tag-push example on the Action guide.
What one run does
Start: v1.3.2 is the last tag, pyproject.toml says version = "1.3.2", and two conventional commits landed since (the kind of subjects commitizen or a squash-merged pull request title would leave). The release step log:
next: v1.4.0
changelog: CHANGELOG.md
bump: pyproject.toml -> 1.4.0
committed: chore(release): v1.4.0
tagged: v1.4.0
next step: git push --follow-tags
To https://github.com/you/project.git
* [new branch] HEAD -> main
* [new tag] v1.4.0 -> v1.4.0
pyproject.toml now says version = "1.4.0", and CHANGELOG.md gains this section above the previous one:
## [v1.4.0] - 2026-10-28
### Features
- add --json output ([#12](https://github.com/you/project/pull/12)) (`e4e9bff`) — Ada Lovelace
### Fixes
- handle empty config ([#13](https://github.com/you/project/pull/13)) (`8c0e206`) — Ada Lovelace
**Full changelog:** https://github.com/you/project/compare/v1.3.2...v1.4.0
The GitHub Release v1.4.0 gets this body; on GitHub, #12 links to the pull request:
## What's changed
### Features
- add --json output (#12) (`e4e9bff`) — Ada Lovelace
### Fixes
- handle empty config (#13) (`8c0e206`) — Ada Lovelace
---
Range: `v1.3.2` → `v1.4.0`.
**Full changelog:** https://github.com/you/project/compare/v1.3.2...v1.4.0
The CHANGELOG.md section and Release body are produced by running the workflow's last step against a scratch repository in relnote's test suite (tests/test_conventional_changelog_docs.py, with gh stubbed); your hashes and dates differ. The chore(release) commit is not listed.
Bumping an npm package? Use --bump-file package.json. If you also keep a package-lock.json, add --exec "npm install --package-lock-only --ignore-scripts --silent" so the lock tracks the new version. Node runs there because the product is npm. See the npm package guide. The standard-version alternative maps the rest of the flags (--release-as, --skip-commit, --skip-tag, --tag-prefix).
FAQ
Does relnote replace commitizen prompts?
No. commitizen and cz-cli (with an adapter such as cz-conventional-changelog) ask for type, scope, and subject so the commit is conventional. relnote does not prompt. It reads commits that are already conventional — from commitizen, a pull-request title, or typed by hand — and writes CHANGELOG.md, an optional version bump, and a tag. Keep commitizen if you want the prompt; you do not need it to generate the changelog.
What about the conventional-changelog CLI on its own?
conventional-changelog-cli, with a conventional-changelog-* preset, writes CHANGELOG.md and stops. It does not bump a version file, commit, or tag. relnote --changelog CHANGELOG.md is that step, offline, with no preset package. examples/release.sh adds the optional bump, the chore(release) commit, and the annotated tag. Section shape: the CHANGELOG.md guide.
How does this differ from standard-version and commit-and-tag-version?
standard-version and its fork commit-and-tag-version already combine a conventional-changelog preset with a version bump, commit, and tag, and they need Node. release.sh is that local command without Node and without .versionrc. Keep commit-and-tag-version if you need prerelease bumps, lifecycle scripts, or custom presets. Flag mapping is on the standard-version alternative page.
How does this differ from the semantic-release and release-please pages?
The semantic-release alternative page is the CI publish loop without Node in the release tooling. The release-please alternative page is the same job for people leaving a release pull request. This page is for the conventional-changelog plus commitizen stack: changelog plugins and commit prompts, with the version bump usually in another tool. The local path is release.sh; the optional Actions job matches those pages. Leaving git-cliff (Rust / cliff.toml / Tera) instead? git-cliff alternative.
How do I dry-run?
Locally, release.sh --dry-run prints the next version, the changelog path, any version file it would bump, and the notes, and writes nothing. Add --bump-file only when a version file should change. Omit --bump-file when you only want a preview of the changelog, commit, and tag. In the Actions job, has-changes false skips the release steps when nothing is unreleased, and a second run whose HEAD is already tagged exits 0.
Are chore(release) commits included in the notes?
No by default. relnote skips chore(release) subjects so the release commit does not appear in its own CHANGELOG.md section or Release body. Pass --include-release-commits on the CLI, or include-release-commits: true on the action, to keep them.
Does an npm package still need Node?
Only for the product, not for this release tooling. The zipapp and release.sh stay free of Node plugins. For an npm package, pass --bump-file package.json, and if you keep a package-lock.json add --exec "npm install --package-lock-only --ignore-scripts --silent" so the lock tracks the new version. See the npm package guide.