# Bump Version

> Bump a native addon package's version in package.json and write its CHANGELOG.md entry. Defaults to a minor bump. Use before /release.

- Skill: `tetherto/bump-version` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tetherto/bump-version`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tetherto/bump-version/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: tetherto (https://skillmd.com/u/tetherto)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tetherto/bump-version

---


# Bump Version

Bump a native addon package's `version` in `package.json` and write the matching `CHANGELOG.md` entry,
so `/release` can publish it. Defaults to a **minor** bump.

`$ARGUMENTS` is `<package-name> [major|minor|patch]` — the package directory name under `packages/`,
plus an optional explicit bump level.

## Usage

```
/bump-version llm-llamacpp              # minor (default): 0.39.0 -> 0.40.0
/bump-version llm-llamacpp patch        # 0.39.0 -> 0.39.1
/bump-version ocr-ggml minor
/bump-version translation-nmtcpp major  # never chosen automatically — must be explicit
```

## When to use this skill

Use it when work has merged to `main` for an addon package and the package still carries the previous
version — i.e. the feature is on `main` but unreleasable because nothing bumped it. This is the routine
gap that `/addon-changelog` and `/release` both refuse to fill:

- `/addon-changelog` writes the changelog but **refuses to bump** — it stops with a
  `VERSION BUMP REQUIRED` banner if `package.json` is unchanged vs `main`.
- `/release` **validates** the bump then stops and tells you to bump first.

Do **not** use it for:

| Case | Use instead |
|---|---|
| SDK/TS pods: `sdk`, `cli`, `rag`, `logging`, `error`, `ai-sdk-provider` | `/sdk-changelog` |
| Back-porting a fix onto a shipped `release-*` line | `/release-patch` |
| A `qvac-fabric` bump across all 7 consumers | `/rollout-phase-b` |
| Publishing an existing bump | `/release` |

## Refusals — stop, do not edit anything

1. **SDK pods.** Their root `CHANGELOG.md` is machine-generated by
   `scripts/sdk/generate-changelog-sdk-pod.cjs` (`rebuildRootChangelog()`); a hand-edit is destroyed on
   the next generator run. Tell the user to run `/sdk-changelog` and stop.
2. **Version not higher than npm `latest`.** Report both numbers and stop.
3. **Working branch is a `release-*` branch.** That is `/release-patch` territory — a release branch
   must stay a byte-exact copy of its base. Stop.
4. **Package has no `CHANGELOG.md`.** Ask the user before creating one.

**`fabric` is NOT a refusal.** An earlier version of this skill refused it, claiming its release read
`packages/fabric/release-notes/v<ver>.md` via `create-github-release-fabric.yml`. That workflow does
not exist and is referenced nowhere; `on-merge-fabric.yml` passes
`changelog-path: packages/fabric/CHANGELOG.md`, and that file is current and correctly bracketed.
`packages/fabric/release-notes/` holds only a `v0.1.0.md` leftover and is not the release source.
Treat `fabric` like any other addon.

## Step 1 — Read the real current version

Never trust the local checkout; it is routinely days or thousands of commits behind.

```bash
git -C <repo> fetch origin main
git -C <repo> show origin/main:packages/<pkg>/package.json
```

Read `version` from that output. Cross-check the published version:

```bash
npm view @qvac/<pkg> dist-tags.latest
```

Expect npm to equal or trail `origin/main`. If `origin/main` is already **ahead** of npm, a bump is
already pending — say so and confirm with the user before adding another.

## Step 2 — Find what changed since the last bump

Resolve the previous release tag (see Step 6 — do **not** guess it from the directory name), then:

```bash
git -C <repo> log <prev-tag>..origin/main --oneline -- packages/<pkg>/
```

If the tag is missing, fall back to the last commit that touched
`packages/<pkg>/package.json` and diff from there. Harvest PR numbers from the commit subjects
(`(#1234)`) for the `### Pull Requests` list, and read the PR titles:

```bash
gh pr view <number> --repo tetherto/qvac --json title,body
```

Drop any PR whose diff does not touch `packages/<pkg>/`. Read the actual source diff for the
user-visible behaviour — do not paraphrase commit messages, which go stale. (A real example: a merged
commit advertised an opt-in env var that did not exist in the merged code.)

## Step 3 — Choose the bump level

Honour an explicit `major|minor|patch` argument. Otherwise default to **minor**, and state the
reasoning before editing. The de-facto rule from this repo's history:

| Change | Level |
|---|---|
| New public API, new config key, new supported model, changed default | **minor** |
| Bug fix only | patch |
| `qvac-fabric` dependency bump | **minor** for `0.x`, **major** for `>=1.0` — see `/rollout-phase-b` |
| Breaking change | **major — never selected automatically** |

**Never auto-select major.** For a package already at major ≥ 1 still default to minor here; a major
bump must be an explicit user decision, passed as the `major` argument.

`translation-nmtcpp` is the only current post-1.0 addon (`9.0.0`). Its history used to be
inconsistent — the same rollout landed as both minor and major — but `8.3.1` → `9.0.0` (#3567), a
dependency-floor alignment with no breaking API change, settled it: post-1.0 packages take the major
on a fabric rollout. `/rollout-phase-b` codifies that and passes `major` explicitly, which satisfies
the never-auto-select rule.

Note that for a 0.x package a minor bump is technically the SemVer breaking-change signal; this repo
leans on that deliberately, so a minor default is correct rather than conservative.

Print the decision before touching files:

```
llm-llamacpp: 0.39.0 -> 0.40.0 (minor)
  reason: adds new supported finetuning architectures (new accepted config values)
```

## Step 4 — Bump `package.json`

Edit `packages/<pkg>/package.json` with the Edit tool, changing **only** the `version` field. Nothing
else in that file, and no other file — in particular never `vcpkg.json` (that is a dependency floor,
not the package version) and never `vcpkg-configuration.json`'s `default-registry.baseline`.

## Step 5 — Write the CHANGELOG entry

Insert at the top of `packages/<pkg>/CHANGELOG.md`, directly below the preamble and above the previous
version heading. The dominant addon shape:

```markdown
## [<version>] - <YYYY-MM-DD>

<optional 1-3 sentence intro — worth it for a feature release, skip for a one-line fix>

### Added

- <bullet; backtick identifiers; use -> for transitions; explain why it matters, not just what changed>

### Pull Requests

- [#3509](https://github.com/tetherto/qvac/pull/3509) - <PR title>
```

Rules:

- **The heading must be bracketed** — literally `## [0.40.0]`. `## 0.40.0`, `##[0.40.0]` and
  `### [0.40.0]` all fail the extractor at `.github/actions/verify-changelog-notes/action.yml`, whose
  awk is `$0 ~ "^## \\[" version "\\]"`. This is the single most common way this task goes wrong.
- Use the date the bump lands, `YYYY-MM-DD`.
- **Section headings are level-3**: `### Added`, `### Changed`, `### Fixed`. Pick by intent — new
  capability is `Added`, altered behaviour or a dependency bump is `Changed`, a defect fix is `Fixed`.
  Do not use the level-2 `## Features` / `## Bug Fixes` style; `/addon-changelog`'s own documentation
  still shows it, but that documentation is stale and no current addon entry uses it.
- The body must be non-empty until the next `^## [`, or the extractor errors with
  `Changelog section for version X is empty`.
- **`## [Unreleased]` blocks** exist in `ocr-ggml`, `tts-ggml` and `decoder-audio`. Insert the new
  dated entry **below** the Unreleased block. Do not fold Unreleased content into your version, and do
  not insert above it — that makes the extractor slice your body off at `## [Unreleased]`. Precedent:
  commit `dfd1fd7eb` on `ocr-ggml`.
- Match the file's existing preamble. Some packages have a Keep-a-Changelog + SemVer preamble, some
  just `# Changelog`. Do not add one that was not there.
- If the version documents work someone else merged, describe the **merged code**, and say so if a
  commit message contradicts it.

## Step 6 — Report what happens next

Look up the git tag by reading `repo_name:` from `.github/workflows/on-merge-<pkg>.yml`. **Never derive
the tag from the directory name** — several are counter-intuitive:

| Package | Tag |
|---|---|
| `llm-llamacpp` | `llamacpp-llm-v<ver>` (reversed) |
| `embed-llamacpp` | `llamacpp-embed-v<ver>` (reversed) |
| `vla-ggml` | `vla-v<ver>` (no `-ggml`) |
| `translation-nmtcpp` | bare `v<ver>` |
| `ocr-ggml` | `ocr-ggml-v<ver>` |
| `classification-ggml` | `classification-ggml-v<ver>` |

Addons get a **git tag only, no GitHub Release** — `on-merge-<pkg>.yml` calls `create-release-tag.yml`
since #2602. Do not promise a Releases-page entry; only the SDK publishes one.

Then tell the user the next step is `/release <pkg>`, which cuts `release-<pkg>-<version>`, dispatches
`on-merge-<pkg>.yml`, and pauses at the human-only `npm` approval gate.

## Step 7 — Verify before committing

```bash
grep -nE "^## \[<version>\]" packages/<pkg>/CHANGELOG.md
git -C <repo> diff --stat origin/main
```

- The `grep` must match. This is the extractor's own regex — if it does not match, the release fails
  later, not now.
- `package.json` version must equal the heading version exactly.
- The diff must show **exactly two files**: `package.json` and `CHANGELOG.md`. Anything else means
  something was swept in.

Then commit on a branch off `origin/main` — never on `main`, never on a `release-*` branch:

```bash
git -C <repo> worktree add -b <TICKET>/bump-<pkg>-<version> <path> origin/main
```

Commit title follows the repo convention `TICKET prefix[tags]: subject`, with `chore` as the prefix
(precedent: `chore: bump llm-llamacpp to 0.38.1`):

```
QVAC-12345 chore: bump <pkg> to <version> for <reason>
```

Add no `[api]` tag unless the public API or config surface actually changed — widening the set of
*accepted values* is not an API change. Add no AI signature or `Co-Authored-By` trailer.

PR body uses the repo's four-section format: problem / how / tested / breaking changes.

## What CI actually enforces

Be accurate about this; two in-repo docs are wrong.

For addons, `release-merge-guard` (`.github/actions/release-merge-guard/`) asserts exactly three
things:

1. the branch matches `release-<slug>-x.y.z`;
2. `package.json` version equals the version in the branch name;
3. `CHANGELOG.md` was **modified** (skipped on initial branch push and on `workflow_dispatch`).

It does **not** validate the heading text for addons, and **no** CI check anywhere compares the new
version against the previous one. So a malformed heading will not fail the guard — it fails `/release`
Step 1, and would fail the release-body extractor for the SDK and the two lib packages that still use
it.

Write the bracketed heading regardless: `/release`, `/addon-changelog`, `/release-patch` and
`/release-fabric-consumers` all assume it.

Do not copy from `ci-validation.mdc:234` (claims the guard checks the heading, and shows an unbracketed
`## x.y.z`) or from `/addon-changelog`'s level-2 section example. Both are stale.

## Bash discipline

Per repo `CLAUDE.md`: no heredocs, no `$()` command substitution, no `&&`/`||`/`;` chaining, no pipes or
redirects. Use Read / Write / Edit / Grep / Glob instead of `cat` / `sed` / `grep` / `find`, and
`git -C <path>` instead of `cd`. Put anything longer than a single command in a script file and run
`bash <file>`.

## Error handling

- **`release-merge-guard` fails on merge** — version not bumped, the branch name does not match the
  version, or `CHANGELOG.md` was not modified.
- **`/release` Step 1 fails** — the changelog heading is missing or unbracketed. Fix the heading.
- **npm publish rejects the version** — it already exists. Check `npm view @qvac/<pkg> versions` and
  pick the next free one.
- Never weaken, skip, or delete a test to get a bump through.

