Weeks 1-7 of the template upgrade plan: - pkg/api: typed HTTPError with sentinels, Wrap/WrapMiddleware, Bind, health probes, OpenAPI schema/param builders - skeleton/packages: ui (design tokens, components), layout (DashboardShell), auth (AuthProvider, ProtectedRoute), api-client - skeleton/pkg: httperror, app/handler, app/bind, app/health, auth (JWT/API key middleware) - components/app-nextjs: Next.js 14 App Router template with dashboard, server actions, auth - cookbooks/feature-development.md with test and validation scripts - Handler tests for components, project management, and woodpecker webhook - 3 rounds of code review fixes applied Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
396 lines
11 KiB
Bash
Executable File
396 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"
|
|
|
|
COMMAND="${1:-run}"
|
|
PROJECT_NAME="template-validation-$(date +%s)"
|
|
|
|
# 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
|
|
|
|
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
|