Skip to main content

release

Cut a release of a Ruby library the way the release kit does it — roll the changelog into the version's heading through a PR, run the repository's release script from a clean main, then verify the publish, RubyGems and the docs deploy. Use when merged work should ship, or as the release step /flow:follow-through and /flow:plan upstream run after a library fix (`/ruby-gem:release patch`).

Source facts

Repository
zoolutions/claude-plugins
Last source activity
October 17, 2026 at 18:59
Detected SKILL.md language
English
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
release
description
Cut a release of a Ruby library the way the release kit does it — roll the changelog into the version's heading through a PR, run the repository's release script from a clean main, then verify the publish, RubyGems and the docs deploy. Use when merged work should ship, or as the release step /flow:follow-through and /flow:plan upstream run after a library fix (`/ruby-gem:release patch`).
argument-hint
patch | minor | major | X.Y.Z (empty = derived from the changelog; a level is a floor the changelog can raise)
allowed-tools
Bash(*), Read, Write, Edit, Glob, Grep, Skill, AskUserQuestion
model
opus
# Release: $ARGUMENTS `main` below is the default branch: Workflow → Base when set, else `main`. Ship what `main` holds. **Read the repository first:** `AGENTS.md` → `## Workflow` → **Release** names the release command, the changelog and its heading, prep commands, the lockfiles that pin the gem, the docs deploy and any human step. Verify, Merge and PR contract apply to the changelog PR. A missing label means "none"; no Release line → stop, this repository is not released this way. **Unattended.** Exactly two things ask (`AskUserQuestion`, once): a major bump, or a changelog with nothing to ship. Under an orchestrator there is no one to ask: those two, and every blocker outside the repository, end the run as `needs-user` with the reason. **The kit does the release.** `bin/release` → `rake release[X.Y.Z]` (`rakelib/release.rake`) bumps `version.rb` and every tracked lockfile pin in place (no re-resolve), builds, commits, pushes `main` and creates the GitHub Release whose `release.yml` publishes to RubyGems. This skill does what the kit leaves out — the changelog — and checks what it starts. The kit's files are byte-identical across the gems (canonical copy in docs-kit): never edit them here. ## Phase 1: State and a clean clone The kit runs only on a clean `main` equal to `origin/main`, and working trees are usually on feature branches (a worktree on `main` fails while `main` is checked out elsewhere). Work in a throw-away clone; every command below runs there (`cd <clone> && …` when the shell does not keep the directory): ```bash clone="$(mktemp -d)/$(basename "$(git rev-parse --show-toplevel)")" gh repo clone "$(gh repo view --json nameWithOwner --jq .nameWithOwner)" "$clone" -- --branch main cd "$clone" && bundle install --quiet && bin/release list ``` `bin/release list` prints the gemspec version, the last tags and what each bump gives. - **Resume.** An open roll PR (`gh pr list --state open --json number,headRefName --jq '.[] | select(.headRefName | startswith("release/v"))'`): its branch names the target → Phase 3 on it. A `v<X.Y.Z>` tag with a GitHub Release → Phase 5 only. - **Version already moved.** The gemspec version is above the newest tag and has no tag (a feature PR bumped `VERSION`): that version is the target; the kit skips the bump. ## Phase 2: Version Read the section to ship: `## [Unreleased]` (or `## Unreleased`), or, in a changelog without one, the version heading above the last released one. | The section holds | Level | |---|---| | a removal, a renamed public name, a behaviour change named as one, an "Upgrading" or "BREAKING" note | major (on `0.x`: minor) | | an addition (`Added`, a new option, method or class) | minor | | only fixes, performance, docs | patch | - An argument level is a floor: the changelog may raise it, never lower it. `X.Y.Z` is taken as given. - Current version is a prerelease (`alpha|beta|rc|pre`): a level bump *finalises* it (9.0.0.alpha2 → 9.0.0), so it counts as major unless an explicit prerelease version was passed. - **Ask** when the result is major (offer it, the next lower level, stop), or the section has no bullet (offer: write the bullets from the PRs merged since the last tag, `gh pr list --state merged --search "merged:>=<last tag date>"`; or stop). Then `bin/release <X.Y.Z> --dry-run`: it must print `release: v<X.Y.Z>` and no "cannot release" blocker. A blocker names a gem in a tracked lockfile that pins this one below the new version: that gem releases first (its own `/ruby-gem:release`), or `needs-user` naming it. ## Phase 3: Roll the changelog (a PR) `git switch -c release/v<X.Y.Z>` in the clone (resuming an open roll PR: `git fetch origin release/v<X.Y.Z>` and `git switch release/v<X.Y.Z>` instead, then continue where it stops), then in `CHANGELOG.md`: 1. **Missed releases.** Every `v*` tag newer than the newest version heading gets its heading (date: `git log -1 --format=%cs <tag>`) with the bullets whose PR it shipped (`git tag --contains <merge sha>`; bullets cite `#N`). This drift is why "changelog headings match what each release shipped" commits exist. 2. **The new heading.** Copy the shape of the previous release heading exactly: brackets or none, `-` or `—`, `(date)` or `- date`, a compare link when the file keeps them. Today's date. Above it a fresh empty `[Unreleased]` in the file's spelling, only if the file has one; category stubs only if the previous roll left them. 3. **Already headed** (the heading for `<X.Y.Z>` was written by the feature PRs): nothing to roll. 4. **Prep.** Run Workflow → Release → prep (a re-recorded baseline when the version string changes a measured file); commit what it writes. Never touch `version.rb` or the lockfile pins here: the kit bumps them in its own commit. Nothing changed → skip to Phase 4. Otherwise commit (`docs(changelog): <X.Y.Z>`), push, `gh pr create` with Workflow → PR contract, and `/flow:finish-prs <PR> automerge` (Skill tool) from the clone. Parked → `needs-user` with its reason. **Human step** (Workflow → Release → human, e.g. reader checks that cannot run in CI): put it in the PR body as a checklist, comment it on the PR with the resume command `/ruby-gem:release <X.Y.Z>`, and end as `needs-user` before merging. A re-run with the checklist ticked by the operator continues. ## Phase 4: Release ```bash git switch main && git pull --ff-only origin main && bundle install --quiet bin/release <X.Y.Z> --dry-run # same version, no blocker, the roll is in the change list printf 'y\n' | bin/release <X.Y.Z> # explicit version: main may have moved since Phase 2 ``` The kit's own guards (branch, clean tree, `HEAD == origin/main`, tag absent, `release:preflight`) abort before anything is pushed: read the message, fix the cause, run again. A rejected push to `main` (a ruleset that admins cannot bypass) publishes nothing: `needs-user`, never a bypass. ## Phase 5: Verify 1. The publish — this release's run, never just the latest: `gh run list --workflow release.yml --limit 20 --json databaseId,headBranch,headSha,status,conclusion --jq '.[] | select(.headBranch == "v<X.Y.Z>" or .headSha == "<the tag's commit>")'` (none yet: it is still starting, ask again), then `gh run watch <id> --exit-status` (Bash timeout 600000; re-run until it finishes). A job waiting on an environment approval → `needs-user`. 2. RubyGems: `curl -fsS https://rubygems.org/api/v2/rubygems/<gem>/versions/<X.Y.Z>.json` (the index lags a few minutes; retry). 3. Docs (Workflow → Release → docs): watch that workflow's run for the release the same way. 4. **Red publish.** `gh run view <id> --log-failed`. A frozen `bundle install` failing on a stale lockfile → a PR that re-locks that file (`bundle install` in its directory), through `/flow:finish-prs <PR> automerge`, then `printf 'y\n' | bin/release <X.Y.Z> --force` once. The publish skips a version already on RubyGems, so a re-cut never double-publishes. Anything else → the same fix-PR-then-re-cut, or `needs-user` when the cause is outside the repository. 5. `rm -rf` the clone. ## Report One line: `released <gem> v<X.Y.Z> — <release URL>` or `needs-user — <reason>`; under an orchestrator, the version and that state in its JSON. ## Never - Edit `bin/release`, `rakelib/release.rake` or `release.yml`'s shared jobs; `gem push`; `git tag`; `git push --tags`. - Bump `version.rb` or a lockfile pin by hand, or re-resolve a lockfile, for a release. - `--force` except the one re-cut after a fixed publish failure. - Release from a branch, a dirty tree or a `main` behind `origin/main`.
View on GitHub