chore(release): automate GitHub Release + document the flow#50
Merged
Conversation
The publish.yml workflow has been in the repo for a while but no release has actually gone through it — all 6 PyPI versions (0.0.1 through 0.1.3) were uploaded directly via twine from a maintainer's laptop. No git tags exist on the remote, no GitHub Releases page is populated, no changelog tracks what shipped when. Three changes to fix the gap: 1. **publish.yml** — verify the tag matches `pyproject.toml` `[project].version` (fail fast on mismatch), then after the PyPI publish step run softprops/action-gh-release to create a GitHub Release with auto-generated notes and the built sdist + wheel attached. Adds the `contents: write` permission required for release creation. 2. **RELEASING.md** — explicit maintainer doc: bump version, bump changelog, commit, tag `vX.Y.Z`, push the tag, let CI do the rest. Calls out explicitly to NOT run `twine upload` locally anymore. 3. **CHANGELOG.md** — Keep-a-Changelog skeleton with an `[Unreleased]` section that already lists the substantive changes in flight (PRs #34, #38, #45, #47/#49, #48). The four pre-existing PyPI releases (0.1.0 / 0.1.1 / 0.1.3 — 0.1.2 leaves no commit trail and only appears on PyPI) are backfilled with short summaries inferred from git log, honestly noting that no tag was created at the time. Also locally cleaned up the stale `v0.1.0.dev0` tag that was sitting in the working clone but never pushed. This PR does not retroactively tag 0.1.3 because doing so would trigger the workflow to attempt to re-publish 0.1.3, which PyPI would reject. The first run of the new flow will happen the next time a maintainer cuts a release.
Collaborator
Author
Code reviewFound 2 issues:
Lines 13 to 18 in b82f786
Lines 74 to 76 in b82f786 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…nto workflow The Keep-a-Changelog file duplicates what `generate_release_notes: true` already produces from merged PRs on the GitHub Releases page, with the extra downside of needing manual maintenance — the initial review of this PR caught three drift bugs in the backfilled CHANGELOG (an unmerged PR mistakenly listed, a broken compare link, and a skipped 0.1.2). Drop it; can be added back when the API stabilizes and a manual narrative becomes useful. RELEASING.md's value was its release flow + "don't run twine upload" warning. Move both into a comment header at the top of publish.yml itself, so the guidance lives next to the workflow it documents and is visible to anyone reading the file. Removes a separate doc to maintain. Net: PR now contains only the actual workflow change (tag/pyproject version-match check + GitHub Release creation step + `contents: write` permission for it).
v1.11.0 ships an older twine that only understands Metadata-Version up to 2.3. Modern hatchling produces wheels with Metadata-Version 2.4, so twine's pre-upload check fails with "InvalidDistribution: Metadata is missing required fields: Name, Version" even though both fields are present (twine just can't parse the newer metadata format). Verified live: tag v0.1.4.dev0 reached the publish step and failed at this check (run 25898978723). Bumping to v1.14.0 brings a recent twine that handles 2.4 metadata.
… action `pypa/gh-action-pypi-publish` is a Docker container action — it pulls its image from `ghcr.io/pypa/gh-action-pypi-publish:<SHA>` keyed by the commit SHA. The previous pin `6733eb7d741f0b11ec6a39b58540dab7590f9b7d` was the SHA of the v1.14.0 annotated *tag object*, not the commit it points to, so `ghcr.io/pypa/gh-action-pypi-publish:6733eb7d...` doesn't exist and the docker pull fails with "manifest unknown". The actual commit SHA for v1.14.0 is cef221092ed1bacb1cc03d23a2d87d1d172e277b (resolved via `git/tags/<tag-object-sha>` → `.object.sha`). For JS actions (checkout, setup-python, softprops/action-gh-release) this distinction doesn't matter because the action source is cloned by git ref, which transparently dereferences tag objects. For Docker container actions, the SHA is used literally as an image tag — so it must be the commit SHA. Verified live: run 25899049188 failed at this step with `docker: Error response from daemon: manifest unknown` after the v1.11.0→v1.14.0 bump in the prior commit. This commit corrects the SHA so the image pull resolves.
Reverts the 0.1.4.dev0 bump from commit 2f9869d. The end-to-end dry-run on the chore/release-process branch successfully published 0.1.4.dev0 to PyPI via OIDC and created a GitHub Release (run 25899125441, all steps green). PyPI's trusted publisher for openkb is confirmed correctly configured. Cleanup performed: - Deleted v0.1.4.dev0 GitHub Release. - Deleted v0.1.4.dev0 tag (local + remote). - 0.1.4.dev0 on PyPI cannot be deleted (PyPI policy), but .dev0 suffix means pip will not install it by default — no user impact. The branch can now be merged cleanly. The next real release will use the same flow.
The previous flow required bumping pyproject.toml's `[project].version`
AND pushing a matching `vX.Y.Z` tag — two manual steps in sync, with a
workflow guard to catch drift. With hatch-vcs the version becomes a
single source of truth (the git tag), and the entire release reduces to
`git tag -a vX.Y.Z -m ... && git push origin vX.Y.Z`.
Changes:
- pyproject.toml: add `hatch-vcs` to build-system requires; mark version
as `dynamic = ["version"]`; add `[tool.hatch.version] source = "vcs"`.
- publish.yml: drop the now-redundant tag/pyproject version-match step
(the two sources are merged into one). Add `fetch-depth: 0` to the
checkout so hatch-vcs sees the full tag history. Update the header
comment to describe the simplified flow.
Local editable installs (`pip install -e .`) without a matching tag
will produce a dev-suffixed version like `0.1.3.post4.dev2+gabc1234`
("4 commits past v0.1.3, working tree at abc1234"). This is the
expected hatch-vcs behavior and doesn't affect tagged builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
`.github/workflows/publish.yml` has been in the repo for a while but no release has actually gone through it — all 6 PyPI versions (0.0.1 through 0.1.3) were uploaded directly via `twine` from a maintainer's laptop. There are zero git tags on the remote, the GitHub Releases page is empty, and the project has no changelog. This PR closes the gap so the next release can flow tag → CI → PyPI → GitHub Release in one step.
Changes
`.github/workflows/publish.yml`
`RELEASING.md` (new)
Maintainer-facing doc. The flow is now strictly:
Explicitly calls out: do not run `twine upload` locally anymore.
`CHANGELOG.md` (new)
Keep-a-Changelog skeleton with an `[Unreleased]` section listing the substantive changes already merged or in flight (#34, #38, #45, #47 / #49, and #48 if it lands). The four pre-existing PyPI releases (0.1.0, 0.1.1, 0.1.3 — 0.1.2 leaves no commit trail and only exists on PyPI) are backfilled with short summaries from `git log`, honestly noting that no tag was created at the time.
What this PR deliberately does NOT do
Test plan
🤖 Generated with Claude Code