mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
be0e37008a
* Action pins: exact release tag in every version comment (D-331) setup-node v7.1.0 and upload-artifact v7.0.2 moved the floating v7 tag, so the "# v7" comments on our still-correct pins (v7.0.0, v7.0.1) stopped naming the pinned commit and zizmor's ref-version-mismatch raised 11 code-scanning alerts on main. The other five major-only comments (checkout, setup-dotnet, cache, attest-build-provenance, functions-action) were one upstream release away from the same alert. All 38 major-only comments now carry the tag their unchanged SHA resolves to (checkout v7.0.1, setup-dotnet v6.0.0, cache v6.1.0, setup-node v7.0.0, upload-artifact v7.0.1, attest-build-provenance v4.2.2, functions-action v1.5.7); no SHA moved, Dependabot keeps advancing SHA and comment together. verify-pins now accepts only a vX.Y.Z comment and names the floating-major case in its own error. Verified: zizmor online (GH_TOKEN) reproduced the 11 findings before and reports 0 ref-version-mismatch after, --min-severity low exits 0; the verify-pins script passes all 51 uses lines and rejects a major-only comment, a missing comment and a tag reference in a scratch workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bump pointer: internal (D-331 action pin comments) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
11 KiB
YAML
213 lines
11 KiB
YAML
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
lfs: false
|
|
fetch-depth: 0 # Full history needed for git-based sitemap lastmod dates
|
|
|
|
# Only the version manifest at the very end needs Azure; logging in here rather than
|
|
# next to that step keeps a credential problem from surfacing after a live deploy.
|
|
- name: Login to Azure (OIDC)
|
|
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
|
|
with:
|
|
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
|
|
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
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
|
|
|
|
# Star counter for the GitHub star link (utils/githubStars.ts). Baked into the static HTML
|
|
# so visitors' browsers never call GitHub; stale until the next web deploy. The token stays
|
|
# in this step, never in the build. Fail-soft: without a number the link has no counter.
|
|
- name: Read GitHub star count (fail-soft)
|
|
id: stars
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
count=$(gh api "repos/$REPO" --jq .stargazers_count 2>/dev/null || true)
|
|
if [[ "$count" =~ ^[0-9]+$ ]]; then
|
|
echo "count=$count" >> "$GITHUB_OUTPUT"
|
|
echo "GitHub stars: $count"
|
|
else
|
|
echo "::warning::GitHub star count unavailable - the star link renders without a counter"
|
|
fi
|
|
|
|
# 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 }}
|
|
# Browsers load the live stats through it, and the build reads it once to bake the
|
|
# numbers of this moment into the landing page's static HTML (lib/platformStats.ts).
|
|
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
|
|
# Star counter from the step above; empty when the lookup failed.
|
|
NEXT_PUBLIC_GITHUB_STARS: ${{ steps.stars.outputs.count }}
|
|
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; }
|
|
# Fail-soft like the star count: browsers still load the stats live; only crawlers and
|
|
# LLM fetchers, which run no JS, see no numbers until the next deploy.
|
|
grep -q 'data-stats-snapshot=' out/index.html || echo "::warning::Platform stats were not baked into the landing page (reason in the build log) - crawlers and LLM fetchers see no numbers until the next deploy"
|
|
|
|
# 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-<sha>.tar.gz --repo okieselbach/AutopilotMonitor
|
|
- name: Attest build provenance (portal export)
|
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
with:
|
|
subject-path: ${{ steps.package.outputs.archive }}
|
|
|
|
- name: Upload build artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
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
|
|
|
|
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
|
|
$url = "${{ env.VERSIONS_CONTAINER }}/web.json"
|
|
# no-cache: rotating pointer, must never be served stale by any fronting cache.
|
|
Invoke-RestMethod -Uri $url -Method Put -Headers ($authHeaders + @{ '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
|