Files
motovaultpro/.gitea/workflows/staging.yaml
Eric Gullickson 72275096f8
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 31s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 22s
Deploy to Staging / Verify Staging (pull_request) Successful in 2m31s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
docs: add unified logging system documentation and CI/CD integration (refs #87)
- Update staging workflow to use LOG_LEVEL=DEBUG
- Create docs/LOGGING.md with unified logging documentation
- Delete docs/UX-DEBUGGING.md (replaced by LOGGING.md)
- Update architecture to 9-container (6 app + 3 logging)
- Update CLAUDE.md, README.md, docs/README.md, docs/CLAUDE.md
- Update docs/PLATFORM-SERVICES.md deployment section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:50:20 -06:00

330 lines
13 KiB
YAML

# MotoVaultPro Staging Deployment Workflow
# Triggers on push to main or any pull request, builds and deploys to staging.motovaultpro.com
# After verification, sends notification with link to trigger production deploy
name: Deploy to Staging
run-name: "Staging - ${{ gitea.event.pull_request.title || gitea.ref_name }}"
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
env:
REGISTRY: git.motovaultpro.com
DEPLOY_PATH: /opt/motovaultpro
COMPOSE_FILE: docker-compose.yml
COMPOSE_STAGING: docker-compose.staging.yml
HEALTH_CHECK_TIMEOUT: "60"
LOG_LEVEL: DEBUG
jobs:
# ============================================
# BUILD - Build and push images
# ============================================
build:
name: Build Images
runs-on: stage
outputs:
backend_image: ${{ steps.tags.outputs.backend_image }}
frontend_image: ${{ steps.tags.outputs.frontend_image }}
ocr_image: ${{ steps.tags.outputs.ocr_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 "ocr_image=$REGISTRY/egullickson/ocr:$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 \
--build-arg VITE_STRIPE_PUBLISHABLE_KEY=${{ vars.VITE_STRIPE_PUBLISHABLE_KEY }} \
--cache-from $REGISTRY/egullickson/frontend:latest \
-t ${{ steps.tags.outputs.frontend_image }} \
-t $REGISTRY/egullickson/frontend:latest \
-f frontend/Dockerfile \
frontend
- name: Build OCR image
run: |
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg REGISTRY_MIRRORS=$REGISTRY/egullickson/mirrors \
--cache-from $REGISTRY/egullickson/ocr:latest \
-t ${{ steps.tags.outputs.ocr_image }} \
-t $REGISTRY/egullickson/ocr:latest \
-f ocr/Dockerfile \
ocr
- name: Push images
run: |
docker push ${{ steps.tags.outputs.backend_image }}
docker push ${{ steps.tags.outputs.frontend_image }}
docker push ${{ steps.tags.outputs.ocr_image }}
docker push $REGISTRY/egullickson/backend:latest
docker push $REGISTRY/egullickson/frontend:latest
docker push $REGISTRY/egullickson/ocr:latest
# ============================================
# DEPLOY STAGING - Deploy to staging server
# ============================================
deploy-staging:
name: Deploy to Staging
runs-on: stage
needs: build
env:
BACKEND_IMAGE: ${{ needs.build.outputs.backend_image }}
FRONTEND_IMAGE: ${{ needs.build.outputs.frontend_image }}
OCR_IMAGE: ${{ needs.build.outputs.ocr_image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Sync config, scripts, and compose files to deploy path
run: |
rsync -av --delete "$GITHUB_WORKSPACE/config/" "$DEPLOY_PATH/config/"
rsync -av --delete "$GITHUB_WORKSPACE/scripts/" "$DEPLOY_PATH/scripts/"
cp "$GITHUB_WORKSPACE/docker-compose.yml" "$DEPLOY_PATH/"
cp "$GITHUB_WORKSPACE/docker-compose.staging.yml" "$DEPLOY_PATH/"
- name: Generate logging configuration
run: |
cd "$DEPLOY_PATH"
chmod +x scripts/ci/generate-log-config.sh
./scripts/ci/generate-log-config.sh "$LOG_LEVEL"
- 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/app" ./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 }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
- name: Initialize data directories
run: |
cd "$DEPLOY_PATH"
sudo mkdir -p data/backups data/documents data/traefik
sudo chown -R 1001:1001 data/backups data/documents
sudo chmod 755 data/backups data/documents
# Traefik acme.json requires 600 permissions
if [ ! -f data/traefik/acme.json ]; then
sudo touch data/traefik/acme.json
fi
sudo chmod 600 data/traefik/acme.json
- name: Pull new images
run: |
docker pull $BACKEND_IMAGE
docker pull $FRONTEND_IMAGE
docker pull $OCR_IMAGE
- name: Deploy staging stack
run: |
cd "$DEPLOY_PATH"
export BACKEND_IMAGE=$BACKEND_IMAGE
export FRONTEND_IMAGE=$FRONTEND_IMAGE
export OCR_IMAGE=$OCR_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 5
# ============================================
# VERIFY STAGING - Health checks
# ============================================
verify-staging:
name: Verify Staging
runs-on: stage
needs: [build, deploy-staging]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check container status and health
run: |
for service in mvp-frontend-staging mvp-backend-staging mvp-ocr-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
# Wait for Docker healthchecks to complete (services with healthcheck defined)
echo ""
echo "Waiting for Docker healthchecks..."
for service in mvp-frontend-staging mvp-backend-staging mvp-ocr-staging mvp-postgres-staging mvp-redis-staging; do
# Check if service has a healthcheck defined
has_healthcheck=$(docker inspect --format='{{if .Config.Healthcheck}}true{{else}}false{{end}}' $service 2>/dev/null || echo "false")
if [ "$has_healthcheck" = "true" ]; then
# 48 attempts x 5 seconds = 4 minutes max wait (backend with fresh migrations can take ~3 min)
for i in $(seq 1 48); do
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
if [ "$health" = "healthy" ]; then
echo "OK: $service is healthy"
break
fi
# Don't fail immediately on unhealthy - container may still be starting up
# and can recover. Let the timeout handle truly broken containers.
if [ $i -eq 48 ]; then
echo "ERROR: $service health check timed out (status: $health)"
docker logs $service --tail 100 2>/dev/null || true
exit 1
fi
echo "Waiting for $service healthcheck... (attempt $i/48, status: $health)"
sleep 5
done
else
echo "SKIP: $service has no healthcheck defined"
fi
done
- name: Wait for backend health
run: |
for i in $(seq 1 12); 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 12 ]; then
echo "ERROR: Backend health check failed after 12 attempts"
docker logs mvp-backend-staging --tail 100
exit 1
fi
echo "Attempt $i/12: Backend not ready, waiting 5s..."
sleep 5
done
- name: Check external endpoint
run: |
REQUIRED_FEATURES='["admin","auth","onboarding","vehicles","documents","fuel-logs","stations","maintenance","platform","notifications","user-profile","user-preferences","user-export"]'
for i in $(seq 1 12); do
RESPONSE=$(curl -sf https://staging.motovaultpro.com/api/health 2>/dev/null) || {
echo "Attempt $i/12: Connection failed, waiting 5s..."
sleep 5
continue
}
# Check status is "healthy"
STATUS=$(echo "$RESPONSE" | jq -r '.status')
if [ "$STATUS" != "healthy" ]; then
echo "Attempt $i/12: Status is '$STATUS', not 'healthy'. Waiting 5s..."
sleep 5
continue
fi
# Check all required features are present
MISSING=$(echo "$RESPONSE" | jq -r --argjson required "$REQUIRED_FEATURES" '
$required - .features | if length > 0 then . else empty end | @json
')
if [ -n "$MISSING" ]; then
echo "Attempt $i/12: Missing features: $MISSING. Waiting 5s..."
sleep 5
continue
fi
FEATURE_COUNT=$(echo "$RESPONSE" | jq '.features | length')
echo "OK: Staging health check passed - status: healthy, features: $FEATURE_COUNT"
exit 0
done
echo "ERROR: Staging health check failed after 12 attempts"
echo "Last response: $RESPONSE"
exit 1
# ============================================
# NOTIFY - Staging ready for production
# ============================================
notify-staging-ready:
name: Notify Staging Ready
runs-on: stage
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: stage
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 }}