grubble 5.2.2

Automatic semantic versioning based on conventional commits, optimized for AI-generated commit messages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# Grubble

[![CI](https://github.com/davegarvey/grubble/actions/workflows/ci.yml/badge.svg)](https://github.com/davegarvey/grubble/actions/workflows/ci.yml)
[![Version & Release](https://github.com/davegarvey/grubble/actions/workflows/version.yml/badge.svg)](https://github.com/davegarvey/grubble/actions/workflows/version.yml)
[![Version](https://img.shields.io/github/v/release/davegarvey/grubble)](https://github.com/davegarvey/grubble/releases)
[![Rust](https://img.shields.io/badge/rust-stable-orange)](https://www.rust-lang.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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.toml` or `package.json`
- Optionally generates a `CHANGELOG.md` (Keep a Changelog format)
- Cuts and pushes tags, including floating `v4` / `v4.1` tags for GitHub Actions
- Plays well with AI agents that emit conventional commits — see [`.github/prompts/sc.prompt.md`].github/prompts/sc.prompt.md

## Quick Start

```bash
# Install
cargo install grubble

# Make some conventional commits
git commit -m "feat: add login"
git commit -m "fix: handle empty input"

# Preview the next version
grubble --dry-run

# Release
grubble --push --tag
```

For CI, jump to [GitHub Actions](#github-actions).

## Installation

### Pre-built binaries

Download the latest release for your platform from [GitHub Releases](https://github.com/davegarvey/grubble/releases), or grab it directly:

```bash
# Linux x86_64
curl -L https://github.com/davegarvey/grubble/releases/latest/download/grubble-linux-x86_64.tar.gz | tar xz
sudo mv grubble /usr/local/bin/

# Linux ARM64
curl -L https://github.com/davegarvey/grubble/releases/latest/download/grubble-linux-aarch64.tar.gz | tar xz
sudo mv grubble /usr/local/bin/

# macOS Intel
curl -L https://github.com/davegarvey/grubble/releases/latest/download/grubble-macos-x86_64.tar.gz | tar xz
sudo mv grubble /usr/local/bin/

# macOS Apple Silicon
curl -L https://github.com/davegarvey/grubble/releases/latest/download/grubble-macos-aarch64.tar.gz | tar xz
sudo mv grubble /usr/local/bin/

# Windows (PowerShell)
Invoke-WebRequest -Uri https://github.com/davegarvey/grubble/releases/latest/download/grubble-windows-x86_64.zip -OutFile grubble.zip
Expand-Archive grubble.zip
```

### Cargo

```bash
cargo install grubble
```

### From source

```bash
git clone https://github.com/davegarvey/grubble.git
cd grubble
cargo build --release
# Binary at target/release/grubble
```

### GitHub Action

```yaml
- 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

```bash
grubble                      # bump based on commits since the last tag
grubble --push               # push the bump commit
grubble --tag                # create a git tag for the new version
grubble --changelog          # generate or update CHANGELOG.md
grubble --update-major-tag   # also maintain a floating v4 tag
grubble --raw                # print the new version, no changes
grubble --dry-run            # preview the bump without applying it
grubble --bump-type          # print major | minor | patch | none
grubble --output json        # emit machine-readable output (with --raw or --bump-type)
grubble --quiet              # suppress the commit list
grubble --git-branch release/v0.35.0  # push the bump to a release branch (works on protected branches)
grubble --help               # full flag reference
```

Every flag has a corresponding option in `.versionrc.json` (see [Configuration](#configuration)). CLI flags override file values.

## Configuration

Grubble reads `.versionrc.json` from the project root. Flags and file values are merged, with flags winning.

```json
{
  "preset": "rust",
  "packageFiles": ["Cargo.toml"],
  "tagPrefix": "v",
  "commitPrefix": "chore(release): bump",
  "tag": true,
  "push": true,
  "changelog": true
}
```

| 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]#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]#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 the `version` field in `Cargo.toml` and refreshes `Cargo.lock`. Pairs with `cargo publish`.
- **`node`** — updates the `version` field in `package.json` and `package-lock.json`. Pairs with `npm 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` → latest `v4.x.x`
- `v4.1` → latest `v4.1.x` (additionally requires `updateMinorTag`)

This is the convention GitHub Actions use, so consumers can reference `uses: owner/repo@v4` and automatically get the latest v4 release.

```bash
grubble --tag --push --update-major-tag --update-minor-tag
```

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](https://keepachangelog.com/en/1.1.0/) format:

- `feat:`**Added**
- `fix:`**Fixed**
- `perf:`, `refactor:`**Changed**
- `revert:`**Removed**
- `security:`**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.

```yaml
name: Release
on:
  pull_request:
    types: [closed]
    branches: [main]

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.

```yaml
name: Release
on:
  pull_request:
    types: [closed]
    branches: [main]

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: write` so the workflow can push commits and tags.
- Use `fetch-depth: 0` on `actions/checkout` so 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:

```yaml
name: Release
on:
  push:
    branches: [main]

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: true` opens a PR from the release branch to the default branch
- `auto-merge: true` enables 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:

```yaml
- 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:

```bash
if [ "$(grubble --bump-type --preset rust)" != "none" ]; then
  grubble --push --tag --preset rust
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:

```bash
case "$(grubble --bump-type)" in
  major) notify "breaking release" ;;
  minor) notify "feature release" ;;
  patch) : ;;                  # silent patch
  none)   exit 0 ;;
esac
```

### 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:

```bash
NEW_VERSION=$(grubble --raw --preset rust)
gh release create "v$NEW_VERSION" --title "Release v$NEW_VERSION"
```

### Machine-readable output

Both `--bump-type` and `--raw` accept `--output json` for stable, machine-readable output:

```bash
$ grubble --bump-type --output json
{
  "bump_type": "minor",
  "current_version": "1.2.3",
  "triggering_commits": ["Minor: feat: add login"],
  "unknown_commits": []
}

$ grubble --raw --preset rust --output json
{
  "version": "1.2.3",
  "preset": "rust"
}
```

`--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

1. Sync package files to the latest tag (for `rust` / `node` presets).
2. Analyze commits since the last tag using conventional commit rules.
3. Pick the highest bump (`major` / `minor` / `patch` / `none`).
4. Update package files and `CHANGELOG.md` if configured.
5. Create the bump commit.
6. Create tags, including `v4` / `v4.1` floating tags if enabled.
7. 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:

```json
{
  "types": {
    "perf": "minor",
    "revert": "none"
  }
}
```

## Troubleshooting

**"Author identity unknown"**

Set a git identity in the workflow before running grubble:

```yaml
- 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/checkout` uses `fetch-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-files` points 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](#releasing-on-protected-branches) (`branch` + `create-pr`), or supply a [bypass token](#bypass-token-advanced) if you must push directly.

**"Invalid format" in `$GITHUB_OUTPUT`**

Fixed in v4.9.4 (see [#54](https://github.com/davegarvey/grubble/issues/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

- **`grubble` exits 0 on success**, including when no bump is needed. Previously, exit 1 meant "no bump" — now exit 1 means "error."
- **`grubble --raw` honors `--preset`**. `--raw --preset rust` reads from `Cargo.toml`; `--raw --preset node` reads from `package.json`; `--raw --preset git` reads from the latest tag. Previously, `--raw` always read from git tags regardless of preset.
- **New `--output text|json` flag** for `--bump-type` and `--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:

```bash
# v4 (broken on v5)
if grubble --dry-run --preset rust; then
  grubble --push --tag --preset rust
fi

# v5
if [ "$(grubble --bump-type --preset rust)" != "none" ]; then
  grubble --push --tag --preset rust
fi
```

Replace `--raw` exit-code handling with output parsing:

```bash
# v4
NEW_VERSION=$(./grubble --raw --preset rust) || NEW_VERSION=$(./grubble --raw --preset rust || echo "0.0.0")

# v5 — exits 0 whenever a version is produced
NEW_VERSION=$(./grubble --raw --preset rust)
```

### 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](CONTRIBUTING.md) for development setup, commit guidelines, and the release process.

## License

[MIT](LICENSE).

## For AI Users

This tool is built for AI-generated commits that follow the conventional commit format. See [`.github/prompts/sc.prompt.md`](.github/prompts/sc.prompt.md) for a prompt that produces commits compatible with grubble.