Skip to content

Merge pull request #217 from MorpheusAIs/fix/case-insensitive-model-name #249

Merge pull request #217 from MorpheusAIs/fix/case-insensitive-model-name

Merge pull request #217 from MorpheusAIs/fix/case-insensitive-model-name #249

Workflow file for this run

name: Morpheus-Mktplc-API-CI-CD
# CI/CD Pipeline for Morpheus Marketplace API
#
# Branch Strategy:
# • dev: Build and test only (no deployment)
# • test: Build, test, and deploy to AWS DEV environment
# • cicd/*: Build, test, and deploy to AWS DEV environment (for fast cycle testing)
# • main: Build, test, create release, and deploy to AWS PRD environment
#
# Key Features:
# • Python/FastAPI application with Poetry dependency management
# • Automated database migrations with Alembic
# • Docker container builds with GitHub Actions caching
# • Automated deployments to AWS ECS Fargate
# • Health check verification with version matching
# • Database rollback on deployment failures
# • Git tags only created after successful deployment (safe to re-run on failure)
# 2026-02-12: Added stg branch support again
on:
workflow_dispatch:
inputs:
create_release:
description: "Create updated Morpheus-Marketplace-API release"
required: true
type: boolean
run_migrations:
description: "Run database migrations during deployment"
required: true
type: boolean
default: true
create_deployment:
description: "Deploy to hosted environments"
required: true
type: boolean
push:
branches:
- main
- test
- stg
- dev
- cicd/*
paths: [".github/**", "src/**", "alembic/**", "pyproject.toml", "poetry.lock", "Dockerfile", "alembic.ini", "tests/**"]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
AWS_REGION: us-east-2
jobs:
Generate-Tag:
runs-on: ubuntu-latest
name: Generate Semantic Version Tag
if: |
github.repository == 'MorpheusAIs/Morpheus-Marketplace-API' &&
(
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/stg' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/heads/cicd/'))) ||
(github.event_name == 'workflow_dispatch')
)
outputs:
tag_name: ${{ steps.gen_tag_name.outputs.tag_name }}
vtag: ${{ steps.gen_tag_name.outputs.vtag }}
vfull: ${{ steps.gen_tag_name.outputs.vfull }}
image_name: ${{ steps.gen_tag_name.outputs.image_name }}
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Determine semantic version tag
id: gen_tag_name
shell: bash
run: |
IMAGE_NAME="ghcr.io/morpheusais/morpheus-marketplace-api"
VMAJ_NEW=1
VMIN_NEW=0
VPAT_NEW=0
set +o pipefail
VLAST=$(git describe --tags --abbrev=0 --match='v[1-9]*' refs/remotes/origin/main 2>/dev/null | cut -c2-)
if [ -n "$VLAST" ]; then
eval $(echo "$VLAST" | awk -F '.' '{print "VMAJ="$1" VMIN="$2" VPAT="$3}')
else
VMAJ=0
VMIN=0
VPAT=0
fi
if [ "$GITHUB_REF_NAME" = "main" ]; then
if [ "$VMAJ_NEW" -gt "$VMAJ" ]; then
VMAJ=$VMAJ_NEW
VMIN=$VMIN_NEW
VPAT=$VPAT_NEW
else
VMIN=$((VMIN+1))
VPAT=0
fi
VFULL=${VMAJ}.${VMIN}.${VPAT}
VTAG=v$VFULL
else
MB=$(git merge-base refs/remotes/origin/main HEAD)
VPAT=$(git rev-list --count --no-merges ${MB}..HEAD)
VFULL=${VMAJ}.${VMIN}.${VPAT}
RNAME=${GITHUB_REF_NAME##*/}
[ "$GITHUB_EVENT_NAME" = "pull_request" ] && RNAME=pr${GITHUB_REF_NAME%/merge}
VTAG=v${VFULL}-${RNAME}
fi
# Output variables for use in subsequent jobs
echo "tag_name=${VTAG}" >> $GITHUB_OUTPUT
echo "vtag=${VTAG}" >> $GITHUB_OUTPUT
echo "vfull=${VFULL}" >> $GITHUB_OUTPUT
echo "image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
echo "✅ New Build Tag: $VTAG" >> $GITHUB_STEP_SUMMARY
echo "✅ Docker Image Tag: ${IMAGE_NAME}:${VTAG}" >> $GITHUB_STEP_SUMMARY
Test-API:
name: Test Morpheus API
if: |
github.repository == 'MorpheusAIs/Morpheus-Marketplace-API' &&
(
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/stg')) ||
(github.event_name == 'workflow_dispatch')
)
runs-on: ubuntu-latest
needs: Generate-Tag
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock', '**/pyproject.toml') }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: |
# Check if lock file is out of sync and regenerate if needed
if ! poetry check --lock; then
echo "🔄 Lock file out of sync, regenerating..."
poetry lock
fi
poetry install --no-interaction --no-root
- name: Install project
run: |
# Check if lock file is out of sync and regenerate if needed
if ! poetry check --lock; then
echo "🔄 Lock file out of sync, regenerating..."
poetry lock
fi
poetry install --no-interaction
- name: Run database migrations (test)
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
ENVIRONMENT: test
run: |
poetry run alembic upgrade head
- name: Run tests
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
ENVIRONMENT: test
run: |
# Run tests but don't fail the build if tests fail (focus on infrastructure)
poetry run pytest tests/ -v --cov=src --cov-report=xml || echo "⚠️ Some tests failed, but continuing with build process"
- name: Upload coverage reports
if: always() # Run even if tests failed
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false # Don't fail if coverage upload fails
Build-and-Push-Container:
name: Build & Push Docker Image
if: |
github.repository == 'MorpheusAIs/Morpheus-Marketplace-API' &&
(
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/stg')) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.create_deployment == 'true')
)
needs:
- Generate-Tag
- Test-API
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Docker Image
run: |
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
BUILDIMAGE=${{ needs.Generate-Tag.outputs.image_name }}
BUILDCOMMIT=${{ github.sha }}
# Determine environment for tagging
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV_TAG="dev-latest"
echo "🚀 Building for development environment"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV_TAG="stg-latest"
echo "🚀 Building for staging environment"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV_TAG="prd-latest"
echo "🚀 Building for production environment"
else
ENV_TAG="$BUILDTAG"
echo "🚀 Building for feature/development branch"
fi
# Use single platform for test builds and feature branches, multi-platform for main
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
PLATFORMS="linux/amd64"
echo "🚀 Building single platform (amd64) for faster deployment"
else
PLATFORMS="linux/amd64,linux/arm64"
echo "🚀 Building multi-platform for production release"
fi
docker buildx build \
--platform $PLATFORMS \
--build-arg BUILD_VERSION=${{ needs.Generate-Tag.outputs.tag_name }} \
--build-arg BUILD_COMMIT=$BUILDCOMMIT \
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--cache-from type=gha \
--cache-to type=gha,mode=min \
--push \
-t $BUILDIMAGE:$BUILDTAG \
-t $BUILDIMAGE:$ENV_TAG \
. || (echo "❌ Failed to push image with tag: $BUILDIMAGE:$BUILDTAG" && exit 1)
echo "✅ API Build and Push of $BUILDIMAGE:$BUILDTAG Successful!"
echo "📋 Git tag will only be created after successful deployment verification"
- name: Optionally Push Latest Tag
if: ${{ github.ref == 'refs/heads/main' }}
run: |
BUILDIMAGE=${{ needs.Generate-Tag.outputs.image_name }}
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
docker pull $BUILDIMAGE:$BUILDTAG || (echo "❌ Failed to pull image: $BUILDIMAGE:$BUILDTAG" && exit 1)
docker tag $BUILDIMAGE:$BUILDTAG $BUILDIMAGE:latest || (echo "❌ Failed to tag image as :latest" && exit 1)
docker push $BUILDIMAGE:latest || (echo "❌ Failed to push image as :latest" && exit 1)
echo "✅ API Push $BUILDIMAGE:latest Tag Successful!"
Deploy-to-ECS:
name: Deploy to AWS ECS
if: |
github.repository == 'MorpheusAIs/Morpheus-Marketplace-API' &&
(
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/stg' || startsWith(github.ref, 'refs/heads/cicd/'))) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.create_deployment == 'true')
)
needs:
- Generate-Tag
- Build-and-Push-Container
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'main' || (github.ref_name == 'stg' && 'stg' || 'test') }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Python (for migrations)
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install dependencies (for migrations)
run: |
# Check if lock file is out of sync and regenerate if needed
if ! poetry check --lock; then
echo "🔄 Lock file out of sync, regenerating..."
poetry lock
fi
poetry install --no-interaction
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.MORPHEUS_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.MORPHEUS_AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Run Database Migrations
if: ${{ github.event.inputs.run_migrations != 'false' }}
run: |
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
# Determine environment from branch
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
else
echo "❌ Unsupported branch for deployment: ${{ github.ref_name }}"
exit 1
fi
# Get database connection details from the dedicated DB-creds secret
# (contains only POSTGRES_USER/PASSWORD/DB/HOST/PORT — no app secrets)
SECRET_VALUE=$(aws secretsmanager get-secret-value \
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
--query SecretString --output text)
DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')
echo "🗄️ Running database migrations for environment: $ENV"
echo "📍 Database host: $DB_HOST"
# Set database URL for migrations
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
export ENVIRONMENT="$ENV"
# Create backup point (get current revision before migration)
echo "📋 Recording current database state before migration..."
CURRENT_REVISION=$(poetry run alembic current 2>/dev/null | head -n 1 | tr -d '[:space:]' || echo "none")
# Validate revision format (should be a hex string or "none")
if [[ "$CURRENT_REVISION" =~ ^[a-f0-9]{12}$ ]] || [ "$CURRENT_REVISION" = "none" ]; then
echo "PRE_MIGRATION_REVISION=$CURRENT_REVISION" >> $GITHUB_ENV
echo "Current revision before migration: $CURRENT_REVISION"
else
echo "⚠️ Invalid revision format detected: '$CURRENT_REVISION'"
echo "PRE_MIGRATION_REVISION=none" >> $GITHUB_ENV
echo "Current revision before migration: none (invalid format detected)"
fi
# Check if there are pending migrations
echo "🔍 Checking for pending migrations..."
PENDING_MIGRATIONS=$(poetry run alembic heads)
echo "Target revision: $PENDING_MIGRATIONS"
# Ensure database state aligns with migration files
echo "🧹 Aligning database state with migration files..."
poetry run python -c "
import asyncio
import sys
import os
sys.path.insert(0, os.path.abspath('.'))
async def align_database_state():
try:
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
engine = create_async_engine(os.getenv('DATABASE_URL'))
async with engine.begin() as conn:
# Check if alembic_version table exists
result = await conn.execute(text(
\"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name='alembic_version')\"
))
table_exists = result.scalar()
if table_exists:
# Get current version
result = await conn.execute(text('SELECT version_num FROM alembic_version'))
current_version = result.scalar()
print(f'Current alembic version: {current_version}')
# Check if chat tables already exist
chat_table_exists = await conn.execute(text(
\"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name='chats')\"
))
chats_exist = chat_table_exists.scalar()
print(f'Chat tables exist in database: {chats_exist}')
# Handle problematic migration references
if current_version == 'fix_message_role_enum':
print(f'🧹 Removing dangling migration reference: {current_version}')
if chats_exist:
print(f'📋 Chat tables exist - marking add_chat_tables as completed')
await conn.execute(text(\"UPDATE alembic_version SET version_num = 'add_chat_tables'\"))
print(f'✅ Set alembic version to add_chat_tables')
else:
print(f'📋 Chat tables do not exist - resetting to pre-chat state')
await conn.execute(text(\"UPDATE alembic_version SET version_num = '6f8a4e1b9d43'\"))
print(f'✅ Reset alembic version to 6f8a4e1b9d43')
# Handle state mismatch where tables exist but migration thinks they don't
elif current_version == '6f8a4e1b9d43' and chats_exist:
print(f'🔄 State mismatch detected: chat tables exist but alembic version is pre-chat')
print(f'📋 Marking add_chat_tables as completed to match database state')
await conn.execute(text(\"UPDATE alembic_version SET version_num = 'add_chat_tables'\"))
print(f'✅ Aligned alembic version to add_chat_tables')
else:
print(f'✅ Database state appears aligned with migration version')
else:
print('No alembic_version table found - this is a fresh database')
await engine.dispose()
return True
except Exception as e:
print(f'Error during state alignment: {e}')
return False
result = asyncio.run(align_database_state())
sys.exit(0 if result else 1)
" || echo "⚠️ State alignment failed, continuing with migrations..."
# Run migrations
echo "🚀 Applying database migrations..."
poetry run alembic upgrade head
# Verify migration success
echo "✅ Verifying migration success..."
NEW_REVISION=$(poetry run alembic current 2>/dev/null | head -n 1 | tr -d '[:space:]' || echo "none")
echo "NEW_REVISION=$NEW_REVISION" >> $GITHUB_ENV
echo "New revision after migration: $NEW_REVISION"
# Test database connectivity and basic functionality
poetry run python -c "
import asyncio
import sys
import os
sys.path.insert(0, os.path.abspath('.'))
async def verify():
try:
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
# Create engine with the same DATABASE_URL
engine = create_async_engine(os.getenv('DATABASE_URL'))
# Test basic connectivity
async with engine.begin() as conn:
result = await conn.execute(text('SELECT 1'))
result.scalar()
await engine.dispose()
print('✅ Database connectivity verified successfully')
return True
except Exception as e:
print(f'❌ Database verification failed: {e}')
import traceback
traceback.print_exc()
return False
result = asyncio.run(verify())
sys.exit(0 if result else 1)
" || (echo "❌ Migration verification failed" && exit 1)
echo "✅ Database migrations completed successfully"
- name: Deploy to ECS
run: |
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
BUILDIMAGE=${{ needs.Generate-Tag.outputs.image_name }}
# Determine environment based on branch
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
else
echo "❌ Unsupported branch for deployment: ${{ github.ref_name }}"
exit 1
fi
echo "🚀 Deploying to Morpheus AWS - Environment: $ENV"
echo "📦 Container Image: $BUILDIMAGE:$BUILDTAG"
# ECS deployment variables (following Node repo pattern)
CLUSTER_NAME="ecs-${ENV}-morpheus-engine"
SERVICE_NAME="svc-${ENV}-api-service"
TASK_FAMILY="tsk-${ENV}-api-service"
CONTAINER_NAME="morpheus-api-service"
# Get current task definition
echo "📋 Retrieving current task definition..."
CURRENT_TASK_DEF=$(aws ecs describe-task-definition \
--task-definition "$TASK_FAMILY" \
--query 'taskDefinition' \
--output json)
if [ $? -ne 0 ]; then
echo "❌ Failed to retrieve current task definition"
exit 1
fi
# Update the image in the task definition
echo "🔄 Creating new task definition with image: $BUILDIMAGE:$BUILDTAG"
# Debug: Show current image before update
echo "🔍 Current image in task definition:"
echo "$CURRENT_TASK_DEF" | jq -r '.containerDefinitions[0].image'
NEW_TASK_DEF=$(echo "$CURRENT_TASK_DEF" | jq --arg IMAGE "$BUILDIMAGE:$BUILDTAG" --arg CONTAINER "$CONTAINER_NAME" '
{
family: .family,
networkMode: .networkMode,
requiresCompatibilities: .requiresCompatibilities,
cpu: .cpu,
memory: .memory,
taskRoleArn: .taskRoleArn,
executionRoleArn: .executionRoleArn,
volumes: .volumes,
containerDefinitions: (.containerDefinitions | map(
if .name == $CONTAINER then
.image = $IMAGE
else
.
end
))
}
')
# Debug: Show new image after update
echo "🔍 New image in task definition:"
echo "$NEW_TASK_DEF" | jq -r '.containerDefinitions[0].image'
# Validate JSON structure
echo "🔍 Validating JSON structure..."
echo "$NEW_TASK_DEF" | jq empty
if [ $? -ne 0 ]; then
echo "❌ Generated JSON is invalid"
exit 1
fi
# Register new task definition
echo "📝 Registering new task definition..."
TEMP_JSON_FILE="/tmp/task_definition_$$.json"
echo "$NEW_TASK_DEF" > "$TEMP_JSON_FILE"
NEW_TASK_ARN=$(aws ecs register-task-definition \
--cli-input-json "file://$TEMP_JSON_FILE" \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
rm -f "$TEMP_JSON_FILE"
if [ $? -ne 0 ] || [ -z "$NEW_TASK_ARN" ]; then
echo "❌ Failed to register new task definition"
exit 1
fi
echo "✅ New task definition registered: $NEW_TASK_ARN"
# Update the ECS service
echo "🔄 Updating ECS service..."
UPDATE_RESULT=$(aws ecs update-service \
--cluster "$CLUSTER_NAME" \
--service "$SERVICE_NAME" \
--task-definition "$NEW_TASK_ARN" \
--force-new-deployment \
--query 'service.{serviceName:serviceName,taskDefinition:taskDefinition,desiredCount:desiredCount,runningCount:runningCount,pendingCount:pendingCount}' \
--output json)
echo "📊 Service Update Summary:"
echo "$UPDATE_RESULT" | jq .
if [ $? -ne 0 ]; then
echo "❌ Failed to update ECS service"
exit 1
fi
echo "✅ ECS service update initiated successfully"
echo "🎯 Environment: $ENV"
echo "🏗️ Cluster: $CLUSTER_NAME"
echo "⚙️ Service: $SERVICE_NAME"
echo "📦 Image: $BUILDIMAGE:$BUILDTAG"
echo "📋 Task Definition: $NEW_TASK_ARN"
- name: Wait for ECS Deployment
run: |
# Determine environment
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
else
echo "❌ Unsupported branch for deployment: ${{ github.ref_name }}"
exit 1
fi
CLUSTER_NAME="ecs-${ENV}-morpheus-engine"
SERVICE_NAME="svc-${ENV}-api-service"
echo "⏳ Waiting for ECS deployment to start (3 minutes)..."
echo "📊 Monitoring service status..."
# Monitor deployment progress for 3 minutes
for i in {1..6}; do
sleep 30
echo "🔍 Check $i/6: Service status at $(date)"
aws ecs describe-services \
--cluster "$CLUSTER_NAME" \
--services "$SERVICE_NAME" \
--query 'services[0].{Status:status,Running:runningCount,Pending:pendingCount,Desired:desiredCount}' \
--output table || echo "Failed to get service status"
# Check task status
TASK_ARNS=$(aws ecs list-tasks --cluster "$CLUSTER_NAME" --service-name "$SERVICE_NAME" --query 'taskArns' --output text)
if [ -n "$TASK_ARNS" ] && [ "$TASK_ARNS" != "None" ]; then
echo "📋 Task details:"
aws ecs describe-tasks \
--cluster "$CLUSTER_NAME" \
--tasks $TASK_ARNS \
--query 'tasks[0].{LastStatus:lastStatus,HealthStatus:healthStatus,StoppedReason:stoppedReason}' \
--output table 2>/dev/null || echo "No task details available"
fi
echo "---"
done
echo "✅ Initial deployment monitoring completed - proceeding to health verification"
- name: Health Check Verification
run: |
BUILDTAG=${{ needs.Generate-Tag.outputs.tag_name }}
# Determine environment and health endpoint
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
HEALTH_ENDPOINT="https://api.dev.mor.org/health"
elif [ "${{ github.ref_name }}" == "stg" ]; then
HEALTH_ENDPOINT="https://api.stg.mor.org/health"
elif [ "${{ github.ref_name }}" == "main" ]; then
HEALTH_ENDPOINT="https://api.mor.org/health"
else
echo "❌ Unsupported branch for health check: ${{ github.ref_name }}"
exit 1
fi
echo "🔍 Waiting 2 minutes for application startup before health check verification..."
sleep 120 # 2-minute wait
echo "🔍 Verifying deployment at: $HEALTH_ENDPOINT"
MAX_RETRIES=30
RETRY_COUNT=0
VERSION_VERIFIED=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
echo "🔄 Health check attempt $((RETRY_COUNT + 1))/$MAX_RETRIES..."
# Use browser user-agent to bypass WAF bot detection
HEALTH_RESPONSE=$(curl -s --max-time 10 \
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
"$HEALTH_ENDPOINT" 2>/dev/null)
CURL_STATUS=$?
if [ $CURL_STATUS -eq 0 ] && [ -n "$HEALTH_RESPONSE" ]; then
echo "📡 Health response: $HEALTH_RESPONSE"
# Parse health check JSON
STATUS=$(echo "$HEALTH_RESPONSE" | jq -r '.status // empty' 2>/dev/null)
DATABASE_STATUS=$(echo "$HEALTH_RESPONSE" | jq -r '.database // empty' 2>/dev/null)
DEPLOYED_VERSION=$(echo "$HEALTH_RESPONSE" | jq -r '.version // empty' 2>/dev/null)
UPTIME=$(echo "$HEALTH_RESPONSE" | jq -r '.uptime.human_readable // empty' 2>/dev/null)
if [ "$STATUS" = "ok" ] && [ "$DATABASE_STATUS" = "healthy" ]; then
echo "🏥 Service Status: $STATUS"
echo "🗄️ Database Status: $DATABASE_STATUS"
echo "📦 Deployed Version: $DEPLOYED_VERSION"
echo "⏰ Service Uptime: $UPTIME"
# Version verification (check if build tag is contained in deployed version)
if [[ "$DEPLOYED_VERSION" == *"$BUILDTAG"* ]] || [[ "$BUILDTAG" == *"$DEPLOYED_VERSION"* ]]; then
echo "✅ Version verification successful! Deployed version matches expected tag."
VERSION_VERIFIED=true
break
else
echo "⚠️ Version mismatch - Expected: $BUILDTAG, Deployed: $DEPLOYED_VERSION"
fi
else
echo "⚠️ Service not healthy - Status: $STATUS, Database: $DATABASE_STATUS"
fi
else
echo "⚠️ Health check failed (curl status: $CURL_STATUS)"
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "⏳ Waiting 10 seconds before retry..."
sleep 10
fi
done
# Final verification result
if [ "$VERSION_VERIFIED" = true ]; then
echo ""
echo "🎉 Deployment verification successful!"
echo "✅ Service is healthy and running the expected version"
echo "🌐 Health Check URL: $HEALTH_ENDPOINT"
echo "📋 Git tag will be created in the next step"
else
echo ""
echo "❌ Deployment verification failed!"
echo "🔍 The service did not pass health check with correct version after multiple retries"
echo "🌐 Health Check URL: $HEALTH_ENDPOINT"
echo "🏗️ Expected version: $BUILDTAG"
if [ -n "$DEPLOYED_VERSION" ]; then
echo "📦 Currently deployed: $DEPLOYED_VERSION"
fi
echo ""
echo "ℹ️ Check AWS ECS console and CloudWatch logs for details."
echo "ℹ️ You can manually verify at: $HEALTH_ENDPOINT"
echo ""
echo "⚠️ Failing deployment to prevent Git tag creation"
echo "ℹ️ You can safely re-run this workflow after investigating the issue"
exit 1
fi
- name: Database Rollback on Deployment Failure
if: failure()
run: |
echo "🚨 Deployment failed - initiating database rollback"
# Check if we have a pre-migration revision to rollback to
if [ -z "${{ env.PRE_MIGRATION_REVISION }}" ] || [ "${{ env.PRE_MIGRATION_REVISION }}" == "none" ]; then
echo "⚠️ No pre-migration revision found - skipping database rollback"
echo "ℹ️ This might be the first deployment or no migrations were run"
exit 0
fi
# Determine environment from branch
if [ "${{ github.ref_name }}" == "test" ] || [[ "${{ github.ref_name }}" == cicd/* ]]; then
ENV="dev"
elif [ "${{ github.ref_name }}" == "stg" ]; then
ENV="stg"
elif [ "${{ github.ref_name }}" == "main" ]; then
ENV="prd"
else
echo "❌ Unknown environment for rollback"
exit 1
fi
# Get database connection details from the dedicated DB-creds secret
SECRET_VALUE=$(aws secretsmanager get-secret-value \
--secret-id "${ENV}-morpheus-api-rds-proxy-credentials" \
--query SecretString --output text)
DB_USER=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_USER')
DB_PASSWORD=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PASSWORD')
DB_NAME=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_DB')
DB_HOST=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_HOST')
DB_PORT=$(echo "$SECRET_VALUE" | jq -r '.POSTGRES_PORT')
echo "📍 Rolling back database in environment: $ENV"
echo "🔄 Target rollback revision: ${{ env.PRE_MIGRATION_REVISION }}"
export DATABASE_URL="postgresql+asyncpg://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
# Check current revision before rollback
CURRENT_REV=$(poetry run alembic current --verbose 2>/dev/null | grep "Current revision" | awk '{print $NF}' || echo "none")
echo "📋 Current revision before rollback: $CURRENT_REV"
# Only rollback if we're not already at the target revision
if [ "$CURRENT_REV" == "${{ env.PRE_MIGRATION_REVISION }}" ]; then
echo "✅ Database is already at the target revision - no rollback needed"
exit 0
fi
# Perform the rollback
echo "📅 Rolling back database to revision: ${{ env.PRE_MIGRATION_REVISION }}"
# Skip rollback if no valid pre-migration revision
if [ "${{ env.PRE_MIGRATION_REVISION }}" = "none" ]; then
echo "⚠️ No valid pre-migration revision available - skipping rollback"
echo "Database may be in initial state or revision detection failed"
elif poetry run alembic downgrade "${{ env.PRE_MIGRATION_REVISION }}"; then
echo "✅ Database rollback command completed"
# Verify rollback success
ROLLED_BACK_REV=$(poetry run alembic current 2>/dev/null | head -n 1 | tr -d '[:space:]' || echo "none")
echo "📋 Revision after rollback: $ROLLED_BACK_REV"
if [ "$ROLLED_BACK_REV" == "${{ env.PRE_MIGRATION_REVISION }}" ]; then
echo "✅ Database rollback successful - revision matches target"
# Test basic database connectivity
poetry run python -c "
import asyncio
import sys
import os
async def test_db():
try:
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
engine = create_async_engine(os.getenv('DATABASE_URL'))
async with engine.begin() as conn:
result = await conn.execute(text('SELECT 1'))
result.scalar()
await engine.dispose()
print('✅ Database connectivity test passed after rollback')
return True
except Exception as e:
print(f'❌ Database connectivity test failed after rollback: {e}')
return False
result = asyncio.run(test_db())
sys.exit(0 if result else 1)
" && echo "✅ Database rollback verification successful" || echo "❌ Database rollback verification failed"
else
echo "❌ Database rollback failed - revision mismatch"
echo "Expected: ${{ env.PRE_MIGRATION_REVISION }}, Got: $ROLLED_BACK_REV"
fi
else
echo "❌ Database rollback command failed"
echo "⚠️ Manual database intervention may be required"
fi
Create-Release:
name: Create GitHub Release (Only After Successful Deployment)
if: |
github.repository == 'MorpheusAIs/Morpheus-Marketplace-API' &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/stg')
needs:
- Generate-Tag
- Deploy-to-ECS
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Create release
id: create_release
uses: anzz1/action-create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.Generate-Tag.outputs.tag_name }}
prerelease: ${{ github.ref != 'refs/heads/main' }}