feat/live-preview #1

Merged
ricardo merged 16 commits from feat/live-preview into Release/V0.0.3 2025-07-23 02:24:34 +00:00
8 changed files with 1466 additions and 1 deletions
Showing only changes of commit 89026e3460 - Show all commits

View File

@ -18,7 +18,8 @@
"Bash(rm:*)",
"Bash(curl:*)",
"Bash(docker-compose up:*)",
"Bash(dotnet build:*)"
"Bash(dotnet build:*)",
"Bash(chmod:*)"
]
},
"enableAllProjectMcpServers": false

View File

@ -0,0 +1,166 @@
name: Release Deployment Pipeline
on:
push:
branches:
- 'Release/*'
env:
REGISTRY: registry.redecarneir.us
IMAGE_NAME: bcards
MONGODB_HOST: 192.168.0.100:27017
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore --configuration Release
- name: Run unit tests
run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage"
- name: Test MongoDB connection
run: |
echo "Testing MongoDB connection to $MONGODB_HOST"
timeout 10 bash -c "</dev/tcp/192.168.0.100/27017" && echo "MongoDB connection successful" || echo "MongoDB connection failed"
build-and-deploy:
name: Build Multi-Arch Image and Deploy
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Extract branch name and version
id: extract_branch
run: |
BRANCH_NAME=${GITHUB_REF#refs/heads/}
VERSION=${BRANCH_NAME#Release/}
COMMIT_SHA=${GITHUB_SHA::7}
echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "commit=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "tag=$VERSION-$COMMIT_SHA" >> $GITHUB_OUTPUT
- name: Build and push multi-arch Docker image
run: |
echo "Building multi-arch image for platforms: linux/amd64,linux/arm64"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file Dockerfile.release \
--tag $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.tag }} \
--tag $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.version }}-latest \
--tag $REGISTRY/$IMAGE_NAME:release-latest \
--push \
--build-arg BUILDPLATFORM=linux/amd64 \
--build-arg VERSION=${{ steps.extract_branch.outputs.version }} \
--build-arg COMMIT=${{ steps.extract_branch.outputs.commit }} \
.
- name: Deploy to staging environment
run: |
echo "Deploying to staging environment..."
# Create deployment directory
sudo mkdir -p /opt/bcards-staging
# Copy docker-compose file
sudo cp docker-compose.staging.yml /opt/bcards-staging/
# Set environment variables
sudo tee /opt/bcards-staging/.env > /dev/null <<EOF
IMAGE_TAG=${{ steps.extract_branch.outputs.tag }}
MONGODB_CONNECTION_STRING=mongodb://$MONGODB_HOST/BCardsDB
ASPNETCORE_ENVIRONMENT=Release
EOF
# Run deployment script
chmod +x scripts/deploy-release.sh
sudo ./scripts/deploy-release.sh ${{ steps.extract_branch.outputs.tag }}
- name: Health check and validation
run: |
echo "Running health checks..."
# Wait for application to start
sleep 30
# Test application health
for i in {1..10}; do
if curl -f http://localhost:8090/health > /dev/null 2>&1; then
echo "Application health check passed"
break
fi
echo "Health check attempt $i failed, retrying in 10 seconds..."
sleep 10
if [ $i -eq 10 ]; then
echo "Health check failed after 10 attempts"
exit 1
fi
done
# Test MongoDB connectivity from application
chmod +x scripts/test-mongodb-connection.sh
./scripts/test-mongodb-connection.sh
- name: Deployment notification
run: |
echo "🚀 Deployment Status: SUCCESS"
echo "📦 Image: $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.tag }}"
echo "🌐 Environment: Staging"
echo "🔗 MongoDB: $MONGODB_HOST"
echo "🏗️ Architecture: Multi-arch (linux/amd64, linux/arm64)"
echo "📋 Branch: ${{ steps.extract_branch.outputs.branch }}"
echo "🆔 Commit: ${{ steps.extract_branch.outputs.commit }}"
rollback:
name: Rollback on Failure
runs-on: ubuntu-latest
needs: [test, build-and-deploy]
if: failure()
steps:
- name: Rollback deployment
run: |
echo "🚨 Deployment failed, initiating rollback..."
# Stop current containers
cd /opt/bcards-staging
sudo docker-compose down
# Restore previous version if exists
if [ -f .env.backup ]; then
sudo mv .env.backup .env
sudo docker-compose up -d
echo "✅ Rollback completed to previous version"
else
echo "❌ No previous version found for rollback"
fi
- name: Failure notification
run: |
echo "❌ Deployment Status: FAILED"
echo "🔄 Rollback: Initiated"
echo "📋 Branch: ${GITHUB_REF#refs/heads/}"
echo "🆔 Commit: ${GITHUB_SHA::7}"

View File

@ -6,6 +6,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCards.Web", "src\BCards.We
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCards.IntegrationTests", "src\BCards.IntegrationTests\BCards.IntegrationTests.csproj", "{8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
scripts\deploy-release.sh = scripts\deploy-release.sh
scripts\init-mongo.js = scripts\init-mongo.js
scripts\test-mongodb-connection.sh = scripts\test-mongodb-connection.sh
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -24,4 +31,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3DA87F09-8B78-450D-9EF8-A0C0E02F0E04}
EndGlobalSection
EndGlobal

123
Dockerfile.release Normal file
View File

@ -0,0 +1,123 @@
# Dockerfile.release - Multi-architecture build for Release environment
# Supports: linux/amd64, linux/arm64
ARG BUILDPLATFORM=linux/amd64
ARG TARGETPLATFORM
ARG VERSION=unknown
ARG COMMIT=unknown
# Base runtime image with multi-arch support
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 8443
# Install dependencies based on target platform
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libgdiplus \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create application directories
RUN mkdir -p /app/uploads /app/logs \
&& chmod 755 /app/uploads /app/logs
# Build stage - use build platform for compilation
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG TARGETPLATFORM
ARG VERSION
ARG COMMIT
WORKDIR /src
# Copy project files and restore dependencies
COPY ["src/BCards.Web/BCards.Web.csproj", "src/BCards.Web/"]
RUN dotnet restore "src/BCards.Web/BCards.Web.csproj" \
--runtime $(echo $TARGETPLATFORM | tr '/' '-')
# Copy source code
COPY . .
WORKDIR "/src/src/BCards.Web"
# Build application with Release configuration
RUN dotnet build "BCards.Web.csproj" \
-c Release \
-o /app/build \
--no-restore \
--runtime $(echo $TARGETPLATFORM | tr '/' '-') \
-p:Version=$VERSION \
-p:InformationalVersion=$COMMIT
# Publish stage - optimize for target platform
FROM build AS publish
ARG TARGETPLATFORM
ARG VERSION
ARG COMMIT
RUN dotnet publish "BCards.Web.csproj" \
-c Release \
-o /app/publish \
--no-restore \
--no-build \
--runtime $(echo $TARGETPLATFORM | tr '/' '-') \
--self-contained false \
-p:PublishReadyToRun=true \
-p:PublishSingleFile=false \
-p:UseAppHost=false \
-p:Version=$VERSION \
-p:InformationalVersion=$COMMIT
# Final stage - runtime optimized for Release environment
FROM base AS final
ARG VERSION=unknown
ARG COMMIT=unknown
ARG TARGETPLATFORM
# Metadata labels
LABEL maintainer="BCards Team"
LABEL version=$VERSION
LABEL commit=$COMMIT
LABEL platform=$TARGETPLATFORM
LABEL environment="release"
WORKDIR /app
# Copy published application
COPY --from=publish /app/publish .
# Create non-root user for security
RUN groupadd -r bcards && useradd -r -g bcards bcards \
&& chown -R bcards:bcards /app \
&& chmod +x /app/BCards.Web.dll
# Environment variables for Release
ENV ASPNETCORE_ENVIRONMENT=Release
ENV ASPNETCORE_URLS=http://+:8080
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENV DOTNET_EnableDiagnostics=0
ENV DOTNET_USE_POLLING_FILE_WATCHER=true
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
# ARM64 specific optimizations
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
echo "Applying ARM64 optimizations..." && \
export DOTNET_TieredPGO=1 && \
export DOTNET_TC_QuickJitForLoops=1 && \
export DOTNET_ReadyToRun=0; \
fi
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Switch to non-root user
USER bcards
# Entry point with optimized runtime settings
ENTRYPOINT ["dotnet", "BCards.Web.dll"]
# Runtime configuration for better performance
CMD ["--urls", "http://0.0.0.0:8080"]

160
docker-compose.staging.yml Normal file
View File

@ -0,0 +1,160 @@
version: '3.8'
services:
bcards-web:
image: ${REGISTRY:-registry.redecarneir.us}/bcards:${IMAGE_TAG:-release-latest}
container_name: bcards-staging
restart: unless-stopped
ports:
- "8090:8080"
- "8453:8443"
environment:
# Core ASP.NET Configuration
- ASPNETCORE_ENVIRONMENT=Release
- ASPNETCORE_URLS=http://+:8080
- ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
# MongoDB Configuration
- MongoDb__ConnectionString=${MONGODB_CONNECTION_STRING:-mongodb://192.168.0.100:27017/BCardsDB}
- MongoDb__DatabaseName=BCardsDB
# Application Settings
- AppSettings__Environment=Staging
- AppSettings__Version=${IMAGE_TAG:-unknown}
- AppSettings__AllowedHosts=*
# Logging Configuration
- Logging__LogLevel__Default=Information
- Logging__LogLevel__Microsoft.AspNetCore=Warning
- Logging__LogLevel__BCards=Debug
# Performance Optimizations
- DOTNET_RUNNING_IN_CONTAINER=true
- DOTNET_EnableDiagnostics=0
- DOTNET_USE_POLLING_FILE_WATCHER=true
- DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
- DOTNET_TieredPGO=1
- DOTNET_TC_QuickJitForLoops=1
# Security Headers
- ASPNETCORE_HTTPS_PORT=8443
- ASPNETCORE_Kestrel__Certificates__Default__Path=/app/certs/cert.pfx
- ASPNETCORE_Kestrel__Certificates__Default__Password=${CERT_PASSWORD:-}
# Redis Configuration (if needed)
- Redis__ConnectionString=localhost:6379
volumes:
# Application logs
- ./logs:/app/logs:rw
# File uploads (if needed)
- ./uploads:/app/uploads:rw
# SSL certificates (if using HTTPS)
# - ./certs:/app/certs:ro
networks:
- bcards-staging-network
# Health check configuration
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Resource limits for staging environment
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 512M
cpus: '0.5'
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
# Platform specification (will use the appropriate arch from multi-arch image)
# platform: linux/amd64 # Uncomment if forcing specific architecture
# Security options
security_opt:
- no-new-privileges:true
read_only: false # Set to true for extra security, but may need volume mounts for temp files
# Process limits
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
# Optional: Redis for caching (if application uses it)
redis:
image: redis:7-alpine
container_name: bcards-redis-staging
restart: unless-stopped
ports:
- "6379:6379"
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_staging_data:/data
networks:
- bcards-staging-network
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
# Optional: Nginx reverse proxy for additional features
nginx:
image: nginx:alpine
container_name: bcards-nginx-staging
restart: unless-stopped
ports:
- "8091:80"
- "8454:443"
volumes:
- ./nginx/staging.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/ssl/certs:ro
- ./logs/nginx:/var/log/nginx:rw
depends_on:
- bcards-web
networks:
- bcards-staging-network
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
# Named volumes for persistent data
volumes:
redis_staging_data:
driver: local
driver_opts:
type: none
o: bind
device: ./data/redis
# Network for staging environment
networks:
bcards-staging-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16

369
scripts/deploy-release.sh Normal file
View File

@ -0,0 +1,369 @@
#!/bin/bash
# Deploy script for Release environment with multi-architecture support
# Usage: ./deploy-release.sh <IMAGE_TAG>
set -euo pipefail
# Configuration
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
readonly DEPLOY_DIR="/opt/bcards-staging"
readonly DOCKER_COMPOSE_FILE="docker-compose.staging.yml"
readonly CONTAINER_NAME="bcards-staging"
readonly HEALTH_CHECK_URL="http://localhost:8090/health"
readonly MAX_HEALTH_CHECK_ATTEMPTS=10
readonly HEALTH_CHECK_INTERVAL=10
readonly ROLLBACK_TIMEOUT=300
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Deployment failed with exit code $exit_code"
rollback_deployment
fi
exit $exit_code
}
# Set trap for cleanup on exit
trap cleanup EXIT
# Validate input parameters
validate_input() {
if [ $# -ne 1 ]; then
log_error "Usage: $0 <IMAGE_TAG>"
exit 1
fi
local image_tag="$1"
if [[ ! "$image_tag" =~ ^[a-zA-Z0-9._-]+$ ]]; then
log_error "Invalid image tag format: $image_tag"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
# Check if Docker is running
if ! docker info >/dev/null 2>&1; then
log_error "Docker is not running or not accessible"
exit 1
fi
# Check if docker-compose is available
if ! command -v docker-compose >/dev/null 2>&1; then
log_error "docker-compose is not installed"
exit 1
fi
# Check if deployment directory exists
if [ ! -d "$DEPLOY_DIR" ]; then
log_info "Creating deployment directory: $DEPLOY_DIR"
mkdir -p "$DEPLOY_DIR"
fi
log_success "Prerequisites check passed"
}
# Backup current deployment
backup_current_deployment() {
log_info "Backing up current deployment..."
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_dir="$DEPLOY_DIR/backups/$timestamp"
mkdir -p "$backup_dir"
# Backup environment file if exists
if [ -f "$DEPLOY_DIR/.env" ]; then
cp "$DEPLOY_DIR/.env" "$backup_dir/.env.backup"
cp "$DEPLOY_DIR/.env" "$DEPLOY_DIR/.env.backup"
log_info "Environment file backed up"
fi
# Backup docker-compose file if exists
if [ -f "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" ]; then
cp "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" "$backup_dir/${DOCKER_COMPOSE_FILE}.backup"
log_info "Docker compose file backed up"
fi
# Get current container image for potential rollback
if docker ps --format "table {{.Names}}\t{{.Image}}" | grep -q "$CONTAINER_NAME"; then
local current_image=$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "")
if [ -n "$current_image" ]; then
echo "$current_image" > "$DEPLOY_DIR/.previous_image"
log_info "Current image backed up: $current_image"
fi
fi
log_success "Backup completed: $backup_dir"
}
# Test MongoDB connectivity
test_mongodb_connection() {
log_info "Testing MongoDB connectivity..."
local mongodb_host="192.168.0.100"
local mongodb_port="27017"
# Test basic connectivity
if timeout 10 bash -c "</dev/tcp/$mongodb_host/$mongodb_port" 2>/dev/null; then
log_success "MongoDB connection test passed"
else
log_error "Cannot connect to MongoDB at $mongodb_host:$mongodb_port"
return 1
fi
# Run detailed MongoDB test script if available
if [ -f "$SCRIPT_DIR/test-mongodb-connection.sh" ]; then
log_info "Running detailed MongoDB connection tests..."
bash "$SCRIPT_DIR/test-mongodb-connection.sh"
fi
}
# Pull new Docker image
pull_docker_image() {
local image_tag="$1"
local full_image="registry.redecarneir.us/bcards:$image_tag"
log_info "Pulling Docker image: $full_image"
# Pull the multi-arch image
if docker pull "$full_image"; then
log_success "Image pulled successfully"
else
log_error "Failed to pull image: $full_image"
return 1
fi
# Verify image architecture
local image_arch=$(docker inspect --format='{{.Architecture}}' "$full_image" 2>/dev/null || echo "unknown")
local system_arch=$(uname -m)
log_info "Image architecture: $image_arch"
log_info "System architecture: $system_arch"
# Convert system arch format to Docker format for comparison
case "$system_arch" in
x86_64) system_arch="amd64" ;;
aarch64) system_arch="arm64" ;;
esac
if [ "$image_arch" = "$system_arch" ] || [ "$image_arch" = "unknown" ]; then
log_success "Image architecture is compatible"
else
log_warning "Image architecture ($image_arch) may not match system ($system_arch), but multi-arch support should handle this"
fi
}
# Deploy new version
deploy_new_version() {
local image_tag="$1"
log_info "Deploying new version with tag: $image_tag"
# Copy docker-compose file to deployment directory
cp "$PROJECT_ROOT/$DOCKER_COMPOSE_FILE" "$DEPLOY_DIR/"
# Create/update environment file
cat > "$DEPLOY_DIR/.env" << EOF
IMAGE_TAG=$image_tag
REGISTRY=registry.redecarneir.us
MONGODB_CONNECTION_STRING=mongodb://192.168.0.100:27017/BCardsDB
ASPNETCORE_ENVIRONMENT=Release
CERT_PASSWORD=
EOF
# Stop existing containers
cd "$DEPLOY_DIR"
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps -q | grep -q .; then
log_info "Stopping existing containers..."
docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans
fi
# Start new containers
log_info "Starting new containers..."
docker-compose -f "$DOCKER_COMPOSE_FILE" up -d
# Wait for containers to start
sleep 15
log_success "New version deployed"
}
# Health check
perform_health_check() {
log_info "Performing health check..."
local attempt=1
while [ $attempt -le $MAX_HEALTH_CHECK_ATTEMPTS ]; do
log_info "Health check attempt $attempt/$MAX_HEALTH_CHECK_ATTEMPTS"
# Check if container is running
if ! docker ps --format "table {{.Names}}" | grep -q "$CONTAINER_NAME"; then
log_warning "Container $CONTAINER_NAME is not running"
else
# Check application health endpoint
if curl -f -s "$HEALTH_CHECK_URL" >/dev/null 2>&1; then
log_success "Health check passed"
return 0
fi
# Check if the application is responding on port 80
if curl -f -s "http://localhost:8090/" >/dev/null 2>&1; then
log_success "Application is responding (health endpoint may not be configured)"
return 0
fi
fi
if [ $attempt -eq $MAX_HEALTH_CHECK_ATTEMPTS ]; then
log_error "Health check failed after $MAX_HEALTH_CHECK_ATTEMPTS attempts"
return 1
fi
log_info "Waiting $HEALTH_CHECK_INTERVAL seconds before next attempt..."
sleep $HEALTH_CHECK_INTERVAL
((attempt++))
done
}
# Rollback deployment
rollback_deployment() {
log_warning "Initiating rollback..."
cd "$DEPLOY_DIR"
# Stop current containers
if [ -f "$DOCKER_COMPOSE_FILE" ]; then
docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans
fi
# Restore previous environment if backup exists
if [ -f ".env.backup" ]; then
mv ".env.backup" ".env"
log_info "Previous environment restored"
fi
# Try to start previous version if image is available
if [ -f ".previous_image" ]; then
local previous_image=$(cat ".previous_image")
log_info "Attempting to restore previous image: $previous_image"
# Update .env with previous image tag
local previous_tag=$(echo "$previous_image" | cut -d':' -f2)
sed -i "s/IMAGE_TAG=.*/IMAGE_TAG=$previous_tag/" .env 2>/dev/null || true
# Try to start previous version
if docker-compose -f "$DOCKER_COMPOSE_FILE" up -d; then
log_success "Rollback completed successfully"
else
log_error "Rollback failed - manual intervention required"
fi
else
log_warning "No previous version found for rollback"
fi
}
# Cleanup old images and containers
cleanup_old_resources() {
log_info "Cleaning up old Docker resources..."
# Remove dangling images
if docker images -f "dangling=true" -q | head -1 | grep -q .; then
docker rmi $(docker images -f "dangling=true" -q) || true
log_info "Dangling images removed"
fi
# Remove old backups (keep last 5)
if [ -d "$DEPLOY_DIR/backups" ]; then
find "$DEPLOY_DIR/backups" -maxdepth 1 -type d -name "20*" | sort -r | tail -n +6 | xargs rm -rf || true
log_info "Old backups cleaned up"
fi
log_success "Cleanup completed"
}
# Display deployment summary
display_summary() {
local image_tag="$1"
log_success "Deployment Summary:"
echo "=================================="
echo "🚀 Image Tag: $image_tag"
echo "🌐 Environment: Release (Staging)"
echo "🔗 Application URL: http://localhost:8090"
echo "🔗 Health Check: $HEALTH_CHECK_URL"
echo "🗄️ MongoDB: 192.168.0.100:27017"
echo "📁 Deploy Directory: $DEPLOY_DIR"
echo "🐳 Container: $CONTAINER_NAME"
# Show container status
echo ""
echo "Container Status:"
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(NAMES|$CONTAINER_NAME)" || true
# Show image info
echo ""
echo "Image Information:"
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}" | grep -E "(REPOSITORY|bcards)" | head -5 || true
echo "=================================="
}
# Main deployment function
main() {
local image_tag="$1"
log_info "Starting deployment process for BCards Release environment"
log_info "Target image tag: $image_tag"
log_info "Target architecture: $(uname -m)"
log_info "Deploy directory: $DEPLOY_DIR"
# Execute deployment steps
validate_input "$@"
check_prerequisites
test_mongodb_connection
backup_current_deployment
pull_docker_image "$image_tag"
deploy_new_version "$image_tag"
# Perform health check (rollback handled by trap if this fails)
if perform_health_check; then
cleanup_old_resources
display_summary "$image_tag"
log_success "Deployment completed successfully!"
else
log_error "Health check failed - rollback will be triggered"
exit 1
fi
}
# Run main function with all arguments
main "$@"

View File

@ -0,0 +1,495 @@
#!/bin/bash
# MongoDB Connection Test Script for Release Environment
# Tests connectivity, database operations, and index validation
set -euo pipefail
# Configuration
readonly MONGODB_HOST="${MONGODB_HOST:-192.168.0.100}"
readonly MONGODB_PORT="${MONGODB_PORT:-27017}"
readonly DATABASE_NAME="${DATABASE_NAME:-BCardsDB}"
readonly CONNECTION_STRING="mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${DATABASE_NAME}"
readonly TIMEOUT=30
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Test basic TCP connectivity
test_tcp_connection() {
log_info "Testing TCP connection to $MONGODB_HOST:$MONGODB_PORT..."
if timeout $TIMEOUT bash -c "</dev/tcp/$MONGODB_HOST/$MONGODB_PORT" 2>/dev/null; then
log_success "TCP connection successful"
return 0
else
log_error "TCP connection failed"
return 1
fi
}
# Test MongoDB connectivity using mongosh if available
test_mongodb_with_mongosh() {
if ! command -v mongosh >/dev/null 2>&1; then
log_warning "mongosh not available, skipping MongoDB shell tests"
return 1
fi
log_info "Testing MongoDB connection with mongosh..."
# Test basic connection
local test_output=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval "db.runCommand({ping: 1})" 2>/dev/null || echo "FAILED")
if [[ "$test_output" == *"ok"* ]]; then
log_success "MongoDB ping successful"
else
log_error "MongoDB ping failed"
return 1
fi
# Test database access
log_info "Testing database operations..."
local db_test=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval "
try {
// Test basic database operations
db.connection_test.insertOne({test: true, timestamp: new Date()});
var result = db.connection_test.findOne({test: true});
db.connection_test.deleteOne({test: true});
print('DATABASE_ACCESS_OK');
} catch (e) {
print('DATABASE_ACCESS_FAILED: ' + e.message);
}
" 2>/dev/null || echo "DATABASE_ACCESS_FAILED")
if [[ "$db_test" == *"DATABASE_ACCESS_OK"* ]]; then
log_success "Database operations test passed"
else
log_error "Database operations test failed: $db_test"
return 1
fi
return 0
}
# Test MongoDB connectivity using Python if available
test_mongodb_with_python() {
if ! command -v python3 >/dev/null 2>&1; then
log_warning "Python3 not available, skipping Python MongoDB tests"
return 1
fi
log_info "Testing MongoDB connection with Python..."
python3 << EOF
import sys
try:
import pymongo
from pymongo import MongoClient
import socket
# Test connection
client = MongoClient("$CONNECTION_STRING", serverSelectionTimeoutMS=$((TIMEOUT * 1000)))
# Test ping
client.admin.command('ping')
print("MongoDB ping successful (Python)")
# Test database access
db = client["$DATABASE_NAME"]
# Insert test document
test_collection = db.connection_test
result = test_collection.insert_one({"test": True, "source": "python"})
# Read test document
doc = test_collection.find_one({"_id": result.inserted_id})
if doc:
print("Database read/write test passed (Python)")
# Cleanup
test_collection.delete_one({"_id": result.inserted_id})
client.close()
print("PYTHON_TEST_SUCCESS")
except ImportError:
print("PyMongo not installed, skipping Python tests")
sys.exit(1)
except Exception as e:
print(f"Python MongoDB test failed: {e}")
sys.exit(1)
EOF
local python_result=$?
if [ $python_result -eq 0 ]; then
log_success "Python MongoDB test passed"
return 0
else
log_error "Python MongoDB test failed"
return 1
fi
}
# Test using Docker MongoDB client
test_mongodb_with_docker() {
if ! command -v docker >/dev/null 2>&1; then
log_warning "Docker not available, skipping Docker MongoDB tests"
return 1
fi
log_info "Testing MongoDB connection using Docker MongoDB client..."
# Use official MongoDB image to test connection
local docker_test=$(timeout $TIMEOUT docker run --rm mongo:7.0 mongosh "$CONNECTION_STRING" --quiet --eval "
try {
db.runCommand({ping: 1});
db.connection_test.insertOne({test: true, source: 'docker', timestamp: new Date()});
var doc = db.connection_test.findOne({source: 'docker'});
db.connection_test.deleteOne({source: 'docker'});
print('DOCKER_TEST_SUCCESS');
} catch (e) {
print('DOCKER_TEST_FAILED: ' + e.message);
}
" 2>/dev/null || echo "DOCKER_TEST_FAILED")
if [[ "$docker_test" == *"DOCKER_TEST_SUCCESS"* ]]; then
log_success "Docker MongoDB test passed"
return 0
else
log_error "Docker MongoDB test failed: $docker_test"
return 1
fi
}
# Test MongoDB from application container
test_from_application_container() {
local container_name="bcards-staging"
if ! docker ps --format "{{.Names}}" | grep -q "^${container_name}$"; then
log_warning "Application container '$container_name' not running, skipping application test"
return 1
fi
log_info "Testing MongoDB connection from application container..."
# Test connection from the application container
local app_test=$(docker exec "$container_name" timeout 10 bash -c "
# Test TCP connection
if timeout 5 bash -c '</dev/tcp/$MONGODB_HOST/$MONGODB_PORT' 2>/dev/null; then
echo 'APP_TCP_OK'
else
echo 'APP_TCP_FAILED'
exit 1
fi
# Test HTTP health endpoint if available
if curl -f -s http://localhost:8080/health >/dev/null 2>&1; then
echo 'APP_HEALTH_OK'
else
echo 'APP_HEALTH_FAILED'
fi
" 2>/dev/null || echo "APP_TEST_FAILED")
if [[ "$app_test" == *"APP_TCP_OK"* ]]; then
log_success "Application container can connect to MongoDB"
if [[ "$app_test" == *"APP_HEALTH_OK"* ]]; then
log_success "Application health check passed"
else
log_warning "Application health check failed - app may still be starting"
fi
return 0
else
log_error "Application container cannot connect to MongoDB"
return 1
fi
}
# Check MongoDB server status and version
check_mongodb_status() {
log_info "Checking MongoDB server status..."
# Try multiple methods to check status
local status_checked=false
# Method 1: Using mongosh
if command -v mongosh >/dev/null 2>&1; then
local server_status=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval "
try {
var status = db.runCommand({serverStatus: 1});
print('MongoDB Version: ' + status.version);
print('Uptime: ' + status.uptime + ' seconds');
print('Connections: ' + status.connections.current + '/' + status.connections.available);
print('STATUS_CHECK_OK');
} catch (e) {
print('STATUS_CHECK_FAILED: ' + e.message);
}
" 2>/dev/null || echo "STATUS_CHECK_FAILED")
if [[ "$server_status" == *"STATUS_CHECK_OK"* ]]; then
echo "$server_status" | grep -v "STATUS_CHECK_OK"
log_success "MongoDB server status check passed"
status_checked=true
fi
fi
# Method 2: Using Docker if mongosh failed
if [ "$status_checked" = false ] && command -v docker >/dev/null 2>&1; then
local docker_status=$(timeout $TIMEOUT docker run --rm mongo:7.0 mongosh "$CONNECTION_STRING" --quiet --eval "
try {
var status = db.runCommand({serverStatus: 1});
print('MongoDB Version: ' + status.version);
print('STATUS_CHECK_OK');
} catch (e) {
print('STATUS_CHECK_FAILED: ' + e.message);
}
" 2>/dev/null || echo "STATUS_CHECK_FAILED")
if [[ "$docker_status" == *"STATUS_CHECK_OK"* ]]; then
echo "$docker_status" | grep -v "STATUS_CHECK_OK"
log_success "MongoDB server status check passed (via Docker)"
status_checked=true
fi
fi
if [ "$status_checked" = false ]; then
log_warning "Could not retrieve MongoDB server status"
return 1
fi
return 0
}
# Test BCards specific collections and indexes
test_bcards_collections() {
if ! command -v mongosh >/dev/null 2>&1 && ! command -v docker >/dev/null 2>&1; then
log_warning "Cannot test BCards collections - no MongoDB client available"
return 1
fi
log_info "Testing BCards specific collections and indexes..."
local mongo_cmd="mongosh"
local docker_prefix=""
if ! command -v mongosh >/dev/null 2>&1; then
mongo_cmd="docker run --rm mongo:7.0 mongosh"
docker_prefix="timeout $TIMEOUT "
fi
local collections_test=$(${docker_prefix}${mongo_cmd} "$CONNECTION_STRING" --quiet --eval "
try {
// Check required collections
var collections = db.listCollectionNames();
var requiredCollections = ['users', 'userpages', 'categories'];
var missingCollections = [];
requiredCollections.forEach(function(collection) {
if (collections.indexOf(collection) === -1) {
missingCollections.push(collection);
}
});
if (missingCollections.length > 0) {
print('Missing collections: ' + missingCollections.join(', '));
} else {
print('All required collections exist');
}
// Check indexes on userpages collection
if (collections.indexOf('userpages') !== -1) {
var indexes = db.userpages.getIndexes();
print('UserPages collection has ' + indexes.length + ' indexes');
// Check for important compound index
var hasCompoundIndex = indexes.some(function(index) {
return index.key && index.key.category && index.key.slug;
});
if (hasCompoundIndex) {
print('Required compound index (category, slug) exists');
} else {
print('WARNING: Compound index (category, slug) is missing');
}
}
print('COLLECTIONS_TEST_OK');
} catch (e) {
print('COLLECTIONS_TEST_FAILED: ' + e.message);
}
" 2>/dev/null || echo "COLLECTIONS_TEST_FAILED")
if [[ "$collections_test" == *"COLLECTIONS_TEST_OK"* ]]; then
echo "$collections_test" | grep -v "COLLECTIONS_TEST_OK"
log_success "BCards collections test passed"
return 0
else
log_warning "BCards collections test had issues: $collections_test"
return 1
fi
}
# Performance test
test_mongodb_performance() {
log_info "Running basic performance test..."
if ! command -v mongosh >/dev/null 2>&1; then
log_warning "mongosh not available, skipping performance test"
return 1
fi
local perf_test=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval "
try {
var start = new Date();
// Insert test documents
var docs = [];
for (var i = 0; i < 100; i++) {
docs.push({test: true, index: i, timestamp: new Date()});
}
db.performance_test.insertMany(docs);
// Read test
var count = db.performance_test.countDocuments({test: true});
// Update test
db.performance_test.updateMany({test: true}, {\$set: {updated: true}});
// Delete test
db.performance_test.deleteMany({test: true});
var end = new Date();
var duration = end - start;
print('Performance test completed in ' + duration + 'ms');
print('Operations: 100 inserts, 1 count, 100 updates, 100 deletes');
if (duration < 5000) {
print('PERFORMANCE_TEST_OK');
} else {
print('PERFORMANCE_TEST_SLOW');
}
} catch (e) {
print('PERFORMANCE_TEST_FAILED: ' + e.message);
}
" 2>/dev/null || echo "PERFORMANCE_TEST_FAILED")
if [[ "$perf_test" == *"PERFORMANCE_TEST_OK"* ]]; then
echo "$perf_test" | grep -v "PERFORMANCE_TEST_OK"
log_success "Performance test passed"
return 0
elif [[ "$perf_test" == *"PERFORMANCE_TEST_SLOW"* ]]; then
echo "$perf_test" | grep -v "PERFORMANCE_TEST_SLOW"
log_warning "Performance test completed but was slow"
return 0
else
log_error "Performance test failed: $perf_test"
return 1
fi
}
# Display connection summary
display_summary() {
echo ""
log_info "MongoDB Connection Test Summary"
echo "=================================================="
echo "🏠 Host: $MONGODB_HOST:$MONGODB_PORT"
echo "🗄️ Database: $DATABASE_NAME"
echo "🔗 Connection String: $CONNECTION_STRING"
echo "⏱️ Timeout: ${TIMEOUT}s"
echo "📊 Tests completed: $(date)"
echo "=================================================="
}
# Main test function
main() {
log_info "Starting MongoDB connection tests for Release environment"
local test_results=()
local overall_success=true
# Run all tests
if test_tcp_connection; then
test_results+=("✅ TCP Connection")
else
test_results+=("❌ TCP Connection")
overall_success=false
fi
if test_mongodb_with_mongosh; then
test_results+=("✅ MongoDB Shell")
elif test_mongodb_with_docker; then
test_results+=("✅ MongoDB Docker")
elif test_mongodb_with_python; then
test_results+=("✅ MongoDB Python")
else
test_results+=("❌ MongoDB Client")
overall_success=false
fi
if test_from_application_container; then
test_results+=("✅ Application Container")
else
test_results+=("⚠️ Application Container")
fi
if check_mongodb_status; then
test_results+=("✅ Server Status")
else
test_results+=("⚠️ Server Status")
fi
if test_bcards_collections; then
test_results+=("✅ BCards Collections")
else
test_results+=("⚠️ BCards Collections")
fi
if test_mongodb_performance; then
test_results+=("✅ Performance Test")
else
test_results+=("⚠️ Performance Test")
fi
# Display results
display_summary
echo ""
log_info "Test Results:"
for result in "${test_results[@]}"; do
echo " $result"
done
echo ""
if [ "$overall_success" = true ]; then
log_success "All critical MongoDB tests passed!"
exit 0
else
log_error "Some critical MongoDB tests failed!"
exit 1
fi
}
# Run main function
main "$@"

View File

@ -0,0 +1,141 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"BCards": "Information"
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Information"
}
},
"File": {
"Path": "/app/logs/bcards-{Date}.log",
"LogLevel": {
"Default": "Information"
}
}
},
"AllowedHosts": "*",
"MongoDb": {
"ConnectionString": "mongodb://192.168.0.100:27017/BCardsDB",
"DatabaseName": "BCardsDB",
"MaxConnectionPoolSize": 100,
"ConnectTimeout": "30s",
"ServerSelectionTimeout": "30s",
"SocketTimeout": "30s"
},
"Stripe": {
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543",
"ApiVersion": "2023-10-16"
},
"Authentication": {
"Google": {
"ClientId": "472850008574-nmeepbdt4hunsk5c8krpbdmd3olc4jv6.apps.googleusercontent.com",
"ClientSecret": "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"
},
"Microsoft": {
"ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3",
"ClientSecret": "T0.8Q~an.51iW1H0DVjL2i1bmSK_qTgVQOuEmapK"
},
"CookieSettings": {
"SecurePolicy": "Always",
"SameSiteMode": "Lax",
"HttpOnly": true,
"ExpireTimeSpan": "7.00:00:00"
}
},
"Plans": {
"Basic": {
"PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y",
"Price": 9.90,
"MaxLinks": 5,
"Features": [ "basic_themes", "simple_analytics" ]
},
"Professional": {
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
"Price": 24.90,
"MaxLinks": 15,
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
},
"Premium": {
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
"Price": 29.90,
"MaxLinks": -1,
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
}
},
"Moderation": {
"PriorityTimeframes": {
"Trial": "7.00:00:00",
"Basic": "7.00:00:00",
"Professional": "3.00:00:00",
"Premium": "1.00:00:00"
},
"MaxAttempts": 3,
"ModeratorEmail": "ricardo.carneiro@jobmaker.com.br",
"ModeratorEmails": [
"rrcgoncalves@gmail.com",
"rirocarneiro@gmail.com"
],
"PreviewTokenExpirationHours": 4,
"AutoRejectDays": 30
},
"SendGrid": {
"ApiKey": "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",
"FromEmail": "ricardo.carneiro@jobmaker.com.br",
"FromName": "BCards - Staging",
"ReplyToEmail": "ricardo.carneiro@jobmaker.com.br"
},
"BaseUrl": "http://192.168.0.100:8090",
"Environment": {
"Name": "Release",
"IsStagingEnvironment": true,
"AllowTestData": true,
"EnableDetailedErrors": false
},
"Performance": {
"EnableCaching": true,
"CacheExpirationMinutes": 30,
"EnableCompression": true,
"EnableResponseCaching": true
},
"Security": {
"EnableHttpsRedirection": false,
"EnableHsts": false,
"RequireHttpsMetadata": false,
"CorsOrigins": [
"http://192.168.0.100:8090",
"http://localhost:8090"
]
},
"HealthChecks": {
"Enabled": true,
"Endpoints": {
"Health": "/health",
"Ready": "/ready",
"Live": "/live"
},
"MongoDb": {
"Enabled": true,
"Timeout": "10s"
}
},
"Monitoring": {
"EnableMetrics": true,
"MetricsEndpoint": "/metrics",
"EnableTracing": false
},
"Features": {
"EnablePreviewMode": true,
"EnableModerationWorkflow": true,
"EnableAnalytics": true,
"EnableFileUploads": true,
"MaxFileUploadSize": "5MB"
}
}