Skip to content

fix(ci): let publish self-restore, bundle package.json for SPA host #7

fix(ci): let publish self-restore, bundle package.json for SPA host

fix(ci): let publish self-restore, bundle package.json for SPA host #7

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"
# 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 WebApplication1.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: 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 the built dist/ along with a minimal package.json so Azure App
# Service (Linux Node) auto-starts it via `npm start`. The start script
# uses pm2's built-in static server with `--spa` so unmatched routes
# fall back to index.html (required for React Router deep links).
#
# Doing it this way removes the need to set a Startup Command in the
# Portal — the zip is self-describing.
- name: Package dist
working-directory: 03-Client
run: |
cp -r dist deploy-frontend
cat > deploy-frontend/package.json <<'JSON'
{
"name": "groundshare-web",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "pm2 serve . --no-daemon --spa"
}
}
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: 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.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