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:
inputs:
force:
description: "Re-release the current tag (use when a previous release partially failed)"
type: boolean
default: false
concurrency:
group: release
cancel-in-progress: false
permissions:
contents: write
jobs:
ci:
if: github.actor != 'sr-releaser[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@v2
id: sr
with:
github-token: ${{ steps.app-token.outputs.token }}
force: ${{ inputs.force }}
outputs:
released: ${{ steps.sr.outputs.released }}
tag: ${{ steps.sr.outputs.tag }}
version: ${{ steps.sr.outputs.version }}
# Uncomment for PyPI publishing:
# publish:
# needs: release
# if: needs.release.outputs.released == 'true'
# runs-on: ubuntu-latest
# permissions:
# id-token: write
# steps:
# - uses: actions/checkout@v4
# with:
# ref: ${{ needs.release.outputs.tag }}
# - uses: astral-sh/setup-uv@v5
# - run: uv python install
# - run: uv build
# - run: uv publish
sr.yaml
branches:
- main
tag_prefix: "v"
commit_pattern: '^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<description>.+)'
breaking_section: Breaking Changes
misc_section: Miscellaneous
types:
- name: feat
bump: minor
section: Features
- name: fix
bump: patch
section: Bug Fixes
- name: perf
bump: patch
section: Performance
- name: docs
section: Documentation
- name: refactor
section: Refactoring
- name: revert
section: Reverts
- name: chore
- name: ci
- name: test
- name: build
- name: style
version_files:
- pyproject.toml
changelog:
file: CHANGELOG.md
stage_files:
- uv.lock
floating_tags: true
hooks:
commit-msg:
- sr hook commit-msg
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}}
Replace <package_name> with the actual package name.
.envrc
use flake .#python
.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