Release process¶
Releases are automated by release-please
reading Conventional Commits history on main, and published to PyPI via
Trusted Publishing (OIDC — no
long-lived API token stored anywhere).
The automated loop¶
- A PR merges into
develop, thendevelopmerges intomainas a merge commit (preserving individual conventional commits — see development.md). release-please.ymlruns on every push tomain. It maintains a single standing PR titledchore(release): x.y.z, whose body is the generated changelog for everything merged since the last release, and whose diff bumps[project].versioninpyproject.tomland updatesCHANGELOG.md.- Merging that PR is what cuts a release — it's the human review gate. On merge, release-please tags the commit and creates a GitHub Release.
publish-to-pypi.ymltriggers onrelease: published, builds the sdist and wheel, and publishes to PyPI via Trusted Publishing. The GitHub environmentpypirequires manual approval before the publish job runs — a second human gate, independent of the release-please merge.
Nothing in this loop requires the maintainer to hand-bump a version number, hand-write a changelog entry, or hold a PyPI API token anywhere.
One-time manual setup (already done for this repo, documented for forks)¶
- Create the GitHub repo
adammatthewsteinberger/claudeloop, pushmainanddevelop, setmainas the default branch. - PyPI → Account settings → Publishing → Add a new pending publisher:
| Field | Value |
|---|---|
| PyPI Project Name | claudeloop |
| Owner | adammatthewsteinberger |
| Repository name | claudeloop |
| Workflow name | publish-to-pypi.yml (the filename — this is load-bearing) |
| Environment name | pypi |
- Create the GitHub environment
pypi(repo Settings → Environments) with the maintainer as a required reviewer. This is what makes Trusted Publishing meaningfully stronger than a repo-scoped API token — per PyPI's own security model documentation, anyone with commit access can otherwise modify a publishing workflow, so the human approval gate on the environment is the actual control, not the OIDC exchange by itself. - Protect
main: require CI (ci.yml) to pass, disallow force-pushes. - Enable GitHub Pages, source: GitHub Actions (for
docs.yml).
A PyPI pending publisher reserves nothing — the project name isn't claimed
until the first real publish succeeds. The working title autoclaude was
rejected as too similar to existing packages, which is why this project
ships as claudeloop; claim it promptly with a real 0.1.0 rather than
deferring indefinitely.
Doing a release dry run¶
Before the first real PyPI publish, validate the whole OIDC + build pipeline against TestPyPI:
- Repeat the pending-publisher setup above at
test.pypi.org(a fully separate account and publisher registry frompypi.org). - Trigger the TestPyPI job manually (
workflow_dispatch), or push a pre-release tag if the workflow is wired to react to one. pip install -i https://test.pypi.org/simple/ claudeloopin a scratch virtual environment and confirm the CLI entry point resolves.- Only once that round-trips cleanly, proceed with a real release-please
PR merge against
main.
What CI checks before any of this runs¶
Every gate in development.md
runs in ci.yml on every push and PR to main/develop, across Python
3.10–3.13. publish-to-pypi.yml does not re-run the test suite — it trusts
that nothing reaches main (protected, CI-gated) without already having
passed it, and its own build job runs twine check --strict on the built
artifacts as its only quality gate, keeping the publish job itself minimal
(it holds the OIDC token; the less it does, the smaller that surface is).
Verifying a completed publish¶
- Attestations:
pypa/gh-action-pypi-publishgenerates signed attestations automatically for Trusted Publishing flows (PEP 740) — visible on the PyPI project page under each release's files. py.typedshipped in the wheel:unzip -l dist/*.whl | grep py.typed.- Metadata:
pypi.org/project/claudeloop/should show the classifiers, keywords, and[project.urls]links configured inpyproject.toml.