with one click
release
// Publish a release — version, release notes, CI, Homebrew tap, tag, and GitHub release.
// Publish a release — version, release notes, CI, Homebrew tap, tag, and GitHub release.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | release |
| description | Publish a release — version, release notes, CI, Homebrew tap, tag, and GitHub release. |
| user-invocable | true |
End-to-end skill for cutting a release of an existing project. Covers discovery, versioning, release notes, CI workflow setup, Homebrew tap, tagging, and GitHub release creation.
marcelocantos (personal)mastermarcelocantos/homebrew-tap — uses homebrew-releaser GitHub Action for automated formula generation and publishingvMAJOR.MINOR.PATCH). Always suggest minor releases (bump MINOR, reset PATCH to 0). Patch releases are reserved for hotfixes to a specific minor release — never use them for regular forward progress. Only use major/patch when the user explicitly requests it.foo → foo2, see Phase B.3) rather than a major bump. Before cutting 1.0, the public API must have accumulated at least 1 month with no backwards-incompatible changes since the last breaking release. Historical SemVer practice was to scale shakeout by surface size (3+ months for >50 items); in the LLM-coding era, real-world API exercise compresses sharply, so a flat 1-month minimum suffices regardless of surface size. If a breaking change is judged necessary mid-shakeout, the clock resets from the new breaking release's tag date. See B.3a for the gate.gh release create locally. CI workflows must never create tags or releases — they only build artifacts and upload them to the existing release.The skill has substantial parallelizable structure. Default to running independent work in parallel — the wall-clock and token wins are large, and the global directive's "default to parallel" bias applies with full force here. The skill historically read as a serial pipeline; it is not one.
Fan out Phase B.1's audits (items 7–11). CLI flag audit, agent-guide check (incl. gotcha staleness), README freshness, vendor licence attribution, and language-binding test coverage are independent read-only investigations with bounded outputs. Spawn each as a Sonnet subagent (subagent_type: Explore or general-purpose with model: sonnet) and assemble the digests in the parent. These subagents must not commit, push, or open PRs — they investigate and report. Any fixes the audits surface are made in the parent context as part of the release-prep PR.
Run B.5, B.6, and B.7 concurrently. Once discover.sh output is parsed and the version number is decided in B.4 step 2, these substeps have no cross-dependencies:
Bash(run_in_background: true) first so it overlaps everything else.git diff against the public surface.Collect B.7's background result before committing the version bump and pushing the PR — a failing test run halts Phase B regardless of how clean the docs and workflow look.
Phase C step 7 (Go submodule tags) runs in parallel with step 8 (gh run watch). They touch disjoint state.
The CI wait between PR push and gh run watch returning green has no useful parallel work if the audit fan-out happened before the PR was pushed, which is the whole point of fanning out early. Late-arriving audit findings force a second PR cycle. If you find yourself wanting to do real work during the CI wait, the fan-out fired too late.
The user runs /release. No arguments needed — the skill discovers everything from the repo.
/release runs in three phases. The ideal scenario is fully unattended from start to finish — only stop and ask the user when there is a genuine reason to.
Phase A — Up-front clarification. A very quick analysis to identify any information likely to be needed from the user given the specific context of the work being released. If nothing is genuinely uncertain, ask nothing and move straight to Phase B. Do not invent questions; do not ask out of habit. The default is zero questions.
Phase B — Unattended execution to a mergeable PR. Run the entire prep workflow without per-phase approval gates: discovery, breaking-change audit, version bump, release notes, CI setup, push, PR open, CI wait, gate check. End by reporting current state, anything messy that happened during the run, and any concerns. Then only stop for confirmation if a serious concern arose about whether it's appropriate to complete the release (failing tests rewritten without justification, a breaking change found, an unresolved CI failure, a missing licence attribution, etc.). If everything is clean, proceed to Phase C without asking.
Phase C — Complete the release. Squash-merge the release PR, tag, run gh release create, monitor CI, install locally, report.
The detailed substeps below are sequenced under these three phases. Where the previous workflow asked "Proceed to Phase N+1?" between substeps, that question is gone — substeps run back-to-back unless something in Phase B's concerns list fires.
Before doing any work, do a fast scan of the repo (latest tag, commits since, working-tree state, version era, project type) and decide whether any of the following are genuinely ambiguous:
open_prs_with_release_work reports a count > 0 from discover.sh). Each listed PR has commits not in the latest tag, so the release scope is genuinely ambiguous: the user may want to (a) wait for the PR to merge and release the resulting master, (b) bundle release-prep into the existing PR rather than open a sibling release-prep PR (honouring "one PR per session"), or (c) release current master independently and let the PR merge into a later release. Ask which — naming each open PR by number, ahead-count, and title from the discover.sh output. Skip the question only if the listed PRs are clearly orthogonal to the release (e.g., long-running dependabot bumps, draft RFCs); the default is to ask.If none of these apply, ask nothing. Proceed to Phase B silently. The whole point is that asking is the exception, not the rule.
If something does apply, ask only the specific questions that matter, in one batch. Do not enumerate the full discovery report — the user already knows what they pushed.
Run the substeps below back-to-back without approval gates. Only halt at the end of Phase B if a serious concern arose.
Assess the project's current release state. Start by running the companion discovery script:
~/.claude/skills/release/discover.sh
(It is already chmod +x — do not wrap it in bash, just invoke the path as the command.)
This script gathers all Phase 1 data and the inputs Phases 2 and 3 need (latest tag, commits since last tag, version era, build system, project type, CI workflows, Homebrew tap, repo description, version macros, vendored dependency licences, working tree status) in one invocation. Do not separately run git tag, git log <last-tag>..HEAD, or gh release list — the script already emits that data. Parse its output, then verify or supplement the following items as needed:
Existing releases: The script's # tags, # latest_tag, and # releases sections cover this. # latest_tag is the most recent semver tag — use it directly for subsequent phases.
Build system: Determine how the project builds — mkfile, Makefile, go build, cargo, cmake, etc. If the project has a mkfile, run mk --help-agent to get build instructions and understand the mk syntax. mk binaries are available from https://github.com/marcelocantos/mk. Check for a distribution generation target (e.g., make dist, mk dist) that produces release artifacts (amalgamated headers, bundled files, etc.) that are checked into the repo. Note the target name for Phase 5.
Project type: Determine whether the project produces standalone binaries or is a library/tool that users consume as source. This affects whether CI binaries and Homebrew tap are relevant.
CI workflows: Check for existing .github/workflows/ files, especially any release-related workflows.
Default-branch CI status — discover.sh reports the conclusion of the latest completed push run on the default branch as # default_branch_ci_status (format: <conclusion>\t<workflow>\t<runId>\t<url>). Read it before doing anything else in Phase 1, because it tells you whether you're starting from a healthy baseline or from a broken one:
success — proceed normally.failure / cancelled / timed_out / action_required — stop and triage. The next push to the default branch (whether the release-prep PR or anything else) will inherit the same failures unless you address them. Pull the failing job log (gh run view <runId> --log-failed | tail -60), classify what broke, and decide:
test job is not infrastructure; a red Deploy to Fly.io job whose needs: doesn't gate artifact upload usually is.swift test failure when only go test is wired into CI): create a follow-up bullseye target for the fix, note it in the release-prep PR's "Known issues" / "Deferred" section, and proceed. Do not silently delete the broken tests as part of the release-prep PR — that's scope creep and obscures the regression.skipped / neutral — read the job names; usually fine, but worth a glance.(no completed CI runs on <branch>) — first-release-of-a-new-repo case. Proceed normally; CI will be exercised by the release-prep PR.Homebrew tap: Check if marcelocantos/homebrew-tap exists and whether it already has a formula for this project. Also check that the HOMEBREW_TAP_TOKEN action secret is set on this repo — discover.sh reports this as homebrew_tap_token_secret (set / missing). homebrew-releaser reads this secret to push the generated formula into the tap; when it's missing, the job fails with the unhelpful error "You must provide all necessary environment variables." on the first release. First-release-of-a-new-repo is the common case — resolve it now from 1Password (see Phase 4 step 2 for the op read + gh secret set commands) rather than discovering it post-tag and having to re-run the failed homebrew-releaser job by hand.
Tap opt-out. Some projects deliberately skip Homebrew distribution — e.g., a package manager that replaces Homebrew can't coherently ship via a tap. Opt out by adding a homebrew_tap: disabled directive to the project's CLAUDE.md (mirrors existing directives like delivery: and profile:). discover.sh honours the directive and reports # homebrew_tap as (disabled — CLAUDE.md declares homebrew_tap: disabled) and # homebrew_tap_token_secret as (n/a — tap disabled). When you see either sentinel, skip the tap checks entirely, skip Phase 4 step 2 (homebrew-releaser job), and skip Phase 5 step 9 (local brew install verification). Note the opt-out in the Phase B report instead.
Repo description: Check that the GitHub repo has a description set (gh repo view --json description). homebrew-releaser crashes on null descriptions. If missing, set one with gh repo edit --description "...". Also verify the description is accurate and up to date — stale descriptions (e.g., referencing renamed concepts) should be updated.
Fan out items 7–11. The next five items are independent read-only investigations. Spawn one Sonnet subagent per item in a single message (multiple Agent tool uses in one assistant turn) and have each return a short digest. Spawn-prompt rule: "investigate and report findings — do not commit, do not push, do not open a PR. The parent will collect digests and decide what to fix in the release-prep PR." See the Parallelization section above. Items 1–6 and item 12 stay in the parent — they are trivial reads of discover.sh output or single git status calls.
CLI flags audit: If the project produces standalone binaries, check that the following flags exist and work:
--version: Search the codebase for how the version string is set — hardcoded strings, constants, build-time injection (e.g., -ldflags -X, #define VERSION). Report whether it exists, where the string is defined, whether it matches the latest tag, and how it gets updated. The Homebrew formula test block relies on this.--help: Verify the binary prints usage information. Most CLI frameworks provide this automatically.--help-agent: Check whether the binary can emit its agent guide (e.g., agents-guide.md or AGENTS-<PROJECT>.md) for use by coding agents. The output should be prefixed with the --help usage text (flags and descriptions) so agents get both CLI reference and domain guide in one call. For Go programs, embed the guide with go:embed and capture flag.PrintDefaults() into a buffer to prepend it. For other languages, equivalent embedding or a bundled string constant.Flag any that are missing.
Agent guide: Check whether the project has an agents-guide.md (or equivalent). This applies to all project types — both standalone binaries and libraries:
agents-guide.md in the project root (or co-located with dist files if the library distributes as dist/). The guide should cover: what it does, how to include it, key API surface, common patterns, and gotchas. Flag if missing.agents-guide.md as the source for --help-agent output (checked in step 7 above). If --help-agent exists but there's no standalone agents-guide.md, that's acceptable. If neither exists, flag both.Also verify that the README mentions the agent guide for discoverability (e.g., "If you use an agentic coding tool, include agents-guide.md in your project context").
Gotcha staleness check: If the agent guide contains a "Gotchas" section (or equivalent list of known caveats), read each gotcha against the commits in this release. A release that fixes a behaviour previously described as a gotcha leaves a stale entry behind — future agents reading the guide will work around a problem that no longer exists, or worse, re-introduce it defensively. Look especially for commits whose messages mention parity fixes, removed workarounds, or "no longer needed" language. For each stale gotcha, either delete it, or rewrite it to reflect the new behaviour (e.g., a "was a hazard, now fixed" note if the historical context is useful). Flag any you find as a release-PR change rather than silently merging release notes over a stale guide.
MCP servers (detected by MCP dependencies in the manifest, a serve subcommand, or "MCP" in the project description): The agents-guide and README must include complete installation instructions. The agents-guide must explicitly frame installation as a multi-step process and state that installation is not complete until all steps succeed — agents that see only brew install will stop there.
Required steps (all must be documented):
brew install marcelocantos/tap/<project>)brew services start <project>) — if the server runs as a persistent daemon, a Homebrew service definition is required (see Phase 4 step 3). Flag if the project listens on a port but has no service definition.claude mcp add --scope user --transport http <name> http://localhost:<port>/mcp (global install to ~/.claude.json)lsof -iTCP:<port> -sTCP:LISTEN to confirm the process is listening. Include an explicit warning not to use curl — MCP endpoints only respond to POST requests with a JSON-RPC body, so a plain GET or empty POST returns nothing, which agents misread as "server not ready" and enter unnecessary diagnostic loops.These instructions must be specific and exact — not vague pointers. Agents that lack precise commands will improvise incorrect paths (wrong config files, wrong scope flags, wrong binary names, plain HTTP health checks). Flag any missing or imprecise steps.
README: Check that a README.md (or README) exists in the repo root and covers the essentials: what the project is, how to install or build it, how to use it, and a licence mention. A missing README is a blocker — every public release needs one. Flag if missing or if key sections (install/build, usage) are absent.
Content freshness: Compare the README's feature/syntax documentation against the current codebase (CLAUDE.md syntax section, agent guide, or equivalent authoritative source). Flag any features present in the agent guide or CLAUDE.md that are missing from the README. New features being released should be documented in the README before tagging.
Quick start for agent-installed tools: If the project is an MCP server or agent tool, the README should include a "Quick start" section with a copy-pasteable prompt that users can give their agent (e.g., "Install X from <repo URL> — brew install, start the service, register it as an MCP server, and restart the session. Follow the agents-guide.md in the repo."). This is distinct from the agents-guide (which the agent reads) — it's for the human who wants to say "install this" without spelling out every step. A fenced code block is ideal since GitHub renders a copy button on them. Flag if missing.
Third-party licence attribution: Scan the project for vendored or bundled third-party code — check vendor/, third_party/, extern/, or similar directories, and any headers/sources copied into the project. For each dependency found:
This check applies to all dependency types: vendored submodules, copied header-only libraries, embedded source files, and generated/bundled code.
Language bindings / wrappers: Check for language-specific bindings or wrappers in the repo (e.g., go/, python/, wasm/, or similar directories). If found, verify their test suites cover the features being released. Flag any new features that lack binding-level tests. Bindings that lag behind the core implementation should be updated before tagging.
Working tree: Verify the working tree is clean and up to date with the remote. If there are uncommitted changes or unpushed commits, flag them before proceeding. If the changes are unrelated WIP, the standard resolution is: git stash push -u -m "WIP: ...", proceed with the release, then git stash pop at the end. Always restore the stash after the release completes.
Ahead-N handling — when discover.sh reports unpushed ≥ 1 on the default branch (not a feature branch), the choice between "fast-forward push then PR for release-prep" vs "single bundled PR" depends on the repo's merge strategy. Read # merge_strategy from the discover.sh output:
merge-commit-allowed — fast-forward push the unpushed commits to origin first, then open the release PR containing only the release-prep commit(s). The unpushed commits' atomic history survives on master verbatim. Do this automatically; don't ask the user.squash-only (the common case for owned repos under the global ~/.claude/CLAUDE.md policy) — bundle the unpushed commits AND the release-prep commits into a single feature branch, open one PR, squash-merge to master. The squash collapses the per-commit history on master, but the per-commit detail is preserved on GitHub forever via the PR's commit list. Do not push to master directly; the global "always PR-flow" directive applies. Do this automatically; don't ask the user. Note the squash collapse in the Phase B report.rebase-allowed (without merge-commit) — same as squash-only. Rebase-merge would preserve atomic history on master, but for the release skill's purposes the bundled PR is cleaner.(gh api failed) or (gh not available...) — assume squash-only and proceed with the bundled PR. This is the safer default.If the fast-forward path is selected but not practical (origin has commits not in local master, i.e. git push would require a merge or rebase), fall back to the bundled PR. Note the collapse in the Phase B report.
Do not use the two-PR variant (squash unpushed in one PR, release prep in another) under any merge strategy — same history loss as the bundled PR with double the CI churn, no upside.
Resolve this before committing any release-prep changes so Phases 4–5 have a clean working tree to operate on.
Proceed straight to B.3 — no approval prompt.
Skip this substep if discover.sh's # version_era reports pre-1.0.
For post-1.0 projects, audit all changes since the last release for backwards-incompatible changes. This is a hard gate — if breaking changes are found, the release must not proceed.
What constitutes a breaking change:
Audit procedure:
Diff the public surface: Run git diff <latest-tag>..HEAD against the project's public-surface files — exported headers (*.h, *.hpp outside internal/ or private/), exported source symbols, CLI entry points (flag/subcommand definition sites), and any wire/config schema files. For Go, focus on capitalised identifiers in non-internal/ packages; for Rust, on pub items outside pub(crate); for C/C++, on items in installed headers.
Classify each change as additive (new functions, new optional fields, new CLI flags) or breaking (anything in the list above). Additive changes are fine for a minor release.
If no breaking changes found: Report the audit results and proceed to B.4.
If breaking changes found: This is a serious concern that halts Phase B. Report each breaking change with specific file:line references. Explain that post-1.0 breaking changes are not permitted as a minor or patch release.
The project's stance on breaking changes is absolute: there is no "v2.0" of the same product. If a project genuinely needs to break backwards compatibility, the correct path is to fork the project into a new product. For example, foo would become foo2 — a new repository (or a hard fork of the existing one) starting at v0.1.0. The original foo continues to exist at its last stable version for existing users.
Present this recommendation to the user and halt Phase B. Do not proceed with the release.
Skip this substep unless the user has explicitly requested a v1.0.0 cut from a pre-1.0 version. The agent never initiates a 1.0 transition; this gate only fires when the user has asked for one.
A 1.0 release locks in a backwards-compatibility contract — after 1.0, breaking changes require forking the project (e.g., foo → foo2) rather than a major bump. The pre-1.0 period exists to shake out the API design before that contract takes effect.
The shakeout rule: at least 1 month must have elapsed since the last release that introduced backwards-incompatible changes to the public API.
Audit procedure:
Identify the last breaking release. Walk back through the project's tags (use # tags from discover.sh) and identify the most recent tag that introduced a breaking change. If the project's STABILITY.md or release notes call this out explicitly, use that. Otherwise, diff each tag's public surface against its predecessor (same procedure as B.3) and find the most recent break. If unclear, ask the user.
Compute the gate date. gate_date = <last-breaking-tag-date> + 1 month. Read the tag date with git log -1 --format=%aI <tag>.
Compare against today.
gate_date: the shakeout is complete. Proceed to B.4.gate_date: the shakeout is not complete. Halt Phase B and report:
not_before: date in the acceptance criteria, with a clock-reset clause).Bullseye integration: projects pursuing 1.0 should encode the shakeout as an explicit sub-target blocking the showcase target, rather than burying the requirement in prose. This makes the prerequisite visible to /cv so it stops recommending the 1.0 cut prematurely. The target name pattern is " has accumulated a 1-month shakeout period with no breaking API changes since ". When this gate halts Phase B for a project that has no such sub-target, offer to create one.
Determine the next version number. Do not ask for confirmation — just use the version determined below.
Changes since last release: Use the # commits_since_last_tag output already produced by discover.sh — do not re-run git log. Summarise the changes.
Determine version: If # latest_tag is (none), use v0.1.0. Otherwise, bump MINOR and reset PATCH to 0 (e.g., v0.1.0 → v0.2.0, v1.3.0 → v1.4.0). Only use a different bump if the user explicitly requested one.
Update version string: If the project has a hardcoded version string (found in Phase 1 step 7), update it to match the new version. Commit the change before proceeding. If the version is injected at build time (e.g., via -ldflags or CI env vars), verify the injection mechanism uses the tag correctly and no manual update is needed.
C/C++ libraries with version macros: If the header defines version macros (e.g., #define PROJECTNAME_VERSION "x.y.z" with _MAJOR, _MINOR, _PATCH companions), update all four macros to match the new version. Verify consistency: the string must equal "MAJOR.MINOR.PATCH".
Go module version constants: If the project has Go wrapper modules with
version constants (e.g., Version = "x.y.z" with VersionMajor,
VersionMinor, VersionPatch), update them to match the new version.
Top-level package Version constant: Some Go libraries expose a
single top-level const Version = "x.y.z" (typically in a small
version.go at the repo root) so consumers can report the library
version at runtime. Detect with
grep -l '^const Version = ' *.go 2>/dev/null at the repo root —
if a match exists, update the quoted string value to the new version
with the leading v stripped (e.g., v0.10.0 → "0.10.0"). The
tag itself keeps the v prefix; only the constant drops it.
No version macros found: If a C/C++ library has no version macros at all, note this as a gap in the Phase B report. Don't block the release.
B.5 (release notes), B.6 (release.yml creation), and B.7 (gate check incl. tests) run concurrently. The minimal harness:
Bash(command: "<project-test-command>", run_in_background: true)
The harness will notify when the test run finishes — do not poll, do not sleep.git add / git commit operations on the parent's working tree; they don't conflict with the background test run.If any of the three fail, halt Phase B per the existing rules — the parallel structure does not change what counts as a serious concern, only when the work happens. See the Parallelization section.
Draft release notes from git history.
Gather material: Use the # commits_since_last_tag output from discover.sh — do not re-run git log. Read the commit subjects (and look up merged PRs if needed).
Draft: Write concise release notes. Group changes by category where natural:
Don't force categories — if there are only a few changes, a simple bullet list is fine.
Display: Print the draft release notes in the transcript so the user can see them. Do not wait for approval — proceed immediately. (The changelog-reviewed gate is automated, not manual.)
Skip this substep if the project is a library without standalone binaries, or if a release CI workflow already exists and is working.
Create release workflow: Create .github/workflows/release.yml that triggers on release events (types: [published]). The workflow must only build and upload artifacts — it must never create tags, releases, or draft releases (the release already exists, created by gh release create in Phase 5). The workflow should:
<project>-<version>-<os>-<arch>.tar.gzgh release uploadCross-platform builds with CGO: When the project requires CGO (e.g., for SQLite via go-sqlite3), cross-compilation is painful. Prefer native ARM runners (ubuntu-24.04-arm) for linux-arm64 builds over installing a cross-compiler (gcc-aarch64-linux-gnu). Native runners are simpler and more reliable. Use runs-on: ${{ matrix.os }} with per-target runner entries in the matrix. macOS arm64 builds on macos-latest (Apple Silicon) natively.
Generated/embedded files: If the Makefile has copy or generate steps that feed go:embed (or similar compile-time embedding), CI must replicate those steps before building. For example, if the Makefile copies agents-guide.md to internal/cli/help_agent.md for go:embed, add an explicit step in the workflow (e.g., cp agents-guide.md internal/cli/help_agent.md) before the build step. Check for Makefile prerequisites of the build target that produce files listed in .gitignore — these are generated files that CI won't have.
mk-based projects: If the project uses mkfile instead of Makefile, CI must install mk before building. Fetch the appropriate binary from https://github.com/marcelocantos/mk/releases. Example step:
- name: Install mk
run: |
MK_VERSION=$(gh release view --repo marcelocantos/mk --json tagName -q .tagName)
curl -sL "https://github.com/marcelocantos/mk/releases/download/${MK_VERSION}/mk-${MK_VERSION#v}-linux-amd64.tar.gz" | tar xz -C /usr/local/bin mk
env:
GH_TOKEN: ${{ github.token }}
Adjust the OS/arch in the tarball name to match the runner. Use mk instead of make in all build and test steps.
Add homebrew-releaser job: Add a job that runs after binaries are uploaded, using homebrew-releaser:
homebrew:
needs: build # wait for binary uploads
runs-on: ubuntu-latest
steps:
- uses: Justintime50/homebrew-releaser@v3
with:
homebrew_owner: marcelocantos
homebrew_tap: homebrew-tap
github_token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
formula_folder: Formula
version: ${{ github.event.release.tag_name }}
install: 'bin.install "<project>" => "<project>"'
target_darwin_arm64: true
target_linux_amd64: true
target_linux_arm64: true
test: 'system bin/"<project>", "--version"'
skip_checksum: true
update_readme_table: true
Key setup requirements:
HOMEBREW_TAP_TOKEN secret: A shared PAT for all repos is stored in 1Password. To add it to a new repo:
op read "op://Personal/GitHub Homebrew Tap PAT/token"gh secret set HOMEBREW_TAP_TOKEN --repo <owner>/<repo> and paste the token value.homebrew-tap repo must exist at marcelocantos/homebrew-tap with a Formula/ directory.<project>-<version>-<os>-<arch>.tar.gz where <version> has no v prefix (e.g., myapp-1.2.0-darwin-arm64.tar.gz). homebrew-releaser strips the v from the tag when searching for assets.install and test fields must match the project's actual binary name and CLI interface.depends_on if the binary needs runtime dependencies.Known homebrew-releaser issues:
skip_checksum: true is required when HOMEBREW_TAP_TOKEN is scoped to the tap repo only. Without it, homebrew-releaser tries to upload checksum.txt to the source repo's release and gets a 403.TypeError: 'NoneType' object is not subscriptable) if the repo description is null. Set it with gh repo edit --description "..." before the first release.version input, homebrew-releaser auto-detects the version from download URLs. Platform-specific URLs like foo-1.0.0-darwin-arm64.tar.gz can confuse the parser — it may extract "64" from "arm64" instead of "1.0.0". Always set version: ${{ github.event.release.tag_name }} to override auto-detection.Homebrew service definition (conditional — persistent servers only): If the project is a long-running server (detected by: listening on a port, --addr flag, serve subcommand, MCP server), the Homebrew formula needs a service definition so brew services start <project> works. There are two approaches:
formula_includes in homebrew-releaser: Add a formula_includes field with a Ruby service block that configures launchd (macOS) and systemd (Linux). Example:
formula_includes: |
service do
run [opt_bin/"<project>"]
keep_alive true
log_path var/"log/<project>.log"
error_log_path var/"log/<project>.log"
end
marcelocantos/homebrew-tap directly after the first release.The agents-guide should document both macOS (brew services start) and Linux (systemd --user unit file) setup. Flag if the project is a persistent server but has no service definition.
Verify: Show the workflow file to the user for review. Commit it to master and push before tagging.
Enforce the project's delivery gates before releasing.
## Gates section from CLAUDE.md to determine the
profile (default: base).~/.claude/gates/base.yaml and the profile YAML (if not base).
Merge them: profile gates add to base; override: [gate: skip]
removes specific base gates.pre-release gate (in addition to any pre-merge gates
that haven't already been satisfied):
manual gate is a serious concern in the Phase B sense — surface it in the Phase B report at the end of execution rather than stopping mid-stream. Do not present its prompt as an in-flight blocker.Run env-gated live tests as part of the tests-exist check. Many
projects gate expensive or API-costing tests behind an environment
variable (CLAUDIA_LIVE=1, RUN_LIVE_TESTS=1, etc.) so they don't
run during routine local development or in contributor CI. These
tests are the exact ones that exercise the user-facing flow —
skipping them at the release gate defeats the whole point of gating
them separately. When running the test suite to verify
tests-exist, inspect the project for such env var conventions
(grep t.Skip.*Getenv\|os.Getenv.*== "" in _test.go files, or
check README/CONTRIBUTING for a "how to run live tests" section) and
re-run the suite with those variables set. Do not rely on the
default go test ./... / cargo test / equivalent to exercise
API-costing tests — its silence on those tests is by design, and
also silent about whether the user-facing flow works.
If no such gating convention exists, the default run is sufficient.
If the project has a specific command for live tests (e.g., a
Makefile target like make test-live), prefer that over
reconstructing the env vars by hand.
This check is part of the tests-exist gate in spirit even if the
gate yaml doesn't mention it explicitly. A "tests exist" pass that
only ran the non-live subset leaves the user-facing flow unverified
and is not sufficient for a release.
Release-prep goes through a PR. The pr-workflow pre-merge gate means release-skill work routes through a feature branch and PR, not a direct push to master. The release PR carries the version bump and any doc changes; CI must go green before squash-merge. If the project has no CI, the pr-workflow gate still applies — you still need to go through a PR, but CI waits are zero.
Release-prep PR is sometimes a no-op — skip it when there's nothing to commit. When all release content has already landed via earlier PRs and the release introduces no new commits (no in-source version string to bump, no STABILITY.md snapshot to update, no dist regen, no release-notes file committed to the repo), there is nothing to PR. Opening an empty PR for the sole purpose of satisfying the pr-workflow gate creates churn without value: an empty diff, a green-CI placeholder, an immediate squash-merge. Don't do it. The pr-workflow gate is satisfied by the PRs that landed the changes being released — those already routed through a feature branch and CI. The release event itself (the gh release create in Phase C) is not a content change to master and doesn't need its own PR. Detect this case by checking that (a) the working tree is clean, (b) there are no in-source version strings to update, (c) discover.sh's dist target is empty, and (d) there are no other release-prep edits the skill would normally make. If all four hold, skip the PR step and proceed straight to Phase C with a one-line note in the Phase B report explaining the skip.
Always squash-merge release PRs via ~/.claude/skills/push/merge.sh, never via raw gh pr merge. After a squash-merge, local master has N pre-squash commits while origin/master has one squash commit with a different SHA — git pull fails to fast-forward, rebase re-applies already-merged content, and merge would create a merge commit (forbidden under squash-only). The only safe resolution is git reset --hard origin/master, which is normally a user-only operation. merge.sh bundles the squash-merge, the fetch, the checkout, the hard reset, and the local feature-branch cleanup into a single vetted script — pre-authorising the reset by virtue of being a known script. Invoke as:
~/.claude/skills/push/merge.sh <pr-number> master <feature-branch>
Calling gh pr merge --squash directly leaves the user staring at a diverged local master with no clean recovery — they have to run git reset --hard origin/master by hand every time. That is the bug merge.sh exists to prevent.
After CI is green on the release PR, produce a brief report covering:
Then decide whether to proceed unattended. Default is yes — proceed straight into Phase C without asking. Only stop and ask if a serious concern arose during Phase B that warrants user review before crossing the merge-to-master line:
manual pre-merge or pre-release gate fired (surface its prompt now).Routine items are not serious concerns: a clean changelog display, a successful dist regen, a normal version bump, expected two-PR flow, etc. Don't ask just to confirm something the user already implicitly authorised by running /release.
If the report shows no serious concerns: print the report, say "proceeding to Phase C", and continue.
If a serious concern fires: print the report with the concern called out at the top and ask one focused question ("merge anyway, or stop here?").
Squash-merge the prepared PR(s), tag, and create the GitHub release. Run unattended unless something fails along the way.
Squash-merge the release PR via ~/.claude/skills/push/merge.sh <pr-number> master <feature-branch>.
Validate version strings: Before tagging, verify that any in-source version strings match the release version. For C/C++ projects with version macros, check that the #define values match the tag (strip leading v). Fail early if they don't — the version commit from B.4 should have already handled this, but double-check.
Push: Ensure all commits (version bump, etc.) are on master before tagging. After the squash-merge in step 1, merge.sh already left the local master aligned with origin/master; double-check.
Regenerate distribution files: If B.1 identified a dist generation target (e.g., make dist), run it now. If it produces any changes, commit them on master (e.g., "Regenerate dist for <version>") and push before tagging. This ensures the release tag includes up-to-date distribution artifacts.
Create the release: Use gh release create which both tags and creates the release:
gh release create <version> --title "<version>" --notes-file <notes-file>
This triggers the release.yml workflow, which builds binaries, uploads them, and (if configured) runs homebrew-releaser to update the tap formula automatically.
Sync local tags with the remote: gh release create creates the
tag on the remote but does not update the local .git/refs/tags/.
Any tool that reads local tags immediately after a release (another
/cv run, a manual git describe, a subagent spawned for follow-up
work) will see the previous tag as the latest, re-detect the
already-shipped commits as unreleased, and potentially recommend
shipping them again. Close the gap here:
git fetch --tags
This is a git-protocol round-trip, not a gh API call — fast and
cheap. Run it unconditionally after every gh release create. The
downstream invariant this enforces is: "after /release returns,
local tags reflect reality." Several other skills (notably /cv's
Step 0.5 "unreleased fixes" check) rely on that invariant holding
and explicitly do not call gh for latency reasons, so it must
be the release skill that keeps local state in sync.
Go module tags: If the project contains Go modules in subdirectories
(e.g., go/sqlpipe/go.mod), create subdirectory-prefixed tags for each
Go module so that go get can resolve them. For a module at path
go/sqlpipe and release version v0.11.0, create and push:
git tag go/sqlpipe/<version> <version>
git push origin go/sqlpipe/<version>
Also update the Go module's version constants (if any) to match the release version during the Phase 2 version bump.
Run this in parallel with step 8. The submodule tag push and gh run watch touch disjoint state. Kick step 7 off as a foreground action and start step 8 immediately after — or run step 8 in the background and do step 7 while it watches.
Monitor CI: Wait for the release workflow to complete in full. The workflow's homebrew job has needs: build, so a successful gh run watch guarantees both that artefacts are uploaded and that homebrew-releaser has pushed the formula to the tap.
gh run list --workflow=release.yml --limit=1
gh run watch <run-id>
If it fails, help diagnose — do not delete the release or tag without asking. Do not start the brew install in step 9 until this returns success — running brew update against a tap that hasn't received the formula yet will install the previous version.
Install locally: Only after step 8 returns success. This step is mandatory for projects with a Homebrew tap — do not skip it and do not ask for permission.
brew update
log=$(mktemp -t brew-upgrade-<project>.XXXXXX) && trap 'rm -f "$log"' EXIT
brew upgrade marcelocantos/tap/<project> 2>&1 | tee "$log" || \
brew install marcelocantos/tap/<project> 2>&1 | tee "$log"
brew update is required to pull the fresh formula from the tap. Use upgrade || install so the command works whether or not the formula is already installed. Always tee the full output to a temp file — when something goes wrong, a tail -5 of the truncated output never shows the actual failure mode, and you'll be guessing instead of diagnosing. Keep the mktemp path so you can inspect it if the verify step below fails.
Persistent services: If the project is a long-running server with a Homebrew service definition (detected in Phase 4 step 3), restart the service so the new binary takes effect:
brew services restart <project>
Verify the install: Run <project> --version (or the equivalent) and confirm the output matches the released version. If it doesn't match, inspect "$log" first — the full upgrade output is there, including any "Failed to fix install linkage" warnings, missing-symlink hints, or Cellar-vs-bin-symlink mismatches. Then fail loud: print the expected version, the observed version, which <project>, and the relevant lines from "$log". Common causes:
bin/<project> missing: a formula-name collision with a published Homebrew cask shadows the symlink-creation step. brew link --overwrite marcelocantos/tap/<project> resolves it; report the formula's name as a candidate for renaming if this recurs.<project> binary earlier in $PATH. which -a <project> shows all candidates.brew upgrade ran but skipped link: the symptom is "Cellar has new version, which returns command-not-found, no obvious error in the truncated tail." This is exactly why tee-ing the full log matters; grep the log for link / symlink / Error: to find the actual cause.Do not enter a diagnostic loop of repeated brew update / brew reinstall attempts — the timing race that used to motivate that loop has been eliminated by the step-8 wait.
Never hand-edit the brew-managed tap working tree at /opt/homebrew/Library/Taps/marcelocantos/homebrew-tap. It is Homebrew's working copy of the tap, not a scratch space. Uncommitted edits there get autostashed and re-applied around the next brew update, which will trip a stash-pop conflict against the homebrew-releaser-regenerated formula and abort the next brew upgrade mid-release with a Ruby parse error full of >>>>>>> Stashed changes markers. Treat that state as a bug, not a workflow.
Non-Homebrew projects: If the project has no Homebrew tap (e.g., a library, or a binary distributed another way), skip this step and note it in the Phase B report.
Report: Print:
brew install marcelocantos/tap/<project>--version output)Retire the release-readiness target and clean the tree: If the
project uses bullseye and the release was driven by a
release-readiness target, retire it now via bullseye_retire. Then
run ~/.claude/skills/release/finalize.sh <version> [target-id] to
commit any resulting bullseye.yaml diff locally (no push). This
enforces the invariant "after /release returns, bullseye.yaml is
clean" — /cv and other gates rely on it.
gh CLI is not installed or not authenticated, tell the user and stop./release invocation finishes without asking the user a single question. Per-phase approval prompts are gone — do not reintroduce them.When running git commit through an MCP-mediated executor (e.g. doit_execute, or any tool that ultimately runs the command through a single-line sh -c invocation), do not use the git commit -m "$(cat <<'EOF' ... EOF)" heredoc pattern. The executor serialises the entire command into a single line before passing it to sh -c, which collapses all the newlines in the heredoc body onto one line. The heredoc terminator then sits on the same line as the message body and sh fails with syntax error near unexpected token '(', producing an empty commit message and aborting the commit.
The reliable pattern is to write the message to a temp file and pass it via -F. Always use mktemp for the path — a fixed path like /tmp/release-commit.txt will collide with leftovers from a prior run, and the Write tool refuses to overwrite an existing file without first Reading it, forcing a fallback to rm -f that needs user approval. mktemp sidesteps that:
# 1. Allocate a unique temp file and arrange cleanup on exit.
msg=$(mktemp -t release-commit.XXXXXX)
trap 'rm -f "$msg"' EXIT
# 2. Write the message to "$msg" with the Write tool (not echo/cat) to preserve formatting.
# 3. Commit from the file.
git commit -F "$msg"
Same applies to gh pr create --body-file ... and gh release create --notes-file ... — prefer the -file variants over inline --body/--notes with multi-paragraph content, and allocate the path via mktemp with a trap to clean it up. This pattern is MCP-safe, gives you a reviewable artefact on disk before the command runs, and avoids the stale-file collision. Under a direct shell (not via an MCP-mediated executor), heredocs work fine — but the skill should default to the -F/--*-file variants so it works identically in both environments.
After each release, reflect on whether any reusable insights were gained during the process — new edge cases encountered, better patterns discovered, additional checks that would have caught problems earlier, or workflow improvements that would benefit future releases across any project. Pay special attention to unexpected failures in companion scripts (e.g., discover.sh) or tool invocations encountered during the run — these may indicate bugs to fix in the skill or its scripts, not just one-off issues. If any improvements are identified, propose the specific changes to this skill file (or its companion files) to the user. Only integrate them with user consent. This keeps the release skill evolving from real-world usage rather than hypothetical planning.