diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index f149a29..c65a3af 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -40,7 +40,7 @@ jobs: name: Run trivy scan (frontend) uses: numerique-gouv/action-trivy-cache@main with: - docker-build-args: '--target frontend-production -f Dockerfile' + docker-build-args: '--target frontend-production -f src/frontend/Dockerfile' docker-image-name: 'docker.io/lasuite/people-frontend:${{ github.sha }}' build-and-push-backend: @@ -105,6 +105,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + file: ./src/frontend/Dockerfile target: frontend-production build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 3f174a4..fcec617 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -167,6 +167,8 @@ jobs: COMPOSE_DOCKER_CLI_BUILD: 1 run: | docker compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1 + make update-keycloak-realm-app + make add-dev-rsa-private-key-to-env make run - name: Apply DRF migrations @@ -177,10 +179,6 @@ jobs: run: | make demo FLUSH_ARGS='--no-input' - - name: Setup Dimail DB - run: | - make dimail-setup-db - - name: Run e2e tests run: cd src/frontend/ && yarn e2e:test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 685dc61..a1972de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to - ✨(resource-server) add SCIM /Me endpoint #895 +### Changed + +- 🧑‍💻(docker) split frontend to another file #924 + ## [1.17.0] - 2025-06-11 ### Added diff --git a/Dockerfile b/Dockerfile index e755fcc..08137d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,61 +10,6 @@ RUN python -m pip install --upgrade pip setuptools RUN apk update && \ apk upgrade -### ---- Front-end dependencies image ---- -FROM node:20 AS frontend-deps - -WORKDIR /deps - -COPY ./src/frontend/package.json ./package.json -COPY ./src/frontend/yarn.lock ./yarn.lock -COPY ./src/frontend/apps/desk/package.json ./apps/desk/package.json -COPY ./src/frontend/packages/i18n/package.json ./packages/i18n/package.json -COPY ./src/frontend/packages/eslint-config-people/package.json ./packages/eslint-config-people/package.json - -RUN yarn --frozen-lockfile - -### ---- Front-end builder dev image ---- -FROM node:20 AS frontend-builder-dev - -WORKDIR /builder - -COPY --from=frontend-deps /deps/node_modules ./node_modules -COPY ./src/frontend . - -WORKDIR ./apps/desk - -### ---- Front-end builder image ---- -FROM frontend-builder-dev AS frontend-builder - -RUN yarn build - -# ---- Front-end image ---- -FROM nginxinc/nginx-unprivileged:1.27-alpine AS frontend-production - -USER root - -RUN apk update && apk upgrade libssl3 libcrypto3 libxml2 - -USER nginx - -# Un-privileged user running the application -ARG DOCKER_USER -USER ${DOCKER_USER} - -COPY --from=frontend-builder \ - /builder/apps/desk/out \ - /usr/share/nginx/html - -COPY ./src/frontend/apps/desk/conf/default.conf /etc/nginx/conf.d - -# Copy entrypoint -COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint - -ENTRYPOINT [ "/usr/local/bin/entrypoint" ] - -CMD ["nginx", "-g", "daemon off;"] - - # ---- Back-end builder image ---- FROM base AS back-builder diff --git a/Makefile b/Makefile index 16f6435..9059650 100644 --- a/Makefile +++ b/Makefile @@ -41,11 +41,9 @@ DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID) COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose COMPOSE_EXEC = $(COMPOSE) exec COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev -COMPOSE_RUN = $(COMPOSE) run --rm +COMPOSE_RUN = $(COMPOSE) run --rm --no-deps COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin -WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s -WAIT_KC_DB = $(COMPOSE_RUN) dockerize -wait tcp://kc_postgresql:5432 -timeout 60s # -- Backend MANAGE = $(COMPOSE_RUN_APP) python manage.py @@ -78,19 +76,33 @@ create-env-files: \ env.d/development/kc_postgresql .PHONY: create-env-files +add-dev-rsa-private-key-to-env: ## Add a generated RSA private key to the env file + @echo "Generating RSA private key PEM for development..." + @mkdir -p env.d/development/rsa + @openssl genrsa -out env.d/development/rsa/private.pem 2048 + @echo -n "\nOAUTH2_PROVIDER_OIDC_RSA_PRIVATE_KEY=\"" >> env.d/development/common + @openssl rsa -in env.d/development/rsa/private.pem -outform PEM >> env.d/development/common + @echo "\"" >> env.d/development/common + @rm -rf env.d/development/rsa +.PHONY: add-dev-rsa-private-key-to-env + +update-keycloak-realm-app: ## Create the Keycloak realm for the project + @echo "$(BOLD)Creating Keycloak realm for 'app'$(RESET)" + @sed -i 's|http://app-dev:8000|http://app:8000|g' ./docker/auth/realm.json +.PHONY: update-keycloak-realm-app + bootstrap: ## Prepare Docker images for the project and install frontend dependencies bootstrap: \ data/media \ data/static \ create-env-files \ build \ - run \ + run-dev \ migrate \ back-i18n-compile \ mails-install \ mails-build \ - dimail-setup-db \ - install-front-desk + dimail-setup-db .PHONY: bootstrap # -- Docker/compose @@ -106,19 +118,14 @@ logs: ## display app-dev logs (follow mode) @$(COMPOSE) logs -f app-dev .PHONY: logs -run: ## start the wsgi (production) and development server - @$(COMPOSE) up --force-recreate -d nginx - @$(COMPOSE) up --force-recreate -d app-dev - @$(COMPOSE) up --force-recreate -d celery-dev - @$(COMPOSE) up --force-recreate -d celery-beat-dev - @$(COMPOSE) up --force-recreate -d flower-dev - @$(COMPOSE) up --force-recreate -d keycloak - @$(COMPOSE) up -d dimail - @echo "Wait for postgresql to be up..." - @$(WAIT_KC_DB) - @$(WAIT_DB) +run: ## start the wsgi (production) and servers with production Docker images + @$(COMPOSE) up --force-recreate --detach app frontend celery celery-beat nginx maildev .PHONY: run +run-dev: ## start the servers in development mode (watch) Docker images + @$(COMPOSE) up --force-recreate --detach app-dev frontend-dev celery-dev celery-beat-dev nginx maildev +.PHONY: run-dev + status: ## an alias for "docker compose ps" @$(COMPOSE) ps .PHONY: status @@ -187,22 +194,16 @@ test-coverage: ## compute, display and save test coverage makemigrations: ## run django makemigrations for the people project. @echo "$(BOLD)Running makemigrations$(RESET)" - @$(COMPOSE) up -d postgresql - @$(WAIT_DB) @$(MANAGE) makemigrations $(ARGS) .PHONY: makemigrations migrate: ## run django migrations for the people project. @echo "$(BOLD)Running migrations$(RESET)" - @$(COMPOSE) up -d postgresql - @$(WAIT_DB) @$(MANAGE) migrate $(ARGS) .PHONY: migrate showmigrations: ## run django showmigrations for the people project. @echo "$(BOLD)Running showmigrations$(RESET)" - @$(COMPOSE) up -d postgresql - @$(WAIT_DB) @$(MANAGE) showmigrations $(ARGS) .PHONY: showmigrations diff --git a/README.md b/README.md index 290e50a..6a71b1d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The easiest way to start working on the project is to use GNU Make: $ make bootstrap ``` -This command builds the `app` container, installs dependencies, performs +This command builds the `app-dev` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues. @@ -46,6 +46,12 @@ Note that if you need to run them afterward, you can use the eponym Make rule: $ make run ``` +or if you want to run them in development mode (with live reloading): + +```bash +$ make run-dev +``` + You can check all available Make rules using: ```bash diff --git a/bin/_config.sh b/bin/_config.sh index 73ceb05..d03974d 100644 --- a/bin/_config.sh +++ b/bin/_config.sh @@ -64,6 +64,23 @@ function _dc_run() { _docker_compose run --rm $user_args "$@" } +# _dc_run_no_deps: wrap docker compose run command without dependencies +# +# usage: _dc_run_no_deps [options] [ARGS...] +# +# options: docker compose run command options +# ARGS : docker compose run command arguments +function _dc_run_no_deps() { + _set_user + + user_args="--user=$USER_ID" + if [ -z $USER_ID ]; then + user_args="" + fi + + _docker_compose run --no-deps --rm $user_args "$@" +} + # _dc_exec: wrap docker compose exec command # # usage: _dc_exec [options] [ARGS...] diff --git a/bin/pylint b/bin/pylint index 8053c7c..2c9adf8 100755 --- a/bin/pylint +++ b/bin/pylint @@ -35,4 +35,4 @@ fi # Fix docker vs local path when project sources are mounted as a volume read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")" -_dc_run app-dev pylint "${paths[@]}" "${args[@]}" +_dc_run_no_deps app-dev pylint "${paths[@]}" "${args[@]}" diff --git a/docker-compose.yml b/docker-compose.yml index 9e7bf9e..64a8ee6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,11 @@ services: - env.d/development/postgresql ports: - "15432:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] + interval: 1s + timeout: 2s + retries: 300 redis: image: redis:5 @@ -23,6 +28,7 @@ services: DOCKER_USER: ${DOCKER_USER:-1000} user: ${DOCKER_USER:-1000} image: people:backend-development + pull_policy: never environment: - PYLINTHOME=/app/.pylint.d - DJANGO_CONFIGURATION=Development @@ -37,11 +43,69 @@ services: - ./data/media:/data/media - ./data/static:/data/static depends_on: - - dimail - - postgresql - - maildev - - redis - + postgresql: + condition: service_healthy + restart: true + dimail: + condition: service_started + maildev: + condition: service_started + redis: + condition: service_started + + frontend-dev: + user: "${DOCKER_USER:-1000}" + build: + context: . + dockerfile: ./src/frontend/Dockerfile + target: frontend-dev + image: people:frontend-development + pull_policy: never + volumes: + - ./src/frontend:/home/frontend + - /home/frontend/node_modules + - /home/frontend/apps/desk/node_modules + ports: + - "3000:3000" + + app: + build: + context: . + target: backend-production + args: + DOCKER_USER: ${DOCKER_USER:-1000} + user: ${DOCKER_USER:-1000} + image: people:backend-production + environment: + - DJANGO_CONFIGURATION=Development + env_file: + - env.d/development/common + - env.d/development/france + - env.d/development/postgresql + ports: + - "8071:8000" + volumes: + - ./data/media:/data/media + depends_on: + postgresql: + condition: service_healthy + restart: true + redis: + condition: service_started + + frontend: + user: "${DOCKER_USER:-1000}" + build: + context: . + dockerfile: ./src/frontend/Dockerfile + target: frontend-production + args: + API_ORIGIN: "http://localhost:8071" + image: people:frontend-production + pull_policy: build + ports: + - "3000:3000" + celery-dev: user: ${DOCKER_USER:-1000} image: people:backend-development @@ -56,7 +120,11 @@ services: - ./data/media:/data/media - ./data/static:/data/static depends_on: - - app-dev + postgresql: + condition: service_healthy + restart: true + app-dev: + condition: service_started celery-beat-dev: user: ${DOCKER_USER:-1000} @@ -72,26 +140,9 @@ services: - ./data/media:/data/media - ./data/static:/data/static depends_on: - - app-dev - - app: - build: - context: . - target: backend-production - args: - DOCKER_USER: ${DOCKER_USER:-1000} - user: ${DOCKER_USER:-1000} - image: people:backend-production - environment: - - DJANGO_CONFIGURATION=Demo - env_file: - - env.d/development/common - - env.d/development/postgresql - volumes: - - ./data/media:/data/media - depends_on: - - postgresql - - redis + postgresql: + condition: service_healthy + restart: true celery: user: ${DOCKER_USER:-1000} @@ -103,7 +154,29 @@ services: - env.d/development/common - env.d/development/postgresql depends_on: - - app + postgresql: + condition: service_healthy + restart: true + app: + condition: service_started + + celery-beat: + user: ${DOCKER_USER:-1000} + image: people:backend-production + command: ["celery", "-A", "people.celery_app", "beat", "-l", "INFO"] + environment: + - DJANGO_CONFIGURATION=Demo + env_file: + - env.d/development/common + - env.d/development/postgresql + volumes: + - ./src/backend:/app + - ./data/media:/data/media + - ./data/static:/data/static + depends_on: + postgresql: + condition: service_healthy + restart: true nginx: image: nginx:1.25 @@ -112,12 +185,9 @@ services: volumes: - ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - - app - - keycloak - - dockerize: - image: jwilder/dockerize - platform: linux/x86_64 + keycloak: + condition: service_healthy + restart: true crowdin: image: crowdin/cli:4.6.1 @@ -137,11 +207,16 @@ services: - ".:/app" kc_postgresql: - image: postgres:14.3 - ports: - - "5433:5432" - env_file: - - env.d/development/kc_postgresql + image: postgres:14.3 + ports: + - "5433:5432" + env_file: + - env.d/development/kc_postgresql + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ] + interval: 1s + timeout: 2s + retries: 300 keycloak: image: quay.io/keycloak/keycloak:20.0.1 @@ -158,6 +233,8 @@ services: - --hostname-admin-url=http://localhost:8083/ - --hostname-strict=false - --hostname-strict-https=false + - --health-enabled=true + - --metrics-enabled=true environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin @@ -168,10 +245,17 @@ services: KC_DB_USERNAME: people KC_DB_SCHEMA: public PROXY_ADDRESS_FORWARDING: 'true' + healthcheck: + test: [ "CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready" ] + interval: 1s + timeout: 2s + retries: 300 ports: - "8080:8080" depends_on: - - kc_postgresql + kc_postgresql: + condition: service_healthy + restart: true dimail: entrypoint: /opt/dimail-api/start-dev.sh diff --git a/docker/auth/realm.json b/docker/auth/realm.json index 4af531b..4edb493 100644 --- a/docker/auth/realm.json +++ b/docker/auth/realm.json @@ -26,7 +26,7 @@ "oauth2DeviceCodeLifespan": 600, "oauth2DevicePollingInterval": 5, "enabled": true, - "sslRequired": "external", + "sslRequired": "none", "registrationAllowed": true, "registrationEmailAsUsername": false, "rememberMe": true, diff --git a/env.d/development/common.dist b/env.d/development/common.dist index dbaeb91..6e013c8 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -3,6 +3,7 @@ DJANGO_ALLOWED_HOSTS=* DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly DJANGO_SETTINGS_MODULE=people.settings DJANGO_SUPERUSER_PASSWORD=admin +DJANGO_CORS_ALLOWED_ORIGINS=http://localhost:3000 # Python PYTHONPATH=/app diff --git a/env.d/development/common.e2e.dist b/env.d/development/common.e2e.dist index 5cb7075..5aa81a0 100644 --- a/env.d/development/common.e2e.dist +++ b/env.d/development/common.e2e.dist @@ -2,7 +2,9 @@ SUSTAINED_THROTTLE_RATES="200/hour" BURST_THROTTLE_RATES="200/minute" -OAUTH2_PROVIDER_OIDC_ENABLED = True -OAUTH2_PROVIDER_VALIDATOR_CLASS: "mailbox_oauth2.validators.ProConnectValidator" +OIDC_ORGANIZATION_REGISTRATION_ID_FIELD="siret" + +OAUTH2_PROVIDER_OIDC_ENABLED=True +OAUTH2_PROVIDER_VALIDATOR_CLASS="mailbox_oauth2.validators.ProConnectValidator" INSTALLED_PLUGINS=plugins.la_suite diff --git a/src/backend/locale/en_US/LC_MESSAGES/django.mo b/src/backend/locale/en_US/LC_MESSAGES/django.mo index f874a49..19c4905 100644 Binary files a/src/backend/locale/en_US/LC_MESSAGES/django.mo and b/src/backend/locale/en_US/LC_MESSAGES/django.mo differ diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.mo b/src/backend/locale/fr_FR/LC_MESSAGES/django.mo index c345ffe..db9947f 100644 Binary files a/src/backend/locale/fr_FR/LC_MESSAGES/django.mo and b/src/backend/locale/fr_FR/LC_MESSAGES/django.mo differ diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index a53eaa1..42b4bb9 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "django-configurations==2.5.1", "django-cors-headers==4.7.0", "django-countries==7.6.1", + "django-extensions==4.1", "django-lasuite==0.0.9", "django-oauth-toolkit==3.0.1", "django-parler==2.3", @@ -70,7 +71,6 @@ dependencies = [ [project.optional-dependencies] dev = [ - "django-extensions==4.1", "drf-spectacular-sidecar==2025.6.1", "ipdb==0.13.13", "ipython==9.3.0", diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile new file mode 100644 index 0000000..f596706 --- /dev/null +++ b/src/frontend/Dockerfile @@ -0,0 +1,63 @@ +FROM node:22-alpine AS frontend-deps + +# Upgrade system packages to install security updates +RUN apk update && \ + apk upgrade && \ + rm -rf /var/cache/apk/* + +WORKDIR /home/frontend/ + +COPY ./src/frontend/package.json ./package.json +COPY ./src/frontend/yarn.lock ./yarn.lock +COPY ./src/frontend/apps/desk/package.json ./apps/desk/package.json +COPY ./src/frontend/packages/i18n/package.json ./packages/i18n/package.json +COPY ./src/frontend/packages/eslint-config-people/package.json ./packages/eslint-config-people/package.json + +RUN yarn install --frozen-lockfile + +COPY .dockerignore ./.dockerignore +COPY ./src/frontend/.prettierrc.js ./.prettierrc.js +COPY ./src/frontend/packages/eslint-config-people ./packages/eslint-config-people +COPY ./src/frontend/apps/desk ./apps/desk + +### ---- Front-end builder image ---- +FROM frontend-deps AS frontend-base + +WORKDIR /home/frontend/apps/desk + +### ---- Front-end builder dev image ---- +FROM frontend-deps AS frontend-dev + +WORKDIR /home/frontend/apps/desk + +EXPOSE 3000 + +CMD [ "yarn", "dev"] + + +FROM frontend-base AS frontend-builder + +WORKDIR /home/frontend/apps/desk + +ARG API_ORIGIN +ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN} + +RUN yarn build + +# ---- Front-end image ---- +FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production + +# Un-privileged user running the application +ARG DOCKER_USER +USER ${DOCKER_USER} + +COPY --from=frontend-builder \ + /home/frontend/apps/desk/out \ + /usr/share/nginx/html + +COPY ./src/frontend/apps/desk/conf/default.conf /etc/nginx/conf.d +COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint + +ENTRYPOINT [ "/usr/local/bin/entrypoint" ] + +CMD ["nginx", "-g", "daemon off;"] diff --git a/src/frontend/apps/desk/conf/default.conf b/src/frontend/apps/desk/conf/default.conf index f620d50..4e6764a 100644 --- a/src/frontend/apps/desk/conf/default.conf +++ b/src/frontend/apps/desk/conf/default.conf @@ -1,4 +1,5 @@ server { + listen 3000; listen 8080; server_name localhost; server_tokens off; diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts index 8ecfede..7d7408f 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain.spec.ts @@ -97,6 +97,7 @@ test.describe('Mail domain', () => { }); await page.goto('/mail-domains/unknown-domain.fr'); + await page.waitForURL('/404/'); await expect( page.getByText( 'It seems that the page you are looking for does not exist or cannot be displayed correctly.', diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts index 97cb12a..f505ede 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts @@ -101,6 +101,8 @@ test.describe('Add Mail Domains', () => { test('checks 404 on mail-domains/[slug] page', async ({ page }) => { await page.goto('/mail-domains/unknown-domain'); + await page.waitForURL('/404/'); + await expect( page.getByText( 'It seems that the page you are looking for does not exist or cannot be displayed correctly.', diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts index bc799d5..80fd73d 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts @@ -83,6 +83,7 @@ test.describe('Teams Create', () => { test('checks 404 on teams/[id] page', async ({ page }) => { await page.goto('/teams/some-unknown-team'); + await page.waitForURL('/404/'); await expect( page.getByText( 'It seems that the page you are looking for does not exist or cannot be displayed correctly.', diff --git a/src/frontend/apps/e2e/playwright.config.ts b/src/frontend/apps/e2e/playwright.config.ts index 82732f8..6d1e7c0 100644 --- a/src/frontend/apps/e2e/playwright.config.ts +++ b/src/frontend/apps/e2e/playwright.config.ts @@ -32,12 +32,10 @@ export default defineConfig({ }, webServer: { - command: `cd ../.. && yarn app:${ - process.env.CI ? 'start -p ' : 'dev --port ' - } ${PORT}`, + command: !process.env.CI ? `cd ../.. && yarn app:dev --port ${PORT}` : '', url: baseURL, timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, + reuseExistingServer: true, }, /* Configure projects for major browsers */