Files
motovaultpro/.gitea/workflows/staging.yaml
Eric Gullickson 864a6b1e86
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 26s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
Deploy to Staging / Verify Staging (pull_request) Successful in 37s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
fix: sync docker-compose files to staging server during deploy (refs #55)
The staging workflow was not copying docker-compose.yml to the server,
causing configuration changes (like Stripe secrets) to not take effect.

Added rsync step to sync config, scripts, and compose files before
deployment, matching the production workflow behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 19:35:18 -06:00

306 lines
12 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 \
--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: 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: 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: 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
- 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 }}