# Baron SSO용 Docker Compose 헬퍼 # 환경 변수 로드 ifneq (,$(wildcard ./.env)) include .env export endif # Compose 파일 경로 COMPOSE_INFRA := compose.infra.yaml COMPOSE_ORY := compose.ory.yaml COMPOSE_APP := docker-compose.yaml AUTH_CONFIG_ENV := config/.generated/auth-config.env DEV_SERVICES ?= backend adminfront devfront orgfront userfront DEV_NETWORKS := baron_net ory-net hydranet kratosnet public_net INFRA_CONTAINERS := baron_postgres baron_clickhouse baron_redis baron_gateway ORY_CONTAINERS := ory_postgres ory_kratos ory_hydra ory_keto ory_oathkeeper ory_clickhouse ory_vector APP_CONTAINERS := baron_backend baron_adminfront baron_devfront baron_orgfront baron_userfront DROP_CONTAINERS := $(INFRA_CONTAINERS) $(ORY_CONTAINERS) $(APP_CONTAINERS) ory_stack_check COMPOSE_CLI_ENV_ARGS := ifneq (,$(wildcard ./.env)) COMPOSE_CLI_ENV_ARGS += --env-file .env endif COMPOSE_CLI_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV) COMPOSE_DROP_ENV_ARGS := ifneq (,$(wildcard ./.env)) COMPOSE_DROP_ENV_ARGS += --env-file .env endif DUMP_SERVICES ?= all RESTORE_SERVICES ?= all DUMP_MODE ?= maintenance BACKUP_USE_DOCKER ?= true BACKUP_TOOLS_IMAGE ?= baron-sso-backup-tools:local BACKUP_TOOLS_DOCKERFILE ?= docker/backup-tools/Dockerfile BACKUP_DOCKER_ENV_ARGS := ifneq (,$(wildcard ./.env)) BACKUP_DOCKER_ENV_ARGS += --env-file .env endif ifneq (,$(wildcard ./$(AUTH_CONFIG_ENV))) BACKUP_DOCKER_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV) endif BACKUP_DOCKER_RUN = docker run --rm $(BACKUP_DOCKER_ENV_ARGS) -e BACKUP_REPO_ROOT=/workspace -v /var/run/docker.sock:/var/run/docker.sock -v "$(CURDIR)":/workspace -v /tmp:/tmp -w /workspace $(BACKUP_TOOLS_IMAGE) .PHONY: build-auth-config validate-auth-config verify-auth-config render-ory-config up up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev dev-debug down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app backup-tools-build dump restore dump-verify restore-verify dump-list restore-plan upload-cloud dump-upload-cloud # --- 인증 설정 빌드/검증 --- build-auth-config: @echo "Building auth config..." @mkdir -p config/.generated @bash scripts/auth_config.sh build validate-auth-config: build-auth-config @echo "Validating auth config..." @bash scripts/auth_config.sh validate verify-auth-config: validate-auth-config @echo "Verifying auth config wiring..." @bash scripts/auth_config.sh verify render-ory-config: validate-auth-config @echo "Rendering Ory config..." @bash scripts/render_ory_config.sh # --- 기본 실행 --- # 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음) up: up-all up-all: ensure-networks render-ory-config @echo "Starting ALL stacks (infra + ory + app)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up --build -d docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) restart kratos # --- 개별 스택 실행 --- up-infra: ensure-networks @echo "Starting Infra stack (postgres/clickhouse/redis)..." docker compose -f $(COMPOSE_INFRA) up -d up-ory: ensure-networks render-ory-config @echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos up-app: ensure-networks render-ory-config @echo "Starting App stack (backend/userfront/adminfront/devfront/orgfront)..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build -d up-backend: ensure-networks render-ory-config @echo "Starting Backend only..." docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build -d backend ensure-networks: @echo "Ensuring Docker networks..." @for network in $(DEV_NETWORKS); do \ if ! docker network inspect "$$network" >/dev/null 2>&1; then \ echo "Creating Docker network $$network..."; \ docker network create "$$network"; \ else \ echo "Docker network $$network already exists."; \ fi; \ done ensure-infra: ensure-networks @echo "Ensuring Infra stack..." @missing=0; \ for container in $(INFRA_CONTAINERS); do \ if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \ missing=1; \ break; \ fi; \ done; \ if [ "$$missing" -eq 1 ]; then \ echo "Starting missing Infra stack containers in daemon mode..."; \ docker compose -f $(COMPOSE_INFRA) up -d; \ else \ echo "Infra stack is already running."; \ fi ensure-ory: ensure-networks render-ory-config @echo "Ensuring Ory stack..." @missing=0; \ for container in $(ORY_CONTAINERS); do \ if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \ missing=1; \ break; \ fi; \ done; \ if [ "$$missing" -eq 1 ]; then \ echo "Starting missing Ory stack containers in daemon mode..."; \ docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d; \ else \ echo "Ory stack is already running. Restarting Kratos to apply rendered dev config..."; \ docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) restart kratos; \ fi up-dev: ensure-infra ensure-ory @echo "Dev stack is up (infra + ory)." up-front-dev: up-infra up-ory up-backend @echo "Dev stack is up (infra + ory + backend)." dev: up-dev @echo "Starting development app containers in foreground attach mode..." BACKEND_LOG_LEVEL=info CLIENT_LOG_DEBUG=false VITE_CLIENT_LOG_DEBUG=false docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES) dev-debug: up-dev @echo "Starting development app containers in foreground attach debug mode..." BACKEND_LOG_LEVEL=debug CLIENT_LOG_DEBUG=true VITE_CLIENT_LOG_DEBUG=true USERFRONT_FLUTTER_RUN_FLAGS=--debug docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up --build $(DEV_SERVICES) # --- 종료 (Down) --- down: @echo "Stopping ALL stacks (infra + ory + app)..." docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down drop: @echo "Dropping Baron SSO local Docker stack containers, volumes, and local images..." -docker compose $(COMPOSE_DROP_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down -v --rmi local @echo "Removing any remaining fixed-name Baron SSO containers..." @for container in $(DROP_CONTAINERS); do \ docker rm -f "$$container" >/dev/null 2>&1 || true; \ done @echo "Drop complete. External Docker networks are preserved." down-app: @echo "Stopping App stack..." docker compose -f $(COMPOSE_APP) down down-backend: @echo "Stopping Backend only..." docker compose -f $(COMPOSE_APP) stop backend down-infra: @echo "Stopping Infra stack..." docker compose -f $(COMPOSE_INFRA) down down-ory: @echo "Stopping Ory stack..." docker compose -f $(COMPOSE_ORY) down # --- 유틸리티 --- # 인프라 상태 확인 check-infra: @echo "Checking infra status..." @if [ "$$(docker inspect -f '{{.State.Health.Status}}' baron_postgres 2>/dev/null)" != "healthy" ]; then \ echo "Error: PostgreSQL is not running or not healthy."; \ echo "Please run 'make up-infra' first."; \ exit 1; \ else \ echo "PostgreSQL is healthy."; \ fi ps: docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) ps logs-infra: docker compose -f $(COMPOSE_INFRA) logs -f logs-ory: docker compose -f $(COMPOSE_ORY) logs -f logs-app: docker compose -f $(COMPOSE_APP) logs -f # --- 백업/복구 --- backup-tools-build: docker build -f $(BACKUP_TOOLS_DOCKERFILE) -t $(BACKUP_TOOLS_IMAGE) . ifeq ($(BACKUP_USE_DOCKER),true) dump: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'DUMP_SERVICES="$(DUMP_SERVICES)" DUMP_MODE="$(DUMP_MODE)" BACKUP="$(BACKUP)" BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump.sh' restore: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh' dump-verify: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" scripts/backup/verify-dump.sh' restore-verify: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" scripts/backup/verify-restore.sh' dump-list: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump-list.sh' restore-plan: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh' upload-cloud: backup-tools-build $(BACKUP_DOCKER_RUN) bash -lc 'WORKS_DRIVE_DRY_RUN="$(WORKS_DRIVE_DRY_RUN)" BACKUP="$(BACKUP)" scripts/backup/upload_cloud.sh' else dump: DUMP_SERVICES="$(DUMP_SERVICES)" DUMP_MODE="$(DUMP_MODE)" BACKUP="$(BACKUP)" BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump.sh restore: BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" ALLOW_NON_EMPTY_RESTORE="$(ALLOW_NON_EMPTY_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore.sh dump-verify: BACKUP="$(BACKUP)" scripts/backup/verify-dump.sh restore-verify: BACKUP="$(BACKUP)" scripts/backup/verify-restore.sh dump-list: BACKUP_ROOT="$(BACKUP_ROOT)" scripts/backup/dump-list.sh restore-plan: BACKUP="$(BACKUP)" DUMP_FILE="$(DUMP_FILE)" RESTORE_SERVICES="$(RESTORE_SERVICES)" CONFIRM_RESTORE="$(CONFIRM_RESTORE)" RESTORE_REPORT="$(RESTORE_REPORT)" scripts/backup/restore-plan.sh upload-cloud: WORKS_DRIVE_DRY_RUN="$(WORKS_DRIVE_DRY_RUN)" BACKUP="$(BACKUP)" scripts/backup/upload_cloud.sh endif dump-upload-cloud: dump upload-cloud # --- 로컬 통합 코드 체크 --- PLAYWRIGHT_BROWSERS_PATH := $(HOME)/.cache/ms-playwright PLAYWRIGHT_CHROMIUM_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/chromium-1208/INSTALLATION_COMPLETE PLAYWRIGHT_FIREFOX_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/firefox-1509/INSTALLATION_COMPLETE PLAYWRIGHT_WEBKIT_COMPLETE := $(PLAYWRIGHT_BROWSERS_PATH)/webkit-2248/INSTALLATION_COMPLETE PLAYWRIGHT_INSTALL_ALL := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_FIREFOX_COMPLETE)" ] && [ -f "$(PLAYWRIGHT_WEBKIT_COMPLETE)" ]; then echo "Playwright browsers already installed"; else npx playwright install; fi' PLAYWRIGHT_INSTALL_CHROMIUM := sh -c 'if [ -f "$(PLAYWRIGHT_CHROMIUM_COMPLETE)" ]; then echo "Playwright chromium already installed"; else npx playwright install chromium; fi' .PHONY: code-check code-check-lint code-check-test-jobs code-check-i18n code-check-i18n-values code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-front-lint code-check-backend-tests code-check-userfront-tests code-check-adminfront-tests code-check-devfront-tests code-check-orgfront-tests code-check-userfront-e2e-tests CODE_CHECK_TEST_JOBS ?= 1 PLAYWRIGHT_WORKERS ?= 1 FLUTTER_TEST_CONCURRENCY ?= 1 code-check: code-check-lint code-check-test-jobs @echo "code-check complete." code-check-lint: code-check-i18n code-check-i18n-values code-check-front-lint code-check-go-lint code-check-sync-userfront-locales code-check-userfront-install code-check-userfront-lint code-check-test-jobs: @echo "==> run CI-equivalent test jobs (parallel)" @$(MAKE) --no-print-directory -j$(CODE_CHECK_TEST_JOBS) --output-sync=target \ code-check-backend-tests \ code-check-userfront-tests \ code-check-userfront-e2e-tests \ code-check-adminfront-tests \ code-check-devfront-tests \ code-check-orgfront-tests code-check-i18n: @echo "==> i18n resource check" @mkdir -p reports node tools/i18n-scanner/index.js node tools/i18n-scanner/report.js @cat reports/i18n-report.txt code-check-i18n-values: @echo "==> i18n value quality check" @mkdir -p reports node tools/i18n-scanner/value-check.js @cat reports/i18n-value-report.txt code-check-go-lint: @echo "==> go lint/format check" @if command -v golangci-lint >/dev/null 2>&1; then \ cd backend && golangci-lint fmt -E gofmt -E gofumpt -d; \ elif command -v docker >/dev/null 2>&1; then \ docker run --rm \ -v "$$(pwd)/backend:/app" \ -w /app \ golangci/golangci-lint:v2.10.1 \ golangci-lint fmt -E gofmt -E gofumpt -d; \ else \ echo "ERROR: golangci-lint not found and docker is unavailable."; \ echo "Install golangci-lint v2.10.1 or Docker to match CI lint step."; \ exit 1; \ fi code-check-sync-userfront-locales: @echo "==> sync userfront locales" /bin/sh ./scripts/sync_userfront_locales.sh code-check-userfront-install: @echo "==> install userfront dependencies" @if command -v flutter >/dev/null 2>&1; then \ cd userfront && flutter pub get; \ else \ echo "WARNING: flutter not found, skipping userfront dependencies install."; \ fi code-check-userfront-lint: @echo "==> userfront format/analyze" @if command -v dart >/dev/null 2>&1; then \ cd userfront && dart format --output=none --set-exit-if-changed lib test; \ else \ echo "WARNING: dart not found, skipping userfront format check."; \ fi @if command -v flutter >/dev/null 2>&1; then \ cd userfront && flutter analyze --no-fatal-warnings --no-fatal-infos; \ else \ echo "WARNING: flutter not found, skipping userfront analyze."; \ fi code-check-front-lint: @echo "==> adminfront biome lint/format check" rm -rf adminfront/playwright-report adminfront/test-results @if [ -d adminfront/node_modules ]; then \ echo "adminfront/node_modules already present; skipping pnpm install."; \ else \ cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts; \ fi cd adminfront && npx biome lint . cd adminfront && npx biome format . @echo "==> devfront biome lint/format check" rm -rf devfront/playwright-report devfront/test-results @if [ -d devfront/node_modules ]; then \ echo "devfront/node_modules already present; skipping npm install."; \ else \ cd devfront && npm ci --ignore-scripts; \ fi cd devfront && npx biome lint . cd devfront && npx biome format . @echo "==> orgfront biome lint/format check" rm -rf orgfront/playwright-report orgfront/test-results @if [ -d orgfront/node_modules ]; then \ echo "orgfront/node_modules already present; skipping npm install."; \ else \ cd orgfront && npm ci --ignore-scripts; \ fi cd orgfront && ./node_modules/@biomejs/biome/bin/biome lint . cd orgfront && ./node_modules/@biomejs/biome/bin/biome format . code-check-backend-tests: @echo "==> backend tests" cd backend && GOCACHE=/tmp/baron-sso-go-cache go test -v ./... code-check-userfront-tests: @echo "==> userfront tests (isolated workspace)" @if ! command -v flutter >/dev/null 2>&1; then \ echo "WARNING: flutter not found, skipping userfront tests."; \ exit 0; \ fi; \ tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-tests.XXXXXX)"; \ trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \ mkdir -p "$$tmp_dir/scripts"; \ cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \ cp -R locales "$$tmp_dir/locales"; \ if command -v rsync >/dev/null 2>&1; then \ rsync -a --delete \ --exclude '.dart_tool' \ --exclude 'build' \ --exclude '.pub-cache' \ --exclude '.flutter-plugins' \ --exclude '.flutter-plugins-dependencies' \ userfront/ "$$tmp_dir/userfront/"; \ else \ cp -R userfront "$$tmp_dir/userfront"; \ rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \ fi; \ cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh; \ cd "$$tmp_dir/userfront" && flutter test --concurrency=$(FLUTTER_TEST_CONCURRENCY) code-check-adminfront-tests: @echo "==> adminfront tests" PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) ./scripts/run_adminfront_ci_tests.sh adminfront-tests code-check-devfront-tests: @echo "==> devfront tests" @mkdir -p reports/devfront @rm -rf reports/devfront/playwright-report reports/devfront/test-results @status=0; \ preview_pattern='[v]ite preview --host 127.0.0.1 --strictPort --port 4174'; \ pkill -f "$$preview_pattern" >/dev/null 2>&1 || true; \ trap 'pkill -f "$$preview_pattern" >/dev/null 2>&1 || true' EXIT INT TERM; \ if [ -d devfront/node_modules ]; then \ echo "devfront/node_modules already present; skipping npm install."; \ else \ (cd devfront && npm ci --ignore-scripts) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ (cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ (cd devfront && PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \ fi; \ [ -d devfront/playwright-report ] && cp -R devfront/playwright-report reports/devfront/ || true; \ [ -d devfront/test-results ] && cp -R devfront/test-results reports/devfront/ || true; \ exit $$status code-check-orgfront-tests: @echo "==> orgfront tests" @mkdir -p reports/orgfront @rm -rf reports/orgfront/playwright-report reports/orgfront/test-results @status=0; \ (cd orgfront && npm ci --ignore-scripts) || status=$$?; \ if [ $$status -eq 0 ]; then \ (cd orgfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ (cd orgfront && PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \ fi; \ [ -d orgfront/playwright-report ] && cp -R orgfront/playwright-report reports/orgfront/ || true; \ [ -d orgfront/test-results ] && cp -R orgfront/test-results reports/orgfront/ || true; \ exit $$status code-check-userfront-e2e-tests: @echo "==> userfront wasm playwright e2e tests (isolated workspace)" @if ! command -v flutter >/dev/null 2>&1; then \ echo "WARNING: flutter not found, skipping userfront e2e tests."; \ exit 0; \ fi; \ mkdir -p reports/userfront-e2e; \ rm -rf reports/userfront-e2e/playwright-report reports/userfront-e2e/test-results; \ tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-e2e-tests.XXXXXX)"; \ trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \ mkdir -p "$$tmp_dir/scripts"; \ cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \ cp -R locales "$$tmp_dir/locales"; \ if command -v rsync >/dev/null 2>&1; then \ rsync -a --delete \ --exclude '.dart_tool' \ --exclude 'build' \ --exclude '.pub-cache' \ --exclude '.flutter-plugins' \ --exclude '.flutter-plugins-dependencies' \ userfront/ "$$tmp_dir/userfront/"; \ rsync -a --delete \ --exclude 'node_modules' \ --exclude 'playwright-report' \ --exclude 'test-results' \ userfront-e2e/ "$$tmp_dir/userfront-e2e/"; \ else \ cp -R userfront "$$tmp_dir/userfront"; \ rm -rf "$$tmp_dir/userfront/.dart_tool" "$$tmp_dir/userfront/build"; \ cp -R userfront-e2e "$$tmp_dir/userfront-e2e"; \ rm -rf "$$tmp_dir/userfront-e2e/node_modules" "$$tmp_dir/userfront-e2e/playwright-report" "$$tmp_dir/userfront-e2e/test-results"; \ fi; \ status=0; \ (cd "$$tmp_dir" && /bin/sh ./scripts/sync_userfront_locales.sh) || status=$$?; \ if [ $$status -eq 0 ]; then \ (cd "$$tmp_dir/userfront-e2e" && npm ci) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ (cd "$$tmp_dir/userfront" && flutter build web --wasm --release) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ (cd "$$tmp_dir/userfront-e2e" && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \ fi; \ if [ $$status -eq 0 ]; then \ port="$$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"; \ echo "==> userfront-e2e using PORT=$$port"; \ (cd "$$tmp_dir/userfront-e2e" && PORT=$$port PLAYWRIGHT_WORKERS=$(PLAYWRIGHT_WORKERS) npm test) || status=$$?; \ fi; \ [ -d "$$tmp_dir/userfront-e2e/playwright-report" ] && cp -R "$$tmp_dir/userfront-e2e/playwright-report" reports/userfront-e2e/ || true; \ [ -d "$$tmp_dir/userfront-e2e/test-results" ] && cp -R "$$tmp_dir/userfront-e2e/test-results" reports/userfront-e2e/ || true; \ exit $$status