Skip to content
Skill Authoring
Skill

/release-habit-hooks

Cut a new release of the habit-hooks packages. Use when asked to release, publish, or bump the version. Reviews what lands, enforces the in-sync versioning rule, validates the changelog, and drives the tag-triggered PyPI publish.

BOOST
From plugin
habit-hooks
2314 skills
Install
$ npx -y skills add habit-hooks/habit-hooks --skill release-habit-hooks --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/release-habit-hooks

Context preview

The summary Claude sees to decide when to auto-load this skill.

Cut a new release of the habit-hooks packages. Use when asked to release, publish, or bump the version. Reviews what lands, enforces the in-sync versioning rule, validates the changelog, and drives the tag-triggered PyPI publish.

SKILL.md

release-habit-hooks.SKILL.md
name: release-habit-hooks
description: "Cut a new release of the habit-hooks packages. Use when asked to release, publish, or bump the version. Reviews what lands, enforces the in-sync versioning rule, validates the changelog, and drives the tag-triggered PyPI publish."

Release Habit Hooks

Seven packages ship from this repo: `habit-hooks` (core), `habit-hooks-generic`, `habit-hooks-python`, `habit-hooks-typescript`, `habit-hooks-php`, `habit-hooks-java`, `habit-hooks-ruby`. A `v*` tag triggers `.github/workflows/release.yml`, which builds all seven and publishes each with `skip-existing: true` — so an unchanged package at an already-published version is simply skipped. Publishing is irreversible: **never tag or push without Ivett's explicit go-ahead.**

1. Review what lands

git describe --tags --abbrev=0            # last release tag
git log <lastTag>..HEAD --oneline

For each package, diff since the tag matching its *current* version and decide if it changed:

git diff --stat <lastTag>..HEAD -- src/                    # core
git diff --stat <tagForItsVersion>..HEAD -- plugins/<name>/

A plugin's shipped package data (`eslint.config.mjs`, `knip.json`, `.jscpd.json`, `ruff.toml`, sensors/guides) is consumer-facing — a change there means that plugin changed. Read every diff; classify each as consumer-facing vs internal.

2. Versioning — all released packages stay in sync

  • The new version is `max(all current package versions)` plus the appropriate

semver bump. Decide the bump from what actually landed: **patch** for fixes/internal, **minor** only for genuinely new backward-compatible features.

  • **Every package ships at that version**, changed or not. `pip install -U

habit-hooks` upgrades a plugin only when the new core stops being satisfied by the installed one, so a plugin left behind hands someone the new core with last release's plugins. `tests/test_the_plugin_floor_tracks_the_release.py` gates it.

  • Bump `version` in the core's `pyproject.toml` and in every

`plugins/*/pyproject.toml`, **and raise each `habit-hooks-<name>` floor in the core's `dependencies` and `optional-dependencies` to the new minor.** Then `uv lock` and confirm the lock shows the new versions. The same test gates both halves, so a forgotten floor fails the suite rather than shipping quietly.

  • The floor is spelled `>=X.Y.dev0,<2`, never `~=X.Y`. By PEP 440 ordering

`X.Y.0rc1` sorts *below* `X.Y`, so `~=X.Y` makes a release candidate declare floors its own plugins cannot satisfy — and no `--pre` flag lifts it, because it is the specifier excluding the version, not a policy about candidates. The same spelling serves every rc and the final release, so nothing is rewritten at the tag.

  • **A release candidate does not move the minor.** `X.Y.0rc1`, `rc2`, `rc3` are

all pre-releases of the same `X.Y.0`; only the `version` lines move between them. Iterate as far as the reporters need.

3. Validate the changelog

`CHANGELOG.md` must have a heading for **every** released version — no lingering `## Unreleased` content that has actually shipped (this has bitten us: 1.0.0–1.0.2 all sat under a stale "Unreleased"). Before releasing:

  • Every version between the last documented one and the new tag has its own

`## X.Y.Z` section.

  • The new version's section records the consumer-facing changes from step 1.
  • Nothing under a version heading is actually unreleased.

4. Verify, then hand off

uv run pytest -q
uv build --all-packages --out-dir dist/_relcheck   # mirrors release.yml; check artifact versions

All green. Then present the tag command as a ⚠️ checkpoint for Ivett to run — do not tag or push yourself:

git tag vX.Y.Z && git push origin main --tags

5. An install instruction is verified from an install, never from empty

Anyone handed an install command already has habit-hooks — that is *why* they are reading it. So test the command from their state, not from an empty machine.

**Half of this belongs before the tag, and half cannot.** The upgrade *mechanics* need only the wheels, which `uv build` has already produced in step 4 — so run them from `dist/_relcheck` while the tag can still be moved. Only the cache behaviour below needs a live PyPI, because the stale cache does not exist until the version does.

Before the tag, against the built wheels:

export UV_TOOL_DIR=$(mktemp -d) UV_TOOL_BIN_DIR=$(mktemp -d)
uv tool install 'habit-hooks[typescript]==<the version they are on now>'
uv tool install --force --find-links dist/_relcheck 'habit-hooks[typescript]==X.Y.Z'
"$UV_TOOL_BIN_DIR/habit-hooks" --version       # did it actually move?

Every package must move together in that output — a core that upgrades while a plugin stays behind is the failure the in-sync rule exists to stop, and it is visible here and nowhere else. Then run the installed binary against a throwaway project and read a real finding out of it, so the answer covers the plugins the core just pulled in, not only its own version string.

After the publish, against PyPI:

uv tool install <the exact command about to be published>
"$UV_TOOL_BIN_DIR/habit-hooks" --version

`uv tool install` **does not upgrade an already-installed tool.** It prints "already installed" and exits 0. It needs `--force`. 1.4.0rc1 was announced on #133 and #134 without it; both reporters retested 1.3.1, and one reported a 1.3.1 bug back as an rc failure.

**`--force` is not enough either: it resolves from uv's cached index.** Anyone who installed the previous version minutes ago has a cache that predates the one just published, and uv answers `No solution found … there is no version of habit-hooks[typescript]==X.Y.Z` for a version sitting on PyPI. So the published command is

uv tool install --force --refresh 'habit-hooks[<extras>]==X.Y.Z'

and all three parts earn their place: the pin, because uv will not upgrade to a pre-release without one; `--force`, because it skips a

Read more
Ships withhabit-hooks

Turn best-practice coding advice into AI habits. Stop reciting software engineering literature to your AI agent.

Get the whole plugin
Stats
233
Stars
29
Forks
Active
Maintenance
Python
Language
MIT
License
20d ago
Last commit
4mo ago
Created
3d ago
Added

Repo: habit-hooks/habit-hooks

Other skills on habit-hooks.