From 980fc2dddd947b7c32d5061e62d03640d3d91074 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Wed, 12 Aug 2026 11:13:39 -0400 Subject: [PATCH] G7.3: pin Python 3.12 in CI and cap job runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real CI run wedged for 14 minutes. Not a network problem, not the isolation work — catthehacker/ubuntu:act-22.04 ships Python 3.10.12 while this project declares requires-python >=3.12, and pip answered that by backtracking through the entire release history of every dependency looking for something 3.10-compatible. At 100% CPU, with -q hiding every line of it, and it would have churned until the runner's 30m timeout. A version mismatch presenting as a hang rather than an error is worth a test, so there is one: the workflow's python image must satisfy pyproject's requires-python, checked by parsing both rather than by eyeballing them. Also: every job now has timeout-minutes. A wedged step should be a red check in minutes, not an occupied runner for half an hour. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/.trigger | 1 - .gitea/workflows/check.yml | 16 +++++++++++++++- api/tests/test_invariants.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) delete mode 100644 .gitea/workflows/.trigger diff --git a/.gitea/workflows/.trigger b/.gitea/workflows/.trigger deleted file mode 100644 index 997b246..0000000 --- a/.gitea/workflows/.trigger +++ /dev/null @@ -1 +0,0 @@ -ci: re-run after routing fix diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 186278f..55545f4 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -16,6 +16,18 @@ on: jobs: gate: runs-on: veron-1 + # Run IN a Python 3.12 image rather than trusting the runner image's + # toolchain. The first real CI run wedged here: catthehacker/ubuntu:act-22.04 + # ships Python 3.10.12, this project declares requires-python >=3.12, and pip + # answered that by backtracking through the entire release history of every + # dependency looking for something 3.10-compatible. It churned for 14 minutes + # at 100% CPU with `-q` hiding all of it, and would have churned until the + # runner timeout. A version mismatch presenting as a hang, not an error. + container: + image: python:3.12-bookworm + # And a hard ceiling, so a wedged step is a red check in minutes rather than + # an occupied runner for half an hour. + timeout-minutes: 12 services: postgres: image: postgres:16-alpine @@ -32,8 +44,10 @@ jobs: - name: install run: | + python3 --version python3 -m venv .venv - .venv/bin/pip install -q -e ".[dev]" + .venv/bin/pip install -q --upgrade pip + .venv/bin/pip install -e ".[dev]" - name: lint run: .venv/bin/ruff check api scripts diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index 2aee0e9..395e18f 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -461,3 +461,33 @@ def test_g75_workflows_use_a_label_this_runner_actually_provides(): continue label = ln.split("runs-on:")[1].strip() assert label in provided, f"{wf.name}: '{label}' is not a provided label" + + +def test_g73_workflow_pins_a_python_that_satisfies_requires_python(): + """The first real CI run wedged for 14 minutes because the runner image + ships Python 3.10 and this project requires 3.12: pip answered by + backtracking through every historical version of every dependency, at full + CPU, silently. A version mismatch presenting as a hang rather than an + error.""" + import re as _re + + pyproject = (ROOT / "pyproject.toml").read_text() + m = _re.search(r'requires-python\s*=\s*">=(\d+)\.(\d+)"', pyproject) + assert m, "pyproject declares no requires-python" + major, minor = int(m.group(1)), int(m.group(2)) + + for wf in ROOT.rglob(".gitea/workflows/*.y*ml"): + text = wf.read_text() + img = _re.search(r"image:\s*python:(\d+)\.(\d+)", text) + assert img, f"{wf.name}: job does not pin a python image" + assert (int(img.group(1)), int(img.group(2))) >= (major, minor), ( + f"{wf.name}: pins python {img.group(0)} but the project requires " + f">={major}.{minor}" + ) + + +def test_g73_every_job_has_a_timeout(): + """A wedged step should be a red check in minutes, not an occupied runner + for half an hour.""" + for wf in ROOT.rglob(".gitea/workflows/*.y*ml"): + assert "timeout-minutes:" in wf.read_text(), f"{wf.name}: no job timeout"