ci: workflows authorize Azure Storage with the federated identity, not SAS secrets (D-222)

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.
This commit is contained in:
Oliver Kieselbach
2026-09-08 17:38:14 +02:00
parent 01bc35aa97
commit e4a5a6654d
11 changed files with 131 additions and 61 deletions
@@ -0,0 +1,51 @@
<#
.SYNOPSIS
Returns the request headers that authorize an Azure Storage REST call with Microsoft Entra.
.DESCRIPTION
One place for the storage OAuth contract shared by every publisher in this repo -- the
agent ZIP and MSI, the customer bootstrap scripts, the version manifests and the build
counters. Resource id, the az invocation, the service version and the failure message
live here so they cannot drift between call sites.
Identity comes from whatever `az login` established: the federated GitHub OIDC principal
in CI (azure/login, subject repo:okieselbach/AutopilotMonitor:ref:refs/heads/main), the
operator's own account for the local build. No secret is read, stored or passed around.
x-ms-version is MANDATORY here, not decoration: Entra-authorized requests are rejected
below 2017-11-09, and Table requests without a SAS must carry the header at all. 2021-08-06
is old enough to be deployed in every region and new enough for everything we send.
Callers merge the result into their own headers -- `$auth + @{ ... }` yields a new
hashtable per request, so nothing is shared across uploads:
$auth = & "$PSScriptRoot/Get-StorageAuthHeaders.ps1"
Invoke-RestMethod -Uri $url -Method Put -Headers ($auth + @{ 'x-ms-blob-type' = 'BlockBlob' }) -Body $bytes
Acquire once per step and reuse: every call shells out to az (~1s) and the token is valid
for the lifetime of a job.
.OUTPUTS
[hashtable] Authorization + x-ms-version.
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
# The data-plane resource, not management.azure.com -- a management token is accepted by
# neither the blob nor the table endpoint.
$token = az account get-access-token --resource https://storage.azure.com --query accessToken -o tsv 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($token)) {
throw ('Could not acquire an Azure Storage token. In CI this means the azure/login step ' +
'is missing or its federated credential does not cover this ref; locally, run ' +
'`az login`. A token alone is not enough: the principal also needs Storage Blob ' +
'Data Contributor on the container it writes, and Storage Table Data Contributor ' +
'on the table -- a missing role shows up later as HTTP 403 AuthorizationPermissionMismatch.')
}
return @{
'Authorization' = "Bearer $token"
'x-ms-version' = '2021-08-06'
}
+10 -19
View File
@@ -31,14 +31,10 @@
lose its agent fields) and AdminConfiguration.LatestBootstrapV2ScriptVersion.
6. Verify through the alias -- re-download each blob and compare SHA-256.
.PARAMETER SasToken
Container SAS for the eu storage account with write permission. Leading '?' tolerated.
.PARAMETER LegacySasToken
Container SAS for the legacy account. Optional -- a mirror failure warns, never fails.
.PARAMETER TableSasToken
Table SAS for AdminConfiguration. Optional -- omitted or 'XXX' skips the oracle update.
The legacy account lives in a different tenant, so the ambient Entra identity cannot
reach it; this is the only credential the publisher still takes.
.PARAMETER RepoRoot
Repository root. Defaults to the parent of .github/scripts.
@@ -50,13 +46,11 @@
./Publish-BootstrapScripts.ps1 -DryRun
.EXAMPLE
./Publish-BootstrapScripts.ps1 -SasToken $env:SAS -TableSasToken $env:TABLE_SAS
./Publish-BootstrapScripts.ps1 -LegacySasToken $env:LEGACY_SAS
#>
[CmdletBinding()]
param(
[string]$SasToken,
[string]$LegacySasToken,
[string]$TableSasToken,
[string]$RepoRoot,
[switch]$DryRun
)
@@ -66,9 +60,6 @@ $ErrorActionPreference = 'Stop'
if (-not $RepoRoot) {
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
}
if (-not $DryRun -and [string]::IsNullOrWhiteSpace($SasToken)) {
throw 'SasToken is required unless -DryRun is used.'
}
$ContainerUrl = 'https://autopilotmonitoreu.blob.core.windows.net/agent'
$LegacyContainerUrl = 'https://autopilotmonitor.blob.core.windows.net/agent'
@@ -218,7 +209,10 @@ foreach ($extra in $extraSources) {
}
$legacySas = if ($LegacySasToken) { $LegacySasToken.TrimStart('?') } else { '' }
$writeSas = if ($SasToken) { $SasToken.TrimStart('?') } else { '' }
# Acquired lazily: a dry run must stay runnable without any Azure login -- the PR gate
# (bootstrap-script-gates.yml) executes this script with -DryRun and never logs in.
$authHeaders = if ($DryRun) { $null } else { & (Join-Path $PSScriptRoot 'Get-StorageAuthHeaders.ps1') }
foreach ($item in $publishSet) {
# Explicit type, not a convenience: an Object[] body is uploaded as space-separated
@@ -242,7 +236,7 @@ foreach ($item in $publishSet) {
# fresh manifest would fail the bootstrap SHA check -- this keeps that class of bug
# impossible even if caching is ever re-enabled.
$headers = @{ 'x-ms-blob-type' = 'BlockBlob'; 'Content-Type' = $ScriptContentType; 'x-ms-blob-cache-control' = 'no-cache' }
Invoke-RestMethod -Uri "$ContainerUrl/$($item.BlobName)?$writeSas" -Method Put -Headers $headers -Body $bytes | Out-Null
Invoke-RestMethod -Uri "$ContainerUrl/$($item.BlobName)" -Method Put -Headers ($authHeaders + $headers) -Body $bytes | Out-Null
# Read straight back from the blob (authoritative, no CDN in the way) before touching the
# mirror or the next file. The alias verification at the end would catch a bad body too,
@@ -290,7 +284,7 @@ if (-not $DryRun) {
'x-ms-blob-cache-control' = 'no-cache'
'If-Match' = $etag
}
Invoke-RestMethod -Uri "$ContainerUrl/version.json?$writeSas" -Method Put -Headers $manifestHeaders -Body $body | Out-Null
Invoke-RestMethod -Uri "$ContainerUrl/version.json" -Method Put -Headers ($authHeaders + $manifestHeaders) -Body $body | Out-Null
Write-Host "version.json bootstrapVersion -> $scriptVersion (agent fields untouched)"
if ($legacySas) {
@@ -310,11 +304,8 @@ if (-not $DryRun) {
}
}
$tableSas = if ($TableSasToken) { $TableSasToken.TrimStart('?') } else { '' }
if ($DryRun) {
Write-Host " [dry-run] would set AdminConfiguration.LatestBootstrapV2ScriptVersion = $scriptVersion"
} elseif ([string]::IsNullOrEmpty($tableSas) -or $tableSas -eq 'XXX') {
Write-Host 'SKIPPED: AdminConfiguration update -- set AZURE_TABLE_ADMIN_SAS_TOKEN to enable the portal version oracle'
} else {
$tableHeaders = @{
'Content-Type' = 'application/json'
@@ -322,7 +313,7 @@ if ($DryRun) {
'If-Match' = '*'
}
$tableBody = @{ LatestBootstrapV2ScriptVersion = $scriptVersion } | ConvertTo-Json
Invoke-RestMethod -Uri "$TableUrl`?$tableSas" -Method Merge -Headers $tableHeaders -Body $tableBody | Out-Null
Invoke-RestMethod -Uri $TableUrl -Method Merge -Headers ($authHeaders + $tableHeaders) -Body $tableBody | Out-Null
Write-Host "AdminConfiguration.LatestBootstrapV2ScriptVersion = $scriptVersion"
}
+8 -11
View File
@@ -12,7 +12,8 @@
A failed build burns a number, which is harmless -- uniqueness matters,
density does not.
The blob is public-read; only the write needs the container SAS.
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
@@ -20,10 +21,7 @@
overwrite each other's versioned artifact.
.PARAMETER CounterUrl
Full URL of the counter blob, without SAS.
.PARAMETER SasToken
Container SAS with create+write permission. A leading '?' is tolerated.
Full URL of the counter blob.
.PARAMETER Override
Explicit build number instead of counter+1. If it is not greater than the
@@ -37,20 +35,19 @@
[int] the reserved build number. Progress goes to the host, not the pipeline.
.EXAMPLE
$n = ./Request-BuildNumber.ps1 -CounterUrl $url -SasToken $sas
$n = ./Request-BuildNumber.ps1 -CounterUrl $url
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$CounterUrl,
[Parameter(Mandatory = $true)][string]$SasToken,
[int]$Override = 0,
[int]$MaxAttempts = 5
)
$ErrorActionPreference = 'Stop'
# Tolerate both secret formats ("sp=cw&..." and "?sp=cw&...").
$writeUrl = $CounterUrl + '?' + $SasToken.TrimStart('?')
# The write is Entra-authorized; the read below stays anonymous (public container).
$authHeaders = & (Join-Path $PSScriptRoot 'Get-StorageAuthHeaders.ps1')
function Get-HttpStatus {
param($ErrorRecord)
@@ -85,8 +82,8 @@ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$target = if ($Override -gt 0) { $Override } else { $counter.Value + 1 }
try {
$headers = @{ 'x-ms-blob-type' = 'BlockBlob'; 'If-Match' = $counter.ETag }
Invoke-RestMethod -Uri $writeUrl -Method Put -Headers $headers -Body "$target" -ContentType 'text/plain' | Out-Null
$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 {
+19 -14
View File
@@ -67,6 +67,18 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v7
# Every storage write below (counter blob, agent ZIP, manifests, AdminConfiguration)
# is authorized by this federated identity -- no SAS secret. One login covers the whole
# job: the federated credential is bound to the ref, and a release run takes ~2 minutes,
# well inside the token lifetime. Only the legacy mirror still needs a SAS: that account
# lives in the pre-migration tenant, where this principal has no identity.
- name: Login to Azure (OIDC)
uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# SDK 10 = single build toolchain (parity with local build.ps1 / global.json).
- name: Setup .NET
uses: actions/setup-dotnet@v6
@@ -85,7 +97,6 @@ jobs:
# build_number could otherwise break out of the quotes and run arbitrary code.
env:
BUILD_NUMBER_OVERRIDE: ${{ inputs.build_number }}
AGENT_SAS_TOKEN: ${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}
run: |
$override = 0
if ($env:BUILD_NUMBER_OVERRIDE -ne '') {
@@ -96,7 +107,7 @@ jobs:
}
$counterUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent/buildcounter-v$($env:LINE_MAJOR).txt"
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl $counterUrl -SasToken $env:AGENT_SAS_TOKEN -Override $override
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl $counterUrl -Override $override
$csproj = Get-Content $env:CSPROJ_PATH -Raw
if ($csproj -match '<VersionPrefix>(\d+\.\d+)</VersionPrefix>') {
@@ -224,7 +235,7 @@ jobs:
shell: pwsh
run: |
$containerUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent"
$sasToken = "${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}".TrimStart('?')
$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
@@ -234,7 +245,6 @@ jobs:
function Send-Blob {
param([string]$Local, [string]$BlobName, [string]$ContentType)
$url = "$containerUrl/$BlobName`?$sasToken"
$bytes = [System.IO.File]::ReadAllBytes($Local)
# no-cache on EVERY blob: they all rotate in place (even the per-line ZIP is
# overwritten each build), and the download alias fronts them with Front Door.
@@ -248,7 +258,7 @@ jobs:
$md5.Dispose()
}
Write-Host "Uploading $BlobName ($([math]::Round($bytes.Length / 1MB, 2)) MB)..."
Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $bytes
Invoke-RestMethod -Uri "$containerUrl/$BlobName" -Method Put -Headers ($authHeaders + $headers) -Body $bytes
Write-Host " uploaded $BlobName"
if (-not [string]::IsNullOrEmpty($legacySasToken)) {
try {
@@ -275,35 +285,30 @@ jobs:
# (dev render, version-bump guard, bootstrapVersion oracles, alias verification).
# Runs AFTER version.json so its bootstrapVersion read sees this build's manifest.
./.github/scripts/Publish-BootstrapScripts.ps1 `
-SasToken '${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}' `
-LegacySasToken '${{ secrets.AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN }}' `
-TableSasToken '${{ secrets.AZURE_TABLE_ADMIN_SAS_TOKEN }}'
-LegacySasToken '${{ secrets.AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN }}'
}
# Update AdminConfiguration LatestAgentV2* -- ONLY on stable cutover. The hash-oracle
# must never point at a build that is not actually in the stable namespace.
# LatestBootstrapV2ScriptVersion is NOT set here: it belongs to the script publisher
# above, which also owns it on script-only pushes. One writer per field.
$tableSasToken = "${{ secrets.AZURE_TABLE_ADMIN_SAS_TOKEN }}".TrimStart('?')
if ('${{ inputs.publish_as_stable }}' -ne 'true') {
Write-Host "SKIPPED: AdminConfiguration update (versioned-only run -- stable namespace unchanged)"
} elseif ([string]::IsNullOrEmpty($tableSasToken) -or $tableSasToken -eq "XXX") {
Write-Host "SKIPPED: Table SAS token not configured -- set AZURE_TABLE_ADMIN_SAS_TOKEN secret to enable backend hash-oracle"
} else {
$sha256 = "${{ steps.artifact.outputs.sha256 }}"
$exeSha256 = "${{ steps.artifact.outputs.exe_sha256 }}"
$agentVersion = "${{ steps.version.outputs.version }}"
$tableUrl = "https://autopilotmonitoreu.table.core.windows.net/AdminConfiguration(PartitionKey='GlobalConfig',RowKey='config')?$tableSasToken"
$tableUrl = "https://autopilotmonitoreu.table.core.windows.net/AdminConfiguration(PartitionKey='GlobalConfig',RowKey='config')"
$tableBody = @{
LatestAgentV2Version = $agentVersion
LatestAgentV2Sha256 = $sha256
LatestAgentV2ExeSha256 = $exeSha256
}
Invoke-RestMethod -Uri $tableUrl -Method Merge -Headers @{
Invoke-RestMethod -Uri $tableUrl -Method Merge -Headers ($authHeaders + @{
"Content-Type" = "application/json"
"Accept" = "application/json;odata=nometadata"
"If-Match" = "*"
} -Body ($tableBody | ConvertTo-Json)
}) -Body ($tableBody | ConvertTo-Json)
Write-Host "AdminConfiguration updated: V2 version=$agentVersion, sha256=$sha256, exeSha256=$exeSha256"
}
+10 -2
View File
@@ -62,6 +62,14 @@ jobs:
Write-Host "MSI version (from Package.wxs): $version"
"version=$version" >> $env:GITHUB_OUTPUT
# The blob upload below is authorized by this federated identity, not by a SAS.
- name: Login to Azure (OIDC)
uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Install WiX toolset
run: dotnet tool install --global wix --version "5.*"
@@ -84,7 +92,7 @@ jobs:
shell: pwsh
run: |
$containerUrl = "https://autopilotmonitoreu.blob.core.windows.net/agent"
$sasToken = "${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}".TrimStart('?')
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
$version = "${{ steps.version.outputs.version }}"
# Same Send-Blob contract as build-agent.yml: no-cache on every blob because the
@@ -102,7 +110,7 @@ jobs:
$headers["Content-MD5"] = [Convert]::ToBase64String($md5.ComputeHash($bytes))
$md5.Dispose()
Write-Host "Uploading $BlobName ($([math]::Round($bytes.Length / 1KB)) KB)..."
Invoke-RestMethod -Uri "$containerUrl/$BlobName`?$sasToken" -Method Put -Headers $headers -Body $bytes
Invoke-RestMethod -Uri "$containerUrl/$BlobName" -Method Put -Headers ($authHeaders + $headers) -Body $bytes
Write-Host " uploaded $BlobName"
}
+4 -4
View File
@@ -41,7 +41,7 @@ jobs:
id: version
shell: pwsh
run: |
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "${{ env.VERSIONS_CONTAINER }}/backend.txt" -SasToken '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "${{ env.VERSIONS_CONTAINER }}/backend.txt"
$csproj = Get-Content ${{ env.CSPROJ_PATH }} -Raw
if ($csproj -notmatch '<VersionPrefix>(\d+\.\d+)</VersionPrefix>') {
@@ -115,9 +115,9 @@ jobs:
runId = '${{ github.run_id }}'
} | ConvertTo-Json
$sas = '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'.TrimStart('?')
$url = "${{ env.VERSIONS_CONTAINER }}/backend.json?$sas"
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
$url = "${{ env.VERSIONS_CONTAINER }}/backend.json"
# no-cache: rotating pointer, must never be served stale by any fronting cache.
Invoke-RestMethod -Uri $url -Method Put -Headers @{ 'x-ms-blob-type' = 'BlockBlob'; 'x-ms-blob-cache-control' = 'no-cache' } -Body $manifest -ContentType 'application/json' | Out-Null
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"
Write-Host $manifest
+4 -4
View File
@@ -59,7 +59,7 @@ jobs:
id: version
shell: pwsh
run: |
$buildNumber = & ./.github/scripts/Request-BuildNumber.ps1 -CounterUrl "${{ env.VERSIONS_CONTAINER }}/mcp.txt" -SasToken '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'
$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 '.'
@@ -144,9 +144,9 @@ jobs:
docsCommit = '${{ steps.docs.outputs.sha }}'.Substring(0, 7)
} | ConvertTo-Json
$sas = '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'.TrimStart('?')
$url = "${{ env.VERSIONS_CONTAINER }}/mcp.json?$sas"
$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 @{ 'x-ms-blob-type' = 'BlockBlob'; 'x-ms-blob-cache-control' = 'no-cache' } -Body $manifest -ContentType 'application/json' | Out-Null
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
+12 -3
View File
@@ -40,6 +40,15 @@ jobs:
lfs: false
fetch-depth: 0 # Full history needed for git-based sitemap lastmod dates
# Only the version manifest at the very end needs Azure; logging in here rather than
# next to that step keeps a credential problem from surfacing after a live deploy.
- name: Login to Azure (OIDC)
uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# 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.
@@ -169,9 +178,9 @@ jobs:
docsCommit = '${{ steps.docs.outputs.sha }}'.Substring(0, 7)
} | ConvertTo-Json
$sas = '${{ secrets.AZURE_BLOB_VERSIONS_SAS_TOKEN }}'.TrimStart('?')
$url = "${{ env.VERSIONS_CONTAINER }}/web.json?$sas"
$authHeaders = & ./.github/scripts/Get-StorageAuthHeaders.ps1
$url = "${{ env.VERSIONS_CONTAINER }}/web.json"
# no-cache: rotating pointer, must never be served stale by any fronting cache.
Invoke-RestMethod -Uri $url -Method Put -Headers @{ 'x-ms-blob-type' = 'BlockBlob'; 'x-ms-blob-cache-control' = 'no-cache' } -Body $manifest -ContentType 'application/json' | Out-Null
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"
Write-Host $manifest
+11 -2
View File
@@ -26,6 +26,7 @@ on:
permissions:
contents: read
id-token: write # OIDC federated login for the storage writes (secret-less)
# Shared with build-agent.yml: a script publish and an agent release both touch
# version.json, and the bootstrapVersion read-modify-write must not interleave with the
@@ -45,12 +46,20 @@ jobs:
steps:
- uses: actions/checkout@v7
# Authorizes the eu blob writes and the AdminConfiguration merge. The gates job above
# runs the same publisher with -DryRun and deliberately without a login -- a dry run
# must never need Azure credentials.
- name: Login to Azure (OIDC)
uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Publish bootstrap + customer scripts
shell: pwsh
run: |
$dryRun = '${{ inputs.dry_run }}' -eq 'true'
./.github/scripts/Publish-BootstrapScripts.ps1 `
-SasToken '${{ secrets.AZURE_BLOB_AGENT_SAS_TOKEN }}' `
-LegacySasToken '${{ secrets.AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN }}' `
-TableSasToken '${{ secrets.AZURE_TABLE_ADMIN_SAS_TOKEN }}' `
-DryRun:$dryRun
+1 -1
Submodule infra updated: 2c2687b6ab...55a8eaf929