Grubble
Automatic semantic versioning from conventional commits. Designed to be driven by AI-generated commit messages.
Why
Grubble reads your commit history, applies conventional commit rules, and bumps the version in one command:
feat:→ minor,fix:→ patch,!/BREAKING CHANGE→ major- Optionally writes the new version to
Cargo.tomlorpackage.json - Optionally generates a
CHANGELOG.md(Keep a Changelog format) - Cuts and pushes tags, including floating
v4/v4.1tags for GitHub Actions - Plays well with AI agents that emit conventional commits — see
.github/prompts/sc.prompt.md
Quick Start
# Install
# Make some conventional commits
# Preview the next version
# Release
For CI, jump to GitHub Actions.
Installation
Pre-built binaries
Download the latest release for your platform from GitHub Releases, or grab it directly:
# Linux x86_64
|
# Linux ARM64
|
# macOS Intel
|
# macOS Apple Silicon
|
# Windows (PowerShell)
Cargo
From source
# Binary at target/release/grubble
GitHub Action
- uses: davegarvey/grubble@v4
The Action exposes three outputs for downstream steps:
| Output | Description |
|---|---|
version |
The new version (e.g. 1.2.3). If no bump was needed, this matches previous-version. |
previous-version |
The version before the bump. |
bump-type |
One of major, minor, patch, none. |
When no bump is needed (e.g. only refactor: or chore: commits since the last tag), the Action exits cleanly with bump-type=none rather than failing.
Usage
Every flag has a corresponding option in .versionrc.json (see Configuration). CLI flags override file values.
Configuration
Grubble reads .versionrc.json from the project root. Flags and file values are merged, with flags winning.
| Option | CLI flag | Default | Description |
|---|---|---|---|
preset |
--preset |
git |
Versioning strategy: git, rust, or node. |
packageFiles |
--package-files |
[] |
Comma-separated files to update (for rust / node). |
tagPrefix |
--tag-prefix |
v |
Prefix for git tags. |
commitPrefix |
--commit-prefix |
chore: bump version |
Prefix for the bump commit message. |
tag |
--tag |
false |
Create a git tag for the new version. |
push |
--push |
false |
Push the commit (and tag) to the remote. |
releaseNotes |
--release-notes (-r) |
false |
Include release notes in the tag annotation. Requires tag: true. |
changelog |
--changelog |
false |
Generate or update CHANGELOG.md. |
updateMajorTag |
--update-major-tag |
false |
Maintain a floating v4 tag pointing to the latest v4.x.x. |
updateMinorTag |
--update-minor-tag |
false |
Maintain a floating v4.1 tag pointing to the latest v4.1.x. |
gitUserName |
--git-user-name |
github-actions[bot] |
Identity used for the bump commit when no local git user is configured. |
| — | --git-branch |
"" |
Push the bump to this branch instead of HEAD. See Releasing on Protected Branches. |
gitUserEmail |
--git-user-email |
41898282+github-actions[bot]@users.noreply.github.com |
Email used for the bump commit when no local git user is configured. |
types |
— | see Commit Types | Per-type bump behavior. Valid values: major, minor, patch, none. |
If your repo has a local user.name / user.email set, grubble uses those and ignores gitUserName / gitUserEmail. In CI, set these to match your bot user (e.g. github-actions[bot]).
Versioning Strategies
The preset option controls what files grubble writes.
git(default) — tracks versions via tags only. No files are modified. Use this for monorepos or projects with their own versioning scheme.rust— updates theversionfield inCargo.tomland refreshesCargo.lock. Pairs withcargo publish.node— updates theversionfield inpackage.jsonandpackage-lock.json. Pairs withnpm publish.
When switching from git to a file-based preset, or when a package file is behind the latest tag, grubble first syncs the file to the tag (with a chore: sync package version to v... commit) and then proceeds with the normal bump.
Major / Minor Tag Tracking
When updateMajorTag is enabled, grubble maintains a lightweight tag that follows the latest release in its range:
v4→ latestv4.x.xv4.1→ latestv4.1.x(additionally requiresupdateMinorTag)
This is the convention GitHub Actions use, so consumers can reference uses: owner/repo@v4 and automatically get the latest v4 release.
These tags are force-pushed. Anyone who has them checked out locally will need to re-fetch. Only enable this if you follow semver strictly — a breaking change bumps the major and consumers pinned to the old v4 will pick it up.
Changelog Generation
grubble --changelog writes a CHANGELOG.md in Keep a Changelog format:
feat:→ Addedfix:→ Fixedperf:,refactor:→ Changedrevert:→ Removedsecurity:→ Security- Breaking changes → Changed with a BREAKING: prefix
The changelog is committed as part of the release.
GitHub Actions
The Action downloads a pre-built binary, runs the bump, and exposes the new version as outputs. It is the simplest way to wire grubble into your release flow.
name: Release
on:
pull_request:
types:
branches:
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: davegarvey/grubble@v4
with:
push: true
tag: true
update-major-tag: true
The davegarvey/grubble@v4 pin floats to the latest v4.x.x release. Pin to a specific tag (e.g. @v4.9.4) if you need the release frozen.
Manual setup
Use this when you want to run tests and linting before the bump, or when self-hosted runners can't reach GitHub Releases.
name: Release
on:
pull_request:
types:
branches:
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- run: cargo test
- run: cargo clippy -- -D warnings
- name: Bump version and release
run: |
grubble \
--preset rust \
--push \
--tag \
--update-major-tag \
--git-user-name "github-actions[bot]" \
--git-user-email "41898282+github-actions[bot]@users.noreply.github.com"
CI checklist:
- Grant
contents: writeso the workflow can push commits and tags. - Use
fetch-depth: 0onactions/checkoutso grubble can see the full history. - Pin the major version (
@v4) unless you want the release frozen. - Use the GitHub Action unless you need custom logic between the lint and the bump.
Releasing on Protected Branches
If your main branch is protected (required PRs, required status checks), the default push: true will fail with GH006: Protected branch update failed because GITHUB_TOKEN cannot bypass branch protection.
The recommended pattern is to push the bump to a release branch and open a PR:
name: Release
on:
push:
branches:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: bump
uses: davegarvey/grubble@v5
with:
push: true
tag: true
create-pr: true
auto-merge: true
How it works:
- The action auto-generates a branch name
release/v<version>from the bumped version and pushes the commit there create-pr: trueopens a PR from the release branch to the default branchauto-merge: trueenables squash auto-merge, so the PR merges when branch protection requirements (status checks, reviews) pass
You can also set an explicit branch name (e.g. branch: release for a single always-current release branch) if you prefer not to auto-generate.
Input validation
| Condition | Result |
|---|---|
create-pr: true without push: true |
Error |
auto-merge: true without create-pr: true |
Error |
create-pr: true without branch |
Branch auto-generated as release/v<version> |
Required permissions
| Mode | contents |
pull-requests |
|---|---|---|
Default (push: true only) |
write |
— |
PR flow (create-pr: true) |
write |
write |
Bypass token (advanced)
If you cannot use the PR flow and must push directly to a protected branch, supply a custom token:
- uses: davegarvey/grubble@v5
with:
push: true
token: ${{ secrets.RELEASE_PAT }}
The Action masks the token in logs and rewrites the remote URL with it before grubble runs. The token must have push access and bypass privileges for the protected branch — typically a GitHub App installation token or a fine-grained PAT with contents: write.
Prefer the PR flow whenever possible. Bypassing branch protection skips required reviews and status checks, which undermines the protections you set up.
CI/CD Patterns
Skip the bump when nothing changed
grubble --bump-type prints major, minor, patch, or none. Use it as a gate:
if [; then
fi
grubble --dry-run always exits 0 (success, including no-op). For the "would a bump happen?" signal, use --bump-type.
Read the bump type
grubble --bump-type prints major, minor, patch, or none. Use it to drive conditional logic:
Read the new version
grubble --raw prints the version that would be released without making changes. It honors --preset (e.g. --preset rust reads from Cargo.toml; --preset node reads from package.json; --preset git reads from the latest tag). Pair it with gh release create or a deploy step:
NEW_VERSION=
Machine-readable output
Both --bump-type and --raw accept --output json for stable, machine-readable output:
{
}
{
}
--output json is rejected when combined with the normal run mode or --dry-run (those modes always print human-readable text). Use --output json from CI scripts that need to parse the result instead of shell-substring matching.
How It Works
- Sync package files to the latest tag (for
rust/nodepresets). - Analyze commits since the last tag using conventional commit rules.
- Pick the highest bump (
major/minor/patch/none). - Update package files and
CHANGELOG.mdif configured. - Create the bump commit.
- Create tags, including
v4/v4.1floating tags if enabled. - Push to the remote if configured.
Commit Types
Default mappings (override with the types option in .versionrc.json):
| Commit | Bump |
|---|---|
! or BREAKING CHANGE |
major |
feat: |
minor |
fix: |
patch |
perf:, refactor: |
none by default; usually remapped to patch or minor |
docs:, test:, chore:, ci:, build:, style: |
none |
Custom mapping example:
Troubleshooting
"Author identity unknown"
Set a git identity in the workflow before running grubble:
- run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
The --git-user-name and --git-user-email flags are ignored when a local user.name / user.email is already set.
"grubble: command not found"
Make sure grubble is on PATH. If you used cargo install --root <dir>, add <dir>/bin to PATH.
No version bump on merge
- Confirm the merged commits include
feat:/fix:(or another type mapped to a bump). - Confirm the workflow has
contents: write. - Confirm
actions/checkoutusesfetch-depth: 0.
Invalid config file
Empty or invalid .versionrc.json falls back to defaults with a warning. Run grubble once to see the warning, then fix the JSON.
Package file not found
- Check that
--package-filespoints to files relative to the repo root. - For multi-package repos, pass each file:
--package-files "Cargo.toml,client/Cargo.toml".
Push fails on protected branches (GH006)
GITHUB_TOKEN cannot bypass branch protection. Use the PR flow (branch + create-pr), or supply a bypass token if you must push directly.
"Invalid format" in $GITHUB_OUTPUT
Fixed in v4.9.4 (see #54). Bump the Action to davegarvey/grubble@v4 (movable) to pick up the fix, or pin to a release ≥ v4.9.4 once you have a chance to verify.
Migration from v4
v5.0.0 is a breaking release for the CLI's exit-code contract. The GitHub Action and version.yml workflow are unchanged in behavior; the v5 contract is what they always wanted.
What changed
grubbleexits 0 on success, including when no bump is needed. Previously, exit 1 meant "no bump" — now exit 1 means "error."grubble --rawhonors--preset.--raw --preset rustreads fromCargo.toml;--raw --preset nodereads frompackage.json;--raw --preset gitreads from the latest tag. Previously,--rawalways read from git tags regardless of preset.- New
--output text|jsonflag for--bump-typeand--raw. Use this from CI scripts that need to parse the result. - Action requires release checksums (was warn-and-continue on missing
.sha256). - The Action's "Get current version" step now uses a single
./grubble --raw --preset <preset>call instead of preset-specific shell branches.
If you script against the CLI
Replace exit-code gates with --bump-type checks:
# v4 (broken on v5)
if ; then
fi
# v5
if [; then
fi
Replace --raw exit-code handling with output parsing:
# v4
NEW_VERSION= || NEW_VERSION=
# v5 — exits 0 whenever a version is produced
NEW_VERSION=
Staying on v4
@v4 (the floating major tag) and all @v4.x.x specific tags remain available indefinitely. Pin to @v4 or @v4.9.4 to stay on the v4 contract. The default floating tag shifts to @v5 once v5 ships.
Contributing
See CONTRIBUTING.md for development setup, commit guidelines, and the release process.
License
MIT.
For AI Users
This tool is built for AI-generated commits that follow the conventional commit format. See .github/prompts/sc.prompt.md for a prompt that produces commits compatible with grubble.