PSA/.github/workflows/e2e-fresh-install-tests.yaml
Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

900 lines
43 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ~510 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