mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
e4a5a6654d
Every storage write against autopilotmonitoreu -- build-counter CAS, agent ZIP and MSI, version manifests, customer bootstrap scripts, the AdminConfiguration hash oracle -- now rides on the existing federated credential (subject repo:.../ref:refs/heads/main) instead of a long-lived SAS in repo secrets. The REST calls themselves are unchanged: '?<sas>' became an Authorization header, so the ETag-CAS of the counters, Content-MD5 and the no-cache contract survive verbatim. That is also why this is not a rewrite onto 'az storage blob upload' -- it cannot express If-Match, and two concurrent builds could then mint the same build number. .github/scripts/Get-StorageAuthHeaders.ps1 is the single place that knows the resource id, the az invocation and the service version. x-ms-version is mandatory for Entra-authorized requests (>= 2017-11-09) and for any Table request without a SAS; 2021-08-06 was verified live against both the blob and the table endpoint of this account. azure/login added to build-agent, build-msi, publish-scripts (plus id-token: write) and deploy-web; deploy-backend and deploy-mcp already had it. Publish-BootstrapScripts.ps1 -DryRun acquires NO token: the PR gate runs it without any Azure login, and that must stay true. The AdminConfiguration merge lost its "secret not configured" skip branch -- a failure there now surfaces instead of silently leaving the hash oracle pointing at an older build. AZURE_BLOB_AGENT_SAS_TOKEN, AZURE_BLOB_VERSIONS_SAS_TOKEN and AZURE_TABLE_ADMIN_SAS_TOKEN are retired once the first runs are green. AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN stays: that account lives in the pre-migration tenant, where this principal has no identity -- it dies with the mirror, not before. Requires on the SP: Storage Blob Data Contributor on the containers 'agent' and 'versions', Storage Table Data Contributor on the table 'AdminConfiguration'. Granted 2026-09-08.
100 lines
3.8 KiB
PowerShell
100 lines
3.8 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Reserves a monotone build number from a counter blob (ETag compare-and-swap).
|
|
|
|
.DESCRIPTION
|
|
Single source of build numbers for every component that ships from this repo
|
|
(agent, backend, MCP). Each component owns one counter blob holding a plain
|
|
integer; this script reads it, writes value+1 back under If-Match, and returns
|
|
the reserved number.
|
|
|
|
Reserve-BEFORE-build: two concurrent builds can never mint the same number.
|
|
A failed build burns a number, which is harmless -- uniqueness matters,
|
|
density does not.
|
|
|
|
The blob is public-read; only the write needs authorization, and that comes from the
|
|
ambient Entra identity (see Get-StorageAuthHeaders.ps1) -- no secret is passed in.
|
|
|
|
Why a counter and not the published version manifest: manifests are written
|
|
on publish, not on build (the agent writes version.json only for stable
|
|
releases), so two dev builds in a row would derive the same number and
|
|
overwrite each other's versioned artifact.
|
|
|
|
.PARAMETER CounterUrl
|
|
Full URL of the counter blob.
|
|
|
|
.PARAMETER Override
|
|
Explicit build number instead of counter+1. If it is not greater than the
|
|
current counter the blob is left untouched (re-build of a number that was
|
|
already reserved). 0 = no override.
|
|
|
|
.PARAMETER MaxAttempts
|
|
CAS retries before giving up.
|
|
|
|
.OUTPUTS
|
|
[int] the reserved build number. Progress goes to the host, not the pipeline.
|
|
|
|
.EXAMPLE
|
|
$n = ./Request-BuildNumber.ps1 -CounterUrl $url
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$CounterUrl,
|
|
[int]$Override = 0,
|
|
[int]$MaxAttempts = 5
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# The write is Entra-authorized; the read below stays anonymous (public container).
|
|
$authHeaders = & (Join-Path $PSScriptRoot 'Get-StorageAuthHeaders.ps1')
|
|
|
|
function Get-HttpStatus {
|
|
param($ErrorRecord)
|
|
if ($ErrorRecord.Exception.PSObject.Properties['Response'] -and $ErrorRecord.Exception.Response) {
|
|
return [int]$ErrorRecord.Exception.Response.StatusCode
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function Read-Counter {
|
|
try {
|
|
$resp = Invoke-WebRequest -Uri $CounterUrl -Method Get -UseBasicParsing
|
|
} catch {
|
|
if ((Get-HttpStatus $_) -eq 404) {
|
|
throw "Counter blob not found at $CounterUrl. Seed it once with the last used build number: PUT the plain integer with 'x-ms-blob-type: BlockBlob' and 'If-None-Match: *'. See internal/docs/versioning.md."
|
|
}
|
|
throw
|
|
}
|
|
$raw = if ($resp.Content -is [byte[]]) { [System.Text.Encoding]::UTF8.GetString($resp.Content) } else { [string]$resp.Content }
|
|
# If-Match requires the quoted ETag form; PowerShell strips the quotes on read.
|
|
$etag = '"' + ([string]($resp.Headers['ETag'] | Select-Object -First 1)).Trim('"') + '"'
|
|
return [pscustomobject]@{ Value = [int]$raw.Trim(); ETag = $etag }
|
|
}
|
|
|
|
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
|
$counter = Read-Counter
|
|
|
|
if ($Override -gt 0 -and $Override -le $counter.Value) {
|
|
Write-Host " Build number $Override <= counter $($counter.Value) -- already reserved, counter blob unchanged"
|
|
return $Override
|
|
}
|
|
$target = if ($Override -gt 0) { $Override } else { $counter.Value + 1 }
|
|
|
|
try {
|
|
$headers = $authHeaders + @{ 'x-ms-blob-type' = 'BlockBlob'; 'If-Match' = $counter.ETag }
|
|
Invoke-RestMethod -Uri $CounterUrl -Method Put -Headers $headers -Body "$target" -ContentType 'text/plain' | Out-Null
|
|
Write-Host " Reserved build number $target (CAS write ok, attempt $attempt)"
|
|
return $target
|
|
} catch {
|
|
if ((Get-HttpStatus $_) -eq 412) {
|
|
Write-Host " CAS conflict on attempt $attempt (another build reserved concurrently) -- retrying"
|
|
Start-Sleep -Seconds ($attempt * 2)
|
|
} else {
|
|
throw
|
|
}
|
|
}
|
|
}
|
|
|
|
throw "Could not reserve a build number after $MaxAttempts CAS attempts"
|