rdev/cookbooks/scripts/template-validation.sh
jordan 572b221e20 feat: add automatic cleanup for cookbook test projects
- Add AUTO_TEARDOWN env var and --auto-teardown flag to cookbook scripts
- Scripts automatically delete created projects on exit (including Ctrl+C)
- Add DELETE /projects/cleanup API endpoint for bulk cleanup
- Supports shell-style glob patterns (e.g., "tree-test-*")
- Includes dry_run mode and older_than_hours filter for safety
- Requires admin scope for actual deletion
- Update cookbook scripts: landing-test, composable-test, template-validation,
  feature-test, tree-runner

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 17:54:15 -07:00

412 lines
11 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Template Validation Script
# Validates all rdev templates by:
# 1. Creating a test project
# 2. Adding each component type
# 3. Verifying files exist and compile
# 4. Running CI pipeline
# 5. Cleaning up
#
# Usage: ./cookbooks/scripts/template-validation.sh <command>
# Commands: run, quick, cleanup
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Parse --auto-teardown flag from args
ARGS=("$@")
for i in "${!ARGS[@]}"; do
if [[ "${ARGS[$i]}" == "--auto-teardown" ]]; then
AUTO_TEARDOWN="true"
unset 'ARGS[$i]'
fi
done
ARGS=("${ARGS[@]}") # Re-index array
COMMAND="${ARGS[0]:-run}"
PROJECT_NAME="template-validation-$(date +%s)"
# Register cleanup trap for auto-teardown
register_cleanup_trap
# Component types to test
COMPONENT_TYPES=(
"service"
"worker"
"app-astro"
"app-react"
"app-nextjs"
"cli"
)
print_banner() {
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ rdev Template Validation - Full Integration Test ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
validate_template_compilation() {
local project_id="$1"
local git_owner
git_owner=$(get_git_owner)
print_header "Validating Template Compilation"
local failures=0
# Check each key file exists
echo "Checking skeleton files..."
local files_to_check=(
"CLAUDE.md"
"README.md"
"go.work"
"pnpm-workspace.yaml"
".woodpecker.yml"
".golangci.yml"
"docker-compose.yml"
"pkg/README.md"
"scripts/dev.sh"
"scripts/discover.sh"
"scripts/quality.sh"
"scripts/install.sh"
)
for file in "${files_to_check[@]}"; do
local check
check=$(curl -s -o /dev/null -w "%{http_code}" \
"https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$file" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null)
if [[ "$check" == "200" ]]; then
print_success "$file"
else
print_error "$file (HTTP $check)"
((failures++))
fi
done
# Check pkg/ packages
echo ""
echo "Checking pkg/ packages..."
local pkg_dirs=(
"pkg/app"
"pkg/httperror"
"pkg/httpresponse"
"pkg/httpvalidation"
"pkg/middleware"
"pkg/auth"
)
for pkg_dir in "${pkg_dirs[@]}"; do
local check
check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$pkg_dir" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r 'if type == "array" then "ok" else "not found" end')
if [[ "$check" == "ok" ]]; then
print_success "$pkg_dir/"
else
print_warning "$pkg_dir/ (may be optional)"
fi
done
# Check packages/
echo ""
echo "Checking packages/..."
local packages=(
"packages/ui"
"packages/layout"
"packages/auth"
"packages/logger"
"packages/api-client"
)
for pkg in "${packages[@]}"; do
local check
check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$pkg/package.json" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r '.name // "not found"')
if [[ "$check" == "package.json" ]]; then
print_success "$pkg/"
else
print_warning "$pkg/ (may be optional)"
fi
done
echo ""
if [[ $failures -gt 0 ]]; then
print_error "Template compilation validation: $failures failures"
return 1
else
print_success "Template compilation validation: all required files present"
return 0
fi
}
validate_component() {
local project_id="$1"
local comp_type="$2"
local comp_name="$3"
local git_owner
git_owner=$(get_git_owner)
echo ""
echo "Validating $comp_type component..."
# Determine expected directory
local comp_dir
case "$comp_type" in
service)
comp_dir="services/$comp_name"
;;
worker)
comp_dir="workers/$comp_name"
;;
app-astro|app-react|app-nextjs)
comp_dir="apps/$comp_name"
;;
cli)
comp_dir="cli/$comp_name"
;;
esac
# Check component directory exists
local check
check=$(curl -s "https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$comp_dir" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null | jq -r 'if type == "array" then "ok" else "not found" end')
if [[ "$check" == "ok" ]]; then
print_success "$comp_type: $comp_dir/ exists"
else
print_error "$comp_type: $comp_dir/ not found"
return 1
fi
# Check component.yaml exists
local yaml_check
yaml_check=$(curl -s -o /dev/null -w "%{http_code}" \
"https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$comp_dir/component.yaml" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null)
if [[ "$yaml_check" == "200" ]]; then
print_success "$comp_type: component.yaml exists"
else
print_warning "$comp_type: component.yaml not found"
fi
# Check Dockerfile exists
local docker_check
docker_check=$(curl -s -o /dev/null -w "%{http_code}" \
"https://git.threesix.ai/api/v1/repos/$git_owner/$project_id/contents/$comp_dir/Dockerfile" \
-H "Authorization: token ${GITEA_TOKEN:-}" 2>/dev/null)
if [[ "$docker_check" == "200" ]]; then
print_success "$comp_type: Dockerfile exists"
else
print_warning "$comp_type: Dockerfile not found (may be optional)"
fi
return 0
}
add_component() {
local comp_type="$1"
local comp_name="$2"
echo "Adding $comp_type component: $comp_name"
local payload
payload=$(jq -n \
--arg type "$comp_type" \
--arg name "$comp_name" \
'{type: $type, name: $name}')
local result
result=$(api_call POST "/projects/$PROJECT_NAME/components" "$payload")
local path
path=$(echo "$result" | jq -r '.data.path // .path // ""')
if [[ -z "$path" ]]; then
print_error "Failed to add $comp_type component"
echo "$result" | jq '.'
return 1
fi
print_success "Added $comp_type/$comp_name at $path"
return 0
}
run_full_validation() {
print_banner
# Step 1: Create project
print_header "Step 1: Creating Test Project"
echo "Project name: $PROJECT_NAME"
local create_payload
create_payload=$(jq -n \
--arg name "$PROJECT_NAME" \
--arg desc "Template validation test project" \
'{name: $name, description: $desc}')
local create_result
create_result=$(api_call POST "/project" "$create_payload")
local domain
domain=$(echo "$create_result" | jq -r '.data.domain // .domain // ""')
if [[ -z "$domain" ]]; then
print_error "Failed to create project"
echo "$create_result" | jq '.'
exit 1
fi
# Track project for auto-cleanup
CLEANUP_PROJECT="$PROJECT_NAME"
print_success "Project created with domain: $domain"
# Step 2: Add all component types
print_header "Step 2: Adding Components"
local comp_names=()
for comp_type in "${COMPONENT_TYPES[@]}"; do
local comp_name
case "$comp_type" in
service)
comp_name="api"
;;
worker)
comp_name="jobs"
;;
app-astro)
comp_name="landing"
;;
app-react)
comp_name="web"
;;
app-nextjs)
comp_name="dashboard"
;;
cli)
comp_name="ctl"
;;
esac
if add_component "$comp_type" "$comp_name"; then
comp_names+=("$comp_type:$comp_name")
fi
done
# Wait for git to sync
print_header "Step 3: Waiting for Git Sync"
echo "Waiting 15 seconds for git to propagate..."
sleep 15
# Step 4: Validate skeleton files
print_header "Step 4: Validating Skeleton"
validate_template_compilation "$PROJECT_NAME" || true
# Step 5: Validate each component
print_header "Step 5: Validating Components"
for comp in "${comp_names[@]}"; do
local type="${comp%%:*}"
local name="${comp##*:}"
validate_component "$PROJECT_NAME" "$type" "$name" || true
done
# Step 6: Check CI pipeline
print_header "Step 6: Checking CI Pipeline"
if wait_for_pipeline "$PROJECT_NAME" 60 5; then
print_success "CI pipeline completed successfully"
else
print_warning "CI pipeline did not complete (may need investigation)"
fi
# Summary
print_header "Validation Summary"
echo "Project: $PROJECT_NAME"
echo "Domain: https://$domain"
echo "Git: https://git.threesix.ai/$(get_git_owner)/$PROJECT_NAME"
echo "CI: https://ci.threesix.ai/$(get_git_owner)/$PROJECT_NAME"
echo ""
echo "Components tested:"
for comp in "${comp_names[@]}"; do
echo " - $comp"
done
echo ""
print_warning "Remember to clean up: $0 cleanup $PROJECT_NAME"
}
run_quick_validation() {
print_banner
echo "Quick validation: checking template API responses only"
echo ""
# Check skeleton template info
print_header "Checking Skeleton Template"
local skeleton_info
skeleton_info=$(api_call GET "/templates/skeleton")
echo "$skeleton_info" | jq '.data // .'
# Check component templates
print_header "Checking Component Templates"
local comp_templates
comp_templates=$(api_call GET "/templates/components")
echo "$comp_templates" | jq '.data.components // .data // .'
# Verify each component type exists
print_header "Verifying Component Types"
for comp_type in "${COMPONENT_TYPES[@]}"; do
local info
info=$(api_call GET "/templates/components/$comp_type" 2>/dev/null || echo '{}')
local found
found=$(echo "$info" | jq -r '.data.type // ""')
if [[ "$found" == "$comp_type" ]]; then
print_success "$comp_type template available"
else
print_error "$comp_type template not found"
fi
done
}
cleanup() {
local project_to_delete="${2:-$PROJECT_NAME}"
print_header "Cleaning Up: $project_to_delete"
local result
result=$(api_call DELETE "/project/$project_to_delete")
echo "$result" | jq '.'
print_success "Project deleted. Gitea repo preserved."
}
case "$COMMAND" in
run)
run_full_validation
;;
quick)
run_quick_validation
;;
cleanup)
cleanup "$@"
;;
*)
echo "Usage: $0 <command>"
echo "Commands:"
echo " run - Full validation (creates project, adds all components, checks CI)"
echo " quick - Quick check (API responses only, no project creation)"
echo " cleanup - Delete test project (optionally specify project name)"
exit 1
;;
esac