refactor: consolidate modernization and test overhaul #2043
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tests And Linting | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| # Cancel in-flight runs of this workflow on the same ref when a newer | |
| # commit lands. Saves runner minutes during a churn of PR pushes. Not | |
| # applied to publish.yml — releases must never be cancelled mid-upload. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| ci-scope: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| ci_required: ${{ steps.scope.outputs.ci_required }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect CI scope | |
| id: scope | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| HEAD_SHA: ${{ github.sha }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PUSH_BEFORE_SHA: ${{ github.event.before }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ci_required=true | |
| base_sha="" | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| base_sha="$PR_BASE_SHA" | |
| elif [ -n "${PUSH_BEFORE_SHA:-}" ] && [[ ! "$PUSH_BEFORE_SHA" =~ ^0+$ ]]; then | |
| base_sha="$PUSH_BEFORE_SHA" | |
| fi | |
| if [ -n "$base_sha" ] && git cat-file -e "$base_sha^{commit}" && git cat-file -e "$HEAD_SHA^{commit}"; then | |
| changed_files="$(git diff --name-only "$base_sha" "$HEAD_SHA")" | |
| if [ -n "$changed_files" ]; then | |
| ci_required=false | |
| while IFS= read -r path; do | |
| [ -z "$path" ] && continue | |
| case "$path" in | |
| docs/examples|docs/examples/*) | |
| ci_required=true | |
| break | |
| ;; | |
| docs/*) | |
| ;; | |
| *.md) | |
| case "$path" in | |
| */*) | |
| ci_required=true | |
| break | |
| ;; | |
| esac | |
| ;; | |
| *) | |
| ci_required=true | |
| break | |
| ;; | |
| esac | |
| done <<< "$changed_files" | |
| fi | |
| fi | |
| echo "ci_required=$ci_required" >> "$GITHUB_OUTPUT" | |
| echo "ci_required=$ci_required" | |
| quality: | |
| needs: [ci-scope] | |
| if: needs.ci-scope.outputs.ci_required == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Set up Python | |
| run: uv python install 3.11 | |
| - name: Cache uv-managed virtualenv | |
| uses: actions/cache@v5 | |
| with: | |
| path: .venv | |
| key: venv-quality-${{ runner.os }}-py3.11-${{ hashFiles('uv.lock', 'pyproject.toml') }} | |
| restore-keys: | | |
| venv-quality-${{ runner.os }}-py3.11- | |
| - name: Install dependencies | |
| run: uv sync --all-extras --dev | |
| - name: Install Pre-Commit hooks | |
| run: uv run pre-commit install | |
| - name: Load cached Pre-Commit Dependencies | |
| id: cached-pre-commit-dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/pre-commit/ | |
| key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} | |
| # Each subsequent step uses `if: always()` so a failing earlier step | |
| # does not stop the others — a single failed run still reports every | |
| # tool's output, matching the parallel-jobs behavior of the old | |
| # validate/mypy/pyright/slotscheck split. | |
| - name: Execute Pre-Commit | |
| id: pre_commit | |
| run: uv run pre-commit run --show-diff-on-failure --color=always --all-files | |
| - name: Run mypy | |
| id: mypy | |
| if: always() && steps.pre_commit.conclusion != 'skipped' && steps.pre_commit.conclusion != 'cancelled' | |
| run: uv run mypy | |
| - name: Run pyright | |
| id: pyright | |
| if: always() && steps.mypy.conclusion != 'skipped' && steps.mypy.conclusion != 'cancelled' | |
| run: uv run pyright | |
| - name: Run slotscheck | |
| if: always() && steps.pyright.conclusion != 'skipped' && steps.pyright.conclusion != 'cancelled' | |
| run: uv run slotscheck sqlspec | |
| # test-unit: fast tier — no Docker, runs tests/unit only. | |
| # Acts as a gate for test-integration: a broken unit test cancels the | |
| # entire matrix (fail-fast: true) AND prevents integration jobs from | |
| # ever starting via `needs:` below. | |
| test-unit: | |
| needs: [ci-scope] | |
| if: needs.ci-scope.outputs.ci_required == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| # fail-fast: true — matrix dimension is Python version only. | |
| # sqlspec failures are typically version-agnostic library bugs, so the | |
| # other 4 jobs running to completion after one fails wastes ~60 min of | |
| # billed runner time. test-build.yml gates package builds behind these | |
| # checks before spending runner minutes on wheel matrices. | |
| fail-fast: true | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Set up Python | |
| run: uv python install ${{ matrix.python-version }} | |
| - name: Cache uv-managed virtualenv | |
| uses: actions/cache@v5 | |
| with: | |
| path: .venv | |
| key: venv-unit-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} | |
| restore-keys: | | |
| venv-unit-${{ runner.os }}-py${{ matrix.python-version }}- | |
| - name: Install dependencies | |
| # tests/unit/ imports every adapter module, so the install set is | |
| # the same as integration. Speed comes from skipping Docker | |
| # pre-pull and running only tests/unit/, not from a lighter sync. | |
| run: uv sync --all-extras --dev | |
| - name: Test | |
| # covdefaults sets fail_under=100 globally; the per-tier run only | |
| # covers a slice of sqlspec, so override to 0 here. The combined | |
| # view in Codecov is the source of truth. | |
| run: >- | |
| uv run pytest tests/unit | |
| -n auto --dist=loadgroup | |
| --cov=sqlspec --cov-report=xml:coverage-unit.xml --cov-report=term-missing | |
| --cov-fail-under=0 | |
| - name: Upload coverage to Codecov | |
| if: always() | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: coverage-unit.xml | |
| flags: unit,py${{ matrix.python-version }} | |
| name: test-unit-${{ matrix.python-version }} | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| test-integration: | |
| needs: [ci-scope, test-unit] | |
| if: needs.ci-scope.outputs.ci_required == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Set up Python | |
| run: uv python install ${{ matrix.python-version }} | |
| - name: Cache uv-managed virtualenv | |
| uses: actions/cache@v5 | |
| with: | |
| path: .venv | |
| key: venv-integration-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} | |
| restore-keys: | | |
| venv-integration-${{ runner.os }}-py${{ matrix.python-version }}- | |
| - name: Install dependencies | |
| run: uv sync --all-extras --dev | |
| - name: Free runner disk before docker work | |
| run: | | |
| set -eu | |
| df -h / | |
| sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL /usr/local/lib/android /usr/local/share/boost | |
| sudo apt-get clean | |
| df -h / | |
| - name: Cache docker images | |
| id: docker-image-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: /tmp/docker-images | |
| key: docker-images-v1-${{ hashFiles('.github/workflows/ci.yml') }} | |
| - name: Load cached docker images | |
| if: steps.docker-image-cache.outputs.cache-hit == 'true' | |
| run: | | |
| docker load -i /tmp/docker-images/sqlspec-test-images.tar | |
| rm -f /tmp/docker-images/sqlspec-test-images.tar | |
| - name: Pre-pull docker images | |
| if: steps.docker-image-cache.outputs.cache-hit != 'true' | |
| run: | | |
| set -eu | |
| images=( | |
| postgres:18 | |
| pgvector/pgvector:pg15 | |
| paradedb/paradedb:0.21.5-pg16 | |
| cockroachdb/cockroach:latest | |
| mysql:8.4 | |
| gvenzl/oracle-free:23-slim-faststart | |
| ghcr.io/goccy/bigquery-emulator:latest | |
| gcr.io/cloud-spanner-emulator/emulator:latest | |
| rustfs/rustfs:latest | |
| rustfs/rc:latest | |
| ) | |
| printf '%s\n' "${images[@]}" | xargs -P 4 -I{} docker pull {} | |
| mkdir -p /tmp/docker-images | |
| docker save "${images[@]}" -o /tmp/docker-images/sqlspec-test-images.tar | |
| - name: Test | |
| id: pytest | |
| env: | |
| PYTHONFAULTHANDLER: "1" | |
| PYTEST_ADDOPTS: "--max-worker-restart=0" | |
| # At 22min, broadcast SIGABRT to ALL python processes (controller + xdist | |
| # workers). Faulthandler in each process dumps every thread to stderr | |
| # before exit, so the test step log captures the actual hang site. | |
| run: | | |
| set -m | |
| ( | |
| sleep 1320 | |
| echo "::group::Hang detected - SIGABRT to all python" | |
| pkill -ABRT -f python || true | |
| sleep 10 | |
| echo "::endgroup::" | |
| ) & | |
| aborter_pid=$! | |
| # covdefaults sets fail_under=100 globally; per-tier coverage | |
| # only covers a slice, so override to 0. Codecov merges the | |
| # tiers server-side. | |
| uv run pytest tests/integration docs/examples -n 2 --dist=loadgroup \ | |
| --cov=sqlspec --cov-report=xml:coverage-integration.xml --cov-report=term-missing \ | |
| --cov-fail-under=0 | |
| rc=$? | |
| kill "$aborter_pid" 2>/dev/null || true | |
| exit $rc | |
| - name: Upload coverage to Codecov | |
| if: always() && steps.pytest.outcome != 'skipped' | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: coverage-integration.xml | |
| flags: integration,py${{ matrix.python-version }} | |
| name: test-integration-${{ matrix.python-version }} | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Dump python stacks on hang | |
| if: failure() && steps.pytest.outcome == 'failure' | |
| run: | | |
| pip install --quiet py-spy || true | |
| for pid in $(pgrep -f 'pytest|python' || true); do | |
| echo "=== py-spy dump pid=$pid ===" | |
| sudo env "PATH=$PATH" py-spy dump --pid "$pid" || true | |
| done | |
| echo "=== docker ps ===" | |
| docker ps -a | |
| echo "=== docker logs (last 100 lines each) ===" | |
| for c in $(docker ps -aq); do | |
| echo "--- container $c ---" | |
| docker logs --tail 100 "$c" 2>&1 || true | |
| done | |
| # test-linux-freethreaded: | |
| # # Disabled: cp314t ecosystem not mature enough yet (missing binary wheels, long build times) | |
| # runs-on: ubuntu-latest | |
| # continue-on-error: true | |
| # timeout-minutes: 30 | |
| # steps: | |
| # - uses: actions/checkout@v4 | |
| # - uses: astral-sh/setup-uv@v3 | |
| # - run: uv python install 3.14t | |
| # - run: uv sync --extra aiosqlite --extra duckdb --dev | |
| # - run: uv run pytest tests/unit -n 2 --dist=loadgroup | |
| # test-windows: | |
| # runs-on: windows-latest | |
| # strategy: | |
| # fail-fast: true | |
| # matrix: | |
| # python-version: ["3.12", "3.13"] | |
| # timeout-minutes: 30 | |
| # steps: | |
| # - name: Check out repository | |
| # uses: actions/checkout@v4 | |
| # - name: Install uv | |
| # uses: astral-sh/setup-uv@v3 | |
| # - name: Set up Python | |
| # run: uv python install ${{ matrix.python-version }} | |
| # - name: Install dependencies | |
| # run: uv sync --all-extras --dev | |
| # - name: Test | |
| # run: uv run pytest -m "" | |
| # test-osx: | |
| # runs-on: macos-latest | |
| # strategy: | |
| # fail-fast: true | |
| # matrix: | |
| # python-version: ["3.11", "3.12", "3.13"] | |
| # timeout-minutes: 30 | |
| # steps: | |
| # - name: Check out repository | |
| # uses: actions/checkout@v4 | |
| # - name: Install uv | |
| # uses: astral-sh/setup-uv@v3 | |
| # - name: Set up Python | |
| # run: uv python install ${{ matrix.python-version }} | |
| # - name: Install dependencies | |
| # run: uv sync --all-extras --dev | |
| # - name: Test | |
| # run: uv run pytest -m "" | |
| build-docs: | |
| needs: | |
| - ci-scope | |
| - quality | |
| - test-unit | |
| if: github.event_name == 'pull_request' && needs.ci-scope.outputs.ci_required == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Set up Python | |
| run: uv python install 3.12 | |
| - name: Install dependencies | |
| run: uv sync --all-extras --dev | |
| - name: Build docs | |
| run: uv run make docs | |
| ci-required: | |
| needs: | |
| - ci-scope | |
| - quality | |
| - test-unit | |
| - test-integration | |
| - build-docs | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify required CI result | |
| env: | |
| SCOPE_RESULT: ${{ needs.ci-scope.result }} | |
| CI_REQUIRED: ${{ needs.ci-scope.outputs.ci_required }} | |
| QUALITY_RESULT: ${{ needs.quality.result }} | |
| UNIT_RESULT: ${{ needs.test-unit.result }} | |
| INTEGRATION_RESULT: ${{ needs.test-integration.result }} | |
| DOCS_RESULT: ${{ needs.build-docs.result }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "$SCOPE_RESULT" != "success" ]; then | |
| echo "::error::SCOPE_RESULT was $SCOPE_RESULT" | |
| exit 1 | |
| fi | |
| if [ "$CI_REQUIRED" = "false" ]; then | |
| echo "Heavy CI intentionally skipped for docs-only or root markdown changes." | |
| exit 0 | |
| fi | |
| if [ "$CI_REQUIRED" != "true" ]; then | |
| echo "::error::CI_REQUIRED was '$CI_REQUIRED'" | |
| exit 1 | |
| fi | |
| failed=0 | |
| for check in QUALITY_RESULT UNIT_RESULT INTEGRATION_RESULT; do | |
| result="${!check}" | |
| if [ "$result" != "success" ]; then | |
| echo "::error::$check was $result" | |
| failed=1 | |
| fi | |
| done | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "$DOCS_RESULT" != "success" ]; then | |
| echo "::error::DOCS_RESULT was $DOCS_RESULT" | |
| failed=1 | |
| fi | |
| exit "$failed" |