relnote docs · GitHub Action · release-please alternative · standard-version alternative · conventional-changelog alternative · git-cliff alternative · npm packages
A semantic-release alternative without npm/Node
semantic-release analyzes conventional commits in CI, picks the next version, writes notes, tags, creates a GitHub Release, and optionally publishes — through a Node plugin stack. If you want that loop without installing Node for the release tooling, without .releaserc / release.config.js, and without plugins that need the network during analysis, this workflow does it with the relnote zipapp, examples/release.sh, and a one-button (or tag-push) Actions job.
semantic-release or this?
| semantic-release | relnote + release.sh | |
|---|---|---|
| Runtime for release tooling | Node.js + npm packages | bash, git, Python 3.11 stdlib (zipapp) |
| Plugins / config | @semantic-release/*, .releaserc / release.config.js | None; flags on release.sh |
| Network during analyze | Often (npm registry for plugins; GitHub API for some plugins) | Offline until gh release create |
| Release PR | No (cuts on the CI run) | No (same; see also release-please alternative) |
| Changelog | @semantic-release/changelog plugin | relnote --changelog via release.sh |
| Publish hooks | Plugins (npm, github, …) | Your own steps after the tag (PyPI, npm, crates.io guides) |
| Monorepo | Multi-package plugins / workspaces setups | --tag-prefix + --path per package |
The workflow
Save as .github/workflows/release.yml, then use Actions → Release → Run workflow. Change --bump-file pyproject.toml to package.json, Cargo.toml, or repeat the flag. No npm install for the release tooling — only curl of the pinned zipapp and script.
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 break YAML). Pin both the action and the curl URLs to the same tag so the job does not change under you.
Bumping an npm package's package.json? 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 — that is the only place Node appears, and only because the product is npm, not because the release tool needs it. See the npm package guide.
What one run does
Start: v1.3.2 is the last tag, pyproject.toml says version = "1.3.2", and two PRs were squash-merged since. The 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 PR:
## 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_semantic_release_docs.py, with gh stubbed); your hashes and dates differ.
Local dry-run (no CI)
curl -fsSL -o release.sh https://raw.githubusercontent.com/loki-inu/relnote/v0.3.7/examples/release.sh
chmod +x release.sh
./release.sh --dry-run --bump-file pyproject.toml
Same script as the standard-version alternative; nothing is written until you drop --dry-run.
Replacing conventional-changelog and commitizen (changelog plugins and commit prompts) rather than the semantic-release plugin stack? conventional-changelog / commitizen alternative.
Leaving git-cliff (Rust binary, cliff.toml, Tera templates) for a Release body plus one CHANGELOG section? git-cliff alternative.
Monorepo packages
Pass the package's tag prefix and path: bash "$RUNNER_TEMP/release.sh" --tag-prefix api/v --bump-file packages/api/package.json -- --path packages/api, then relnote --tag-prefix api/v --path packages/api --output …. Change the guard's 'v*' to 'api/v*'. See the monorepo guide.
Publishing the artifact? Python / PyPI, Rust / crates.io, or npm add the language-specific publish steps after the tag.
FAQ
Why leave semantic-release?
Keep semantic-release if every merge to main should publish through a Node plugin stack (@semantic-release/commit-analyzer, changelog, npm, github, …) and you are happy maintaining that config. This page is for repos that want the same analyze → version → CHANGELOG → tag → GitHub Release loop without installing Node for the release tooling, without .releaserc, and without plugins that talk to the network during analysis.
What if main is protected?
The push uses the workflow's GITHUB_TOKEN. If branch protection blocks direct pushes to main, allow GitHub Actions to bypass the rule, or push with a GitHub App token or deploy key that may. semantic-release often pushes tags with the same token constraints.
Will the new tag trigger my other workflows?
Not with GITHUB_TOKEN: GitHub does not start new workflow runs for pushes made with it. Put publishing steps in this same job after gh release create, or push with a GitHub App token if you want an on: push: tags workflow to fire.
How do I dry-run?
Locally: release.sh --dry-run --bump-file pyproject.toml prints the next version, the files it would change, and the release notes without writing anything. In CI, the has-changes gate skips the release steps when there is nothing unreleased.
How does this differ from the release-please and standard-version pages?
release-please-alternative.html targets people leaving a release PR bot. standard-version-alternative.html is the local one-command bump without Node. This page targets people searching for a semantic-release replacement who do not want Node/npm in the release pipeline; the workflow is the same release.sh + Actions job, emphasized as Node-free tooling.
Does npm publish still need Node?
Only if the product itself is an npm package. The release tooling path (relnote zipapp + release.sh) stays Node-free. For an npm product, optionally add --exec "npm install --package-lock-only --ignore-scripts --silent" so package-lock.json tracks the new version, and run npm publish in the same job or on tag push — see the npm package guide.
What about multi-package / monorepos?
Pass --tag-prefix and --path through release.sh and the notes step, and change the tag guard pattern. See the monorepo guide. semantic-release multi-package setups usually need more plugins; here each package is one bump-file and path.
Are chore(release) commits included in the notes?
No by default. The relnote action and CLI skip chore(release) subjects so the release commit does not appear in its own notes. Pass include-release-commits: true (action) or --include-release-commits (CLI) to keep them.