# Release

> release — bump, commit, tag, push, publish GitHub Release

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

---


# release — bump, commit, tag, push, publish GitHub Release

Cut a release of AperiSolve. The steps are: bump the version, commit the bump as
`chore(release): X.Y.Z`, push `main`, create and push an annotated tag `X.Y.Z`,
then publish a GitHub Release whose body is **computed from the commits since the
last tag**.

The user's version request (if any) is: **$ARGUMENTS** — an explicit version
(`3.3.3`), a bump level (`patch` / `minor` / `major`), or empty (infer + confirm).

> ⚠️ **Pushing the `X.Y.Z` tag triggers `.github/workflows/release.yml`, which
> builds the Docker image AND deploys to production.** Treat this as a
> hard-to-reverse, outward-facing action: you MUST show the computed plan and get
> an explicit go-ahead (step 4) before executing step 5 onward.

## How this repo versions (facts, don't re-derive)
- Version lives in **`pyproject.toml`** (`[project] version = "X.Y.Z"`) and in
  **`uv.lock`** (under `[[package]] name = "aperisolve"`).
- ⚠️ **Never blind-`sed` `uv.lock`.** Unrelated packages can share the version
  string (e.g. `greenlet` has sat at the same `3.3.x`), so a global replace
  corrupts them. Bump the lockfile with **`uv lock`**, or, if offline, edit only
  the `version = "…"` line that sits directly under `name = "aperisolve"`.
- Tags are **bare `X.Y.Z`** (no `v` prefix), **annotated**, message
  `Release X.Y.Z — <one-line summary>`.
- The release commit contains **only the version bump** (2 files). Feature code is
  expected to already be merged to `main` (via `/commit`).
- Repo slug for compare/release URLs: **`Zeecka/AperiSolve`**.

## Workflow

Run in order. **Stop and report** on any problem instead of forcing anything.
Combine independent Bash calls where you can.

### 1. Preflight
- `git rev-parse --is-inside-work-tree`; confirm `origin` exists (`git remote`).
- `gh auth status` — must be logged in (needed for the GitHub Release).
- Branch: `git branch --show-current`. Releases deploy from `main`, so require
  `main`. If you're **not** on `main`, stop and tell the user to land the code on
  `main` first (e.g. `/commit`) then re-run — do **not** tag a feature branch.
- Sync: `git fetch --tags origin`, then ensure local `main` matches
  `origin/main` (`git pull --ff-only origin main`). If it can't fast-forward,
  stop and report.
- Clean tree: `git status --porcelain` must be empty (the only diff will be the
  bump this skill creates). If dirty, stop — tell the user to commit/stash first.

### 2. Compute the version and the release text
- **Previous version** = `pyproject.toml` `[project] version` (call it `$PREV`).
  Sanity-check a tag `$PREV` exists: `git rev-parse -q --verify "refs/tags/$PREV"`.
- **Commits to release** = `git log "$PREV"..HEAD --no-merges --format='%h %s'`
  (also skim bodies for `BREAKING CHANGE`). If **empty**, stop: "nothing to
  release since $PREV".
- **Next version `$NEXT`**:
  - If `$ARGUMENTS` is an explicit `X.Y.Z`, use it.
  - If it's `patch`/`minor`/`major`, apply that bump to `$PREV`.
  - If empty, **infer** and propose (confirm in step 4): any `feat` → **minor**;
    only `fix`/`perf`/`refactor`/`chore`/`docs`/`test` → **patch**; any `!` or
    `BREAKING CHANGE` → **major**. This is a suggestion, not a rule — the
    maintainer decides (past `feat` releases have shipped as patches).
  - `$NEXT` must be strictly greater than `$PREV` and not already a tag.
- **Compose two texts from the commits** (this is the "computed content"):
  1. **`$SUMMARY`** — one line (≤ ~120 chars), plain, what changed. Used in the
     tag message and as the commit-body lead.
  2. **`$NOTES`** — the GitHub Release body, markdown, in this repo's house style:
     a themed `##`/`###` heading, grouped bullets that describe user-visible
     changes (not raw commit subjects), and end with the compare link:
     ```
     **Full changelog:** https://github.com/Zeecka/AperiSolve/compare/$PREV...$NEXT
     ```
     Keep it faithful to the actual commits — summarize, don't invent.

### 3. Bump the version
- Edit `pyproject.toml`: `[project] version = "$PREV"` → `"$NEXT"` (the version
  line under `[project]`, **not** `target-version`).
- Update the lockfile: run `uv lock` (regenerates `uv.lock`, touching only the
  `aperisolve` version). If `uv` is unavailable/offline, edit **only** the
  `version` line directly beneath `name = "aperisolve"` in `uv.lock`.
- Verify the diff is exactly those two files and just the version:
  `git diff --stat` (expect `pyproject.toml`, `uv.lock`) and eyeball
  `git diff -- pyproject.toml uv.lock`. If anything else changed, stop and report.

### 4. Confirm (mandatory gate — this deploys to prod)
Show the user, and get an explicit go-ahead before proceeding:
- `$PREV` → `$NEXT` and how it was chosen,
- the commit subject `chore(release): $NEXT` + `$SUMMARY` body,
- the tag message `Release $NEXT — $SUMMARY`,
- the full `$NOTES` release body,
- a reminder that pushing the tag builds the image and deploys production.

If the user asked to proceed non-interactively in this turn, that go-ahead counts.

### 5. Commit the bump
Use a heredoc so the body + trailer stay intact:
```sh
git commit -aF - <<'EOF'
chore(release): $NEXT

$SUMMARY

<session-required trailer lines>
EOF
```
- **Append the trailer this harness session requires** — the `Co-Authored-By:`
  and `Claude-Session:` lines from your current environment's git/commit
  instructions. Read them from the live session; **never hardcode** them (the
  session URL changes each session). If the session specifies none, omit them.

### 6. Push `main`
- `git push origin main`.
- If **rejected** (non-fast-forward): stop, suggest `git pull --rebase origin
  main` and re-run. **Never** force-push.

### 7. Tag and push the tag (⇒ triggers build + deploy)
- `git tag -a "$NEXT" -m "Release $NEXT — $SUMMARY"`.
- `git push origin "$NEXT"`.

### 8. Publish the GitHub Release
- Write `$NOTES` to a temp file (use the session scratchpad dir) and run:
  ```sh
  gh release create "$NEXT" --title "$NEXT" --notes-file <notes-file> --latest
  ```
- The tag already exists on the remote, so `gh` attaches the release to it
  (it won't move or overwrite your annotated tag).

### 9. Report
- New commit sha + subject, the pushed tag, and the release URL
  (`gh release view "$NEXT" --json url -q .url`).
- Note that the tag push kicked off the Docker build + production deploy, and
  point at Actions to watch it: `gh run list --workflow release.yml --limit 3`.

## Safety rules (non-negotiable)
- **Confirm before step 5** — the tag push deploys production. No silent releases.
- **Never blind-replace in `uv.lock`** — `uv lock` or the anchored `aperisolve`
  line only.
- Release only from `main`, with a clean, up-to-date tree.
- `$NEXT` must be > `$PREV` and not an existing tag; tags are bare `X.Y.Z`.
- Never force-push; never delete or move an existing tag/branch/release.
- If any step fails, **stop and report** — don't improvise around a failure.

## Example usage
```
/release            # infer the bump from commits since the last tag, then confirm
/release patch      # 3.3.2 -> 3.3.3
/release minor      # 3.3.2 -> 3.4.0
/release 3.4.0      # explicit version
```

