Skills · Maintainer
dev-release
/dev-releaseCut a new release of the `ralphy` CLI across all 3 distribution channels in one shot — GitHub Release (binaries + checksums), Homebrew tap (`alecs5am/homebrew-tap`), and npm (`@…
SKILL.mdRelease skill — ralphy CLI across all channels
When to fire
Hard triggers (always do it):
- User types
/release - "cut a release" / "publish a release" / "tag a new version"
- "release notes for the last week"
- "publish to brew/npm" — even if the binaries are already up
Proactive triggers (offer it, don't auto-execute):
- A meaningful CLI feature just landed on
mainand there are ≥5 commits since the last tag. - User says "this is the final one" / "ready to ship" after a feature session.
- A new install instruction was just published and
brew upgrade/npm updatewould resolve to the previous version.
What I produce
Three live releases, all reachable from the same vX.Y.Z tag:
- GitHub Release at
alecs5am/ralphy/releases/tag/vX.Y.Zwith 5 binaries (darwin-arm64/x64, linux-arm64/x64, windows-x64.exe) +SHA256SUMS, built and published by theReleaseGitHub Actions workflow on tag push. - Homebrew tap at
alecs5am/homebrew-tap—Formula/ralphy.rbbumped to point at the new release with sha256-pinned URLs for each platform. - npm registry —
@alecs5am/ralphy@X.Y.Zpublished with public access. The thin npm wrapper'spostinstallhook downloads the matching binary from the GitHub Release the first time it runs.
All three reference the same vX.Y.Z tag and the same set of binaries — there is no version drift between channels.
Hard scope: CLI + CLI-docs
This skill never touches:
landing/— the Next.js marketing landing is shipped independently (Vercel / Netlify).docs/,README.md— these are repo-internal playbook / orientation docs. Onlypackage.json/cli/lib/version.ts/npm/package.json/AGENTS.md(the single version line) /scripts/release/last-release-commit/docs-mintlify/**get edited under this skill..agents/skills/— other skills are versioned with the repo, not with the binary.
Two targeted exceptions that go inside the version-bump commit (or the docs-sync commit right before it):
AGENTS.mdcarries a single line (> **Current ralphy CLI: \vX.Y.Z`** ...) that lives in the system prompt of every ralphy session. The/release` skill bumps that line in lockstep with the four real version files so the agent always knows which binary the user should be on. Find/replace only — never restructure AGENTS.md otherwise.docs-mintlify/**is the public docs site for the CLI (hosted at the Mintlify URL — seedocs-mintlify/docs.jsonfor the deploy config). It describes the same binary this skill ships, so it MUST be kept in sync. Each release walks the diff since the last release commit and updates any affected page underdocs-mintlify/cli/,docs-mintlify/reference/,docs-mintlify/authoring/, plusquickstart.mdx/introduction.mdx/concepts.mdxif the surface they describe changed. New top-level commands → new.mdxpage + entry indocs.json. See step 3b below.scripts/release/last-release-commitstores the exact commit SHA the previous release shipped. Step 1 reads it to scope the diff; step 5 rewrites it withHEADof the new release commit so the next/releaseinvocation starts from a clean baseline.
If the user asks to "release the landing" — handback. Not this skill.
End-to-end flow
Run the steps in order. After step 4 you must stop and confirm before doing anything destructive. After step 6 the GitHub Release happens automatically via CI; steps 8 and 9 are local-only and require an authenticated gh (alecs5am) plus npm login session.
1 — Snapshot current state + credential preflight
# Credential preflight — both must succeed BEFORE any destructive step.
gh auth status --hostname github.com 2>&1 | head -5 # expect: account alecs5am, active
npm whoami # expect: alecs5am
# Repo state
gh repo view alecs5am/ralphy --json defaultBranchRef,latestRelease | jq
git fetch origin --tags
# Diff baseline — prefer the explicit pointer at scripts/release/last-release-commit
# (rewritten by the previous /release run), then fall back to the last tag, then
# to HEAD~50 if this is a freshly-cloned repo with no tags / no pointer.
BASELINE="$(cat scripts/release/last-release-commit 2>/dev/null | tr -d '[:space:]')"
if [ -z "$BASELINE" ] || ! git cat-file -e "$BASELINE^{commit}" 2>/dev/null; then
BASELINE="$(git describe --tags --abbrev=0 2>/dev/null || echo HEAD~50)"
echo "⚠️ scripts/release/last-release-commit missing or stale — falling back to ${BASELINE}"
fi
echo "Release diff baseline: ${BASELINE}"
git log --no-merges --pretty=format:"%h %s" "${BASELINE}..HEAD"
git diff --stat "${BASELINE}..HEAD"
git status --short
What you need:
- The diff baseline commit (from
scripts/release/last-release-commit, the single source of truth for "what shipped last time"). Keep${BASELINE}in a variable for the rest of the release — steps 3, 4, and 4b all reuse it. - The previous tag (for the GitHub Release notes section title).
- The working tree state.
- A rough sense of churn (commit count + files touched).
- Confirmation that both
ghandnpmresolve to thealecs5amaccount.
If working tree isn't clean: stop. Tell the user, suggest committing or stashing.
If scripts/release/last-release-commit is missing entirely (first time after this skill change rolled out): use the last tag as baseline for this release, but still rewrite the pointer in step 5 so subsequent runs are clean.
If npm whoami errors: the bypass token in ~/.npmrc is missing or revoked. Stop and ask the user to regenerate per the "Credentials" section at the bottom of this skill.
2 — Decide the version bump
Inspect commit subjects since ${BASELINE} and apply Conventional-Commits-ish logic:
| Pattern in commits | Bump |
|---|---|
BREAKING CHANGE: body line OR !: in subject (e.g. feat!:) | major |
At least one feat: / feat(scope): | minor |
Only fix: / chore: / docs: / refactor: / test: / style: | patch |
| Pre-1.0.0 and there's a breaking change | patch (we're still in baseline shaping) |
Show the user the proposed bump in one line: "5 feats + 8 fixes → minor → v0.1.0".
3 — Draft the changelog
Group commits into buckets, dropping noise (merge commits, version bumps, pure formatting):
## Highlights
- 1-3 sentences of what users will actually feel different about.
## Features
- feat(scope): description (#PR)
## Fixes
- fix(scope): description (#PR)
## Internal / Chores
- chore: description
- refactor: description
Useful raw sources:
git log --no-merges --pretty=format:"%h %s%n%b%n---" "${BASELINE}..HEAD"— the canonical input. Read full bodies, not just subjects, soBREAKING CHANGE:footers don't slip past.gh pr list --search "merged:>=YYYY-MM-DD" --json number,title,author— PR titles are usually richer than raw commit subjects.gh api repos/alecs5am/ralphy/compare/${BASELINE}...main --jq '.commits[].commit.message'— handy if${BASELINE}is older than the local clone has fetched.
3b — Plan the docs-mintlify/ sync
The Mintlify docs site is the public-facing reference for the binary this skill ships. Out-of-date docs published alongside a new binary are a release defect, not a follow-up task.
Walk the diff under three CLI-surface paths and identify any user-visible changes:
# What CLI surface changed since the last release?
git diff --stat "${BASELINE}..HEAD" -- cli/commands/ cli/lib/ scripts/install.sh
git log --no-merges --pretty=format:"%h %s" "${BASELINE}..HEAD" -- cli/commands/ cli/lib/ scripts/install.sh
For each user-visible change, decide which Mintlify page(s) need an update. The current layout (verify with ls docs-mintlify/):
| What changed in code | Page(s) to touch under docs-mintlify/ |
|---|---|
New top-level command (e.g. ralphy foo) added in cli/commands/foo.ts | New cli/foo.mdx + add to docs.json navigation; cross-link from cli/overview.mdx |
| New / renamed flag on existing command | The matching cli/<verb>.mdx page |
Sub-command added to existing resource (e.g. ralphy project log-prompt) | The matching cli/<resource>.mdx page |
New / removed model in cli/lib/providers/ or MODELS.md | reference/models.mdx |
New env var, install path, or doctor check | quickstart.mdx and/or reference/troubleshooting.mdx |
| Conceptual change (project layout, asset pool, template kinds, log format) | concepts.mdx, introduction.mdx, or the matching authoring/*.mdx |
| Pure refactor with no user-visible delta | Nothing — note "no docs delta" in the changelog and move on |
Rules for the doc edits:
- Use
ralphy <cmd> --helpas ground truth when wording a flag or sub-command description. Don't invent flags from memory. - Examples must use the new version. Hard-coded
vX.Y.Zstrings insidedocs-mintlify/(e.g. install snippets,--versionoutput) get bumped in the same find/replace asAGENTS.mdin step 5. docs.jsonholds the nav tree. New page → add an entry under the right group. Renamed page → rename the entry. Usejqorsdto keep the file valid JSON.- Smoke-check the build if any structural change happened:
bunx mintlify dev docs-mintlify(if Mintlify CLI is installed locally) or justjq . docs-mintlify/docs.jsonto confirm the nav file is still valid JSON. Don't ship a broken docs build. docs-mintlify/lives in this repo and is deployed by Mintlify on push tomain. The version-bump commit is what triggers the docs redeploy — you don'tgh releasethe docs separately.- Don't write the edits yet. This step just produces the plan — a list of files + a one-liner per file describing what changes. The actual writes happen alongside the version bump in step 5, so they all land in one atomic commit. (Exception: if a docs edit is large or risky, stage it in the working tree now so the step-4 diff shows it — just don't commit until step 5.)
Output for step 4 review: either the list [file → change] pairs, or the literal string "no docs delta".
4 — Show diff + draft, confirm
Print to the user:
- Proposed new version (e.g.
0.0.1 → 0.1.0). - The full draft changelog (markdown).
- The diff baseline:
${BASELINE}(so the user can sanity-check what "since last release" means). - The exact list of files that will change in the bump commit:
package.json,cli/lib/version.ts,npm/package.json,AGENTS.md(single line), plus whateverdocs-mintlify/**pages came out of step 3b. - A separate trailing commit will rewrite
scripts/release/last-release-commit(step 5) — call this out so the user isn't surprised by two commits.
Wait for explicit user confirmation ("yes" / "go" / "let's go") before doing anything destructive.
5 — Bump version files + commit
Four files hold the version — keep them in sync so ralphy --version, the GitHub Release tag, the brew formula version, the npm package version, AND the AGENTS.md system-prompt line all agree. The version-bump commit also carries the docs-mintlify/ edits from step 3b and the pointer rewrite so the next /release run has a clean baseline.
NEW_VERSION="0.1.0" # without the v prefix
TODAY="$(date -u +%Y-%m-%d)"
# Core version files
jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
sd 'export const VERSION = "[^"]+"' "export const VERSION = \"${NEW_VERSION}\"" cli/lib/version.ts
jq --arg v "$NEW_VERSION" '.version = $v' npm/package.json > npm/package.json.tmp && mv npm/package.json.tmp npm/package.json
# AGENTS.md system-prompt line — the agent reads this on every session start
sd '> \*\*Current ralphy CLI: `v[^`]+`\*\* \(released [^)]+\)' \
"> **Current ralphy CLI: \`v${NEW_VERSION}\`** (released ${TODAY})" AGENTS.md
# docs-mintlify: bump any hard-coded version strings (install snippets, --version
# example output, etc). This is a sweep — actual content edits already happened
# in step 3b and should already be staged in the working tree.
PREV_VERSION="$(git show "${BASELINE}:package.json" 2>/dev/null | jq -r .version)"
if [ -n "$PREV_VERSION" ] && [ "$PREV_VERSION" != "$NEW_VERSION" ]; then
rg -l --fixed-strings "v${PREV_VERSION}" docs-mintlify/ 2>/dev/null \
| xargs -r -I {} sd "v${PREV_VERSION}" "v${NEW_VERSION}" {}
rg -l --fixed-strings "\"${PREV_VERSION}\"" docs-mintlify/ 2>/dev/null \
| xargs -r -I {} sd "\"${PREV_VERSION}\"" "\"${NEW_VERSION}\"" {}
fi
git add package.json cli/lib/version.ts npm/package.json AGENTS.md docs-mintlify/
git commit -m "chore(release): v${NEW_VERSION}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
# Diff-baseline pointer for the NEXT release. Lands in a SECOND commit so the
# tag (created in step 7) points at the clean version-bump commit and the
# pointer reflects "everything on main as of right now".
git rev-parse HEAD > scripts/release/last-release-commit
git add scripts/release/last-release-commit
git commit -m "chore(release): bump diff baseline to v${NEW_VERSION} commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
Verification before tagging: confirm all four version files agree, the pointer file matches the parent of HEAD (the release commit we'll tag), and docs-mintlify/docs.json is still valid JSON.
grep -E "0\.0\.|0\.1\.|1\.0\." package.json cli/lib/version.ts npm/package.json AGENTS.md | head -10
diff <(cat scripts/release/last-release-commit) <(git rev-parse HEAD~1) # must be empty — points at the release commit
jq -e . docs-mintlify/docs.json >/dev/null && echo "docs.json OK"
6 — Local build smoke-test (current platform only)
Don't ship a tag the local box can't build:
bun run build:bin:current
dist/binaries/ralphy-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed -e 's/x86_64/x64/' -e 's/aarch64/arm64/') --version
The full cross-compile happens on CI; this is just a sanity check. If it fails, stop and fix before tagging.
7 — Tag + push (CI builds + publishes the GitHub Release)
The tag goes on the release commit (HEAD~1 — chore(release): v${NEW_VERSION}), not the pointer-bump commit (HEAD — chore(release): bump diff baseline ...). This keeps the tag attached to the actual version bump.
NEW_TAG="v${NEW_VERSION}"
git tag -a "$NEW_TAG" HEAD~1 -m "Release ${NEW_TAG}"
git push origin main
git push origin "$NEW_TAG"
The Release GitHub Actions workflow fires on tag push: cross-compiles all 5 binaries, generates SHA256SUMS, creates the GitHub Release with assets, and writes basic release notes. Watch it finish:
gh run watch -R alecs5am/ralphy --exit-status
If you want to replace the auto-generated notes with the changelog from step 3:
CHANGELOG_FILE=$(mktemp)
cat > "$CHANGELOG_FILE" <<'EOF'
<paste the full drafted changelog from step 3>
EOF
gh release edit "$NEW_TAG" --notes-file "$CHANGELOG_FILE"
rm -f "$CHANGELOG_FILE"
8 — Update the Homebrew tap
Wait until gh run watch exits cleanly — the brew step depends on the release's SHA256SUMS asset being present.
./scripts/release/update-brew-tap.sh "$NEW_VERSION"
That script (scripts/release/update-brew-tap.sh):
- Pulls
SHA256SUMSfromhttps://github.com/alecs5am/ralphy/releases/download/${NEW_TAG}/SHA256SUMS. - Extracts per-platform hashes (darwin-arm64, darwin-x64, linux-arm64, linux-x64).
- Clones
alecs5am/homebrew-tap, regeneratesFormula/ralphy.rbwith the new version + URLs + SHAs. - Commits as
ralphy: bump to vX.Y.Zand pushes tomainof the tap.
Smoke-test the formula after:
brew update
brew upgrade ralphy # or: brew install alecs5am/tap/ralphy
ralphy --version # should match $NEW_VERSION
9 — Publish to npm
./scripts/release/publish-npm.sh "$NEW_VERSION"
That script (scripts/release/publish-npm.sh):
- Verifies
npm whoamireturns thealecs5amaccount. - Probes auth + tarball with
npm pack --dry-run(catches a bad token before bumping the version). - Bumps
npm/package.jsonto the new version (redundant safety — already done in step 5). - Runs
npm publish --access publicfromnpm/— non-interactive. Auth comes from~/.npmrc, which holds a Granular Access Token with the "bypass 2FA" capability (rotated 2026-05-14). No OTP prompt. - Verifies the registry now reports the new version.
If npm publish fails with granular access token with bypass 2fa enabled is required, the token in ~/.npmrc has been revoked or regenerated without the bypass flag. Regenerate at https://www.npmjs.com/settings/alecs5am/tokens and tick "Allow this token to bypass 2FA" (the box is under Permissions, easy to miss). Then:
echo "//registry.npmjs.org/:_authToken=npm_NEW_TOKEN" > ~/.npmrc
chmod 600 ~/.npmrc
Smoke-test:
npm view @alecs5am/ralphy version # should match $NEW_VERSION
10 — Verify all three install paths
Don't trust, verify:
# Direct from GH Release (what install.sh resolves to)
curl -fsSL "https://api.github.com/repos/alecs5am/ralphy/releases/latest" | jq -r '.tag_name'
# brew
brew info alecs5am/tap/ralphy | head -3
# npm
npm view @alecs5am/ralphy version
All three should print the new vX.Y.Z / X.Y.Z.
11 — Report
Print to the user a single-message summary with all three live URLs:
v0.1.0 published across all channels:
• GitHub → https://github.com/alecs5am/ralphy/releases/tag/v0.1.0
• brew → brew install alecs5am/tap/ralphy (formula sha256-pinned)
• npm → npm install -g @alecs5am/ralphy@0.1.0 (binaries auto-fetched)
Pitfalls to avoid
- Don't
gh release createmanually. CI does it on tag push. Manually creating the release would race with the workflow and produce duplicate assets. - Don't run
update-brew-tap.shbefore CI is done. It pulls SHAs from the release'sSHA256SUMSasset; if CI hasn't uploaded it yet, the script aborts with a friendly error. Rungh run watchbetween step 7 and step 8. - Don't bypass 2FA on npm.
npm publish --otp=<code>is fine if you can get the OTP into the script's stdin (e.g. viaread), but--ignore-scriptsstyle bypasses are not — they leave the package unpublishable without a manualnpm unpublish. - Don't reuse a published version. If
update-brew-tap.shfails halfway, don't delete + retag — bump to the next patch instead. Published versions on npm and brew are one-shot. - Don't commit
npm/vendor/. It's the postinstall download cache and is gitignored. Re-runningnpm installalways re-fetches it. - Don't
bun run build:bin(the full cross-compile) locally as part of the release. CI does it; the local build is just the current-platform sanity check. - Don't bump major before 1.0.0. Pre-1.0, breaking changes ride in
feat:and get minor bumps. After 1.0.0,feat!:/BREAKING CHANGE:triggers major. - Don't skip the docs-mintlify sync (step 3b). A release that ships a new CLI verb but leaves
docs-mintlify/stale is a public docs defect — Mintlify auto-deploys on push tomain, so out-of-date pages go live the moment you push the bump commit. If the diff is genuinely doc-irrelevant, say "no docs delta" in the changelog and move on; don't silently skip the check. - Don't forget to bump
scripts/release/last-release-commit. It's the diff baseline for the next release. If you forget, the next/releaseeither falls back to the previous tag (works, but loses any post-tag commits the pointer was meant to capture) or — worse — re-includes the version-bump commit itself in the next changelog. - Don't tag the pointer-bump commit. The tag belongs on the
chore(release): v${NEW_VERSION}commit (HEAD~1after step 5), not on the trailing pointer commit. Tagging the wrong commit meansgit describereturns one short of the real release.
When NOT to fire
- The user is mid-development on a feature branch (you'd be tagging unfinished work).
- The local current-platform build fails — fix the build first.
- There are no commits since the last tag (no-op release).
- The change is only in
landing/,docs/(internal playbooks),.agents/skills/, or a non-CLI directory — that's not what this skill ships. Note: a docs-only change insidedocs-mintlify/also doesn't need a tagged release — Mintlify auto-redeploys on any push tomain. Just commit and push without invoking/release. - The user said "do not release" / "let's check first" earlier in the same conversation.
npm whoami/gh auth statusis not the right account.
State sources
- Current version:
package.json→.version(single source of truth, mirrored tocli/lib/version.tsandnpm/package.json). - Diff baseline (what shipped last time):
scripts/release/last-release-commit— a one-line file containing a single git SHA, rewritten by step 5 of every release. Falls back togit describe --tags --abbrev=0only if missing/stale. - Previous tag (for release-notes header / GH compare URL):
git describe --tags --abbrev=0. - Default branch:
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name. - Build script:
scripts/build-binaries.ts. - Brew tap repo:
alecs5am/homebrew-tap(overrideable viaRALPHY_BREW_TAP). - npm package:
@alecs5am/ralphy, lives innpm/of this repo. - Public docs site (CLI reference):
docs-mintlify/— content +docs.jsonnav config. Auto-deployed by Mintlify on push tomain. - Helper scripts:
scripts/release/update-brew-tap.shandscripts/release/publish-npm.sh. - Release workflow:
.github/workflows/release.yml(fires onpush: tags: ['v*']).
Credentials (where they live)
The skill assumes these are persisted locally — it never asks for them mid-run:
- GitHub (gh CLI keyring) —
gh auth statusshould showalecs5amas active. Used for:git push,gh release edit,gh repo clone alecs5am/homebrew-tap. Rotate viagh auth loginif revoked. - npm —
~/.npmrcline//registry.npmjs.org/:_authToken=npm_XXX, chmod 600. Must be a Granular Access Token with "Allow this token to bypass 2FA" enabled, scoped to@alecs5am/*with read+write permission. Rotate at https://www.npmjs.com/settings/alecs5am/tokens.
npm whoami and gh auth status --hostname github.com are the two pre-flight checks. If either fails, stop and ask the user to refresh the relevant credential before proceeding.
