Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 74 additions & 57 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,98 +6,115 @@ on:
pull_request:
branches: [main]

env:
PYTHON_VERSION: "3.13"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
version: "latest"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Set up Python
run: uv python install 3.13

- name: Download NLTK data
run: |
python -c "import nltk; nltk.download('averaged_perceptron_tagger_eng'); nltk.download('punkt_tab')"
- name: Install dependencies
run: uv sync --all-extras

- name: Run tests with coverage
run: |
pytest --cov=src/drover --cov-report=xml --cov-report=term-missing
- name: Run ruff check
run: uv run ruff check src/ tests/

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
retention-days: 7
- name: Run ruff format check
run: uv run ruff format --check src/ tests/

lint:
name: Lint
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
version: "latest"

- name: Install ruff
run: pip install ruff
- name: Set up Python
run: uv python install 3.13

- name: Check formatting
run: ruff format --check src/ tests/
- name: Install dependencies
run: uv sync --all-extras

- name: Check linting
run: ruff check src/ tests/
- name: Run mypy
run: uv run mypy src/

type-check:
name: Type Check
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
version: "latest"

- name: Set up Python
run: uv python install 3.13

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
run: uv sync --all-extras

- name: Run mypy
run: mypy src/
- name: Download NLTK data
run: uv run python -c "import nltk; nltk.download('averaged_perceptron_tagger_eng'); nltk.download('punkt_tab')"

security:
name: Security Scan
- name: Run tests with coverage
run: uv run pytest tests/ --cov=src --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

version-check:
name: Version Consistency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
version: "latest"

- name: Install bandit
run: pip install bandit
- name: Set up Python
run: uv python install 3.13

- name: Check version consistency
run: uv run python scripts/check_version.py

- name: Run security scan
all-checks:
name: All Checks Passed
runs-on: ubuntu-latest
needs: [lint, typecheck, test, version-check]
if: always()
steps:
- name: Check all jobs passed
run: |
bandit -r src/ -c pyproject.toml --severity-level medium --confidence-level medium
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.typecheck.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.version-check.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
echo "All checks passed!"
98 changes: 98 additions & 0 deletions scripts/check_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Validate version consistency between __init__.py and pyproject.toml.

This script ensures the version in src/drover/__init__.py matches
the version in pyproject.toml to prevent version drift.

Exit codes:
0: Versions match
1: Version mismatch or error
"""

from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path


def get_init_version(init_file: Path) -> str | None:
"""Extract version from __init__.py file.

Args:
init_file: Path to __init__.py file.

Returns:
Version string if found, None otherwise.
"""
content = init_file.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
return match.group(1) if match else None


def get_pyproject_version(pyproject_file: Path) -> str | None:
"""Extract version from pyproject.toml file.

Args:
pyproject_file: Path to pyproject.toml file.

Returns:
Version string if found, None otherwise.
"""
try:
content = pyproject_file.read_text(encoding="utf-8")
data = tomllib.loads(content)
return data.get("project", {}).get("version")
except (tomllib.TOMLDecodeError, KeyError):
return None


def main() -> int:
"""Main validation logic.

Returns:
Exit code (0 for success, 1 for failure).
"""
# Determine project root (script is in scripts/ directory)
project_root = Path(__file__).parent.parent
init_file = project_root / "src" / "drover" / "__init__.py"
pyproject_file = project_root / "pyproject.toml"

# Check files exist
if not init_file.exists():
print(f"❌ Error: {init_file} not found", file=sys.stderr)
return 1

if not pyproject_file.exists():
print(f"❌ Error: {pyproject_file} not found", file=sys.stderr)
return 1

# Extract versions
init_version = get_init_version(init_file)
if init_version is None:
print(f"❌ Error: Could not find __version__ in {init_file}", file=sys.stderr)
return 1

pyproject_version = get_pyproject_version(pyproject_file)
if pyproject_version is None:
print(f"❌ Error: Could not find version in {pyproject_file}", file=sys.stderr)
return 1

# Compare versions
if init_version != pyproject_version:
print("❌ Version mismatch detected!", file=sys.stderr)
print(f" src/drover/__init__.py: {init_version}", file=sys.stderr)
print(f" pyproject.toml: {pyproject_version}", file=sys.stderr)
print(file=sys.stderr)
print("Please update both files to match.", file=sys.stderr)
print("See RELEASING.md for version management guidelines.", file=sys.stderr)
return 1

# Success
print(f"✅ Version consistency check passed: {init_version}")
return 0


if __name__ == "__main__":
sys.exit(main())