scaffold-python · git:20261009.9dd80f8 · 2026-10-09 · sha256 973375da1242d2d9
scaffold-python git:20261009.9dd80f8A
Immutable. This exact content never changes and is served at /api/v1/blob/973375da1242d2d9.
---
name: scaffold-python
description: >
Scaffold a complete Python project with CI/CD, release pipeline, justfile, sr.yaml,
pyproject.toml, .envrc, and standard files. Uses uv, ruff, and justfile (Python lacks
a native task runner like pnpm scripts, so just fills that gap). Loads on top of
scaffold-project (run that first for cross-language standard files). Use when creating a
new Python CLI, library, or application, or when the user mentions "new Python project",
"uv init", or "Python scaffold". Do NOT use for Go, Rust, Node, or Terraform
projects; use the matching scaffold-<lang> skill.
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(uv *), Bash(git *)
user-invocable: true
metadata:
title: Scaffold Python Project
category: development
order: 12
---
# 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`
```yaml
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`
```yaml
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@v9
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`:
```yaml
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]
# 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`
```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`
```just
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`
```sh
layout python # Auto-creates and activates .venv
```
### `.python-version`
```text
3.12
```
### Project Layout
```text
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 `uv` exclusively. No pip, pipenv, poetry, or conda
- `uv sync --group dev` installs dev dependencies; `uv sync` for production only
- `uv run` prefixes all commands to ensure they run in the project venv
- `ruff` replaces black, isort, flake8, and pyflakes. One tool for format + lint
- Python version comes from `pyproject.toml` `requires-python` field; `uv python install` resolves it
- `astral-sh/setup-uv@v5` handles caching automatically
- For PyPI publishing, `uv publish` uses trusted publishers (OIDC). Configure on pypi.org first
- sr v9 rewrites the project's own entries in `uv.lock` (sources `editable`, `virtual`, `directory`) during the bump and stages it in the release commit. Do not list `uv.lock` in `stage_files` or add a post-release `uv lock` commit. Keep `uv.lock` beside `pyproject.toml` (or in a parent directory) so sr discovers it