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
49 changes: 49 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Codecov Configuration
# Documentation: https://docs.codecov.com/docs/codecovyml-reference

coverage:
precision: 2 # Number of decimal places (0-5)
round: down # How to round coverage (down/up/nearest)
range: 70..100 # Color coding range (red at 70%, green at 100%)

status:
# Project coverage: overall repository coverage
project:
default:
target: 90% # Minimum coverage threshold
threshold: null # No threshold - only fail if below target
base: auto # Compare against base branch
informational: false # Fail the check if below target

# Patch coverage: coverage on changed lines only
patch:
default:
target: 80% # New code should have at least 80% coverage
threshold: 0% # No wiggle room for patch coverage
base: auto
informational: false # Fail if new code doesn't meet target

# Pull request comment configuration
comment:
layout: "diff, flags, files, footer" # What to show in PR comments
behavior: default # Comment on all PRs
require_changes: false # Comment even if coverage unchanged
require_base: false # Comment even without base report
require_head: true # Only comment if head report exists

# Paths to ignore in coverage reports
ignore:
- "tests/*" # Test files
- "tests/**/*" # All test subdirectories
- "docs/*" # Documentation
- "site/*" # Built documentation site
- "htmlcov/*" # Coverage HTML reports
- ".venv/*" # Virtual environment
- ".tox/*" # Tox environments
- "**/__pycache__/*" # Python cache
- "**/conftest.py" # Pytest configuration
- "update_tests.py" # Utility scripts

# GitHub Checks configuration
github_checks:
annotations: true # Show coverage annotations on changed files
28 changes: 28 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: 'Setup Environment'
description: 'Setup Python with uv and install dependencies'
inputs:
python-version:
description: 'Python version'
required: false
default: '3.12'
install-deps:
description: 'Install dependencies'
required: false
default: 'true'
install-groups:
description: 'Dependency groups to install'
required: false
default: '--all-extras --all-groups'
runs:
using: 'composite'
steps:
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ inputs.python-version }}
enable-cache: true

- name: Install dependencies
if: inputs.install-deps == 'true'
run: uv sync ${{ inputs.install-groups }}
shell: bash
52 changes: 52 additions & 0 deletions .github/scripts/test_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Generate GitHub Actions test summary from JUnit XML."""

import sys
import xml.etree.ElementTree as ET
from pathlib import Path


def main() -> int:
"""Parse JUnit XML and write summary to GitHub Actions step summary."""
xml_path = Path("test-results.xml")

if not xml_path.exists():
print("⚠️ No test results found", file=sys.stderr)
return 1

try:
tree = ET.parse(xml_path)
root = tree.getroot()

tests = int(root.get("tests", 0))
failures = int(root.get("failures", 0))
errors = int(root.get("errors", 0))
skipped = int(root.get("skipped", 0))
passed = tests - failures - errors - skipped

# Determine status emoji
if failures + errors > 0:
status = "❌"
elif skipped == tests:
status = "⏭️"
else:
status = "✅"

# Print summary lines
print(f"{status} **Test Results Summary**")
print(f"- ✅ Passed: {passed}")
print(f"- ❌ Failed: {failures}")
print(f"- ⚠️ Errors: {errors}")
print(f"- ⏭️ Skipped: {skipped}")
print(f"- **Total: {tests}**")

# Exit with error if tests failed
return 1 if (failures + errors > 0) else 0

except ET.ParseError as e:
print(f"❌ Failed to parse XML: {e}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
52 changes: 32 additions & 20 deletions .github/workflows/check_pull_request_title.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
name: "Check PR title"

on:
pull_request:
types: [edited, opened, synchronize, reopened]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read
statuses: write

jobs:
pr-title-check:
runs-on: ubuntu-latest
timeout-minutes: 5
if: ${{ github.event.pull_request.user.login != 'allcontributors[bot]' }}
steps:
# Echo the user's login
- name: Echo user login
run: echo ${{ github.event.pull_request.user.login }}

- uses: naveenk1223/action-pr-title@master
- uses: amannn/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Require imperative mood (e.g. "Add feature" not "Adds feature")
# ^ Start of string
# [A-Z] First character must be an uppercase ASCII letter
# [a-zA-Z]* Followed by zero or more ASCII letters
# (?<![^s]s) Negative lookbehind: disallow a single 's' at the end of the first word
# ( .+)+ At least one space and one or more characters (requires more words)
# [a-z]* Followed by zero or more lowercase letters
# (?<![^s]s) Negative lookbehind: disallow single 's' at end of first word
# ( .+)+ At least one space and one or more characters
# [^.] Final character must not be a period
# $ End of string
regex: "^[A-Z][a-zA-Z]*(?<![^s]s)( .+)+[^.]$"
# Valid titles:
# - "Do something"
# - "Address something"
# Invalid title:
# - "do something"
# - "Do something."
# - "Does something"
# - "Do"
# - "Addresses something"
min_length: 10
max_length: 72
subjectPattern: '^[A-Z][a-z]*(?<![^s]s)( .+)+[^.]$'
subjectPatternError: |
The PR title must use imperative mood and proper formatting:
- Start with uppercase letter (e.g., "Add" not "add")
- Use base form of verb (e.g., "Add" not "Adds" or "Adding")
- Have multiple words (minimum 10 characters)
- Not end with a period

Valid: "Add new feature", "Fix bug in parser"
Invalid: "add feature", "Adds feature", "Add.", "Fix"
# Disable type prefixes (we don't use conventional commits format)
requireScope: false
ignoreLabels:
- ignore-title-check
73 changes: 73 additions & 0 deletions .github/workflows/ci-full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: CI Full

on:
push:
branches:
- main
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
checks: write

env:
FORCE_COLOR: 1

jobs:
tests:
name: Python ${{ matrix.python }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ./.github/actions/setup
with:
python-version: ${{ matrix.python }}

- name: Run all tests with coverage
run: |
uv run coverage run -m pytest tests --durations=10 \
--junit-xml=test-results.xml \
--html=test-report.html --self-contained-html

- name: Generate test summary
if: always()
run: |
echo "## Test Results - Python ${{ matrix.python }}" >> $GITHUB_STEP_SUMMARY
python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-py${{ matrix.python }}
retention-days: 7
path: |
test-results.xml
test-report.html
.coverage

- name: Generate coverage report
if: matrix.python == '3.12'
run: uv run coverage xml

- name: Upload coverage to Codecov
if: matrix.python == '3.12'
uses: codecov/[email protected]
with:
files: ./coverage.xml
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
checks: write

env:
FORCE_COLOR: 1

jobs:
quality:
name: ${{ matrix.check }}
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
check:
- format
- lint
- types

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ./.github/actions/setup
with:
python-version: "3.12"
install-deps: ${{ matrix.check == 'types' && 'true' || 'false' }}

- name: Run format check
if: matrix.check == 'format'
run: uvx ruff format --diff

- name: Run lint check
if: matrix.check == 'lint'
run: uvx ruff check

- name: Run type check
if: matrix.check == 'types'
run: uv run mypy src

tests:
name: Tests (Python 3.12)
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: ./.github/actions/setup
with:
python-version: "3.12"

- name: Run pytest with coverage
run: |
uv run coverage run -m pytest tests --durations=10 -m "not slow" \
--junit-xml=test-results.xml \
--html=test-report.html --self-contained-html

- name: Generate test summary
if: always()
run: python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-py312
retention-days: 7
path: |
test-results.xml
test-report.html
.coverage

- name: Generate coverage report
run: uv run coverage xml

- name: Upload coverage to Codecov
uses: codecov/[email protected]
with:
files: ./coverage.xml
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Loading