mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-23 20:26:10 +00:00
CI: one endpoint registry for every host the workflows use
.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>
This commit is contained in:
@@ -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)."
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<!-- release-notes: none -->"
|
||||
$notes = "Minor enhancements and bug fixes. See the [Agent Changelog]($env:DOCS_BASE_URL/changelog/agent-changelog) for details.`n<!-- release-notes: none -->"
|
||||
}
|
||||
|
||||
# Previous release for the compare link. Fail-soft: the first release has none,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 '<VersionPrefix>(\d+\.\d+)</VersionPrefix>') {
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 '<loc>\K[^<]+')
|
||||
URLS=$(curl -s "$WWW_BASE_URL/sitemap.xml" | grep -oP '<loc>\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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user