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"):
$SUMMARY — one line (≤ ~120 chars), plain, what changed. Used in the
tag message and as the commit-body lead.
$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:
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
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
1---2name: release3description: release — bump, commit, tag, push, publish GitHub Release4---56# release — bump, commit, tag, push, publish GitHub Release78Cut a release of AperiSolve. The steps are: bump the version, commit the bump as9`chore(release): X.Y.Z`, push `main`, create and push an annotated tag `X.Y.Z`,10then publish a GitHub Release whose body is **computed from the commits since the11last tag**.1213The user's version request (if any) is: **$ARGUMENTS** — an explicit version14(`3.3.3`), a bump level (`patch` / `minor` / `major`), or empty (infer + confirm).1516> ⚠️ **Pushing the `X.Y.Z` tag triggers `.github/workflows/release.yml`, which17> builds the Docker image AND deploys to production.** Treat this as a18> hard-to-reverse, outward-facing action: you MUST show the computed plan and get19> an explicit go-ahead (step 4) before executing step 5 onward.2021## How this repo versions (facts, don't re-derive)22- Version lives in **`pyproject.toml`** (`[project] version = "X.Y.Z"`) and in23 **`uv.lock`** (under `[[package]] name = "aperisolve"`).24- ⚠️ **Never blind-`sed` `uv.lock`.** Unrelated packages can share the version25 string (e.g. `greenlet` has sat at the same `3.3.x`), so a global replace26 corrupts them. Bump the lockfile with **`uv lock`**, or, if offline, edit only27 the `version = "…"` line that sits directly under `name = "aperisolve"`.28- Tags are **bare `X.Y.Z`** (no `v` prefix), **annotated**, message29 `Release X.Y.Z — <one-line summary>`.30- The release commit contains **only the version bump** (2 files). Feature code is31 expected to already be merged to `main` (via `/commit`).32- Repo slug for compare/release URLs: **`Zeecka/AperiSolve`**.3334## Workflow3536Run in order. **Stop and report** on any problem instead of forcing anything.37Combine independent Bash calls where you can.3839### 1. Preflight40- `git rev-parse --is-inside-work-tree`; confirm `origin` exists (`git remote`).41- `gh auth status` — must be logged in (needed for the GitHub Release).42- Branch: `git branch --show-current`. Releases deploy from `main`, so require43 `main`. If you're **not** on `main`, stop and tell the user to land the code on44 `main` first (e.g. `/commit`) then re-run — do **not** tag a feature branch.45- Sync: `git fetch --tags origin`, then ensure local `main` matches46 `origin/main` (`git pull --ff-only origin main`). If it can't fast-forward,47 stop and report.48- Clean tree: `git status --porcelain` must be empty (the only diff will be the49 bump this skill creates). If dirty, stop — tell the user to commit/stash first.5051### 2. Compute the version and the release text52- **Previous version** = `pyproject.toml` `[project] version` (call it `$PREV`).53 Sanity-check a tag `$PREV` exists: `git rev-parse -q --verify "refs/tags/$PREV"`.54- **Commits to release** = `git log "$PREV"..HEAD --no-merges --format='%h %s'`55 (also skim bodies for `BREAKING CHANGE`). If **empty**, stop: "nothing to56 release since $PREV".57- **Next version `$NEXT`**:58 - If `$ARGUMENTS` is an explicit `X.Y.Z`, use it.59 - If it's `patch`/`minor`/`major`, apply that bump to `$PREV`.60 - If empty, **infer** and propose (confirm in step 4): any `feat` → **minor**;61 only `fix`/`perf`/`refactor`/`chore`/`docs`/`test` → **patch**; any `!` or62 `BREAKING CHANGE` → **major**. This is a suggestion, not a rule — the63 maintainer decides (past `feat` releases have shipped as patches).64 - `$NEXT` must be strictly greater than `$PREV` and not already a tag.65- **Compose two texts from the commits** (this is the "computed content"):66 1. **`$SUMMARY`** — one line (≤ ~120 chars), plain, what changed. Used in the67 tag message and as the commit-body lead.68 2. **`$NOTES`** — the GitHub Release body, markdown, in this repo's house style:69 a themed `##`/`###` heading, grouped bullets that describe user-visible70 changes (not raw commit subjects), and end with the compare link:71 ```72 **Full changelog:** https://github.com/Zeecka/AperiSolve/compare/$PREV...$NEXT73 ```74 Keep it faithful to the actual commits — summarize, don't invent.7576### 3. Bump the version77- Edit `pyproject.toml`: `[project] version = "$PREV"` → `"$NEXT"` (the version78 line under `[project]`, **not** `target-version`).79- Update the lockfile: run `uv lock` (regenerates `uv.lock`, touching only the80 `aperisolve` version). If `uv` is unavailable/offline, edit **only** the81 `version` line directly beneath `name = "aperisolve"` in `uv.lock`.82- Verify the diff is exactly those two files and just the version:83 `git diff --stat` (expect `pyproject.toml`, `uv.lock`) and eyeball84 `git diff -- pyproject.toml uv.lock`. If anything else changed, stop and report.8586### 4. Confirm (mandatory gate — this deploys to prod)87Show the user, and get an explicit go-ahead before proceeding:88- `$PREV` → `$NEXT` and how it was chosen,89- the commit subject `chore(release): $NEXT` + `$SUMMARY` body,90- the tag message `Release $NEXT — $SUMMARY`,91- the full `$NOTES` release body,92- a reminder that pushing the tag builds the image and deploys production.9394If the user asked to proceed non-interactively in this turn, that go-ahead counts.9596### 5. Commit the bump97Use a heredoc so the body + trailer stay intact:98```sh99git commit -aF - <<'EOF'100chore(release): $NEXT101102$SUMMARY103104<session-required trailer lines>105EOF106```107- **Append the trailer this harness session requires** — the `Co-Authored-By:`108 and `Claude-Session:` lines from your current environment's git/commit109 instructions. Read them from the live session; **never hardcode** them (the110 session URL changes each session). If the session specifies none, omit them.111112### 6. Push `main`113- `git push origin main`.114- If **rejected** (non-fast-forward): stop, suggest `git pull --rebase origin115 main` and re-run. **Never** force-push.116117### 7. Tag and push the tag (⇒ triggers build + deploy)118- `git tag -a "$NEXT" -m "Release $NEXT — $SUMMARY"`.119- `git push origin "$NEXT"`.120121### 8. Publish the GitHub Release122- Write `$NOTES` to a temp file (use the session scratchpad dir) and run:123 ```sh124 gh release create "$NEXT" --title "$NEXT" --notes-file <notes-file> --latest125 ```126- The tag already exists on the remote, so `gh` attaches the release to it127 (it won't move or overwrite your annotated tag).128129### 9. Report130- New commit sha + subject, the pushed tag, and the release URL131 (`gh release view "$NEXT" --json url -q .url`).132- Note that the tag push kicked off the Docker build + production deploy, and133 point at Actions to watch it: `gh run list --workflow release.yml --limit 3`.134135## Safety rules (non-negotiable)136- **Confirm before step 5** — the tag push deploys production. No silent releases.137- **Never blind-replace in `uv.lock`** — `uv lock` or the anchored `aperisolve`138 line only.139- Release only from `main`, with a clean, up-to-date tree.140- `$NEXT` must be > `$PREV` and not an existing tag; tags are bare `X.Y.Z`.141- Never force-push; never delete or move an existing tag/branch/release.142- If any step fails, **stop and report** — don't improvise around a failure.143144## Example usage145```146/release # infer the bump from commits since the last tag, then confirm147/release patch # 3.3.2 -> 3.3.3148/release minor # 3.3.2 -> 3.4.0149/release 3.4.0 # explicit version150```