Documents / Runbooks / run-30aceb4eacc6
Publishing to PyPI
How to publish docir to PyPI with uv and GitHub Actions trusted publishing.
This project uses UV as the package manager and GitHub Actions for automated publishing to PyPI.
Prerequisites¶
- PyPI Account: Create an account at https://pypi.org/
- Trusted Publishing: Configure trusted publishing (no API tokens needed!) at https://pypi.org/manage/account/publishing/
- Add a new publisher with:
- PyPI Project Name:
docir - Owner:
l0kifs - Repository name:
docir - Workflow name:
publish-to-pypi.yml - Environment name: (leave blank)
- PyPI Project Name:
- Add a new publisher with:
Automated Publishing (Recommended)¶
The project is configured to automatically publish to PyPI when a new GitHub release is created.
Check current release version before starting:
gh release list --limit 10 2>&1 | cat
-
Update version in
pyproject.toml(the single source of truth for the version):tomlversion = "0.2.0" # Update to your new version -
Update CHANGELOG.md (required): move entries from
[Unreleased]into a new version section and update the compare links at the bottom. A section per Keep-a-Changelog heading, plus Measured and rejected for anything built and removed — the measurement is the artifact, not the code. -
Refresh the generated agent instructions (required):
docir agent updatestamps the files from the running__version__, so it has to run after the bump in step 1 and before the commit in step 5. Nothing detects a stale stamp later — 0.11.0 shipped with docir's own.claude/skills/docir/SKILL.mdstill claiming v0.10.0. Seerun-f4a756206fe0for what a consumer of the release then has to run.bashuv run docir agent update # the workspace build, not the installed tool -
Stop the daemon (required, and easy to skip):
docir daemon stop.A daemon started before the bump is still running the old build, and its watcher keeps reindexing whenever a file under
docs/changes — stamping the index with the version it loaded.--no-daemoncommands do not replace it, because nothing routes through it to notice the mismatch, sodocir checkgoes clean after a reindex and then goes stale again on its own the next time anything writes a document.bashdocir daemon stop # the next command spawns one on the new build docir daemon status # says which build is serving, and whether it is staleThis happened cutting 0.19.0.
daemon statusdiagnosed it exactly — serving 0.18.0 (stale code — the next command replaces it) — and it was only noticed becauseadr-f14682e3f4d6requires re-runningcheckagainst the corpus after the release rather than trusting the green from before it. -
Commit and push your changes:
bashgit add pyproject.toml CHANGELOG.md .claude/skills/docir/SKILL.md git commit -m "Bump version to 0.2.0" git push -
Check the release notes before publishing (required if they show a
--expr):bashuv run python scripts/check_expressions.py notes.mdA release body is the one surface docir's own guards do not reach — the prose test covers what ships in the wheel and
lint --deepcovers a store's documents, but a release page is written once, published, and copied from. v0.18.0 shippedowner == nullin its notes, which is a JMESPath identifier rather than a literal: it gave the right answer for the wrong reason until 0.19.0 refused it, and the page had to be corrected after the fact.The check takes the notes file, so it runs before
gh release createand needs no network. It prints how many arguments it checked — "0 problems" and "nothing was checked" are the one pair a gate must never conflate. -
Create a GitHub release:
Using GitHub CLI with inline notes:
bash# Create the release gh release create v0.2.0 \ --title "v0.2.0 - Release Title" \ --notes "## 🎯 New Features - Feature 1 description - Feature 2 description ## 🐛 Bug Fixes - Fix 1 description ## 📚 Documentation - Doc updates ## 🔗 Full Changelog See [CHANGELOG.md](https://github.com/l0kifs/docir/blob/v0.2.0/CHANGELOG.md)"Or using the GitHub web interface:
- Go to https://github.com/l0kifs/docir/releases/new
- Create a new tag (e.g.,
v0.2.0) - Add release title and description
- Click "Publish release"
To verify the release:
bashgh release view v0.2.0 -
GitHub Actions will automatically:
- Build the package using UV
- Publish to PyPI using trusted publishing
- You can monitor the progress in the Actions tab
-
Record the release in the store (required): a
release_notedocument, linked to the decisions the release is made of.bashdocir add --type release_note --status published \ --title "0.X.0 — <the thesis, same as the release title>" \ --description "<one sentence: what the release made possible>" \ --related adr-...,adr-...,ref-... \ --stdin < notes.mdNot a second changelog.
CHANGELOG.mdand the GitHub release carry the full text; this carries what neither can — the edges. Link every decision the release contains, especially the ones recording work that was built and thrown away: those are what a later reader most needs, because they are what somebody will otherwise propose again, and a changelog has nowhere to put them.Carry the upgrade note too. It is the actionable half, and
docir context "what shipped in 0.X.0"is where somebody will look for it.Status
published, since the release is. Commit it separately fromchore(release): the version bump is the release, and this describes it.
Manual Publishing¶
If you need to publish manually:
-
Install UV (if not already installed):
bashpip install uv -
Build the package:
bashuv buildThis creates distribution files in the
dist/directory. -
Publish using UV (requires PyPI API token):
bashuv publishOr use
twine:bashpip install twine twine upload dist/*
Testing on TestPyPI¶
Before publishing to the main PyPI, you can test on TestPyPI:
-
Configure trusted publishing for TestPyPI at https://test.pypi.org/manage/account/publishing/
-
Manually trigger the workflow or publish directly to TestPyPI:
bashuv publish --index-url https://test.pypi.org/legacy/ -
Test installation:
bashpip install --index-url https://test.pypi.org/simple/ docir
Best Practices¶
- Always create tags on the
mainbranch - Never tag ondevelopor feature branches - Merge develop to main before tagging - Ensure all changes are in main
- Test on TestPyPI first (optional but recommended for major releases)
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Analyze changes in the repository between the last release and current state
- Update CHANGELOG.md with all changes before release (required)
- Test build locally before pushing tags
- Keep credentials secure - use project-specific tokens
- Test installation from PyPI after publishing
- Create a GitHub Release after a successful publish
- Monitor PyPI stats and user feedback