All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All summary cards now use primary.main for consistent branding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
296 lines
11 KiB
YAML
296 lines
11 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"
|
|
|
|
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 }}
|
|
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: stage
|
|
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/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 }}
|
|
|
|
- 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
|
|
|
|
- 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: 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-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-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
|
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
|
health=$(docker inspect --format='{{.State.Health.Status}}' $service 2>/dev/null || echo "unknown")
|
|
if [ "$health" = "healthy" ]; then
|
|
echo "OK: $service is healthy"
|
|
break
|
|
elif [ "$health" = "unhealthy" ]; then
|
|
echo "ERROR: $service is unhealthy"
|
|
docker logs $service --tail 50 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
if [ $i -eq 10 ]; then
|
|
echo "ERROR: $service health check timed out (status: $health)"
|
|
docker logs $service --tail 50 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo "Waiting for $service healthcheck... (attempt $i/10, status: $health)"
|
|
sleep 5
|
|
done
|
|
else
|
|
echo "SKIP: $service has no healthcheck defined"
|
|
fi
|
|
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: |
|
|
REQUIRED_FEATURES='["admin","auth","onboarding","vehicles","documents","fuel-logs","stations","maintenance","platform","notifications","user-profile","user-preferences","user-export"]'
|
|
|
|
for i in 1 2 3 4 5 6; do
|
|
RESPONSE=$(curl -sf https://staging.motovaultpro.com/api/health 2>/dev/null) || {
|
|
echo "Attempt $i/6: Connection failed, waiting 10s..."
|
|
sleep 10
|
|
continue
|
|
}
|
|
|
|
# Check status is "healthy"
|
|
STATUS=$(echo "$RESPONSE" | jq -r '.status')
|
|
if [ "$STATUS" != "healthy" ]; then
|
|
echo "Attempt $i/6: Status is '$STATUS', not 'healthy'. Waiting 10s..."
|
|
sleep 10
|
|
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/6: Missing features: $MISSING. Waiting 10s..."
|
|
sleep 10
|
|
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 6 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 }}
|