publishing.md
July 15, 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
Releases are cut from
main. Every step below assumes you are on an up-to-datemain, so start there.If
ghcannot infer the repo (for example, the git remote points at a proxy rather thangithub.com), pass-R OWNER/PROJECTto everyghcommand below. You can confirm auth withgh auth status.
-
Check out
mainand make sure it is current:git checkout main git pull origin main git status # working tree should be clean git log origin/main..HEAD # should be empty (nothing unpushed) -
Run linting and tests locally:
make lint-check make test make build -
Confirm CI is passing on
main:gh run list --branch main --limit 3The latest run on
mainmust be green before you tag. (A superseded older failure is fine, but the most recent run for the commit you are about to tag must pass.) Or check the Actions tab on GitHub. -
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
-
-
Bump the skill’s
uvxbootstrap pin (one source of truth):The repo-root
skills/flowmark/SKILL.md(shipped tonpx skills add jlevy/flowmark@flowmarkusers who do not have flowmark pre-installed) and the README’s runner examples all pinuvx --from flowmark==<X.Y.Z>. That pin must reference a real, PyPI-installable release (never a<version>placeholder or a.dev/local-suffix string), and it must be the release you are about to cut, or agents bootstrap a stale flowmark.There is exactly one place to change: the
DISCOVERY_VERSIONconstant insrc/flowmark/skill.py.make formatpropagates it to every artifact: the discovery copy (viagenerate-skill-discovery.py), the README runner examples (via the__FLOWMARK_VERSION__placeholder indocs/shared/flowmark-readme-shared.mdthatgenerate-python-readme.pysubstitutes), and this repo’s own installed skill surfaces (.agents/,.claude/, theAGENTS.mdblock, viaflowmark --install-skill). Sogit add -Aaftermake formatcaptures the whole set.Do this in the final PR before tagging:
DISCOVERY_VERSIONshould equal the version you are about to release. The pin then references the imminent release: it is not installable from PyPI until the tag publishes (expected), and the publish workflow refuses to publish if the tag andDISCOVERY_VERSIONdisagree. Bump, regenerate, verify, commit:# In src/flowmark/skill.py: DISCOVERY_VERSION = "<NEW_TAG without leading v>" make format make check-release-pin VERSION=<NEW_TAG without leading v> # must print "Release pin OK" git add -A git commit -m "skill: bump DISCOVERY_VERSION to vX.Y.Z"Guards (no stale or non-resolvable pin can ship):
tests/test_skill_artifacts.py::test_discovery_copy_has_resolvable_version_pin— the pin is a real PyPI release, not a placeholder/dev string.tests/test_skill_artifacts.py::test_shipped_artifacts_pin_discovery_version— every shipped artifact pins exactlyDISCOVERY_VERSION(catches a forgottenmake format).scripts/check-release-pin.py(run inpublish.ymlagainst the release tag, and viamake check-release-pin), which fails the publish ifDISCOVERY_VERSIONdoes not match the release being cut.
Create the Release
-
Generate release notes content:
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 -
Create the release with
gh:Write the notes to a file and pass
--notes-file. Author the file directly in your editor — do not pipe the notes through a shell heredoc. Release notes are Markdown prose that routinely contains backticks and$, and a heredoc mangles both:- an unquoted heredoc (
<<EOF) runs command substitution on`...`and expands$name, so`--flag`in your notes executes and$foodisappears; - a quoted heredoc (
<<'EOF') is literal, but then a${LAST_TAG}/${NEW_TAG}compare link ships verbatim instead of expanding.
So just write the file (it is plain Markdown — no shell involved), put a concrete compare link in it, proofread the rendered Markdown, then create the release:
NEW_TAG="vX.Y.Z" # the tag you are about to create # Find the previous tag for the compare link: gh release list --limit 1 # Write release-notes.md in your editor, following "Release Notes Format" below, # ending with a concrete compare link, e.g.: # https://github.com/OWNER/PROJECT/compare/vPREV...vX.Y.Z gh release create "${NEW_TAG}" --title "${NEW_TAG}" --notes-file release-notes.mdTagging triggers
publish.yml, which re-runsscripts/check-release-pin.pyagainst the tag and refuses to publish ifDISCOVERY_VERSIONdisagrees with the tag — so a mismatched pin fails the publish rather than shipping a stale one.Alternatively, use
--generate-notesfor GitHub’s auto-generated notes. - an unquoted heredoc (
-
Verify the release published successfully:
# Watch the release workflow to completion: gh run list --workflow=publish.yml --limit 1 gh run watch "$(gh run list --workflow=publish.yml --limit 1 --json databaseId -q '.[0].databaseId')" # Verify on PyPI (may take a minute): # https://pypi.org/project/PROJECTThen smoke-test the published artifact — the pin agents bootstrap with should now resolve:
uvx --from flowmark==X.Y.Z flowmark --version
Release Notes Format
Use this structure for release notes. List sections in this order, from most to least disruptive, and omit any section that is empty (do not pad with “none”):
## What's Changed
### Breaking Changes
**Short title of breaking change**
What was removed or changed incompatibly (API signature, CLI flag, removed behavior) and
how to migrate.
### Behavior and Compatibility Changes
**Short title of behavior change**
A change to default *output* or runtime behavior that is not an API break. For example,
the formatter now produces different (but valid) Markdown for some input, line breaks
land differently, or default option values changed. Say exactly which inputs are
affected and whether the result is rendering-equivalent.
### New Features and API
**Short title of feature or new public API**
New capabilities, new CLI flags, and new public functions/types. Additive only: anything
that *changes* existing behavior belongs above, not here.
### Bug Fixes
**Short title of fix**
What was fixed and why it matters. If the fix changes output for previously-broken input,
note that here (it is a fix, not a behavior change) but be explicit that output differs.
### Full Changelog
https://github.com/OWNER/PROJECT/compare/vPREVIOUS...vNEW
Guidelines:
-
Use
## What's Changedas the top-level heading. -
The four categories are deliberately distinct. Classify each change by asking, in order:
-
Does it remove or incompatibly change a public API, CLI flag, or documented behavior? → Breaking Changes.
-
For the same input, does the tool now produce different output or behave differently (even if valid and rendering-equivalent), or did a default change? → Behavior and Compatibility Changes. This is the category most often missed: a formatter whose output drifts between versions is a compatibility concern (diffs, golden tests, re-flowed files) even when nothing is strictly “broken”.
-
Is it purely additive (new flag, new public function/type, new capability, with no change to existing behavior)? → New Features and API.
-
Did it correct previously-wrong or broken output? → Bug Fixes (and state plainly when output changes as a result).
-
-
When in doubt between Behavior and Compatibility and Bug Fixes, prefer Behavior and Compatibility and explain why: readers diffing reformatted files care about any output change regardless of intent.
-
Describe the aggregate delta between the previous release and this one, not individual commits. If a feature was added and then fixed before release, describe the feature as it now works rather than listing the intermediate fix separately.
-
Skip internal-only changes that users never see: CI/tooling, pure refactors, test-only work, and dependency or doc housekeeping.
-
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, but still group it under these headings so behavior/compatibility changes are never buried among features or fixes.
This file was built with simple-modern-uv.