End-to-end release flow for `lukas-grigis/ralphctl`. Mirrors the established release pattern: branch → bump → changelog → PR → CI → merge → tag → workflow.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add lukas-grigis/ralphctl --skill release --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Release?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lukas-grigis-release)More formats (shields.io, HTML) on the badges page.
---
name: release
description:
Cut a new release of ralphctl — bumps `package.json`, promotes the `## [Unreleased]` CHANGELOG section to a dated `## [X.Y.Z]`, opens a `chore(release): X.Y.Z` PR, defers to `merge-pr` to wait for CI and merge with admin bypass, then tags the merge commit and pushes the tag (which fires the `release.yml` workflow → npm publish + GitHub Release). Use when the user says "/release X.Y.Z", "release X.Y.Z", "ship 1.2.3", "cut a new version", or otherwise asks to publish a new version of ralphctl.
when_to_use: When the user explicitly asks to ship a release. Requires the version arg in semver form (e.g. `0.4.5`, no `v` prefix). Pre-conditions checked at runtime — clean working tree on `main`, no other release branch in flight, `## [Unreleased]` has content worth releasing.
allowed-tools: Bash, Read, Edit
disable-model-invocation: true
---
# Release
End-to-end release flow for `lukas-grigis/ralphctl`. Mirrors the established release pattern:
branch → bump → changelog → PR → CI → merge → tag → workflow.
## Arg
- `<version>` — semver, no `v` prefix (e.g. `0.4.5`, `1.0.0-rc.1`). Reject anything that doesn't match
`^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$`.
## Pre-flight (stop on any failure — don't try to recover)
```bash
git rev-parse --abbrev-ref HEAD # must be `main`
[ -z "$(git status --porcelain)" ] # working tree clean
git fetch origin && git pull --ff-only origin main
node -p "require('./package.json').version" # must be < <version>
```
If any check fails, surface the exact problem and stop. Don't stash, don't switch branches, don't auto-fix — the user
wants to know.
### `## [Unreleased]` auto-draft
Check whether `## [Unreleased]` has content. Grab the body between that heading and the next `## [` heading:
```bash
awk '/^## \[Unreleased\]/{flag=1;next} /^## \[/{flag=0} flag' CHANGELOG.md | sed '/^$/d'
```
**If empty,** auto-draft entries from the commit range since the last tag, **skipping contributor-side tooling that
isn't shipped to npm** (`.claude/**`, `.github/**`, `CHANGELOG.md`, `CLAUDE.md`):
```bash
LAST_TAG="$(git describe --tags --abbrev=0)"
git log --no-merges --pretty='- %s' "${LAST_TAG}..HEAD" -- \
':!.claude' ':!.github' ':!CHANGELOG.md' ':!CLAUDE.md'
```
Group the surviving commit subjects by conventional-commit prefix into `### Added` (`feat:`), `### Fixed` (`fix:`),
`### Changed` (`refactor:` / `perf:` / user-facing `chore:`). Write the result under `## [Unreleased]` in
`CHANGELOG.md` (edit only — don't commit yet; the release commit in step 5 picks it up).
**If the filtered `git log` is also empty,** there's nothing user-facing to release — stop and tell the user. Don't
promote an empty section; `release.yml` hard-fails on a missing or empty `## [<version>]` section (there is no
fallback), so the workflow would die after the tag is already public.
**If the original `## [Unreleased]` had content,** skip the draft — the user already wrote what they want. Proceed.
Surface the drafted entries back to the user before moving on so they can veto / rewrite in-place on the release
branch if anything looks off.
## Steps
1. **Branch.** `git checkout -b release/<version>`
2. **Bump `package.json`.** Edit the `"version"` field directly. **Do not** use `npm version` / `pnpm version` — those
create a tag immediately, and we tag the _merge commit on `main`_, not the release-branch commit.
3. **Promote the changelog.** In `CHANGELOG.md`, replace exactly:
```
## [Unreleased]
```
with:
```
## [Unreleased]
## [<version>] - <YYYY-MM-DD>
```
Use today's UTC date in ISO format (`date -u +%Y-%m-%d`). Keep the existing entries where they are — they belong
under the new dated heading by virtue of position. The heading format is **load-bearing**: `release.yml` extracts the
GitHub Release body by `awk`-ing for `## [<version>]`, and a typo there fails the workflow — after the tag is
already public.
**Pre-release versions** (`1.0.0-rc.1`): the full suffix must appear in both `package.json#version` and the
changelog heading (`## [1.0.0-rc.1]`). The workflow publishes hyphenated versions under the `next` npm dist-tag
(never `latest`) and flags the GitHub Release `prerelease`. After the matching stable ships later, run
`npm dist-tag add ralphctl@X.Y.Z next` (or `npm dist-tag rm ralphctl next`) so `next` never serves a superseded
rc — see the release procedure in `.claude/docs/PERFORMANCE.md`.
4. **Local gate.** Run the project's full check sequence (same as the `verify` skill):
```bash
pnpm typecheck && pnpm lint && pnpm test
```
All three must pass. If anything fails, stop — fix on a separate branch first; don't pile fixes onto a release
branch.
5. **Commit + push.**
```bash
git add package.json CHANGELOG.md
git commit -m "chore(release): <version>"
git push -u origin release/<version>
```
6. **Open the PR.**
```bash
gh pr create --base main \
--title "chore(release): <version>" \
--body "$(cat <<'EOF'
## Summary
- Bump version to <version>
- Promote `## [Unreleased]` CHANGELOG section to `## [<version>]`
## Test plan
- [x] `pnpm typecheck` · `pnpm lint` · `pnpm test` — green locally
- [ ] CI green
EOF
)"
```
Capture the PR number from the output URL.
7. **Defer to `merge-pr`.** Follow the `merge-pr` skill with the new PR number — it `--watch`es CI, runs
`gh pr merge --merge --admin`, then **cleans up the trailing branch** (switches to `main`, fast-forwards, prunes the
stale remote-tracking ref, deletes the local `release/<version>`). The merge commit lands on `main` with subject
`chore(release): <version> (#NN)`. (See `.claude/skills/merge-pr/SKILL.md` for the why behind `--merge --admin`.)
8. **Tag the merge commit.** `merge-pr` already left us on `main` at the merge commit, so just tag and push:
```bash
git tag v<version> # tags HEAD = the merge commit
git push origin v<version>
```
9. **Watch the release workflow.** Tag push fires `.github/workflows/release.yml`:
- re-runs format / lint / typecheck / test / build
- verifies tag matches `package.json` version
- publishes to npm with `--provenance`
- creates a GitHub Release using the matching `## [<version>]` section as the body
```bash
sleep 3 # let GH register the workflow run
gh run watch --exit-status \
"$(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')"
```
10. **Done.** Print:
- npm: `https://www.npmjs.com/package/ralphctl/v/<version>`
- Release: `https://github.com/lukas-grigis/ralphctl/releases/tag/v<version>`
## Failure recovery
- **Local gate red:** Stop. Fix on a non-release branch. Restart `release` once `main` is green again.
- **CI red on the PR (step 7):** Don't merge. The branch is still a normal branch — push fixes, and `merge-pr`
re-watches.
- **Workflow red after tag push (step 9):** The tag is already public. **Do not delete or move it** — npm provenance
will refuse to re-publish the same version, and consumers may have already pulled. Cut the next patch version with the
fix.
- **Cold feet before merge:** `gh pr close <num>` (auto-deletes the branch). No tag has been pushed yet. The version
bump + changelog promotion exist only on the closed PR.
## Why this shape
- **Branch + PR (not direct push to `main`):** Branch protection requires a PR. Even with admin bypass, the audit trail
and the PR's CI run are valuable — the workflow re-runs everything, but the PR catches issues before tagging (and tags
are forever once published).
- **Tag the merge commit, not the release-branch tip:** The published artifact must match what's actually on `main`.
Tagging the merge commit guarantees `git checkout v<version>` shows the same tree `main` had at release time.
- **One source of truth for release notes:** The workflow extracts `## [<version>] - <YYYY-MM-DD>` from `CHANGELOG.md`.
Match the format exactly; a missing or empty section fails the workflow (the old raw-`git log` fallback was removed
on purpose — a commit-subject dump is not release notes).
- **Admin bypass is loud, on purpose:** Each release surfaces the bypass via `--admin` rather than relying on auto-merge
with weakened protections. The protections stay strict; the bypass is invoked explicitly.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!