Skip to main content
General

scaffold-python

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.

scaffold-python

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 prepareuv buildsr 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 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
  • stage_files: [uv.lock] ensures lockfile stays in sync after version bumps