From ffaffd7b0b0b01127ddba29a91a7683c1680bfdb Mon Sep 17 00:00:00 2001 From: Dacian Popute Date: Wed, 7 Oct 2026 10:08:08 +0300 Subject: [PATCH 1/3] ci: run the Python/Django combinations as one ubuntu-slim job GitHub bills every job rounded up to a whole minute, so the five-job matrix plus the build job cost six minutes per run for about three minutes of work. One job runs each combination as its own step, in its own venv, and keeps running after a failure so every combination reports. PRs into main only, drafts skipped, superseded PR runs cancelled. Refs #160214 --- .github/workflows/ci.yml | 107 ++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3880082..fcb7ff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,45 +3,92 @@ name: CI on: push: branches: [main] - # Every PR, not only those into main, so a stacked PR is tested before its - # base merges. + # PRs into main only (#160214). A stacked PR is tested once it targets main + # and gets a new push; push: main still tests every merge. pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - # Explicit Django dimension: 4.2 covers the django>=4.0 floor, - # 5.2 LTS is what consuming projects run, 6.0 is current. - # Pairings follow Django's own python support (4.2/5.2: <=3.12 and - # >=3.10 here; 6.0: >=3.12). - python-version: ["3.10", "3.12"] - django-version: ["4.2", "5.2", "6.0"] - exclude: - - python-version: "3.10" - django-version: "6.0" + # One job, not a matrix (#160214): GitHub bills every job rounded up to a + # whole minute, so six half-minute jobs cost six minutes. Each combination + # is its own step with its own venv; !cancelled() runs every combination + # even after one fails, so a red step names exactly what broke. + ci: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + runs-on: ubuntu-slim steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - with: - python-version: ${{ matrix.python-version }} - - run: uv sync --group test --extra django - # The pip install overrides the lock's Django; every later step MUST + + # Explicit Django dimension: 4.2 covers the django>=4.0 floor, + # 5.2 LTS is what consuming projects run, 6.0 is current. + # Pairings follow Django's own python support (4.2/5.2: <=3.12 and + # >=3.10 here; 6.0: >=3.12). + # + # The venvs live in runner.temp, not the checkout: hatchling does not + # read a venv's own .gitignore, so `uv build` would pack one into the + # sdist. + # + # The pip install overrides the lock's Django; every later command MUST # use --no-sync (a plain `uv run` or second `uv sync` re-syncs the - # venv to the lock and silently reverts the matrix pin). - - run: uv pip install "Django~=${{ matrix.django-version }}.0" - - run: uv run --no-sync pytest tests/ -v + # venv to the lock and silently reverts the pin). + - name: Python 3.10 / Django 4.2 + if: ${{ !cancelled() }} + env: + UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/py310-dj42 + run: | + uv sync --python 3.10 --group test --extra django + uv pip install --python "$UV_PROJECT_ENVIRONMENT" "Django~=4.2.0" + uv run --no-sync pytest tests/ -v + + - name: Python 3.10 / Django 5.2 + if: ${{ !cancelled() }} + env: + UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/py310-dj52 + run: | + uv sync --python 3.10 --group test --extra django + uv pip install --python "$UV_PROJECT_ENVIRONMENT" "Django~=5.2.0" + uv run --no-sync pytest tests/ -v + + - name: Python 3.12 / Django 4.2 + if: ${{ !cancelled() }} + env: + UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/py312-dj42 + run: | + uv sync --python 3.12 --group test --extra django + uv pip install --python "$UV_PROJECT_ENVIRONMENT" "Django~=4.2.0" + uv run --no-sync pytest tests/ -v + + - name: Python 3.12 / Django 5.2 + if: ${{ !cancelled() }} + env: + UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/py312-dj52 + run: | + uv sync --python 3.12 --group test --extra django + uv pip install --python "$UV_PROJECT_ENVIRONMENT" "Django~=5.2.0" + uv run --no-sync pytest tests/ -v + + - name: Python 3.12 / Django 6.0 + if: ${{ !cancelled() }} + env: + UV_PROJECT_ENVIRONMENT: ${{ runner.temp }}/py312-dj60 + run: | + uv sync --python 3.12 --group test --extra django + uv pip install --python "$UV_PROJECT_ENVIRONMENT" "Django~=6.0.0" + uv run --no-sync pytest tests/ -v + + - name: Build + id: build + if: ${{ !cancelled() }} + run: uv build --python 3.12 - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - with: - python-version: "3.12" - - run: uv build - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() && steps.build.outcome == 'success' }} with: name: dist path: dist/ From bb85f50f1a172c7c663f05dd337ab6301c6c7659 Mon Sep 17 00:00:00 2001 From: Dacian Popute Date: Wed, 7 Oct 2026 10:14:52 +0300 Subject: [PATCH 2/3] ci: keep the single CI job on ubuntu-latest On ubuntu-slim's single vCPU the wall-clock bound in the XML masking linear-time test failed in all five combinations. Refs #160214 --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcb7ff7..bdb621b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,12 @@ jobs: # whole minute, so six half-minute jobs cost six minutes. Each combination # is its own step with its own venv; !cancelled() runs every combination # even after one fails, so a red step names exactly what broke. + # Not ubuntu-slim: on its single vCPU the 0.25 s wall-clock bound in + # test_many_start_tags_nothing_closes_take_linear_time fails in every + # combination. ci: if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-slim + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 From 0012c0d8a3ca8811fa252ed242b53a1217fa642e Mon Sep 17 00:00:00 2001 From: Dacian Popute Date: Wed, 7 Oct 2026 10:23:03 +0300 Subject: [PATCH 3/3] ci: give each push its own concurrency group A concurrency group holds one pending run and cancels the older pending one when a newer run arrives, even with cancel-in-progress false, so a group shared by every push to a branch could drop a queued push. Pushes now group by run_id; PR runs still group by PR number and cancel their superseded run. Refs #160214 --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdb621b..28896d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,12 @@ on: branches: [main] types: [opened, synchronize, reopened, ready_for_review] +# A new push to a PR cancels its older run. A push to main gets a group of its +# own (run_id): a group holds one pending run, so a shared one would drop +# queued pushes. concurrency: - group: ci-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + group: ci-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true jobs: # One job, not a matrix (#160214): GitHub bills every job rounded up to a