From 29ce83e27454b08ef3728a6369f7d849d751d105 Mon Sep 17 00:00:00 2001 From: CaliBrain Date: Tue, 11 Aug 2026 23:58:37 -0400 Subject: [PATCH] Stop dependabot proposing Python pre-releases, bound the e2e health wait (#1189) PR #1169 (python:3.14.6-slim -> python:3.15.0b3-slim) ran for 6h before GitHub's max job limit killed it, then did it again on re-run. Two independent defects. Dependabot proposed a beta at all: the config already excluded python from the docker digest group for dependabot-core#9496, but the comment claimed ungrouped python updates get their pre-release filtered. They don't. dependabot-core#13815 rewrote the Docker pre-release heuristic to catch PEP 440 tags (its tests cover 3.15.0a2 and 3.5.0b3), yet the suffixed real tag still got through seven months later. CPython spells pre-releases without a separator, so 3.15.0b3 parses as an ordinary version sorting above 3.14.6. Ignore python semver-minor/major instead of trusting the heuristic; patch and digest updates still flow. The run took hours rather than failing: the health wait looked bounded at 60 iterations x 2s, but bare `curl` has no timeout. The 3.15 image booted a container that bound 8084 without ever serving (greenlet has no 3.15 wheel, so the gevent gunicorn worker was wedged), so curl blocked on read forever and the loop never reached iteration 2. Every job's orphan process at cancellation was that curl. Bound each probe and switch to a wall-clock deadline, and add timeout-minutes so a hang can never reach 6h again. Verified against a socket that accepts and never responds: the old loop was still hung at 30s, the new one exits at 120s with HEALTHY=0 into the existing log-dump path, and a responsive endpoint is still detected immediately. --- .github/dependabot.yml | 22 +++++++++++++++++++--- .github/workflows/e2e-platform.yml | 5 +++++ tests/e2e/platform/run-e2e.sh | 13 +++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0a923973..cd3c92ce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -46,9 +46,7 @@ updates: # pre-release filter is bypassed for *grouped* updates # (dependabot-core#9496), so a grouped python update proposes pre-release # tags like python:3.15.0b2 as if they were a normal stable minor bump. - # Updated individually, python is filtered correctly: alpha/beta/rc tags - # are skipped and only stable releases (e.g. 3.15.0 once final) are - # proposed. node + uv stay grouped into a single digest PR. + # node + uv stay grouped into a single digest PR. patterns: ["*"] exclude-patterns: ["python"] ignore: @@ -58,6 +56,24 @@ updates: - dependency-name: "node" update-types: ["version-update:semver-major"] + # Python: block minor/major bumps. Ungrouping python (above) is NOT enough + # to keep pre-releases out — dependabot-core#13815 rewrote the Docker + # pre-release heuristic to catch PEP 440 tags like 3.15.0a2 / 3.5.0b3, but + # the suffixed real tag still slipped through as PR #1169 + # (python:3.14.6-slim -> python:3.15.0b3-slim). CPython spells + # pre-releases without a separator, so tag parsing reads 3.15.0b3 as an + # ordinary version that sorts above 3.14.6. + # + # A minor-version ignore blocks it regardless of spelling. Patch bumps + # (3.14.6 -> 3.14.7) and same-tag digest refreshes still land automatically. + # Moving the runtime to a new Python minor is a manual, deliberate change: + # bump the tag here and confirm C-extension wheels (greenlet/gevent) exist + # for it — a source build against a pre-release ABI boots an app that binds + # its port but never serves, which wedges e2e for the full 6h job limit. + - dependency-name: "python" + update-types: + ["version-update:semver-major", "version-update:semver-minor"] + # GitHub Actions - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/e2e-platform.yml b/.github/workflows/e2e-platform.yml index 263f330a..30310bf2 100644 --- a/.github/workflows/e2e-platform.yml +++ b/.github/workflows/e2e-platform.yml @@ -60,6 +60,9 @@ jobs: e2e: needs: select-profiles runs-on: ubuntu-latest + # A wedged app under test must not burn GitHub's 6h max job limit. A healthy + # profile run finishes in ~3-5 min; anything past 25 is hung, not slow. + timeout-minutes: 25 strategy: fail-fast: false matrix: @@ -87,6 +90,8 @@ jobs: needs: changes if: needs.changes.outputs.relevant == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-latest + # Real Chrome + qBittorrent is the slowest profile; still nowhere near 40 min. + timeout-minutes: 40 name: e2e (full — real Chrome + qBittorrent) steps: - name: Checkout diff --git a/tests/e2e/platform/run-e2e.sh b/tests/e2e/platform/run-e2e.sh index c12e83ff..f3585636 100755 --- a/tests/e2e/platform/run-e2e.sh +++ b/tests/e2e/platform/run-e2e.sh @@ -59,8 +59,17 @@ fi echo "==> [$PROFILE] waiting for shelfmark health" HEALTHY=0 -for _ in $(seq 1 60); do - if curl -fsS http://localhost:8084/api/health >/dev/null 2>&1; then HEALTHY=1; break; fi +# The curl timeouts are load-bearing, not belt-and-braces. A container that +# binds 8084 but never answers (e.g. a broken C-extension wheel wedging the +# gunicorn worker) blocks a bare `curl` forever on read, so an iteration-counted +# loop never reaches iteration 2 and the wait becomes unbounded — that hung CI +# for the full 6h job limit on PR #1169. Bound each probe AND the whole wait. +HEALTH_DEADLINE=$((SECONDS + 120)) +while ((SECONDS < HEALTH_DEADLINE)); do + if curl -fsS --connect-timeout 3 --max-time 5 http://localhost:8084/api/health >/dev/null 2>&1; then + HEALTHY=1 + break + fi sleep 2 done