Skip to main content
ClaudeWave
Skill262 repo starsupdated yesterday

han-release

The han-release skill automates releasing a meta-plugin and its child plugins by validating prerequisites (gh CLI, jq, git repo), comparing baseline and current versions in plugin.json files, determining target versions, and creating appropriate git tags and GitHub releases. Use this when preparing a production release of the han plugin suite after updating version numbers in marketplace.json and CHANGELOG.md files.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/testdouble/han /tmp/han-release && cp -r /tmp/han-release/.claude/skills/han-release ~/.claude/skills/han-release
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

<!--
`AskUserQuestion` is deliberately absent from `allowed-tools`, and must stay absent. Listing it makes Claude Code's
permission evaluator auto-approve the tool through its always-allow path and return empty answers without ever
rendering the prompt, so every gate in this skill would silently pass. See
han-plugin-builder/skills/guidance/references/skill-building-guidance/allowed-tools-AskUserQuestion.md. The tool still
works unlisted; it prompts once for permission.
-->

## Pre-requisites

- gh CLI: !`which gh 2>/dev/null || echo "not installed"`
- jq: !`which jq 2>/dev/null || echo "not installed"`
- claude CLI: !`which claude 2>/dev/null || echo "not installed"`
- git repo: !`git rev-parse --is-inside-work-tree 2>/dev/null || echo NO`

**If `gh`, `jq`, or `claude` reads `not installed`, or this is not a git repo:** tell the operator which prerequisite is
missing and that it must be installed/configured before `/han-release` can run, then **immediately stop**. The skill
cannot proceed without all four.

The `claude` CLI is what creates the per-plugin tags in Step 10. Every invocation of it in this skill goes through the
shell's `command` builtin (`command claude ...`), never a bare `claude`, because an operator's shell commonly wraps
`claude` in a function or alias that blocks waiting for terminal input. The `which` probe above resolves the same way a
bare call would, so it reports the wrapper's presence rather than the executable's; treat a non-empty result as "the
tool is reachable" and let Step 10's first invocation be what proves it runs.

## Project Context

- repo: !`gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || git config --get remote.origin.url`
- current branch: !`git branch --show-current 2>/dev/null || echo unknown`
- default branch: !`git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || echo unknown`
- working tree: !`git status --porcelain 2>/dev/null || echo NO`
- parent plugin name: !`jq -r .name .claude-plugin/marketplace.json 2>/dev/null`
- plugins (name source version):
  !`jq -r '.plugins[] | "\(.name)\t\(.source)\t\(.version)"' .claude-plugin/marketplace.json 2>/dev/null`
- latest parent tag: !`git fetch --tags --quiet >/dev/null 2>&1; git tag -l 'han--v*' --sort=-v:refname | head -n1`
- latest suite tag: !`git tag -l 'v*.*.*' --sort=-v:refname | head -n1`
- changelog head: !`grep -m1 '^## v' CHANGELOG.md 2>/dev/null`

The two tag probes are separate on purpose. A single pattern covering both namings returns the **wrong** tag: version
sorting compares the whole refname, so `v4.6.0` sorts ahead of `han--v5.0.0` and the old suite tag wins on every release
after the transition. The fetch runs on the first probe only; both read the same refreshed tag list.

`latest parent tag` carries the literal `han--v*` pattern, because a context-injection command is a fixed string and
cannot interpolate `parent plugin name`. If `parent plugin name` is not `han`, redo the lookup in Step 2 with the actual
name before using either value. Skipping that turns a renamed parent into an empty probe, which Step 2 would read as a
first release and silently expand the changelog to the whole repository history.

### Vocabulary used throughout this skill

- **parent** — the meta-plugin whose name equals the marketplace `name` (`parent plugin name` above, normally `han`). It
  has no skills or agents of its own; it exists to install the children via `dependencies`. The parent's own per-plugin
  tag is the one the GitHub release attaches to, so the release tag is `{parent plugin name}--v{parent target}`.
- **children** — every other entry in `marketplace.json.plugins[]` (`han-core`, `han-github`, `han-reporting`, and any
  future `han-*` plugin). Each child has its own version line, bumped independently of the others.
- **baseline** of a plugin — its version at `prev` (the latest release tag). For the parent this is `prev#`. For a child
  it is the version recorded in that child's `plugin.json` at `prev`; if the child did not exist at `prev`, it is a
  **new plugin** (see Step 3).
- **current** of a plugin — the version in its working-tree `plugin.json`.
- **target** of a plugin — the version being released for it. The release tag is
  `{parent plugin name}--v{parent target}`.
- **tag name** of a plugin — `{name}--v{target}`, for example `han--v5.0.0` and `han-core--v3.0.0`. Every plugin gets
  one, and the parent's is what the GitHub release attaches to.
- `prev` is the previous release's tag, resolved in Step 2 from the two tag probes. On the first release it is empty.
- `prev#` is **the part of `prev` after its last `v`**. That rule is correct under both namings, which matters because
  `prev` can be either shape:

  ```
  v4.6.0        -> prev# is 4.6.0
  han--v5.0.0   -> prev# is 5.0.0
  ```

  Do not read `prev#` as "the number without the leading `v`". That older rule returns the whole tag string for a
  per-plugin tag, and `prev#` is the parent's baseline (see 3a in `references/version-plan-rules.md`), so a wrong value here silently corrupts the entire
  version plan rather than failing.

- Each plugin's source directory comes from the `source` field in `marketplace.json` (for example `./han-core`), so its
  `plugin.json` is `{source}/.claude-plugin/plugin.json`. Use `{source}` verbatim in every git command: the
  `./`-prefixed form works both after a `{ref}:` colon (`git show {prev}:{source}/...`) and as a pathspec
  (`git diff ... -- {source}/`). Do not strip the leading `./`.

## Step 1: Parse the invocation and check release safety

1. **Parse `$ARGUMENTS`** for two independent flags, then treat the remaining free text as optional release context that
   informs the changelog narrative:
   - `pause_before_publish` — true if the argument contains "pause", "review", or "confirm before publish"
     (case-insensitive). Default **false**.
   - `draft_release` — true if the argument contains "draft". Default **false**.
   - The leftov