|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Runs one end-to-end suite group over real HTTP, each against its own `next dev` app. |
| 3 | +# |
| 4 | +# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox> (run from apps/sim) |
| 5 | +# |
| 6 | +# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of |
| 7 | +# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e. |
| 8 | +# |
| 9 | +# The first request cold-compiles the app under Turbopack, which takes 42-150s on CI runners, so |
| 10 | +# the readiness deadline only has to catch a hung boot: an exited server fails immediately, and |
| 11 | +# either way the server log tail lands in the job log. |
| 12 | +# |
| 13 | +# Each app starts from an empty Turbopack dev cache: a cache written under other NEXT_PUBLIC_* |
| 14 | +# values, by a server that `next dev` SIGKILLs 100ms after SIGTERM, can panic Turbopack or wedge a |
| 15 | +# route compile on restore. It runs in its own session under an E2E_APP tag, and stop-session.sh |
| 16 | +# returns only once every process in that session or carrying that tag has exited (Next's |
| 17 | +# telemetry flush runs detached and still writes .next/dev). |
| 18 | +set -euo pipefail |
| 19 | + |
| 20 | +group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox>} |
| 21 | +report_dir="$RUNNER_TEMP/e2e" |
| 22 | +ready_timeout_seconds=300 |
| 23 | +mkdir -p "$report_dir" |
| 24 | + |
| 25 | +server_pid='' |
| 26 | +app_tag='' |
| 27 | +server_log='' |
| 28 | +status_log='' |
| 29 | + |
| 30 | +finish() { |
| 31 | + local status=$? |
| 32 | + if [ -n "$server_pid" ]; then |
| 33 | + bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag" || status=1 |
| 34 | + wait "$server_pid" 2>/dev/null || true |
| 35 | + if [ -n "$status_log" ]; then |
| 36 | + awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$status_log" |
| 37 | + fi |
| 38 | + if [ "$status" -ne 0 ]; then |
| 39 | + tail -n 200 "$server_log" |
| 40 | + fi |
| 41 | + fi |
| 42 | + exit "$status" |
| 43 | +} |
| 44 | +trap finish EXIT |
| 45 | + |
| 46 | +# start_app <name> <port> <label> [record-http-status] |
| 47 | +start_app() { |
| 48 | + local name=$1 port=$2 label=$3 |
| 49 | + server_log="$report_dir/$name-next.log" |
| 50 | + if [ "${4:-}" = record-http-status ]; then |
| 51 | + status_log="$report_dir/$name-http-status.log" |
| 52 | + fi |
| 53 | + app_tag="$name-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$" |
| 54 | + export NEXT_PUBLIC_APP_URL="http://127.0.0.1:$port" |
| 55 | + export BETTER_AUTH_URL="$NEXT_PUBLIC_APP_URL" |
| 56 | + export DISABLE_TELEMETRY=true NEXT_TELEMETRY_DISABLED=1 |
| 57 | + rm -rf .next/dev |
| 58 | + E2E_APP="$app_tag" setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \ |
| 59 | + --port "$port" > "$server_log" 2>&1 & |
| 60 | + server_pid=$! |
| 61 | + |
| 62 | + local started=$SECONDS |
| 63 | + until curl --fail --silent --max-time 10 "$NEXT_PUBLIC_APP_URL/api/health" > /dev/null; do |
| 64 | + if ! kill -0 "$server_pid" 2>/dev/null; then |
| 65 | + echo "::error::Local $label app exited during startup." |
| 66 | + exit 1 |
| 67 | + fi |
| 68 | + if [ $((SECONDS - started)) -ge "$ready_timeout_seconds" ]; then |
| 69 | + echo "::error::Local $label app did not become ready within $ready_timeout_seconds seconds." |
| 70 | + exit 1 |
| 71 | + fi |
| 72 | + sleep 2 |
| 73 | + done |
| 74 | + echo "Local $label app ready after $((SECONDS - started))s" |
| 75 | +} |
| 76 | + |
| 77 | +case "$group" in |
| 78 | + scim) |
| 79 | + export NEXT_PUBLIC_FORCE_HOSTED=true |
| 80 | + export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true |
| 81 | + export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true |
| 82 | + export SCIM_ENABLED=true NEXT_PUBLIC_SCIM_ENABLED=true |
| 83 | + export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true |
| 84 | + export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true |
| 85 | + export INTERNAL_API_SECRET=scim-http-ci-local-secret-at-least-32-characters |
| 86 | + export DB_TX_TRIPWIRE=throw |
| 87 | + export NEXT_PUBLIC_CHAT_DISABLED=true |
| 88 | + start_app scim 3017 SCIM record-http-status |
| 89 | + SCIM_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ |
| 90 | + SCIM_E2E_DATABASE_URL="$DATABASE_URL" \ |
| 91 | + SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ |
| 92 | + SCIM_E2E_REPORT_PATH="$report_dir/scim-e2e-report.json" \ |
| 93 | + bun run test:scim:e2e |
| 94 | + VERSION_COMPARE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ |
| 95 | + VERSION_COMPARE_E2E_DATABASE_URL="$DATABASE_URL" \ |
| 96 | + VERSION_COMPARE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ |
| 97 | + VERSION_COMPARE_E2E_REPORT_PATH="$report_dir/version-compare-http-report.json" \ |
| 98 | + bun run test:workflow-version-compare:e2e |
| 99 | + ;; |
| 100 | + |
| 101 | + # The search suites serve their own fixtures in-process and need no app. They share this group |
| 102 | + # because they finish in seconds. Self-hosted without billing: hosted billing admits runs through |
| 103 | + # Redis, which the CLI app is not given. |
| 104 | + cli) |
| 105 | + for search in google-content lucid zoom google-meet; do |
| 106 | + report_var="SEARCH_$(echo "$search" | tr 'a-z-' 'A-Z_')_REPORT_PATH" |
| 107 | + env NEXT_PUBLIC_APP_URL=http://127.0.0.1:3040 NEXT_PUBLIC_FORCE_HOSTED=false \ |
| 108 | + "$report_var=$report_dir/search-$search.json" \ |
| 109 | + bun "scripts/test-search-$search-e2e.ts" |
| 110 | + done |
| 111 | + export NEXT_PUBLIC_FORCE_HOSTED=false |
| 112 | + export INTERNAL_API_SECRET=cli-http-ci-local-secret-at-least-32-characters |
| 113 | + start_app cli 3018 CLI |
| 114 | + CLI_LATENCY_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ |
| 115 | + CLI_LATENCY_E2E_DATABASE_URL="$DATABASE_URL" \ |
| 116 | + CLI_LATENCY_E2E_RUNS=3 \ |
| 117 | + CLI_LATENCY_E2E_WARMUP=1 \ |
| 118 | + CLI_LATENCY_E2E_REPORT_PATH="$report_dir/cli-run-latency-report.json" \ |
| 119 | + bun run test:cli-run-latency:e2e |
| 120 | + ;; |
| 121 | + |
| 122 | + # Self-hosted: hosted billing admits a run only through a Redis usage reservation. |
| 123 | + stop-after) |
| 124 | + export NEXT_PUBLIC_FORCE_HOSTED=false |
| 125 | + export INTERNAL_API_SECRET=stop-after-http-ci-local-secret-at-least-32-characters |
| 126 | + export DB_TX_TRIPWIRE=throw |
| 127 | + export NEXT_PUBLIC_CHAT_DISABLED=true |
| 128 | + start_app stop-after 3018 workflow record-http-status |
| 129 | + STOP_AFTER_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ |
| 130 | + STOP_AFTER_E2E_DATABASE_URL="$DATABASE_URL" \ |
| 131 | + STOP_AFTER_E2E_REPORT_PATH="$report_dir/stop-after-http-report.json" \ |
| 132 | + bun run test:workflow-stop-after:e2e |
| 133 | + ;; |
| 134 | + |
| 135 | + # The desktop background executor's protocol: device registration, the SSE doorbell over Redis |
| 136 | + # pub/sub, presence, leased claims, Stop and isolation. The only group whose app gets Redis. |
| 137 | + desktop-inbox) |
| 138 | + export REDIS_URL=redis://127.0.0.1:6379 |
| 139 | + export NEXT_PUBLIC_FORCE_HOSTED=false |
| 140 | + export MSHIP_DESKTOP_BACKGROUND_EXECUTOR=true |
| 141 | + export COPILOT_TOOL_PERMISSIONS_ENABLED=true |
| 142 | + export INTERNAL_API_SECRET=desktop-inbox-http-ci-local-secret-at-least-32-characters |
| 143 | + export DB_TX_TRIPWIRE=throw |
| 144 | + start_app desktop-inbox 3019 'desktop executor' record-http-status |
| 145 | + DESKTOP_INBOX_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ |
| 146 | + DESKTOP_INBOX_E2E_DATABASE_URL="$DATABASE_URL" \ |
| 147 | + DESKTOP_INBOX_E2E_REDIS_URL="$REDIS_URL" \ |
| 148 | + DESKTOP_INBOX_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ |
| 149 | + DESKTOP_INBOX_E2E_REPORT_PATH="$report_dir/desktop-inbox-http-report.json" \ |
| 150 | + bun run test:desktop-inbox:e2e |
| 151 | + ;; |
| 152 | + |
| 153 | + *) |
| 154 | + echo "::error::Unknown end-to-end group: $group" >&2 |
| 155 | + exit 2 |
| 156 | + ;; |
| 157 | +esac |
0 commit comments