diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50bcaee..7f5af7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,19 +78,13 @@ jobs: e2e: runs-on: ubuntu-latest needs: [check] - container: - # Must stay pinned to the installed @playwright/test version, because the - # image carries the matching browser build and Playwright refuses a - # mismatched pair. Microsoft publishes the image a while after the npm - # release, which is why package.json pins 1.61.1 rather than the newer - # 1.62.0: no v1.62.0-noble image exists yet. Bump both together, and check - # the tag is published first: - # curl -sI https://mcr.microsoft.com/v2/playwright/manifests/v1.61.1-noble - image: mcr.microsoft.com/playwright:v1.61.1-noble - # Chromium needs a real /dev/shm in a container. - options: --ipc=host - # No --user override on purpose: the image has no bun, and installing one - # needs root. The runner is ephemeral, so root-owned files do no harm. + # Runs directly on the runner rather than in the Playwright container image. + # Two reasons, both learned the hard way. MinIO cannot be a `services:` entry + # (see the step that starts it below), and starting it as a step needs a + # docker CLI, which a container job does not have. And the Playwright image + # ships neither bun nor unzip, so bun had to be installed through npm and the + # image tag had to be kept in lockstep with the npm package. Installing the + # browser here costs about a minute and removes all of that. services: postgres: image: postgres:16-alpine @@ -98,32 +92,15 @@ jobs: POSTGRES_USER: openframe POSTGRES_PASSWORD: openframe POSTGRES_DB: openframe_test + ports: + - 5432:5432 options: >- --health-cmd "pg_isready -U openframe -d openframe_test" --health-interval 2s --health-timeout 3s --health-retries 30 - # Object storage for video-upload.spec.ts. The browser PUTs the file - # straight at the presigned URL, so there is nothing to mock at that - # boundary from inside a browser. Mirrors the minio-test service in - # docker-compose.test.yml. - minio: - image: minio/minio:latest - env: - MINIO_ROOT_USER: openframe - MINIO_ROOT_PASSWORD: openframe-test-secret - # lib/r2.ts signs with `region: 'auto'`, so MinIO has to accept it. - MINIO_REGION_NAME: auto - options: >- - --health-cmd "mc ready local" - --health-interval 2s - --health-timeout 3s - --health-retries 30 env: - # This job runs inside a container, so it shares a network with its - # services and reaches them by service name. No published ports are - # involved, which is why there are no `ports:` blocks above. - DATABASE_URL: postgresql://openframe:openframe@postgres:5432/openframe_test?schema=public + DATABASE_URL: postgresql://openframe:openframe@localhost:5432/openframe_test?schema=public # The Playwright web server builds and starts the app, and `next build` # does not run with NODE_ENV=test, so it never picks up `.env.test`. These # values are therefore set on the job itself. Port 3100 matches @@ -149,40 +126,60 @@ jobs: OPENFRAME_REQUIRE_INVITE_CODE: 'true' INVITE_CODE: test-invite TRUSTED_PROXY_MODE: none - # Direct video uploads, pointed at the MinIO service. Without these the - # `Direct Upload` tab is not rendered and video-upload.spec.ts fails on its - # first assertion rather than silently testing nothing. + # Direct video uploads, pointed at the MinIO container started below. + # Without these the `Direct Upload` tab is not rendered and + # video-upload.spec.ts fails on its first assertion rather than silently + # testing nothing. The browser PUTs the file straight at the presigned URL, + # so the app and the browser have to agree on this host, and both run on + # the runner. OPENFRAME_ENABLE_S3_VIDEO_UPLOADS: 'true' OPENFRAME_ENABLE_BUNNY_UPLOADS: 'false' - R2_ENDPOINT: http://minio:9000 + R2_ENDPOINT: http://localhost:9000 R2_ACCESS_KEY_ID: openframe R2_SECRET_ACCESS_KEY: openframe-test-secret R2_BUCKET_NAME: openframe-test steps: - uses: actions/checkout@v4 - - name: Install bun - # oven-sh/setup-bun cannot be used inside this container: it unpacks a - # zip archive and the Playwright image ships no unzip. It ships node and - # npm, so npm installs the bun binary instead. bun is needed both for - # `bun run test:e2e` and for the web server command in - # playwright.config.ts. - run: npm install --global bun + - uses: oven-sh/setup-bun@v2 - run: bun install - name: Create .env.test from the committed example run: | cp .env.test.example .env.test printf '\nDATABASE_URL=%s\n' "$DATABASE_URL" >> .env.test + - name: Start MinIO + # Not a `services:` entry, because a service block cannot pass a command + # to the container and the MinIO entrypoint requires `server /data`. + # Without arguments the container prints its usage text, exits, and the + # job dies at "Failed to initialize container minio/minio:latest". + run: | + docker run -d --name minio -p 9000:9000 \ + -e MINIO_ROOT_USER="$R2_ACCESS_KEY_ID" \ + -e MINIO_ROOT_PASSWORD="$R2_SECRET_ACCESS_KEY" \ + -e MINIO_REGION_NAME=auto \ + minio/minio:latest server /data + for _ in $(seq 1 60); do + if curl -sf http://localhost:9000/minio/health/live >/dev/null; then + echo 'minio is ready' + exit 0 + fi + sleep 1 + done + echo 'minio did not become ready within 60 seconds' >&2 + docker logs minio >&2 + exit 1 - name: Create the MinIO bucket # Nothing at runtime creates it: ensureR2BucketExists() lives in # scripts/self-host-bootstrap.ts, not on the request path, so a missing - # bucket would surface as a presigned PUT returning NoSuchBucket. The - # Playwright image has no mc, so this goes through the same image the - # service container uses. + # bucket would surface as a presigned PUT returning NoSuchBucket. run: | - curl -sSfL -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc - chmod +x /usr/local/bin/mc - mc alias set ciminio "$R2_ENDPOINT" "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY" - mc mb --ignore-existing "ciminio/$R2_BUCKET_NAME" + curl -sSfL -o "$RUNNER_TEMP/mc" https://dl.min.io/client/mc/release/linux-amd64/mc + chmod +x "$RUNNER_TEMP/mc" + "$RUNNER_TEMP/mc" alias set ciminio "$R2_ENDPOINT" "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY" + "$RUNNER_TEMP/mc" mb --ignore-existing "ciminio/$R2_BUCKET_NAME" + - name: Install the Playwright browser + # Only chromium: playwright.config.ts runs a desktop Chromium project and + # a Pixel 7 project, and the mobile one is Chromium too. + run: bunx playwright install --with-deps chromium # No `bun run test:db:bootstrap` step: tests/e2e/global-setup.ts calls the # same setup function before the web server starts, and also clears the # rate_limits table so a retry does not inherit a spent window. diff --git a/vitest.config.ts b/vitest.config.ts index f2f66a0..a50f673 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,20 @@ import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [tsconfigPaths()], test: { + server: { + deps: { + // Anything reaching lib/auth.ts pulls in next-auth, whose lib/env.js imports + // the extensionless specifier 'next/server'. Node's ESM resolver cannot + // resolve that, so the module has to go through Vite's resolver instead of + // being externalised. Bun resolves it either way, which is why this is easy + // to miss: the containers used locally have no node at all, so vitest runs + // under bun there, while on a GitHub runner the vitest bin's + // `#!/usr/bin/env node` shebang wins and every suite that touches auth dies + // with ERR_MODULE_NOT_FOUND. Declared at the root so all three projects + // inherit it through `extends: true`. + inline: [/next-auth/], + }, + }, projects: [ { extends: true, @@ -12,16 +26,6 @@ export default defineConfig({ name: 'unit', environment: 'node', include: ['tests/unit/**/*.test.ts'], - server: { - deps: { - // lib/auth.ts pulls in next-auth, whose lib/env.js imports - // 'next/server'. Node's ESM resolver cannot resolve that extensionless - // specifier, so the module has to go through Vite's resolver instead - // of being externalised. Bun resolves it either way; this keeps the - // suite runnable under plain Node too, which is how coverage runs. - inline: [/next-auth/], - }, - }, }, }, {