mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
f5efadfd07
A tag is a mutable pointer owned by the action's publisher. These jobs are not ordinary CI: they hold the code-signing Key Vault token (D-223), the federated storage credentials (D-222), and combine-rules.yml holds a contents:write token that pushes to main. Whoever could repoint v7 would run code with all of that. azure/login and Azure/functions-action were already pinned; the actions/* and github/codeql-action references were not, on the usual argument that GitHub owns them and shares the runner's trust boundary. That argument does not survive the fact that they run in the same jobs as the signing identity. All seven are now pinned by SHA with the version in a trailing comment -- the comment is what keeps the line reviewable, and Dependabot rewrites both together. action-pins.yml is the guard: pins nobody enforces rot, and no review reliably catches one @v4 added to a 200-line workflow. It lives in its own workflow because ci.yml deliberately ignores changes to the other workflow files, so a job placed there would stay silent for exactly the commits it must inspect. Like CI it reports rather than blocks -- main is unprotected; making it binding would mean adding it as a required status check, which is a branch-protection decision. Verified both directions: 49 references pass against the repo, and a fixture with @v7, @main and a bare SHA fails with three annotations while the local reusable workflow refs are skipped. Two floating tool versions closed while here. wix went from the range 5.* to 5.0.2 -- it writes the bytes of the MSI customers install, where AzureSignTool only signs them. ajv was the worse one: unpinned, and npm runs lifecycle scripts at install, so a hijacked release would have executed with the push token before a single rule was validated; now exact versions with --ignore-scripts. Line endings: sed -i rewrote the workflow files as LF. Git normalises on commit, so the content diff is unaffected and indexnow-notify.yml carries no change at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
187 lines
9.0 KiB
YAML
187 lines
9.0 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
|
|
with:
|
|
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@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3
|
|
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
|
|
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@820762786026740c76f36085b0efc47a31fe5020 # 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-<sha>.tar.gz --repo okieselbach/AutopilotMonitor
|
|
- name: Attest build provenance (portal export)
|
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
|
|
with:
|
|
subject-path: ${{ steps.package.outputs.archive }}
|
|
|
|
- name: Upload build artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 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
|
|
|
|
$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
|