diff --git a/.github/actions/setup-python-uv/action.yml b/.github/actions/setup-python-uv/action.yml new file mode 100644 index 0000000..f3c0aa6 --- /dev/null +++ b/.github/actions/setup-python-uv/action.yml @@ -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 diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..4eb2495 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -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 diff --git a/README.md b/README.md index cbf2dbb..90c5df7 100644 --- a/README.md +++ b/README.md @@ -7,43 +7,104 @@ Reusable GitHub Actions workflows and composite actions for CI/CD pipelines. ## Workflows -``` -.github/workflows/ -├── docker-build.yml # Build, scan, and push Docker images -├── terraform-plan.yml # Terraform plan with cost estimation -├── k8s-deploy.yml # Kubernetes deployment with ArgoCD -├── security-scan.yml # SAST, DAST, dependency scanning -└── release.yml # Semantic release automation -``` +| Workflow | Description | +|----------|-------------| +| [`python-ci.yml`](.github/workflows/python-ci.yml) | Python CI with UV (lint, type-check, test, security) | ## Composite Actions -``` -actions/ -├── docker-build/ # Multi-arch Docker build -├── terraform-plan/ # Terraform plan with PR comments -├── k8s-deploy/ # Kubernetes deployment -└── security-scan/ # Trivy, Grype, CodeQL -``` +| Action | Description | +|--------|-------------| +| [`setup-python-uv`](actions/setup-python-uv) | Fast Python setup with UV package manager | -## Usage +## Quick Start + +### Python CI ```yaml +# .github/workflows/ci.yml +name: CI +on: [push, pull_request] + jobs: - build: - uses: ghndrx/github-actions-library/.github/workflows/docker-build.yml@main + ci: + uses: ghndrx/github-actions-library/.github/workflows/python-ci.yml@main with: - image-name: myapp - secrets: inherit + python-versions: '["3.11", "3.12", "3.13"]' + run-typecheck: true + coverage-threshold: 80 ``` -## Features +### Setup Python with UV (Composite Action) -- ✅ Reusable workflows (DRY) -- ✅ Matrix builds -- ✅ Security scanning built-in -- ✅ Caching optimization -- ✅ OIDC authentication (no long-lived secrets) +```yaml +steps: + - uses: actions/checkout@v4 + + - uses: ghndrx/github-actions-library/actions/setup-python-uv@main + with: + python-version: '3.12' + extras: 'dev,test' + + - run: uv run pytest +``` + +## Python CI Workflow Features + +The `python-ci.yml` reusable workflow provides: + +- **Ruff linting** - Fast Python linter with auto-fix suggestions +- **Pyright type checking** - Strict type validation +- **Matrix testing** - Test across multiple Python versions +- **Coverage enforcement** - Fail if coverage drops below threshold +- **Bandit security scanning** - Detect security vulnerabilities +- **UV caching** - 10-100x faster than pip installs + +### Inputs + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `python-versions` | string | `'["3.12"]'` | JSON array of Python versions | +| `working-directory` | string | `.` | Project directory | +| `run-lint` | boolean | `true` | Run Ruff linter | +| `run-typecheck` | boolean | `true` | Run Pyright | +| `run-tests` | boolean | `true` | Run pytest | +| `run-security` | boolean | `true` | Run Bandit scanner | +| `test-command` | string | `pytest --cov --cov-report=xml` | Custom test command | +| `coverage-threshold` | number | `0` | Min coverage % (0 to disable) | +| `extras` | string | `''` | Extra dependency groups | + +## Requirements + +Projects using the Python CI workflow should have: + +- `pyproject.toml` with UV-compatible configuration +- Dev dependencies: `ruff`, `pyright`, `pytest`, `pytest-cov`, `bandit` + +Example `pyproject.toml`: + +```toml +[project] +name = "myproject" +requires-python = ">=3.11" + +[tool.uv] +dev-dependencies = [ + "ruff>=0.8", + "pyright>=1.1", + "pytest>=8.0", + "pytest-cov>=6.0", + "bandit>=1.8", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.pyright] +pythonVersion = "3.12" +typeCheckingMode = "standard" +``` ## License diff --git a/actions/setup-python-uv/action.yml b/actions/setup-python-uv/action.yml new file mode 100644 index 0000000..f3c0aa6 --- /dev/null +++ b/actions/setup-python-uv/action.yml @@ -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