changenote
A Rust CLI for language-independent change notes, SemVer versions, changelogs, and release tags. Inspired by Changesets.
Each package has an explicit directory and a version file: a package manifest or a plain-text file. No language runtime or package manager is required after the CLI is built.
Install
Requires stable Rust with edition 2024 support. Git is required only for tag.
cargo install --path . --locked
Single package
Run commands from the repository root, or use the global --root PATH option.
changenote init --name pkg-a --version 0.0.0
changenote add hello-world --package pkg-a --bump minor --message "Initial release"
This creates .changenote/hello-world.md:
---
pkg-a: minor
---
Initial release
Quoted keys, such as "pkg-a": minor, are also accepted. You can write notes directly without the CLI.
changenote status # JSON plan; no file changes
changenote version # Apply plan and consume notes
git add -- .changenote VERSION CHANGELOG.md
git commit -m "chore(release): version packages"
changenote tag # Create local annotated tag v0.1.0
git push origin HEAD
git push origin refs/tags/v0.1.0
version takes the highest requested bump for each package and applies it once. Multiple patch notes cause one patch bump, not multiple bumps. Major, minor, and patch follow stable SemVer rules, including versions below 1.0.0.
The command updates each configured version file, prepends entries to CHANGELOG.md, saves the latest release batch to .changenote/releases.json, and removes consumed notes. A run without pending notes does not change files or clear the saved batch.
Monorepo
Create .changenote/config.json with explicit packages:
{
"$schema": "https://raw.githubusercontent.com/narumiruna/changenote/main/schema.json",
"packages": {
"pkg-a": { "path": "packages/pkg-a", "version_file": "VERSION" },
"pkg-b": { "path": "packages/pkg-b", "version_file": "VERSION" }
}
}
The optional $schema field enables editor validation and completion with the repository's JSON Schema. init adds the GitHub raw URL shown above. Replace main with a commit SHA to pin the schema, or use a local schema path. The CLI accepts the field as metadata; it does not fetch the schema or use it for validation. Configurations without $schema remain supported. Unknown configuration fields are rejected. The CLI also checks paths, files, and versions that the schema cannot verify.
Each package directory must exist and contain its version file, for example packages/pkg-a/VERSION:
0.1.0
Packages in any language can use a plain-text VERSION file, including Python, Rust, Node.js, and Go. Package manifests are optional version sources. You can mix manifests and VERSION files in the same repository:
{
"packages": {
"pkg-python": { "path": "packages/pkg-python", "version_file": "pyproject.toml" },
"pkg-rust": { "path": "packages/pkg-rust", "version_file": "Cargo.toml" },
"pkg-node": { "path": "packages/pkg-node", "version_file": "package.json" },
"pkg-go": { "path": "packages/pkg-go", "version_file": "VERSION" }
}
}
version_file is relative to the package directory. Its filename selects the version field:
| Filename | Version field |
|---|---|
pyproject.toml |
project.version |
Cargo.toml |
package.version |
package.json |
Top-level version |
Other filenames, including VERSION |
Plain-text stable SemVer string |
Manifest versions must be explicit strings. Dynamic Python versions, Poetry-only versions, and inherited Cargo workspace versions are not supported. TOML comments and unrelated fields are preserved. JSON fields and their order are preserved; output uses two-space indentation and a final newline. Lockfiles and dependency versions are not updated. init still creates a plain-text VERSION; configure existing manifests manually.
One note can name multiple packages with different bumps:
---
"pkg-a": patch
"pkg-b": minor
---
Update the shared API
With two or more configured packages, tags use changenote/<package>@v<version>. A patch for pkg-a at 0.1.0 creates changenote/pkg-a@v0.1.1. Packages without notes remain unchanged. Tag format depends on the number of configured packages, not the number changed in a release.
To create a note with the same bump for several packages:
changenote add shared-api -p pkg-a -p pkg-b --bump minor -m "Update the shared API"
GitHub Actions
This repository provides two reusable composite Actions:
narumiruna/changenote@<full-commit-sha>: create or update a Release PR.narumiruna/changenote/publish@<full-commit-sha>: publish annotated tags and GitHub Releases.
Both require Linux, Bash, Git, gh, jq, a clean checkout, and an installed changenote executable on PATH. Ubuntu GitHub-hosted runners include the non-CLI tools. Package version files, changelogs, and .changenote/releases.json must not be symlinks. Both Actions check the batch path before CLI writes. Run at the repository root; the Actions load their scripts from the Action checkout, not the consumer repository. They do not install the CLI or publish registries. The CLI updates configured version files only; it does not synchronize other manifests or lockfiles.
Release PR workflow
Use this workflow in a consumer repository. Replace each SHA placeholder with a reviewed full commit SHA. The CLI and Action must use compatible release-plan formats.
name: Release
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: changenote-release-main
cancel-in-progress: false
jobs:
release:
permissions:
contents: write
pull-requests: write
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Install changenote
run: cargo install --git https://github.com/narumiruna/changenote --rev <full-cli-commit-sha> --locked
- uses: narumiruna/changenote@<full-action-commit-sha>
with:
base-branch: main
publish:
needs: release
if: ${{ always() && github.ref == 'refs/heads/main' && needs.release.result != 'cancelled' }}
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Install changenote
run: cargo install --git https://github.com/narumiruna/changenote --rev <full-cli-commit-sha> --locked
- uses: narumiruna/changenote/publish@<full-action-commit-sha>
with:
base-branch: main
The Action fetches the current base branch, applies pending notes in a temporary worktree, and creates or updates one PR from narumi/chore/changenote-release-<base-branch>. Reserve that branch for automation. New notes update the same PR; unchanged generated trees do not create new commits or pushes. If notes are removed, the Action closes only its managed PR and leaves the branch in place. A manually closed PR is replaced when notes remain.
Release PR descriptions use a Releases heading and package@version headings. Notes are grouped by their own bump under Major Changes, Minor Changes, and Patch Changes; empty groups are omitted. Multiline notes remain within their list items. The Action requires a CLI that supports status --include-note-bumps. This option adds a note_bumps array aligned with notes; default release-plan output and saved batches are unchanged.
Existing branches require a marked PR in their history. Lease-based pushes stop if another actor moves the branch. The Action stages only release outputs and consumed notes. Notes and titles are data, not shell commands. PR bodies over 60,000 bytes omit release details.
In repository settings, enable Actions > General > Allow GitHub Actions to create and approve pull requests. Branch rules must permit the bot to push the release branch. The Action never approves or merges its PR.
PRs created with the default GITHUB_TOKEN do not automatically trigger PR workflows. If CI must run on the Release PR, use a GitHub App token (recommended) or PAT with repository contents and pull requests write permissions. Pass it through github-token. Checkout credentials do not select the Action's token.
Publication workflow
The automatic workflow above runs publication after PR management. Publication can recover an earlier batch even if PR management fails. Its job needs only contents: write.
To retry a partial publication failure, run Release manually on main. This runs both PR management and publication again. They use one workflow concurrency group, so no separate retry workflow is needed. Keep required publication steps in this workflow: tags pushed with GITHUB_TOKEN do not trigger other push workflows.
Publication selects the latest first-parent commit that changes .changenote/releases.json. It reconstructs that commit from the parent's notes and compares the complete generated tree before publication. Standard merge, squash, and rebase merges are supported. A later unrelated commit or new pending notes do not change the release target.
Existing remote tags must point to the selected commit. Missing tags and GitHub Releases are created; existing release text is not replaced. Draft releases, missing tags for existing releases, malformed batches, batch deletion, manually edited release trees, and tag conflicts stop publication. A batch in a root commit cannot be verified. No batch is a no-op.
The repository's .github/workflows/release.yml runs PR management and GitHub publication on pushes to main or manual dispatch. It builds the CLI instead of installing it and does not push version commits directly to main. It then builds Python distributions from the validated release commit and uploads them to PyPI with uv publish and Trusted Publishing. See python/README.md for supported platforms and required publisher settings. These repository-specific jobs are not part of the reusable Actions.
Inputs and outputs
| Action | Input | Default |
|---|---|---|
| Both | github-token |
Workflow token |
| Both | base-branch |
Repository default branch |
| Release PR | commit-message |
chore(release): version packages |
| Release PR | pr-title |
chore(release): version packages |
The PR Action outputs has-notes (true or false), pr-number (number or empty string), and pr-operation (created, updated, closed, or none). updated includes a no-op version rerun that refreshes the PR metadata.
The publication Action outputs release-commit (commit SHA or empty string), published (true if this run created any remote tag or GitHub Release), and published-packages (JSON, for example [{"name":"pkg-a","version":"1.2.0"}]). This is not registry publication. Read outputs only after successful steps.
Recovery and limits
Disable the Release workflow to stop automation. Unmerged Release PR changes do not affect the base branch.
After a partial publication failure, rerun Release. The Action uses the saved release commit even after unrelated commits. Do not merge another Release PR until the current batch is fully published: discovery covers only the latest batch. History rewrites or manual batch edits require manual inspection. Never move tags automatically.
If PR creation fails after pushing a new branch, the Action checks for a successful remote PR creation before it removes that branch with a lease. If the API or cleanup also fails, inspect the marked PR and branch manually before retrying; an unowned branch is deliberately rejected. Do not delete a branch without checking its commit and PR history.
Before production use, verify repository permissions, branch rules, and App-token CI behavior in a disposable repository.
Scope and safety
- Configured version files are the release version source and must contain a stable SemVer version in the field listed above. Only configured files are updated; other manifests, lockfiles, and internal dependencies are not updated. Use a package-specific build or publication step if those files require synchronization. The CLI's
--versionreports its Cargo build version, not a configured package version. - Names use ASCII letters, digits, hyphens, and underscores. Paths are relative to the repository root and cannot contain parent components. Package output files cannot overlap or resolve outside the repository.
- Unknown packages, invalid bumps, duplicate YAML keys, empty summaries, and prerelease versions are rejected.
addnever overwrites a note. - Only top-level
.changenote/*.mdfiles are read. Hidden files andREADME.md,AGENTS.md,CLAUDE.md, andGEMINI.mdare ignored. tagrequires a clean Git working tree and committed release data. It creates local tags only; it does not push, publish artifacts, or run arbitrary commands.- Input validation and output preparation happen before version writes. Filesystem failures during writes can leave partial changes. Run in a clean checkout, inspect changes, and use Git to restore files before retrying. Do not run concurrent local version commands.
- Dependency propagation, registry publication, prereleases, and fixed or linked version groups are not implemented.
Development
The repository contains one Rust Cargo package and one Python distribution that packages its executable. Neither uses a workspace; the CLI still supports release plans for other monorepos.
Cargo.toml: Rust package metadata and dependencies.Cargo.lock: Rust dependency lockfile.src/: Rust CLI and release logic.tests/: Rust integration tests.VERSION: CLI release-plan version file.pyproject.toml: maturin package metadata and build configuration.uv.lock: Python project dependency lockfile.python/changenote/: Python wrapper module for the Rust CLI.python/tests/: wrapper tests..changenote/: repository-level notes and release configuration.action.yml,publish/action.yml: reusable Linux GitHub Actions.scripts/action/,tests/actions/: Action scripts and local Git/mock GitHub tests.
Run checks from the repository root:
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
cargo build --locked --release
cargo run --locked -p changenote -- status
The Rust package and executable names are changenote, so cargo run -- status works from the repository root. Rust build outputs go to target/.
For the Actions, install gh, jq, ShellCheck, and actionlint, then run:
for script in scripts/action/*.sh; do bash -n "$script"; done
shellcheck -x scripts/action/*.sh
actionlint
cargo build --locked
uv run --no-project python -m unittest discover -s tests/actions -v
uv run --no-project --with pyyaml python -m unittest discover -s tests/action_metadata -v
uv run --no-project python -m unittest discover -s tests/python_release -v
Action tests use temporary consumer repositories and a file-backed GitHub mock. They do not create remote GitHub tags or releases.
For the Python distribution, run from the repository root:
uv sync --locked
uv run --locked changenote status
uv run --locked python -m changenote --help
uv run --locked python -m unittest discover -s python/tests
uv build --out-dir dist/python-binary
uvx --isolated --no-index --find-links dist/python-binary changenote --help
The Python distribution changenote, with wrapper sources in python/changenote, packages the Rust executable using maturin bin bindings, like uv. It does not reimplement the CLI in Python. Compatible wheels require no Rust toolchain at runtime; source builds require Rust and Cargo. The optional python -m changenote runner invokes the installed executable.
The existing changenote PyPI project is the publication target. The Release workflow builds platform wheels with maturin Action and publishes with uv publish. Configure its PyPI trusted publisher before the first upload. After a successful publication, users on supported platforms can run uvx changenote status. See python/README.md.
The Rust CLI and Python distribution share the changenote release plan and root CHANGELOG.md; the current single release package uses v<version> tags. Python has no independent version file or bump. Its manifest uses a dynamic version from root Cargo.toml, so Python wheel metadata and the included Rust CLI version match. The PyPI workflow sets the Cargo package and its Cargo.lock entry to the release's VERSION in a disposable build checkout. It does not change the committed Cargo files. For manual publication, align the Rust crate version with VERSION and update lockfiles first.
CI currently runs on Ubuntu only. It runs Rust format checks, lint checks, and tests, plus Python wrapper tests, binary wheel builds, and uvx checks with Python 3.14. The container image uses the Rust executable; mount a repository at /repo to use it.
Metadata
Release files for changenote 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| changenote-0.2.0.tar.gz | 59.6 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| changenote-0.2.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| changenote-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| changenote-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| changenote-0.2.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| changenote-0.2.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 3.9 MB
Release files / changenote-0.2.0.tar.gz
| Download URL | changenote-0.2.0.tar.gz |
|---|---|
| Size | 59.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ad4f770ea884bc3d72f5ed5904a1ebabd41f0c402b0c7850f4259bc519f2401c
|
|
BLAKE2b-256 checksum How to use checksums |
cf7c39ff975a9837cdc841891aaeb1ed291c30505d4d98e8e79a7a4e7b60eb67
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / changenote-0.2.0-py3-none-win_amd64.whl
| Download URL | changenote-0.2.0-py3-none-win_amd64.whl |
|---|---|
| Size | 734.4 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
94a73cf74c4b203f271fadcc30d398ed90c621be2b3002e5e0d8c87985438f5f
|
|
BLAKE2b-256 checksum How to use checksums |
71bd53a4ca225e5116191765959f0c8adde61086aefd000f003f2f147ffc71db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / changenote-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | changenote-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 807.6 kB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
966df24a7164d6eec23a3b640337f7baac94fd38a4be505e79885ce5175edc39
|
|
BLAKE2b-256 checksum How to use checksums |
3354530575a9e1e8112a68120d8e965d96682061ce0d08f0c75975357d4a3607
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / changenote-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | changenote-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 768.2 kB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
d5b5aaba11164407cf1be1c51fc2daecd4234b181548d8a77d3efac459caf01b
|
|
BLAKE2b-256 checksum How to use checksums |
c96c8f82b4c72a764277919d277290ea5b1b44213490dcf9ba8e3bff1c7b63d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / changenote-0.2.0-py3-none-macosx_11_0_arm64.whl
| Download URL | changenote-0.2.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 750.6 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
d0b465d9ea36e6d3aad43152371f01dd9ed0ee35b11576d81612ac648bb7db68
|
|
BLAKE2b-256 checksum How to use checksums |
7c81fd741bdeeedc5b15e6357da3817bf1a9f2cc37f4cf4841e79d1cec817aba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / changenote-0.2.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | changenote-0.2.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 768.6 kB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
086f302f2bc2a516e35b34695b3f78ca0cbb163ac7af40099ab5960a75e3ccbf
|
|
BLAKE2b-256 checksum How to use checksums |
a72b5515cebae4d8ddf86c9f8c97fb3e88e2631a874b2cd022be22bc52e347b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|