From a30f0118dbec5c525d0ddf7f28fc05fb7a37bbbc Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Sat, 30 May 2026 12:32:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(ci):=20split=20smoke=20gate=20=E2=80=94=20r?= =?UTF-8?q?equired=20curl=20smoke=20+=20advisory=20Playwright?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The combined smoke gate was the chronic CI pain point: - Boots compose + runs the full Playwright Chromium suite in one job. - 20-min timeout — slow signal on legitimate failures. - Playwright surface (browser timing, animation, transient network) flakes occasionally; flake = blocked PR until rerun. - Sometimes the runner gets stuck pre-start and GitHub's job timeout doesn't apply; the run hangs indefinitely until manually cancelled. Happened to run 26681455502 today. Split into two workflows so each part can have the cost/value contract that suits it: - `infra-compose-full-stack-smoke.yml` (REQUIRED) — curl-only register → login → /me → register-new → dashboard round-trip. Timeout 8 min. The curl path completes in ~3–4 min on warm runners, so an 8-min ceiling fails fast instead of hanging for 20. - `infra-compose-playwright-e2e.yml` (ADVISORY) — boots the same stack, runs the apps/ui Playwright Chromium suite. NOT a required check; a Playwright flake no longer blocks merges. Failure means "go look at the run", not "rerun and hope". The OpenAPI schema check that was bundled into smoke is dropped — `apps-api-openapi-drift.yml` already runs the same check as a dedicated required workflow. Both workflows keep the same paths-filter triggers; smoke also keeps the no-paths `pull_request:` from PR #59 so it reports a status on docs PRs via the internal skip-with-success pattern. Operator follow-up (manual, in branch protection settings): - The Playwright job name "UI Playwright E2E (browser-based smoke)" will appear in the required-check picker once it runs once. Leave it OFF — that's the whole point of the split. --- .../infra-compose-full-stack-smoke.yml | 57 ++----- .../infra-compose-playwright-e2e.yml | 151 ++++++++++++++++++ 2 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/infra-compose-playwright-e2e.yml diff --git a/.github/workflows/infra-compose-full-stack-smoke.yml b/.github/workflows/infra-compose-full-stack-smoke.yml index b93e6655..b5e2a421 100644 --- a/.github/workflows/infra-compose-full-stack-smoke.yml +++ b/.github/workflows/infra-compose-full-stack-smoke.yml @@ -1,9 +1,14 @@ name: full-stack-smoke -# Boots the entire dev stack (postgres + valkey + api-dev + ui-dev + traefik) -# via docker compose and exercises the register → login → /me happy path. -# Requires monorepo apps/api and apps/ui (compose build contexts). -# Triggered manually or on relevant pushes; slower than validate-compose. +# Boots the dev compose stack and exercises the login → /me → register → +# dashboard round-trip through curl. Catches real integration breakage +# (compose service wiring, Vite/nginx → API proxy, Postgres + Valkey +# readiness, auth cookie path) on every PR within a few minutes. +# +# Deliberately curl-only — the browser E2E surface lives in a separate +# advisory workflow (`infra-compose-playwright-e2e.yml`) so a Playwright +# flake or browser quirk doesn't block PR merges through this required +# check. on: push: @@ -35,7 +40,11 @@ jobs: smoke: name: register → login → /me through the full stack runs-on: ubuntu-latest - timeout-minutes: 20 + # 8 min upper bound: with Playwright moved out, the curl-only path + # completes in ~3–4 min on a warm runner. A short ceiling means a + # stuck step fails fast instead of running for 20 min like the old + # combined gate. + timeout-minutes: 8 steps: - name: Checkout monorepo @@ -163,33 +172,6 @@ jobs: exit 1 } - - name: Set up Bun - if: steps.filter.outputs.code == 'true' - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.3.14 - - - name: Check UI OpenAPI schema is up to date - if: steps.filter.outputs.code == 'true' - working-directory: apps/ui - run: | - # The ui-dev container runs as root and writes into the bind-mounted - # apps/ui node_modules from the container, so the host runner can't recreate it. - # Drop root-owned bits before installing as the runner user. - sudo rm -rf node_modules - bun install --frozen-lockfile - bun run generate:api:check - - - name: Run UI Playwright E2E - if: steps.filter.outputs.code == 'true' - working-directory: apps/ui - timeout-minutes: 12 - env: - PLAYWRIGHT_REUSE_SERVER: "true" - run: | - bun run e2e:install:chromium - bun run e2e:ci:chromium - - name: Smoke — register a brand-new user if: steps.filter.outputs.code == 'true' run: | @@ -228,17 +210,6 @@ jobs: docker logs "$c" 2>&1 || true done - - name: Upload Playwright artifacts on failure - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: playwright-results - path: | - apps/ui/test-results - apps/ui/playwright-report - if-no-files-found: ignore - retention-days: 7 - - name: Tear down if: always() working-directory: infra/compose/compose diff --git a/.github/workflows/infra-compose-playwright-e2e.yml b/.github/workflows/infra-compose-playwright-e2e.yml new file mode 100644 index 00000000..f6b8168d --- /dev/null +++ b/.github/workflows/infra-compose-playwright-e2e.yml @@ -0,0 +1,151 @@ +name: playwright-e2e + +# Browser-based end-to-end smoke. Boots the same compose stack as +# `full-stack-smoke`, then runs the apps/ui Playwright Chromium suite +# against it. +# +# NOT a required check in branch protection — intentionally advisory. +# The Playwright surface flakes occasionally (browser timing, animation, +# transient network) and forcing every PR to wait on a re-run is more +# friction than the signal is worth. The fast curl smoke +# (`full-stack-smoke`) gates merges; this workflow catches the +# regressions curl can't see, and a failure here means "go look". +# +# Path-filtered so docs-only PRs don't waste runner time. + +on: + push: + branches: [main] + paths: + - "infra/compose/**" + - "apps/api/**" + - "apps/ui/**" + - ".github/workflows/infra-compose-playwright-e2e.yml" + pull_request: + paths: + - "infra/compose/**" + - "apps/api/**" + - "apps/ui/**" + - ".github/workflows/infra-compose-playwright-e2e.yml" + workflow_dispatch: + +concurrency: + group: infra-compose-playwright-e2e-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + playwright: + name: UI Playwright E2E (browser-based smoke) + runs-on: ubuntu-latest + # 15 min upper bound: Playwright's own inner timeout is 12 min; the + # buffer covers compose build + image pulls + browser install. A + # genuine hang surfaces within minutes of the inner timeout rather + # than the runner's default 6 h. + timeout-minutes: 15 + + steps: + - name: Checkout monorepo + uses: actions/checkout@v6 + + - name: Seed compose/.env + working-directory: infra/compose/compose + run: | + cp .env.example .env + { + echo "" + echo "# Playwright E2E only — never use these in real deployments." + echo "SUPERUSER_EMAIL=demo@example.com" + echo "SUPERUSER_PASSWORD=password123" + } >> .env + + - name: Make scripts executable + run: | + chmod +x infra/compose/compose/dev.sh + chmod +x infra/compose/scripts/*.sh + + - name: Boot the smoke stack + working-directory: infra/compose/compose + env: + STACK: smoke + run: ./dev.sh up -d --build + + - name: Wait for API health + run: | + for i in {1..60}; do + if curl -fsS http://localhost:7330/health > /dev/null 2>&1; then + echo "API healthy after ${i}s" + break + fi + sleep 1 + done + curl -fsS http://localhost:7330/health + + - name: Wait for UI + run: | + for i in {1..60}; do + if curl -fsS http://localhost:7331/ > /dev/null 2>&1; then + echo "UI ready after ${i}s" + break + fi + sleep 1 + done + curl -fsSI http://localhost:7331/ | head -1 + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.14 + + - name: Install apps/ui deps + working-directory: apps/ui + run: | + # The ui-dev container runs as root and writes into the bind-mounted + # apps/ui node_modules from the container, so the host runner can't + # recreate it. Drop root-owned bits before installing as the runner user. + sudo rm -rf node_modules + bun install --frozen-lockfile + + - name: Run UI Playwright E2E + working-directory: apps/ui + timeout-minutes: 12 + env: + PLAYWRIGHT_REUSE_SERVER: "true" + run: | + bun run e2e:install:chromium + bun run e2e:ci:chromium + + - name: Compose ps on failure + if: failure() + working-directory: infra/compose/compose + run: | + docker compose -f docker-compose.yml -f docker-compose.development-labels.yml --profile smoke ps + + - name: Container logs on failure + if: failure() + run: | + for c in $(docker ps -aq); do + echo "===== $(docker inspect --format '{{.Name}}' "$c") =====" + docker logs "$c" 2>&1 || true + done + + - name: Upload Playwright artifacts on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-results + path: | + apps/ui/test-results + apps/ui/playwright-report + if-no-files-found: ignore + retention-days: 7 + + - name: Tear down + if: always() + working-directory: infra/compose/compose + env: + STACK: smoke + run: | + ./dev.sh down -v || true