feat: Add Python CI workflow with UV package manager

- Add setup-python-uv composite action for fast cached Python setup
- Add python-ci.yml reusable workflow with:
  - Ruff linting and formatting
  - Pyright type checking
  - Matrix pytest with coverage enforcement
  - Bandit security scanning (SARIF upload)
- Update README with comprehensive documentation
- Based on 2025 best practices using astral-sh/setup-uv@v5
This commit is contained in:
2026-02-01 18:01:49 +00:00
parent 68211418a5
commit 93dad2156e
4 changed files with 388 additions and 26 deletions

View File

@@ -0,0 +1,71 @@
# Composite action: Setup Python with UV package manager
# Fast, cached Python environment setup using Astral's UV
name: Setup Python with UV
description: Install UV, cache dependencies, and sync project
inputs:
python-version:
description: 'Python version to install'
required: false
default: '3.12'
working-directory:
description: 'Directory containing pyproject.toml'
required: false
default: '.'
extras:
description: 'Extra dependency groups to install (comma-separated)'
required: false
default: ''
dev:
description: 'Install dev dependencies'
required: false
default: 'true'
outputs:
python-path:
description: 'Path to Python executable'
value: ${{ steps.setup.outputs.python-path }}
cache-hit:
description: 'Whether cache was hit'
value: ${{ steps.setup-uv.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Install UV
id: setup-uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: |
${{ inputs.working-directory }}/uv.lock
${{ inputs.working-directory }}/pyproject.toml
- name: Set up Python
id: setup
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
uv python install ${{ inputs.python-version }}
echo "python-path=$(uv python find)" >> "$GITHUB_OUTPUT"
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
SYNC_ARGS=""
# Add dev dependencies if requested
if [ "${{ inputs.dev }}" = "true" ]; then
SYNC_ARGS="$SYNC_ARGS --dev"
fi
# Add extras if specified
if [ -n "${{ inputs.extras }}" ]; then
IFS=',' read -ra EXTRAS <<< "${{ inputs.extras }}"
for extra in "${EXTRAS[@]}"; do
SYNC_ARGS="$SYNC_ARGS --extra $(echo $extra | xargs)"
done
fi
uv sync $SYNC_ARGS

159
.github/workflows/python-ci.yml vendored Normal file
View File

@@ -0,0 +1,159 @@
# Reusable workflow: Python CI with UV
# Provides linting, type-checking, testing, and security scanning
name: Python CI
on:
workflow_call:
inputs:
python-versions:
description: 'JSON array of Python versions to test'
type: string
default: '["3.12"]'
working-directory:
description: 'Directory containing the Python project'
type: string
default: '.'
run-lint:
description: 'Run Ruff linter'
type: boolean
default: true
run-typecheck:
description: 'Run type checking with Pyright'
type: boolean
default: true
run-tests:
description: 'Run pytest'
type: boolean
default: true
run-security:
description: 'Run Bandit security scanner'
type: boolean
default: true
test-command:
description: 'Custom test command (default: pytest)'
type: string
default: 'pytest --cov --cov-report=xml'
coverage-threshold:
description: 'Minimum coverage percentage (0 to disable)'
type: number
default: 0
extras:
description: 'Extra dependency groups to install'
type: string
default: ''
jobs:
lint:
if: ${{ inputs.run-lint }}
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python with UV
uses: ./.github/actions/setup-python-uv
with:
python-version: '3.12'
working-directory: ${{ inputs.working-directory }}
extras: ${{ inputs.extras }}
- name: Run Ruff linter
working-directory: ${{ inputs.working-directory }}
run: |
uv run ruff check . --output-format=github
- name: Run Ruff formatter check
working-directory: ${{ inputs.working-directory }}
run: |
uv run ruff format . --check --diff
typecheck:
if: ${{ inputs.run-typecheck }}
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python with UV
uses: ./.github/actions/setup-python-uv
with:
python-version: '3.12'
working-directory: ${{ inputs.working-directory }}
extras: ${{ inputs.extras }}
- name: Run Pyright
working-directory: ${{ inputs.working-directory }}
run: |
uv run pyright
test:
if: ${{ inputs.run-tests }}
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(inputs.python-versions) }}
steps:
- uses: actions/checkout@v4
- name: Setup Python with UV
uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ matrix.python-version }}
working-directory: ${{ inputs.working-directory }}
extras: ${{ inputs.extras }}
- name: Run tests
working-directory: ${{ inputs.working-directory }}
run: |
uv run ${{ inputs.test-command }}
- name: Check coverage threshold
if: ${{ inputs.coverage-threshold > 0 }}
working-directory: ${{ inputs.working-directory }}
run: |
COVERAGE=$(uv run coverage report --format=total 2>/dev/null || echo "0")
if [ "$COVERAGE" -lt "${{ inputs.coverage-threshold }}" ]; then
echo "::error::Coverage ${COVERAGE}% is below threshold ${{ inputs.coverage-threshold }}%"
exit 1
fi
echo "Coverage: ${COVERAGE}% (threshold: ${{ inputs.coverage-threshold }}%)"
- name: Upload coverage
if: ${{ matrix.python-version == '3.12' }}
uses: codecov/codecov-action@v4
with:
file: ${{ inputs.working-directory }}/coverage.xml
fail_ci_if_error: false
continue-on-error: true
security:
if: ${{ inputs.run-security }}
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python with UV
uses: ./.github/actions/setup-python-uv
with:
python-version: '3.12'
working-directory: ${{ inputs.working-directory }}
dev: 'true'
- name: Run Bandit security scanner
working-directory: ${{ inputs.working-directory }}
run: |
uv run bandit -r . -x ./tests -f sarif -o bandit-results.sarif || true
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ inputs.working-directory }}/bandit-results.sarif
continue-on-error: true
- name: Check for high-severity issues
working-directory: ${{ inputs.working-directory }}
run: |
uv run bandit -r . -x ./tests -ll -f txt