Opinionated blueprint for Python packages with automated CI, releases, and publishing.
| File | Purpose |
|---|---|
pyproject.toml |
hatchling + hatch-vcs, version from git tags |
.github/workflows/ci.yaml |
Lint (ruff), type check (mypy), test (pytest), docs build |
.github/workflows/release.yaml |
release-please: version bumps + changelog |
.github/workflows/publish.yaml |
PyPI publish on tag push or release |
.github/workflows/pr-title.yaml |
Conventional commit PR title validation |
.github/workflows/dependabot-auto-merge.yaml |
Auto-merge minor/patch updates |
.readthedocs.yaml |
Read the Docs with uv |
merge to main → release-please opens version bump PR
(auto-generated CHANGELOG.md + version bump)
Review the PR. To customize the changelog before releasing, push edits to the release-please branch:
git fetch origin
git checkout release-please--branches--main
# edit CHANGELOG.md
git add CHANGELOG.md
git commit -m "docs: refine changelog for vX.Y.Z"
git pushThen merge the PR:
merge release PR → git tag + GitHub Release → PyPI publish → RTD rebuild
Note: release-please regenerates the PR when new commits land on
main. Edit the changelog only when you're ready to merge, and don't push tomainin between.
git tag v1.0.0-rc.1
git push origin v1.0.0-rc.1
# → PyPI publish + GitHub Release (marked as pre-release)| Placeholder | Replace with | Example |
|---|---|---|
my-package |
PyPI package name | fluxopt |
my_package |
Python import name | fluxopt |
Needed so release-please PRs trigger CI (the default GITHUB_TOKEN can't).
- Create a GitHub App with Contents and Pull requests read & write
- Install it on your repo
- Add secrets
APP_IDandAPP_PRIVATE_KEY
- Create a
pypienvironment in repo settings (Settings → Environments) - Add a trusted publisher — workflow:
publish.yaml, environment:pypi
Connect your repo at readthedocs.org. Rebuilds automatically on push.
Add CODECOV_TOKEN as a repository secret.
Add CI Success as a required status check on main.
Warning: GitHub matches required checks by exact job name string. If you rename a job in a workflow, update branch protection to match — otherwise the old check silently never completes.