# Release

> Ship a Vibe release end to end — decide whether server/ needs its own release first, bump the version, PR and squash-merge, start the Windows sign server, run release.yml with signing, promote the prerelease, deploy the website. Use when the user asks to cut, ship or publish a release, or invokes /release.

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

---


# Release

Seven steps. Each has a gate — do not start the next one until the gate is green.
Report the version, the run URLs and the gate results as you go; ask before
skipping a step.

## 0. Preflight

```
git switch main && git pull --ff-only
git status --short          # must be empty
```

## 1. Does `server/` need a release first?

The desktop app downloads a pinned engine, so engine fixes only reach users
through a new server release.

```
pin=$(cat .server-version)                          # e.g. v0.6.9
git log --oneline "server-${pin#v}"..main -- server/ 2>/dev/null ||
  git log --oneline "server-$pin"..main -- server/
```

- **No commits** → skip to step 2, leave `.server-version` alone.
- **Only docs/comments** → say so and ask whether to skip.
- **Code changed** → release the server first:
  - If anything under `server/libs/` changed (`ggml-version`, `patches/`,
    `libs.chore`, `include/`), bump `server/libs/revision` in the same commit and
    run `server-libs.yml` **first** (~9 min); `upload-libs` refuses a tag that
    exists for another tree.
  - `git tag server-vX.Y.Z && git push origin server-vX.Y.Z` on main.
  - **Gate:** `server-release.yml` green (~8 min) and all 5 assets uploaded — the
    Intel Mac asset lands last. It publishes as a *prerelease* on purpose; leave
    it that way so Latest stays with the app.

## 2. Bump and write the notes

Three files, one commit:

- `desktop/src-tauri/tauri.conf.json` → `"version"`.
- `.server-version` → the new server tag minus `server-` (only if step 1 released one).
- `i18n/changelog/<version>.md`. **release.yml fails without it.**

If that file does not exist, write it. Read what actually shipped —
`git log --oneline <previous tag>..main` and the PR bodies — and write the entry
the way its neighbours are written: frontmatter (`version`, `date`, `title`), then
`## New` / `## Improved` / `## Fixed` sections of `- 🎯 **Term** — what changed`
bullets, keeping the `(#1234)` refs and `@handle` credits. Read the last two
entries first and match their voice: plain, concrete, user-visible. A refactor
nobody can see does not get a bullet.

Then translate it: invoke the `translate` skill, which fans one cheap subagent per
locale over the new file and stamps the hashes. Untranslated is not fatal — the
page falls back to English per entry — but the notes are the one page every user
opens on update day, so translate them unless the user says otherwise.

**Gate:** `uv run scripts/check_i18n.py` passes and `chore check-types` is clean.

## 3. PR and squash merge

Commit title style: `Bump version to X, and <feature> (#PR)`.

```
git switch -c release/vX.Y.Z && git commit && git push -u origin HEAD
gh pr create --fill
```

**Gate:** `fmt` and `clippy` pass, then `gh pr merge <n> --squash --delete-branch`.
Repo-level auto-merge is **off** (`allow_auto_merge: false`), so `--auto` silently
does nothing — poll the checks and merge when they settle. Then `git switch main
&& git pull`.

## 4. Start the sign server

The Windows job reaches a YubiKey on this machine through a Cloudflare tunnel.

```
uv run --env-file .env.local scripts/sign.py serve      # background
curl "$TUNNEL_URL/"                                     # {"status":"ok"}
```

Values live only in `.env.local` (gitignored) — pass the file, never print or copy
the values. The server prints a ready-to-paste `export SIGN_TUNNEL_SECRET=…` line
at startup, so whatever you redirect its output to holds a live secret: keep that
log outside the repo and delete it when the release is done.

Check the tunnel with **curl**, not urllib or requests — Cloudflare answers a
non-browser user-agent with `403 error code: 1010`, which looks exactly like a dead
tunnel.

**Gate:** the curl returns `{"status":"ok"}`. Never trigger step 5 before it does —
an unsigned Windows build ships a SmartScreen warning to every user.

## 5. Release

```
gh workflow run release.yml --ref main -f sign-windows=true -f sign-macos=true
```

~20 min. tauri-action creates the tag `vX.Y.Z` and a **prerelease**.

**Gate:** all 5 platform jobs green, and `[sign] OK: vibe.exe` in the Windows job
log. A single failed platform can be re-run with `gh run rerun <id> --failed`.

## 6. Promote

```
gh release edit vX.Y.Z --prerelease=false --latest
```

Do not write release notes — the body is already a link to
`https://thewh1teagle.github.io/vibe/changelog/<version>`, which is where the notes
live, in the reader's language.

## 7. Deploy the website and stop the sign server

```
gh workflow run website.yml --ref main
pkill -f 'sign.py serve'
```

The website also deploys on `release: published`, but trigger it explicitly so the
changelog page carries the new entry without waiting.

**Gate:** `website.yml` green, and `https://thewh1teagle.github.io/vibe/changelog/<version>`
serves the new entry.

