Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
900 lines
43 KiB
YAML
900 lines
43 KiB
YAML
name: E2E Fresh Install Tests
|
||
|
||
on:
|
||
workflow_dispatch: # Allows manual triggering
|
||
pull_request:
|
||
branches:
|
||
- '**'
|
||
# paths filter removed to always trigger
|
||
|
||
push:
|
||
branches:
|
||
- main
|
||
# paths filter removed to always trigger
|
||
|
||
jobs:
|
||
fresh-install-e2e:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 60 # Set a timeout for the job
|
||
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0 # Fetches all history for all branches and tags, required for tj-actions/changed-files
|
||
|
||
#--------------------------------------------------------------
|
||
# Detect whether the proposed changes require running the
|
||
# (slow) E2E installation tests. Skip if only non-application
|
||
# files changed (e.g., Helm charts, docs, scripts).
|
||
#--------------------------------------------------------------
|
||
- name: Check for relevant file changes
|
||
id: changed_files_check
|
||
uses: tj-actions/changed-files@v46 # Updated from v42 to fix CVE-2025-30066
|
||
with:
|
||
# List of paths that, when modified, SHOULD trigger the heavy
|
||
# E2E job. Paths NOT listed here (helm/, docs/, scripts/, sdk/)
|
||
# will not trigger the tests.
|
||
files: |
|
||
.github/workflows/e2e-fresh-install-tests.yaml
|
||
docker-compose.base.yaml
|
||
docker-compose.ce.yaml
|
||
docker-compose.prod.yaml
|
||
Dockerfile
|
||
Dockerfile.build
|
||
server/**
|
||
setup/**
|
||
shared/**
|
||
packages/**
|
||
services/**
|
||
ee/**
|
||
e2e-tests/**
|
||
package.json
|
||
package-lock.json
|
||
tsconfig.base.json
|
||
|
||
# Short-circuit the job if no relevant files changed
|
||
- name: Skip job -- no application files changed
|
||
if: steps.changed_files_check.outputs.any_changed == 'false'
|
||
run: |
|
||
echo "No application files were modified – skipping E2E Fresh Install Tests."
|
||
echo "No further steps will be executed."
|
||
# Nothing else to do, the job will report success because the
|
||
# previous steps (including this one) have succeeded.
|
||
|
||
|
||
- name: Set up test environment secrets and .env
|
||
if: steps.changed_files_check.outputs.any_changed == 'true'
|
||
run: |
|
||
# Create secrets directory
|
||
mkdir -p secrets
|
||
|
||
# Create sample secrets (placeholders, real values not needed for this test if services are self-contained)
|
||
# Using similar placeholders as pr-checks.yaml for consistency
|
||
# Use printf instead of echo to avoid trailing newlines
|
||
printf "placeholder-password" > secrets/postgres_password
|
||
printf "placeholder-password" > secrets/db_password_server
|
||
printf "placeholder-password" > secrets/db_password_hocuspocus
|
||
printf "placeholder-password" > secrets/redis_password
|
||
printf "placeholder-key-32-chars-long-01" > secrets/alga_auth_key
|
||
printf "placeholder-key-32-chars-long-02" > secrets/crypto_key
|
||
printf "placeholder-key-32-chars-long-03" > secrets/token_secret_key
|
||
printf "placeholder-key-32-chars-long-04" > secrets/nextauth_secret
|
||
printf "placeholder-password" > secrets/email_password
|
||
printf "placeholder-id" > secrets/google_oauth_client_id
|
||
printf "placeholder-secret" > secrets/google_oauth_client_secret
|
||
|
||
# Set permissions
|
||
chmod 600 secrets/*
|
||
|
||
# Copy and configure environment file
|
||
cp .env.example .env
|
||
|
||
# Configure required environment variables for the test
|
||
# Set APP_ENV to production for production build testing
|
||
cat >> .env << EOL
|
||
APP_VERSION=1.0.0-e2e
|
||
APP_NAME=alga-e2e-test
|
||
APP_ENV=production
|
||
APP_HOST=0.0.0.0
|
||
APP_PORT=3000
|
||
APP_EDITION=community
|
||
|
||
# Database Configuration (will be overridden by docker-compose services but good to have)
|
||
DB_TYPE=postgres
|
||
DB_USER_ADMIN=postgres
|
||
|
||
# Logging Configuration
|
||
LOG_LEVEL=INFO
|
||
LOG_IS_FORMAT_JSON=false
|
||
LOG_IS_FULL_DETAILS=false
|
||
|
||
# Email Configuration (disabled for tests)
|
||
EMAIL_ENABLE=false
|
||
|
||
# Authentication Configuration
|
||
NEXTAUTH_URL=http://localhost:3000
|
||
NEXTAUTH_SECRET=placeholder-key-32-chars-long-04
|
||
NEXTAUTH_SESSION_EXPIRES=86400
|
||
AUTH_TRUST_HOST=true
|
||
|
||
# Optional Configuration
|
||
REQUIRE_HOCUSPOCUS=false
|
||
|
||
# Secret Provider Configuration (override production defaults)
|
||
SECRET_READ_CHAIN=env,filesystem
|
||
SECRET_WRITE_PROVIDER=filesystem
|
||
EOL
|
||
shell: bash
|
||
|
||
- name: Temporarily rename root docker-compose.yaml to avoid conflict in act
|
||
if: env.ACT
|
||
run: |
|
||
if [ -f docker-compose.yaml ]; then
|
||
echo "Temporarily renaming root docker-compose.yaml to docker-compose.yaml.ignored"
|
||
sudo mv docker-compose.yaml docker-compose.yaml.ignored
|
||
fi
|
||
shell: bash
|
||
|
||
- name: Install Docker Compose v1.29.2 via curl
|
||
if: steps.changed_files_check.outputs.any_changed == 'true'
|
||
id: install_docker_compose # Add an ID for dependent steps
|
||
run: |
|
||
COMPOSE_VERSION="v2.36.0"
|
||
COMPOSE_URL="https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)"
|
||
DEST_PATH="/usr/local/bin/docker-compose" # Standard location
|
||
|
||
echo "Downloading Docker Compose from ${COMPOSE_URL} to ${DEST_PATH}"
|
||
sudo rm -f "${DEST_PATH}" # Remove existing to avoid conflicts
|
||
# Use curl with -fS (fail silently on server errors, show client errors) and -L (follow redirects)
|
||
sudo curl -fSL "${COMPOSE_URL}" -o "${DEST_PATH}"
|
||
|
||
# Verify download was successful and file is not empty and is executable
|
||
if [ ! -s "${DEST_PATH}" ]; then
|
||
echo "Error: Downloaded docker-compose is empty. URL ${COMPOSE_URL} might be incorrect or file not found."
|
||
exit 1
|
||
fi
|
||
if ! file "${DEST_PATH}" | grep -q "executable"; then
|
||
echo "Error: Downloaded file at ${DEST_PATH} is not an executable. It might be an HTML error page."
|
||
echo "Downloaded content (first 5 lines):"
|
||
sudo head -n 5 "${DEST_PATH}"
|
||
exit 1
|
||
fi
|
||
|
||
sudo chmod +x "${DEST_PATH}"
|
||
echo "Docker Compose version:"
|
||
docker-compose --version # Verify installation
|
||
shell: bash
|
||
|
||
- name: Free up disk space
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Freeing up disk space before build..."
|
||
df -h
|
||
# Remove large pre-installed toolchains this job never uses. Each is
|
||
# guarded with `|| true` so a missing path can't fail the step (the
|
||
# shell runs with `set -e`). The previous run started with only ~19G
|
||
# free and the four --no-cache image builds exhausted it; this frees
|
||
# ~15-20G more on the root volume.
|
||
sudo rm -rf /usr/share/dotnet || true
|
||
sudo rm -rf /usr/local/lib/android || true
|
||
sudo rm -rf /opt/ghc /usr/local/.ghcup || true
|
||
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
|
||
sudo rm -rf /usr/local/share/boost || true
|
||
sudo rm -rf /usr/share/swift || true
|
||
# Drop apt caches.
|
||
sudo apt-get clean || true
|
||
# Reclaim all Docker space (images, build cache, volumes).
|
||
docker system prune -a -f --volumes || true
|
||
echo "Disk space after cleanup:"
|
||
df -h
|
||
shell: bash
|
||
|
||
- name: Build images with no cache
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Building all required images with --no-cache to ensure fresh build"
|
||
# Build all images needed for e2e tests upfront
|
||
# Excludes: workflow-worker (EE only, not needed for CE e2e)
|
||
# Excludes: hocuspocus (built separately below with repo-root context)
|
||
# Note: setup is built here using docker-compose.setup-ubuntu.override.yaml so
|
||
# it produces the alga-setup:ubuntu image used by the later "Start setup
|
||
# service (Ubuntu build override)" step. That step then starts the
|
||
# container without --build to avoid a redundant rebuild.
|
||
docker-compose -p alga-e2e-test \
|
||
-f docker-compose.base.yaml \
|
||
-f docker-compose.ce.yaml \
|
||
-f docker-compose.prod.yaml \
|
||
-f docker-compose.setup-ubuntu.override.yaml \
|
||
build --no-cache \
|
||
server setup email-service
|
||
shell: bash
|
||
|
||
- name: Reclaim disk space before hocuspocus build
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Disk space before reclaim:"
|
||
df -h /
|
||
# The previous --no-cache build leaves dangling intermediate
|
||
# (multi-stage) layers and BuildKit cache behind. Reclaim them so the
|
||
# hocuspocus build has headroom -- the earlier run ran out of space
|
||
# mid-`apt-get` precisely here, on the last image of the job.
|
||
docker image prune -f || true
|
||
docker builder prune -af || true
|
||
echo "Disk space after reclaim:"
|
||
df -h /
|
||
shell: bash
|
||
|
||
- name: Build hocuspocus image with repo-root context
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
# hocuspocus/Dockerfile expects repo-root as build context (it does
|
||
# `COPY hocuspocus/package.json ...`), but docker-compose.ce.yaml sets
|
||
# `context: ./hocuspocus`, which breaks the build. Build it directly
|
||
# here with the correct context and tag it with the name compose v2
|
||
# auto-generates (`<project>-<service>:latest`) so subsequent
|
||
# `docker-compose up` calls reuse this image instead of rebuilding.
|
||
docker build --no-cache \
|
||
-f hocuspocus/Dockerfile \
|
||
-t alga-e2e-test-hocuspocus:latest \
|
||
.
|
||
shell: bash
|
||
|
||
- name: Start foundational services (postgres and redis)
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Starting foundational services: postgres and redis"
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml up --build -d postgres redis
|
||
shell: bash
|
||
|
||
- name: Wait for postgres to be ready
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Waiting for postgres to be ready..."
|
||
|
||
# Wait for postgres to be healthy
|
||
echo "Checking postgres health..."
|
||
MAX_ATTEMPTS=30
|
||
ATTEMPT_NUM=1
|
||
until docker-compose -p alga-e2e-test exec -T postgres pg_isready -U postgres; do
|
||
if [ $ATTEMPT_NUM -ge $MAX_ATTEMPTS ]; then
|
||
echo "Timeout: Postgres did not become ready within the allocated time."
|
||
docker-compose -p alga-e2e-test logs postgres
|
||
exit 1
|
||
fi
|
||
echo "Attempt $ATTEMPT_NUM/$MAX_ATTEMPTS: Postgres not ready. Waiting 5 seconds..."
|
||
sleep 5
|
||
ATTEMPT_NUM=$((ATTEMPT_NUM+1))
|
||
done
|
||
echo "Postgres is ready."
|
||
shell: bash
|
||
|
||
- name: Start pgbouncer service
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Starting pgbouncer service"
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml up --build -d pgbouncer
|
||
shell: bash
|
||
|
||
- name: Wait for pgbouncer to be ready
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Waiting for pgbouncer to be ready..."
|
||
|
||
# Wait for pgbouncer port to be open
|
||
echo "Checking pgbouncer port availability..."
|
||
MAX_ATTEMPTS=30
|
||
ATTEMPT_NUM=1
|
||
until docker run --rm --network alga-e2e-test_app-network busybox nc -z pgbouncer 6432; do
|
||
if [ $ATTEMPT_NUM -ge $MAX_ATTEMPTS ]; then
|
||
echo "Timeout: PgBouncer port did not become available within the allocated time."
|
||
docker-compose -p alga-e2e-test logs pgbouncer
|
||
exit 1
|
||
fi
|
||
echo "Attempt $ATTEMPT_NUM/$MAX_ATTEMPTS: PgBouncer port not ready. Waiting 5 seconds..."
|
||
sleep 5
|
||
ATTEMPT_NUM=$((ATTEMPT_NUM+1))
|
||
done
|
||
echo "PgBouncer port is ready."
|
||
|
||
# Additional wait for pgbouncer to fully initialize
|
||
echo "Waiting additional 5 seconds for pgbouncer to fully initialize..."
|
||
sleep 5
|
||
shell: bash
|
||
|
||
- name: Start setup service (Ubuntu build override)
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Starting setup service"
|
||
# Image (alga-setup:ubuntu) was already built fresh in the earlier
|
||
# "Build images with no cache" step using the same override file, so
|
||
# we start without --build here to avoid a duplicate ~5–10 min rebuild
|
||
# that previously pushed the job past its 60-minute timeout.
|
||
# Set DB_HOST_ADMIN and DB_PORT_ADMIN to connect directly to postgres for admin operations
|
||
# while keeping DB_HOST and DB_PORT pointing to pgbouncer for migrations/seeds
|
||
DB_HOST_ADMIN=postgres \
|
||
DB_PORT_ADMIN=5432 \
|
||
docker-compose -p alga-e2e-test \
|
||
-f docker-compose.base.yaml \
|
||
-f docker-compose.ce.yaml \
|
||
-f docker-compose.prod.yaml \
|
||
-f docker-compose.setup-ubuntu.override.yaml \
|
||
up -d setup
|
||
shell: bash
|
||
|
||
- name: Wait for Setup service to complete (Ubuntu build override)
|
||
if: steps.install_docker_compose.outcome == 'success'
|
||
id: wait_for_setup
|
||
run: |
|
||
echo "Waiting for setup service to complete..."
|
||
MAX_ATTEMPTS=60 # 10 minutes (60 attempts * 10 seconds)
|
||
ATTEMPT_NUM=1
|
||
|
||
# Get the container ID before the loop, it might be gone after exit
|
||
SETUP_CONTAINER_ID=$(docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml ps -q setup)
|
||
if [ -z "$SETUP_CONTAINER_ID" ]; then
|
||
echo "Critical: Could not get initial container ID for setup service. Assuming it failed to start."
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml logs setup
|
||
echo "status=failure" >> $GITHUB_OUTPUT
|
||
exit 1
|
||
fi
|
||
echo "Monitoring Setup Container ID: $SETUP_CONTAINER_ID"
|
||
|
||
while [ $ATTEMPT_NUM -le $MAX_ATTEMPTS ]; do
|
||
# Check if the setup container is still running
|
||
if ! docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml ps setup | grep -q "Up"; then
|
||
# Container has exited, check its exit code using the stored ID
|
||
echo "Setup container (ID: $SETUP_CONTAINER_ID) is no longer 'Up'. Checking exit code."
|
||
# Use the stored SETUP_CONTAINER_ID
|
||
if [ -z "$SETUP_CONTAINER_ID" ]; then # Should not happen if initial check passed
|
||
echo "Error: Lost setup container ID." # Should be redundant due to initial check
|
||
SETUP_EXIT_CODE="1"
|
||
else
|
||
echo "Inspecting container ID: $SETUP_CONTAINER_ID"
|
||
INSPECT_OUTPUT=$(docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' "$SETUP_CONTAINER_ID" 2>/dev/null)
|
||
echo "Inspect output: '$INSPECT_OUTPUT'"
|
||
SETUP_EXIT_CODE=$(echo "$INSPECT_OUTPUT" | awk '{print $1}')
|
||
# If SETUP_EXIT_CODE is empty or not a number, default to 1
|
||
if ! [[ "$SETUP_EXIT_CODE" =~ ^[0-9]+$ ]]; then
|
||
echo "Failed to parse exit code from inspect output, defaulting to 1."
|
||
SETUP_EXIT_CODE="1"
|
||
fi
|
||
fi
|
||
|
||
echo "Reported Setup Exit Code: $SETUP_EXIT_CODE"
|
||
if [ "$SETUP_EXIT_CODE" -eq 0 ]; then
|
||
echo "Setup service completed successfully."
|
||
echo "status=success" >> $GITHUB_OUTPUT
|
||
exit 0
|
||
else
|
||
echo "Setup service failed with exit code $SETUP_EXIT_CODE."
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml logs setup
|
||
echo "status=failure" >> $GITHUB_OUTPUT
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# Check logs for completion message as a secondary check (optional, primary is exit code)
|
||
if docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml logs setup | grep -q "Setup completed!"; then
|
||
echo "Setup completion message found in logs. Waiting a bit for container to exit."
|
||
sleep 5 # Give it a moment to exit gracefully
|
||
# Re-check exit status using the stored ID
|
||
if ! docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.setup-ubuntu.override.yaml ps setup | grep -q "Up"; then
|
||
# Use the stored SETUP_CONTAINER_ID
|
||
INSPECT_OUTPUT_LOG_CHECK=$(docker inspect -f '{{.State.ExitCode}}' "$SETUP_CONTAINER_ID" 2>/dev/null)
|
||
SETUP_EXIT_CODE_LOG_CHECK=$(echo "$INSPECT_OUTPUT_LOG_CHECK" | awk '{print $1}')
|
||
if ! [[ "$SETUP_EXIT_CODE_LOG_CHECK" =~ ^[0-9]+$ ]]; then
|
||
echo "Failed to parse exit code from inspect output (log check), defaulting to 1."
|
||
SETUP_EXIT_CODE_LOG_CHECK="1"
|
||
fi
|
||
|
||
if [ "$SETUP_EXIT_CODE_LOG_CHECK" -eq 0 ]; then
|
||
echo "Setup service completed successfully after log check (Exit Code: $SETUP_EXIT_CODE_LOG_CHECK)."
|
||
echo "status=success" >> $GITHUB_OUTPUT
|
||
exit 0 # Successful exit from the script
|
||
else
|
||
echo "Setup service showed completion log but exited with code $SETUP_EXIT_CODE_LOG_CHECK."
|
||
docker-compose -p alga-e2e-test logs setup
|
||
echo "status=failure" >> $GITHUB_OUTPUT
|
||
exit 1 # Failed exit from the script
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
echo "Attempt $ATTEMPT_NUM/$MAX_ATTEMPTS: Setup service still running. Waiting 10 seconds..."
|
||
sleep 10
|
||
ATTEMPT_NUM=$((ATTEMPT_NUM+1))
|
||
done
|
||
echo "Timeout: Setup service did not complete within the allocated time."
|
||
docker-compose -p alga-e2e-test logs
|
||
echo "status=failure" >> $GITHUB_OUTPUT
|
||
exit 1
|
||
shell: bash
|
||
|
||
- name: Start remaining services after setup completion
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Setup completed successfully. Starting remaining services..."
|
||
# Start CE services needed for e2e tests (images already built earlier)
|
||
# Use --no-recreate to avoid restarting setup which already completed
|
||
# Use --no-deps to avoid waiting on setup dependency again
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml -f docker-compose.imap-test.yaml up -d \
|
||
--no-recreate --no-deps \
|
||
server email-service imap-test-server hocuspocus
|
||
shell: bash
|
||
|
||
- name: Collect initial container logs
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "=== CONTAINER STATUS ==="
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml ps
|
||
|
||
echo -e "\n=== ALL CONTAINER LOGS ==="
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml logs --tail=100
|
||
|
||
echo -e "\n=== SERVER LOGS (detailed) ==="
|
||
docker-compose -p alga-e2e-test logs server --tail=200
|
||
shell: bash
|
||
|
||
- name: Wait for Server service to be healthy
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Waiting for server service to be healthy..."
|
||
MAX_ATTEMPTS=30 # 5 minutes (30 attempts * 10 seconds)
|
||
ATTEMPT_NUM=1
|
||
# Using APP_PORT from .env, default to 3000 if not found (though it should be there)
|
||
APP_PORT_VALUE=$(grep APP_PORT .env | cut -d '=' -f2 | head -n 1 || echo "3000")
|
||
HEALTH_CHECK_URL="http://localhost:${APP_PORT_VALUE}/api/health" # Assuming a health endpoint exists
|
||
# Fallback if /api/health is not standard, try base URL
|
||
# HEALTH_CHECK_URL="http://localhost:${APP_PORT_VALUE}/"
|
||
|
||
until curl --output /dev/null --silent --head --fail $HEALTH_CHECK_URL; do
|
||
if [ $ATTEMPT_NUM -ge $MAX_ATTEMPTS ]; then
|
||
echo "Timeout: Server service did not become healthy at $HEALTH_CHECK_URL within the allocated time."
|
||
echo "=== FINAL SERVER LOGS ==="
|
||
docker-compose -p alga-e2e-test logs server --tail=500
|
||
echo "=== ALL CONTAINER STATUS ==="
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml ps
|
||
exit 1
|
||
fi
|
||
echo "Attempt $ATTEMPT_NUM/$MAX_ATTEMPTS: Server not yet healthy at $HEALTH_CHECK_URL. Waiting 10 seconds..."
|
||
|
||
# Show server logs every 5 attempts to debug issues
|
||
if [ $((ATTEMPT_NUM % 5)) -eq 0 ]; then
|
||
echo "=== SERVER LOGS (attempt $ATTEMPT_NUM) ==="
|
||
docker-compose -p alga-e2e-test logs server --tail=50
|
||
fi
|
||
|
||
sleep 10
|
||
ATTEMPT_NUM=$((ATTEMPT_NUM+1))
|
||
done
|
||
echo "Server service is healthy at $HEALTH_CHECK_URL."
|
||
shell: bash
|
||
|
||
- name: Trigger Login Page to Generate Credentials
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
echo "Attempting to trigger login page to generate credentials in server logs..."
|
||
APP_PORT_VALUE=$(grep APP_PORT .env | cut -d '=' -f2 | head -n 1 || echo "3000")
|
||
LOGIN_PAGE_URL="http://localhost:${APP_PORT_VALUE}/auth/signin"
|
||
# Perform a curl request to the login page. We don't need the output, just the action of hitting the page.
|
||
# Allow failure as the page might not return 200 immediately or might redirect,
|
||
# but the act of requesting it should trigger the log.
|
||
curl -s -o /dev/null -w "%{http_code}" "${LOGIN_PAGE_URL}" || echo "Curl to login page finished (ignore exit code here)."
|
||
echo "Login page triggered. Waiting a few seconds for logs to propagate..."
|
||
sleep 3 # Wait for server to process the request and log credentials
|
||
shell: bash
|
||
|
||
- name: Capture Credentials from Server Logs
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.install_docker_compose.outcome == 'success'
|
||
id: capture_creds
|
||
run: |
|
||
echo "Attempting to capture credentials from server logs..."
|
||
# Wait a few seconds for logs to flush if needed
|
||
sleep 5
|
||
SERVER_LOGS=$(docker-compose -p alga-e2e-test logs server)
|
||
|
||
# Extract email (should be glinda@emeraldcity.oz)
|
||
USER_EMAIL=$(echo "$SERVER_LOGS" | grep -oP 'User Email is -> \[ \K[^ ]+' || echo "")
|
||
# Extract password
|
||
USER_PASSWORD=$(echo "$SERVER_LOGS" | grep -oP 'Password is -> \[ \K[^ ]+' || echo "")
|
||
|
||
if [ -z "$USER_EMAIL" ] || [ -z "$USER_PASSWORD" ]; then
|
||
echo "Failed to extract credentials from server logs."
|
||
echo "Server Logs:"
|
||
echo "$SERVER_LOGS"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Successfully extracted credentials."
|
||
echo "e2e_user_email=$USER_EMAIL" >> $GITHUB_OUTPUT
|
||
# Use a delimiter to safely pass passwords with special characters
|
||
EOF_DELIM=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||
echo "e2e_user_password<<$EOF_DELIM" >> $GITHUB_OUTPUT
|
||
echo "$USER_PASSWORD" >> $GITHUB_OUTPUT
|
||
echo "$EOF_DELIM" >> $GITHUB_OUTPUT
|
||
# Mask the password in logs
|
||
echo "::add-mask::$USER_PASSWORD"
|
||
shell: bash
|
||
|
||
- name: Warm up authentication system
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.capture_creds.outputs.e2e_user_email != ''
|
||
run: |
|
||
echo "Warming up authentication system to prime database connections..."
|
||
APP_PORT_VALUE=$(grep APP_PORT .env | cut -d '=' -f2 | head -n 1 || echo "3000")
|
||
|
||
# Make a few requests to warm up the auth system and connection pool
|
||
# First, hit the CSRF endpoint to initialize NextAuth
|
||
curl -s -o /dev/null -w "CSRF endpoint: %{http_code}\n" \
|
||
"http://localhost:${APP_PORT_VALUE}/api/auth/csrf" || true
|
||
|
||
# Then make a login attempt with invalid credentials to warm up the full auth path
|
||
# This primes: database connections, NextAuth session machinery, lazy-loaded modules
|
||
CSRF_TOKEN=$(curl -s "http://localhost:${APP_PORT_VALUE}/api/auth/csrf" | grep -oP '"csrfToken":"\K[^"]+' || echo "")
|
||
curl -s -o /dev/null -w "Auth warmup: %{http_code}\n" \
|
||
-X POST "http://localhost:${APP_PORT_VALUE}/api/auth/callback/credentials" \
|
||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||
-d "email=warmup@test.invalid&password=warmup&csrfToken=${CSRF_TOKEN}" || true
|
||
|
||
echo "Auth system warmed up. Waiting for connections to stabilize..."
|
||
sleep 3
|
||
shell: bash
|
||
|
||
- name: Set up Node.js
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
uses: actions/setup-node@v3
|
||
with:
|
||
node-version: '18' # Or your project's Node version
|
||
|
||
- name: Create E2E test directory and package.json
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
run: |
|
||
mkdir -p e2e-tests/tests
|
||
cat << EOF > e2e-tests/package.json
|
||
{
|
||
"name": "alga-e2e-tests",
|
||
"version": "1.0.0",
|
||
"description": "E2E tests for Alga PSA",
|
||
"main": "index.js",
|
||
"scripts": {
|
||
"test": "playwright test"
|
||
},
|
||
"keywords": [],
|
||
"author": "",
|
||
"license": "ISC",
|
||
"devDependencies": {
|
||
"@playwright/test": "^1.40.0"
|
||
}
|
||
}
|
||
EOF
|
||
shell: bash
|
||
|
||
- name: Install Playwright npm packages
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
working-directory: ./e2e-tests
|
||
run: npm install
|
||
shell: bash
|
||
|
||
- name: Get Playwright version
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
id: playwright_version
|
||
working-directory: ./e2e-tests
|
||
run: |
|
||
PLAYWRIGHT_VERSION=$(npx playwright --version | awk '{print $2}')
|
||
echo "version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
|
||
echo "Playwright version: $PLAYWRIGHT_VERSION"
|
||
shell: bash
|
||
|
||
- name: Cache Playwright browsers
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
id: playwright_cache
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-browsers-${{ steps.playwright_version.outputs.version }}
|
||
|
||
- name: Install Playwright browsers
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.playwright_cache.outputs.cache-hit != 'true'
|
||
working-directory: ./e2e-tests
|
||
run: npx playwright install
|
||
shell: bash
|
||
|
||
- name: Install Playwright system dependencies
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
working-directory: ./e2e-tests
|
||
run: npx playwright install-deps
|
||
shell: bash
|
||
|
||
- name: Create Playwright config file
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
run: |
|
||
cat << EOF > e2e-tests/playwright.config.ts
|
||
import { defineConfig, devices } from '@playwright/test';
|
||
|
||
export default defineConfig({
|
||
testDir: './tests',
|
||
fullyParallel: true,
|
||
forbidOnly: !!process.env.CI,
|
||
retries: process.env.CI ? 2 : 0,
|
||
workers: process.env.CI ? 1 : undefined,
|
||
reporter: 'html',
|
||
timeout: 120000, // 2 minutes global timeout
|
||
use: {
|
||
baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000', // Default to localhost:3000
|
||
trace: 'on-first-retry',
|
||
actionTimeout: 60000, // 60 seconds for actions
|
||
navigationTimeout: 90000, // 90 seconds for navigation
|
||
},
|
||
projects: [
|
||
{
|
||
name: 'chromium',
|
||
use: { ...devices['Desktop Chrome'] },
|
||
},
|
||
],
|
||
});
|
||
EOF
|
||
shell: bash
|
||
|
||
- name: Create E2E login test spec
|
||
if: steps.wait_for_setup.outputs.status == 'success'
|
||
run: |
|
||
# Ensure APP_PORT is available for constructing the baseURL if not set via E2E_BASE_URL
|
||
APP_PORT_VALUE_FROM_ENV=$(grep '^APP_PORT=' ./.env | cut -d '=' -f2 | head -n 1)
|
||
APP_PORT_VALUE=${APP_PORT_VALUE_FROM_ENV:-3000}
|
||
BASE_URL="http://localhost:${APP_PORT_VALUE}"
|
||
echo "Resolved BASE_URL for Playwright spec: ${BASE_URL}"
|
||
|
||
cat << EOF > e2e-tests/tests/login.spec.ts
|
||
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('Login Functionality', () => {
|
||
test('should allow a user to log in and redirect to dashboard', async ({ page }) => {
|
||
const email = process.env.E2E_USER_EMAIL;
|
||
const password = process.env.E2E_USER_PASSWORD;
|
||
const baseUrl = process.env.E2E_BASE_URL || '${BASE_URL}'; // Use env var or default
|
||
|
||
if (!email || !password) {
|
||
throw new Error('E2E_USER_EMAIL or E2E_USER_PASSWORD environment variables are not set.');
|
||
}
|
||
|
||
// Navigate to the login page
|
||
await page.goto(\`\${baseUrl}/auth/signin\`);
|
||
|
||
// Fill in the email and password
|
||
try {
|
||
console.log('Waiting for email field...');
|
||
await page.waitForSelector('input[id="msp-email-field"]', { timeout: 15000 });
|
||
console.log(\`Filling email: \${email}\`);
|
||
await page.fill('input[id="msp-email-field"]', email);
|
||
console.log('Waiting for password field...');
|
||
await page.waitForSelector('input[id="msp-password-field"]', { timeout: 5000 });
|
||
console.log('Filling password...');
|
||
await page.fill('input[id="msp-password-field"]', password);
|
||
console.log('Credentials filled successfully');
|
||
} catch (e) {
|
||
console.error('Error filling form fields. Current page HTML:', await page.content());
|
||
throw e;
|
||
}
|
||
|
||
// Submit the login form
|
||
// Note: Using keyboard Enter instead of button click because React form
|
||
// onSubmit handlers are sometimes not triggered by Playwright's click()
|
||
try {
|
||
console.log('Waiting for sign-in button...');
|
||
await page.waitForSelector('button[id="msp-sign-in-button"]', { timeout: 5000 });
|
||
console.log('Submitting form via Enter key on password field...');
|
||
|
||
// Focus the password field and press Enter to submit the form
|
||
// This is more reliable than clicking the submit button for React forms
|
||
await page.focus('input[id="msp-password-field"]');
|
||
|
||
// Wait for the credentials POST request while pressing Enter
|
||
// NextAuth credentials login POSTs to /api/auth/callback/credentials
|
||
const [credentialsResponse] = await Promise.all([
|
||
page.waitForResponse(
|
||
response => {
|
||
const isCredentialsPost = response.url().includes('/api/auth/callback') &&
|
||
response.request().method() === 'POST';
|
||
if (isCredentialsPost) {
|
||
console.log(\`Auth callback response: \${response.status()} \${response.request().method()} \${response.url()}\`);
|
||
}
|
||
return isCredentialsPost;
|
||
},
|
||
{ timeout: 30000 }
|
||
).catch(e => {
|
||
console.log('No auth callback POST detected - trying button click as fallback');
|
||
return null;
|
||
}),
|
||
page.keyboard.press('Enter')
|
||
]);
|
||
|
||
// If Enter didn't work, try clicking the button
|
||
if (!credentialsResponse) {
|
||
console.log('Enter key did not trigger form submit, trying button click...');
|
||
await Promise.all([
|
||
page.waitForResponse(
|
||
response => {
|
||
const isCredentialsPost = response.url().includes('/api/auth/callback') &&
|
||
response.request().method() === 'POST';
|
||
if (isCredentialsPost) {
|
||
console.log(\`Auth callback response (via click): \${response.status()} \${response.request().method()} \${response.url()}\`);
|
||
}
|
||
return isCredentialsPost;
|
||
},
|
||
{ timeout: 30000 }
|
||
).catch(e => {
|
||
console.log('Still no auth callback POST after button click');
|
||
return null;
|
||
}),
|
||
page.click('button[id="msp-sign-in-button"]')
|
||
]);
|
||
}
|
||
|
||
console.log('Form submitted, waiting for page response...');
|
||
} catch (e) {
|
||
console.error('Error submitting login form:', e);
|
||
console.error('Current page HTML:', await page.content());
|
||
throw e;
|
||
}
|
||
|
||
// Wait for authentication to complete
|
||
console.log('Waiting for network to become idle after login...');
|
||
await page.waitForLoadState('networkidle', { timeout: 60000 });
|
||
|
||
// Check current state after networkidle
|
||
const currentUrl = page.url();
|
||
console.log(\`Current URL after networkidle: \${currentUrl}\`);
|
||
|
||
// If still on login page, capture diagnostic info
|
||
if (currentUrl.includes('/auth/')) {
|
||
console.log('Still on auth page - checking for errors...');
|
||
|
||
// Check for any visible error messages
|
||
const pageText = await page.textContent('body');
|
||
if (pageText.toLowerCase().includes('error') || pageText.toLowerCase().includes('invalid')) {
|
||
console.error('Error text found on page:', pageText.substring(0, 1000));
|
||
}
|
||
|
||
// Check for alert elements
|
||
const alertVisible = await page.locator('[role="alert"]').isVisible().catch(() => false);
|
||
if (alertVisible) {
|
||
const alertText = await page.locator('[role="alert"]').textContent();
|
||
console.error('Alert message found:', alertText);
|
||
}
|
||
|
||
// Log the visible form state
|
||
const emailValue = await page.inputValue('input[id="msp-email-field"]').catch(() => 'N/A');
|
||
const passwordValue = await page.inputValue('input[id="msp-password-field"]').catch(() => 'N/A');
|
||
console.log(\`Form state - Email field value: \${emailValue}, Password filled: \${passwordValue ? 'yes' : 'no'}\`);
|
||
|
||
// Take a screenshot for debugging
|
||
await page.screenshot({ path: 'login-debug-screenshot.png', fullPage: true });
|
||
console.log('Debug screenshot saved to login-debug-screenshot.png');
|
||
}
|
||
|
||
// Wait for navigation to the dashboard
|
||
console.log(\`Waiting for URL: \${baseUrl}/msp/dashboard with 90s timeout.\`);
|
||
await page.waitForURL(\`\${baseUrl}/msp/dashboard\`, { timeout: 90000 });
|
||
|
||
// Assert that the URL is the dashboard URL
|
||
expect(page.url()).toBe(\`\${baseUrl}/msp/dashboard\`);
|
||
|
||
// Optional: Add more assertions, e.g., check for a welcome message or specific element
|
||
// await expect(page.locator('h1')).toContainText('Dashboard');
|
||
});
|
||
});
|
||
EOF
|
||
working-directory: ./
|
||
shell: bash
|
||
|
||
|
||
- name: Run Playwright E2E tests
|
||
if: steps.wait_for_setup.outputs.status == 'success' && steps.capture_creds.outputs.e2e_user_email != ''
|
||
working-directory: ./e2e-tests
|
||
env:
|
||
E2E_USER_EMAIL: ${{ steps.capture_creds.outputs.e2e_user_email }}
|
||
E2E_USER_PASSWORD: ${{ steps.capture_creds.outputs.e2e_user_password }}
|
||
run: npx playwright test
|
||
shell: bash
|
||
|
||
- name: Collect all container logs on failure
|
||
if: failure() && steps.install_docker_compose.outcome == 'success'
|
||
timeout-minutes: 5
|
||
run: |
|
||
echo "=== COLLECTING ALL LOGS DUE TO FAILURE ==="
|
||
|
||
echo "=== CONTAINER STATUS ==="
|
||
timeout 60 docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml ps || echo "Could not get container status"
|
||
|
||
echo -e "\n=== DOCKER SYSTEM INFO ==="
|
||
# `docker system df` can hang for minutes against a disk-full/wedged
|
||
# daemon (it previously stalled here until the step was SIGTERM'd ->
|
||
# exit 143), so cap it with `timeout`.
|
||
timeout 60 docker system df || echo "docker system df timed out or failed"
|
||
timeout 60 docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" || echo "docker images timed out or failed"
|
||
|
||
echo -e "\n=== ALL CONTAINER LOGS ==="
|
||
docker-compose -p alga-e2e-test logs --tail=1000 || echo "Could not get compose logs"
|
||
|
||
echo -e "\n=== INDIVIDUAL SERVICE LOGS ==="
|
||
for service in server setup postgres redis pgbouncer hocuspocus workflow-worker; do
|
||
echo "--- $service logs ---"
|
||
docker-compose -p alga-e2e-test logs $service --tail=200 2>/dev/null || echo "No logs for $service"
|
||
done
|
||
|
||
echo -e "\n=== DOCKER INSPECT (running containers) ==="
|
||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep alga-e2e-test || echo "No running containers found"
|
||
|
||
# Try to get logs from any containers that might have the project name
|
||
echo -e "\n=== DIRECT DOCKER LOGS ==="
|
||
for container in $(docker ps -a --filter "name=alga-e2e-test" --format "{{.Names}}"); do
|
||
echo "--- Direct logs for $container ---"
|
||
docker logs $container --tail=100 2>/dev/null || echo "Could not get logs for $container"
|
||
done
|
||
shell: bash
|
||
|
||
- name: Save container logs as artifacts
|
||
if: always() && steps.install_docker_compose.outcome == 'success'
|
||
run: |
|
||
mkdir -p logs
|
||
|
||
# Copy any Playwright screenshots to logs directory
|
||
cp e2e-tests/*.png logs/ 2>/dev/null || echo "No Playwright screenshots found"
|
||
cp e2e-tests/test-results/**/*.png logs/ 2>/dev/null || echo "No test-results screenshots found"
|
||
|
||
# Save logs for each service
|
||
for service in server setup postgres redis pgbouncer hocuspocus workflow-worker; do
|
||
echo "Saving logs for $service..."
|
||
docker-compose -p alga-e2e-test logs $service --no-color > "logs/${service}.log" 2>/dev/null || echo "No logs for $service" > "logs/${service}.log"
|
||
done
|
||
|
||
# Save combined logs
|
||
docker-compose -p alga-e2e-test logs --no-color > "logs/all-services.log" 2>/dev/null || echo "Could not save combined logs"
|
||
|
||
# Save container status
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml ps > "logs/container-status.txt" 2>/dev/null || echo "Could not save container status"
|
||
|
||
# Save environment info
|
||
echo "Environment variables:" > logs/environment.txt
|
||
env | grep -E "(APP_|NODE_|DB_|REDIS_)" >> logs/environment.txt || echo "Could not save environment"
|
||
|
||
ls -la logs/
|
||
shell: bash
|
||
|
||
- name: Upload logs as artifacts
|
||
if: always() && steps.install_docker_compose.outcome == 'success' && github.actor != 'nektos/act'
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: container-logs
|
||
path: logs/
|
||
retention-days: 7
|
||
|
||
# - name: Upload Playwright Test Report
|
||
# if: always() && steps.changed_files_check.outputs.any_changed == 'true' && steps.wait_for_setup.outputs.status == 'success' && !env.ACT # Run even if tests fail, but setup was ok. Skip in ACT due to token issues.
|
||
# uses: actions/upload-artifact@v4
|
||
# with:
|
||
# name: playwright-report
|
||
# path: e2e-tests/playwright-report/
|
||
# retention-days: 7
|
||
|
||
- name: Cleanup Docker Compose
|
||
if: always() && steps.install_docker_compose.outcome == 'success' # Ensure docker-compose was attempted to be installed
|
||
run: |
|
||
echo "Cleaning up Docker Compose environment..."
|
||
# Check if docker-compose command is available before trying to use it
|
||
if command -v docker-compose &> /dev/null; then
|
||
docker-compose -p alga-e2e-test -f docker-compose.base.yaml -f docker-compose.ce.yaml -f docker-compose.prod.yaml down -v --remove-orphans || echo "Docker Compose cleanup failed, but continuing."
|
||
else
|
||
echo "docker-compose command not found, skipping cleanup."
|
||
fi
|
||
shell: bash
|
||
|
||
- name: Restore root docker-compose.yaml
|
||
if: always() && env.ACT
|
||
run: |
|
||
if [ -f docker-compose.yaml.ignored ]; then
|
||
echo "Restoring root docker-compose.yaml"
|
||
sudo mv docker-compose.yaml.ignored docker-compose.yaml
|
||
fi
|
||
shell: bash
|