prepare-release
SkillDocs & knowledgeLets your agent prepare a software release by writing release notes, bumping version files, and creating a release branch.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the prepare-release skill
About this capability
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.
What this skill tells your AI
The instructions your AI receives, as published by actual-chat/actual-chat in .claude/skills/prepare-release/SKILL.md and read by ahel’s review.
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-editversion.json's version field. Branch name comes fromversion.json'srelease.branchName=release/v{version}.- RoboKitty MCP (
mcp__voxt-robokitty__post_message) posts the announcement — same path/my-changes --postand/robokitty-postuse. 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) feedpromote-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
configssibling repo needs a matchingrelease/vX.Ybranch — CI loads config from it. Locate the clone first (step 3); don't skip step 3b.
Prerequisites
- Clean working tree on
dev(git statusis clean; if not, stop and ask). nbgvavailable. It's a local tool pinned in.config/dotnet-tools.json, so rundotnet tool restoreonce, thendotnet nbgv get-versionto confirm.- You are up to date:
git fetch origin && git switch dev && git pull --ff-only.
Steps
1. Read the target version
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:
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:
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:
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:
[ -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:
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:
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:
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.txtfor 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
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
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:
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(placepmMsV1UVKG)
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:
# 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: . Approve the prod deployment, test the staged builds, then run
/promote-releaseto 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. Letnbgvown 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
configsrelease branch (step 3b). The CI release build loads config fromconfigs'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-releasefail at its first step.
Signals
- GitHub stars
- 55
- Forks
- 10
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
prepare-release- Source
- github.com/actual-chat/actual-chat