hv-release

star 0

Cut a release — walk the project's per-project release checklist (`.hv/RELEASE.md`) as a preflight gate, bump version (major/minor/patch), generate categorized release notes from commits since the last tag, prepend a section to CHANGELOG.md, create an annotated git tag, push, publish a release on GitHub or GitLab if origin is set, and offer to close any upstream issues still open for shipped items. Use on "release", "cut a release", "tag a release", "ship X.Y.Z".

l4ci By l4ci schedule Updated 5/22/2026

name: hv-release description: Cut a release — walk the project's per-project release checklist (.hv/RELEASE.md) as a preflight gate, bump version (major/minor/patch), generate categorized release notes from commits since the last tag, prepend a section to CHANGELOG.md, create an annotated git tag, push, publish a release on GitHub or GitLab if origin is set, and offer to close any upstream issues still open for shipped items. Use on "release", "cut a release", "tag a release", "ship X.Y.Z". user-invocable: true

Print the banner below verbatim before any other action — skip if dispatched as a subagent. See references/banner-preamble.md.

════════════════════════════════════════════════════════════════════════
  🏷️  hv-release  ·  bump version, tag, notes, CHANGELOG, publish
  triggers: "release", "cut release", "ship X.Y.Z"  ·  pairs: hv-ship
════════════════════════════════════════════════════════════════════════

hv-release — Cut a Release

Configuration

Read from .hv/config.json (all keys optional — defaults apply if absent):

Key Default Notes
release.versionFile (auto-detect) Explicit path override; skips auto-detect search
release.changelogPath CHANGELOG.md Project-root relative
release.checklistPath .hv/RELEASE.md Per-project release checklist walked in Step 1.5; absent = offer scaffold
release.tagPrefix v Set to "" for unprefixed tags
release.draft false Pass --draft to gh/glab
release.requireCleanTree true Set false to allow dirty releases (testing only)
release.confirmLargePushCommits 10 Threshold (commits) above which auto/loop autonomy still confirms before pushing unpushed HEAD

Step 1 — Preflight & Guard

.hv/bin/hv-preflight

See docs/reference/preflight.md for exit-code handling.

Then verify:

  1. Clean tree — run git status --porcelain. If output is non-empty and release.requireCleanTree is true (default), stop: "Working tree is dirty. Commit or stash changes first, or set release.requireCleanTree: false." Show git status -s in the error.

  2. On main/trunkgit rev-parse --abbrev-ref HEAD. If not main, master, or trunk, stop with a one-liner.

  3. HEAD pushedgit rev-parse HEAD vs git rev-parse @{u}. If they differ, branch on autonomy.level from .hv/config.json:

    • "auto" or "loop" — count unpushed commits via git rev-list @{u}..HEAD --count.
      • If the count is < release.confirmLargePushCommits (default 10), silently run git push origin <current-branch> and continue. A release intends to ship the local commits; an unpushed HEAD is part of the release, not a pre-flight error.
      • If the count is ≥ release.confirmLargePushCommits, interject ONE AskUserQuestion regardless of autonomy:
        • Header: "Large push"
        • Question: " unpushed commits about to be pushed as part of this release. Continue?"
        • Options:
          1. "Push and continue (Recommended)" — runs git push origin <current-branch>, then proceed
          2. "Abort" — stop without writing anything
        • Plain-text fallback: "Push commits and continue, or abort?"
    • "off" (default) — use AskUserQuestion:
      • Header: "Unpushed"
      • Question: "HEAD has unpushed commits. Push them as part of this release?"
      • Options:
        1. "Push and continue (Recommended)" — runs git push origin <current-branch>, then proceed
        2. "Abort" — stop without writing anything
      • Plain-text fallback: "Push and continue, or abort?"

Initialize task list. Follow the canonical pattern in references/task-list-init.md — load TaskCreate(…) via ToolSearch select:TaskCreate,TaskUpdate if needed, then create one task per phase below.

Phases:

  1. Preflight & guard — clean tree, on main, HEAD pushed (Step 1)
  2. Project checklist — walk .hv/RELEASE.md items as gates (Step 1.5)
  3. Bump version — version source detected and incremented (Steps 2–4)
  4. Generate notes — categorized release notes drafted from commits (Steps 5–7)
  5. Tag & push — annotated tag created, branch + tag pushed (Steps 8–12)
  6. Publishgh/glab release published if origin matches (Step 13)
  7. Close upstream issues — manual gate to close any GH/GL issues still open for shipped items (Step 13.4)
  8. Post-release nudges — summary + autonomy-aware chaining (Step 14+)

Step 1.5 — Project Checklist

Per-project release steps that aren't (and shouldn't be) hardcoded into the skill — sibling version files, lockfiles, docs version refs, infra rollouts, anything project-specific. Lives in release.checklistPath (default .hv/RELEASE.md). Tracked by default — shared with the team like any other source file.

Skip the whole step in --dry-run mode (print the parsed items and DRY RUN — checklist walk skipped. instead).

File absent. Branch on autonomy.level from .hv/config.json:

  • "off" (default) — AskUserQuestion:
    • Header: "Checklist"
    • Question: "No <release.checklistPath> found. Scaffold a starter checklist now, or continue without?"
    • Options (single-select):
      1. Scaffold starter (Recommended) — write the template below, open it for the user to edit, then re-read and walk it
      2. Continue without — note it in the Step 14 summary as "no project checklist"; continue to Step 2
      3. Abort
    • Plain-text fallback: "Scaffold checklist, continue without, or abort?"
  • "auto" or "loop" — skip silently. No checklist means no gate; do not interrupt unattended runs to scaffold one.

Starter template (write verbatim on Scaffold):

# Release Checklist

Each `- [ ]` line is a gate `/hv-release` walks before bumping the version. Edit freely — nothing here is hardcoded. Items marked `- [x]` are ignored. Append `(manual)` to any item that must interject even in `autonomy.level: auto`/`loop`.

- [ ] Sibling version-bearing files are in sync (e.g., `.claude-plugin/marketplace.json`, lockfiles, docs version refs)
- [ ] CI is green on the release branch
- [ ] Migration notes for users on the prior version are written

(Add project-specific items below.)

File present. Parse every line matching ^\s*-\s+\[\s*\]\s+(.+)$ as a gate (in file order). Lines matching - [x] are skipped. If zero gates, print "Checklist has no open items — continuing." and proceed.

For each gate, branch on autonomy.level:

  • "off" — always interject:
    • Header: "Checklist"
    • Question: "Checklist item: <text>. Done?"
    • Options (single-select):
      1. Yes, continue (Recommended) — proceed to next item
      2. Fix it now and continue — pause for the user; re-ask the same item afterward
      3. Skip this item — note in the Step 14 summary as skipped: <text>
      4. Abort release — stop with "Release aborted at checklist item: <text>. Nothing written."
    • Plain-text fallback: "Done, fix now, skip, or abort?"
  • "auto" or "loop" — auto-acknowledge items whose text does not end with (manual); interject (using the off-mode prompt above) for items that do. This lets users mark sensitive items (Push staging migration (manual)) as always-confirmed even in unattended runs.

After all gates pass, continue to Step 2.

Step 2 — Detect Version Source

.hv/bin/hv-release-detect-version

Parse JSON output {file, version, kind}:

  • file — absolute path to the version-bearing file
  • version — current semver string (e.g., 1.10.0)
  • kindplugin-json | package-json | pyproject | cargo | plain

If exit 1, surface the stderr message verbatim and stop. If output indicates multiple candidates, use release.versionFile to disambiguate — document that the user should set it in .hv/config.json.

Step 3 — Determine Bump Type

Accept an arg if the user supplied one: major, minor, patch, or an explicit X.Y.Z string. Also accept --dry-run flag — run all steps but skip all writes, commits, tags, and pushes; print what would happen.

If no arg, first generate the commit-range preview for a recommendation:

.hv/bin/hv-release-changelog-from-commits <prev-tag>..HEAD

Scan output for bucket headings:

  • ## Breaking present → recommend major
  • ## New present (no Breaking) → recommend minor
  • else → recommend patch

Then use AskUserQuestion:

  • Header: "Bump type"
  • Question: "Current version: <current>. What bump type?"
  • Options (single-select, mark recommended):
    1. patch — <current> → <X.Y.Z+1> (mark Recommended if applicable)
    2. minor — <current> → <X.Y+1.0>
    3. major — <current> → <X+1.0.0>
    4. Explicit version — prompt for the exact string via Other
    5. Abort

Plain-text fallback: "Bump type? (major / minor / patch / X.Y.Z / abort)"

If the user picks explicit, validate: must be valid semver and strictly greater than current. If invalid, stop with an error.

Step 4 — Compute New Version

Use the helper's --dry-run mode so version computation has one source of truth — Step 8 will repeat the call without --dry-run to actually write the file.

new_version=$(.hv/bin/hv-release-bump-version --dry-run <file> <kind> <bump>)

<file> and <kind> come from Step 2; <bump> is patch, minor, major, or the explicit semver from Step 3. The helper validates the bump (must be valid semver and strictly greater than current for the explicit path) and prints the new version. If validation fails, the helper exits 1 with a message — surface it and stop.

Store new_version for use in Steps 6, 7, 8, 9, 10, 11, 12, 13, 14.

If BREAKING CHANGE commits were detected (Step 3 scan) but the user chose patch or minor, interject with AskUserQuestion before continuing:

  • Header: "Escalate"
  • Question: "Commits contain BREAKING CHANGE: footers but bump type is <chosen>. Escalate to major?"
  • Options: Escalate to major (Recommended) / Keep <chosen> / Abort
  • Plain-text fallback: "Escalate to major, keep , or abort?" — default to Recommended (Escalate to major) on ambiguity, naming it explicitly. See references/ask-user-question-fallback.md.

Step 5 — Detect Previous Tag

git describe --tags --abbrev=0 2>/dev/null || true

If empty (no tags exist), range = full history; set prev_tag = "". Note this in the Step 14 summary. If a tag exists, set prev_tag = <value> and range = <prev_tag>..HEAD.

Step 6 — Generate Release Notes

.hv/bin/hv-release-changelog-from-commits <range>

Captures categorized Markdown (buckets in helper-emit order: Breaking, New, Fixed, Performance, Changed, Documentation, Other). Merge commits are filtered by the helper.

Compact dense buckets. When a bucket has 3+ entries that clearly belong to the same feature or concern (e.g., 7 feat: commits all touching one new skill), replace the raw list with a single model-written summary line capturing the theme, optionally followed by 1–2 bullets naming the highest-impact pieces (a breaking change, a flag flip, a new public surface). Buckets with fewer than 3 entries stay as-is — the noise floor is low and the model adds little value. The helper's job is the raw categorization; editorial collapse is yours.

Append stats line — run:

git diff --shortstat <prev_tag>..HEAD   # omit <prev_tag>.. when no previous tag

Format as ## Stats\n<N commits, M files changed, +X −Y lines>.

Build compare URL (only when prev_tag is non-empty):

.hv/bin/hv-release-detect-host
  • github or github-enterprisehttps://<host>/<owner>/<repo>/compare/<prev_tag>...v<new_version>
  • gitlab or gitlab-self-hostedhttps://<host>/<owner>/<repo>/-/compare/<prev_tag>...v<new_version>
  • none → omit compare URL

Append **Full changelog:** <compare-url> to the notes (or omit if no prev tag or no host).

Prepend a model-written one-line summary scoped to the top 2-3 themes from the buckets. This summary also becomes the release title suffix in Step 13.

Run the self-audit before Step 7 displays the draft. Compact bucket summaries, the prepended one-line summary, and any other model-written prose in the notes are user-facing artifacts that ship to GitHub/GitLab and live in CHANGELOG.md indefinitely. Apply the rule sheet and self-audit pass in references/humanizing-prose.md to the assembled notes silently — the user sees the post-audit draft, not the pre-audit one.

Step 7 — Review Notes

Display the full notes draft to the user, then use AskUserQuestion:

  • Header: "Notes"
  • Question: "Release v<new_version> — notes look good?"
  • Options (single-select):
    1. Looks good (Recommended)
    2. Edit — accept replacement text via Other (free-text replaces the draft verbatim)
    3. Abort release

Plain-text fallback: "Proceed, edit, or abort?"

If the user picks Edit, accept the replacement text and store it. Re-display the edited notes before continuing (no second prompt — one edit pass only).

If the user picks Abort, stop with one line: "Release aborted. Nothing written."

Write the (possibly edited) notes to a temp file:

NOTES_FILE=$(mktemp /tmp/hv-release-notes.XXXXXX.md)

Step 8 — Update Version File

written=$(.hv/bin/hv-release-bump-version <file> <kind> <bump>)

Same arguments as Step 4's --dry-run call; this writes the file and prints the new version. Assert written == new_version; if they diverge, stop with an error (the file may be partially modified — surface the discrepancy and let the user investigate).

Skip in the skill's --dry-run mode (different from the helper's flag); print what would be written instead.

Step 9 — Update CHANGELOG.md

.hv/bin/hv-release-update-changelog <new_version> "$NOTES_FILE" [--path <release.changelogPath>]

If exit 1 (version section already exists), surface the error and stop — do not proceed to commit.

Skip in --dry-run mode; print what would be prepended instead.

Step 10 — Commit

git add <version-file> <release.changelogPath>
git commit -m "chore: release v<new_version>"

Skip in --dry-run mode.

Step 11 — Tag

Check whether user.signingkey is set:

git config --get user.signingkey 2>/dev/null

If set, use git tag -s; otherwise git tag -a:

git tag [-a|-s] v<new_version> -F "$NOTES_FILE"

Skip in --dry-run mode; print the tag command that would run.

Step 12 — Push

Manual gate — filing a public artifact. Pushing the tag creates externally-visible state. This step is always manual — never auto-invoked, regardless of autonomy.level. Step 7 (Review Notes) already gathered explicit user approval of the release notes; the push only fires after that approval. See references/manual-gates.md.

git push origin <current-branch> v<new_version>

Single call — pushes both the commit and the tag atomically. If this fails (e.g., origin not set), stop and print the tag SHA for manual recovery.

Skip in --dry-run mode.

Step 13 — Create Remote Release

Manual gate — filing a public artifact. Publishing the release on GitHub/GitLab creates externally-visible state. This step is always manual — never auto-invoked, regardless of autonomy.level. The user already approved the release notes in Step 7; this step is downstream of that approval. See references/manual-gates.md.

.hv/bin/hv-release-detect-host

Branch on the helper's host output. Host-specific command blocks live in references/release-hosts.md — substitute <new_version>, $NOTES_FILE, and the summary verbatim.

host Section in references/release-hosts.md
github, github-enterprise ## github or github-enterprise
gitlab, gitlab-self-hosted ## gitlab or gitlab-self-hosted
none ## none

Add new host values to both the helper and the reference together.

Skip the whole step in --dry-run mode; print the gh/glab command that would run.

Step 13.4 — Close Upstream Issues

Mirrors /hv-ship Step 6c, scoped to the whole release range rather than a single ship cycle. Closes upstream issues that landed in this release but stayed open because the work was pushed directly to main (skipping /hv-ship entirely) or because /hv-ship chose "leave open" at the time.

Skip the whole step in --dry-run mode (nothing was actually tagged or pushed).

1. List candidates.

.hv/bin/hv-issues-imported --open-only

The --open-only flag drops entries whose upstream issue is already closed (or whose state can't be resolved — missing CLI, deleted issue), so the gate only surfaces issues still actually open. If the resulting JSON array is empty, skip the rest of this step silently.

2. Manual gate.

Manual gate — closing public upstream issues. Closing the issues posts a tracking comment and changes their state on the remote — externally-visible. This step is always manual — never auto-invoked, regardless of autonomy.level. The release already published; this step decides whether to close the upstream issues too. See references/manual-gates.md.

3. Ask the user.

Invoke AskUserQuestion (single-select, ≤4 options):

  • Header: "Close"
  • Question: "Close N upstream issue(s) released in v<new_version>? (<comma-separated list of #N>)"
  • Options: "Yes, close all", "Pick subset", "No, leave open"

This gate is always manual — never auto-picked in loop mode. Stop the loop here and wait for the user's answer.

4. On "Yes, close all": dispatch parallel hv-issues-close calls (one per candidate, all in a single batch of tool calls), passing the release commit SHA from Step 10:

.hv/bin/hv-issues-close --issue <N> --commit <release-commit-sha> --item <ID> [--repo <name>]

Pass --repo only for entries whose repo field is non-null (umbrella mode).

5. On "Pick subset": invoke a second AskUserQuestion (multiSelect, ≤4 candidates per call; chunk if N>4):

  • Header: "Pick issues"
  • Question: "Which issue(s) should be closed?"
  • Options: one entry per candidate formatted as "#N (item <ID>)"

Then dispatch parallel hv-issues-close calls for each selected entry as in step 4.

6. On "No, leave open": print:

Skipping upstream issue close — N issue(s) left open. Run `gh issue close <N>` / `glab issue close <N>` manually if desired.

Step 13.5 — Docs After-Work (Nudge or Auto-Invoke)

Read docs.afterWork from .hv/config.json (default false). If it's false, skip this step entirely. Users opt in via /hv-config or by running /hv-ship --docs manually once.

When the flag is on, a release is a natural docs trigger — release notes and CHANGELOG entries are user-facing artifacts that often imply other docs (READMEs, getting-started guides, reference pages) need a refresh. Skip in --dry-run mode. Don't repeat in the same session.

When triggered, branch on autonomy.level:

  • "off" — append one line to the Step 14 summary — "Release shipped. Run /hv-ship --docs to review and update public docs (after-work mode)."
  • "auto" or "loop"dispatch hv-ship --docs via Skill immediately — no prompt, no confirmation, no "want me to" question. Pass a brief naming the new version, the bump type, and a one-line summary of what shipped (from Step 6's release notes title).

If <docs.path>/ doesn't exist or is empty, /hv-ship's Docs Mode after-work flow self-skips (printing a one-line "not yet initialized" notice) — no extra check needed here.

Step 14 — Summary

Print one compact block:

Released v<new_version>
  Tag:      v<new_version> (<tag-SHA>)
  Commit:   <commit-SHA>
  CHANGELOG: <release.changelogPath>
  Remote:   <release-URL or "skipped">
  Checklist: <N> done / <M> skipped (or "no project checklist" / "skipped in dry-run")
  [No previous tag — full history used as range.]   ← only when no prev tag

If any checklist items were skipped, append a Skipped checklist items: block listing each on its own line.

In --dry-run mode, prefix the block with DRY RUN — no changes written.

Edge Cases

  • No previous tag — full history range; add the one-liner to the Step 14 summary. Omit compare URL from notes.
  • Multiple version files — currently first-match wins; set release.versionFile in .hv/config.json to pin the file explicitly.
  • Working tree dirty — fail at Step 1 unless release.requireCleanTree: false; show git status -s in the error message.
  • BREAKING CHANGE with patch/minor bump — escalate via AskUserQuestion in Step 4 before any writes.
  • gh/glab not installed but origin matches — fail at Step 13 after tag push; print recovery: gh release create v<X.Y.Z> --notes-file <path> and the push-delete command to revert the tag if needed: git push --delete origin v<X.Y.Z>.
  • No origin — Step 13 silently skipped; tag and CHANGELOG still committed locally.
  • CHANGELOG section already exists for this version — helper exits 1 at Step 9; surface the error and stop.
  • Existing CHANGELOG.md without # Changelog header — helper preserves existing content; inserts after H1 if present, else prepends.
  • Compare URL when no previous tag — omit the Full changelog: line from the notes.
  • Checklist absent in auto/loop — silently skip Step 1.5; do not interrupt unattended runs to scaffold. The Step 14 summary notes "no project checklist".
  • Checklist item user-skipped — proceed, but list the skipped text under Skipped checklist items: in the Step 14 summary so the release record is honest.

Rules

  • Never bump without an explicit user-confirmed bump type.
  • Never push the tag without the user reviewing and approving release notes (Step 7).
  • Never duplicate a CHANGELOG section — the helper enforces this; stop if it exits 1.
  • Atomic writes only — all file mutations go through the helpers which use atomic write semantics.
  • Surface failures from any helper immediately; do not continue past a non-zero exit.
  • --dry-run skips all writes, commits, tags, and pushes — output shows what would happen.
  • Never hardcode project-specific release steps into the skill — they live in release.checklistPath per project. Step 1.5 walks the file; the skill itself stays generic.

References

Install via CLI
npx skills add https://github.com/l4ci/hv-skills --skill hv-release
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator