From 9cdc2c2e01c0fbe3004f495b8433f02e10013d5f Mon Sep 17 00:00:00 2001 From: wjduenow Date: Sat, 30 May 2026 16:14:34 -0700 Subject: [PATCH] chore: patch release-manager skill with v0.5.0 lessons learned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five edits, each capturing one mistake from cutting v0.5.0: 1. **Pre-flight branch check** — recognise the dev-ahead-of-main case (common when main is at v(N-1) and dev carries v(N) work). Skill previously required `main`; now detects the deviation and points to Step 3's two-shape guidance. 2. **Pre-flight item 5 — stale-`[Unreleased]` carryover guard** — diff `[Unreleased]` against the previous `[X.(Y-1).0]` section; identical → STOP. This is the load-bearing check that would have caught v0.5.0's "release notes byte-identical to v0.4.0" bug at pre-flight time instead of at PR-merge-conflict time. Linked to Step 9's cleanup as the long-term fix. 3. **Step 1 — skill metadata lockstep bump** — `signalforge-version` stamps in `src/signalforge/skills/signalforge/SKILL.md` frontmatter and `assets/SKILL.eval.json` must match `__version__` (and bump in lockstep at every version edit). v0.5.0 shipped without these bumps on the first attempt; the patched skill grep+edits both. 4. **Step 3 — dev-ahead-of-main branching shape** — two variants documented inline. Normal case = branch from main. Dev-ahead case = branch from main + cherry-pick the post-v(N-1) commits (often a single squash-merge). Verification recipe: `git diff release/X.Y.Z origin/dev --stat` should show only the release-prep edits. 5. **Steps 8 + 9 combined** — one PR per release-cycle housekeeping pass (next-dev bump + skill metadata + backmerge + CHANGELOG cleanup). Crucially: `git checkout --theirs CHANGELOG.md` during the merge resets dev to main's clean post-release shape, which is what prevents pre-flight item 5 from firing next cycle. The old separate-PR shape (Step 8 = bump, Step 9 = backmerge) loses signal when dev is ahead. Also bumps the skill's own `signalforge-version` metadata 0.1.0 → 0.5.0 (was stale from when the skill was authored). Memory cross-link: [[release-clear-unreleased-on-backmerge]] (the load-bearing CHANGELOG cleanup, now codified in Step 9 itself) and [[release-full-from-dev-ahead-of-main]] (the parent shape). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/release-manager/SKILL.md | 119 ++++++++++++++++++------ 1 file changed, 91 insertions(+), 28 deletions(-) diff --git a/.claude/skills/release-manager/SKILL.md b/.claude/skills/release-manager/SKILL.md index 75b623d7..8e42dcb4 100644 --- a/.claude/skills/release-manager/SKILL.md +++ b/.claude/skills/release-manager/SKILL.md @@ -3,7 +3,7 @@ name: release-manager description: Cut a signalforge-dbt release. Test releases publish to TestPyPI via a prerelease GitHub Release; full releases publish to PyPI via a non-prerelease GitHub Release. compatibility: "Requires: gh CLI, git, uv (build + validation), uvx (for twine), pip (clean-room TestPyPI install test). Must be run from the SignalForge repo root." metadata: - signalforge-version: "0.1.0" + signalforge-version: "0.5.0" disable-model-invocation: true allowed-tools: Bash(git *), Bash(gh *), Bash(uv *), Bash(uvx *), Bash(grep *), Bash(cat *), Bash(sleep *), Bash(pip *), Bash(python *), Bash(curl *), Bash(awk *), Bash(rm *), Bash(date *), Read, Edit, Write --- @@ -39,7 +39,7 @@ Run these checks and STOP if any fail — report the problem clearly and do not **Branch check (differs by release type):** - **Test release**: `git branch --show-current` must return `dev`. Test releases tag `dev` HEAD and publish to TestPyPI. If the user is on another branch, STOP and ask them to `git checkout dev` (and ensure the rc commit is on `dev`) first. -- **Full release**: `git branch --show-current` must return `main`. Full releases tag `main` HEAD and publish to PyPI. +- **Full release**: normally `git branch --show-current` returns `main`. But when **dev is ahead of main with the release content** (common — main was last released as v(N-1), dev carries v(N) work), starting on `main` produces an empty release branch with no v(N) content. Detect via `git log --oneline origin/main..origin/dev`: any output → dev-ahead case. Proceed from `dev` instead, and document the deviation in the pre-flight summary. Step 3 ("Open release PR") covers the dev-ahead branching shape (base `release/X.Y.Z` on dev OR on main + cherry-pick of the post-release-N-1 commits — see Step 3 § "Dev-ahead-of-main shape"). **Checks for both modes:** @@ -53,16 +53,25 @@ Run these checks and STOP if any fail — report the problem clearly and do not uv sync --dev && uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run pytest ``` 4. **CHANGELOG `[Unreleased]` is current**. Read `CHANGELOG.md` and show the user the current `[Unreleased]` section. Ask: "Does this cover everything shipping in this release?" Pause for confirmation — for **full** releases the `[Unreleased]` content gets promoted to `[X.Y.Z]` and used as the GitHub Release body. Empty / stale `[Unreleased]` means an empty / stale release page. If the user wants to edit it, stop here, let them edit, then re-run pre-flight. +5. **Stale-`[Unreleased]` carryover guard** (full releases only — load-bearing, see Step 9). Compare `[Unreleased]` against the existing `[X.(Y-1).0]` section. If they are byte-identical, the previous release's backmerge didn't clear `[Unreleased]` — promoting it now would publish v(N-1)'s release notes verbatim as v(N), and the release PR will conflict at every CHANGELOG line on merge into main. Detect with: + ```bash + awk '/^## \[Unreleased\]/{p=1; next} /^## \[/{p=0} p' CHANGELOG.md > /tmp/sf-unreleased.txt + awk -v ver="$(grep '^## \[0\.' CHANGELOG.md | sed -n 2p | sed -E 's/^## \[([^]]+)\].*/\1/')" \ + '$0 ~ "^## \\["ver"\\] "{p=1; next} /^## \[/{p=0} p' CHANGELOG.md > /tmp/sf-prev.txt + diff -q /tmp/sf-unreleased.txt /tmp/sf-prev.txt + ``` + If `diff` reports "identical" (no output), STOP and surface the issue: dev's `[Unreleased]` is a stale duplicate of the previous release. The user must either edit `[Unreleased]` to reflect what's actually new in v(N) (drop the v(N-1)-duplicate bullets) OR confirm the release truly contains nothing new and a separate decision is needed. Don't proceed until resolved. **Report a pre-flight summary** — always, even when every check passes: ``` Pre-flight checks: -- Branch (test→dev | full→main): PASS|FAIL +- Branch (test→dev | full→main, or dev-ahead-of-main deviation): PASS|FAIL - Clean working tree: PASS|FAIL - Up to date with origin: PASS|FAIL - ruff check / format / pyright / pytest: PASS|FAIL - CHANGELOG [Unreleased] reviewed: PASS|FAIL +- CHANGELOG [Unreleased] not a stale duplicate of previous release (full only): PASS|FAIL ``` Then continue to "Determine version" (on all-PASS) or STOP with the failing check highlighted. @@ -184,6 +193,18 @@ Report: TestPyPI URL `https://test.pypi.org/project/signalforge-dbt/{release_ver Edit `src/signalforge/__init__.py`: set `__version__ = "{release_version}"`. +**Bump skill metadata in lockstep.** The bundled Claude Code skill carries the version stamp in two places that mirror `__version__`. Grep + edit both so the shipped wheel's skill metadata matches the release: + +```bash +grep -rln 'signalforge-version\|"version":' src/signalforge/skills/ 2>/dev/null +``` + +Expected hits (as of v0.5.0): +- `src/signalforge/skills/signalforge/SKILL.md` — frontmatter ` signalforge-version: "{old}"` → `"{release_version}"` +- `src/signalforge/skills/signalforge/assets/SKILL.eval.json` — `"version": "{old}"` → `"{release_version}"` + +A new bundled skill in v0.X may add more — the grep is the source of truth. Bumping these in lockstep is what keeps the wheel's `--version` reading and `SKILL.md`'s self-reported version honest. + ### Step 1b — Promote CHANGELOG `[Unreleased]` to `[{release_version}]` Edit `CHANGELOG.md` so this release has its own dated section the GitHub Release body can quote verbatim: @@ -213,19 +234,46 @@ Both artifacts must show `PASSED`. STOP and report if either fails. ### Step 3 — Open release PR -Push the version bump and the CHANGELOG promotion together on a release branch, then open a PR — direct push to `main` is blocked by branch protection. +Push the version bump, the CHANGELOG promotion, AND the skill-metadata bumps from Step 1 together on a release branch, then open a PR — direct push to `main` is blocked by branch protection. + +**Two branching shapes depending on whether dev is ahead of main:** + +**(a) Normal case (main has the release content)** — branch from main: + +```bash +git checkout -b release/{release_version} +git add src/signalforge/__init__.py CHANGELOG.md src/signalforge/skills/ +git commit -m "chore: release {release_version}" +git push -u origin release/{release_version} +``` + +**(b) Dev-ahead-of-main case** — branch from main + cherry-pick the post-v(N-1) commits from dev (or, when that's a single squash-merge commit, just cherry-pick it): ```bash +git checkout main && git pull origin main git checkout -b release/{release_version} -git add src/signalforge/__init__.py CHANGELOG.md +# Find the dev commits not on main: +git log --oneline origin/main..origin/dev +# Cherry-pick the one(s) that add v(N) content (often a single squash-merge): +git cherry-pick +# Then bump version + promote CHANGELOG + bump skill metadata as in Step 1: +# (edit src/signalforge/__init__.py, CHANGELOG.md, and the skill metadata files) +git add src/signalforge/__init__.py CHANGELOG.md src/signalforge/skills/ git commit -m "chore: release {release_version}" git push -u origin release/{release_version} +``` + +Verify the cherry-pick reproduces dev's tree: `git diff release/{release_version} origin/dev --stat` should show only `CHANGELOG.md` + `src/signalforge/__init__.py` + the skill metadata files (i.e. only your release-prep edits). If more files differ, dev has work the cherry-pick missed — extend the cherry-pick or rebuild the branch. + +**Common to both shapes — open the PR:** + +```bash gh pr create --base main --head release/{release_version} \ --title "chore: release {release_version}" \ --body "Cuts v{release_version} to PyPI. Pre-flight passed (ruff/pyright/pytest); \`uv build\` + \`uvx twine check\` PASSED on wheel and sdist. CHANGELOG promoted from \`[Unreleased]\`." ``` -STOP and ask the user to merge the PR via GitHub. Once merged, continue. +STOP and ask the user to merge the PR via GitHub. Expect codecov + CodeRabbit comments on this main-targeting PR — both run on PRs into main, neither on PRs into dev. Once merged, continue. ### Step 4 — Pull main, tag, push tag @@ -274,40 +322,55 @@ curl -sf "https://pypi.org/pypi/signalforge-dbt/{release_version}/json" \ Confirm `{release_version}` appears. -### Step 8 — Open next-dev bump PR (target dev) +### Steps 8 + 9 — Combined dev PR (next-dev bump + backmerge + CHANGELOG cleanup) -Edit `src/signalforge/__init__.py`: set `__version__ = "{next_dev_version}"`. +After the release publishes, dev needs three things and they fit naturally in one PR: + +1. **Bump `__version__`** to `{next_dev_version}` (e.g. release `0.5.0` → next dev `0.6.0.dev0`). +2. **Bump skill metadata** in lockstep — same files as Step 1 (`SKILL.md` frontmatter + `assets/SKILL.eval.json`), now to `{next_dev_version}`. +3. **Backmerge main → dev** so dev = main (v{release_version}) + future work, and **clear dev's stale `[Unreleased]` carryover** via `git checkout --theirs CHANGELOG.md` (load-bearing — see below). ```bash git checkout dev git pull origin dev -git checkout -b chore/begin-{next_dev_version} -git add src/signalforge/__init__.py -git commit -m "chore: begin {next_dev_version}" -git push -u origin chore/begin-{next_dev_version} -gh pr create --base dev --head chore/begin-{next_dev_version} \ - --title "chore: begin {next_dev_version}" \ - --body "Bumps version to {next_dev_version} after the v{release_version} release." +git checkout -b chore/post-{release_version}-bump-and-backmerge +git merge origin/main --no-edit ``` -Ask the user to merge. +The merge will conflict on `src/signalforge/__init__.py` (dev has `{release_version}.dev0`, main has `{release_version}`), the skill metadata files (same shape), and `CHANGELOG.md` (dev's stale `[Unreleased]` vs main's promoted-and-cleared shape). Resolve as follows: -### Step 9 — Backmerge main → dev +```bash +# __init__.py — write the next-dev value (not ours, not theirs) +cat > src/signalforge/__init__.py <