mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-23 20:26:10 +00:00
c26542776e
Every job that runs third-party code (restore, build, tests, npm ci, next build, wix) now holds only a read-only token and hands its output on as one archive. Signing, publishing and deploying happen in separate jobs on a fresh runner that run only this repository's scripts and pinned actions, unpack artifacts outside the checkout and take no build-job output into a script. - build-agent: reserve -> build -> release - build-msi: sign-loader -> build -> release - deploy-backend: reserve -> build -> deploy (publish output as one tar.gz) - deploy-web: build -> deploy (SWA token only, no id-token) -> record - combine-rules: validate (read-only, PRs too) -> publish (push to main only) - deploy-mcp: same shape; documents docker build as its boundary Deploy-only jobs run in the production environment. The jobs that sign stay without an environment for now: a job carries one OIDC subject, and the signing identity still trusts the ref. oidc-login-test.yml is removed; combine-rules no longer triggers on the dev branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
7.7 KiB
YAML
162 lines
7.7 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 }}
|
|
HEALTH_URL: https://mcp.autopilotmonitor.com/health
|
|
VERSIONS_CONTAINER: https://autopilotmonitoreu.blob.core.windows.net/versions
|
|
PACKAGE_JSON: src/McpServer/autopilot-monitor-mcp/package.json
|
|
|
|
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
|
|
|
|
# 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 }}/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.HEALTH_URL }}' -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 }}/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
|