diff --git a/.github/scripts/Assert-HostsFromRegistry.ps1 b/.github/scripts/Assert-HostsFromRegistry.ps1
new file mode 100644
index 00000000..6c9fad39
--- /dev/null
+++ b/.github/scripts/Assert-HostsFromRegistry.ps1
@@ -0,0 +1,84 @@
+<#
+.SYNOPSIS
+ Fails when a workflow or a CI helper spells out a host instead of reading Get-Endpoints.ps1.
+
+.DESCRIPTION
+ The CI counterpart of the URL-registry guard tests (HardcodedUrlGuardTests,
+ hardcodedUrls.guard.test.ts, hardcoded-urls.guard.test.ts). Enforced: every host the registry
+ defines -- derived from it, so the list cannot fall behind -- plus any Azure storage, App Service
+ or autopilotmonitor.com host, so a new or misspelled account is caught as well.
+
+ Scans .github/workflows/*.yml and .github/scripts/*.ps1 except the registry itself. Comment lines
+ and PowerShell block comments do not count: comments may cite URLs.
+
+.PARAMETER Root
+ Repository root to scan. Defaults to the parent of .github/scripts.
+#>
+[CmdletBinding()]
+param(
+ [string]$Root
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not $Root) {
+ $Root = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
+}
+
+$endpoints = & (Join-Path $PSScriptRoot 'Get-Endpoints.ps1')
+$registryHosts = $endpoints.Values | ForEach-Object { ([uri]$_).Host } | Sort-Object -Unique
+$patterns = @($registryHosts | ForEach-Object { [regex]::Escape($_) }) + @(
+ '[a-z0-9-]+\.(blob|table|queue|file)\.core\.windows\.net'
+ '[a-z0-9-]+\.azurewebsites\.net'
+ '([a-z0-9-]+\.)*autopilotmonitor\.com'
+)
+$hostPattern = [regex]::new('(' + ($patterns -join '|') + ')', 'IgnoreCase')
+
+$files = @(
+ Get-ChildItem -Path (Join-Path $Root '.github/workflows') -File -Filter '*.yml'
+ Get-ChildItem -Path (Join-Path $Root '.github/workflows') -File -Filter '*.yaml'
+ Get-ChildItem -Path (Join-Path $Root '.github/scripts') -File -Filter '*.ps1' |
+ Where-Object Name -ne 'Get-Endpoints.ps1'
+)
+
+$findings = 0
+foreach ($file in $files) {
+ $relative = [System.IO.Path]::GetRelativePath($Root, $file.FullName) -replace '\\', '/'
+ $isPowerShell = $file.Extension -eq '.ps1'
+ $inBlock = $false
+ $lineNumber = 0
+ foreach ($line in [System.IO.File]::ReadAllLines($file.FullName)) {
+ $lineNumber++
+ $code = $line
+ if ($isPowerShell) {
+ if ($inBlock) {
+ $end = $code.IndexOf('#>')
+ if ($end -lt 0) { continue }
+ $code = $code.Substring($end + 2)
+ $inBlock = $false
+ }
+ $start = $code.IndexOf('<#')
+ if ($start -ge 0) {
+ $end = $code.IndexOf('#>', $start + 2)
+ if ($end -lt 0) {
+ $code = $code.Substring(0, $start)
+ $inBlock = $true
+ } else {
+ $code = $code.Remove($start, $end + 2 - $start)
+ }
+ }
+ }
+ if ($code.TrimStart().StartsWith('#')) { continue }
+
+ $match = $hostPattern.Match($code)
+ if ($match.Success) {
+ Write-Host "::error file=$relative,line=$lineNumber::$($match.Value) is spelled out here -- take it from .github/scripts/Get-Endpoints.ps1."
+ $findings++
+ }
+ }
+}
+
+if ($findings -gt 0) {
+ throw "$findings host literal(s) outside the endpoint registry."
+}
+Write-Host "No host literal outside .github/scripts/Get-Endpoints.ps1 ($($files.Count) files scanned)."
diff --git a/.github/scripts/Get-Endpoints.ps1 b/.github/scripts/Get-Endpoints.ps1
new file mode 100644
index 00000000..2a453457
--- /dev/null
+++ b/.github/scripts/Get-Endpoints.ps1
@@ -0,0 +1,71 @@
+<#
+.SYNOPSIS
+ The one place the workflows and the release scripts take their hosts from.
+
+.DESCRIPTION
+ CI's member of the URL registry (Shared Constants.cs, web lib/config.ts, MCP src/config.ts):
+ every own host the workflows, the .github/scripts helpers and the local release build talk to,
+ plus the Authenticode timestamp server, is defined here once. Assert-HostsFromRegistry.ps1
+ (Workflow gates) fails on any of these hosts spelled out elsewhere under .github/.
+
+ The storage URLs derive from the account name, so blob and table can never point at two
+ different accounts.
+
+ A file in the repository rather than a GitHub variable: the local release build reads it too,
+ and bearer tokens are sent to these URLs -- where a token goes stays a reviewed line in git.
+
+.PARAMETER ExportToGitHubEnv
+ Write every entry to $GITHUB_ENV instead of returning it, so the following steps of the job
+ read them as environment variables ($env:AGENT_CONTAINER_URL, ${{ env.AGENT_CONTAINER_URL }}).
+
+.OUTPUTS
+ [ordered] hashtable, keyed by the names the environment variables carry.
+
+.EXAMPLE
+ $endpoints = & "$PSScriptRoot/Get-Endpoints.ps1"
+ Invoke-WebRequest -Uri "$($endpoints.AGENT_CONTAINER_URL)/version.json"
+#>
+[CmdletBinding()]
+param(
+ [switch]$ExportToGitHubEnv
+)
+
+$ErrorActionPreference = 'Stop'
+
+$storageAccount = 'autopilotmonitoreu'
+# Pre-migration account in another tenant; only the fail-soft legacy mirror still writes there.
+$legacyStorageAccount = 'autopilotmonitor'
+
+$blob = "https://$storageAccount.blob.core.windows.net"
+$table = "https://$storageAccount.table.core.windows.net"
+
+$endpoints = [ordered]@{
+ STORAGE_BLOB_URL = $blob
+ STORAGE_TABLE_URL = $table
+ AGENT_CONTAINER_URL = "$blob/agent"
+ VERSIONS_CONTAINER_URL = "$blob/versions"
+ STATS_CONTAINER_URL = "$blob/stats"
+ # Platform settings row; CI writes only its agent and bootstrap version columns.
+ ADMIN_CONFIG_URL = "$table/AdminConfiguration(PartitionKey='GlobalConfig',RowKey='config')"
+ LEGACY_AGENT_CONTAINER_URL = "https://$legacyStorageAccount.blob.core.windows.net/agent"
+ # Front Door in front of the agent container: what customer scripts download from.
+ DOWNLOAD_ALIAS_URL = 'https://download.autopilotmonitor.com/agent'
+ API_BASE_URL = 'https://autopilotmonitor-api-eu.azurewebsites.net'
+ MCP_BASE_URL = 'https://mcp.autopilotmonitor.com'
+ PORTAL_BASE_URL = 'https://portal.autopilotmonitor.com'
+ WWW_BASE_URL = 'https://www.autopilotmonitor.com'
+ DOCS_BASE_URL = 'https://docs.autopilotmonitor.com'
+ CODE_SIGN_TIMESTAMP_URL = 'http://timestamp.acs.microsoft.com'
+}
+
+if ($ExportToGitHubEnv) {
+ if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) {
+ throw '-ExportToGitHubEnv only works inside a GitHub Actions step.'
+ }
+ $endpoints.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" } |
+ Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+ $endpoints.GetEnumerator() | ForEach-Object { Write-Host "$($_.Key) = $($_.Value)" }
+ return
+}
+
+return $endpoints
diff --git a/.github/scripts/Publish-BootstrapScripts.ps1 b/.github/scripts/Publish-BootstrapScripts.ps1
index 614b64a4..bbee78c0 100644
--- a/.github/scripts/Publish-BootstrapScripts.ps1
+++ b/.github/scripts/Publish-BootstrapScripts.ps1
@@ -81,7 +81,6 @@ param(
[string]$SigningToken,
[string]$KeyVaultUrl,
[string]$CertificateName,
- [string]$TimestampUrl = 'http://timestamp.acs.microsoft.com',
[string]$ExpectedPublisher,
[string]$SignedOutputDir,
[switch]$DryRun
@@ -93,10 +92,13 @@ if (-not $RepoRoot) {
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
}
-$ContainerUrl = 'https://autopilotmonitoreu.blob.core.windows.net/agent'
-$LegacyContainerUrl = 'https://autopilotmonitor.blob.core.windows.net/agent'
-$AliasUrl = 'https://download.autopilotmonitor.com/agent'
-$TableUrl = "https://autopilotmonitoreu.table.core.windows.net/AdminConfiguration(PartitionKey='GlobalConfig',RowKey='config')"
+# Every host, the timestamp server included, from the one endpoint registry.
+$Endpoints = & (Join-Path $PSScriptRoot 'Get-Endpoints.ps1')
+$ContainerUrl = $Endpoints.AGENT_CONTAINER_URL
+$LegacyContainerUrl = $Endpoints.LEGACY_AGENT_CONTAINER_URL
+$AliasUrl = $Endpoints.DOWNLOAD_ALIAS_URL
+$TableUrl = $Endpoints.ADMIN_CONFIG_URL
+$TimestampUrl = $Endpoints.CODE_SIGN_TIMESTAMP_URL
$BootstrapSource = Join-Path $RepoRoot 'scripts/Bootstrap/Install-AutopilotMonitor.ps1'
$BootstrapBlob = 'Install-AutopilotMonitor.ps1'
@@ -210,7 +212,7 @@ function Set-PublishSetSignature {
--timestamp-rfc3161 $TimestampUrl `
--file-digest sha256 `
--description 'Autopilot Monitor bootstrap' `
- --description-url 'https://www.autopilotmonitor.com' `
+ --description-url $Endpoints.WWW_BASE_URL `
$paths
# Native tool: PowerShell does not throw on a non-zero exit, and an unnoticed signing
@@ -327,8 +329,10 @@ $loaderVersion = Get-BootstrapScriptVersion -Content $loaderContent -Origin $Loa
Write-Host "Loader script version: $loaderVersion"
# ------------------------------------------------------------------ 2. dev render
-$stableUrlLiteral = '$AgentDownloadUrl = "https://download.autopilotmonitor.com/agent/AutopilotMonitor-Agent.zip"'
-$devUrlLiteral = '$AgentDownloadUrl = "https://download.autopilotmonitor.com/agent/AutopilotMonitor-Agent-dev.zip"'
+# The anchors are the customer scripts' own text, built from the alias in the registry. Should
+# the alias ever move, the scripts have to move with it -- until they do, the render stops here.
+$stableUrlLiteral = '$AgentDownloadUrl = "' + $AliasUrl + '/AutopilotMonitor-Agent.zip"'
+$devUrlLiteral = '$AgentDownloadUrl = "' + $AliasUrl + '/AutopilotMonitor-Agent-dev.zip"'
$stableManLiteral = '$VersionJsonName = "version.json"'
$devManLiteral = '$VersionJsonName = "version-dev.json"'
@@ -339,8 +343,8 @@ $devContent = $sourceContent.Replace($stableUrlLiteral, $devUrlLiteral).Replace(
Write-Host "Rendered $DevBlob (dev agent URL + version-dev.json)"
# The dev loader must fetch the dev bootstrap, or a dev device would run the stable chain.
-$stableLoaderLiteral = '$BootstrapUrl = "https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor.ps1"'
-$devLoaderLiteral = '$BootstrapUrl = "https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor-Dev.ps1"'
+$stableLoaderLiteral = '$BootstrapUrl = "' + $AliasUrl + '/Install-AutopilotMonitor.ps1"'
+$devLoaderLiteral = '$BootstrapUrl = "' + $AliasUrl + '/Install-AutopilotMonitor-Dev.ps1"'
if ($loaderContent.IndexOf($stableLoaderLiteral) -lt 0) {
throw "Loader dev-render: anchor literal missing in $LoaderSource"
diff --git a/.github/workflows/action-pins.yml b/.github/workflows/action-pins.yml
index b54bb06f..daf3bd6c 100644
--- a/.github/workflows/action-pins.yml
+++ b/.github/workflows/action-pins.yml
@@ -1,12 +1,15 @@
name: Workflow gates
-# Two checks over the workflow files themselves:
+# Three checks over the workflow files and their helper scripts:
# * verify-pins -- every third-party action is referenced by a full commit SHA plus a
# version comment (stricter than zizmor's unpinned-uses audit).
# * zizmor -- static analysis of every workflow (permissions, credential
# persistence, template injection, ...). Configuration and the
# reasoned ignores live in zizmor.yml at the repository root; the
# same run is expected locally before a workflow change is committed.
+# * hosts -- no workflow or .github/scripts helper spells out a host; every one
+# comes from .github/scripts/Get-Endpoints.ps1, CI's member of the URL
+# registry (the C#, web and MCP registries have guard tests of their own).
#
# Every third-party action must be referenced by a full commit SHA, never by a tag.
# Tags are mutable: whoever controls an action repository can repoint v4 at new code,
@@ -34,10 +37,10 @@ name: Workflow gates
on:
push:
branches: [main]
- paths: ['.github/workflows/**', '.github/dependabot.yml', 'zizmor.yml']
+ paths: ['.github/workflows/**', '.github/scripts/**', '.github/dependabot.yml', 'zizmor.yml']
pull_request:
branches: [main]
- paths: ['.github/workflows/**', '.github/dependabot.yml', 'zizmor.yml']
+ paths: ['.github/workflows/**', '.github/scripts/**', '.github/dependabot.yml', 'zizmor.yml']
workflow_dispatch:
permissions:
@@ -106,3 +109,17 @@ jobs:
with:
version: 1.30.1
min-severity: low
+
+ hosts:
+ name: Hosts only from the endpoint registry
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ # The enforced host list is derived from the registry itself, plus every Azure storage,
+ # App Service and autopilotmonitor.com host, so it cannot fall behind a new entry.
+ - name: No host literal outside .github/scripts/Get-Endpoints.ps1
+ shell: pwsh
+ run: ./.github/scripts/Assert-HostsFromRegistry.ps1
diff --git a/.github/workflows/build-agent.yml b/.github/workflows/build-agent.yml
index 1b0dc2a2..3c42a252 100644
--- a/.github/workflows/build-agent.yml
+++ b/.github/workflows/build-agent.yml
@@ -60,13 +60,13 @@ env:
VERSIONED_ZIP: AutopilotMonitor-Agent-v2.zip
STABLE_ZIP: AutopilotMonitor-Agent.zip
STABLE_MANIFEST: version.json
- # Authenticode via Azure Key Vault; identity and vault come from repository variables.
+ # Authenticode via Azure Key Vault; identity and vault come from repository variables, the
+ # timestamp server (CODE_SIGN_TIMESTAMP_URL) from the endpoint registry like every host here.
# CODE_SIGN_EXPECTED_SUBJECT is optional and pins the publisher in the verify step.
CODE_SIGN_CLIENT_ID: ${{ vars.CODE_SIGN_CLIENT_ID }}
CODE_SIGN_CERTIFICATE_NAME: ${{ vars.CODE_SIGN_CERTIFICATE_NAME }}
CODE_SIGN_KEY_VAULT_URL: ${{ vars.CODE_SIGN_KEY_VAULT_URL }}
CODE_SIGN_EXPECTED_SUBJECT: ${{ vars.CODE_SIGN_EXPECTED_SUBJECT }}
- CODE_SIGN_TIMESTAMP_URL: http://timestamp.acs.microsoft.com
jobs:
# Runs only this repository's scripts. The deploy identity trusts the `production`
@@ -89,6 +89,11 @@ jobs:
with:
persist-credentials: false
+ # Every host in this workflow comes from the one endpoint registry.
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
# Every released build is signed; a missing variable stops the run before a build
# number is burnt.
- name: Check code-signing configuration
@@ -129,7 +134,7 @@ jobs:
$override = [int]$env:BUILD_NUMBER_OVERRIDE
}
- $counterUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent/buildcounter-v$($env:LINE_MAJOR).txt"
+ $counterUrl = "$env:AGENT_CONTAINER_URL/buildcounter-v$($env:LINE_MAJOR).txt"
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl $counterUrl -Override $override
$csproj = Get-Content $env:CSPROJ_PATH -Raw
@@ -238,6 +243,11 @@ jobs:
with:
persist-credentials: false
+ # Storage, legacy mirror, docs link, timestamp server: all from the endpoint registry.
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
- name: Download unsigned build output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -336,7 +346,7 @@ jobs:
--timestamp-rfc3161 "$env:CODE_SIGN_TIMESTAMP_URL" `
--file-digest sha256 `
--description "Autopilot Monitor Agent" `
- --description-url "https://www.autopilotmonitor.com" `
+ --description-url "$env:WWW_BASE_URL" `
($targets | ForEach-Object { $_.FullName })
if ($LASTEXITCODE -ne 0) {
@@ -485,13 +495,13 @@ jobs:
ZIP_SHA256: ${{ steps.artifact.outputs.sha256 }}
EXE_SHA256: ${{ steps.artifact.outputs.exe_sha256 }}
run: |
- $containerUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent"
+ $containerUrl = $env:AGENT_CONTAINER_URL
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
# Legacy mirror (transition until customers moved to the download.-alias):
# blobs are ALSO pushed to the old account so the bootstrap URL baked into
# customers' Intune scripts keeps serving current bits. Fail-soft -- a
# legacy hiccup must never fail a release. Counter lives ONLY on eu.
- $legacyContainerUrl = "https://autopilotmonitor.blob.core.windows.net/agent"
+ $legacyContainerUrl = $env:LEGACY_AGENT_CONTAINER_URL
$legacySasToken = "${{ secrets.AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN }}".TrimStart('?')
function Send-Blob {
@@ -542,7 +552,6 @@ jobs:
-SigningToken $env:CODE_SIGN_AKV_ACCESS_TOKEN `
-KeyVaultUrl $env:CODE_SIGN_KEY_VAULT_URL `
-CertificateName $env:CODE_SIGN_CERTIFICATE_NAME `
- -TimestampUrl $env:CODE_SIGN_TIMESTAMP_URL `
-ExpectedPublisher $env:CODE_SIGN_EXPECTED_SUBJECT
}
@@ -556,7 +565,7 @@ jobs:
$sha256 = $env:ZIP_SHA256
$exeSha256 = $env:EXE_SHA256
$agentVersion = $env:AGENT_VERSION
- $tableUrl = "https://autopilotmonitoreu.table.core.windows.net/AdminConfiguration(PartitionKey='GlobalConfig',RowKey='config')"
+ $tableUrl = $env:ADMIN_CONFIG_URL
$tableBody = @{
LatestAgentV2Version = $agentVersion
LatestAgentV2Sha256 = $sha256
@@ -636,7 +645,7 @@ jobs:
$notes = "$env:RELEASE_NOTES".Trim()
if ($notes -eq '') {
- $notes = "Minor enhancements and bug fixes. See the [Agent Changelog](https://docs.autopilotmonitor.com/changelog/agent-changelog) for details.`n"
+ $notes = "Minor enhancements and bug fixes. See the [Agent Changelog]($env:DOCS_BASE_URL/changelog/agent-changelog) for details.`n"
}
# Previous release for the compare link. Fail-soft: the first release has none,
diff --git a/.github/workflows/build-msi.yml b/.github/workflows/build-msi.yml
index c13b4cb5..59453a19 100644
--- a/.github/workflows/build-msi.yml
+++ b/.github/workflows/build-msi.yml
@@ -11,7 +11,7 @@ on:
workflow_dispatch:
inputs:
upload_blob:
- description: 'Upload to blob (customer-facing via download.autopilotmonitor.com/agent/)'
+ description: 'Upload to blob (customer-facing through the download alias)'
required: true
type: boolean
default: true
@@ -25,13 +25,13 @@ env:
MSI_PROJECT_DIR: src/Agent/AutopilotMonitor.BootstrapMsi
RUNNER_SCRIPT: scripts/Bootstrap/Start-AutopilotMonitor.ps1
STABLE_MSI: AutopilotMonitor-Bootstrap.msi
- # Authenticode via Azure Key Vault; identity and vault come from repository variables.
+ # Authenticode via Azure Key Vault; identity and vault come from repository variables, the
+ # timestamp server (CODE_SIGN_TIMESTAMP_URL) from the endpoint registry like every host here.
# CODE_SIGN_EXPECTED_SUBJECT is optional and pins the publisher in the verify steps.
CODE_SIGN_CLIENT_ID: ${{ vars.CODE_SIGN_CLIENT_ID }}
CODE_SIGN_CERTIFICATE_NAME: ${{ vars.CODE_SIGN_CERTIFICATE_NAME }}
CODE_SIGN_KEY_VAULT_URL: ${{ vars.CODE_SIGN_KEY_VAULT_URL }}
CODE_SIGN_EXPECTED_SUBJECT: ${{ vars.CODE_SIGN_EXPECTED_SUBJECT }}
- CODE_SIGN_TIMESTAMP_URL: http://timestamp.acs.microsoft.com
jobs:
# Signs the loader BEFORE the build: wix embeds the file as it finds it, so a loader signed
@@ -54,6 +54,11 @@ jobs:
with:
persist-credentials: false
+ # Every host in this workflow comes from the one endpoint registry.
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
# The loader executes as SYSTEM on every enrolling device — a parse error or
# non-ASCII mojibake (PS 5.1 codepage roulette) must fail the build, not the fleet.
# bootstrap-script-gates.yml runs the same checks plus Pester on pushes; this job
@@ -118,7 +123,7 @@ jobs:
--timestamp-rfc3161 "$env:CODE_SIGN_TIMESTAMP_URL" `
--file-digest sha256 `
--description 'Autopilot Monitor bootstrap runner' `
- --description-url 'https://www.autopilotmonitor.com' `
+ --description-url $env:WWW_BASE_URL `
$env:RUNNER_SCRIPT
if ($LASTEXITCODE -ne 0) { throw "azuresigntool failed with exit code $LASTEXITCODE" }
@@ -210,6 +215,10 @@ jobs:
with:
persist-credentials: false
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
# Read from this job's own checkout, never handed over by the build job.
- name: Read MSI version from Package.wxs
id: version
@@ -284,7 +293,7 @@ jobs:
--timestamp-rfc3161 "$env:CODE_SIGN_TIMESTAMP_URL" `
--file-digest sha256 `
--description 'Autopilot Monitor Bootstrap' `
- --description-url 'https://www.autopilotmonitor.com' `
+ --description-url $env:WWW_BASE_URL `
$env:STABLE_MSI
if ($LASTEXITCODE -ne 0) { throw "azuresigntool failed with exit code $LASTEXITCODE" }
@@ -305,7 +314,7 @@ jobs:
env:
MSI_VERSION: ${{ steps.version.outputs.version }}
run: |
- $containerUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent"
+ $containerUrl = $env:AGENT_CONTAINER_URL
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
$version = $env:MSI_VERSION
@@ -338,14 +347,14 @@ jobs:
shell: pwsh
run: |
$localSize = (Get-Item $env:STABLE_MSI).Length
- $resp = Invoke-WebRequest -Uri "https://download.autopilotmonitor.com/agent/$env:STABLE_MSI" -Method Head -UseBasicParsing
+ $resp = Invoke-WebRequest -Uri "$env:DOWNLOAD_ALIAS_URL/$env:STABLE_MSI" -Method Head -UseBasicParsing
if ($resp.StatusCode -ne 200) { Write-Error "Alias HEAD returned $($resp.StatusCode)"; exit 1 }
$remoteSize = [long]$resp.Headers['Content-Length'][0]
if ($remoteSize -ne $localSize) {
Write-Error "Size mismatch: alias serves $remoteSize bytes, local MSI is $localSize bytes"
exit 1
}
- Write-Host "Alias verified: https://download.autopilotmonitor.com/agent/$env:STABLE_MSI ($remoteSize bytes)"
+ Write-Host "Alias verified: $env:DOWNLOAD_ALIAS_URL/$env:STABLE_MSI ($remoteSize bytes)"
# Sigstore keyless provenance, same as the agent ZIP; covers the signed bytes. Skipped
# on runs that publish nothing -- no log entry for bytes nobody receives. Verify:
diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml
index 09f47ad1..f71c8e88 100644
--- a/.github/workflows/deploy-backend.yml
+++ b/.github/workflows/deploy-backend.yml
@@ -9,10 +9,9 @@ on:
permissions:
contents: read
+# Hosts (API, versions container) come from .github/scripts/Get-Endpoints.ps1.
env:
FUNCTION_APP: autopilotmonitor-api-eu
- HEALTH_URL: https://autopilotmonitor-api-eu.azurewebsites.net/api/health
- VERSIONS_CONTAINER: https://autopilotmonitoreu.blob.core.windows.net/versions
CSPROJ_PATH: src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj
jobs:
@@ -35,6 +34,10 @@ jobs:
with:
persist-credentials: false
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
- name: Login to Azure (OIDC)
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
with:
@@ -50,7 +53,7 @@ jobs:
id: version
shell: pwsh
run: |
- $buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "${{ env.VERSIONS_CONTAINER }}/backend.txt"
+ $buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "$env:VERSIONS_CONTAINER_URL/backend.txt"
$csproj = Get-Content ${{ env.CSPROJ_PATH }} -Raw
if ($csproj -notmatch '(\d+\.\d+)') {
@@ -120,6 +123,10 @@ jobs:
with:
persist-credentials: false
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
- name: Download publish output
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -162,7 +169,7 @@ jobs:
while ([DateTime]::UtcNow -lt $deadline) {
$attempt++
try {
- $health = Invoke-RestMethod -Uri '${{ env.HEALTH_URL }}' -Method Get -TimeoutSec 30
+ $health = Invoke-RestMethod -Uri "$env:API_BASE_URL/api/health" -Method Get -TimeoutSec 30
if ($health.commitHash -eq $expectedCommit -and $health.version -eq $expectedVersion) {
Write-Host "Live after $attempt attempt(s): $($health.version) @ $($health.commitHash), built $($health.buildUtc)"
exit 0
@@ -196,7 +203,7 @@ jobs:
} | ConvertTo-Json
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
- $url = "${{ env.VERSIONS_CONTAINER }}/backend.json"
+ $url = "$env:VERSIONS_CONTAINER_URL/backend.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/backend.json"
diff --git a/.github/workflows/deploy-mcp.yml b/.github/workflows/deploy-mcp.yml
index 035a3ec8..60b61a66 100644
--- a/.github/workflows/deploy-mcp.yml
+++ b/.github/workflows/deploy-mcp.yml
@@ -16,9 +16,8 @@ env:
# 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
+ # 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
@@ -36,6 +35,10 @@ jobs:
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
@@ -68,7 +71,7 @@ jobs:
id: version
shell: pwsh
run: |
- $buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "${{ env.VERSIONS_CONTAINER }}/mcp.txt"
+ $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 '.'
@@ -123,7 +126,7 @@ jobs:
while ([DateTime]::UtcNow -lt $deadline) {
$attempt++
try {
- $health = Invoke-RestMethod -Uri '${{ env.HEALTH_URL }}' -Method Get -TimeoutSec 60
+ $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
@@ -154,7 +157,7 @@ jobs:
} | ConvertTo-Json
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
- $url = "${{ env.VERSIONS_CONTAINER }}/mcp.json"
+ $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"
diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml
index f2b92cf5..ebc790e2 100644
--- a/.github/workflows/deploy-web.yml
+++ b/.github/workflows/deploy-web.yml
@@ -28,9 +28,9 @@ on:
permissions:
contents: read
+# Hosts (API, portal, storage) come from .github/scripts/Get-Endpoints.ps1, never from a
+# repository variable.
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.
@@ -52,6 +52,10 @@ jobs:
lfs: false
fetch-depth: 0 # Full history needed for git-based sitemap lastmod dates
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
# 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.
@@ -109,10 +113,6 @@ jobs:
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.
@@ -122,6 +122,14 @@ jobs:
# Star counter from the step above; empty when the lookup failed.
NEXT_PUBLIC_GITHUB_STARS: ${{ steps.stars.outputs.count }}
run: |
+ # Both from the endpoint registry (Load endpoints). Empty, the portal would silently call
+ # localhost:7071. The stats manifest is what browsers load the live numbers from, and the
+ # build reads it once to bake the numbers of this moment into the landing page's static
+ # HTML (lib/platformStats.ts).
+ export NEXT_PUBLIC_API_BASE_URL="$API_BASE_URL"
+ export NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL="$STATS_CONTAINER_URL"
+ test -n "$NEXT_PUBLIC_API_BASE_URL" || { echo "::error::API_BASE_URL is empty - the portal would call localhost"; exit 1; }
+ test -n "$NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL" || { echo "::error::STATS_CONTAINER_URL is empty"; exit 1; }
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; }
@@ -200,6 +208,10 @@ jobs:
with:
persist-credentials: false
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
- name: Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -234,7 +246,7 @@ jobs:
while ([DateTime]::UtcNow -lt $deadline) {
$attempt++
try {
- $v = Invoke-RestMethod -Uri '${{ env.PORTAL_VERSION_URL }}' -Method Get -TimeoutSec 30 -Headers @{ 'Cache-Control' = 'no-cache' }
+ $v = Invoke-RestMethod -Uri "$env:PORTAL_BASE_URL/version.json" -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
@@ -277,7 +289,7 @@ jobs:
} | ConvertTo-Json
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
- $url = "${{ env.VERSIONS_CONTAINER }}/web.json"
+ $url = "$env:VERSIONS_CONTAINER_URL/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"
diff --git a/.github/workflows/indexnow-notify.yml b/.github/workflows/indexnow-notify.yml
index 32d15c8a..5206b312 100644
--- a/.github/workflows/indexnow-notify.yml
+++ b/.github/workflows/indexnow-notify.yml
@@ -3,24 +3,34 @@ name: IndexNow Notify (Bing/Yandex)
on:
workflow_dispatch:
-# The job only talks to public endpoints; it never needs a repository token.
-permissions: {}
+# Read-only, for the checkout of the endpoint registry; beyond that the job only talks to
+# public endpoints.
+permissions:
+ contents: read
jobs:
notify:
runs-on: ubuntu-latest
name: Submit URLs to IndexNow
steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Load endpoints
+ shell: pwsh
+ run: ./.github/scripts/Get-Endpoints.ps1 -ExportToGitHubEnv
+
- name: Notify IndexNow
run: |
- URLS=$(curl -s https://www.autopilotmonitor.com/sitemap.xml | grep -oP '\K[^<]+')
+ URLS=$(curl -s "$WWW_BASE_URL/sitemap.xml" | grep -oP '\K[^<]+')
URL_JSON=$(echo "$URLS" | jq -R -s 'split("\n") | map(select(length > 0))')
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json" \
-d "{
- \"host\": \"www.autopilotmonitor.com\",
+ \"host\": \"${WWW_BASE_URL#https://}\",
\"key\": \"8f1710b5-f14b-477e-938b-6c01ef851661\",
- \"keyLocation\": \"https://www.autopilotmonitor.com/IndexNow.txt\",
+ \"keyLocation\": \"$WWW_BASE_URL/IndexNow.txt\",
\"urlList\": $URL_JSON
}")
echo "IndexNow response: HTTP $RESPONSE"
diff --git a/.github/workflows/publish-scripts.yml b/.github/workflows/publish-scripts.yml
index 982e5893..13591dce 100644
--- a/.github/workflows/publish-scripts.yml
+++ b/.github/workflows/publish-scripts.yml
@@ -36,13 +36,13 @@ concurrency:
# Authenticode for the customer scripts. Same identity and same repository variables as
# build-agent.yml -- the loader on every device pins this publisher, so the two must not
-# drift apart.
+# drift apart. The publisher takes every host, the timestamp server included, from
+# .github/scripts/Get-Endpoints.ps1.
env:
CODE_SIGN_CLIENT_ID: ${{ vars.CODE_SIGN_CLIENT_ID }}
CODE_SIGN_CERTIFICATE_NAME: ${{ vars.CODE_SIGN_CERTIFICATE_NAME }}
CODE_SIGN_KEY_VAULT_URL: ${{ vars.CODE_SIGN_KEY_VAULT_URL }}
CODE_SIGN_EXPECTED_SUBJECT: ${{ vars.CODE_SIGN_EXPECTED_SUBJECT }}
- CODE_SIGN_TIMESTAMP_URL: http://timestamp.acs.microsoft.com
jobs:
# $/ is the self-repository form: this repository at the running commit, never a
@@ -124,7 +124,6 @@ jobs:
-SigningToken $env:CODE_SIGN_AKV_ACCESS_TOKEN `
-KeyVaultUrl $env:CODE_SIGN_KEY_VAULT_URL `
-CertificateName $env:CODE_SIGN_CERTIFICATE_NAME `
- -TimestampUrl $env:CODE_SIGN_TIMESTAMP_URL `
-ExpectedPublisher $env:CODE_SIGN_EXPECTED_SUBJECT `
-SignedOutputDir "$env:RUNNER_TEMP/signed-scripts" `
-DryRun:$dryRun