hermes-hookdeck — release workflow
Canonical documentation
Follow README.md § Releasing for the human steps (GitHub UI). This skill adds how the automation works, the gates that must pass first, and a research loop for drafting notes.
The tag is the version
Nothing in the repo declares a version. setuptools-scm derives it from the git
tag at build time and bakes it into hookdeck/_version.py inside the wheel, so
hookdeck.__version__ and the PyPI version are the same string by
construction and cannot drift.
The practical consequence: you choose the number when you create the release, not in a commit beforehand. There is nothing to bump, and no pre-release step that can be forgotten.
PyPI is append-only. A version number that has been published can never be reused or replaced, even after a yank. That is why the number is worth getting right before you publish — it is the one part of this that cannot be undone.
Between releases a source checkout reports a dev version derived from the last
tag (0.1.2.dev4+g1a2b3c4). That is expected, not a bug: it says "four commits
past v0.1.1".
Agent checklist (end-to-end)
Follow in order. Items marked gate are blocking unless the maintainer explicitly overrides.
- Everything intended for the release is merged to
main. Nothing is released from a branch. -
PREV_TAG/NEW_TAGconfirmed —git tag --sort=-v:refname | head -1for the current release; proposeNEW_TAGand agree it. - Change set reviewed:
git log PREV_TAG..origin/main— read the full messages, not just subjects. Group for user-facing notes. - gate — SemVer:
NEW_TAGmatches the minimum bump for the delta (see SemVer below). Stop and realign if under-bumped. Nothing else validates this — the tag is the version, so a wrong tag is a wrong release. - gate — CI green on
main: the tip ofmainhas passing checks. - Release notes drafted — see Drafting release notes and references/release-notes-template.md.
- gate — maintainer approval of tag name and notes. No surprise releases; publishing is irreversible on PyPI.
- Publish with
gh release create(below), targetingmain. - Confirm the
Releaseworkflow succeeded and the version is on PyPI.
What triggers a release
Publishing a GitHub release — .github/workflows/release.yml
runs on release: [published]. There is no separate approval step: publishing
the release is the decision to ship.
Pushing a bare tag does nothing. The tag is created by the release.
What the workflow does
- build — checks out the release's tag with full history (setuptools-scm
needs the tag present to resolve the version), runs
ruffand the full test suite, builds the wheel and sdist, and runstwine check. - publish — uploads to PyPI via Trusted Publishing (OIDC, no stored
token), in the
pypienvironment. - attach-artifacts — attaches the wheel and sdist to the GitHub release you created, so it carries the artifacts that actually went to PyPI.
A failure in build means nothing was published; fix main and publish a
new release. A failure in publish can be re-run from the Actions tab
(gh run rerun <id> --failed) — do not create a second release for the same
version.
Because the version comes from the tag, a mistyped tag produces a real release at the wrong number rather than an error. Check the tag before publishing; that is the check the workflow can no longer do for you.
SemVer: validate the proposed version
Check any proposed tag against what actually changed since PREV_TAG.
The contract this package offers is its configuration, its environment variables, its CLI, its agent tools, and the Hermes surfaces it registers.
Change since PREV_TAG |
Bump | Examples |
|---|---|---|
| Breaking — an existing install stops working, or behaves differently without the operator changing anything | MAJOR | Renamed or removed config key or HOOKDECK_EG_* variable; a default that changes delivery behaviour; a removed hermes hookdeck subcommand; a renamed or removed hookdeck_* agent tool; a ledger schema change that an older row cannot satisfy |
| New capability, backward compatible | MINOR | New config option with a safe default; new subcommand or agent tool; a new route feature; support for a Hermes version that was not supported before |
| Fixes and corrections, no new capability, nothing breaks | PATCH | Bug fixes; corrections to the bundled skill or plugin.yaml; dependency bumps with no behaviour change; packaging metadata |
A change under hookdeck/ ships even when it is only prose. The bundled
skill (hookdeck/skills/), plugin.yaml and the dashboard bundle are inside
the wheel, so correcting them is a patch release, not a docs-only change.
A change outside hookdeck/ usually does not ship. docs/, README.md,
AGENTS.md, tests/, skills/ at the repo root and .github/ are not
packaged. A release containing only those has nothing for a user to install —
say so rather than cutting one.
Verify which of the two you are in rather than assuming:
git diff --stat PREV_TAG..origin/main -- hookdeck/
Empty output means nothing shipped.
Agent behaviour: state the minimum bump the change set requires, then compare it to what was proposed. If they conflict, do not treat the proposal as authoritative — explain the mismatch and recommend the correct version. If it is genuinely ambiguous whether something breaks an existing install, ask.
Publish with GitHub CLI (gh)
Create the release with gh rather than pushing a tag — a bare tag does not
trigger anything.
Write the notes to a temp file and register cleanup, so a failure does not leave it behind:
NOTES_FILE="$(mktemp "${TMPDIR:-/tmp}/hermes-hookdeck-release-notes.XXXXXX.md")" trap 'rm -f "$NOTES_FILE"' EXITWrite the final markdown body to
"$NOTES_FILE".Create the release, targeting
main:gh release create "vM.m.p" \ --repo hookdeck/hermes-hookdeck \ --target main \ --title "vM.m.p" \ --notes-file "$NOTES_FILE"Add
--prereleaseforrc,aorbversions, so the repository's "latest release" does not point at a release candidate.Watch it:
gh run watch— orgh run list --workflow=release.yml --limit 1.
Requirements: gh authenticated. Nothing needs to be bumped first — the
tag you pass here becomes the version. Do not put secrets in the notes file.
Verify afterwards
pip download --no-deps -d /tmp/verify hermes-hookdeck==M.m.p
For a release that changed anything under hookdeck/, confirm the change is
really in the artifact rather than only in the repo — install it into a clean
venv and check the file, not the working tree.
Drafting release notes
Use references/release-notes-template.md as the skeleton. Include only headings with real content — omit a section rather than writing "None".
Write for someone who runs a gateway, not for someone who reads this repo. The useful question is "what changes for me, and do I have to do anything?" — not "which files moved".
- Lead with anything that requires action. Config changes, renamed variables, anything that alters delivery behaviour. If nothing does, say the upgrade is a drop-in.
- A fix is only worth a bullet if the reader could have hit it. Say what went wrong from the outside ("the bundled skill never loaded"), not what the code did.
- Always end with the compare link:
https://github.com/hookdeck/hermes-hookdeck/compare/<prev_tag>...<new_tag>
Contributors: do not add a generic thanks block every release. Include one only for a first-time contributor or an exceptionally large contribution.
Research loop
Tags:
git tag --sort=-v:refname | head -5. ConfirmPREV_TAG.Commits:
git log PREV_TAG..origin/main— full messages. This repo writes the reasoning into commit bodies; that is the changelog source.Shipped or not:
git diff --stat PREV_TAG..origin/main -- hookdeck/.Group by user impact, not by file.
SemVer check against the table above.
CI:
gh api "repos/hookdeck/hermes-hookdeck/commits/$(git rev-parse origin/main)/check-runs" \ --jq '.check_runs[] | "\(.name): \(.status)/\(.conclusion // "-")"'Every run must be
completed/success. Do not release otherwise.Ask for check-runs, not
/status. The latter reports the legacy commit Status API, which this repo does not use — with no statuses recorded it answerspendingforever, so it reads as a red gate on a greenmain.
Safety and governance
- PyPI is append-only. A published version cannot be reused, replaced, or truly deleted. Yanking hides it from resolvers; it does not free the number.
- Do not release a red
main. The workflow runs the suite itself and will fail the build, but finding out during a release is the wrong time. - Do not under-bump. Resolve a SemVer disagreement with the maintainer before publishing, not after.
- The tag is unchecked. Nothing compares it to the change set, so a typo
(
v0.2.0for a patch) ships at that number and burns it permanently. - Green tests are not proof of integration. The suite runs against a stub
this repo also owns. Both defects found after 200 green tests came from a
real
hermes gateway run. For a release touching the delivery path, say which level you actually exercised — see AGENTS.md.
Related files
| Topic | Location |
|---|---|
| Human release steps | README.md § Releasing |
| CI entrypoint | .github/workflows/release.yml |
| Version derivation | pyproject.toml [tool.setuptools_scm] |
| What is packaged | pyproject.toml [tool.setuptools.package-data] |
| Repo conventions | AGENTS.md |