#!/bin/sh set -eu ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT" "$ROOT/scripts/ensure-local-e2e-env.sh" E2E_ENV=${WNH_E2E_ENV_FILE:-"$ROOT/output/runtime/e2e.env"} run_id="$(date -u +%Y%m%d%H%M%S)-$$" project="who_need_help_e2e_$run_id" output_dir="$ROOT/output/e2e/$run_id" case "$project" in who_need_help_e2e_*) ;; *) echo "Refusing unexpected E2E project name: $project" >&2 exit 1 ;; esac mkdir -p "$output_dir" export TRAEFIK_PROJECT_CONSTRAINT="$project" export TRAEFIK_APP_NAME="who-need-help-e2e-$run_id" export TRAEFIK_DOCKER_NETWORK="${project}_ingress" # Each run owns its image tags. The cleanup trap removes only these exact tags # after their project containers are gone, so parallel or interrupted runs do # not accumulate shared test tags. export APP_IMAGE="who-need-help:e2e-$run_id" export SOCKET_PROXY_IMAGE="who-need-help:socket-proxy-e2e-$run_id" export POSTGIS_IMAGE="who-need-help:postgis-e2e-$run_id" export E2E_TEST_IMAGE="who-need-help-e2e-tests:$run_id" export E2E_OUTPUT_DIR="$output_dir" export E2E_UID E2E_UID=$(id -u) export E2E_GID E2E_GID=$(id -g) export CODEX_SESSION_ID="${CODEX_SESSION_ID:-${CODEX_THREAD_ID:-local-e2e}}" # Keep raster-map traffic inside the isolated application network. MapLibre # performs tile fetches from a worker, so browser-context request interception # is not a reliable network boundary for these requests. export MAP_TILE_URL="/__e2e__/map-tile.png?z={z}&x={x}&y={y}" compose() { docker compose \ --project-name "$project" \ --env-file "$E2E_ENV" \ --file "$ROOT/compose.yaml" \ --file "$ROOT/compose.e2e.yaml" \ "$@" } image_tag_is_referenced() { expected_image=$1 for container_id in $(docker ps -aq --filter "ancestor=$expected_image"); do observed_image=$(docker inspect --format '{{.Config.Image}}' "$container_id") if [ "$observed_image" = "$expected_image" ]; then return 0 fi done return 1 } remove_run_image() { image=$1 if docker image inspect "$image" >/dev/null 2>&1; then if image_tag_is_referenced "$image"; then echo "Refusing to remove referenced E2E image: $image" >&2 cleanup_status=1 else docker image rm "$image" >/dev/null || cleanup_status=$? fi fi } cleanup() { status=$? cleanup_status=0 trap - EXIT HUP INT TERM if [ "$status" -ne 0 ]; then compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true compose logs --no-color >"$output_dir/compose.log" 2>&1 || true fi if [ "${KEEP_E2E_STACK:-0}" = "1" ]; then echo "Keeping isolated E2E project $project for inspection." echo "Artifacts: $output_dir" else compose down --remove-orphans >/dev/null 2>&1 || cleanup_status=$? postgres_volume="${project}_postgres_data" if docker volume inspect "$postgres_volume" >/dev/null 2>&1; then observed_project=$(docker volume inspect --format \ '{{index .Labels "com.docker.compose.project"}}' "$postgres_volume") observed_volume=$(docker volume inspect --format \ '{{index .Labels "com.docker.compose.volume"}}' "$postgres_volume") remaining_refs=$(docker ps -aq --filter "volume=$postgres_volume") if [ "$observed_project" != "$project" ] || \ [ "$observed_volume" != "postgres_data" ] || \ [ -n "$remaining_refs" ]; then echo "Refusing to remove unexpected or referenced E2E volume: $postgres_volume" >&2 cleanup_status=1 else docker volume rm "$postgres_volume" >/dev/null || cleanup_status=$? fi fi remove_run_image "$E2E_TEST_IMAGE" remove_run_image "$APP_IMAGE" remove_run_image "$POSTGIS_IMAGE" remove_run_image "$SOCKET_PROXY_IMAGE" fi if [ "$status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then status=$cleanup_status fi exit "$status" } trap cleanup EXIT HUP INT TERM compose config --quiet compose up -d --build --wait db mailpit migrate proxy web worker compose build e2e compose run --rm --no-deps e2e \ npx playwright test --config=playwright.bootstrap.config.ts admin_email="e2e-admin@example.invalid" encoded_admin_email=$(printf %s "$admin_email" | base64 | tr -d '\n') admin_expression="case Base.decode64!(\"$encoded_admin_email\") |> WhoNeedHelp.Release.bootstrap_admin() do {:ok, admin} -> IO.inspect({:ok, admin.email}); {:error, reason} -> raise \"Admin bootstrap failed: #{inspect(reason)}\" end" web_container=$(compose ps -q web | head -n 1) if [ -z "$web_container" ]; then echo "No E2E web replica was found for administrator bootstrap." >&2 exit 1 fi docker exec "$web_container" /app/bin/who_need_help rpc "$admin_expression" compose run --rm --no-deps e2e npx playwright test "$@" echo "Isolated browser E2E passed." echo "Artifacts: $output_dir"