CI/CD Gitea v1.0
Some checks failed
Deploy to Staging / Build Images (push) Failing after 7s
Deploy to Staging / Deploy to Staging (push) Has been skipped
Deploy to Staging / Verify Staging (push) Has been skipped
Deploy to Staging / Notify Staging Ready (push) Has been skipped
Deploy to Staging / Notify Staging Failure (push) Failing after 6s

This commit is contained in:
Eric Gullickson
2025-12-29 18:51:41 -06:00
parent 9b0de6a5b8
commit 83d79da3aa
15 changed files with 1101 additions and 929 deletions

View File

@@ -0,0 +1,67 @@
# MotoVaultPro Maintenance Migration Workflow
# Manual trigger for breaking database migrations requiring downtime
name: Maintenance Migration
run-name: Maintenance Migration
on:
workflow_dispatch:
inputs:
create_backup:
description: 'Create database backup before migration'
required: true
default: 'yes'
type: choice
options:
- 'yes'
- 'no'
env:
DEPLOY_PATH: /opt/motovaultpro
jobs:
maintenance-migration:
name: Run Maintenance Migration
runs-on: mvp-prod
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Send maintenance start notification
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh maintenance_start "Starting maintenance window for database migration"
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
- name: Run maintenance migration
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/maintenance-migrate.sh
if [ "${{ inputs.create_backup }}" = "yes" ]; then
./scripts/ci/maintenance-migrate.sh backup
else
./scripts/ci/maintenance-migrate.sh
fi
- name: Send maintenance complete notification
if: success()
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh maintenance_end "Maintenance window complete. Database migration successful."
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
- name: Send maintenance failure notification
if: failure()
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh failure "Maintenance migration FAILED. Manual intervention required."
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}

View File

@@ -0,0 +1,38 @@
# MotoVaultPro Base Image Mirroring Workflow
# Mirrors upstream Docker images to Gitea Package Registry
# Runs weekly on schedule or manual trigger
name: Mirror Base Images
run-name: Mirror Base Images to Registry
on:
schedule:
# Run every Sunday at 3:00 AM UTC
- cron: '0 3 * * 0'
workflow_dispatch:
env:
REGISTRY: git.motovaultpro.com
jobs:
mirror:
name: Mirror Base Images
runs-on: mvp-build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
- name: Run mirror script
run: |
chmod +x scripts/ci/mirror-base-images.sh
REGISTRY=$REGISTRY/egullickson/mirrors ./scripts/ci/mirror-base-images.sh
- name: Report results
if: always()
run: |
echo "Base image mirroring complete"
echo "Mirrored images available at: $REGISTRY/egullickson/mirrors/"

View File

@@ -0,0 +1,260 @@
# MotoVaultPro Production Deployment Workflow
# Manual trigger only - run after verifying staging
# Blue-green deployment with auto-rollback
name: Deploy to Production
run-name: Production Deploy - ${{ inputs.image_tag || 'latest' }}
on:
workflow_dispatch:
inputs:
image_tag:
description: 'Image tag to deploy (defaults to latest)'
required: false
default: 'latest'
env:
REGISTRY: git.motovaultpro.com
DEPLOY_PATH: /opt/motovaultpro
COMPOSE_FILE: docker-compose.yml
COMPOSE_BLUE_GREEN: docker-compose.blue-green.yml
HEALTH_CHECK_TIMEOUT: "60"
jobs:
# ============================================
# VALIDATE - Determine target stack
# ============================================
validate:
name: Validate Prerequisites
runs-on: mvp-prod
outputs:
target_stack: ${{ steps.determine-stack.outputs.target_stack }}
backend_image: ${{ steps.set-images.outputs.backend_image }}
frontend_image: ${{ steps.set-images.outputs.frontend_image }}
steps:
- name: Check Docker availability
run: |
docker info > /dev/null 2>&1 || (echo "ERROR - Docker not accessible" && exit 1)
docker compose version > /dev/null 2>&1 || (echo "ERROR - Docker Compose not available" && exit 1)
- name: Check deployment path
run: test -d "$DEPLOY_PATH" || (echo "ERROR - DEPLOY_PATH not found" && exit 1)
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
- name: Set image tags
id: set-images
run: |
TAG="${{ inputs.image_tag }}"
echo "backend_image=$REGISTRY/egullickson/backend:$TAG" >> $GITHUB_OUTPUT
echo "frontend_image=$REGISTRY/egullickson/frontend:$TAG" >> $GITHUB_OUTPUT
- name: Determine target stack
id: determine-stack
run: |
STATE_FILE="$DEPLOY_PATH/config/deployment/state.json"
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
ACTIVE_STACK=$(jq -r '.active_stack // "blue"' "$STATE_FILE")
if [ "$ACTIVE_STACK" = "blue" ]; then
echo "target_stack=green" >> $GITHUB_OUTPUT
echo "Deploying to GREEN stack (BLUE is currently active)"
else
echo "target_stack=blue" >> $GITHUB_OUTPUT
echo "Deploying to BLUE stack (GREEN is currently active)"
fi
else
echo "target_stack=green" >> $GITHUB_OUTPUT
echo "No state file found, defaulting to GREEN stack"
fi
# ============================================
# DEPLOY PROD - Blue-green deployment
# ============================================
deploy-prod:
name: Deploy to Production
runs-on: mvp-prod
needs: validate
env:
TARGET_STACK: ${{ needs.validate.outputs.target_stack }}
BACKEND_IMAGE: ${{ needs.validate.outputs.backend_image }}
FRONTEND_IMAGE: ${{ needs.validate.outputs.frontend_image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
- name: Inject secrets
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/inject-secrets.sh
./scripts/inject-secrets.sh
env:
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }}
AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }}
CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
- name: Initialize data directories
run: |
cd "$DEPLOY_PATH"
sudo mkdir -p data/backups data/documents
sudo chown -R 1001:1001 data/backups data/documents
sudo chmod 755 data/backups data/documents
- name: Pull new images
run: |
docker pull $BACKEND_IMAGE
docker pull $FRONTEND_IMAGE
- name: Start target stack
run: |
cd "$DEPLOY_PATH"
export BACKEND_IMAGE=$BACKEND_IMAGE
export FRONTEND_IMAGE=$FRONTEND_IMAGE
docker compose -f $COMPOSE_FILE -f $COMPOSE_BLUE_GREEN up -d \
mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK
- name: Wait for stack initialization
run: sleep 10
- name: Run health check
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/health-check.sh
./scripts/ci/health-check.sh $TARGET_STACK $HEALTH_CHECK_TIMEOUT
- name: Switch traffic
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/switch-traffic.sh
./scripts/ci/switch-traffic.sh $TARGET_STACK instant
- name: Update deployment state
run: |
cd "$DEPLOY_PATH"
STATE_FILE="config/deployment/state.json"
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
jq --arg commit "${{ inputs.image_tag }}" \
--arg ts "$TIMESTAMP" \
'.last_deployment = $ts | .last_deployment_commit = $commit | .last_deployment_status = "success" | .rollback_available = true' \
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
fi
# ============================================
# VERIFY PROD - External health check
# ============================================
verify-prod:
name: Verify Production
runs-on: mvp-prod
needs: [validate, deploy-prod]
env:
TARGET_STACK: ${{ needs.validate.outputs.target_stack }}
steps:
- name: Wait for routing propagation
run: sleep 5
- name: External health check
run: |
for i in 1 2 3 4 5 6; do
if curl -sf https://motovaultpro.com/api/health > /dev/null 2>&1; then
echo "OK: Production external health check passed"
exit 0
fi
if [ $i -eq 6 ]; then
echo "ERROR: Production external health check failed after 6 attempts"
exit 1
fi
echo "Attempt $i/6: Waiting 10s..."
sleep 10
done
- name: Verify container status
run: |
for service in mvp-frontend-$TARGET_STACK mvp-backend-$TARGET_STACK; do
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
if [ "$status" != "running" ] || [ "$health" != "healthy" ]; then
echo "ERROR: $service is not healthy (status: $status, health: $health)"
docker logs $service --tail 50 2>/dev/null || true
exit 1
fi
echo "OK: $service is running and healthy"
done
# ============================================
# ROLLBACK - Auto-rollback on failure
# ============================================
rollback:
name: Auto Rollback
runs-on: mvp-prod
needs: [validate, deploy-prod, verify-prod]
if: failure()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Execute rollback
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/auto-rollback.sh
./scripts/ci/auto-rollback.sh "Production verification failed - automatic rollback"
- name: Update state
run: |
cd "$DEPLOY_PATH"
STATE_FILE="config/deployment/state.json"
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
jq '.last_deployment_status = "rolled_back"' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
fi
# ============================================
# NOTIFY SUCCESS
# ============================================
notify-success:
name: Notify Success
runs-on: mvp-prod
needs: [validate, verify-prod]
if: success()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Send success notification
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh success "Production deployment successful - ${{ inputs.image_tag }} is now live" ${{ inputs.image_tag }}
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
# ============================================
# NOTIFY FAILURE
# ============================================
notify-failure:
name: Notify Failure
runs-on: mvp-prod
needs: [validate, deploy-prod, verify-prod, rollback]
if: failure()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Send failure notification
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh failure "Production deployment failed for ${{ inputs.image_tag }}" ${{ inputs.image_tag }}
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}

View File

@@ -0,0 +1,234 @@
# MotoVaultPro Staging Deployment Workflow
# Triggers on push to main, builds and deploys to staging.motovaultpro.com
# After verification, sends notification with link to trigger production deploy
name: Deploy to Staging
run-name: Staging Deploy - ${{ gitea.sha }}
on:
push:
branches:
- main
env:
REGISTRY: git.motovaultpro.com
DEPLOY_PATH: /opt/motovaultpro
COMPOSE_FILE: docker-compose.yml
COMPOSE_STAGING: docker-compose.staging.yml
HEALTH_CHECK_TIMEOUT: "60"
jobs:
# ============================================
# BUILD - Build and push images
# ============================================
build:
name: Build Images
runs-on: mvp-build
outputs:
backend_image: ${{ steps.tags.outputs.backend_image }}
frontend_image: ${{ steps.tags.outputs.frontend_image }}
short_sha: ${{ steps.tags.outputs.short_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Gitea Container Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
- name: Set image tags
id: tags
run: |
SHORT_SHA="${{ gitea.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
echo "backend_image=$REGISTRY/egullickson/backend:$SHORT_SHA" >> $GITHUB_OUTPUT
echo "frontend_image=$REGISTRY/egullickson/frontend:$SHORT_SHA" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
- name: Build backend image
run: |
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \
--cache-from $REGISTRY/egullickson/backend:latest \
-t ${{ steps.tags.outputs.backend_image }} \
-t $REGISTRY/egullickson/backend:latest \
-f backend/Dockerfile \
.
- name: Build frontend image
run: |
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \
--build-arg VITE_AUTH0_DOMAIN=${{ vars.VITE_AUTH0_DOMAIN }} \
--build-arg VITE_AUTH0_CLIENT_ID=${{ vars.VITE_AUTH0_CLIENT_ID }} \
--build-arg VITE_AUTH0_AUDIENCE=${{ vars.VITE_AUTH0_AUDIENCE }} \
--build-arg VITE_API_BASE_URL=/api \
--cache-from $REGISTRY/egullickson/frontend:latest \
-t ${{ steps.tags.outputs.frontend_image }} \
-t $REGISTRY/egullickson/frontend:latest \
-f frontend/Dockerfile \
frontend
- name: Push images
run: |
docker push ${{ steps.tags.outputs.backend_image }}
docker push ${{ steps.tags.outputs.frontend_image }}
docker push $REGISTRY/egullickson/backend:latest
docker push $REGISTRY/egullickson/frontend:latest
# ============================================
# DEPLOY STAGING - Deploy to staging server
# ============================================
deploy-staging:
name: Deploy to Staging
runs-on: mvp-build
needs: build
env:
BACKEND_IMAGE: ${{ needs.build.outputs.backend_image }}
FRONTEND_IMAGE: ${{ needs.build.outputs.frontend_image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USER }}" --password-stdin "$REGISTRY"
- name: Inject secrets
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/inject-secrets.sh
SECRETS_DIR="$DEPLOY_PATH/secrets/staging" ./scripts/inject-secrets.sh
env:
POSTGRES_PASSWORD: ${{ secrets.STAGING_POSTGRES_PASSWORD }}
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_MANAGEMENT_CLIENT_ID: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_ID }}
AUTH0_MANAGEMENT_CLIENT_SECRET: ${{ secrets.AUTH0_MANAGEMENT_CLIENT_SECRET }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
GOOGLE_MAPS_MAP_ID: ${{ secrets.GOOGLE_MAPS_MAP_ID }}
CF_DNS_API_TOKEN: ${{ secrets.CF_DNS_API_TOKEN }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
- name: Initialize data directories
run: |
cd "$DEPLOY_PATH"
sudo mkdir -p data/backups data/documents
sudo chown -R 1001:1001 data/backups data/documents
sudo chmod 755 data/backups data/documents
- name: Pull new images
run: |
docker pull $BACKEND_IMAGE
docker pull $FRONTEND_IMAGE
- name: Deploy staging stack
run: |
cd "$DEPLOY_PATH"
export BACKEND_IMAGE=$BACKEND_IMAGE
export FRONTEND_IMAGE=$FRONTEND_IMAGE
docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING down --timeout 30 || true
docker compose -f $COMPOSE_FILE -f $COMPOSE_STAGING up -d
- name: Wait for services
run: sleep 15
# ============================================
# VERIFY STAGING - Health checks
# ============================================
verify-staging:
name: Verify Staging
runs-on: mvp-build
needs: [build, deploy-staging]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check container status
run: |
for service in mvp-frontend-staging mvp-backend-staging mvp-postgres-staging mvp-redis-staging; do
status=$(docker inspect --format='{{.State.Status}}' $service 2>/dev/null || echo "not found")
if [ "$status" != "running" ]; then
echo "ERROR: $service is not running (status: $status)"
docker logs $service --tail 50 2>/dev/null || true
exit 1
fi
echo "OK: $service is running"
done
- name: Wait for backend health
run: |
for i in 1 2 3 4 5 6; do
if docker exec mvp-backend-staging curl -sf http://localhost:3001/health > /dev/null 2>&1; then
echo "OK: Backend health check passed"
exit 0
fi
if [ $i -eq 6 ]; then
echo "ERROR: Backend health check failed after 6 attempts"
docker logs mvp-backend-staging --tail 100
exit 1
fi
echo "Attempt $i/6: Backend not ready, waiting 10s..."
sleep 10
done
- name: Check external endpoint
run: |
for i in 1 2 3 4 5 6; do
if curl -sf https://staging.motovaultpro.com/api/health > /dev/null 2>&1; then
echo "OK: Staging external health check passed"
exit 0
fi
if [ $i -eq 6 ]; then
echo "ERROR: Staging external health check failed after 6 attempts"
exit 1
fi
echo "Attempt $i/6: Waiting 10s..."
sleep 10
done
# ============================================
# NOTIFY - Staging ready for production
# ============================================
notify-staging-ready:
name: Notify Staging Ready
runs-on: mvp-build
needs: [build, verify-staging]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Send staging ready notification
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
./scripts/ci/notify.sh staging_ready \
"Staging verified for commit ${{ needs.build.outputs.short_sha }}. Review at https://staging.motovaultpro.com then trigger production deploy at https://git.motovaultpro.com/egullickson/motovaultpro/actions" \
${{ needs.build.outputs.short_sha }}
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
# ============================================
# NOTIFY FAILURE - Staging failed
# ============================================
notify-staging-failure:
name: Notify Staging Failure
runs-on: mvp-build
needs: [build, deploy-staging, verify-staging]
if: failure()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Send failure notification
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/notify.sh
SHORT_SHA="${{ gitea.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
./scripts/ci/notify.sh failure "Staging deployment failed for commit $SHORT_SHA" $SHORT_SHA
env:
DEPLOY_NOTIFY_EMAIL: ${{ vars.DEPLOY_NOTIFY_EMAIL }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}