name: Deploy Web (Static Web App) # Build here, upload with the pinned SWA CLI. No third-party action sees the deploy token # (the CLI is Microsoft-published npm code at an exact version), the build runs on the same # path ci.yml tests (setup-node 22 + npm ci + next build) instead of Oryx guessing inside the # action's container, and the exported site is attested like the agent ZIP and the MSI. # # PR preview environments were dropped with the action: their per-PR URLs are never # registered as redirect URIs, so nobody could log in on them anyway. on: push: branches: - main paths: - 'src/Web/autopilot-monitor-web/**' - '.github/workflows/deploy-web.yml' workflow_dispatch: permissions: contents: read id-token: write # keyless Sigstore signing (artifact attestation) attestations: write # store the provenance attestation env: PORTAL_VERSION_URL: https://portal.autopilotmonitor.com/version.json VERSIONS_CONTAINER: https://autopilotmonitoreu.blob.core.windows.net/versions WEB_DIR: src/Web/autopilot-monitor-web # Exact version: `npx --package` resolves from the registry at run time, so the pin is the # only thing that keeps the deploy step's code fixed. Bump deliberately. SWA_CLI_VERSION: 2.0.10 jobs: build_and_deploy_job: runs-on: ubuntu-latest name: Build and Deploy Job steps: - uses: actions/checkout@v7 with: lfs: false fetch-depth: 0 # Full history needed for git-based sitemap lastmod dates # The customer docs repo is the single source of the "What's new" panel: the prebuild # step (scripts/whats-new/build-whats-new.js) turns changelog/*.md into # public/whats-new.json and dates every bullet via git blame — hence the full history. # Public repo, no token. Editing the docs does NOT update the live panel until this # workflow runs again (push under src/Web/** or a manual dispatch); versions/web.json # records which docs commit is live. - name: Check out documentation bundle uses: actions/checkout@v7 with: repository: autopilotmonitor/autopilotmonitor-docs path: whats-new-docs fetch-depth: 0 - name: Resolve docs commit id: docs run: echo "sha=$(git -C whats-new-docs rev-parse HEAD)" >> "$GITHUB_OUTPUT" - uses: actions/setup-node@v7 with: node-version: '22' cache: npm cache-dependency-path: ${{ env.WEB_DIR }}/package-lock.json - name: Install dependencies working-directory: ${{ env.WEB_DIR }} run: npm ci # next.config output:'export' -- `next build` writes the static site to out/ # (prebuild: sync-error-codes, generate-lastmod, generate-version, whats-new). - name: Build working-directory: ${{ env.WEB_DIR }} env: NEXT_PUBLIC_ENTRA_CLIENT_ID: ${{ vars.NEXT_PUBLIC_ENTRA_CLIENT_ID }} # Dual app-reg window: previous app registration (unset = single-app behaviour) # and the default app for browsers without a stored selection (legacy|primary). NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID: ${{ vars.NEXT_PUBLIC_ENTRA_LEGACY_CLIENT_ID }} NEXT_PUBLIC_ENTRA_DEFAULT_APP: ${{ vars.NEXT_PUBLIC_ENTRA_DEFAULT_APP }} NEXT_PUBLIC_ENTRA_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_ENTRA_REDIRECT_URI }} NEXT_PUBLIC_ENTRA_POST_LOGOUT_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_ENTRA_POST_LOGOUT_REDIRECT_URI }} NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }} NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL: ${{ vars.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL }} NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING: ${{ secrets.NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING }} # Build stamp for public/version.json (scripts/generate-version.js in prebuild); # the verify step below polls the deployed file for exactly this commit. GITHUB_SHA: ${{ github.sha }} # Docs checkout for public/whats-new.json; the prebuild fails in CI without it. WHATS_NEW_DOCS_DIR: ${{ github.workspace }}/whats-new-docs run: | npm run build test -f out/version.json || { echo "out/version.json missing - export did not run"; exit 1; } test -f out/whats-new.json || { echo "out/whats-new.json missing - whats-new prebuild did not run"; exit 1; } # One archive of the exact bytes that get uploaded, so the attestation has a single subject. - name: Package export id: package run: | archive="portal-${GITHUB_SHA::7}.tar.gz" tar -C "$WEB_DIR/out" -czf "$archive" . echo "archive=$archive" >> "$GITHUB_OUTPUT" sha256sum "$archive" # Sigstore keyless provenance: binds the export digest to this workflow run (repo, ref, # commit, workflow file) in a public transparency log. # Verify: gh attestation verify portal-.tar.gz --repo okieselbach/AutopilotMonitor - name: Attest build provenance (portal export) uses: actions/attest-build-provenance@v4 with: subject-path: ${{ steps.package.outputs.archive }} - name: Upload build artifact uses: actions/upload-artifact@v7 with: name: portal-${{ github.sha }} path: ${{ steps.package.outputs.archive }} # Upload only -- out/ is already built. --swa-config-location points at the app root, # where staticwebapp.config.json lives (next export does not copy it into out/). - name: Deploy to Static Web App env: SWA_CLI_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} run: | npx --yes --package "@azure/static-web-apps-cli@${SWA_CLI_VERSION}" swa deploy \ --app-location "$WEB_DIR" \ --output-location out \ --swa-config-location "$WEB_DIR" \ --env production \ --no-use-keychain # The CLI reporting success proves the upload, not that the production host serves the # new build -- verify against the live /version.json (Cache-Control: no-store via # staticwebapp.config.json) before publishing the version manifest. - name: Verify deployed portal build is live shell: pwsh run: | $expectedCommit = '${{ github.sha }}'.Substring(0, 7) $deadline = [DateTime]::UtcNow.AddMinutes(5) $attempt = 0 while ([DateTime]::UtcNow -lt $deadline) { $attempt++ try { $v = Invoke-RestMethod -Uri '${{ env.PORTAL_VERSION_URL }}' -Method Get -TimeoutSec 30 -Headers @{ 'Cache-Control' = 'no-cache' } if ($v.commit -eq $expectedCommit) { Write-Host "Live after $attempt attempt(s): web @ $($v.commit), built $($v.buildUtc)" exit 0 } Write-Host "Attempt ${attempt}: serving $($v.commit) -- waiting for $expectedCommit" } catch { Write-Host "Attempt ${attempt}: /version.json not answering yet -- $($_.Exception.Message)" } Start-Sleep -Seconds 15 } throw "Portal did not serve commit $expectedCommit within 5 minutes -- deployment not verified, version manifest NOT published" # Cross-component deploy record, written last and only on a verified deploy -- # same contract as versions/backend.json and versions/mcp.json. docsCommit says which # docs revision the live What's new panel was generated from (as for the MCP corpus). - name: Publish version manifest shell: pwsh run: | $manifest = [ordered]@{ component = 'web' commit = '${{ github.sha }}'.Substring(0, 7) deployedUtc = [DateTime]::UtcNow.ToString('o') runId = '${{ github.run_id }}' docsCommit = '${{ steps.docs.outputs.sha }}'.Substring(0, 7) } | ConvertTo-Json $sas = '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'.TrimStart('?') $url = "${{ env.VERSIONS_CONTAINER }}/web.json?$sas" # no-cache: rotating pointer, must never be served stale by any fronting cache. Invoke-RestMethod -Uri $url -Method Put -Headers @{ 'x-ms-blob-type' = 'BlockBlob'; 'x-ms-blob-cache-control' = 'no-cache' } -Body $manifest -ContentType 'application/json' | Out-Null Write-Host "Published versions/web.json" Write-Host $manifest