- name
- hermes-hookdeck-release
- description
- Guides maintainers through releasing the hermes-hookdeck plugin to PyPI. Publishing a GitHub release is what triggers the publish, and the tag is the version — nothing in the repo is bumped. Validates the proposed version against SemVer from the actual change set, including the rule that a change to the bundled skill or plugin.yaml is a shipped change. Use when cutting a release, publishing to PyPI, drafting release notes, choosing vMAJOR.MINOR.PATCH, `gh release create`, or following the release checklist.
# hermes-hookdeck — release workflow
## Canonical documentation
Follow **[README.md](../../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_TAG` confirmed** — `git tag --sort=-v:refname | head -1`
for the current release; propose `NEW_TAG` and 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_TAG` matches 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 of `main` has passing checks.
- [ ] **Release notes drafted** — see **Drafting release notes** and
[references/release-notes-template.md](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), targeting `main`.
- [ ] **Confirm** the `Release` workflow succeeded and the version is on PyPI.
## What triggers a release
**Publishing a GitHub release** — [.github/workflows/release.yml](../../.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
1. **build** — checks out the release's tag with full history (setuptools-scm
needs the tag present to resolve the version), runs `ruff` and the full test
suite, builds the wheel and sdist, and runs `twine check`.
2. **publish** — uploads to PyPI via Trusted Publishing (OIDC, no stored
token), in the `pypi` environment.
3. **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:
```bash
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.
1. **Write the notes to a temp file** and register cleanup, so a failure does
not leave it behind:
```bash
NOTES_FILE="$(mktemp "${TMPDIR:-/tmp}/hermes-hookdeck-release-notes.XXXXXX.md")"
trap 'rm -f "$NOTES_FILE"' EXIT
```
2. **Write** the final markdown body to `"$NOTES_FILE"`.
3. **Create the release**, targeting `main`:
```bash
gh release create "vM.m.p" \
--repo hookdeck/hermes-hookdeck \
--target main \
--title "vM.m.p" \
--notes-file "$NOTES_FILE"
```
Add `--prerelease` for `rc`, `a` or `b` versions, so the repository's
"latest release" does not point at a release candidate.
4. **Watch it**: `gh run watch` — or `gh 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
```bash
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](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
1. **Tags:** `git tag --sort=-v:refname | head -5`. Confirm `PREV_TAG`.
2. **Commits:** `git log PREV_TAG..origin/main` — full messages. This repo
writes the reasoning into commit bodies; that is the changelog source.
3. **Shipped or not:** `git diff --stat PREV_TAG..origin/main -- hookdeck/`.
4. **Group** by user impact, not by file.
5. **SemVer check** against the table above.
6. **CI:**
```bash
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
answers `pending` forever, so it reads as a red gate on a green `main`.
## 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.0` for 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](../../AGENTS.md).
## Related files
| Topic | Location |
|---|---|
| Human release steps | [README.md § Releasing](../../README.md) |
| CI entrypoint | [.github/workflows/release.yml](../../.github/workflows/release.yml) |
| Version derivation | [pyproject.toml](../../pyproject.toml) `[tool.setuptools_scm]` |
| What is packaged | [pyproject.toml](../../pyproject.toml) `[tool.setuptools.package-data]` |
| Repo conventions | [AGENTS.md](../../AGENTS.md) |
Ver no GitHub