This is the runbook for cutting a new trellis-ai release to PyPI. Follow it top to bottom.
- Create a PyPI project + register the trusted publisher.
- Go to https://pypi.org/manage/account/publishing/.
- Add a "pending publisher":
- PyPI project name:
trellis-ai - Owner:
ronsse - Repository:
trellis-ai - Workflow filename:
publish.yml - Environment:
pypi
- PyPI project name:
- The first publish will reserve the project name and finalize the trust relationship.
- Create the GitHub
pypienvironment.- Repo → Settings → Environments → New environment → name
pypi. - (Optional but recommended) require a manual approval reviewer so prod publishes are gated.
- Repo → Settings → Environments → New environment → name
- Confirm
id-token: writeworks. The publish workflow already sets this — no per-release action needed.
make check # lint + typecheck + tests
make verify-wheel # builds + lists what's in dist/*.whl and dist/*.tar.gz
make publish-check # twine check on the artifactsEyeball make verify-wheel output. Confirm:
src/trellis*packages are present.- No stray
examples/,docs/,tests/under the wheel (those go in the sdist only). py.typedmarker files appear under eachtrellis*package.
- Move items from
## [Unreleased]into a new## [X.Y.Z] - YYYY-MM-DDsection. - Group by Added / Changed / Fixed / Removed / Breaking.
- For breaking changes, name the user-visible thing that broke (entry point, API, config key) so users grep the changelog and find the migration path.
git add CHANGELOG.md
git commit -m "Release vX.Y.Z"
git tag -s vX.Y.Z -m "vX.Y.Z"
git push origin main --follow-tagsThe tag triggers .github/workflows/publish.yml. Watch it on the Actions tab.
- The
testjob runs lint + mypy + pytest on Python 3.11/3.12/3.13. - The
publishjob builds, runstwine check, and uploads via OIDC trusted publishing — no API token needed. - If the
pypienvironment requires an approval, click Approve and deploy.
- Verify on PyPI: https://pypi.org/project/trellis-ai/X.Y.Z/.
- Confirm
pip install trellis-ai==X.Y.Zworks in a clean venv:python -m venv /tmp/trellis-test && /tmp/trellis-test/bin/pip install "trellis-ai==X.Y.Z" /tmp/trellis-test/bin/trellis --version /tmp/trellis-test/bin/trellis-mcp --help
- Create the GitHub release: Releases → Draft a new release → choose tag
vX.Y.Z→ paste the matching CHANGELOG section → publish. - Reset
## [Unreleased]at the top of CHANGELOG.md for the next cycle.
The publish workflow accepts workflow_dispatch with a ref input. If a tag's run failed transiently (rate limit, flaky CI, network), re-run it from the Actions tab without re-tagging:
- Actions → Publish to PyPI → Run workflow → enter the tag (e.g.
v0.2.0) → Run.
If the build itself was broken, fix on main, tag a new patch version (vX.Y.Z+1), and push. Do not re-tag — PyPI rejects re-uploads of the same version, and yanking is for emergencies, not retries.
- We use SemVer. Pre-1.0 means we may break minor versions, but we'll call it out in CHANGELOG and bump
0.Xaccordingly. - The version is derived from git tags by
hatch-vcs. Don't write it into a Python file. Tagging is the source of truth. - For RCs, tag
vX.Y.Z-rc1etc. Hatch derives PEP 440-compliant pre-release versions (X.Y.Z.dev0+gSHAfor untagged builds,X.Y.Zrc1for RC tags).
If you want to test the release pipeline without affecting real PyPI:
- Register a separate trusted publisher on https://test.pypi.org/ (same project name).
- Add a parallel workflow file or a job step pointing at
repository-url: https://test.pypi.org/legacy/on thepypa/gh-action-pypi-publishaction. - Tag
vX.Y.Z-rc1and push. Verify on https://test.pypi.org/project/trellis-ai/.
This adds setup cost — skip it until you've shipped at least one real release and want a buffered rollout for future major versions.
If a release is actively harmful (data loss, install-time crash):
# From the PyPI project page → Manage → Releases → ⋯ → Yank releaseYank, don't delete. Yanked versions stay installable by exact pin (==X.Y.Z) but are excluded from solver resolution. Then ship a fixed X.Y.Z+1 immediately and call out the yank in the CHANGELOG.