mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 00:26:42 +02:00
🔀 merge: integrate till-dev into main
Merge till-dev branch containing: - Planta plant care tracking application - Clock backend with alarms, timers, world clocks - Zitare backend with favorites and lists - Various app improvements and fixes - Auth system updates - Infrastructure improvements Note: Some type-check issues may need resolution after merge. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
49a8c652da
475 changed files with 28008 additions and 22742 deletions
|
|
@ -1,102 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build web apps that have changes compared to the remote branch
|
||||
# This catches build failures (like npm package incompatibilities) before push
|
||||
|
||||
set -e
|
||||
|
||||
# Get the remote branch we're pushing to (default to origin/dev)
|
||||
REMOTE_BRANCH="${1:-origin/dev}"
|
||||
|
||||
echo "🔍 Checking for changed web apps compared to $REMOTE_BRANCH..."
|
||||
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only "$REMOTE_BRANCH" HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD)
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "No changes detected, skipping build check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find unique web app directories that have changes
|
||||
WEB_APPS=""
|
||||
|
||||
for file in $CHANGED_FILES; do
|
||||
app_path=""
|
||||
if [[ $file =~ ^(apps/[^/]+/apps/web)/ ]]; then
|
||||
app_path="${BASH_REMATCH[1]}"
|
||||
elif [[ $file =~ ^(games/[^/]+/apps/web)/ ]]; then
|
||||
app_path="${BASH_REMATCH[1]}"
|
||||
elif [[ $file =~ ^(packages/[^/]+)/ ]]; then
|
||||
pkg_name="${BASH_REMATCH[1]}"
|
||||
# Shared packages affect multiple apps - check which ones depend on them
|
||||
echo "⚠️ Changes in shared package: $pkg_name"
|
||||
|
||||
# For critical shared packages, build the main apps
|
||||
case "$pkg_name" in
|
||||
packages/shared-ui|packages/shared-auth|packages/shared-theme)
|
||||
# These affect most web apps - build the main ones
|
||||
for main_app in "apps/manacore/apps/web" "apps/todo/apps/web" "apps/chat/apps/web"; do
|
||||
if [ -d "$main_app" ] && [[ ! " $WEB_APPS " =~ " $main_app " ]]; then
|
||||
WEB_APPS="$WEB_APPS $main_app"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Add to list if not already present
|
||||
if [ -n "$app_path" ] && [ -d "$app_path" ]; then
|
||||
if [[ ! " $WEB_APPS " =~ " $app_path " ]]; then
|
||||
WEB_APPS="$WEB_APPS $app_path"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Trim leading space
|
||||
WEB_APPS=$(echo "$WEB_APPS" | xargs)
|
||||
|
||||
if [ -z "$WEB_APPS" ]; then
|
||||
echo "✅ No web app changes detected, skipping build"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 Building changed web apps..."
|
||||
echo " Apps to build: $WEB_APPS"
|
||||
echo ""
|
||||
|
||||
FAILED=0
|
||||
|
||||
for app in $WEB_APPS; do
|
||||
if [ -f "$app/package.json" ]; then
|
||||
# Get the package name for pnpm filter
|
||||
PKG_NAME=$(node -p "require('./$app/package.json').name" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$PKG_NAME" ]; then
|
||||
echo "━━━ Building $PKG_NAME ━━━"
|
||||
|
||||
if pnpm --filter "$PKG_NAME" build 2>&1; then
|
||||
echo "✅ Build passed for $PKG_NAME"
|
||||
else
|
||||
echo "❌ Build failed for $PKG_NAME"
|
||||
FAILED=1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -eq 1 ]; then
|
||||
echo ""
|
||||
echo "❌ Build failed! Fix the issues above before pushing."
|
||||
echo ""
|
||||
echo "Common issues:"
|
||||
echo " - npm package incompatibility (check node_modules versions)"
|
||||
echo " - Missing workspace dependencies in Dockerfile"
|
||||
echo " - TypeScript errors in production build"
|
||||
echo ""
|
||||
echo "To skip this check (emergency only): git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All builds passed!"
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Run production builds on web apps that have changes since main/dev
|
||||
# This catches issues that only appear in production builds
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-check-staged.sh # Check changes vs origin/dev
|
||||
# ./scripts/build-check-staged.sh main # Check changes vs origin/main
|
||||
# ./scripts/build-check-staged.sh --all # Check all web apps
|
||||
|
||||
set -e
|
||||
|
||||
BASE_BRANCH="${1:-origin/dev}"
|
||||
|
||||
# Handle --all flag
|
||||
if [ "$1" = "--all" ]; then
|
||||
echo "🔨 Building ALL web apps..."
|
||||
APPS_TO_BUILD=(
|
||||
"apps/todo/apps/web"
|
||||
"apps/chat/apps/web"
|
||||
"apps/calendar/apps/web"
|
||||
"apps/clock/apps/web"
|
||||
"apps/manacore/apps/web"
|
||||
"apps/contacts/apps/web"
|
||||
"apps/zitare/apps/web"
|
||||
"apps/picture/apps/web"
|
||||
"apps/manadeck/apps/web"
|
||||
)
|
||||
else
|
||||
echo "🔍 Finding changed files since $BASE_BRANCH..."
|
||||
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only "$BASE_BRANCH"...HEAD 2>/dev/null || git diff --name-only HEAD~10...HEAD)
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "No changes detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find unique web app directories that have changes
|
||||
declare -A WEB_APPS
|
||||
|
||||
for file in $CHANGED_FILES; do
|
||||
# Direct changes in web app
|
||||
if [[ $file =~ ^(apps/[^/]+/apps/web)/ ]]; then
|
||||
WEB_APPS["${BASH_REMATCH[1]}"]=1
|
||||
elif [[ $file =~ ^(games/[^/]+/apps/web)/ ]]; then
|
||||
WEB_APPS["${BASH_REMATCH[1]}"]=1
|
||||
# Changes in shared packages affect all web apps using them
|
||||
elif [[ $file =~ ^packages/shared- ]]; then
|
||||
echo "⚠️ Shared package changed: $file"
|
||||
echo " All web apps may be affected"
|
||||
# Add major web apps
|
||||
WEB_APPS["apps/todo/apps/web"]=1
|
||||
WEB_APPS["apps/chat/apps/web"]=1
|
||||
WEB_APPS["apps/calendar/apps/web"]=1
|
||||
WEB_APPS["apps/manacore/apps/web"]=1
|
||||
fi
|
||||
done
|
||||
|
||||
APPS_TO_BUILD=("${!WEB_APPS[@]}")
|
||||
fi
|
||||
|
||||
if [ ${#APPS_TO_BUILD[@]} -eq 0 ]; then
|
||||
echo "No web app changes detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔨 Building affected web apps..."
|
||||
echo " Apps: ${APPS_TO_BUILD[*]}"
|
||||
echo ""
|
||||
|
||||
# First build shared packages (needed for web apps)
|
||||
echo "━━━ Building shared packages ━━━"
|
||||
pnpm run build:packages || {
|
||||
echo "❌ Failed to build shared packages"
|
||||
exit 1
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
|
||||
for app in "${APPS_TO_BUILD[@]}"; do
|
||||
if [ -f "$app/package.json" ]; then
|
||||
echo ""
|
||||
echo "━━━ Building $app ━━━"
|
||||
|
||||
PKG_NAME=$(node -p "require('./$app/package.json').name" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$PKG_NAME" ]; then
|
||||
if ! pnpm --filter "$PKG_NAME" build 2>&1; then
|
||||
echo "❌ Build failed for $app"
|
||||
FAILED=1
|
||||
else
|
||||
echo "✅ Build passed for $app"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -eq 1 ]; then
|
||||
echo ""
|
||||
echo "❌ Build check failed! Fix the issues above before pushing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All builds passed!"
|
||||
|
|
@ -66,7 +66,8 @@ const APP_CONFIGS = [
|
|||
REDIS_HOST: (env) => env.REDIS_HOST,
|
||||
REDIS_PORT: (env) => env.REDIS_PORT,
|
||||
REDIS_PASSWORD: (env) => env.REDIS_PASSWORD || '',
|
||||
// JWT keys managed by Better Auth (EdDSA) - stored in auth.jwks table
|
||||
JWT_PRIVATE_KEY: (env) => env.JWT_PRIVATE_KEY,
|
||||
JWT_PUBLIC_KEY: (env) => env.JWT_PUBLIC_KEY,
|
||||
JWT_ACCESS_TOKEN_EXPIRY: (env) => env.JWT_ACCESS_TOKEN_EXPIRY,
|
||||
JWT_REFRESH_TOKEN_EXPIRY: (env) => env.JWT_REFRESH_TOKEN_EXPIRY,
|
||||
JWT_ISSUER: (env) => env.JWT_ISSUER,
|
||||
|
|
@ -172,7 +173,7 @@ const APP_CONFIGS = [
|
|||
vars: {
|
||||
PUBLIC_SUPABASE_URL: (env) => env.MANACORE_SUPABASE_URL,
|
||||
PUBLIC_SUPABASE_ANON_KEY: (env) => env.MANACORE_SUPABASE_ANON_KEY,
|
||||
PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
||||
MIDDLEWARE_URL: (env) => env.MANA_CORE_AUTH_URL,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -340,7 +341,7 @@ const APP_CONFIGS = [
|
|||
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
|
||||
DEV_BYPASS_AUTH: () => 'true',
|
||||
DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000',
|
||||
// JWT keys fetched via JWKS from MANA_CORE_AUTH_URL/api/v1/auth/jwks
|
||||
JWT_PUBLIC_KEY: (env) => env.JWT_PUBLIC_KEY,
|
||||
CORS_ORIGINS: (env) => env.CORS_ORIGINS,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
124
scripts/generate-staging-secrets.sh
Executable file
124
scripts/generate-staging-secrets.sh
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Generate Staging Secrets for GitHub
|
||||
# Run this script and copy the output to GitHub Secrets
|
||||
|
||||
set -e
|
||||
|
||||
echo "================================================"
|
||||
echo " STAGING SECRETS GENERATOR"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Copy each value below to GitHub Settings → Secrets and variables → Actions"
|
||||
echo ""
|
||||
echo "Note: Configuration values (host, ports, etc.) are now hardcoded in the workflow"
|
||||
echo "Only sensitive values (passwords, keys) need to be added as secrets"
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Generate secure random passwords
|
||||
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||
REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||
JWT_SECRET=$(openssl rand -base64 64 | tr -d "=+/" | cut -c1-64)
|
||||
|
||||
# Generate Ed25519 key pair for JWT
|
||||
TEMP_KEY_DIR=$(mktemp -d)
|
||||
ssh-keygen -t ed25519 -f "$TEMP_KEY_DIR/jwt_key" -N "" -C "manacore-staging-jwt" > /dev/null 2>&1
|
||||
|
||||
# Convert SSH keys to raw format for JWT
|
||||
PRIVATE_KEY=$(cat "$TEMP_KEY_DIR/jwt_key" | grep -v "BEGIN" | grep -v "END" | tr -d '\n')
|
||||
PUBLIC_KEY=$(ssh-keygen -e -m PKCS8 -f "$TEMP_KEY_DIR/jwt_key.pub" 2>/dev/null | grep -v "BEGIN" | grep -v "END" | tr -d '\n' || cat "$TEMP_KEY_DIR/jwt_key.pub" | awk '{print $2}')
|
||||
|
||||
# Clean up temp files
|
||||
rm -rf "$TEMP_KEY_DIR"
|
||||
|
||||
# Output all secrets in GitHub format
|
||||
echo "# ============================================"
|
||||
echo "# DATABASE SECRETS (2 secrets)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_POSTGRES_PASSWORD"
|
||||
echo "$POSTGRES_PASSWORD"
|
||||
echo ""
|
||||
|
||||
echo "# ============================================"
|
||||
echo "# REDIS SECRETS (1 secret)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_REDIS_PASSWORD"
|
||||
echo "$REDIS_PASSWORD"
|
||||
echo ""
|
||||
|
||||
echo "# ============================================"
|
||||
echo "# MANA CORE AUTH SECRETS (3 secrets)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_JWT_SECRET"
|
||||
echo "$JWT_SECRET"
|
||||
echo ""
|
||||
echo "STAGING_JWT_PUBLIC_KEY"
|
||||
echo "$PUBLIC_KEY"
|
||||
echo ""
|
||||
echo "STAGING_JWT_PRIVATE_KEY"
|
||||
echo "$PRIVATE_KEY"
|
||||
echo ""
|
||||
|
||||
echo "# ============================================"
|
||||
echo "# SUPABASE SECRETS (Fill these manually - 3 secrets)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_SUPABASE_URL"
|
||||
echo "https://YOUR_PROJECT.supabase.co"
|
||||
echo ""
|
||||
echo "STAGING_SUPABASE_ANON_KEY"
|
||||
echo "YOUR_SUPABASE_ANON_KEY_HERE"
|
||||
echo ""
|
||||
echo "STAGING_SUPABASE_SERVICE_ROLE_KEY"
|
||||
echo "YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE"
|
||||
echo ""
|
||||
|
||||
echo "# ============================================"
|
||||
echo "# AZURE OPENAI SECRETS (Fill these manually - 2 secrets)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_AZURE_OPENAI_ENDPOINT"
|
||||
echo "https://YOUR_RESOURCE.openai.azure.com/"
|
||||
echo ""
|
||||
echo "STAGING_AZURE_OPENAI_API_KEY"
|
||||
echo "YOUR_AZURE_OPENAI_API_KEY_HERE"
|
||||
echo ""
|
||||
|
||||
echo "# ============================================"
|
||||
echo "# SSH DEPLOYMENT SECRETS (Fill these manually - 1 secret)"
|
||||
echo "# ============================================"
|
||||
echo ""
|
||||
echo "STAGING_SSH_KEY"
|
||||
echo "Run: cat ~/.ssh/hetzner_deploy_key"
|
||||
echo "(Copy the ENTIRE output including -----BEGIN and -----END lines)"
|
||||
echo ""
|
||||
|
||||
echo "================================================"
|
||||
echo " SUMMARY"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Total secrets to add: 12"
|
||||
echo " - Auto-generated: 6 (passwords, JWT keys)"
|
||||
echo " - Manual: 6 (Supabase, Azure, SSH key)"
|
||||
echo ""
|
||||
echo "The following are now HARDCODED in the workflow:"
|
||||
echo " - POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER"
|
||||
echo " - REDIS_HOST, REDIS_PORT"
|
||||
echo " - MANA_SERVICE_URL"
|
||||
echo " - STAGING_HOST (46.224.108.214)"
|
||||
echo " - STAGING_USER (deploy)"
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Go to: https://github.com/YOUR_ORG/manacore-monorepo/settings/secrets/actions"
|
||||
echo "2. Click 'New repository secret' for each value above"
|
||||
echo "3. Copy the secret name (e.g., STAGING_POSTGRES_PASSWORD)"
|
||||
echo "4. Copy the secret value (the line below the name)"
|
||||
echo "5. Fill in Supabase, Azure, and SSH key values manually"
|
||||
echo ""
|
||||
|
|
@ -71,6 +71,7 @@ ALL_DATABASES=(
|
|||
"techbase"
|
||||
"voxel_lava"
|
||||
"figgos"
|
||||
"planta"
|
||||
)
|
||||
|
||||
# Check if specific service requested
|
||||
|
|
@ -136,9 +137,13 @@ setup_service() {
|
|||
create_db_if_not_exists "figgos"
|
||||
push_schema "@figgos/backend" "figgos"
|
||||
;;
|
||||
planta)
|
||||
create_db_if_not_exists "planta"
|
||||
push_schema "@planta/backend" "planta"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown service: $service${NC}"
|
||||
echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, mail, moodlit, finance, voxel-lava, figgos"
|
||||
echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, mail, moodlit, finance, voxel-lava, figgos, planta"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -162,7 +167,7 @@ echo -e "\n${GREEN}Step 2: Pushing schemas${NC}"
|
|||
echo "--------------------------------------"
|
||||
|
||||
# Push schemas for all known services
|
||||
for service in auth chat zitare contacts calendar clock todo manadeck picture mail moodlit finance voxel-lava figgos; do
|
||||
for service in auth chat zitare contacts calendar clock todo manadeck picture mail moodlit finance voxel-lava figgos planta; do
|
||||
setup_service "$service" 2>/dev/null || true
|
||||
done
|
||||
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run svelte-check on web apps that have staged .svelte files
|
||||
# This catches a11y warnings, Svelte 5 issues, and import errors before CI
|
||||
|
||||
set -e
|
||||
|
||||
# Get list of staged svelte files
|
||||
STAGED_SVELTE=$(git diff --cached --name-only --diff-filter=ACM | grep '\.svelte$' || true)
|
||||
|
||||
if [ -z "$STAGED_SVELTE" ]; then
|
||||
echo "No staged .svelte files, skipping svelte-check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find unique web app directories that have changes
|
||||
WEB_APPS=""
|
||||
|
||||
for file in $STAGED_SVELTE; do
|
||||
# Extract the web app path (e.g., apps/todo/apps/web)
|
||||
app_path=""
|
||||
if [[ $file =~ ^(apps/[^/]+/apps/web)/ ]]; then
|
||||
app_path="${BASH_REMATCH[1]}"
|
||||
elif [[ $file =~ ^(games/[^/]+/apps/web)/ ]]; then
|
||||
app_path="${BASH_REMATCH[1]}"
|
||||
elif [[ $file =~ ^(packages/[^/]+)/ ]]; then
|
||||
# For shared packages, check all web apps that might use them
|
||||
echo "⚠️ Changes in shared package: $file"
|
||||
echo " Consider running: pnpm run build:check to verify all web apps"
|
||||
fi
|
||||
|
||||
# Add to list if not already present
|
||||
if [ -n "$app_path" ]; then
|
||||
if [[ ! " $WEB_APPS " =~ " $app_path " ]]; then
|
||||
WEB_APPS="$WEB_APPS $app_path"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Trim leading space
|
||||
WEB_APPS=$(echo "$WEB_APPS" | xargs)
|
||||
|
||||
if [ -z "$WEB_APPS" ]; then
|
||||
echo "No web app changes detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Running svelte-check on affected web apps..."
|
||||
FAILED=0
|
||||
|
||||
for app in $WEB_APPS; do
|
||||
if [ -f "$app/package.json" ]; then
|
||||
echo ""
|
||||
echo "━━━ Checking $app ━━━"
|
||||
|
||||
# Get the package name for pnpm filter
|
||||
PKG_NAME=$(node -p "require('./$app/package.json').name" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$PKG_NAME" ]; then
|
||||
# Run svelte-check - fails on both errors AND warnings
|
||||
# This ensures no a11y issues or Svelte problems slip through
|
||||
if ! pnpm --filter "$PKG_NAME" exec svelte-check --tsconfig ./tsconfig.json --threshold warning 2>&1; then
|
||||
echo "❌ svelte-check failed for $app"
|
||||
FAILED=1
|
||||
else
|
||||
echo "✅ svelte-check passed for $app"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FAILED -eq 1 ]; then
|
||||
echo ""
|
||||
echo "❌ svelte-check failed! Fix the issues above before committing."
|
||||
echo ""
|
||||
echo "Common fixes:"
|
||||
echo " - Add role and tabindex to interactive divs"
|
||||
echo " - Add onkeydown handler alongside onclick"
|
||||
echo " - Use \$state() for reactive variables in Svelte 5"
|
||||
echo " - Check that all imports resolve correctly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All svelte-checks passed!"
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Migration Validation Script
|
||||
*
|
||||
* Scans migration files for destructive SQL patterns and fails CI if found.
|
||||
* This prevents accidental data loss in production deployments.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/validate-migrations.mjs [--allow-destructive]
|
||||
*
|
||||
* Destructive patterns detected:
|
||||
* - DROP TABLE
|
||||
* - DROP COLUMN
|
||||
* - DROP INDEX (without CONCURRENTLY)
|
||||
* - DROP SCHEMA
|
||||
* - TRUNCATE
|
||||
* - DELETE FROM (without WHERE)
|
||||
*
|
||||
* Safe patterns (allowed):
|
||||
* - DROP ... IF EXISTS (only creates if not exists)
|
||||
* - CREATE ... IF NOT EXISTS
|
||||
* - ALTER TABLE ... ADD COLUMN
|
||||
* - CREATE INDEX CONCURRENTLY
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Configuration
|
||||
const MIGRATION_DIRS = [
|
||||
'services/mana-core-auth/src/db/migrations',
|
||||
// Add other services as needed
|
||||
];
|
||||
|
||||
// Destructive patterns to detect
|
||||
const DESTRUCTIVE_PATTERNS = [
|
||||
{
|
||||
pattern: /DROP\s+TABLE(?!\s+IF\s+EXISTS)/gi,
|
||||
message: 'DROP TABLE without IF EXISTS - will fail if table does not exist and is destructive',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DROP\s+TABLE\s+IF\s+EXISTS\s+(?!.*CASCADE\s*;)/gi,
|
||||
message: 'DROP TABLE IF EXISTS - THIS WILL DELETE DATA',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DROP\s+TABLE.*CASCADE/gi,
|
||||
message: 'DROP TABLE CASCADE - THIS WILL DELETE DATA AND DEPENDENT OBJECTS',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /ALTER\s+TABLE\s+\S+\s+DROP\s+COLUMN/gi,
|
||||
message: 'DROP COLUMN - THIS WILL DELETE DATA',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DROP\s+SCHEMA(?!\s+IF\s+EXISTS)/gi,
|
||||
message: 'DROP SCHEMA without IF EXISTS',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DROP\s+SCHEMA\s+IF\s+EXISTS.*CASCADE/gi,
|
||||
message: 'DROP SCHEMA CASCADE - THIS WILL DELETE ALL TABLES IN SCHEMA',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /TRUNCATE\s+(?:TABLE\s+)?/gi,
|
||||
message: 'TRUNCATE - THIS WILL DELETE ALL DATA IN TABLE',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DELETE\s+FROM\s+\S+\s*(?:;|$)/gim,
|
||||
message: 'DELETE FROM without WHERE clause - THIS WILL DELETE ALL DATA',
|
||||
severity: 'error',
|
||||
},
|
||||
{
|
||||
pattern: /DROP\s+INDEX(?!\s+CONCURRENTLY)/gi,
|
||||
message: 'DROP INDEX without CONCURRENTLY - may cause table locks',
|
||||
severity: 'warning',
|
||||
},
|
||||
];
|
||||
|
||||
// Safe patterns that override destructive checks
|
||||
const SAFE_PATTERNS = [
|
||||
// These patterns indicate safe, idempotent operations
|
||||
/CREATE\s+(?:TABLE|INDEX|SCHEMA|TYPE)\s+IF\s+NOT\s+EXISTS/gi,
|
||||
/DO\s+\$\$.*EXCEPTION.*WHEN\s+duplicate_object/gis, // Safe enum creation pattern
|
||||
];
|
||||
|
||||
function findMigrationFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && entry !== 'meta') {
|
||||
// Check subdirectories for .sql files
|
||||
files.push(...findMigrationFiles(fullPath));
|
||||
} else if (entry.endsWith('.sql')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function validateMigration(filePath) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const issues = [];
|
||||
|
||||
// Check if file uses safe patterns throughout (reserved for future use)
|
||||
// const isSafeFile = SAFE_PATTERNS.some((pattern) => pattern.test(content));
|
||||
void SAFE_PATTERNS; // Silence unused warning - patterns reserved for future enhancements
|
||||
|
||||
for (const { pattern, message, severity } of DESTRUCTIVE_PATTERNS) {
|
||||
// Reset regex lastIndex
|
||||
pattern.lastIndex = 0;
|
||||
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
// Find line number
|
||||
const beforeMatch = content.substring(0, match.index);
|
||||
const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
|
||||
|
||||
issues.push({
|
||||
file: filePath,
|
||||
line: lineNumber,
|
||||
message,
|
||||
severity,
|
||||
match: match[0].trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const allowDestructive = args.includes('--allow-destructive');
|
||||
|
||||
console.log('🔍 Validating migration files for destructive patterns...\n');
|
||||
|
||||
const allIssues = [];
|
||||
let filesChecked = 0;
|
||||
|
||||
for (const dir of MIGRATION_DIRS) {
|
||||
const files = findMigrationFiles(dir);
|
||||
filesChecked += files.length;
|
||||
|
||||
for (const file of files) {
|
||||
const issues = validateMigration(file);
|
||||
allIssues.push(...issues);
|
||||
}
|
||||
}
|
||||
|
||||
// Separate errors and warnings
|
||||
const errors = allIssues.filter((i) => i.severity === 'error');
|
||||
const warnings = allIssues.filter((i) => i.severity === 'warning');
|
||||
|
||||
console.log(`📁 Checked ${filesChecked} migration files\n`);
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log('⚠️ WARNINGS:\n');
|
||||
for (const issue of warnings) {
|
||||
console.log(` ${issue.file}:${issue.line}`);
|
||||
console.log(` ${issue.message}`);
|
||||
console.log(` Found: ${issue.match}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log('❌ DESTRUCTIVE PATTERNS DETECTED:\n');
|
||||
for (const issue of errors) {
|
||||
console.log(` ${issue.file}:${issue.line}`);
|
||||
console.log(` ${issue.message}`);
|
||||
console.log(` Found: ${issue.match}\n`);
|
||||
}
|
||||
|
||||
if (allowDestructive) {
|
||||
console.log('⚠️ --allow-destructive flag set, continuing despite errors\n');
|
||||
console.log('🟡 Migration validation passed with warnings');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('💡 To proceed with destructive migrations, use --allow-destructive flag');
|
||||
console.log(' Or review and update the migration to use safe patterns.\n');
|
||||
console.log('❌ Migration validation FAILED');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Migration validation passed - no destructive patterns found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Runtime Configuration Validator
|
||||
*
|
||||
* Validates that all web apps follow the runtime configuration pattern correctly.
|
||||
* This prevents bugs where apps use build-time env vars or window injection instead
|
||||
* of the proper runtime config loader.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/validate-runtime-config.mjs
|
||||
* pnpm validate:runtime-config
|
||||
*
|
||||
* What it checks:
|
||||
* 1. Required files exist (runtime.ts, docker-entrypoint.sh, Dockerfile)
|
||||
* 2. No window injection patterns (window.__PUBLIC_*)
|
||||
* 3. No direct build-time env imports in stores/api (import.meta.env.PUBLIC_*)
|
||||
* 4. Correct async patterns (no missing await on getAuthUrl(), etc.)
|
||||
* 5. Docker entrypoint generates config.json correctly
|
||||
*/
|
||||
|
||||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const monorepoRoot = join(__dirname, '..');
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
bold: '\x1b[1m',
|
||||
};
|
||||
|
||||
const { reset, red, green, yellow, blue, bold } = colors;
|
||||
|
||||
// Validation results
|
||||
const results = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all web apps in the monorepo
|
||||
*/
|
||||
async function findWebApps() {
|
||||
const webApps = [];
|
||||
const appsDirs = [join(monorepoRoot, 'apps'), join(monorepoRoot, 'games')];
|
||||
|
||||
for (const appsDir of appsDirs) {
|
||||
try {
|
||||
const projects = await readdir(appsDir);
|
||||
for (const project of projects) {
|
||||
const webAppPath = join(appsDir, project, 'apps', 'web');
|
||||
try {
|
||||
const stats = await stat(webAppPath);
|
||||
if (stats.isDirectory()) {
|
||||
webApps.push({
|
||||
path: webAppPath,
|
||||
name: `${relative(monorepoRoot, appsDir)}/${project}/apps/web`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// No web app in this project, skip
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Apps directory doesn't exist, skip
|
||||
}
|
||||
}
|
||||
|
||||
return webApps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists
|
||||
*/
|
||||
async function fileExists(filePath) {
|
||||
try {
|
||||
await stat(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content safely
|
||||
*/
|
||||
async function readFileSafe(filePath) {
|
||||
try {
|
||||
return await readFile(filePath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required files exist
|
||||
*/
|
||||
async function validateRequiredFiles(webApp) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// Check for runtime.ts
|
||||
const runtimeTsPath = join(webApp.path, 'src/lib/config/runtime.ts');
|
||||
if (!(await fileExists(runtimeTsPath))) {
|
||||
errors.push('Missing src/lib/config/runtime.ts');
|
||||
}
|
||||
|
||||
// Check for docker-entrypoint.sh
|
||||
const entrypointPath = join(webApp.path, 'docker-entrypoint.sh');
|
||||
if (!(await fileExists(entrypointPath))) {
|
||||
warnings.push('Missing docker-entrypoint.sh (required for Docker deployment)');
|
||||
}
|
||||
|
||||
// Check for Dockerfile
|
||||
const dockerfilePath = join(webApp.path, 'Dockerfile');
|
||||
if (!(await fileExists(dockerfilePath))) {
|
||||
warnings.push('Missing Dockerfile (required for Docker deployment)');
|
||||
}
|
||||
|
||||
// Check for static/config.json (dev fallback)
|
||||
const configJsonPath = join(webApp.path, 'static/config.json');
|
||||
if (!(await fileExists(configJsonPath))) {
|
||||
warnings.push('Missing static/config.json (dev fallback)');
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for window injection anti-pattern
|
||||
*/
|
||||
async function validateNoWindowInjection(webApp) {
|
||||
const errors = [];
|
||||
const srcPath = join(webApp.path, 'src');
|
||||
|
||||
async function scanDirectory(dir) {
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip node_modules, build artifacts
|
||||
if (!['node_modules', 'build', '.svelte-kit', 'dist'].includes(entry.name)) {
|
||||
await scanDirectory(fullPath);
|
||||
}
|
||||
} else if (entry.name.match(/\.(ts|js|svelte)$/)) {
|
||||
const content = await readFileSafe(fullPath);
|
||||
if (!content) continue;
|
||||
|
||||
// Check for window.__PUBLIC_* pattern
|
||||
const windowInjectionPattern = /window\.__PUBLIC_[A-Z_]+__/g;
|
||||
const matches = content.match(windowInjectionPattern);
|
||||
|
||||
if (matches) {
|
||||
const relativePath = relative(webApp.path, fullPath);
|
||||
errors.push(
|
||||
`${relativePath}: Found window injection pattern (${matches.join(', ')}). Use runtime config instead.`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for (window as any).__PUBLIC_* pattern
|
||||
const windowAsAnyPattern = /\(window as (?:any|unknown)\)\.__PUBLIC_[A-Z_]+__/g;
|
||||
const asAnyMatches = content.match(windowAsAnyPattern);
|
||||
|
||||
if (asAnyMatches) {
|
||||
const relativePath = relative(webApp.path, fullPath);
|
||||
errors.push(
|
||||
`${relativePath}: Found window injection with type assertion (${asAnyMatches.join(', ')}). Use runtime config instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist or can't be read
|
||||
}
|
||||
}
|
||||
|
||||
await scanDirectory(srcPath);
|
||||
return { errors, warnings: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for direct build-time env usage in stores/api files
|
||||
*/
|
||||
async function validateNoBuildTimeEnv(webApp) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const criticalPaths = [
|
||||
join(webApp.path, 'src/lib/stores'),
|
||||
join(webApp.path, 'src/lib/api'),
|
||||
join(webApp.path, 'src/lib/config'),
|
||||
];
|
||||
|
||||
for (const criticalPath of criticalPaths) {
|
||||
if (!(await fileExists(criticalPath))) continue;
|
||||
|
||||
async function scanDirectory(dir) {
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await scanDirectory(fullPath);
|
||||
} else if (entry.name.match(/\.(ts|js)$/)) {
|
||||
const content = await readFileSafe(fullPath);
|
||||
if (!content) continue;
|
||||
|
||||
// Check for import.meta.env.PUBLIC_* in stores/api
|
||||
const envPattern = /import\.meta\.env\.PUBLIC_[A-Z_]+/g;
|
||||
const matches = content.match(envPattern);
|
||||
|
||||
if (matches) {
|
||||
const relativePath = relative(webApp.path, fullPath);
|
||||
// Allow in config files if they have backward compat exports
|
||||
if (
|
||||
relativePath.includes('config/api.ts') ||
|
||||
relativePath.includes('config/env.ts')
|
||||
) {
|
||||
warnings.push(
|
||||
`${relativePath}: Uses build-time env vars (${matches.join(', ')}). Consider migrating to runtime config.`
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
`${relativePath}: Uses build-time env vars (${matches.join(', ')}). Use runtime config instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist or can't be read
|
||||
}
|
||||
}
|
||||
|
||||
await scanDirectory(criticalPath);
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for missing await on async config functions
|
||||
*/
|
||||
async function validateAsyncUsage(webApp) {
|
||||
const errors = [];
|
||||
const srcPath = join(webApp.path, 'src');
|
||||
|
||||
async function scanDirectory(dir) {
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!['node_modules', 'build', '.svelte-kit', 'dist'].includes(entry.name)) {
|
||||
await scanDirectory(fullPath);
|
||||
}
|
||||
} else if (entry.name.match(/\.(ts|js|svelte)$/)) {
|
||||
const content = await readFileSafe(fullPath);
|
||||
if (!content) continue;
|
||||
|
||||
// Check for common async config functions called without await
|
||||
const asyncFunctions = [
|
||||
'getAuthUrl',
|
||||
'getBackendUrl',
|
||||
'getApiBaseUrl',
|
||||
'getTodoApiUrl',
|
||||
'getCalendarApiUrl',
|
||||
'getClockApiUrl',
|
||||
'getContactsApiUrl',
|
||||
'getConfig',
|
||||
];
|
||||
|
||||
for (const funcName of asyncFunctions) {
|
||||
// Pattern: using function in template literal without await
|
||||
// e.g., `${getAuthUrl()}/api` instead of `${await getAuthUrl()}/api`
|
||||
const templateLiteralPattern = new RegExp(`\\$\\{\\s*${funcName}\\(\\)\\s*\\}`, 'g');
|
||||
const matches = content.match(templateLiteralPattern);
|
||||
|
||||
if (matches) {
|
||||
// Check if there's await before it
|
||||
const fullPattern = new RegExp(`await\\s+${funcName}\\(\\)`, 'g');
|
||||
const awaitMatches = content.match(fullPattern);
|
||||
|
||||
// If we found calls without await
|
||||
if (!awaitMatches || matches.length > awaitMatches.length) {
|
||||
const relativePath = relative(webApp.path, fullPath);
|
||||
errors.push(
|
||||
`${relativePath}: Missing 'await' on async function ${funcName}(). This causes "[object Promise]" in URLs.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist or can't be read
|
||||
}
|
||||
}
|
||||
|
||||
await scanDirectory(srcPath);
|
||||
return { errors, warnings: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Docker entrypoint generates config.json correctly
|
||||
*/
|
||||
async function validateDockerEntrypoint(webApp) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const entrypointPath = join(webApp.path, 'docker-entrypoint.sh');
|
||||
const content = await readFileSafe(entrypointPath);
|
||||
|
||||
if (!content) {
|
||||
// Already caught in validateRequiredFiles
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
// Check that it generates config.json
|
||||
if (!content.includes('config.json')) {
|
||||
errors.push('docker-entrypoint.sh does not generate config.json');
|
||||
}
|
||||
|
||||
// Check that it uses relative paths (not absolute like /app/build/client/config.json)
|
||||
if (content.includes('> /app/build/client/config.json')) {
|
||||
errors.push(
|
||||
'docker-entrypoint.sh uses absolute path for config.json. Use relative path (build/client/config.json) instead.'
|
||||
);
|
||||
}
|
||||
|
||||
// Check that it has mkdir -p for config directory
|
||||
if (!content.includes('mkdir -p')) {
|
||||
warnings.push('docker-entrypoint.sh should include "mkdir -p build/client" for safety');
|
||||
}
|
||||
|
||||
// Check that it executes the CMD with exec "$@"
|
||||
if (!content.includes('exec "$@"')) {
|
||||
warnings.push('docker-entrypoint.sh should end with: exec "$@"');
|
||||
}
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single web app
|
||||
*/
|
||||
async function validateWebApp(webApp) {
|
||||
console.log(`\n${blue}${bold}Checking:${reset} ${webApp.name}`);
|
||||
|
||||
const checks = [
|
||||
{ name: 'Required files', fn: validateRequiredFiles },
|
||||
{ name: 'No window injection', fn: validateNoWindowInjection },
|
||||
{ name: 'No build-time env in stores/api', fn: validateNoBuildTimeEnv },
|
||||
{ name: 'Async function usage', fn: validateAsyncUsage },
|
||||
{ name: 'Docker entrypoint', fn: validateDockerEntrypoint },
|
||||
];
|
||||
|
||||
const allErrors = [];
|
||||
const allWarnings = [];
|
||||
|
||||
for (const check of checks) {
|
||||
const { errors, warnings } = await check.fn(webApp);
|
||||
if (errors.length > 0) {
|
||||
allErrors.push(...errors);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
allWarnings.push(...warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// Print results
|
||||
if (allErrors.length === 0 && allWarnings.length === 0) {
|
||||
console.log(`${green}✓${reset} All checks passed`);
|
||||
results.passed.push(webApp.name);
|
||||
} else {
|
||||
if (allErrors.length > 0) {
|
||||
console.log(`${red}✗${reset} ${allErrors.length} error(s):`);
|
||||
allErrors.forEach((error) => console.log(` ${red}•${reset} ${error}`));
|
||||
results.failed.push({ name: webApp.name, errors: allErrors });
|
||||
}
|
||||
|
||||
if (allWarnings.length > 0) {
|
||||
console.log(`${yellow}⚠${reset} ${allWarnings.length} warning(s):`);
|
||||
allWarnings.forEach((warning) => console.log(` ${yellow}•${reset} ${warning}`));
|
||||
results.warnings.push({ name: webApp.name, warnings: allWarnings });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation function
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${bold}Runtime Configuration Validator${reset}\n`);
|
||||
console.log('Scanning for web apps...\n');
|
||||
|
||||
const webApps = await findWebApps();
|
||||
|
||||
if (webApps.length === 0) {
|
||||
console.log(`${yellow}No web apps found${reset}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${webApps.length} web app(s)\n`);
|
||||
|
||||
// Validate each web app
|
||||
for (const webApp of webApps) {
|
||||
await validateWebApp(webApp);
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(`\n${bold}═══════════════════════════════════════${reset}`);
|
||||
console.log(`${bold}Summary${reset}\n`);
|
||||
console.log(`${green}✓${reset} Passed: ${results.passed.length}`);
|
||||
console.log(`${yellow}⚠${reset} Warnings: ${results.warnings.length}`);
|
||||
console.log(`${red}✗${reset} Failed: ${results.failed.length}`);
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log(`\n${red}${bold}Failed apps:${reset}`);
|
||||
results.failed.forEach(({ name }) => console.log(` ${red}•${reset} ${name}`));
|
||||
}
|
||||
|
||||
// Exit with error if any validations failed
|
||||
if (results.failed.length > 0) {
|
||||
console.log(`\n${red}${bold}Validation failed!${reset}`);
|
||||
console.log('\nFix the errors above to ensure runtime configuration is implemented correctly.');
|
||||
console.log('See docs/RUNTIME_CONFIG.md for implementation guide.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n${green}${bold}All validations passed!${reset}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`${red}${bold}Validation script error:${reset}`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue