|
| 1 | +# --------------------------------------------------------------------------- |
| 2 | +# Build + deploy the Evidence Portal SPA (Angular 19) and API (Spring Boot 3 |
| 3 | +# on Java 17) to the existing Azure App Services in resource group |
| 4 | +# rg-evidence-workshop. |
| 5 | +# |
| 6 | +# This workflow does NOT: |
| 7 | +# * Bootstrap Entra ID app registrations (run scripts/setup-entra-apps.ps1 |
| 8 | +# once locally; the resulting app reg IDs are committed in |
| 9 | +# .entra-apps.json). |
| 10 | +# * Provision or modify Azure infrastructure (run scripts/deploy.ps1 |
| 11 | +# locally for the first deploy; this workflow only updates the running |
| 12 | +# code on the existing App Services). |
| 13 | +# * Reseed the sample evidence PDFs in storage (one-time, done by |
| 14 | +# scripts/deploy.ps1). |
| 15 | +# |
| 16 | +# Required GitHub repository secrets (configured under |
| 17 | +# Settings -> Secrets and variables -> Actions): |
| 18 | +# |
| 19 | +# AZURE_CLIENT_ID App registration with Federated Identity Credential |
| 20 | +# for this repository (subject: |
| 21 | +# repo:devopsabcs-engineering/msal-java:ref:refs/heads/main). |
| 22 | +# The SP must have Website Contributor on |
| 23 | +# rg-evidence-workshop. |
| 24 | +# AZURE_TENANT_ID Entra tenant id. |
| 25 | +# AZURE_SUBSCRIPTION_ID Subscription id hosting rg-evidence-workshop. |
| 26 | +# --------------------------------------------------------------------------- |
| 27 | +name: Deploy SPA and API |
| 28 | + |
| 29 | +on: |
| 30 | + push: |
| 31 | + branches: [main] |
| 32 | + paths: |
| 33 | + - 'sample-app/**' |
| 34 | + - '.github/workflows/deploy.yml' |
| 35 | + workflow_dispatch: |
| 36 | + |
| 37 | +# Required for OIDC federated login to Azure (no client secret needed). |
| 38 | +permissions: |
| 39 | + id-token: write |
| 40 | + contents: read |
| 41 | + |
| 42 | +concurrency: |
| 43 | + group: deploy-workshop |
| 44 | + cancel-in-progress: false |
| 45 | + |
| 46 | +env: |
| 47 | + RESOURCE_GROUP: rg-evidence-workshop |
| 48 | + API_APP_NAME: app-evidence-api-workshop |
| 49 | + SPA_APP_NAME: app-evidence-spa-workshop |
| 50 | + |
| 51 | +jobs: |
| 52 | + # ------------------------------------------------------------------------- |
| 53 | + # Build SPA + API in parallel-friendly steps and publish artefacts. |
| 54 | + # ------------------------------------------------------------------------- |
| 55 | + build: |
| 56 | + name: Build artefacts |
| 57 | + runs-on: ubuntu-latest |
| 58 | + steps: |
| 59 | + - name: Checkout |
| 60 | + uses: actions/checkout@v4 |
| 61 | + |
| 62 | + - name: Set up Java 17 (Temurin) |
| 63 | + uses: actions/setup-java@v4 |
| 64 | + with: |
| 65 | + distribution: temurin |
| 66 | + java-version: '17' |
| 67 | + cache: maven |
| 68 | + |
| 69 | + - name: Set up Node 20 |
| 70 | + uses: actions/setup-node@v4 |
| 71 | + with: |
| 72 | + node-version: '20' |
| 73 | + cache: npm |
| 74 | + cache-dependency-path: sample-app/spa/package-lock.json |
| 75 | + |
| 76 | + # ---- API ---- |
| 77 | + - name: Build Spring Boot API |
| 78 | + working-directory: sample-app/api |
| 79 | + run: mvn -B -ntp clean package -DskipTests |
| 80 | + |
| 81 | + - name: Capture API jar name |
| 82 | + id: api |
| 83 | + working-directory: sample-app/api/target |
| 84 | + run: | |
| 85 | + jar=$(ls evidence-api-*.jar | grep -v -E '\-(sources|javadoc)\.jar$' | head -1) |
| 86 | + echo "Found jar: $jar" |
| 87 | + echo "jar=$jar" >> "$GITHUB_OUTPUT" |
| 88 | +
|
| 89 | + - name: Upload API artefact |
| 90 | + uses: actions/upload-artifact@v4 |
| 91 | + with: |
| 92 | + name: api-jar |
| 93 | + path: sample-app/api/target/${{ steps.api.outputs.jar }} |
| 94 | + if-no-files-found: error |
| 95 | + retention-days: 7 |
| 96 | + |
| 97 | + # ---- SPA ---- |
| 98 | + - name: Fetch Ontario Design System assets |
| 99 | + shell: pwsh |
| 100 | + run: ./scripts/fetch-ontario-design-system.ps1 |
| 101 | + |
| 102 | + - name: Install SPA dependencies |
| 103 | + working-directory: sample-app/spa |
| 104 | + run: npm ci |
| 105 | + |
| 106 | + - name: Build Angular SPA (production) |
| 107 | + working-directory: sample-app/spa |
| 108 | + run: npx ng build --configuration production |
| 109 | + |
| 110 | + - name: Upload SPA artefact |
| 111 | + uses: actions/upload-artifact@v4 |
| 112 | + with: |
| 113 | + name: spa-dist |
| 114 | + path: sample-app/spa/dist/evidence-portal/browser/ |
| 115 | + if-no-files-found: error |
| 116 | + retention-days: 7 |
| 117 | + |
| 118 | + # ------------------------------------------------------------------------- |
| 119 | + # Deploy to App Service via OIDC. Runs after build succeeds. |
| 120 | + # ------------------------------------------------------------------------- |
| 121 | + deploy: |
| 122 | + name: Deploy to App Service |
| 123 | + needs: build |
| 124 | + runs-on: ubuntu-latest |
| 125 | + environment: |
| 126 | + name: workshop |
| 127 | + url: https://${{ env.SPA_APP_NAME }}.azurewebsites.net |
| 128 | + steps: |
| 129 | + - name: Download API artefact |
| 130 | + uses: actions/download-artifact@v4 |
| 131 | + with: |
| 132 | + name: api-jar |
| 133 | + path: ./api |
| 134 | + |
| 135 | + - name: Download SPA artefact |
| 136 | + uses: actions/download-artifact@v4 |
| 137 | + with: |
| 138 | + name: spa-dist |
| 139 | + path: ./spa-dist |
| 140 | + |
| 141 | + - name: Azure login (OIDC) |
| 142 | + uses: azure/login@v2 |
| 143 | + with: |
| 144 | + client-id: ${{ secrets.AZURE_CLIENT_ID }} |
| 145 | + tenant-id: ${{ secrets.AZURE_TENANT_ID }} |
| 146 | + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} |
| 147 | + |
| 148 | + - name: Resolve API jar path |
| 149 | + id: jar |
| 150 | + run: | |
| 151 | + jar=$(ls ./api/evidence-api-*.jar | head -1) |
| 152 | + echo "Found: $jar" |
| 153 | + echo "path=$jar" >> "$GITHUB_OUTPUT" |
| 154 | +
|
| 155 | + - name: Deploy API jar to App Service |
| 156 | + uses: azure/webapps-deploy@v3 |
| 157 | + with: |
| 158 | + app-name: ${{ env.API_APP_NAME }} |
| 159 | + package: ${{ steps.jar.outputs.path }} |
| 160 | + type: jar |
| 161 | + |
| 162 | + - name: Package SPA into zip |
| 163 | + run: | |
| 164 | + cd ./spa-dist |
| 165 | + zip -qr ../spa.zip . |
| 166 | + ls -lh ../spa.zip |
| 167 | +
|
| 168 | + - name: Deploy SPA zip to App Service |
| 169 | + uses: azure/webapps-deploy@v3 |
| 170 | + with: |
| 171 | + app-name: ${{ env.SPA_APP_NAME }} |
| 172 | + package: ./spa.zip |
| 173 | + type: zip |
| 174 | + |
| 175 | + - name: Smoke test |
| 176 | + run: | |
| 177 | + set -euo pipefail |
| 178 | + echo "Waiting 90s for warmup..." |
| 179 | + sleep 90 |
| 180 | +
|
| 181 | + spa_url="https://${SPA_APP_NAME}.azurewebsites.net" |
| 182 | + api_url="https://${API_APP_NAME}.azurewebsites.net/api/cases" |
| 183 | +
|
| 184 | + spa_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$spa_url" || echo "000") |
| 185 | + echo "SPA $spa_url => $spa_status" |
| 186 | + if [ "$spa_status" != "200" ]; then |
| 187 | + echo "::warning::SPA did not return 200 (got $spa_status); may still be warming up." |
| 188 | + fi |
| 189 | +
|
| 190 | + api_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$api_url" || echo "000") |
| 191 | + echo "API $api_url => $api_status" |
| 192 | + # Anonymous /api/cases must return 401 (JWT enforced). 200 means auth |
| 193 | + # is broken; 5xx means the app failed to start or the storage path |
| 194 | + # regression is back. |
| 195 | + if [ "$api_status" != "401" ]; then |
| 196 | + echo "::error::API returned $api_status; expected 401 (JWT validation enforced)." |
| 197 | + exit 1 |
| 198 | + fi |
0 commit comments