Cut a new Voxt release. Use when the user says "prepare a release", "cut a release", "release Voxt", "do a release", "/prepare-release", or wants to run nbgv prepare-release, bump the version, generate end-user release notes, and announce them in the Releases chat.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add Actual-Chat/actual-chat --skill prepare-release --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Prepare Release?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/actual-chat-prepare-release)More formats (shields.io, HTML) on the badges page.
---
name: prepare-release
description: |
Cut a new Voxt release. Use when the user says "prepare a release", "cut a
release", "release Voxt", "do a release", "/prepare-release", or wants to run
nbgv prepare-release, bump the version, generate end-user release notes, and
announce them in the Releases chat.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, mcp__voxt-robokitty__post_message, mcp__voxt-robokitty__list_place_chats
---
# prepare-release
Cut a Voxt release end to end: bump the version with `nbgv`, push `dev` + the
new `release/vX.Y` branch, generate **end-user** release notes from the commit
log, commit them to `docs/releases/`, and announce them in the **Releases**
chat via RoboKitty.
This is a release action that pushes branches and posts publicly. Do the git
mutations without stopping between mechanical steps, but **pause once** to let
the user review the drafted release notes before the notes commit and the
RoboKitty post (steps 6–8). Those are the hard-to-revert, outward-facing parts.
## Reuse
- **`nbgv`** (Nerdbank.GitVersioning) drives all versioning — never hand-edit
`version.json`'s version field. Branch name comes from `version.json`'s
`release.branchName` = `release/v{version}`.
- **RoboKitty MCP** (`mcp__voxt-robokitty__post_message`) posts the announcement
— same path `/my-changes --post` and `/robokitty-post` use. No new HTTP code.
- Release-notes **style** is defined below; match prior notes' tone, don't
reinvent a format.
- **Store notes** (`docs/releases/store-notes-vX.Y.txt`) feed
`promote-release.yml` — the same file is pushed to Google Play, the App Store
and the Microsoft Store, so it's a plain-text distillation of the notes.
- The **`configs`** sibling repo needs a matching `release/vX.Y` branch — CI
loads config from it. Locate the clone first (step 3); don't skip step 3b.
## Prerequisites
- Clean working tree on `dev` (`git status` is clean; if not, stop and ask).
- `nbgv` available. It's a **local** tool pinned in `.config/dotnet-tools.json`,
so run `dotnet tool restore` once, then `dotnet nbgv get-version` to confirm.
- You are up to date: `git fetch origin && git switch dev && git pull --ff-only`.
## Steps
### 1. Read the target version
```bash
cat version.json # e.g. "version": "2.13-alpha"
```
The release version is that value with any `-alpha`/prerelease tag dropped
(`2.13-alpha` → **2.13**). Call it `X.Y`. The new branch will be `release/vX.Y`.
After `prepare-release`, `dev` bumps to the next minor (`2.14-alpha`).
### 2. Run prepare-release
On `dev`:
```bash
dotnet nbgv prepare-release
```
This creates `release/vX.Y` (version set to `X.Y`) and advances `dev` to the
next `-alpha`, committing on both. Verify:
```bash
git branch --list 'release/v*' | tail; git log --oneline -1 dev
```
### 3. Cut and push the configs release branch FIRST
**The CI build loads configuration from the `configs` repo's `release/vX.Y`
branch, so it must exist before you push the app's release branch — otherwise
the release build the app-branch push triggers fails for lack of config.** Its
remote is `git@github.com:Actual-Chat/configs.git`.
**Find the actual clone first — do not assume a path or a folder name.** It
lives beside this repo, but the folder may be called `configs`,
`ActualChat.configs`, `actual-chat-configs`, or anything similar, and the
project root differs per environment (`/proj` in the AgentCli Docker container,
`D:\Projects` on Windows, `~/Projects` on macOS — see `AC_ProjectRoot`). Match
on the **remote URL**, which is the only stable identifier:
```bash
root="${AC_ProjectRoot:-$(cd "$(git rev-parse --show-toplevel)/.." && pwd)}"
configs=""
for d in "$root"/*/; do
url=$(git -C "$d" remote get-url origin 2>/dev/null) || continue
case "${url%.git}" in *[:/]Actual-Chat/configs) configs="${d%/}"; break;; esac
done
[ -n "$configs" ] || { git clone git@github.com:Actual-Chat/configs.git "$root/configs" && configs="$root/configs"; }
echo "configs repo: $configs"
```
Match the remote path **exactly** (after stripping `.git`), not as a
substring — sibling roots hold near-miss repos that a loose pattern grabs
instead. The `[:/]` class covers both remote forms
(`git@github.com:Actual-Chat/configs.git` and
`https://github.com/Actual-Chat/configs`).
**Verify `$configs` is non-empty before using it.** `git -C ""` silently
operates on the current directory, so an empty variable turns every command
below into one that rewrites branches in the *app* repo:
```bash
[ -n "$configs" ] && git -C "$configs" rev-parse --show-toplevel || { echo "configs repo not found — stop"; }
```
Then create `release/vX.Y` from the latest `master` and push. Use
`git -C "$configs"` rather than `cd`, so later steps aren't left running from
the wrong directory — the non-empty check above is what actually protects the
app repo:
```bash
git -C "$configs" fetch origin
git -C "$configs" switch master && git -C "$configs" pull --ff-only
git -C "$configs" switch -c release/vX.Y # skip if it already exists
git -C "$configs" push -u origin release/vX.Y
```
If the SSH remote can't authenticate in this environment, push over HTTPS with
the token instead:
`git -C "$configs" push "https://x-access-token:${GH_TOKEN}@github.com/Actual-Chat/configs.git" release/vX.Y`.
If `release/vX.Y` already exists on origin and equals `origin/master`, it's
already done — leave it.
Leave the clone on `master` when you're done. `git switch -c` without `-u`
leaves the new branch untracked, and a checkout parked on a release branch is
how prompt edits meant for `dev` end up on the wrong branch — `dev` CI reads
configs `master`, release branches read their own.
### 3b. Push the app branches
Only after the configs branch is live:
```bash
git push origin dev
git push origin release/vX.Y
```
### 4. Get the one-line commit log since the previous release
The previous release branch is `release/vX.(Y-1)` on origin (e.g. `release/v2.12`).
Collect subjects for the notes-writing input:
```bash
git log --format='%s' origin/release/vX.(Y-1)..release/vX.Y > /tmp/release-commits-vX.Y.txt
wc -l /tmp/release-commits-vX.Y.txt
```
Ignore the housekeeping lines (`Set version to …`, `Merge branch …`, AOT/AGENTS
regeneration, pure `test:`/`build:`/`chore:` churn) when writing notes — they're
not user-facing.
### 5. Write end-user release notes
Read the full commit log and distill it into **end-user** notes. This is the
core judgment step — see **Release-notes style** below. Draft the file at
`docs/releases/release-notes-vX.Y.md`.
### 5b. Write the store notes
Distill the notes once more into `docs/releases/store-notes-vX.Y.txt`. This
file goes **verbatim** into Google Play's "What's new", the App Store's
"What's New" and the Microsoft Store's release notes when the release is
promoted (`/promote-release`), so:
- **500 characters max** including newlines — Play's limit, and the promote
workflow refuses a longer file. Check with `wc -m`.
- Plain text: no markdown, no bold, no headings. Emoji-free except `•` bullets.
- First line: one sentence with the release theme. Then 4–6 `•` bullets, the
biggest user-visible wins first, one line each.
- Same voice as the notes, no sign-off. See `store-notes-v2.17.txt` for the
shape.
**STOP and show both drafts (notes + store notes) to the user for review
before committing.** Incorporate any edits they ask for.
### 6. Commit the notes on the release branch
```bash
git switch release/vX.Y
# write docs/releases/release-notes-vX.Y.md and store-notes-vX.Y.txt
git add docs/releases/release-notes-vX.Y.md docs/releases/store-notes-vX.Y.txt
git commit -m "docs: add release notes vX.Y"
git push origin release/vX.Y
```
### 7. Merge the notes into dev, push dev
```bash
git switch dev
git merge --no-ff release/vX.Y -m "Merge release notes vX.Y into dev"
git push origin dev
```
**Usually this merges cleanly** — git's `ort` strategy keeps dev's newer
`version.json`, so the only change that lands on dev is the notes file. Verify
after: `grep '"version"' version.json` should still show the next `-alpha`.
Only if `version.json` **does** conflict (release branch has `X.Y`, dev has the
next `-alpha`), resolve by keeping dev's version before pushing:
```bash
git checkout --ours version.json && git add version.json
# resolve any other conflicts by keeping dev's side unless it's the notes file
git commit --no-edit
git push origin dev
```
If the only thing you actually need on `dev` is the notes files and the merge
is noisy, the equivalent clean alternative is:
`git switch dev && git checkout release/vX.Y -- docs/releases/release-notes-vX.Y.md docs/releases/store-notes-vX.Y.txt && git commit -m "docs: add release notes vX.Y" && git push origin dev`.
`/promote-release` dispatches `promote-release.yml` on `release/vX.Y` and
reads `docs/releases/store-notes-vX.Y.txt` from there, so step 6 is what
makes the promotion possible; this merge just keeps `dev` complete.
### 8. Announce in the Releases chat via RoboKitty
Only if the RoboKitty MCP is available (`mcp__voxt-robokitty__post_message`
tool present). The **Releases** chat is:
- URL: `https://voxt.ai/chat/s-pmMsV1UVKG-dCKQXnYpX9`
- `chatId`: `s-pmMsV1UVKG-dCKQXnYpX9` (place `pmMsV1UVKG`)
Post the release notes wrapped in a fenced code block so they render verbatim:
```
mcp__voxt-robokitty__post_message(
chatId = "s-pmMsV1UVKG-dCKQXnYpX9",
text = "```\n<the full release-notes markdown>\n```"
)
```
Confirm with a one-liner: `Posted release notes vX.Y → Releases (LID: <id>).`
On any MCP failure, surface the error verbatim and stop — don't retry blindly.
**If the `mcp__voxt-robokitty__*` tools aren't loaded this session** (common —
the server is declared in `.mcp.json` but not always auto-connected), call the
HTTP endpoint directly instead of asking the user to paste. It's the same
RoboKitty server over plain JSON-RPC, authed with `ActualChat_RoboKitty_API_Key`:
```bash
# wrap the committed notes in a code fence
{ echo '```'; cat docs/releases/release-notes-vX.Y.md; echo '```'; } > /tmp/rk-text.txt
# build the JSON-RPC body with jq (safe escaping of backticks/emoji/newlines)
jq -n --rawfile t /tmp/rk-text.txt \
'{jsonrpc:"2.0",id:2,method:"tools/call",params:{name:"post_message",
arguments:{chatId:"s-pmMsV1UVKG-dCKQXnYpX9",text:$t}}}' > /tmp/rk-body.json
curl -s -X POST "https://voxt.ai/api/mcp" \
-H "Authorization: Bearer ${ActualChat_RoboKitty_API_Key}" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data-binary @/tmp/rk-body.json
```
The response is an SSE `data:` line; success looks like
`"structuredContent":{"result":<LID>}`. Report that LID. (Use the prod key/URL
above; `ActualChat_RoboKitty_Dev_API_Key` + `https://dev.voxt.ai/api/mcp` target
the dev instance.) Only if neither the tool nor the key is available, print the
notes for the user to paste manually.
### 9. Hand off to the promotion
The `release/vX.Y` push triggers the release build. Its prod deploy jobs wait
for a Maintainers approval in the `prod` environment, then only **stage** the
apps: Play internal track, TestFlight (iOS + Mac), a pending Microsoft Store
submission. Nothing reaches end users yet. Tell the user:
> Release build: <run url>. Approve the prod deployment, test the staged
> builds, then run `/promote-release` to publish them.
## Release-notes style
The notes are for **end users**, not engineers. Translate commits into user
value; drop implementation detail entirely.
- **Header:** `**🎉 Voxt vX.Y is here! 🎉**` then a one/two-sentence summary of
the release's theme.
- **Body:** grouped sections under bold headings with an emoji, e.g.
`**🚀 What's New**`, then feature blocks. Use short prose or tight bullet
lists. Lead with the biggest user-visible wins.
- Group many small commits into one plain-language line ("Dozens of small
fixes — quiet polish across UI, animations, and edge cases").
- No commit hashes, file names, class names, RPC/codec/internal terms.
- Roughly one screenful; short releases can be ~15 lines, big ones ~40.
- **Footer:** a warm thanks + `— The Voxt.ai team 🎧`.
Keep the voice friendly and confident. When in doubt about whether a change is
worth mentioning, ask: "would a user notice or care?" If no, fold it into the
"small fixes" line or drop it.
## Quick reference
| Step | Command / action |
|---|---|
| Target version | `cat version.json` → drop `-alpha` → `X.Y` |
| Bump | `dotnet nbgv prepare-release` (on `dev`) |
| Config branch (first!) | configs clone (locate by remote URL, step 3): `release/vX.Y` from latest `master`, push — CI loads it |
| Push app branches | `git push origin dev && git push origin release/vX.Y` (after config branch) |
| Commit log | `git log --format='%s' origin/release/vX.(Y-1)..release/vX.Y` |
| Notes file | `docs/releases/release-notes-vX.Y.md` |
| Store notes | `docs/releases/store-notes-vX.Y.txt` — plain text, ≤ 500 chars, committed with the notes |
| Merge to dev | `git merge --no-ff release/vX.Y`, keep dev's `version.json` |
| Announce | `mcp__voxt-robokitty__post_message` → `s-pmMsV1UVKG-dCKQXnYpX9`, code-fenced |
| Publish to stores | not here — `/promote-release` after the builds are tested |
## Common mistakes
- **Hand-editing `version.json`'s version.** Let `nbgv` own it. Only ever
resolve it in the merge by keeping dev's side.
- **Copying commit subjects into the notes.** Notes are user value, not a
changelog. Rewrite everything.
- **Forgetting the code fence** in the RoboKitty post — the notes must be inside
triple-backticks so markdown renders literally.
- **Wrong previous-release branch** for the diff → notes miss or double-count
commits. Confirm `origin/release/vX.(Y-1)` is the actual prior release.
- **Skipping the review pause.** The notes are public; show them first.
- **Forgetting the `configs` release branch (step 3b).** The CI release build
loads config from `configs`' `release/vX.Y`; without it the build fails.
- **Skipping the store notes, or writing them in markdown.** They're pasted
verbatim into three store listings; a missing or 500+ character file makes
`/promote-release` fail at its first step.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!