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