Skip to content

refactor: consolidate modernization and test overhaul #2043

refactor: consolidate modernization and test overhaul

refactor: consolidate modernization and test overhaul #2043

Workflow file for this run

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"