diff --git a/.gitea/workflows/playwright.yml b/.gitea/workflows/playwright.yml new file mode 100644 index 0000000..bda9934 --- /dev/null +++ b/.gitea/workflows/playwright.yml @@ -0,0 +1,107 @@ +name: Playwright Tests + +on: + push: + branches: + - main + paths: + - backend/** + - frontend/** + - docker/compose*.yml + - .env.example + - .gitea/workflows/playwright.yml + pull_request: + types: + - opened + - synchronize + paths: + - backend/** + - frontend/** + - docker/compose*.yml + - .env.example + - .gitea/workflows/playwright.yml + +env: + # The playwright runner service comes from the dev overlay. It loads the SPA + # from the nginx `frontend` service and reaches the API as http://backend:8000 + # on the project network — both wired up in compose.ci.yml. + COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml:docker/compose.ci.yml + # frontend/playwright.config.ts keys the blob reporter, the retries and + # forbidOnly off CI, and compose.dev.yml forwards it into the container. + CI: "true" + +jobs: + test-playwright: + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + # Two spec files (tests/flows.spec.ts, tests/admin.spec.ts) plus + # tests/auth.setup.ts, which every shard reruns as its setup project. + # More shards than that would only pay container startup twice over. + shardIndex: [1, 2] + shardTotal: [2] + env: + # One stack per shard, so a `down -v` cannot reach the other shard's + # volumes. compose.ci.yml above drops the fixed container_names and host + # ports, which the project name does not cover — without it two shards on + # the same runner host collide on the daemon-global names. + COMPOSE_PROJECT_NAME: fluksio-app-ci-pw-${{ matrix.shardIndex }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Write .env + # Not committed; compose interpolates it and the runner container reads + # FIRST_SUPERUSER / FIRST_SUPERUSER_PASSWORD out of it (tests/config.ts). + run: cp .env.example .env + - run: docker compose build backend frontend playwright + - run: docker compose down -v --remove-orphans + # PLAYWRIGHT_BASE_URL points the browser at `frontend`, which is not a + # dependency of the playwright service, so bring it up explicitly. + - run: docker compose up -d --wait backend frontend + - name: Run Playwright tests + run: >- + docker compose run --rm playwright + bunx playwright test --trace=retain-on-failure + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Tear down + if: always() + run: docker compose down -v --remove-orphans + - name: Upload blob report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: frontend/blob-report + include-hidden-files: true + retention-days: 1 + + merge-reports: + needs: + - test-playwright + # Merge even when a shard failed — that report is the one worth reading. + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Download blob reports + uses: actions/download-artifact@v4 + with: + path: frontend/all-blob-reports + pattern: blob-report-* + merge-multiple: true + - name: Merge into an HTML report + run: bunx playwright merge-reports --reporter html ./all-blob-reports + working-directory: frontend + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: frontend/playwright-report + include-hidden-files: true + retention-days: 7 diff --git a/.gitea/workflows/pre-commit.yml b/.gitea/workflows/pre-commit.yml new file mode 100644 index 0000000..b1b9a14 --- /dev/null +++ b/.gitea/workflows/pre-commit.yml @@ -0,0 +1,53 @@ +name: pre-commit + +# Gitea Actions. `uses:` is resolved against the instance's action mirror, the +# same convention the sibling N3XD repositories use — plain `owner/action@tag`. +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + # Matches backend/Dockerfile, so the gate type-checks and formats against + # the interpreter the image ships. + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + pyproject.toml + backend/pyproject.toml + uv.lock + - name: Write .env + # Not committed, and the generate-frontend-sdk hook imports the backend + # settings, which read ../.env from backend/. + run: cp .env.example .env + # The hooks call `uv run ruff` from the repo root, so the workspace .venv + # (pyproject.toml → [tool.uv.workspace]) is what has to exist. + - name: Install backend dependencies + run: uv sync + - name: Install frontend dependencies + run: bun install --frozen-lockfile + # The formatting hooks rewrite files; pre-commit exits non-zero when they + # do, which fails the run. No auto-commit branch here: that needs a push + # token, and pre-commit-ci/lite-action talks to a GitHub-only service. + - name: Run pre-commit + run: uvx pre-commit run --all-files --show-diff-on-failure + # The hooks cover ruff and biome but not mypy, which `make lint-backend` + # runs and which is strict for everything outside the flow prototypes. + - name: Typecheck backend + run: uv run mypy app + working-directory: backend diff --git a/.gitea/workflows/test-backend.yml b/.gitea/workflows/test-backend.yml new file mode 100644 index 0000000..0aafc34 --- /dev/null +++ b/.gitea/workflows/test-backend.yml @@ -0,0 +1,58 @@ +name: Test Backend + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +env: + # The compose files live in docker/, so a bare `docker compose` from the repo + # root finds nothing. With COMPOSE_FILE set, compose reads .env from the + # working directory — the same file the backend settings load as ../.env. + # The CI-only project name keeps the `down -v` steps away from a dev stack. + COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml + COMPOSE_PROJECT_NAME: fluksio-app-ci + +jobs: + test-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + pyproject.toml + backend/pyproject.toml + uv.lock + - name: Write .env + # Not committed, and both compose and the backend settings need it. + # POSTGRES_SERVER=localhost there matches the port db publishes in the + # dev overlay, so the suite runs on the runner against the container. + run: cp .env.example .env + - run: docker compose down -v --remove-orphans + # Postgres is the only service the suite needs: the mail paths are + # patched in tests/, and the flow engine falls back to in-memory state + # while REDIS_HOST is empty. + - run: docker compose up -d --wait db + - run: uv sync + - name: Migrate DB and seed the superuser + run: uv run bash scripts/prestart.sh + working-directory: backend + # scripts/tests-start.sh waits for the DB, then runs pytest under + # coverage and prints the report (see backend/scripts/test.sh). + - name: Run tests with coverage + run: uv run bash scripts/tests-start.sh "Coverage for ${{ github.sha }}" + working-directory: backend + - name: Tear down + if: always() + run: docker compose down -v --remove-orphans diff --git a/.gitea/workflows/test-compose.yml b/.gitea/workflows/test-compose.yml new file mode 100644 index 0000000..4bb3761 --- /dev/null +++ b/.gitea/workflows/test-compose.yml @@ -0,0 +1,63 @@ +name: Compose Smoke Test + +# Lives in this repository rather than the workspace root: the stack it starts +# is entirely app-local (docker/compose*.yml + .env.example). A root workflow +# would additionally need a token to check out the private submodules and would +# have to run scripts/setup.sh to generate secrets, for the same coverage. +on: + push: + branches: + - main + paths: + - backend/** + - frontend/** + - docker/compose*.yml + - .env.example + - .gitea/workflows/test-compose.yml + pull_request: + types: + - opened + - synchronize + paths: + - backend/** + - frontend/** + - docker/compose*.yml + - .env.example + - .gitea/workflows/test-compose.yml + +env: + # compose.ci.yml drops the fixed container_names and host ports so this job + # can share a runner host with the Playwright shards. + COMPOSE_FILE: docker/compose.yml:docker/compose.dev.yml:docker/compose.ci.yml + # Own project name so this job's `down -v` cannot reach another stack. + COMPOSE_PROJECT_NAME: fluksio-app-ci-compose + +jobs: + test-compose: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Write .env + run: cp .env.example .env + # The Playwright job runs the SPA off a Vite server inside its own + # container, so the nginx image is only ever built there. This job is what + # covers it — the SPA fallback in frontend/nginx.conf is the part that + # breaks silently. + - run: docker compose build backend frontend + - run: docker compose down -v --remove-orphans + # db and prestart come along as depends_on; --wait blocks on the backend + # healthcheck in docker/compose.yml. + - run: docker compose up -d --wait backend frontend + # Nothing is published on the host (compose.ci.yml), so the checks run + # from inside the backend container against the project network. + - name: Backend is up + run: docker compose exec -T backend curl -f http://localhost:8000/api/v1/utils/health-check/ + - name: Frontend is up + run: docker compose exec -T backend curl -f http://frontend/ + - name: An authenticated route serves the SPA shell + run: docker compose exec -T backend curl -f http://frontend/flows + - name: Tear down + if: always() + run: docker compose down -v --remove-orphans diff --git a/docker/compose.ci.yml b/docker/compose.ci.yml new file mode 100644 index 0000000..541d3c4 --- /dev/null +++ b/docker/compose.ci.yml @@ -0,0 +1,61 @@ +# CI isolation overrides — layered on top of compose.yml + compose.dev.yml by +# the Playwright shards and the compose smoke job in .gitea/workflows/. +# +# A self-hosted runner can put several jobs on the same host. A per-job +# COMPOSE_PROJECT_NAME keeps their containers, networks and volumes apart, but +# it does not cover the two things that are global to the Docker daemon: +# +# 1. `container_name` ignores the project name, so a second job fails with +# "the container name /fluksio-db is already in use". +# 2. Published host ports are first come, first served. +# +# Neither is needed in CI: everything reaches everything else by compose +# service name on the project network. +services: + + proxy: + ports: !reset [] + + db: + container_name: !reset null + ports: !reset [] + + redis: + container_name: !reset null + + adminer: + container_name: !reset null + ports: !reset [] + + mailcatcher: + ports: !reset [] + + prestart: + container_name: !reset null + + backend: + container_name: !reset null + ports: !reset [] + + frontend: + container_name: !reset null + ports: !reset [] + # Baked at build time. compose.dev.yml bakes http://localhost:8000, which + # resolves to the playwright container itself; the browser has to reach the + # API by service name on the project network instead. + build: + args: + - VITE_API_URL=http://backend:8000 + # Image tags are daemon-global too, and this one no longer holds the same + # bytes as the smoke job's: without a per-job tag the two jobs overwrite + # each other's `fluksio-frontend:latest` and the SPA calls the wrong API. + image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${COMPOSE_PROJECT_NAME:-ci}' + + playwright: + ports: !reset [] + # frontend/playwright.config.ts defaults baseURL to http://app.localhost, + # which is right for a local run against the integrated stack but resolves + # to 127.0.0.1 in here. In CI the nginx `frontend` service serves the SPA. + # This origin must stay in BACKEND_CORS_ORIGINS (.env.example). + environment: + - PLAYWRIGHT_BASE_URL=http://frontend