name: Test permissions: contents: read on: push: tags-ignore: - '*' branches: - '*' pull_request: workflow_call: workflow_dispatch: inputs: debug: description: 'Open ssh debug session.' required: true default: false type: boolean schedule: - cron: '0 13 * * *' # Runs at 6 am pacific every day jobs: postgres: runs-on: ubuntu-latest permissions: contents: read actions: write # Service containers to run with `container-job` strategy: fail-fast: false matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] postgres-version: ['12', '14', 'latest'] psycopg-version: ['psycopg2', 'psycopg3'] django-version: - '4.2' # LTS April 2026 - '5.1' # December 2025 - '5.2' # LTS April 2028 - '6.0' # exclude: - python-version: '3.10' django-version: '6.0' - python-version: '3.11' django-version: '6.0' - python-version: '3.13' django-version: '4.2' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.1' - python-version: '3.14' django-version: '5.2' - postgres-version: '12' django-version: '5.1' - postgres-version: '12' django-version: '5.2' - postgres-version: '12' django-version: '6.0' - postgres-version: '14' django-version: '4.2' - postgres-version: '14' django-version: '5.2' - postgres-version: '14' django-version: '6.0' - postgres-version: 'latest' django-version: '4.2' - postgres-version: 'latest' django-version: '5.1' - postgres-version: '12' psycopg-version: 'psycopg3' - postgres-version: 'latest' psycopg-version: 'psycopg2' # https://github.com/psycopg/psycopg2/pull/1695 - python-version: '3.13' psycopg-version: 'psycopg2' - python-version: '3.14' psycopg-version: 'psycopg2' env: RDBMS: postgres POSTGRES_PASSWORD: postgres PGPASSWORD: postgres POSTGRES_USER: postgres POSTGRES_HOST: localhost POSTGRES_PORT: 5432 COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-${{ matrix.psycopg-version }}-pg${{ matrix.postgres-version }}.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_CLIENT_VERSION: ${{ matrix.psycopg-version }} TEST_DATABASE_VERSION: ${{ matrix.postgres-version }} # Service containers to run with `runner-job` services: # Label used to access the service container postgres: # Docker Hub image image: postgres:${{ matrix.postgres-version }} # Provide the password for postgres env: POSTGRES_PASSWORD: postgres # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} id: sp uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Create test databases run: | psql -h localhost -p 5432 -U postgres -d postgres -c "CREATE DATABASE test1;" psql -h localhost -p 5432 -U postgres -d postgres -c "CREATE DATABASE test2;" - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test-db ${{ matrix.psycopg-version }} just test-integrations ${{ matrix.psycopg-version }} - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} sqlite: runs-on: ubuntu-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test-db just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} mysql: runs-on: ubuntu-latest permissions: contents: read actions: write strategy: fail-fast: false matrix: python-version: ['3.10', '3.12', '3.14'] mysql-version: ['8.0', 'latest'] mysqlclient-version: ['1.4.3', ''] django-version: - '4.2' # LTS April 2024 - '5.2' # LTS April 2028 - '6.0' # April 2027 exclude: - django-version: '4.2' mysql-version: 'latest' - django-version: '6.0' mysql-version: '8.0' - mysql-version: 'latest' mysqlclient-version: '1.4.3' - python-version: '3.12' django-version: '4.2' - python-version: '3.14' django-version: '4.2' - python-version: '3.10' django-version: '5.2' - python-version: '3.14' django-version: '5.2' - django-version: '5.2' mysqlclient-version: '1.4.3' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '6.0' env: RDBMS: mysql MYSQL_VERSION: ${{ matrix.mysql-version }} COVERAGE_FILE: linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-${{ matrix.mysqlclient-version }}-mysql${{ matrix.mysql-version }}.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_CLIENT_VERSION: ${{ matrix.mysqlclient-version }} TEST_DATABASE_VERSION: ${{ matrix.mysql-version }} services: mysql: # Docker Hub image image: mysql:${{ matrix.mysql-version }} # Provide the password for mysql env: MYSQL_ROOT_PASSWORD: root MYSQL_MULTIPLE_DATABASES: test1,test2 # Set health checks to wait until mysql has started options: >- --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 3306 on service container to the host - 3306:3306 steps: # make text comparisons case sensitive (some tests) - name: Set default collation for MySQL run: | mysql -h 127.0.0.1 -u root -proot <- --health-cmd="${{ matrix.mariadb-healthcheck }}" --health-interval 10s --health-timeout 5s --health-retries 5 ports: # Maps tcp port 3306 on service container to the host - 3306:3306 steps: # make text comparisons case sensitive (some tests) - name: Set default collation for MariaDB run: | mysql -h 127.0.0.1 -u root -proot <- --health-cmd healthcheck.sh --health-interval 10s --health-timeout 5s --health-retries 10 oracle2: image: gvenzl/${{ matrix.oracle-version }} env: ORACLE_PASSWORD: password ORACLE_DATABASE: test2 # Forward Oracle port ports: - 1522:1521 # Provide healthcheck script options for startup options: >- --health-cmd healthcheck.sh --health-interval 10s --health-timeout 5s --health-retries 10 steps: - name: Set coverage file run: | ORACLE_TAG=$(echo "${{ matrix.oracle-version }}" | cut -d':' -f2) echo "COVERAGE_FILE=linux-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-oracle${ORACLE_TAG}.coverage" >> $GITHUB_ENV - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} id: sp uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Emacs if: ${{ github.event.inputs.debug == 'true' }} run: | sudo apt install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Oracle Client # https://askubuntu.com/questions/1512196/libaio1-on-noble run: | sudo apt install alien libaio1t64 sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1 if [[ "${{ matrix.oracle-version }}" == oracle-xe* ]]; then curl --output oracle-client.rpm https://download.oracle.com/otn_software/linux/instantclient/2116000/oracle-instantclient-basiclite-21.16.0.0.0-1.el8.x86_64.rpm sudo alien -i oracle-client.rpm sudo sh -c "echo /usr/lib/oracle/21/client64/lib/ > /etc/ld.so.conf.d/oracle.conf" else curl --output oracle-client.rpm https://download.oracle.com/otn_software/linux/instantclient/2326000/oracle-instantclient-basiclite-23.26.0.0.0-1.el9.x86_64.rpm sudo alien -i oracle-client.rpm sudo sh -c "echo /usr/lib/oracle/23/client64/lib/ > /etc/ld.so.conf.d/oracle.conf" fi sudo ldconfig - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi # we don't run integration tests against Oracle in CI, these are slow enough - name: Run Full Unit Tests run: | if [[ "${{ matrix.oracle-version }}" == oracle-xe* ]]; then just test-db cx_oracle else just test-db oracledb fi - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} windows: runs-on: windows-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: windows-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: install-vim-windows if: ${{ github.event.inputs.debug == 'true' }} uses: rhysd/action-setup-vim@v1 - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} $version = "${{ matrix.django-version }}" if ($version -match "(a|b|rc)") { just test-lock "Django==$version" } else { just test-lock "Django~=$version.0" } - name: Run Unit Tests run: | just test just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} macos: runs-on: macos-latest permissions: contents: read actions: write env: RDBMS: sqlite COVERAGE_FILE: macos-py${{ matrix.python-version }}-dj${{ matrix.django-version }}-sqlite.coverage TEST_PYTHON_VERSION: ${{ matrix.python-version }} TEST_DJANGO_VERSION: ${{ matrix.django-version }} TEST_DATABASE_VERSION: "sqlite" strategy: fail-fast: false matrix: python-version: [ '3.10', '3.12', '3.14'] django-version: - '4.2' # LTS April 2026 - '5.2' # LTS April 2028 - '6.0' # LTS April 2028 exclude: - python-version: '3.10' django-version: '5.2' - python-version: '3.10' django-version: '6.0' - python-version: '3.12' django-version: '4.2' - python-version: '3.12' django-version: '6.0' - python-version: '3.14' django-version: '4.2' - python-version: '3.14' django-version: '5.2' steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 id: sp with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: install-emacs-macos if: ${{ github.event.inputs.debug == 'true' }} run: | brew install emacs - name: Setup tmate session if: ${{ github.event.inputs.debug == 'true' }} uses: mxschmitt/action-tmate@v3.22 with: detached: true timeout-minutes: 60 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} if [[ "${{ matrix.django-version }}" =~ (a|b|rc) ]]; then just test-lock Django==${{ matrix.django-version }} else just test-lock Django~=${{ matrix.django-version }}.0 fi - name: Run Unit Tests run: | just test just test-integrations - name: Store coverage files uses: actions/upload-artifact@v4 with: name: ${{ env.COVERAGE_FILE }} path: ${{ env.COVERAGE_FILE }} coverage-combine: needs: [postgres, sqlite, mysql, mariadb, oracle, windows, macos] runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 id: sp - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true - name: Setup Just uses: extractions/setup-just@v3 - name: Install Release Dependencies run: | just setup ${{ steps.sp.outputs.python-path }} - name: Get coverage files uses: actions/download-artifact@v7 with: pattern: "*.coverage" merge-multiple: true - run: ls -la *.coverage - run: just coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml