|
| 1 | +--- |
| 2 | +name: beta-release-prep |
| 3 | +description: Prepare the BETA branch for release into main. Use when the user wants to "prep beta", "cut a release", "get beta ready for main", or "do release prep". Researches every commit between the last release tag and BETA to verify the CHANGELOG is complete and to build release notes, creates a release branch off BETA, flips the eye-in-the-sky coach plugin from the BETA git dependency to the published production npm package, sets the version in package.json to match the topmost unreleased CHANGELOG heading, dates that heading to today, runs the green-before-merge checks, opens a PR into BETA, ensures a BETA→main PR exists whose body is copy-paste-ready release notes, and opens a follow-up "flip coach back to BETA" PR to merge after release. Documents the manual post-release tag + back-merge of main into BETA. Never creates tags. |
| 4 | +--- |
| 5 | + |
| 6 | +# Beta → Main Release Prep |
| 7 | + |
| 8 | +Automates the release-prep checklist for Dove's DataViewer / LapWing: get the |
| 9 | +`BETA` branch ready to merge into `main`. The headline gotcha this skill exists |
| 10 | +to catch: **the coach plugin (`eye-in-the-sky`) must be on the published |
| 11 | +production npm package on `main`, but during BETA development it points at the |
| 12 | +coach repo's git branch — and people forget to flip it.** |
| 13 | + |
| 14 | +> Read `CLAUDE.md` → the "⚠️ SUPER IMPORTANT — coach source differs by branch" |
| 15 | +> block before running this. This skill operationalizes that block. |
| 16 | +
|
| 17 | +## The two coach-source states (the only lines that differ between BETA and main) |
| 18 | + |
| 19 | +| | `package.json` dependency | `vite.config.ts` `DEFAULT_PLUGIN_PACKAGES` | |
| 20 | +|---|---|---| |
| 21 | +| **Production (main)** | `"@perchwerks/eye-in-the-sky": "~<x.y.z>"` (tilde-pinned published release) | `"@perchwerks/eye-in-the-sky"` | |
| 22 | +| **BETA (dev)** | `"@theangryraven/eye-in-the-sky": "github:TheAngryRaven/DataViewer_coach#BETA"` | `"@theangryraven/eye-in-the-sky"` | |
| 23 | + |
| 24 | +These two lines — `package.json` and `vite.config.ts` line ~156 — are the |
| 25 | +**only** ones that change between the states. Nothing else about the coach moves. |
| 26 | + |
| 27 | +## Hard rules |
| 28 | + |
| 29 | +- **NEVER create or push a git tag.** Tagging from Claude errors out. The |
| 30 | + maintainer tags the release manually after merge. Do not run `git tag` or |
| 31 | + `git push --tags`. |
| 32 | +- **NEVER push to `BETA` or `main` directly.** All changes go through PRs. |
| 33 | +- Work happens on a fresh branch cut from the current `origin/BETA`. |
| 34 | +- The skill is **idempotent**: if the coach is already on production, leave it; |
| 35 | + if the CHANGELOG is already dated, don't re-date it. Report what was already |
| 36 | + correct rather than forcing a no-op change. |
| 37 | + |
| 38 | +## Procedure |
| 39 | + |
| 40 | +### 0. Sync and branch |
| 41 | + |
| 42 | +```bash |
| 43 | +git fetch --all --prune --tags |
| 44 | +``` |
| 45 | + |
| 46 | +Cut the release branch from `origin/BETA` (not from your current HEAD): |
| 47 | + |
| 48 | +```bash |
| 49 | +git switch -c release/v<X.Y.Z> origin/BETA # name decided in step 2 |
| 50 | +``` |
| 51 | + |
| 52 | +If you must name the branch before knowing the version, use a placeholder like |
| 53 | +`release/beta-prep` and rename once the version is known, or just keep the |
| 54 | +descriptive name you were given. The important part is that it is based on |
| 55 | +`origin/BETA` and contains a clean tree. |
| 56 | + |
| 57 | +### 0.5. Research the commits since the last release |
| 58 | + |
| 59 | +Releases are tagged on `main` (e.g. `v2.9.1`). The maintainer tags manually |
| 60 | +after merge, so the **latest tag is the last shipped version**. Diff every commit |
| 61 | +between it and `BETA` — this drives both a CHANGELOG-completeness check and the |
| 62 | +release notes in step 6. |
| 63 | + |
| 64 | +```bash |
| 65 | +LAST_TAG=$(git tag --sort=-v:refname | head -1) |
| 66 | +echo "Last released tag: $LAST_TAG" |
| 67 | +git log "$LAST_TAG"..origin/BETA --oneline # full commit list |
| 68 | +git log "$LAST_TAG"..origin/BETA --oneline --merges # merged PRs (often the cleanest summary) |
| 69 | +git diff "$LAST_TAG"..origin/BETA --stat # what changed, by file |
| 70 | +``` |
| 71 | + |
| 72 | +> The tag lives on `main` and (due to the back-merge model — see "Post-release |
| 73 | +> back-merge" below) may **not** be an ancestor of `BETA`. That's fine: the |
| 74 | +> tagged commit still has `BETA`'s previously-released history as ancestors, so |
| 75 | +> `"$LAST_TAG"..origin/BETA` correctly lists only the *new* commits since release. |
| 76 | +
|
| 77 | +**Use this to verify the CHANGELOG is complete.** Walk the merged-PR / commit |
| 78 | +list and confirm every user-facing change is reflected under the topmost |
| 79 | +`## [X.Y.Z] - unreleased` heading. If something user-facing is missing, add it to |
| 80 | +that section before continuing (the changelog is part of the change — Golden Rule |
| 81 | +4). Internal-only commits (CI, tests, refactors with no user impact) don't need a |
| 82 | +changelog line. Keep the raw `git log` output around — step 6 reuses it. |
| 83 | + |
| 84 | +### 1. Flip the coach plugin to production |
| 85 | + |
| 86 | +Inspect the current state first: |
| 87 | + |
| 88 | +```bash |
| 89 | +grep -n "eye-in-the-sky" package.json |
| 90 | +grep -n "DEFAULT_PLUGIN_PACKAGES" vite.config.ts |
| 91 | +``` |
| 92 | + |
| 93 | +If it already reads `@perchwerks/eye-in-the-sky` in **both** files, it's already |
| 94 | +production — note that and skip the edits. Otherwise: |
| 95 | + |
| 96 | +1. In `package.json`, replace the BETA git dependency line |
| 97 | + ```json |
| 98 | + "@theangryraven/eye-in-the-sky": "github:TheAngryRaven/DataViewer_coach#BETA" |
| 99 | + ``` |
| 100 | + with the tilde-pinned published release |
| 101 | + ```json |
| 102 | + "@perchwerks/eye-in-the-sky": "~<x.y.z>" |
| 103 | + ``` |
| 104 | + Determine `<x.y.z>` — the published npm version to ship. If the user gave a |
| 105 | + version, use it. Otherwise find the latest published version and **confirm it |
| 106 | + with the user** before pinning (don't guess a coach version silently): |
| 107 | + ```bash |
| 108 | + npm view @perchwerks/eye-in-the-sky version |
| 109 | + ``` |
| 110 | +2. In `vite.config.ts`, set |
| 111 | + ```ts |
| 112 | + const DEFAULT_PLUGIN_PACKAGES = "@perchwerks/eye-in-the-sky"; |
| 113 | + ``` |
| 114 | +3. Re-resolve the lockfile and install: |
| 115 | + ```bash |
| 116 | + bun install |
| 117 | + ``` |
| 118 | + Commit the updated `bun.lock` alongside the two source edits. |
| 119 | + |
| 120 | +### 2. Set the version |
| 121 | + |
| 122 | +The CHANGELOG drives the version. Find the topmost unreleased heading: |
| 123 | + |
| 124 | +```bash |
| 125 | +grep -n "unreleased" CHANGELOG.md | head -1 |
| 126 | +``` |
| 127 | + |
| 128 | +That heading is `## [<X.Y.Z>] - unreleased`. Set `package.json` `"version"` to |
| 129 | +exactly `<X.Y.Z>`: |
| 130 | + |
| 131 | +```bash |
| 132 | +grep -n '"version"' package.json |
| 133 | +``` |
| 134 | + |
| 135 | +The footer/build version stamp is baked from `package.json` (`vite.config.ts`), |
| 136 | +so `package.json` is the only place to edit — there is no second version string |
| 137 | +to keep in sync. (`CHANGELOG.md` is the only other place the version appears.) |
| 138 | + |
| 139 | +### 3. Date the changelog |
| 140 | + |
| 141 | +Replace `- unreleased` with today's date on that same topmost heading only: |
| 142 | + |
| 143 | +``` |
| 144 | +## [<X.Y.Z>] - <YYYY-MM-DD> |
| 145 | +``` |
| 146 | + |
| 147 | +Use the real current date (the harness provides it; don't hardcode). Leave every |
| 148 | +older heading untouched. Do **not** create a new `[Unreleased]` block — per the |
| 149 | +changelog rule the next version's block is started after this release ships. |
| 150 | + |
| 151 | +### 4. Green before merge |
| 152 | + |
| 153 | +All four must pass before opening any PR (CI runs them as separate workflows): |
| 154 | + |
| 155 | +```bash |
| 156 | +bun run lint |
| 157 | +bun run typecheck |
| 158 | +bun run test:run |
| 159 | +bun run build |
| 160 | +``` |
| 161 | + |
| 162 | +If any fail, fix forward on this branch before proceeding. The `build` step in |
| 163 | +particular validates that the production coach package resolves and bundles. |
| 164 | + |
| 165 | +### 5. Commit and push |
| 166 | + |
| 167 | +```bash |
| 168 | +git add package.json vite.config.ts bun.lock CHANGELOG.md |
| 169 | +git commit -m "chore(release): prep v<X.Y.Z> — production coach + dated changelog" |
| 170 | +git push -u origin release/v<X.Y.Z> |
| 171 | +``` |
| 172 | + |
| 173 | +(Retry push up to 4× with exponential backoff on network errors only.) |
| 174 | +**Do not tag.** |
| 175 | + |
| 176 | +### 6. Open the PRs |
| 177 | + |
| 178 | +Use the available GitHub mechanism — `gh` CLI locally, or the `mcp__github__*` |
| 179 | +tools in the remote/web environment. Owner/repo: `TheAngryRaven/DovesDataViewer`. |
| 180 | + |
| 181 | +**PR A — release branch → BETA** (the actual prep change): |
| 182 | +- base: `BETA`, head: `release/v<X.Y.Z>` |
| 183 | +- title: `Release prep v<X.Y.Z>` |
| 184 | +- body: summarize the coach flip (→ production), the version bump, and the dated |
| 185 | + changelog. |
| 186 | + |
| 187 | +**PR B — BETA → main** (the release itself, in preparation): |
| 188 | +- First check whether one already exists: |
| 189 | + ``` |
| 190 | + list_pull_requests(owner, repo, base="main", head="BETA", state="open") |
| 191 | + ``` |
| 192 | +- **The body must BE the release notes**, written so the maintainer can |
| 193 | + copy-paste it verbatim into the GitHub Release notes when they tag. Build it |
| 194 | + from the dated `## [X.Y.Z]` CHANGELOG section (the one you just finalized), |
| 195 | + cross-checked against the step 0.5 commit research so nothing user-facing is |
| 196 | + missing. Format: |
| 197 | + - Start with a one-line `## v<X.Y.Z> — <YYYY-MM-DD>` header. |
| 198 | + - Reproduce the CHANGELOG section's `### Added / Changed / Fixed / …` |
| 199 | + subsections and bullets verbatim — this is the human-readable changelog, |
| 200 | + not a commit dump. |
| 201 | + - End with a `**Full changelog:** <LAST_TAG>...v<X.Y.Z>` compare link, e.g. |
| 202 | + `https://github.com/TheAngryRaven/DovesDataViewer/compare/<LAST_TAG>...main`. |
| 203 | + - Do **not** append the Claude/Co-Authored-By footer to this body — it's |
| 204 | + release-notes copy, not a normal PR description. |
| 205 | +- **If the PR already exists**, don't open a duplicate — instead update its body |
| 206 | + to the freshly-generated release notes (`update_pull_request`) so it stays |
| 207 | + copy-paste ready, and note its number/URL. |
| 208 | +- If it doesn't exist, create it: base `main`, head `BETA`, |
| 209 | + title `Release v<X.Y.Z>`, body = the release notes above. It may stay a draft |
| 210 | + until PR A lands. |
| 211 | + |
| 212 | +**PR C — flip the coach back to BETA** (post-release cleanup, opened now): |
| 213 | +- Cut another branch from `origin/BETA`: |
| 214 | + ```bash |
| 215 | + git switch -c chore/coach-back-to-beta origin/BETA |
| 216 | + ``` |
| 217 | +- Apply the **reverse** of step 1: `package.json` → |
| 218 | + `"@theangryraven/eye-in-the-sky": "github:TheAngryRaven/DataViewer_coach#BETA"`, |
| 219 | + `vite.config.ts` → `DEFAULT_PLUGIN_PACKAGES = "@theangryraven/eye-in-the-sky"`, |
| 220 | + then `bun install`. Commit and push. |
| 221 | +- Open PR: base `BETA`, head `chore/coach-back-to-beta`. |
| 222 | +- **Title must signal ordering**, e.g. |
| 223 | + `[DO NOT MERGE YET] Flip coach back to BETA — merge AFTER beta is merged into main` |
| 224 | +- body: explain this restores BETA's git-dependency coach source and must only be |
| 225 | + merged once PR B (BETA → main) has merged, so the release isn't undone. |
| 226 | + |
| 227 | +### 7. Report |
| 228 | + |
| 229 | +Summarize for the user: the release version, whether the coach needed flipping or |
| 230 | +was already production, the three PR URLs (noting if PR B already existed / |
| 231 | +was updated), and an explicit reminder that **no tag was created** — they tag |
| 232 | +manually after merge. Then remind them of the two manual post-release steps below. |
| 233 | + |
| 234 | +## Post-release: tag, then back-merge main into BETA (manual, after merge) |
| 235 | + |
| 236 | +This skill stops at *prep* — it can't tag or back-merge, because at prep time the |
| 237 | +release isn't merged into `main` yet. Once the maintainer merges **PR B |
| 238 | +(BETA → main)**, two things happen in order: |
| 239 | + |
| 240 | +1. **Tag the release on `main`** (maintainer does this by hand — Claude tagging |
| 241 | + errors out). The tag (`v<X.Y.Z>`) lands on the `main` merge commit. |
| 242 | +2. **Back-merge `main` into `BETA`.** This is the part that's easy to forget and |
| 243 | + the reason `git describe` / "commits since last tag" goes wrong over time. |
| 244 | + |
| 245 | +**Why the back-merge is required.** Merging `BETA → main` creates a merge commit |
| 246 | +on `main`; the tag sits on `main` only and is **never** copied to `BETA`. So |
| 247 | +`BETA` and `main` diverge — the tag isn't an ancestor of `BETA`, `git describe` |
| 248 | +from `BETA` can't find the latest release, and the gap compounds with every |
| 249 | +release (you can confirm with `git merge-base --is-ancestor v<X.Y.Z> origin/BETA` |
| 250 | +returning non-zero). Merging `main` back into `BETA` reconverges the branches so |
| 251 | +the tag is reachable from `BETA` again. This is the existing `Main2beta-*` branch |
| 252 | +pattern in the repo history. |
| 253 | + |
| 254 | +Do it via a normal PR (don't push to `BETA` directly): |
| 255 | + |
| 256 | +```bash |
| 257 | +git fetch --all --tags |
| 258 | +git switch -c chore/main-back-to-beta origin/main |
| 259 | +git push -u origin chore/main-back-to-beta |
| 260 | +# open PR: base BETA, head chore/main-back-to-beta, title "Back-merge main → BETA after v<X.Y.Z>" |
| 261 | +``` |
| 262 | + |
| 263 | +> Order matters: merge **PR C (flip coach back to BETA)** and this back-merge into |
| 264 | +> `BETA` *after* the release is on `main`. If PR C's coach revert lands on `BETA` |
| 265 | +> before `BETA → main` merges, the release ships with the BETA git-dependency |
| 266 | +> coach — the exact bug this whole skill exists to prevent. |
| 267 | +
|
| 268 | +## Notes & edge cases |
| 269 | + |
| 270 | +- If the working tree is dirty at the start, stop and ask the user — don't stash |
| 271 | + or discard their work. |
| 272 | +- If there is no `unreleased` heading in the CHANGELOG, the release version is |
| 273 | + ambiguous — ask the user which version is being cut. |
| 274 | +- A git dependency records the resolved coach commit in `bun.lock`; flipping to |
| 275 | + the npm package changes the lockfile, which is expected and must be committed. |
| 276 | +- If `bun install` can't reach the registry in a restricted network, surface the |
| 277 | + error — don't hand-edit `bun.lock`. |
0 commit comments