Pipeline #351 hit a NoSuchWindowException / browsing-context-discarded flake on the LAST channels FT (test_first_done_polarity_sees_other_group_settling_message) — typical cumulative-Firefox-memory-pressure failure on a multi-browser test run as the 22nd in its bucket. Test passes locally and in isolation; no code regression. The other two FT stages (test-FTs-room, test-FTs-non-room) already route through `_retry_failed.sh`, which parses Django's FAIL:/ERROR: lines from stdout and re-runs only the failed labels. Wrapping the three two-browser-FTs commands (two-browser / sequential / channels tags) in the same script gives the channels suite the same flake recovery without slowing the happy path (first-run-green short-circuits to exit 0). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
255 lines
10 KiB
YAML
255 lines
10 KiB
YAML
services:
|
|
- name: postgres
|
|
image: postgres:16
|
|
environment:
|
|
POSTGRES_DB: python_tdd_test
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
|
|
- name: redis
|
|
image: redis:7
|
|
|
|
steps:
|
|
- name: test-UTs-n-ITs
|
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
|
environment:
|
|
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
|
|
CELERY_BROKER_URL: redis://redis:6379/0
|
|
REDIS_URL: redis://redis:6379/1
|
|
PIP_CACHE_DIR: .pip-cache
|
|
commands:
|
|
# `requirements.dev.txt` is the pinned superset Dockerfile.ci pre-
|
|
# installs; pinning here means pip skips resolver+download and just
|
|
# verifies "already satisfied" (~5-10s) instead of resolving unpinned
|
|
# requirements.txt against PyPI from scratch (~30-60s). Drift safety
|
|
# net: if requirements.dev.txt has changed since the CI image was
|
|
# last rebuilt + pushed, pip installs the delta — slower for that
|
|
# run but never broken. See TDD SKILL.md § CI dependency discipline.
|
|
- pip install -r requirements.dev.txt
|
|
- cd ./src
|
|
- python manage.py test apps
|
|
when:
|
|
- event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: test-two-browser-FTs
|
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
|
depends_on:
|
|
- test-UTs-n-ITs
|
|
environment:
|
|
HEADLESS: 1
|
|
CELERY_BROKER_URL: redis://redis:6379/0
|
|
REDIS_URL: redis://redis:6379/1
|
|
STRIPE_SECRET_KEY:
|
|
from_secret: stripe_secret_key
|
|
STRIPE_PUBLISHABLE_KEY:
|
|
from_secret: stripe_publishable_key
|
|
PIP_CACHE_DIR: .pip-cache
|
|
commands:
|
|
- pip install -r requirements.dev.txt
|
|
- cd ./src
|
|
# Also collectstatic'd here; output sits in the shared workspace so
|
|
# the downstream FT steps don't have to repeat it.
|
|
- python manage.py collectstatic --noinput
|
|
# All three tag-stages run through `_retry_failed.sh` so a single
|
|
# browsing-context-discarded / NoSuchWindow flake on a multi-browser
|
|
# channels FT (typically the LAST test in the suite, when Firefox
|
|
# has accumulated memory pressure from 21 prior browser launches)
|
|
# costs ~30s on retry instead of failing the whole step. Matches
|
|
# the retry posture of test-FTs-room + test-FTs-non-room. First-
|
|
# run-green still exits 0 immediately — no overhead in the happy
|
|
# path. First-run-crash w. no parseable labels propagates the
|
|
# original exit (genuine infra problems aren't masked).
|
|
- bash ../.woodpecker/_retry_failed.sh functional_tests --tag=two-browser
|
|
- bash ../.woodpecker/_retry_failed.sh functional_tests --tag=sequential
|
|
- bash ../.woodpecker/_retry_failed.sh functional_tests --tag=channels
|
|
when:
|
|
- event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
# ── FT split (stage-parallel, intra-stage sequential) ─────────────────
|
|
#
|
|
# test_game_room_* is the heaviest cluster — 9 Selenium-driven room-flow
|
|
# FTs that historically dominate the FT step wall-clock (~70% of the
|
|
# ~40-min single-step runs). Split off into its own step (`test-FTs-room`)
|
|
# so the partition is visible in the pipeline view; the non-room bucket
|
|
# is `test-FTs-non-room`. Both depend on test-two-browser-FTs only, so
|
|
# they fan out + run concurrently.
|
|
#
|
|
# The previous SQLite-collision blocker (pipeline #296: second step
|
|
# started against the first step's half-created `src/test_db.sqlite3`
|
|
# → Django interactive prompt → EOFError under non-interactive CI
|
|
# stdin) is resolved by giving each step a distinct `DATABASE_URL`
|
|
# pointing at its own sqlite file under /tmp — outside the shared
|
|
# workspace mount so the two stages can't see each other's DB.
|
|
#
|
|
# `--parallel` is dropped from both steps. Empirically (pipelines
|
|
# #302-304) it was giving ~1-1.5x speedup at most on these Selenium
|
|
# FTs — Firefox spawn cost + RAM pressure + SQLite file-lock contention
|
|
# eat most of the gain — while amplifying every transient-DOM flake
|
|
# (login-race, gecko-perms, ElementNotInteractable, Jasmine-timeout).
|
|
# Stage-level parallelism gives the same wall-clock reduction without
|
|
# contention amplification.
|
|
|
|
- name: test-FTs-non-room
|
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
|
depends_on:
|
|
- test-two-browser-FTs
|
|
environment:
|
|
HEADLESS: 1
|
|
# /tmp path (not workspace-relative) so the parallel test-FTs-room
|
|
# step can't see this DB + vice versa. See split-rationale above.
|
|
DATABASE_URL: sqlite:////tmp/test_db_non_room.sqlite3
|
|
CELERY_BROKER_URL: redis://redis:6379/0
|
|
REDIS_URL: redis://redis:6379/1
|
|
STRIPE_SECRET_KEY:
|
|
from_secret: stripe_secret_key
|
|
STRIPE_PUBLISHABLE_KEY:
|
|
from_secret: stripe_publishable_key
|
|
PIP_CACHE_DIR: .pip-cache
|
|
commands:
|
|
- pip install -r requirements.dev.txt
|
|
- cd ./src
|
|
# Every FT file EXCEPT test_game_room_*, test_trinket_*, AND
|
|
# test_game_my_sea* — all three clusters run in test-FTs-room.
|
|
# Channels + two-browser tags already covered upstream.
|
|
# `ls | grep -v | sed` enumerates module dotted-paths from
|
|
# filenames. (No trailing `_` in the my-sea alternative — the
|
|
# file is `test_game_my_sea.py` w. no further suffix today.)
|
|
#
|
|
# Wrapped in `_retry_failed.sh` so a single Selenium flake (browser
|
|
# hang, gecko-perms blip, login race) at test N/M doesn't cost the
|
|
# full step wall-clock on retry — the script parses Django's
|
|
# FAIL:/ERROR: lines from stdout + re-runs only those labels.
|
|
- bash ../.woodpecker/_retry_failed.sh --exclude-tag=channels --exclude-tag=two-browser $(ls functional_tests/test_*.py | grep -vE 'test_(game_room|trinket)_|test_game_my_sea' | sed 's|/|.|g;s|\.py||')
|
|
when:
|
|
- event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: test-FTs-room
|
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
|
depends_on:
|
|
- test-two-browser-FTs
|
|
environment:
|
|
HEADLESS: 1
|
|
# /tmp path (not workspace-relative) so test-FTs-non-room can't see
|
|
# this DB + vice versa. See split-rationale above.
|
|
DATABASE_URL: sqlite:////tmp/test_db_room.sqlite3
|
|
CELERY_BROKER_URL: redis://redis:6379/0
|
|
REDIS_URL: redis://redis:6379/1
|
|
STRIPE_SECRET_KEY:
|
|
from_secret: stripe_secret_key
|
|
STRIPE_PUBLISHABLE_KEY:
|
|
from_secret: stripe_publishable_key
|
|
PIP_CACHE_DIR: .pip-cache
|
|
commands:
|
|
- pip install -r requirements.dev.txt
|
|
- cd ./src
|
|
# Heavy Selenium room flows — test_game_room_* (deck_contrib,
|
|
# gatekeeper, invite, select_role/sea/sig/sky, tray, tray_tooltip),
|
|
# test_trinket_* (carte_blanche, coin_on_a_string, backstage_pass)
|
|
# since trinket FTs create rooms + load the room template (where
|
|
# the table hex SCSS + chair geometry live), AND test_game_my_sea*
|
|
# (49 my-sea FTs that DRY-reuse the room-shell hex + sea-cross
|
|
# picker — same Selenium surface, so the same parallel-stage
|
|
# contention concerns apply). Runs in parallel w. test-FTs-non-room
|
|
# (distinct DATABASE_URL paths under /tmp; see split-rationale).
|
|
#
|
|
# `_retry_failed.sh` parses Django FAIL:/ERROR: lines from the first
|
|
# run's stdout + re-runs just those labels — single-flake retries
|
|
# cost ~22s instead of the full ~35-min step wall-clock. Genuine
|
|
# regressions still fail (second run output is the authoritative
|
|
# report); first-run crashes w. no parseable labels propagate
|
|
# the original exit code (don't silently mask infra problems).
|
|
- bash ../.woodpecker/_retry_failed.sh --exclude-tag=channels --exclude-tag=two-browser $(ls functional_tests/test_game_room_*.py functional_tests/test_trinket_*.py functional_tests/test_game_my_sea*.py | sed 's|/|.|g;s|\.py||')
|
|
when:
|
|
- event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: screendumps
|
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
|
depends_on:
|
|
- test-FTs-non-room
|
|
- test-FTs-room
|
|
commands:
|
|
- cat ./src/functional_tests/screendumps/*.html || echo "No screendumps found"
|
|
when:
|
|
- event: push
|
|
status: failure
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: build-and-push
|
|
image: docker:cli
|
|
depends_on:
|
|
- test-FTs-non-room
|
|
- test-FTs-room
|
|
environment:
|
|
REGISTRY_PASSWORD:
|
|
from_secret: gitea_registry_password
|
|
commands:
|
|
- echo "$REGISTRY_PASSWORD" | docker login gitea.earthmanrpg.me -u discoman --password-stdin
|
|
- docker build -t gitea.earthmanrpg.me/discoman/gamearray:latest .
|
|
- docker push gitea.earthmanrpg.me/discoman/gamearray:latest
|
|
when:
|
|
- branch: main
|
|
event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- "Dockerfile"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: deploy-staging
|
|
image: alpine
|
|
depends_on:
|
|
- build-and-push
|
|
environment:
|
|
SSH_KEY:
|
|
from_secret: deploy_ssh_key
|
|
commands:
|
|
- apk add --no-cache openssh-client
|
|
- mkdir -p ~/.ssh
|
|
- printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
|
|
- chmod 600 ~/.ssh/id_ed25519
|
|
- ssh -o StrictHostKeyChecking=no discoman@staging.earthmanrpg.me /opt/gamearray/deploy.sh
|
|
when:
|
|
- branch: main
|
|
event: push
|
|
path:
|
|
- "src/**"
|
|
- "requirements.txt"
|
|
- "Dockerfile"
|
|
- "infra/**"
|
|
- ".woodpecker/main.yaml"
|
|
|
|
- name: deploy-prod
|
|
image: alpine
|
|
depends_on:
|
|
- build-and-push
|
|
environment:
|
|
SSH_KEY:
|
|
from_secret: deploy_ssh_key
|
|
commands:
|
|
- apk add --no-cache openssh-client
|
|
- mkdir -p ~/.ssh
|
|
- printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
|
|
- chmod 600 ~/.ssh/id_ed25519
|
|
- ssh -o StrictHostKeyChecking=no discoman@staging.earthmanrpg.me /opt/gamearray/deploy.sh
|
|
when:
|
|
- event: tag
|