publishing.md
July 13, 2026 · View on GitHub
Publishing Releases
This is how to publish a Python package to PyPI from GitHub Actions, when using the simple-modern-uv template.
Thanks to
the dynamic versioning plugin and
the
publish.yml workflow,
you can simply create tagged releases (using standard format for the tag name, e.g.
v0.1.0) on GitHub and the tag will trigger a release build, which then uploads it to
PyPI.
First-Time Setup
This part is a little confusing the first time. Here is the simplest way to do it. For the purposes of this example replace OWNER and PROJECT with the right values.
Note: These steps assume you already have a GitHub repo with your code pushed. If not, create an empty GitHub repo (no README, no .gitignore, no license; the template already provides these) and push your code to it. See the README for details.
-
Get a PyPI account at pypi.org and sign in.
-
Pick a name for the project that isn’t already taken.
-
Go to
https://pypi.org/project/PROJECTto see if another project with that name already exists. -
If needed, update your
pyproject.tomlwith the correct name.
-
-
Authorize your repository to publish to PyPI:
-
Go to the publishing settings page.
-
Find “Trusted Publisher Management” and register your GitHub repo as a new “pending” trusted publisher.
-
Enter the project name, repo owner, repo name, and
publish.ymlas the workflow name. (You can leave the “environment name” field blank.)
-
-
Create a release on GitHub:
-
Commit code and make sure it’s running correctly.
-
Go to your GitHub project page, then click on Actions tab.
-
Confirm all tests are passing in the last CI workflow. (If you want, you can even publish this template when it’s empty as just a stub project, to try all this out.)
-
Go to your GitHub project page, click on Releases.
-
Fill in the tag and the release name. Select to create a new tag, and pick a version. A good option is
v0.1.0. (It’s wise to have it start with av.) -
Submit to create the release.
-
-
Confirm it publishes to PyPI
-
Watch for the release workflow in the GitHub Actions tab.
-
If it succeeds, you should see it appear at
https://pypi.org/project/PROJECT.
-
Publishing Subsequent Releases
Follow this checklist for each new release.
Pre-Release Checklist
-
Cut the release from an up-to-date
main:git checkout main git pull origin main git status # confirm a clean working tree -
Verify all changes are committed and pushed:
git log origin/main..HEAD # should be empty if pushed -
Run linting and tests locally:
make lint make test -
Confirm CI is passing on
main:gh run list --branch main --limit 3Or check the Actions tab on GitHub. The most recent run for the commit you’re about to tag must be green (a superseded older failure is fine).
-
Determine the new version number:
# Check current/latest version: gh release list --limit 1Use semantic versioning:
-
Patch (e.g.,
v0.5.8→v0.5.9): Bug fixes, minor changes -
Minor (e.g.,
v0.5.9→v0.6.0): New features, backward-compatible -
Major (e.g.,
v0.6.0→v1.0.0): Breaking changes
-
Create the Release
-
Review changes since the last release:
# Get the last release tag: LAST_TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName') # View commits since last release: git log ${LAST_TAG}..HEAD --oneline # View full diff: git diff ${LAST_TAG}..HEAD -
Write the release notes in a file, then create the release:
Author the notes as plain Markdown in a file (see Release Notes Format below), then pass it with
--notes-file. Writing the notes in a file keeps the shell out of the way: release notes routinely contain backticks and$, which a shell heredoc would try to run as commands or expand as variables. End the notes with a concrete compare link built from the actual tags (substitute the realLAST_TAGandNEW_TAGvalues into the URL), e.g.https://github.com/OWNER/PROJECT/compare/v0.1.0...v0.2.0.NEW_TAG="vX.Y.Z" # Replace with the actual version # Edit release-notes.md in your editor, ending with the concrete compare link. gh release create "${NEW_TAG}" --title "${NEW_TAG}" --notes-file release-notes.mdAlternatively, use
--generate-notesfor GitHub’s auto-generated notes. -
Verify the release published successfully:
# Check the release workflow: gh run list --workflow=publish.yml --limit 1 # Verify on PyPI (may take a minute): # https://pypi.org/project/PROJECTOnce it appears on PyPI, smoke-test that the published artifact actually resolves and installs from PyPI. If your project exposes a CLI:
uvx --from PROJECT==X.Y.Z PROJECT --version
Agent Runbook: kash Specifics
Repo-specific additions to the checklist above (keep this section when updating the template).
-
Release notes: load
tbd guidelines release-notes-guidelinesand write the aggregate delta since the last tag (not a commit recap), using the format below. If the release changes dependency policy (pins, overrides, floors), say what installed environments actually get:tool.uvoverrides and constraints are lock-only and do NOT ship in wheel metadata. -
If
ghis unavailable (some sandboxes block it or the GitHub API), the equivalent flow is:# Annotated tag carries the release notes; tag the merge commit on main. git tag -a vX.Y.Z <sha> -F release-notes.md git push origin vX.Y.ZThen trigger
publish.ymlon the tag ref via the GitHub Actions UI or API (workflow_dispatchis enabled). The workflow publishes to PyPI and then auto-creates the matching GitHub Release from the annotated tag's message, so every PyPI release has a Release page either way. To backfill a Release page for an older tag, dispatchgithub-release.ymlwith the tag as input. Publishing is idempotent (--check-url), so re-runs never fail on existing files. -
Confirm on PyPI, scriptably (the JSON API updates within ~a minute):
curl -s https://pypi.org/pypi/kash-shell/json | python3 -c \ "import json,sys; print(json.load(sys.stdin)['info']['version'])"Then smoke-test the published artifact:
uvx --from kash-shell==X.Y.Z kash --version. -
Downstream follow-through (the kash release order is kash → kash-docs → kash-media → apps like deep-transcribe):
-
Bump
kash-shell==X.Y.Zin kash-docs (and kash-docs in kash-media, etc.). -
A same-day first-party release is inside the 14-day cool-off window; use a per-package exception in the downstream repo rather than weakening the global policy, and remove it at the next routine upgrade:
[tool.uv] exclude-newer-package = { kash-shell = "<tomorrow>T00:00:00Z" } -
File or close the matching tbd beads and run
tbd sync.
-
Release Notes Format
Use this structure for release notes:
## What's Changed
### Bug Fixes
**Short title of fix**
Description of what was fixed and why it matters.
### New Features
**Short title of feature**
Description of the new capability.
### Breaking Changes
**Short title of breaking change**
Description of what changed and how to migrate.
### Full Changelog
https://github.com/OWNER/PROJECT/compare/vPREVIOUS...vNEW
Guidelines:
-
Use
## What's Changedas the top-level heading. -
Group changes under
### Bug Fixes,### New Features,### Breaking Changes, etc. as appropriate. -
Use
**bold**for short titles of individual changes. -
Include technical details only when helpful for users.
-
Always include the Full Changelog compare link at the end.
-
For small releases, a simple bullet list is acceptable instead of full sections.
This file was built with simple-modern-uv.