From python-package
This skill should be used when the user is setting up GitHub Actions, configuring CI/CD pipelines, creating test matrices, enabling trusted publishing with OIDC, automating PyPI releases, adding Dependabot or Renovate, or configuring Sigstore attestations. Covers workflow architecture, lint/type-check/test/build/publish jobs, caching, concurrency, reusable workflows, SLSA provenance.
npx claudepluginhub oborchers/fractional-cto --plugin python-packageThis skill uses the workspace's default tool permissions.
A Python package without CI is a Python package with bugs you discover after release. Every serious package -- FastAPI, Pydantic, httpx, Ruff, uv, Polars -- follows the same five-stage pipeline: lint, type-check, test, build, publish. These stages run as separate jobs, not steps, because separate jobs enable parallel execution, clear failure attribution, and conditional downstream gates. The pu...
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
A Python package without CI is a Python package with bugs you discover after release. Every serious package -- FastAPI, Pydantic, httpx, Ruff, uv, Polars -- follows the same five-stage pipeline: lint, type-check, test, build, publish. These stages run as separate jobs, not steps, because separate jobs enable parallel execution, clear failure attribution, and conditional downstream gates. The publish job uses OIDC trusted publishing with zero long-lived secrets -- API tokens are a supply chain risk that PyPI's trusted publishing eliminates entirely.
Structure every CI pipeline as a dependency graph of discrete jobs:
lint + type-check (parallel)
|
tests (matrix: Python versions x OS)
|
build (sdist + wheel, upload artifact)
|
publish (trusted publishing, environment protection)
| Principle | Rule |
|---|---|
| Separate jobs, not steps | Each stage is its own job for parallelism and clear failure |
needs for dependencies | Tests need lint + type-check; build needs tests; publish needs build |
fail-fast: false | See all failures across the matrix, not just the first |
| Build once, publish once | Pass artifacts via upload-artifact/download-artifact -- never rebuild in publish |
| Concurrency control | Cancel stale runs with concurrency + cancel-in-progress: true |
Run the full Python version matrix on Linux. Add macOS and Windows only if your package has platform-specific behavior (path handling, native extensions, OS-specific dependencies). When cross-platform testing is needed, test oldest + newest Python versions only to keep CI costs manageable.
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest]
include:
# Add these only if your package has platform-specific behavior
- { python-version: "3.10", os: macos-latest }
- { python-version: "3.13", os: macos-latest }
- { python-version: "3.10", os: windows-latest }
- { python-version: "3.13", os: windows-latest }
Upload coverage from a single matrix entry (latest Python, Linux) to avoid duplicate reports.
Publish to PyPI with zero secrets. The pypa/gh-action-pypi-publish action handles the OIDC token exchange automatically. No password, no user, no PYPI_API_TOKEN. See the security-supply-chain skill for the threat model and broader supply chain hardening context.
| Bad Pattern | Good Pattern |
|---|---|
password: ${{ secrets.PYPI_API_TOKEN }} | No password field -- OIDC handles auth |
| Build and publish in the same job | Separate jobs with artifact passing |
| Publishing from a local machine | CI-only publishing with attestations |
Setup on PyPI: Project Settings > Publishing > Add trusted publisher with owner, repo, workflow filename, and environment name. For new packages, use a "pending publisher" at pypi.org/manage/account/publishing/.
Required permissions on the publish job:
permissions:
id-token: write # OIDC token exchange
attestations: write # Sigstore attestations
contents: read
GitHub environment: Create a release environment in Repository Settings > Environments. Add branch protection rules limiting to main or v* tags.
Use astral-sh/setup-uv@v5 with enable-cache: true in every job. This single line caches uv's global package cache keyed on the lock file.
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
For legacy pip projects, use actions/setup-python@v5 with cache: "pip".
Trigger releases from GitHub Release events, not tag pushes. GitHub Releases provide a UI for release notes, drafts, and deliberate publishing.
on:
release:
types: [published]
Use hatch-vcs for version-from-tag: tag v1.2.3 produces version 1.2.3. Requires fetch-depth: 0 in the checkout step.
Configure auto-generated release notes in .github/release.yml:
changelog:
categories:
- title: "Breaking Changes"
labels: ["breaking"]
- title: "Features"
labels: ["enhancement", "feature"]
- title: "Bug Fixes"
labels: ["bug", "fix"]
- title: "Documentation"
labels: ["docs"]
- title: "Internal"
labels: ["internal", "ci", "dependencies"]
Configure .github/dependabot.yml with both uv (or pip for non-uv projects) and github-actions ecosystems. Use groups to combine minor/patch updates into single PRs instead of one-PR-per-dependency.
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
groups:
python-packages:
patterns: ["*"]
update-types: ["minor", "patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Note: Dependabot updates pyproject.toml but does not regenerate uv.lock. Add a CI step or post-update script to run uv lock after Dependabot PRs.
Use Renovate instead for monorepos, auto-merge rules, or when you need more granular control over update strategies.
| Anti-Pattern | Risk | Fix |
|---|---|---|
| Long-lived API tokens | Token theft, supply chain attack | Trusted publishing (OIDC) |
fail-fast: true (default) | Missing version-specific failures | fail-fast: false |
| Build + publish in same job | No artifact inspection, no separation | Separate jobs, artifact passing |
| Full matrix on all OS | Wasted CI minutes (macOS is 10x cost) | Full Linux matrix, oldest + newest elsewhere (only if needed) |
ruff check --fix in CI | Violations pass silently | ruff check without --fix |
| No concurrency control | Stale runs waste minutes | concurrency + cancel-in-progress |
Missing fetch-depth: 0 | Wrong version with VCS versioning | Full checkout for tag-based versions |
Pinning actions to main | Breaking changes, security risk | Pin to version tags (@v4, @v5) |
.github/workflows/ci.ymlname: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with: { enable-cache: true }
- run: uv run ruff check src tests
- run: uv run ruff format --check src tests
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with: { enable-cache: true }
- run: uv run mypy src
# For libraries, also run pyright (see code-quality skill)
# - run: uv run pyright src
test:
needs: [lint, type-check]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest]
include:
# Add these only if your package has platform-specific behavior
- { python-version: "3.10", os: macos-latest }
- { python-version: "3.13", os: macos-latest }
- { python-version: "3.10", os: windows-latest }
- { python-version: "3.13", os: windows-latest }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with: { enable-cache: true }
- run: uv run --python ${{ matrix.python-version }} pytest --cov --cov-report=xml
- if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
uses: codecov/codecov-action@v5
with: { token: "${{ secrets.CODECOV_TOKEN }}", files: coverage.xml }
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: astral-sh/setup-uv@v5
with: { enable-cache: true }
- run: uv build
- run: uvx twine check dist/*
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/, if-no-files-found: error }
.github/workflows/release.ymlname: Release
on:
release:
types: [published]
jobs:
ci:
uses: ./.github/workflows/ci.yml
publish:
needs: ci
runs-on: ubuntu-latest
environment: release
permissions: { id-token: write, attestations: write, contents: read }
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- uses: pypa/gh-action-pypi-publish@release/v1
with: { attestations: true }
When reviewing CI/CD configuration:
ruff check and ruff format --check without --fixmypy src (or pyright) in parallel with lintfail-fast: false is set on the test strategyconcurrency with cancel-in-progress: true prevents stale runsastral-sh/setup-uv with enable-cache: trueuv build + twine check + upload-artifact as separate step from publishpypa/gh-action-pypi-publish with OIDC -- no API tokens or secretspermissions: id-token: write is set on the publish jobattestations: truerelease environment configured with branch protection rulesfetch-depth: 0 set in checkout if using VCS-based versioningon: release: types: [published], not raw tag push