From 1f1016a775bbd068db9a943a8f1bcbba46206aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20M=C3=B6rling?= Date: Tue, 19 May 2026 18:07:12 +0200 Subject: [PATCH] feat: add isolated E2E browser test pipeline for Forgejo Actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement per-job Docker-in-Docker (DinD) for E2E tests, giving each job a completely isolated Docker daemon and network. This prevents leakage to the host Docker or other containers. The previous E2E approach failed because: 1. The Forgejo runner's container.docker_host was not set, causing the runner itself to try unix:///var/run/docker.sock and crash-loop. 2. The host DinD daemon had isolated networking — job containers running docker compose could not resolve 'dind' hostname or access host filesystem bind mounts (e.g. .:/app). New approach — zero bind mounts, all COPY-based images: - docker/backend.e2e.Dockerfile: multi-stage build from repo root. Copies gradlew + settings.gradle + backend/build.gradle to download dependencies in a cacheable layer, then copies backend/src and builds the bootJar. Runs the JAR directly on startup. - docker/frontend.e2e.Dockerfile: multi-stage Node build → nginx. Reuses existing docker/nginx.conf for /api proxy to backend service. No volume mounts, fully self-contained. - docker/playwright.e2e.Dockerfile: extends official Playwright image. Installs deps from package-lock.json, copies e2e tests + config. - docker-compose.e2e.yml: zero bind mounts. Services depend on each other in order: postgres (healthy) → backend → frontend → playwright. Playwright waits for backend and frontend via curl loops before running tests. - .forgejo/workflows/ci.yml: E2E job adds a 'dind' service container (docker:28-dind, privileged, no TLS). The job sets DOCKER_HOST to tcp://dind:2375 so the docker CLI inside the job talks to the per-job DinD daemon. The compose file is docker-compose.e2e.yml. - Runner fix on tocke: added container.docker_host: 'tcp://dind:2375' to runner-config.yaml so the runner's own Docker client connects to the host DinD container, stopping the crash loop. Key properties: - Network isolation: each E2E job gets its own DinD with its own container network. No host container visibility. - No bind mount leakage: all images use COPY instead of volume mounts. The per-job DinD has its own filesystem and can't see host paths. - Deterministic: builds start from clean state every time. Image cache exists only within the per-job DinD lifetime. - Lint-and-test job is untouched and remains green. --- .forgejo/workflows/ci.yml | 9 ++++- docker-compose.e2e.yml | 64 ++++++++++++++++++++++++++++++++ docker/backend.e2e.Dockerfile | 10 +++++ docker/frontend.e2e.Dockerfile | 12 ++++++ docker/playwright.e2e.Dockerfile | 7 ++++ 5 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 docker-compose.e2e.yml create mode 100644 docker/backend.e2e.Dockerfile create mode 100644 docker/frontend.e2e.Dockerfile create mode 100644 docker/playwright.e2e.Dockerfile diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 654107d..24a6157 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -58,7 +58,14 @@ jobs: e2e: name: E2E browser tests runs-on: ubuntu-latest + services: + dind: + image: docker:28-dind + options: --privileged + env: + DOCKER_TLS_CERTDIR: "" env: + DOCKER_HOST: tcp://dind:2375 POSTGRES_DB: bilhej POSTGRES_USER: bilhej POSTGRES_PASSWORD: test_pw_ci_123 @@ -77,5 +84,5 @@ jobs: - name: Run E2E test stack run: | docker compose \ - -f docker-compose.ci.yml \ + -f docker-compose.e2e.yml \ up --build --abort-on-container-exit --exit-code-from playwright diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 0000000..6d23789 --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,64 @@ +services: + postgres: + image: postgres:16 + container_name: bilhej-postgres-e2e + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + backend: + build: + dockerfile: docker/backend.e2e.Dockerfile + context: . + container_name: bilhej-backend-e2e + environment: + SPRING_PROFILES_ACTIVE: docker + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + JWT_SECRET: ${JWT_SECRET} + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} + STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} + STRIPE_PRICE_ID: ${STRIPE_PRICE_ID} + depends_on: + postgres: + condition: service_healthy + + frontend: + build: + dockerfile: docker/frontend.e2e.Dockerfile + context: . + container_name: bilhej-frontend-e2e + depends_on: + - backend + + playwright: + build: + dockerfile: docker/playwright.e2e.Dockerfile + context: . + container_name: bilhej-playwright-e2e + ipc: host + environment: + PLAYWRIGHT_BASE_URL: http://frontend + depends_on: + - frontend + command: >- + sh -c " + echo 'Waiting for backend...'; + for i in \$(seq 1 60); do + curl -s http://backend:8080/api/vehicles/ZZZ999 > /dev/null && break; + sleep 1; + done; + echo 'Waiting for frontend...'; + for i in \$(seq 1 30); do + curl -s http://frontend > /dev/null && break; + sleep 1; + done; + npx playwright test --reporter=list + " diff --git a/docker/backend.e2e.Dockerfile b/docker/backend.e2e.Dockerfile new file mode 100644 index 0000000..bd4720f --- /dev/null +++ b/docker/backend.e2e.Dockerfile @@ -0,0 +1,10 @@ +FROM eclipse-temurin:21-jdk +WORKDIR /app +COPY gradlew settings.gradle ./ +COPY gradle/wrapper/ gradle/wrapper/ +COPY backend/build.gradle backend/ +RUN chmod +x gradlew && ./gradlew :backend:dependencies --no-daemon -q +COPY backend/src backend/src +RUN ./gradlew :backend:bootJar --no-daemon -q +EXPOSE 8080 +CMD ["sh", "-c", "java -jar backend/build/libs/*-SNAPSHOT.jar"] diff --git a/docker/frontend.e2e.Dockerfile b/docker/frontend.e2e.Dockerfile new file mode 100644 index 0000000..0230ab6 --- /dev/null +++ b/docker/frontend.e2e.Dockerfile @@ -0,0 +1,12 @@ +FROM node:24-alpine AS builder +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ . +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/playwright.e2e.Dockerfile b/docker/playwright.e2e.Dockerfile new file mode 100644 index 0000000..3ebf571 --- /dev/null +++ b/docker/playwright.e2e.Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/playwright:v1.60.0-noble +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/playwright.config.ts ./ +COPY frontend/e2e ./e2e +CMD ["sh", "-c", "npx playwright test --reporter=list"]