Release Habit Hooks
SkillDev toolsCut 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Release Habit Hooks skill
What this skill tells your AI
The instructions your AI receives, as published by habit-hooks/habit-hooks in skills/release-habit-hooks/SKILL.md and read by ahel’s review.
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-hooksupgrades 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.pygates it. - Bump
versionin the core'spyproject.tomland in everyplugins/*/pyproject.toml, and raise eachhabit-hooks-<name>floor in the core'sdependenciesandoptional-dependenciesto the new minor. Thenuv lockand 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 orderingX.Y.0rc1sorts belowX.Y, so~=X.Ymakes a release candidate declare floors its own plugins cannot satisfy — and no--preflag 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,rc3are all pre-releases of the sameX.Y.0; only theversionlines 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.Zsection. - 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 tool already installed;
--refresh, because its index cache does not know the release exists. rc3 was
caught by this step, with the packages already live on PyPI.
A fresh venv proves the package builds. It says nothing about the upgrade path, and the upgrade path is the only one a reader takes.
6. Bump the Homebrew tap
Only after the PyPI publish — the formula pins each artifact's URL and
sha256 from PyPI, so the values don't exist until the release is live.
The habit-hooks/homebrew-tap repo's Formula/habit-hooks.rb carries the core's sdist URL +
sha256, one resource block per plugin with its own URL + sha256, and a
test do block asserting the exact list of plugin entry points.
- Move every resource block to the new version's URL + sha256. A plugin
new in this release needs its own resource block added, not just the
existing ones bumped — a missing one ships a
brew installwithout that plugin. - Add the new plugin to the
test doblock's asserted entry-point list. - Open it as a pull request against the tap — never push to its
main.brew test-botbuilds bottles either way, butpublish.yml(brew pr-pull) attaches them from a PR number; pushed straight to main, 1.2.1 shipped with no bottles.
Signals
- GitHub stars
- 166
- Forks
- 17
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
release-habit-hooks- Source
- github.com/habit-hooks/habit-hooks