mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
8d24c5680b
.github/scripts/Get-Endpoints.ps1 defines the storage account (blob and table derived from its name), the containers, the AdminConfiguration row, the legacy mirror, the download alias, API, MCP, portal, www, docs and the timestamp server. Workflows load it once per job; the script publisher and the local release build call it; the dev-render anchors are built from the alias entry. deploy-web takes NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL from the registry instead of repository variables and refuses an empty value. A new Workflow gates job fails on any host spelled out in a workflow or CI helper; the enforced list is derived from the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
7.8 KiB
YAML
165 lines
7.8 KiB
YAML
name: Deploy MCP Server (Container App)
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
id-token: write # OIDC federated login (secret-less)
|
|
contents: read
|
|
|
|
env:
|
|
ACR_NAME: autopilotmonitoreuacr
|
|
CONTAINER_APP_NAME: autopilotmonitor-mcp
|
|
RESOURCE_GROUP: ${{ secrets.AZURE_MCP_RESOURCE_GROUP }}
|
|
# Unique per RUN, not per commit: the docs corpus is baked into the image, so a
|
|
# docs-only redeploy at an unchanged code sha must still produce a new image
|
|
# reference — with a sha-only tag `az containerapp update` saw the same image
|
|
# and silently created no revision (2026-08-28).
|
|
IMAGE_TAG: ${{ github.sha }}-${{ github.run_number }}
|
|
PACKAGE_JSON: src/McpServer/autopilot-monitor-mcp/package.json
|
|
# Hosts (MCP, versions container) come from .github/scripts/Get-Endpoints.ps1.
|
|
|
|
jobs:
|
|
# One job on purpose, unlike the other deploys: all third-party code (npm ci, the build, the
|
|
# embedding precompute) runs inside `docker build`, whose containers see neither the az
|
|
# login nor the registry credentials nor this job's OIDC request token. That container is
|
|
# the boundary -- never move npm onto the runner here, or the build gets the job's
|
|
# credentials. The deploy identity trusts the `production` environment, which admits main only.
|
|
build-and-deploy:
|
|
runs-on: ubuntu-latest
|
|
name: Build and Deploy MCP Server
|
|
environment: production
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Load endpoints
|
|
shell: pwsh
|
|
run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
|
|
|
|
# The published customer documentation lives in its own PUBLIC repo and is
|
|
# baked into the image as the search_docs corpus (see the Dockerfile). No
|
|
# token is needed. Checking it out here rather than vendoring it keeps the
|
|
# docs single-sourced — but it also means EDITING THE DOCS DOES NOT UPDATE A
|
|
# RUNNING SERVER; this workflow must be re-run (IMAGE_TAG is per run so the
|
|
# re-run actually rolls a new revision). /health reports the SHA captured
|
|
# below so that staleness is checkable.
|
|
- name: Check out documentation bundle
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
repository: autopilotmonitor/autopilotmonitor-docs
|
|
path: mcp-docs
|
|
|
|
- name: Resolve docs commit
|
|
id: docs
|
|
run: echo "sha=$(git -C mcp-docs rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- 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 }}
|
|
|
|
# Same counter mechanism as agent and backend: reserve-before-build with
|
|
# ETag-CAS against the component's own counter blob. major.minor stays curated
|
|
# in package.json; only the build part is minted here.
|
|
- name: Reserve build number (counter blob CAS)
|
|
id: version
|
|
shell: pwsh
|
|
run: |
|
|
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "$env:VERSIONS_CONTAINER_URL/mcp.txt"
|
|
|
|
$pkgVersion = (Get-Content ${{ env.PACKAGE_JSON }} -Raw | ConvertFrom-Json).version
|
|
$prefix = ($pkgVersion -split '\.')[0..1] -join '.'
|
|
$version = "$prefix.$buildNumber"
|
|
$buildUtc = [DateTime]::UtcNow.ToString('o')
|
|
Write-Host "MCP version: $version (built $buildUtc)"
|
|
|
|
# 7 chars, matching what the backend's BackendBuildInfo reports — one commit
|
|
# shape across all components so the values can be compared as-is.
|
|
$shortSha = '${{ github.sha }}'.Substring(0, 7)
|
|
|
|
"build_number=$buildNumber" >> $env:GITHUB_OUTPUT
|
|
"version=$version" >> $env:GITHUB_OUTPUT
|
|
"build_utc=$buildUtc" >> $env:GITHUB_OUTPUT
|
|
"short_sha=$shortSha" >> $env:GITHUB_OUTPUT
|
|
|
|
- name: Login to ACR
|
|
run: az acr login --name "$ACR_NAME"
|
|
|
|
- name: Build and push container image
|
|
run: |
|
|
docker build \
|
|
-f src/McpServer/autopilot-monitor-mcp/Dockerfile \
|
|
--build-arg DOCS_COMMIT=${{ steps.docs.outputs.sha }} \
|
|
--build-arg BUILD_NUMBER=${{ steps.version.outputs.build_number }} \
|
|
--build-arg BUILD_COMMIT=${{ steps.version.outputs.short_sha }} \
|
|
--build-arg BUILD_UTC=${{ steps.version.outputs.build_utc }} \
|
|
-t "$ACR_NAME.azurecr.io/$CONTAINER_APP_NAME:$IMAGE_TAG" \
|
|
-t "$ACR_NAME.azurecr.io/$CONTAINER_APP_NAME:latest" \
|
|
.
|
|
docker push "$ACR_NAME.azurecr.io/$CONTAINER_APP_NAME" --all-tags
|
|
|
|
- name: Deploy to Container App
|
|
run: |
|
|
az containerapp update \
|
|
--name "$CONTAINER_APP_NAME" \
|
|
--resource-group "$RESOURCE_GROUP" \
|
|
--image "$ACR_NAME.azurecr.io/$CONTAINER_APP_NAME:$IMAGE_TAG"
|
|
|
|
# `az containerapp update` returns once the revision is provisioned, not once
|
|
# it serves traffic — and with minReplicas=0 the first request also pays a cold
|
|
# start. Polling /health until it reports THIS build is what makes the manifest
|
|
# below mean "live"; a full 5 minutes because the cold start alone is ~25s.
|
|
- name: Verify deployed build is live
|
|
shell: pwsh
|
|
run: |
|
|
$expectedCommit = '${{ steps.version.outputs.short_sha }}'
|
|
$expectedVersion = '${{ steps.version.outputs.version }}'
|
|
$deadline = [DateTime]::UtcNow.AddMinutes(5)
|
|
$attempt = 0
|
|
|
|
while ([DateTime]::UtcNow -lt $deadline) {
|
|
$attempt++
|
|
try {
|
|
$health = Invoke-RestMethod -Uri "$env:MCP_BASE_URL/health" -Method Get -TimeoutSec 60
|
|
if ($health.commit -eq $expectedCommit -and $health.version -eq $expectedVersion) {
|
|
Write-Host "Live after $attempt attempt(s): $($health.version) @ $($health.commit), docs $($health.docs.commit) ($($health.docs.chunks) chunks)"
|
|
exit 0
|
|
}
|
|
Write-Host "Attempt ${attempt}: serving $($health.version) @ $($health.commit) -- waiting for $expectedVersion @ $expectedCommit"
|
|
} catch {
|
|
Write-Host "Attempt ${attempt}: /health not answering yet -- $($_.Exception.Message)"
|
|
}
|
|
Start-Sleep -Seconds 15
|
|
}
|
|
|
|
throw "MCP did not report $expectedVersion @ $expectedCommit within 5 minutes -- deployment not verified, version manifest NOT published"
|
|
|
|
# Cross-component record of what is deployed where; written last and only on a
|
|
# verified deploy. docsCommit rides along because for this component "which
|
|
# build" and "which documentation" are two separate questions.
|
|
- name: Publish version manifest
|
|
shell: pwsh
|
|
run: |
|
|
$manifest = [ordered]@{
|
|
component = 'mcp'
|
|
version = '${{ steps.version.outputs.version }}'
|
|
commit = '${{ steps.version.outputs.short_sha }}'
|
|
buildUtc = '${{ steps.version.outputs.build_utc }}'
|
|
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_URL/mcp.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/mcp.json"
|
|
Write-Host $manifest
|