Fix homepage layout: replace Insiders-only grid cards with tables, ad… #60
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # EAFW FloodWatch - Build & Deploy to Staging | |
| # Triggers on push to eafw branch | |
| # Only builds images when their source files change | |
| name: Build & Deploy to Staging | |
| on: | |
| push: | |
| branches: | |
| - eafw | |
| workflow_dispatch: | |
| inputs: | |
| force_build: | |
| description: 'Force rebuild all images' | |
| type: boolean | |
| default: false | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }} | |
| jobs: | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| api: ${{ steps.changes.outputs.api }} | |
| cms: ${{ steps.changes.outputs.cms }} | |
| mapviewer: ${{ steps.changes.outputs.mapviewer }} | |
| mapserver: ${{ steps.changes.outputs.mapserver }} | |
| mapcache: ${{ steps.changes.outputs.mapcache }} | |
| jobs: ${{ steps.changes.outputs.jobs }} | |
| deploy_only: ${{ steps.changes.outputs.deploy_only }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| - name: Check for changes | |
| id: changes | |
| run: | | |
| # Get changed files | |
| CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") | |
| echo "Changed files: $CHANGED" | |
| # Check each component | |
| if echo "$CHANGED" | grep -qE "^eafw_api/"; then | |
| echo "api=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "api=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED" | grep -qE "^eafw_cms/|^eafw_docker/cms/"; then | |
| echo "cms=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "cms=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED" | grep -qE "^eafw_mapviewer/|^eafw_docker/mapviewer/"; then | |
| echo "mapviewer=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "mapviewer=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED" | grep -qE "^eafw_docker/mapserver/"; then | |
| echo "mapserver=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "mapserver=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED" | grep -qE "^eafw_docker/mapcache/"; then | |
| echo "mapcache=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "mapcache=false" >> $GITHUB_OUTPUT | |
| fi | |
| if echo "$CHANGED" | grep -qE "^eafw_jobs/|^eafw_docker/jobs/"; then | |
| echo "jobs=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "jobs=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Check if only config/deploy files changed (no build needed) | |
| if echo "$CHANGED" | grep -qvE "^eafw_api/|^eafw_cms/|^eafw_docker/cms/|^eafw_mapviewer/|^eafw_docker/mapviewer/|^eafw_docker/mapserver/|^eafw_docker/mapcache/|^eafw_jobs/|^eafw_docker/jobs/"; then | |
| echo "deploy_only=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "deploy_only=false" >> $GITHUB_OUTPUT | |
| fi | |
| build-api: | |
| name: Build API | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.api == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push API | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./eafw_api | |
| file: eafw_api/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-api:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-api:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-cms: | |
| name: Build CMS | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.cms == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push CMS | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: eafw_docker/cms/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-cms:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-cms:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-mapviewer: | |
| name: Build Mapviewer | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.mapviewer == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push Mapviewer | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: eafw_docker/mapviewer/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapviewer:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapviewer:${{ github.sha }} | |
| build-args: | | |
| CMS_API=/api | |
| BASE_PATH=/ | |
| ASSET_PREFIX= | |
| ADMIN_BOUNDARY_API=/api/v1/boundaries/admin-boundaries/ | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-mapserver: | |
| name: Build Mapserver | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.mapserver == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push Mapserver | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: eafw_docker/mapserver/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapserver:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapserver:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-mapcache: | |
| name: Build Mapcache | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.mapcache == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push Mapcache | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: eafw_docker/mapcache/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapcache:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-mapcache:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-jobs: | |
| name: Build Jobs | |
| runs-on: ubuntu-latest | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.jobs == 'true' || github.event.inputs.force_build == 'true' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push Jobs | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: eafw_docker/jobs/Dockerfile | |
| push: true | |
| tags: | | |
| ${{ env.IMAGE_PREFIX }}/eafw-jobs:latest | |
| ${{ env.IMAGE_PREFIX }}/eafw-jobs:${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| deploy: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes, build-api, build-cms, build-mapviewer, build-mapserver, build-mapcache, build-jobs] | |
| if: always() && !cancelled() | |
| steps: | |
| - name: Deploy to staging server | |
| uses: appleboy/ssh-action@v1.0.3 | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_PAT }} | |
| DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }} | |
| DJANGO_SECRET_KEY: ${{ secrets.STAGING_DJANGO_SECRET_KEY }} | |
| SFTP_PASSWORD: ${{ secrets.STAGING_SFTP_PASSWORD }} | |
| ENSEMBLE_FTP_PASSWORD: ${{ secrets.STAGING_ENSEMBLE_FTP_PASSWORD }} | |
| with: | |
| host: ${{ secrets.STAGING_HOST }} | |
| username: ${{ secrets.STAGING_USER }} | |
| key: ${{ secrets.STAGING_SSH_KEY }} | |
| port: 22 | |
| command_timeout: 60m | |
| envs: GH_TOKEN,DB_PASSWORD,DJANGO_SECRET_KEY,SFTP_PASSWORD,ENSEMBLE_FTP_PASSWORD | |
| script: | | |
| set -e | |
| cd ~ | |
| # Login to GitHub Container Registry | |
| echo ${GH_TOKEN} | docker login ghcr.io -u icpac-igad --password-stdin | |
| # Safe repo update | |
| if [ -d "eafw/.git" ]; then | |
| echo "Updating existing repo..." | |
| cd eafw | |
| git fetch origin | |
| git reset --hard origin/eafw | |
| elif [ -d "eafw" ]; then | |
| echo "Directory eafw/ exists but is not a git repo. Renaming and cloning fresh..." | |
| mv eafw eafw_old_$(date +%Y%m%d%H%M%S) | |
| git clone -b eafw https://${GH_TOKEN}@github.com/icpac-igad/flood_watch_system.git eafw | |
| cd eafw | |
| else | |
| echo "Cloning repo..." | |
| git clone -b eafw https://${GH_TOKEN}@github.com/icpac-igad/flood_watch_system.git eafw | |
| cd eafw | |
| fi | |
| echo "Current directory: $(pwd)" | |
| # Smart .env — create from template only on first deploy | |
| if [ ! -f .env ]; then | |
| cp staging.env.example .env | |
| echo "FIRST DEPLOY: created .env from template" | |
| fi | |
| # Inject secrets from GitHub Secrets (always update these) | |
| sed -i "s|^CMS_DB_PASSWORD=.*|CMS_DB_PASSWORD=${DB_PASSWORD}|" .env | |
| sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${DJANGO_SECRET_KEY}|" .env | |
| sed -i "s|^SFTP_PASSWORD=.*|SFTP_PASSWORD=${SFTP_PASSWORD}|" .env | |
| sed -i "s|^FLOODPROOFS_SFTP_PASSWORD=.*|FLOODPROOFS_SFTP_PASSWORD=${SFTP_PASSWORD}|" .env | |
| sed -i "s|^ENSEMBLE_FTP_PASSWORD=.*|ENSEMBLE_FTP_PASSWORD=${ENSEMBLE_FTP_PASSWORD}|" .env | |
| # Backup database before deploy (skip if DB is unhealthy/restarting) | |
| DB_STATUS=$(docker inspect --format='{{.State.Status}}' eafw-pgdb 2>/dev/null || echo "none") | |
| DB_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' eafw-pgdb 2>/dev/null || echo "none") | |
| if [ "$DB_STATUS" = "running" ] && [ "$DB_HEALTH" != "unhealthy" ]; then | |
| echo "Backing up database..." | |
| mkdir -p ~/eafw-backups | |
| BACKUP="$HOME/eafw-backups/db_$(date +%Y%m%d_%H%M%S).dump" | |
| docker exec eafw-pgdb pg_dump -U eafw_user -Fc eafw_db > "$BACKUP" || echo "WARN: backup failed, continuing deploy" | |
| if [ -f "$BACKUP" ] && [ -s "$BACKUP" ]; then | |
| echo "Backup saved: $BACKUP ($(du -h "$BACKUP" | cut -f1))" | |
| fi | |
| # Keep last 5 backups | |
| ls -t ~/eafw-backups/db_*.dump 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null | |
| else | |
| echo "DB not healthy ($DB_STATUS/$DB_HEALTH), skipping backup" | |
| fi | |
| # If DB is crash-looping, reset corrupt pgdata volume | |
| # Init scripts only run on empty data dir — partial init = permanent crash loop | |
| if [ "$DB_STATUS" = "restarting" ] || [ "$DB_STATUS" = "exited" ]; then | |
| echo "DB is in failed state ($DB_STATUS), resetting pgdata volume..." | |
| docker compose -f docker-compose.staging.yml --env-file .env down 2>/dev/null || true | |
| docker volume rm eafw_pgdata 2>/dev/null || true | |
| echo "pgdata volume removed. Fresh init will run on next start." | |
| fi | |
| # Pull latest images | |
| docker compose -f docker-compose.staging.yml --env-file .env pull --ignore-pull-failures | |
| # Deploy services (|| true: don't let set -e kill script during init) | |
| docker compose -f docker-compose.staging.yml --env-file .env up -d || true | |
| # Wait for DB to be ready (init loads 60MB+ on first deploy) | |
| echo "Waiting for database to be ready..." | |
| DB_READY=0 | |
| for i in $(seq 1 60); do | |
| if docker exec eafw-pgdb pg_isready -U eafw_user 2>/dev/null; then | |
| DB_READY=1 | |
| echo "DB ready after $((i*5))s" | |
| break | |
| fi | |
| [ $((i % 10)) -eq 0 ] && echo " Still waiting ($i/60)..." | |
| sleep 5 | |
| done | |
| if [ "$DB_READY" -eq 0 ]; then | |
| echo "=== DB LOGS ===" | |
| docker logs eafw-pgdb 2>&1 | tail -30 | |
| echo "FATAL: DB did not start" | |
| exit 1 | |
| fi | |
| # Run DB migrations (add country_code if missing, populate via ST_Within) | |
| echo "Running DB migrations..." | |
| docker exec eafw-pgdb psql -U eafw_user -d eafw_db -c " | |
| ALTER TABLE gha.multimodal_control_points ADD COLUMN IF NOT EXISTS country_code VARCHAR(2); | |
| " 2>/dev/null || true | |
| # Populate country_code if any rows are NULL | |
| docker exec eafw-pgdb psql -U eafw_user -d eafw_db -c " | |
| UPDATE gha.multimodal_control_points cp | |
| SET country_code = CASE | |
| WHEN LOWER(a0.country) = 'ethiopia' THEN 'ET' | |
| WHEN LOWER(a0.country) = 'kenya' THEN 'KE' | |
| WHEN LOWER(a0.country) = 'uganda' THEN 'UG' | |
| WHEN LOWER(a0.country) = 'sudan' THEN 'SD' | |
| WHEN LOWER(a0.country) = 'south sudan' THEN 'SS' | |
| WHEN LOWER(a0.country) IN ('tanzania','zanzibar') THEN 'TZ' | |
| WHEN LOWER(a0.country) = 'rwanda' THEN 'RW' | |
| WHEN LOWER(a0.country) = 'burundi' THEN 'BI' | |
| WHEN LOWER(a0.country) = 'somalia' THEN 'SO' | |
| WHEN LOWER(a0.country) = 'djibouti' THEN 'DJ' | |
| WHEN LOWER(a0.country) = 'eritrea' THEN 'ER' | |
| ELSE 'UN' | |
| END | |
| FROM gha.admin0 a0 | |
| WHERE ST_Within(cp.geom, a0.geom) AND cp.country_code IS NULL; | |
| " 2>/dev/null || true | |
| # Wait for CMS to be ready before running Django migrations | |
| echo "Waiting for CMS to be ready..." | |
| CMS_READY=0 | |
| for i in $(seq 1 30); do | |
| if docker exec eafw-cms curl -sf http://localhost:8000/ >/dev/null 2>&1; then | |
| CMS_READY=1 | |
| echo "CMS ready after $((i*5))s" | |
| break | |
| fi | |
| [ $((i % 6)) -eq 0 ] && echo " Still waiting for CMS ($i/30)..." | |
| sleep 5 | |
| done | |
| # Run Django migrations explicitly (entrypoint runs them too, but this ensures | |
| # they complete and we see the output in CI logs) | |
| if [ "$CMS_READY" -eq 1 ]; then | |
| echo "Running Django migrations..." | |
| docker exec eafw-cms /opt/venv/bin/python manage.py migrate --noinput 2>&1 || echo "WARN: Django migrate returned non-zero" | |
| echo "Collecting static files..." | |
| docker exec eafw-cms /opt/venv/bin/python manage.py collectstatic --clear --no-input 2>&1 || echo "WARN: collectstatic returned non-zero" | |
| else | |
| echo "WARN: CMS not ready, skipping explicit Django migrations (entrypoint will handle them)" | |
| fi | |
| # Force-restart nginx to pick up updated config (bind-mounted, not baked into image) | |
| echo "Reloading nginx config..." | |
| docker exec eafw-nginx nginx -s reload 2>&1 || docker restart eafw-nginx 2>&1 || echo "WARN: nginx reload/restart failed" | |
| # Cleanup dangling images | |
| docker image prune -f | |
| # Health check | |
| echo "Waiting 30s for remaining services..." | |
| sleep 30 | |
| FAILED=0 | |
| for svc in eafw-pgdb eafw-pgbouncer eafw-cms eafw-api eafw-mapviewer eafw-nginx; do | |
| STATUS=$(docker inspect --format='{{.State.Status}}' $svc 2>/dev/null) | |
| if [ "$STATUS" != "running" ]; then | |
| echo "WARN: $svc is $STATUS" | |
| docker logs $svc 2>&1 | tail -5 | |
| FAILED=1 | |
| fi | |
| done | |
| docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | |
| [ $FAILED -eq 1 ] && echo "SOME SERVICES NOT HEALTHY" && exit 1 | |
| echo "Deploy successful!" |