Files
awesome-copilot/skills/python-pypi-package-builder/references/ci-publishing.md
Patel Dhruv 49fd3f3faf Add new skill: Python PyPI Package Builder (#1302)
* Add python-pypi-package-builder skill for Python packaging

- Created `SKILL.md` defining decision-driven workflow for building, testing, versioning, and publishing Python packages.
- Added reference modules covering PyPA packaging, architecture patterns, CI/CD, testing, versioning strategy, and release governance.
- Implemented scaffold script to generate complete project structure with pyproject.toml, CI workflows, tests, and configuration.
- Included support for multiple build backends (setuptools_scm, hatchling, flit, poetry) with clear decision rules.
- Added secure release practices including tag-based versioning, branch protection, and OIDC Trusted Publishing.

* fix: correct spelling issues detected by codespell
2026-04-09 10:36:17 +10:00

316 lines
7.9 KiB
Markdown

# CI/CD, Publishing, and Changelog
## Table of Contents
1. [Changelog format](#1-changelog-format)
2. [ci.yml — lint, type-check, test matrix](#2-ciyml)
3. [publish.yml — triggered on version tags](#3-publishyml)
4. [PyPI Trusted Publishing (no API tokens)](#4-pypi-trusted-publishing)
5. [Manual publish fallback](#5-manual-publish-fallback)
6. [Release checklist](#6-release-checklist)
7. [Verify py.typed ships in the wheel](#7-verify-pytyped-ships-in-the-wheel)
8. [Semver change-type guide](#8-semver-change-type-guide)
---
## 1. Changelog Format
Keep a `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/) conventions.
Every PR should update the `[Unreleased]` section. Before releasing, move those entries to a
new version section with the date.
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [Unreleased]
### Added
- (in-progress features go here)
---
## [1.0.0] - 2026-04-02
### Added
- Initial stable release
- `YourMiddleware` with gradual, strict, and combined modes
- In-memory backend (no extra deps)
- Optional Redis backend (`pip install pkg[redis]`)
- Per-route override via `Depends(RouteThrottle(...))`
- `py.typed` marker — PEP 561 typed package
- GitHub Actions CI: lint, mypy, test matrix, Trusted Publishing
### Changed
### Fixed
### Removed
---
## [0.1.0] - 2026-03-01
### Added
- Initial project scaffold
[Unreleased]: https://github.com/you/your-package/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/you/your-package/compare/v0.1.0...v1.0.0
[0.1.0]: https://github.com/you/your-package/releases/tag/v0.1.0
```
### Semver — what bumps what
| Change type | Bump | Example |
|---|---|---|
| Breaking API change | MAJOR | `1.0.0 → 2.0.0` |
| New feature, backward-compatible | MINOR | `1.0.0 → 1.1.0` |
| Bug fix | PATCH | `1.0.0 → 1.0.1` |
---
## 2. `ci.yml`
Runs on every push and pull request. Tests across all supported Python versions.
```yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint:
name: Lint, Format & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dev dependencies
run: pip install -e ".[dev]"
- name: ruff lint
run: ruff check .
- name: ruff format check
run: ruff format --check .
- name: mypy
run: |
if [ -d "src" ]; then
mypy src/
else
mypy {mod}/
fi
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # REQUIRED for setuptools_scm to read git tags
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Run tests with coverage
run: pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
test-redis:
name: Test Redis backend
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install with Redis extra
run: pip install -e ".[dev,redis]"
- name: Run Redis tests
run: pytest tests/test_redis_backend.py -v
```
> **Always add `fetch-depth: 0`** to every checkout step when using `setuptools_scm`.
> Without full git history, `setuptools_scm` can't find tags and the build fails with a version
> detection error.
---
## 3. `publish.yml`
Triggered automatically when you push a tag matching `v*.*.*`. Uses Trusted Publishing (OIDC) —
no API tokens in repository secrets.
```yaml
# .github/workflows/publish.yml
name: Publish to PyPI
on:
push:
tags:
- "v*.*.*"
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Critical for setuptools_scm
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build tools
run: pip install build twine
- name: Build package
run: python -m build
- name: Check distribution
run: twine check dist/*
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # Required for Trusted Publishing (OIDC)
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
```
---
## 4. PyPI Trusted Publishing
Trusted Publishing uses OpenID Connect (OIDC) so PyPI can verify that a publish came from your
specific GitHub Actions workflow — no long-lived API tokens required, no rotation burden.
### One-time setup
1. Create an account at https://pypi.org
2. Go to **Account → Publishing → Add a new pending publisher**
3. Fill in:
- GitHub owner (your username or org)
- Repository name
- Workflow filename: `publish.yml`
- Environment name: `pypi`
4. Create the `pypi` environment in GitHub:
**repo → Settings → Environments → New environment → name it `pypi`**
That's it. The next time you push a `v*.*.*` tag, the workflow authenticates automatically.
---
## 5. Manual Publish Fallback
If CI isn't set up yet or you need to publish from your machine:
```bash
pip install build twine
# Build wheel + sdist
python -m build
# Validate before uploading
twine check dist/*
# Upload to PyPI
twine upload dist/*
# OR test on TestPyPI first (recommended for first release)
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ your-package
python -c "import your_package; print(your_package.__version__)"
```
---
## 6. Release Checklist
```
[ ] All tests pass on main/master
[ ] CHANGELOG.md updated — move [Unreleased] items to new version section with date
[ ] Update diff comparison links at bottom of CHANGELOG
[ ] git tag vX.Y.Z
[ ] git push origin master --tags
[ ] Monitor GitHub Actions publish.yml run
[ ] Verify on PyPI: pip install your-package==X.Y.Z
[ ] Test the installed version:
python -c "import your_package; print(your_package.__version__)"
```
---
## 7. Verify py.typed Ships in the Wheel
After every build, confirm the typed marker is included:
```bash
python -m build
unzip -l dist/your_package-*.whl | grep py.typed
# Must print: your_package/py.typed
# If missing, check [tool.setuptools.package-data] in pyproject.toml
```
If it's missing from the wheel, users won't get type information even though your code is
fully typed. This is a silent failure — always verify before releasing.
---
## 8. Semver Change-Type Guide
| Change | Version bump | Example |
|---|---|---|
| Breaking API change (remove/rename public symbol) | MAJOR | `1.2.3 → 2.0.0` |
| New feature, fully backward-compatible | MINOR | `1.2.3 → 1.3.0` |
| Bug fix, no API change | PATCH | `1.2.3 → 1.2.4` |
| Pre-release | suffix | `2.0.0a1 → 2.0.0rc1 → 2.0.0` |
| Packaging-only fix (no code change) | post-release | `1.2.3 → 1.2.3.post1` |