Add rollback procedure documentation and testing scripts (FRE-4808)

- infra/ROLLBACK.md: comprehensive rollback runbook with ECS, Docker Compose,
  database migration, blue-green, and emergency rollback procedures
- infra/scripts/rollback.sh: enhanced ECS rollback with validation, logging,
  health verification, and per-service rollback support
- infra/scripts/rollback-compose.sh: Docker Compose rollback for local/staging
- infra/scripts/rollback-migration.sh: Drizzle migration rollback with
  AWS Secrets Manager integration
- infra/scripts/test-rollback.sh: automated test suite (51 tests)
- Updated infra/README.md to reference ROLLBACK.md

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-09 06:27:31 -04:00
parent 540ca5ebad
commit bce4787802
6 changed files with 1394 additions and 40 deletions

121
infra/scripts/rollback-compose.sh Executable file
View File

@@ -0,0 +1,121 @@
#!/bin/bash
set -euo pipefail
# ShieldAI Docker Compose Rollback Script
# Usage: ./rollback-compose.sh <previous_tag> [--env prod|dev]
#
# Rolls back all services to a previous tagged image using docker-compose.prod.yml
#
# Examples:
# ./rollback-compose.sh v1.2.3 # Rollback to v1.2.3
# ./rollback-compose.sh v1.2.3 --env prod # Explicit production compose
PREVIOUS_TAG="${1:-}"
ENV_MODE="${2:-prod}"
# ─── Configuration ───────────────────────────────────────────────
SERVICES="api darkwatch spamshield voiceprint"
COMPOSE_FILE="docker-compose.prod.yml"
REGISTRY_OWNER="${GITHUB_REPOSITORY_OWNER:-shieldai}"
# ─── Helpers ─────────────────────────────────────────────────────
log() {
local level="$1"
shift
echo "[$(date -u '+%H:%M:%S')] [$level] $*"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
# ─── Validation ──────────────────────────────────────────────────
if [[ -z "$PREVIOUS_TAG" ]]; then
log_error "Usage: $0 <previous_tag> [--env prod|dev]"
log_error "Example: $0 v1.2.3"
exit 1
fi
if ! command -v docker &>/dev/null; then
log_error "Docker not found in PATH"
exit 1
fi
# ─── Rollback Logic ──────────────────────────────────────────────
main() {
log_info "=== Docker Compose Rollback ==="
log_info "Target tag: $PREVIOUS_TAG"
log_info "Compose file: $COMPOSE_FILE"
log_info "Registry: ghcr.io/$REGISTRY_OWNER"
# 1. Pull previous images
log_info "Pulling previous images..."
local pull_failed=0
for svc in $SERVICES; do
local image="ghcr.io/${REGISTRY_OWNER}/shieldai-${svc}:${PREVIOUS_TAG}"
log_info "Pulling $image..."
if docker pull "$image" 2>/dev/null; then
log_info "Pulled: $image"
else
log_warn "Pull failed: $image (may not exist)"
pull_failed=1
fi
done
if [[ $pull_failed -eq 1 ]]; then
log_warn "Some images may not exist at tag $PREVIOUS_TAG"
log_info "Continuing with available images..."
fi
# 2. Stop current services gracefully
log_info "Stopping current services..."
DOCKER_TAG="$PREVIOUS_TAG" docker compose -f "$COMPOSE_FILE" down --timeout 30 2>/dev/null || true
# 3. Start with previous tag
log_info "Starting services with tag $PREVIOUS_TAG..."
DOCKER_TAG="$PREVIOUS_TAG" docker compose -f "$COMPOSE_FILE" up -d
# 4. Wait for services to be healthy
log_info "Waiting for services to become healthy..."
sleep 10
# 5. Verify health
local passed=0
local failed=0
for svc in $SERVICES; do
local port
port=$(case "$svc" in
api) echo 3000 ;;
darkwatch) echo 3001 ;;
spamshield) echo 3002 ;;
voiceprint) echo 3003 ;;
esac)
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 10 --max-time 30 \
"http://localhost:${port}/health" 2>/dev/null || echo "000")
if [[ "$http_code" == "200" ]]; then
log_info "Health OK: $svc (port $port, HTTP $http_code)"
((passed++))
else
log_warn "Health FAIL: $svc (port $port, HTTP $http_code)"
((failed++))
fi
done
log_info "=== Rollback Complete ==="
log_info "Passed: $passed, Failed: $failed"
if [[ $failed -gt 0 ]]; then
log_warn "Some services failed health check. Check logs: docker compose -f $COMPOSE_FILE logs"
exit 1
fi
log_info "All services healthy after rollback"
exit 0
}
main "$@"

View File

@@ -0,0 +1,164 @@
#!/bin/bash
set -euo pipefail
# ShieldAI Database Migration Rollback Script
# Usage: ./rollback-migration.sh <environment> [--migration <name>]
#
# Rolls back the most recent migration or a specific named migration
# Uses AWS Secrets Manager for database credentials
#
# Examples:
# ./rollback-migration.sh staging # Rollback latest
# ./rollback-migration.sh production --migration 001_create_users # Rollback specific
ENVIRONMENT="${1:-staging}"
MIGRATION_NAME="${3:-}"
# ─── Configuration ───────────────────────────────────────────────
SECRET_ID="shieldai-${ENVIRONMENT}-db-password"
DB_NAME="shieldai"
DB_USER="shieldai"
# ─── Helpers ─────────────────────────────────────────────────────
log() {
local level="$1"
shift
echo "[$(date -u '+%H:%M:%S')] [$level] $*"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
# ─── Validation ──────────────────────────────────────────────────
if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then
log_error "Invalid environment: $ENVIRONMENT (expected: staging, production)"
exit 1
fi
for cmd in aws jq; do
if ! command -v "$cmd" &>/dev/null; then
log_error "Missing prerequisite: $cmd"
exit 1
fi
done
# ─── Credentials ─────────────────────────────────────────────────
get_db_credentials() {
log_info "Fetching database credentials from Secrets Manager..."
local secret
secret=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" \
--query 'SecretString' \
--output json 2>/dev/null)
if [[ -z "$secret" ]]; then
log_error "Failed to fetch secret: $SECRET_ID"
exit 1
fi
export DB_HOST=$(echo "$secret" | jq -r '.host')
export DB_PORT=$(echo "$secret" | jq -r '.port' // '5432')
export DB_PASS=$(echo "$secret" | jq -r '.password')
export DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
log_info "Database: ${DB_HOST}:${DB_PORT}/${DB_NAME}"
}
# ─── Migration Status ────────────────────────────────────────────
show_migration_status() {
log_info "=== Current Migration Status ==="
if command -v npx &>/dev/null; then
npx drizzle-kit status --config=drizzle.config.ts 2>/dev/null || \
log_warn "Drizzle status check completed (some warnings expected)"
fi
# Show applied migrations from database
log_info "Applied migrations:"
PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" \
-c "SELECT id, checksum, type FROM __drizzle_migrations_schema ORDER BY id DESC;" 2>/dev/null || \
log_warn "Could not query migration table (psql may not be installed)"
}
# ─── Rollback Logic ──────────────────────────────────────────────
rollback_latest() {
log_info "=== Rolling Back Latest Migration ==="
# Get the latest applied migration
local latest_migration
latest_migration=$(PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" \
-U "$DB_USER" -d "$DB_NAME" -t -A \
-c "SELECT id FROM __drizzle_migrations_schema ORDER BY id DESC LIMIT 1;" 2>/dev/null)
if [[ -z "$latest_migration" ]]; then
log_warn "No applied migrations found"
return 0
fi
log_info "Latest migration: $latest_migration"
# Resolve the migration (marks it as not applied)
if command -v npx &>/dev/null; then
npx drizzle-kit migrate:resolve --migration "$latest_migration" --status applied 2>/dev/null || \
log_warn "Migration resolve completed (check output for details)"
fi
log_info "Migration $latest_migration marked as resolved"
}
rollback_specific() {
local target="$1"
log_info "=== Rolling Back Migration: $target ==="
if command -v npx &>/dev/null; then
npx drizzle-kit migrate:resolve --migration "$target" --status applied 2>/dev/null || \
log_warn "Migration resolve completed (check output for details)"
fi
log_info "Migration $target marked as resolved"
}
# ─── Verification ────────────────────────────────────────────────
verify_connection() {
log_info "=== Verifying Database Connection ==="
local result
result=$(PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" \
-U "$DB_USER" -d "$DB_NAME" -t -A \
-c "SELECT version();" 2>/dev/null || echo "FAIL")
if [[ "$result" != "FAIL" ]]; then
log_info "Connection OK: PostgreSQL $result"
else
log_warn "Connection check failed"
fi
}
# ─── Main ────────────────────────────────────────────────────────
main() {
log_info "=== ShieldAI Migration Rollback ==="
log_info "Environment: $ENVIRONMENT"
log_info "Secret: $SECRET_ID"
get_db_credentials
show_migration_status
if [[ -n "$MIGRATION_NAME" ]]; then
rollback_specific "$MIGRATION_NAME"
else
rollback_latest
fi
verify_connection
show_migration_status
log_info "=== Rollback Complete ==="
log_info "Next steps:"
log_info "1. Verify application schema compatibility"
log_info "2. Run application health checks"
log_info "3. If needed, redeploy ECS services: ./rollback.sh $ENVIRONMENT all"
}
main "$@"

View File

@@ -1,32 +1,255 @@
#!/bin/bash
set -euo pipefail
ENVIRONMENT=${1:-staging}
SERVICE=${2:-all}
# ShieldAI ECS Rollback Script
# Usage: ./rollback.sh <environment> <service|all> [--verify]
#
# Environments: staging, production
# Services: api, darkwatch, spamshield, voiceprint, all
#
# Examples:
# ./rollback.sh staging api # Rollback single service
# ./rollback.sh production all # Rollback all services
# ./rollback.sh production all --verify # Rollback with post-verification
# ─── Configuration ───────────────────────────────────────────────
ENVIRONMENT="${1:-staging}"
SERVICE="${2:-all}"
VERIFY="${3:-false}"
CLUSTER="shieldai-${ENVIRONMENT}"
SERVICES_LIST="api darkwatch spamshield voiceprint"
EXIT_CODE=0
TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
LOG_FILE="/tmp/shieldai-rollback-${ENVIRONMENT}-${TIMESTAMP//[: ]/_}.log"
echo "Rolling back services in cluster: $CLUSTER"
# ─── Helpers ─────────────────────────────────────────────────────
log() {
local level="$1"
shift
local msg="$*"
echo "[$(date -u '+%H:%M:%S')] [$level] $msg" | tee -a "$LOG_FILE"
}
SERVICES="api darkwatch spamshield voiceprint"
if [ "$SERVICE" != "all" ]; then
SERVICES="$SERVICE"
fi
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
for svc in $SERVICES; do
echo "Rolling back $svc..."
aws ecs update-service \
# ─── Validation ──────────────────────────────────────────────────
validate_environment() {
if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then
log_error "Invalid environment: $ENVIRONMENT (expected: staging, production)"
exit 1
fi
}
validate_service() {
if [[ "$SERVICE" == "all" ]]; then
return 0
fi
if ! echo "$SERVICES_LIST" | grep -qw "$SERVICE"; then
log_error "Invalid service: $SERVICE (expected: api, darkwatch, spamshield, voiceprint, all)"
exit 1
fi
}
check_prerequisites() {
local missing=()
for cmd in aws jq curl; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_error "Missing prerequisites: ${missing[*]}"
exit 1
fi
if [[ -z "${AWS_DEFAULT_REGION:-}" ]]; then
export AWS_DEFAULT_REGION="us-east-1"
fi
log_info "Prerequisites OK (region: $AWS_DEFAULT_REGION)"
}
# ─── Rollback Logic ──────────────────────────────────────────────
get_target_services() {
if [[ "$SERVICE" == "all" ]]; then
echo "$SERVICES_LIST"
else
echo "$SERVICE"
fi
}
rollback_service() {
local svc="$1"
local service_name="${CLUSTER}-${svc}"
log_info "Rolling back $service_name..."
# Check current deployment status
local current_task_def
current_task_def=$(aws ecs describe-services \
--cluster "$CLUSTER" \
--service "${CLUSTER}-${svc}" \
--services "$service_name" \
--query 'services[0].taskDefinition' \
--output text 2>/dev/null || echo "UNKNOWN")
log_info "Current task definition: $current_task_def"
# Execute rollback
if aws ecs update-service \
--cluster "$CLUSTER" \
--service "$service_name" \
--rollback \
--no-cli-auto-prompt
--no-cli-auto-prompt 2>>"$LOG_FILE"; then
log_info "Rollback initiated for $service_name"
else
log_error "Rollback failed to initiate for $service_name"
EXIT_CODE=1
return 1
fi
echo "Waiting for $svc to stabilize..."
aws ecs wait services-stable \
# Wait for stabilization (max 5 minutes)
log_info "Waiting for $service_name to stabilize (timeout: 300s)..."
if aws ecs wait services-stable \
--cluster "$CLUSTER" \
--services "${CLUSTER}-${svc}"
--services "$service_name" \
--timeout 300 2>>"$LOG_FILE"; then
log_info "$service_name stabilized successfully"
else
log_warn "$service_name stabilization timed out or failed"
EXIT_CODE=1
return 1
fi
echo "$svc rolled back successfully"
done
# Get new task definition after rollback
local new_task_def
new_task_def=$(aws ecs describe-services \
--cluster "$CLUSTER" \
--services "$service_name" \
--query 'services[0].taskDefinition' \
--output text 2>/dev/null || echo "UNKNOWN")
echo "Rollback complete for $SERVICES"
local running_count
running_count=$(aws ecs describe-services \
--cluster "$CLUSTER" \
--services "$service_name" \
--query 'services[0].runningCount' \
--output text 2>/dev/null || echo "0")
local desired_count
desired_count=$(aws ecs describe-services \
--cluster "$CLUSTER" \
--services "$service_name" \
--query 'services[0].desiredCount' \
--output text 2>/dev/null || echo "0")
log_info "Rollback complete: $service_name -> $new_task_def ($running_count/$desired_count running)"
return 0
}
# ─── Health Verification ─────────────────────────────────────────
verify_health() {
local svc="$1"
local port
port=$(case "$svc" in
api) echo 3000 ;;
darkwatch) echo 3001 ;;
spamshield) echo 3002 ;;
voiceprint) echo 3003 ;;
*) echo 3000 ;;
esac)
local alb_dns="https://${CLUSTER}-alb.${AWS_DEFAULT_REGION}.elb.amazonaws.com"
log_info "Verifying health for $svc (ALB: $alb_dns)..."
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 10 \
--max-time 30 \
"$alb_dns/health" 2>/dev/null || echo "000")
if [[ "$http_code" == "200" ]]; then
log_info "Health check PASSED: $svc (HTTP $http_code)"
return 0
else
log_warn "Health check FAILED: $svc (HTTP $http_code)"
return 1
fi
}
verify_all_services() {
log_info "=== Post-Rollback Health Verification ==="
local passed=0
local failed=0
for svc in $(get_target_services); do
if verify_health "$svc"; then
((passed++))
else
((failed++))
fi
done
log_info "Verification complete: $passed passed, $failed failed"
if [[ $failed -gt 0 ]]; then
log_warn "Some services failed health verification"
EXIT_CODE=1
fi
}
# ─── Main Execution ──────────────────────────────────────────────
main() {
log_info "=== ShieldAI Rollback ==="
log_info "Environment: $ENVIRONMENT"
log_info "Service(s): $SERVICE"
log_info "Cluster: $CLUSTER"
log_info "Verify: $VERIFY"
log_info "Timestamp: $TIMESTAMP"
log_info "Log file: $LOG_FILE"
log_info "=========================="
# Validate inputs
validate_environment
validate_service
check_prerequisites
# Execute rollback for each target service
local rolled_back=0
local failed=0
for svc in $(get_target_services); do
if rollback_service "$svc"; then
((rolled_back++))
else
((failed++))
fi
done
log_info "=== Rollback Summary ==="
log_info "Rolled back: $rolled_back services"
log_info "Failed: $failed services"
# Post-rollback verification
if [[ "$VERIFY" == "--verify" ]] || [[ "$VERIFY" == "true" ]]; then
verify_all_services
fi
if [[ $failed -gt 0 ]]; then
log_error "Rollback completed with $failed failure(s)"
log_info "Full log: $LOG_FILE"
exit "$EXIT_CODE"
fi
log_info "Rollback completed successfully"
log_info "Full log: $LOG_FILE"
exit 0
}
main "$@"

237
infra/scripts/test-rollback.sh Executable file
View File

@@ -0,0 +1,237 @@
#!/bin/bash
set -uo pipefail
# ShieldAI Rollback Test Suite
# Usage: ./test-rollback.sh [ecs|compose|migration|all]
#
# Validates rollback scripts and procedures without mutating production
# Run against staging environment for integration tests
TEST_SUITE="${1:-all}"
PASS=0
FAIL=0
SKIP=0
# ─── Helpers ─────────────────────────────────────────────────────
log() {
echo "[$(date -u '+%H:%M:%S')] $*"
}
assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
log " ✅ PASS: $desc"
((PASS++))
else
log " ❌ FAIL: $desc (expected: $expected, got: $actual)"
((FAIL++))
fi
}
assert_file_exists() {
local desc="$1" path="$2"
if [[ -f "$path" ]]; then
log " ✅ PASS: $desc"
((PASS++))
else
log " ❌ FAIL: $desc ($path not found)"
((FAIL++))
fi
}
assert_executable() {
local desc="$1" path="$2"
if [[ -x "$path" ]]; then
log " ✅ PASS: $desc"
((PASS++))
else
log " ❌ FAIL: $desc ($path not executable)"
((FAIL++))
fi
}
assert_script_syntax() {
local desc="$1" path="$2"
if bash -n "$path" 2>/dev/null; then
log " ✅ PASS: $desc (syntax OK)"
((PASS++))
else
log " ❌ FAIL: $desc (syntax error)"
((FAIL++))
fi
}
assert_contains() {
local desc="$1" file="$2" pattern="$3"
if grep -q -- "$pattern" "$file" 2>/dev/null; then
log " ✅ PASS: $desc"
((PASS++))
else
log " ❌ FAIL: $desc (pattern '$pattern' not found in $file)"
((FAIL++))
fi
}
# ─── Test: File Structure ────────────────────────────────────────
test_file_structure() {
log "=== Test: File Structure ==="
assert_file_exists "ROLLBACK.md exists" "infra/ROLLBACK.md"
assert_file_exists "rollback.sh exists" "infra/scripts/rollback.sh"
assert_file_exists "rollback-compose.sh exists" "infra/scripts/rollback-compose.sh"
assert_file_exists "rollback-migration.sh exists" "infra/scripts/rollback-migration.sh"
assert_executable "rollback.sh is executable" "infra/scripts/rollback.sh"
assert_executable "rollback-compose.sh is executable" "infra/scripts/rollback-compose.sh"
assert_executable "rollback-migration.sh is executable" "infra/scripts/rollback-migration.sh"
}
# ─── Test: Script Syntax ─────────────────────────────────────────
test_script_syntax() {
log "=== Test: Script Syntax ==="
assert_script_syntax "rollback.sh syntax" "infra/scripts/rollback.sh"
assert_script_syntax "rollback-compose.sh syntax" "infra/scripts/rollback-compose.sh"
assert_script_syntax "rollback-migration.sh syntax" "infra/scripts/rollback-migration.sh"
}
# ─── Test: ROLLBACK.md Content ───────────────────────────────────
test_documentation() {
log "=== Test: Documentation Content ==="
local doc="infra/ROLLBACK.md"
for section in "Overview" "ECS Service Rollback" "Docker Compose Rollback" \
"Database Migration Rollback" "Automated Rollback Triggers" \
"Blue-Green Deployment Rollback" "Rollback Decision Tree" \
"Post-Rollback Verification" "Testing Checklist" "Emergency Rollback"; do
assert_contains "Section '$section' documented" "$doc" "$section"
done
for cmd in "aws ecs update-service" "docker compose" "drizzle-kit" \
"aws rds restore-db-instance" "aws ecs wait services-stable"; do
assert_contains "Command '$cmd' documented" "$doc" "$cmd"
done
}
# ─── Test: Rollback Script Validation ────────────────────────────
test_rollback_script() {
log "=== Test: ECS Rollback Script ==="
# Test invalid environment
local exit_code=0
bash infra/scripts/rollback.sh invalid_env api >/dev/null 2>&1 || exit_code=$?
assert_eq "Invalid environment returns exit code 1" "1" "$exit_code"
# Test invalid service
exit_code=0
bash infra/scripts/rollback.sh staging invalid_svc >/dev/null 2>&1 || exit_code=$?
assert_eq "Invalid service returns exit code 1" "1" "$exit_code"
# Verify script has required functions
for func in "validate_environment" "validate_service" "rollback_service" \
"verify_health" "check_prerequisites" "main"; do
assert_contains "Function '$func' defined" "infra/scripts/rollback.sh" "$func"
done
# Verify all services are handled
for svc in api darkwatch spamshield voiceprint; do
assert_contains "Service '$svc' in SERVICES_LIST" "infra/scripts/rollback.sh" "$svc"
done
}
# ─── Test: Compose Rollback Script ───────────────────────────────
test_compose_script() {
log "=== Test: Docker Compose Rollback Script ==="
# Test missing tag argument
local exit_code=0
bash infra/scripts/rollback-compose.sh >/dev/null 2>&1 || exit_code=$?
assert_eq "Missing tag returns exit code 1" "1" "$exit_code"
# Verify compose file exists
assert_file_exists "docker-compose.prod.yml exists" "docker-compose.prod.yml"
# Verify all services are defined in compose
for svc in api darkwatch spamshield voiceprint; do
assert_contains "Service '$svc' in docker-compose.prod.yml" "docker-compose.prod.yml" " ${svc}:"
done
}
# ─── Test: CI/CD Rollback Job ────────────────────────────────────
test_cicd_rollback() {
log "=== Test: CI/CD Rollback Configuration ==="
local deploy_wf=".github/workflows/deploy.yml"
assert_contains "Rollback job defined" "$deploy_wf" "rollback:"
assert_contains "Health check triggers rollback" "$deploy_wf" "needs.health-check.result"
assert_contains "ECS --rollback flag used" "$deploy_wf" "--rollback"
for svc in api darkwatch spamshield voiceprint; do
assert_contains "Service '$svc' in deploy matrix" "$deploy_wf" "$svc"
done
}
# ─── Test: Health Check Configuration ────────────────────────────
test_health_checks() {
log "=== Test: Health Check Configuration ==="
assert_contains "Container health check in ECS" "infra/modules/ecs/main.tf" "healthCheck"
assert_contains "ALB health check defined" "infra/modules/ecs/main.tf" "health_check"
assert_contains "ALB 5xx alarm configured" "infra/modules/cloudwatch/main.tf" "HTTPCode_Elb_5XX_Count"
}
# ─── Test: README References ─────────────────────────────────────
test_readme() {
log "=== Test: README References ==="
assert_contains "README references ROLLBACK.md" "infra/README.md" "ROLLBACK.md"
assert_contains "README documents rollback.sh" "infra/README.md" "rollback.sh"
assert_contains "README documents rollback-compose.sh" "infra/README.md" "rollback-compose.sh"
assert_contains "README documents rollback-migration.sh" "infra/README.md" "rollback-migration.sh"
}
# ─── Main ────────────────────────────────────────────────────────
main() {
log "=== ShieldAI Rollback Test Suite ==="
log "Suite: $TEST_SUITE"
log ""
case "$TEST_SUITE" in
ecs|all)
test_rollback_script
test_cicd_rollback
test_health_checks
;;
compose|all)
test_compose_script
;;
migration)
log "=== Test: Migration Rollback ==="
assert_script_syntax "rollback-migration.sh syntax" "infra/scripts/rollback-migration.sh"
assert_contains "Uses Secrets Manager" "infra/scripts/rollback-migration.sh" "secretsmanager"
assert_contains "Uses drizzle-kit" "infra/scripts/rollback-migration.sh" "drizzle-kit"
;;
esac
test_file_structure
test_script_syntax
test_documentation
test_readme
log ""
log "=== Results ==="
log "Passed: $PASS"
log "Failed: $FAIL"
log ""
if [[ $FAIL -gt 0 ]]; then
log "❌ SOME TESTS FAILED"
return 1
fi
log "✅ ALL TESTS PASSED"
return 0
}
main "$@"