Adds the composable monorepo template system that generates project skeletons with pluggable components (service, worker, app-react, app-astro, cli). Key changes: - Monorepo skeleton templates with shared pkg/, scripts/, and git hooks - Component templates (service, worker, app-react, app-astro, cli) with Dockerfiles, CI steps, and component.yaml manifests - Component domain model with validation and dependency resolution - Component handler endpoints for CRUD and composition - Template provider extended with BuildComposableProject and component assembly - Deployer extended with composable project deployment support - Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning) - envutil package for centralized env var reads with defaults - api.DecodeJSON helper for standardized request body decoding - Standardized response helpers (WriteBadRequest, WriteNotFound, etc.) - Replaced fullstack-app cookbook with composable-app cookbook - Hardened handler timeouts, logging, and error responses across all handlers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
325 lines
9.5 KiB
Bash
Executable File
325 lines
9.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Landing Page Cookbook Test Script
|
|
# Tests the composable landing page flow from cookbooks/landing-page.md
|
|
#
|
|
# Flow:
|
|
# 1. Create project (monorepo skeleton)
|
|
# 2. Add app-astro component
|
|
# 3. Wait for CI pipeline
|
|
# 4. Verify site is live
|
|
#
|
|
# Usage:
|
|
# ./cookbooks/scripts/landing-test.sh run [name] # Run the full flow
|
|
# ./cookbooks/scripts/landing-test.sh teardown [name] # Clean up test resources
|
|
# ./cookbooks/scripts/landing-test.sh status [name] # Check current status
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}"
|
|
API_KEY="${RDEV_API_KEY:?RDEV_API_KEY environment variable required}"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
# Timeouts
|
|
PIPELINE_TIMEOUT=300 # 5 minutes max wait for CI pipeline
|
|
PIPELINE_POLL_INTERVAL=10
|
|
SITE_TIMEOUT=120 # 2 minutes max wait for site to be live
|
|
|
|
api_call() {
|
|
local method="$1"
|
|
local endpoint="$2"
|
|
local data="${3:-}"
|
|
|
|
if [[ -n "$data" ]]; then
|
|
curl -s -X "$method" "${API_URL}${endpoint}" \
|
|
-H "X-API-Key: ${API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$data"
|
|
else
|
|
curl -s -X "$method" "${API_URL}${endpoint}" \
|
|
-H "X-API-Key: ${API_KEY}"
|
|
fi
|
|
}
|
|
|
|
check_health() {
|
|
log_info "Checking API health..."
|
|
local response
|
|
response=$(curl -s "${API_URL}/health")
|
|
if echo "$response" | jq -e '.data.status == "ok"' > /dev/null 2>&1; then
|
|
log_success "API is healthy"
|
|
return 0
|
|
else
|
|
log_error "API health check failed"
|
|
echo "$response" | jq .
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Wait for pipeline to appear and complete
|
|
wait_for_pipeline() {
|
|
local project_name="$1"
|
|
local start_time=$(date +%s)
|
|
local pipeline_found=false
|
|
local pipeline_number=""
|
|
local pipeline_status=""
|
|
|
|
log_info "Waiting for CI pipeline to start (timeout: ${PIPELINE_TIMEOUT}s)..."
|
|
|
|
while true; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
if [[ $elapsed -ge $PIPELINE_TIMEOUT ]]; then
|
|
log_error "Pipeline timeout after ${PIPELINE_TIMEOUT}s"
|
|
return 1
|
|
fi
|
|
|
|
local response
|
|
response=$(api_call GET "/projects/$project_name/pipelines" 2>/dev/null || echo "{}")
|
|
|
|
# Check if we have pipelines
|
|
local pipeline_count
|
|
pipeline_count=$(echo "$response" | jq -r '.data | length' 2>/dev/null || echo "0")
|
|
|
|
if [[ "$pipeline_count" -gt 0 ]]; then
|
|
if [[ "$pipeline_found" == "false" ]]; then
|
|
pipeline_found=true
|
|
pipeline_number=$(echo "$response" | jq -r '.data[0].number')
|
|
log_success "Pipeline #$pipeline_number started"
|
|
fi
|
|
|
|
# Get latest pipeline status
|
|
pipeline_status=$(echo "$response" | jq -r '.data[0].status')
|
|
|
|
case "$pipeline_status" in
|
|
success)
|
|
echo ""
|
|
log_success "Pipeline #$pipeline_number completed successfully (${elapsed}s)"
|
|
return 0
|
|
;;
|
|
failure|error|killed|declined)
|
|
echo ""
|
|
log_error "Pipeline #$pipeline_number failed with status: $pipeline_status"
|
|
echo "$response" | jq '.data[0]'
|
|
return 1
|
|
;;
|
|
running|pending)
|
|
echo -ne "\r${BLUE}[INFO]${NC} Pipeline #$pipeline_number status: $pipeline_status (${elapsed}s)... "
|
|
;;
|
|
*)
|
|
echo -ne "\r${BLUE}[INFO]${NC} Pipeline #$pipeline_number status: $pipeline_status (${elapsed}s)... "
|
|
;;
|
|
esac
|
|
else
|
|
echo -ne "\r${BLUE}[INFO]${NC} Waiting for pipeline to start (${elapsed}s)... "
|
|
fi
|
|
|
|
sleep $PIPELINE_POLL_INTERVAL
|
|
done
|
|
}
|
|
|
|
# Wait for site to be accessible
|
|
wait_for_site() {
|
|
local domain="$1"
|
|
local start_time=$(date +%s)
|
|
|
|
log_info "Waiting for site to be live: https://$domain (timeout: ${SITE_TIMEOUT}s)..."
|
|
|
|
while true; do
|
|
local elapsed=$(($(date +%s) - start_time))
|
|
if [[ $elapsed -ge $SITE_TIMEOUT ]]; then
|
|
log_warn "Site timeout after ${SITE_TIMEOUT}s - may still be deploying"
|
|
return 1
|
|
fi
|
|
|
|
local http_code
|
|
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "https://$domain" 2>/dev/null || echo "000")
|
|
|
|
if [[ "$http_code" == "200" ]]; then
|
|
echo ""
|
|
log_success "Site is live! HTTP $http_code (${elapsed}s)"
|
|
return 0
|
|
elif [[ "$http_code" == "000" ]]; then
|
|
echo -ne "\r${BLUE}[INFO]${NC} Waiting for site... (${elapsed}s, connection failed) "
|
|
else
|
|
echo -ne "\r${BLUE}[INFO]${NC} Waiting for site... (${elapsed}s, HTTP $http_code) "
|
|
fi
|
|
|
|
sleep 3
|
|
done
|
|
}
|
|
|
|
# Main run flow
|
|
run_flow() {
|
|
local project_name="$1"
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Landing Page E2E Test (Composable)"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "Project: $project_name"
|
|
echo ""
|
|
|
|
# Step 0: Health check
|
|
check_health || exit 1
|
|
echo ""
|
|
|
|
# Step 1: Create project (monorepo skeleton)
|
|
log_info "Step 1: Creating project skeleton..."
|
|
|
|
local create_response
|
|
create_response=$(api_call POST "/projects" "{\"name\": \"$project_name\", \"description\": \"Landing page E2E test\"}")
|
|
|
|
local domain
|
|
domain=$(echo "$create_response" | jq -r '.data.domain // ""')
|
|
|
|
if [[ -z "$domain" || "$domain" == "null" ]]; then
|
|
log_error "Failed to create project"
|
|
echo "$create_response" | jq .
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Project created: $project_name"
|
|
echo " Domain: $domain"
|
|
echo " Git: https://git.threesix.ai/jordan/$project_name"
|
|
echo ""
|
|
|
|
# Step 2: Add app-astro component
|
|
log_info "Step 2: Adding landing page component (app-astro)..."
|
|
|
|
local component_response
|
|
component_response=$(api_call POST "/projects/$project_name/components" '{"type": "app", "name": "landing", "template": "app-astro"}')
|
|
|
|
local component_path
|
|
component_path=$(echo "$component_response" | jq -r '.data.path // ""')
|
|
|
|
if [[ -z "$component_path" || "$component_path" == "null" ]]; then
|
|
log_error "Failed to add component"
|
|
echo "$component_response" | jq .
|
|
exit 1
|
|
fi
|
|
|
|
local component_port
|
|
component_port=$(echo "$component_response" | jq -r '.data.port // "N/A"')
|
|
|
|
log_success "Component added: $component_path (port: $component_port)"
|
|
echo ""
|
|
|
|
# Step 3: Wait for pipeline
|
|
log_info "Step 3: Waiting for CI pipeline..."
|
|
echo ""
|
|
|
|
if ! wait_for_pipeline "$project_name"; then
|
|
log_warn "Pipeline failed, but continuing to check if site is accessible..."
|
|
fi
|
|
echo ""
|
|
|
|
# Step 4: Wait for site
|
|
log_info "Step 4: Verifying site is accessible..."
|
|
|
|
if wait_for_site "$domain"; then
|
|
log_success "Site verified!"
|
|
else
|
|
log_warn "Site not accessible yet, may need more time"
|
|
fi
|
|
echo ""
|
|
|
|
# Summary
|
|
echo "=========================================="
|
|
echo " Test Complete"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo " Site URL: https://$domain"
|
|
echo " Git: https://git.threesix.ai/jordan/$project_name"
|
|
echo " CI: https://ci.threesix.ai/jordan/$project_name"
|
|
echo ""
|
|
echo " To customize: POST /projects/$project_name/builds with a prompt"
|
|
echo " To teardown: $0 teardown $project_name"
|
|
echo ""
|
|
}
|
|
|
|
# Check status
|
|
check_status() {
|
|
local project_name="$1"
|
|
|
|
echo ""
|
|
echo "=== Project Status: $project_name ==="
|
|
echo ""
|
|
|
|
log_info "Project info:"
|
|
api_call GET "/projects/$project_name" | jq '.data // .'
|
|
echo ""
|
|
|
|
log_info "Components:"
|
|
api_call GET "/projects/$project_name/components" | jq '.data // .'
|
|
echo ""
|
|
|
|
log_info "Latest pipelines:"
|
|
api_call GET "/projects/$project_name/pipelines" | jq '.data[:3] // .'
|
|
echo ""
|
|
}
|
|
|
|
# Teardown
|
|
teardown() {
|
|
local project_name="$1"
|
|
|
|
echo ""
|
|
log_info "Tearing down project: $project_name"
|
|
|
|
local response
|
|
response=$(api_call DELETE "/projects/$project_name")
|
|
|
|
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
|
|
log_error "Teardown failed"
|
|
echo "$response" | jq .
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Project deleted (Gitea repo preserved)"
|
|
echo "$response" | jq '.data // .'
|
|
echo ""
|
|
}
|
|
|
|
# Parse command
|
|
COMMAND="${1:-}"
|
|
PROJECT_NAME="${2:-landing-test-$(date +%s)}"
|
|
|
|
case "$COMMAND" in
|
|
run)
|
|
run_flow "$PROJECT_NAME"
|
|
;;
|
|
status)
|
|
check_status "$PROJECT_NAME"
|
|
;;
|
|
teardown)
|
|
teardown "$PROJECT_NAME"
|
|
;;
|
|
*)
|
|
echo "Landing Page E2E Test Script"
|
|
echo ""
|
|
echo "Usage: $0 <command> [project-name]"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " run Run the full composable landing page flow"
|
|
echo " status Check project and component status"
|
|
echo " teardown Delete project (preserves git repo)"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 run my-landing"
|
|
echo " $0 status my-landing"
|
|
echo " $0 teardown my-landing"
|
|
echo ""
|
|
exit 1
|
|
;;
|
|
esac
|