diff --git a/.github/scripts/Get-StorageAuthHeaders.ps1 b/.github/scripts/Get-StorageAuthHeaders.ps1
new file mode 100644
index 00000000..ca42620a
--- /dev/null
+++ b/.github/scripts/Get-StorageAuthHeaders.ps1
@@ -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'
+}
diff --git a/.github/scripts/Publish-BootstrapScripts.ps1 b/.github/scripts/Publish-BootstrapScripts.ps1
index 688d0ea0..6c1b1a6f 100644
--- a/.github/scripts/Publish-BootstrapScripts.ps1
+++ b/.github/scripts/Publish-BootstrapScripts.ps1
@@ -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"
}
diff --git a/.github/scripts/Request-BuildNumber.ps1 b/.github/scripts/Request-BuildNumber.ps1
index 1b2fa652..a7ab4603 100644
--- a/.github/scripts/Request-BuildNumber.ps1
+++ b/.github/scripts/Request-BuildNumber.ps1
@@ -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 {
diff --git a/.github/workflows/build-agent.yml b/.github/workflows/build-agent.yml
index bbdbf8ec..e1b8ce37 100644
--- a/.github/workflows/build-agent.yml
+++ b/.github/workflows/build-agent.yml
@@ -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 '(\d+\.\d+)') {
@@ -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"
}
diff --git a/.github/workflows/build-msi.yml b/.github/workflows/build-msi.yml
index a0c5b821..cf6f46a6 100644
--- a/.github/workflows/build-msi.yml
+++ b/.github/workflows/build-msi.yml
@@ -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"
}
diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml
index 1338d7e5..0de02b84 100644
--- a/.github/workflows/deploy-backend.yml
+++ b/.github/workflows/deploy-backend.yml
@@ -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 '(\d+\.\d+)') {
@@ -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
diff --git a/.github/workflows/deploy-mcp.yml b/.github/workflows/deploy-mcp.yml
index f29a18cc..7ca211cd 100644
--- a/.github/workflows/deploy-mcp.yml
+++ b/.github/workflows/deploy-mcp.yml
@@ -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
diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml
index d98620ef..386f45b8 100644
--- a/.github/workflows/deploy-web.yml
+++ b/.github/workflows/deploy-web.yml
@@ -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
diff --git a/.github/workflows/publish-scripts.yml b/.github/workflows/publish-scripts.yml
index 47016900..789b841e 100644
--- a/.github/workflows/publish-scripts.yml
+++ b/.github/workflows/publish-scripts.yml
@@ -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
diff --git a/infra b/infra
index 2c2687b6..55a8eaf9 160000
--- a/infra
+++ b/infra
@@ -1 +1 @@
-Subproject commit 2c2687b6abf35ef6580d630b236af403098b8d36
+Subproject commit 55a8eaf9290c3cf1ee574593789d300a89876123
diff --git a/internal b/internal
index 41d3f151..702dbb53 160000
--- a/internal
+++ b/internal
@@ -1 +1 @@
-Subproject commit 41d3f15126df53b5ca497336bf1c93e1f5718743
+Subproject commit 702dbb53dca97d5f3e80f0a8dbd8619f273a8de5