Skip to content

Publish releases to PyPI from version tags - #12

Merged
chris-colinsky merged 2 commits into
mainfrom
chore/pypi-release
Sep 25, 2026
Merged

chris-colinsky merged 2 commits into
mainfrom
chore/pypi-release

Conversation

@chris-colinsky

@chris-colinsky chris-colinsky commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

What changes

  • Releases publish from a tag. Pushing a tag such as v0.1.0 runs .github/workflows/release.yml, which runs four jobs in order:

    1. test: the same checks as CI, on Python 3.12, 3.13 and 3.14. It first checks that the tag matches the version in pyproject.toml, and that CHANGELOG.md has exactly ## [X.Y.Z] - YYYY-MM-DD, dated within a day of the tag.
    2. build: builds the sdist and the wheel with uv build, and takes that version's CHANGELOG.md section as the release notes, failing if it's empty.
    3. publish: publishes both to PyPI through trusted publishing (OIDC, no stored token) in the pypi environment.
    4. release: creates the GitHub Release, with the notes from build and both files attached.

    Every check that can stop a release runs before publish, because a PyPI version number can never be reused.

    It follows the forbin-mcp release workflow, adapted to this repo: every action pinned to a commit SHA like the existing CI, uv for the build, release notes from the changelog instead of generated ones, and no Homebrew. Permissions are read-only by default; only publish (id-token: write) and release (contents: write) ask for more.

  • CI tests every supported Python. The CI test job now runs on 3.12, 3.13 and 3.14, the versions requires-python allows, instead of only the pinned 3.12. Each job's version overrides .python-version.

  • docs/RELEASING.md covers the one-time PyPI and GitHub setup, the release steps (changelog, docs sweep, version and date, review, tag), how to check a release, and troubleshooting.

  • pyproject.toml gains keywords, Documentation, Changelog and Issues links, and 3.13 and 3.14 classifiers. The sdist now lists what it includes. Hatchling honours .gitignore but not .git/info/exclude, so a hand-built sdist could otherwise pick up untracked local files.

  • README.md points to the release guide. Its install section stays on the git install until 0.1.0 is on PyPI; switching to uv tool install akceo is part of the release branch.

Setup

The PyPI trusted publisher (pending, for akceo: owner LunarCommand, repo akceo, workflow release.yml, environment pypi) and the GitHub pypi environment are already in place.

Not in this PR

Releasing 0.1.0. That gets its own branch: finishing the changelog, sweeping the docs, dating the section and reviewing everything that ships, before any tag is pushed.

Testing

  • Checked locally, with GNU date and the runner's bash -eo pipefail:
    • The heading check: passes for today and tomorrow; fails with no date, on a 0x1x0 look-alike, on an invalid date (2026-02-30), on a five-day-old date, and on a section still called Unreleased. A tag that doesn't match pyproject.toml fails too.
    • The notes extraction: it takes exactly the version's own section, and fails on an empty one.
    • uv build: the sdist holds only the listed files, and the wheel's metadata has the license expression, links and keywords.
  • PyPI already accepts Metadata-Version: 2.5, which hatchling now writes; hatchling 1.32.4's own wheel was published with it.
  • The locked Pillow has wheels for 3.12 to 3.14, and mini-racer is pure py3.
  • Locally on 3.12: 168 passed, plus ruff, pyright and every pre-commit hook. 3.13 and 3.14 run for the first time in this PR's CI.
  • The release workflow itself only runs on a real tag.

Pushing a vX.Y.Z tag now tests the package on Python 3.12 to 3.14,
builds it, publishes it to PyPI through trusted publishing, and
creates a GitHub Release whose notes are that version's CHANGELOG.md
section. The release stops early if the tag, the version in
pyproject.toml and the changelog disagree. docs/RELEASING.md covers
the one-time setup and the release steps.

CI now runs the tests on Python 3.12, 3.13 and 3.14, the versions
requires-python allows, instead of only the pinned 3.12.

The sdist lists what it includes, so files kept out of git some other
way can't slip into a package built from a working copy. The README
installs from PyPI, and the project metadata gains keywords and links.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Changelog validation can accept malformed headings and detects empty release notes only after PyPI publication.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Adds tag-driven PyPI and GitHub releases, expands supported-Python CI coverage, and documents packaging and release procedures.

Changes:

  • Adds a four-stage release workflow using PyPI trusted publishing.
  • Tests Python 3.12–3.14 and defines explicit source-distribution contents.
  • Updates installation, metadata, changelog, and release documentation.
File Description
.github/​workflows/​release.yml Adds testing, building, publishing, and GitHub release jobs.
.github/​workflows/​ci.yml Adds a supported-Python test matrix.
pyproject.toml Expands package metadata and configures sdist contents.
docs/​RELEASING.md Documents release setup and procedures.
README.md Adds PyPI installation and release guidance.
CHANGELOG.md Records PyPI availability.
CLAUDE.md Documents the new release workflow and guide.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread README.md Outdated
A published PyPI version can never be reused, so every check that can
stop a release now runs before the publish job. The changelog heading
must match exactly, with the version's dots taken literally, and be
dated within a day of the tag. The release notes are taken and checked
in the build job, and handed to the release job as their own artifact,
so an empty section no longer fails only after publishing.

Keep the README's install on the repository until 0.1.0 is on PyPI;
the PyPI install moves to the release branch.
@chris-colinsky
chris-colinsky merged commit 3597e8d into main Sep 25, 2026
3 checks passed
@chris-colinsky
chris-colinsky deleted the chore/pypi-release branch September 25, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants