Skip to content

ci(deploy): host frontend on App Service instead of SWA #6

ci(deploy): host frontend on App Service instead of SWA

ci(deploy): host frontend on App Service instead of SWA #6

Workflow file for this run

# ---------------------------------------------------------------------------
# CD Pipeline — deploy backend + frontend to Azure on push to main
# ---------------------------------------------------------------------------
# Triggered after CI passes on main branch.
#
# Jobs:
# 1. backend — publish .NET 8 app → Azure App Service (zip deploy)
# 2. frontend — build Vite app → Azure App Service (static host w/ SPA fallback)
#
# Why App Service for frontend instead of Static Web Apps?
# The Azure subscription is region-locked (israelcentral / swedencentral / etc.)
# and SWA is not available in those regions. App Service is, so we host the
# built `dist/` on a Linux App Service using the built-in `pm2 serve` command
# with `--spa` so client-side routes fall back to index.html.
#
# Required GitHub Secrets:
# - AZURE_WEBAPP_PUBLISH_PROFILE (backend App Service publish profile XML)
# - AZURE_WEBAPP_FRONTEND_PUBLISH_PROFILE (frontend App Service publish profile XML)
#
# Gating via GitHub Repository Variables (Settings > Secrets and variables >
# Actions > Variables tab):
# - DEPLOY_BACKEND_ENABLED=true enables the backend job
# - DEPLOY_FRONTEND_ENABLED=true enables the frontend job
# If either variable is unset or != "true", that job is skipped so the CD
# run stays green while Azure resources/secrets are still being provisioned.
# ---------------------------------------------------------------------------
name: CD
on:
push:
branches: [main]
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: false # Don't cancel in-flight deploys
jobs:
backend:
name: Deploy Backend
if: vars.DEPLOY_BACKEND_ENABLED == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: 02-Server
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Restore
run: dotnet restore
# Publish targeting linux-x64 so the output matches the App Service
# runtime and does NOT include runtimes/win/* (backslash paths in those
# files have historically corrupted wwwroot on Linux App Service).
- name: Publish (linux-x64, framework-dependent)
run: dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish-linux --no-restore
# Zip on Linux so path separators are forward slashes. Paired with
# WEBSITE_RUN_FROM_PACKAGE=1 on the App Service, the deploy mounts the
# zip read-only and avoids rsync into the locked wwwroot share.
- name: Package
run: |
cd publish-linux
zip -r ../deploy-linux.zip . -x "*.pdb"
- name: Deploy to Azure App Service
uses: azure/webapps-deploy@v3
with:
app-name: app-groundshare-api
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: 02-Server/deploy-linux.zip
- name: Health check
run: |
sleep 30
for i in 1 2 3 4 5; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://app-groundshare-api.azurewebsites.net/api/health || true)
if [ "$STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i: status $STATUS, retrying in 15s..."
sleep 15
done
echo "Health check failed after 5 attempts"
exit 1
frontend:
name: Deploy Frontend
if: vars.DEPLOY_FRONTEND_ENABLED == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: 03-Client/package-lock.json
- name: Install dependencies
working-directory: 03-Client
run: npm ci
- name: Build
working-directory: 03-Client
run: npm run build
env:
VITE_API_BASE_URL: https://app-groundshare-api.azurewebsites.net/api
# Zip only the built dist/ so the App Service wwwroot contains the SPA
# files at its root. The App Service Startup Command serves them via:
# pm2 serve /home/site/wwwroot --no-daemon --spa
# `--spa` makes unmatched routes fall back to index.html so client-side
# routing (React Router) works on deep links and page refreshes.
- name: Package dist
working-directory: 03-Client
run: |
cd dist
zip -r ../deploy-frontend.zip .
- name: Deploy to Azure App Service (frontend)
uses: azure/webapps-deploy@v3
with:
app-name: app-groundshare-web
publish-profile: ${{ secrets.AZURE_WEBAPP_FRONTEND_PUBLISH_PROFILE }}
package: 03-Client/deploy-frontend.zip
- name: Health check
run: |
sleep 30
for i in 1 2 3 4 5; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://app-groundshare-web.azurewebsites.net/ || true)
if [ "$STATUS" = "200" ]; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i: status $STATUS, retrying in 15s..."
sleep 15
done
echo "Health check failed after 5 attempts"
exit 1