Scaffold Python Project
Generate a production-ready Python project following established CI/CD patterns. Read the scaffold-project skill first for standard files (README, AGENTS.md, LICENSE, CONTRIBUTING.md, llms.txt).
When to Use
- Creating a new Python CLI, library, or application
- Adding CI/CD to an existing Python project missing workflows
- Standardizing a Python project to match org conventions
Generated Files
.github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
workflow_call:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv python install
- run: uv sync --group dev
- name: Check formatting
run: uv run ruff format --check .
- name: Run linter
run: uv run ruff check .
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv python install
- run: uv sync --group dev
- name: Run tests
run: uv run pytest
.github/workflows/release.yml
name: Release
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write
jobs:
ci:
if: github.actor != 'sr[bot]'
uses: ./.github/workflows/ci.yml
release:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SR_RELEASER_APP_ID }}
private-key: ${{ secrets.SR_RELEASER_PRIVATE_KEY }}
repositories: ${{ github.event.repository.name }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- uses: urmzd/sr@v8
id: sr
with:
github-token: ${{ steps.app-token.outputs.token }}
outputs:
released: ${{ steps.sr.outputs.released }}
tag: ${{ steps.sr.outputs.tag }}
version: ${{ steps.sr.outputs.version }}
For PyPI publishing, use the pypi typed publisher in sr.yaml (below) and split into sr prepare → uv build → sr release jobs so wheels embed the bumped version. See sync-release for the multi-job pattern.
sr.yaml
Generate with sr init:
git:
tag_prefix: "v"
floating_tag: true
v0_protection: true
commit:
types:
minor: [feat]
patch: [fix, perf, refactor]
none: [docs, revert, chore, ci, test, build, style]
changelog:
file: CHANGELOG.md
groups:
- { name: breaking, content: [breaking] }
- { name: features, content: [feat] }
- { name: bug-fixes, content: [fix] }
- { name: performance, content: [perf] }
- { name: misc, content: [chore, ci, test, build, style] }
channels:
default: stable
branch: main
content:
- name: stable
packages:
- path: .
version_files: [pyproject.toml]
stage_files: [uv.lock]
# Uncomment to publish to PyPI (requires wheels to be built in CI between
# `sr prepare` and `sr release` so they embed the bumped version):
# publish:
# type: pypi
# workspace: true # iterate [tool.uv.workspace].members
See sync-release for the full schema and sr migrate for upgrading from older versions.
pyproject.toml
[project]
name = "<project-name>"
version = "0.1.0"
description = ""
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
# <project-name> = "<package>.cli:main"
[dependency-groups]
dev = ["pytest", "ruff", "ty"]
[tool.ruff]
line-length = 100
select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
justfile
default: check
init:
git config core.hooksPath .githooks
uv sync --group dev
build:
uv build
test:
uv run pytest
lint:
uv run ruff check .
fmt:
uv run ruff format .
typecheck:
uv run ty check src/
check: fmt lint test
run *args="":
uv run python -m <package_name> {{args}}
record:
teasr showme
Replace <package_name> with the actual package name.
.envrc
layout python # Auto-creates and activates .venv
.python-version
3.12
Project Layout
src/<package_name>/
__init__.py
cli.py # if CLI
py.typed # if library with type stubs
tests/
__init__.py
test_*.py
pyproject.toml
uv.lock
Gotchas
- Use
uvexclusively. No pip, pipenv, poetry, or conda uv sync --group devinstalls dev dependencies;uv syncfor production onlyuv runprefixes all commands to ensure they run in the project venvruffreplaces black, isort, flake8, and pyflakes. One tool for format + lint- Python version comes from
pyproject.tomlrequires-pythonfield;uv python installresolves it astral-sh/setup-uv@v5handles caching automatically- For PyPI publishing,
uv publishuses trusted publishers (OIDC). Configure on pypi.org first stage_files: [uv.lock]ensures lockfile stays in sync after version bumps