Skip to content

Merge branch 'main' of https://github.com/Ovalvoi/GroundShare #263

Merge branch 'main' of https://github.com/Ovalvoi/GroundShare

Merge branch 'main' of https://github.com/Ovalvoi/GroundShare #263

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 via a small zero-dependency Node server
# (src/03-client/deploy/server.js) that does SPA fallback AND injects security
# headers (CSP report-only, X-Frame-Options, HSTS, ...). The previous
# `pm2 serve --spa` host could not set headers and added a blanket
# Access-Control-Allow-Origin: * to every response.
#
# 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]
permissions:
contents: read
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: src/02-server
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: "8.0.x"
# 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).
#
# We let `publish` do its own restore here. The Ubuntu runner ships
# newer .NET SDKs alongside 8.0, and a prior `dotnet restore` without
# an explicit RID produced an assets file that didn't satisfy
# `publish -r linux-x64 --no-restore`.
- name: Publish (linux-x64, framework-dependent)
run: dotnet publish GroundShareAPI.csproj -c Release -r linux-x64 --self-contained false -o ./publish-linux
# 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: src/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@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
cache-dependency-path: src/03-client/package-lock.json
- name: Install dependencies
working-directory: src/03-client
run: npm ci
- name: Build
working-directory: src/03-client
run: npm run build
env:
# Must be SAME-SITE with the web app (groundshare.app) so the
# httpOnly SameSite=Lax refresh cookie is sent on the boot
# /auth/refresh. api.groundshare.app proxies (Cloudflare) to the
# Azure API. Pointing this at *.azurewebsites.net makes the cookie
# cross-site → it is withheld → users get logged out on every reload.
VITE_API_BASE_URL: https://api.groundshare.app/api
VITE_FIREBASE_API_KEY: ${{ vars.VITE_FIREBASE_API_KEY }}
VITE_FIREBASE_AUTH_DOMAIN: ${{ vars.VITE_FIREBASE_AUTH_DOMAIN }}
VITE_FIREBASE_PROJECT_ID: ${{ vars.VITE_FIREBASE_PROJECT_ID }}
VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ vars.VITE_FIREBASE_MESSAGING_SENDER_ID }}
VITE_FIREBASE_APP_ID: ${{ vars.VITE_FIREBASE_APP_ID }}
VITE_FIREBASE_VAPID_KEY: ${{ vars.VITE_FIREBASE_VAPID_KEY }}
# Zip the built dist/ along with server.cjs and a minimal package.json.
# server.cjs serves dist/ with SPA fallback (required for React Router
# deep links) and injects security headers. It has zero npm dependencies,
# so Oryx has nothing to install and cold starts stay fast.
#
# NOTE: the App Service has an explicit Startup Command
# ("node /home/site/wwwroot/server.cjs") which OVERRIDES the zip's
# `npm start`. If server.cjs is ever renamed/moved, update the Startup
# Command in the Portal too or the site keeps serving the old process
# (this bit us when replacing `pm2 serve` — the portal command silently
# kept pm2 running after a green deploy).
- name: Package dist
working-directory: src/03-client
run: |
cp -r dist deploy-frontend
cp deploy/server.cjs deploy-frontend/server.cjs
cat > deploy-frontend/package.json <<'JSON'
{
"name": "groundshare-web",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.cjs"
}
}
JSON
cd deploy-frontend
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: src/03-client/deploy-frontend.zip
# First-time Oryx build + pm2 warmup on a cold Free-tier App Service
# can take 2-3 minutes. Poll for up to ~4 minutes before failing.
- name: Health check
run: |
sleep 60
for i in $(seq 1 12); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://app-groundshare-web-ekfneqfaahaca8ek.israelcentral-01.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"
exit 1