diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b07c571..4727a98 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,10 @@ on: branches: [main] workflow_call: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + permissions: contents: read @@ -13,7 +17,6 @@ env: CARGO_TERM_COLOR: always FORCE_COLOR: "1" PYTHONUNBUFFERED: "1" - UV_VERSION: "0.4.x" jobs: pre-commit: @@ -25,7 +28,7 @@ jobs: uses: astral-sh/setup-uv@v5 with: enable-cache: true - version: ${{ env.UV_VERSION }} + pyproject-file: pyproject.toml - uses: actions/cache@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97dce31..7ed0855 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,8 +12,14 @@ on: tags: - "*" pull_request: + branches: + - "release/*" workflow_dispatch: +concurrency: + group: test-${{ github.head_ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e26f5e..8108a5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,14 @@ on: push: branches: [main] workflow_call: + inputs: + os: + description: "Comma-delineated list of OS targets to run tests on" + required: false + type: string concurrency: - group: test-${{ github.head_ref }} + group: ${{ github.workflow }}-${{ github.head_ref }} cancel-in-progress: true env: @@ -16,15 +21,10 @@ env: PYTHONUNBUFFERED: "1" jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: - - macos-latest - - ubuntu-latest - - windows-latest + generate-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v4 @@ -34,10 +34,26 @@ jobs: enable-cache: true pyproject-file: pyproject.toml - - name: Install dependencies and build + - id: set-matrix run: | - uv sync --frozen - uv run maturin build + uv run noxfile.py --session gha_matrix -- "${{ inputs.os }}" + + test: + name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: generate-matrix + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + pyproject-file: pyproject.toml - name: Run tests - run: cargo test --verbose + run: | + uv run noxfile.py --session "tests(python='${{ matrix.python-version }}', django='${{ matrix.django-version }}')" diff --git a/Justfile b/Justfile index 038e315..959c4ad 100644 --- a/Justfile +++ b/Justfile @@ -8,6 +8,10 @@ mod docs ".just/docs.just" default: @just --list +[private] +nox SESSION *ARGS: + uv run noxfile.py --session "{{ SESSION }}" -- "{{ ARGS }}" + bumpver *ARGS: uv run --with bumpver bumpver {{ ARGS }} @@ -17,4 +21,10 @@ clean: # run pre-commit on all files lint: @just --fmt - uv run --with pre-commit-uv pre-commit run --all-files + @just nox lint + +test *ARGS: + @just nox test {{ ARGS }} + +testall *ARGS: + @just nox tests {{ ARGS }} diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..a1bd185 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,154 @@ +#!/usr/bin/env -S uv run --quiet +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "nox", +# ] +# /// + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import nox + +nox.options.default_venv_backend = "uv|virtualenv" +nox.options.reuse_existing_virtualenvs = True + +PY39 = "3.9" +PY310 = "3.10" +PY311 = "3.11" +PY312 = "3.12" +PY313 = "3.13" +PY_VERSIONS = [PY39, PY310, PY311, PY312, PY313] +PY_DEFAULT = PY_VERSIONS[0] +PY_LATEST = PY_VERSIONS[-1] + +DJ42 = "4.2" +DJ50 = "5.0" +DJ51 = "5.1" +DJMAIN = "main" +DJMAIN_MIN_PY = PY312 +DJ_VERSIONS = [DJ42, DJ50, DJ51, DJMAIN] +DJ_LTS = [ + version for version in DJ_VERSIONS if version.endswith(".2") and version != DJMAIN +] +DJ_DEFAULT = DJ_LTS[0] +DJ_LATEST = DJ_VERSIONS[-2] + + +def version(ver: str) -> tuple[int, ...]: + """Convert a string version to a tuple of ints, e.g. "3.10" -> (3, 10)""" + return tuple(map(int, ver.split("."))) + + +def should_skip(python: str, django: str) -> bool: + """Return True if the test should be skipped""" + + if django == DJMAIN and version(python) < version(DJMAIN_MIN_PY): + # Django main requires Python 3.10+ + return True + + if django == DJ51 and version(python) < version(PY310): + # Django 5.1 requires Python 3.10+ + return True + + if django == DJ50 and version(python) < version(PY310): + # Django 5.0 requires Python 3.10+ + return True + + return False + + +@nox.session +def test(session): + session.notify(f"tests(python='{PY_DEFAULT}', django='{DJ_DEFAULT}')") + + +@nox.session +@nox.parametrize( + "python,django", + [ + (python, django) + for python in PY_VERSIONS + for django in DJ_VERSIONS + if not should_skip(python, django) + ], +) +def tests(session, django): + session.run_install( + "uv", + "sync", + "--frozen", + "--inexact", + "--no-install-package", + "django", + "--python", + session.python, + env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}, + ) + + if django == DJMAIN: + session.install( + "django @ https://github.com/django/django/archive/refs/heads/main.zip" + ) + else: + session.install(f"django=={django}") + + command = ["cargo", "test"] + if session.posargs: + args = [] + for arg in session.posargs: + if arg: + args.extend(arg.split(" ")) + command.extend(args) + session.run(*command, external=True) + + +@nox.session +def lint(session): + session.run( + "uv", + "run", + "--with", + "pre-commit-uv", + "--python", + PY_LATEST, + "pre-commit", + "run", + "--all-files", + ) + + +@nox.session +def gha_matrix(session): + os_args = session.posargs[0] if session.posargs else "" + os_list = [os.strip() for os in os_args.split(",") if os_args.strip()] or [ + "ubuntu-latest" + ] + + sessions = session.run("nox", "-l", "--json", external=True, silent=True) + versions_list = [ + { + "django-version": session["call_spec"]["django"], + "python-version": session["python"], + } + for session in json.loads(sessions) + if session["name"] == "tests" + ] + + matrix = { + "include": [{**combo, "os": os} for os in os_list for combo in versions_list] + } + + if os.environ.get("GITHUB_OUTPUT"): + with Path(os.environ["GITHUB_OUTPUT"]).open("a") as fh: + print(f"matrix={matrix}", file=fh) + else: + print(matrix) + + +if __name__ == "__main__": + nox.main()