From 43a74a1cf72d7e290de655a218ad8c201004398f Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Fri, 28 Aug 2026 21:38:40 -0300 Subject: [PATCH 1/2] Rename Azure.Cli package id to azx.cli --- .github/workflows/build.yml | 4 ++-- .github/workflows/publish.yml | 22 +++++++++--------- CONTEXT.md | 4 ++-- docs/adr/0002-nupkg-version-is-upstream.md | 2 +- readme.md | 2 +- src/Azure.Cli/Azure.Cli.csproj | 10 ++++---- src/Azure.Cli/Cli.cs | 2 +- src/Azure.Cli/build/Azure.Cli.targets | 3 --- src/Azure.Cli/build/azx.cli.targets | 3 +++ .../{Azure.Cli.targets => azx.cli.targets} | 2 +- src/Tests/PackTests.cs | 23 +++++++++++-------- src/Tests/ReleaseTests.cs | 4 +++- src/azx/azx.csproj | 4 ++-- src/azx/nuget.config | 4 ++-- 14 files changed, 48 insertions(+), 41 deletions(-) delete mode 100644 src/Azure.Cli/build/Azure.Cli.targets create mode 100644 src/Azure.Cli/build/azx.cli.targets rename src/Azure.Cli/buildTransitive/{Azure.Cli.targets => azx.cli.targets} (94%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d15d31e..731bd13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,9 +74,9 @@ jobs: run: | $ErrorActionPreference = 'Stop' $script = Join-Path $pwd 'src/Azure.Cli/unix-exec.ps1' - $azure = Get-ChildItem bin -Filter "Azure.Cli.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 + $azure = Get-ChildItem bin -Filter "azx.cli.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 $azx = Get-ChildItem bin -Filter "azx.linux-x64.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 - if (-not $azure) { throw "Azure.Cli.linux-x64 nupkg not found" } + if (-not $azure) { throw "azx.cli.linux-x64 nupkg not found" } if (-not $azx) { throw "azx.linux-x64 nupkg not found" } foreach ($pkg in @($azure, $azx)) { & pwsh -NoProfile -File $script -Nupkg $pkg.FullName -Assert diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b6c6bd5..f2d9057 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -70,9 +70,9 @@ jobs: run: | $ErrorActionPreference = 'Stop' $script = Join-Path $pwd 'src/Azure.Cli/unix-exec.ps1' - $azure = Get-ChildItem bin -Filter "Azure.Cli.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 + $azure = Get-ChildItem bin -Filter "azx.cli.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 $azx = Get-ChildItem bin -Filter "azx.${{ matrix.rid }}.*.nupkg" | Where-Object Name -notlike '*.symbols.*' | Select-Object -First 1 - if (-not $azure) { throw "Azure.Cli.${{ matrix.rid }} nupkg not found" } + if (-not $azure) { throw "azx.cli.${{ matrix.rid }} nupkg not found" } if (-not $azx) { throw "azx.${{ matrix.rid }} nupkg not found" } foreach ($pkg in @($azure, $azx)) { & pwsh -NoProfile -File $script -Nupkg $pkg.FullName -Assert @@ -84,9 +84,9 @@ jobs: with: name: package-${{ matrix.rid }} # Only RID nupkgs. Pointer nupkgs are produced on every OS; merging them - # into one Azure.Cli.{version}.nupkg corrupts the zip (NUGET 400 / sleet EOCD). + # into one azx.cli.{version}.nupkg corrupts the zip (NUGET 400 / sleet EOCD). path: | - bin/Azure.Cli.${{ matrix.rid }}.*.nupkg + bin/azx.cli.${{ matrix.rid }}.*.nupkg bin/azx.${{ matrix.rid }}.*.nupkg retention-days: 30 if-no-files-found: error @@ -97,7 +97,7 @@ jobs: with: name: package-pointer path: | - bin/Azure.Cli.${{ env.Version }}.nupkg + bin/azx.cli.${{ env.Version }}.nupkg bin/azx.${{ env.Version }}.nupkg retention-days: 30 if-no-files-found: error @@ -161,12 +161,12 @@ jobs: } $names = Get-ChildItem -File -Filter *.nupkg | ForEach-Object Name foreach ($need in @( - "Azure.Cli.$env:Version.nupkg", - "Azure.Cli.win-x64.$env:Version.nupkg", - "Azure.Cli.linux-x64.$env:Version.nupkg", - "Azure.Cli.linux-arm64.$env:Version.nupkg", - "Azure.Cli.osx-x64.$env:Version.nupkg", - "Azure.Cli.osx-arm64.$env:Version.nupkg", + "azx.cli.$env:Version.nupkg", + "azx.cli.win-x64.$env:Version.nupkg", + "azx.cli.linux-x64.$env:Version.nupkg", + "azx.cli.linux-arm64.$env:Version.nupkg", + "azx.cli.osx-x64.$env:Version.nupkg", + "azx.cli.osx-arm64.$env:Version.nupkg", "azx.$env:Version.nupkg", "azx.win-x64.$env:Version.nupkg", "azx.linux-x64.$env:Version.nupkg", diff --git a/CONTEXT.md b/CONTEXT.md index e8938d4..fe25f84 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -5,11 +5,11 @@ NuGet pointer and per-RID payloads that put a working Azure CLI next to a .NET a ## Language **Pointer**: -The `Azure.Cli` nupkg: `Cli` plus `runtime.json` mapping each supported RID to a RID package. +The `azx.cli` nupkg: `Cli` plus `runtime.json` mapping each supported RID to a RID package. _Avoid_: metapackage, tool package, native package **RID package**: -`Azure.Cli.{rid}` — one nupkg, one RID, one Payload. +`azx.cli.{rid}` — one nupkg, one RID, one Payload. _Avoid_: native package, runtime pack, sidecar package **Payload**: diff --git a/docs/adr/0002-nupkg-version-is-upstream.md b/docs/adr/0002-nupkg-version-is-upstream.md index 64cffb4..b4128f3 100644 --- a/docs/adr/0002-nupkg-version-is-upstream.md +++ b/docs/adr/0002-nupkg-version-is-upstream.md @@ -1,3 +1,3 @@ # Nupkg version is Upstream -`Azure.Cli` and `azx` use the Azure CLI version (`2.89.1`) so `ndnx azx@2.89.1` is that CLI. The pin is `azure-cli.version`. CI dogfood stays `42.42.*`. Packaging-only republishes of the same CLI use a SemVer label, not a different major. +`azx.cli` and `azx` use the Azure CLI version (`2.89.1`) so `ndnx azx@2.89.1` is that CLI. The pin is `azure-cli.version`. CI dogfood stays `42.42.*`. Packaging-only republishes of the same CLI use a SemVer label, not a different major. diff --git a/readme.md b/readme.md index 90d5843..7e86f17 100644 --- a/readme.md +++ b/readme.md @@ -32,7 +32,7 @@ dotnet tool install -g azx #once azx --version # azx version, then az --version ``` -`PackageReference` `Azure.Cli` and pack/publish for your RID to copy `az/` next to the app. `Azure.Cli.ResolvePath()` is the Payload `az` executable. +`PackageReference` `azx.cli` and pack/publish for your RID to copy `az/` next to the app. `Azure.Cli.ResolvePath()` is the Payload `az` executable. --- diff --git a/src/Azure.Cli/Azure.Cli.csproj b/src/Azure.Cli/Azure.Cli.csproj index c51c963..8a3a44b 100644 --- a/src/Azure.Cli/Azure.Cli.csproj +++ b/src/Azure.Cli/Azure.Cli.csproj @@ -1,10 +1,10 @@ net10.0 - Azure.Cli + azx.cli Azure Azure.Cli - Azure CLI payload for .NET: PackageReference Azure.Cli and publish/pack for your RID to get az/ plus Azure.Cli.ResolvePath. + Azure CLI payload for .NET: PackageReference azx.cli and publish/pack for your RID to get az/ plus Azure.Cli.ResolvePath. true true win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 @@ -30,10 +30,10 @@ - + - - + + diff --git a/src/Azure.Cli/Cli.cs b/src/Azure.Cli/Cli.cs index 2086660..7e04c3c 100644 --- a/src/Azure.Cli/Cli.cs +++ b/src/Azure.Cli/Cli.cs @@ -17,7 +17,7 @@ public static string ResolvePath(string? baseDirectory = null) if (!File.Exists(path)) { throw new FileNotFoundException( - $"Azure CLI payload was not found at '{path}'. PackageReference Azure.Cli and publish/pack for your RID.", + $"Azure CLI payload was not found at '{path}'. PackageReference azx.cli and publish/pack for your RID.", path); } diff --git a/src/Azure.Cli/build/Azure.Cli.targets b/src/Azure.Cli/build/Azure.Cli.targets deleted file mode 100644 index 69908a6..0000000 --- a/src/Azure.Cli/build/Azure.Cli.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Azure.Cli/build/azx.cli.targets b/src/Azure.Cli/build/azx.cli.targets new file mode 100644 index 0000000..582cb1e --- /dev/null +++ b/src/Azure.Cli/build/azx.cli.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/Azure.Cli/buildTransitive/Azure.Cli.targets b/src/Azure.Cli/buildTransitive/azx.cli.targets similarity index 94% rename from src/Azure.Cli/buildTransitive/Azure.Cli.targets rename to src/Azure.Cli/buildTransitive/azx.cli.targets index 8c0cc4a..0c022a2 100644 --- a/src/Azure.Cli/buildTransitive/Azure.Cli.targets +++ b/src/Azure.Cli/buildTransitive/azx.cli.targets @@ -14,7 +14,7 @@ <_AzureCliVersion>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..')))) - <_AzureCliPackageFolder>$([System.String]::Copy('azure.cli.$(_AzureCliRid)').ToLowerInvariant()) + <_AzureCliPackageFolder>$([System.String]::Copy('$(MSBuildThisFileName).$(_AzureCliRid)').ToLowerInvariant()) $(NuGetPackageRoot)$(_AzureCliPackageFolder)\$(_AzureCliVersion)\az diff --git a/src/Tests/PackTests.cs b/src/Tests/PackTests.cs index f5aa5ff..20016db 100644 --- a/src/Tests/PackTests.cs +++ b/src/Tests/PackTests.cs @@ -23,12 +23,15 @@ public void Pointer_and_rid_csproj_use_pack_split() Assert.Contains("src/azx/azx.csproj", slnx); var azureCli = File.ReadAllText(Path.Combine(repo, "src", "Azure.Cli", "Azure.Cli.csproj")); - Assert.Contains("Azure.Cli", azureCli); + Assert.Contains("azx.cli", azureCli); + Assert.Contains("Azure.Cli", azureCli); + Assert.Contains("Azure", azureCli); Assert.Contains("win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64", azureCli); Assert.DoesNotContain("NuGetizer", azureCli, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("az\", consumer.Replace('/', '\\')); Assert.DoesNotContain("runtimes/$(RuntimeIdentifier)/native/", consumer); @@ -72,13 +75,15 @@ public void Pointer_and_rid_csproj_use_pack_split() Assert.Contains("true", azx); Assert.Contains("azx", azx); Assert.Contains("win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64", azx); - Assert.Contains("""""", azx); + Assert.Contains("""""", azx); Assert.DoesNotContain("NuGetizer", azx, StringComparison.OrdinalIgnoreCase); Assert.Contains("Readme", azx); var nuget = File.ReadAllText(Path.Combine(repo, "src", "azx", "nuget.config")); Assert.Contains("key=\"local\"", nuget); Assert.Contains("../../bin", nuget); - Assert.Contains("Azure.Cli", nuget); + Assert.Contains("pattern=\"azx.cli\"", nuget); + Assert.Contains("pattern=\"azx.cli.*\"", nuget); + Assert.DoesNotContain("Azure.Cli", nuget); Assert.DoesNotContain("win-arm64", azx); Assert.DoesNotContain("azx.$(RuntimeIdentifier)", azx); @@ -131,8 +136,8 @@ public void WriteAzureCliRuntimeJson_maps_five_rids() { var range = runtimes .GetProperty(rid) - .GetProperty("Azure.Cli") - .GetProperty("Azure.Cli." + rid) + .GetProperty("azx.cli") + .GetProperty("azx.cli." + rid) .GetString(); Assert.False(string.IsNullOrWhiteSpace(range)); Assert.StartsWith("[", range); @@ -149,7 +154,7 @@ public void Packed_nupkgs_have_pointer_and_rid_layout() if (!Directory.Exists(bin)) return; - var nupkgs = Directory.GetFiles(bin, "Azure.Cli*.nupkg") + var nupkgs = Directory.GetFiles(bin, "azx.cli*.nupkg") .Where(f => !f.Contains(".symbols.", StringComparison.OrdinalIgnoreCase)) .ToArray(); if (nupkgs.Length == 0) @@ -162,7 +167,7 @@ public void Packed_nupkgs_have_pointer_and_rid_layout() var names = ZipNames(pointer); Assert.Contains(names, n => n == "runtime.json" || n == "runtime.json/"); Assert.Contains(names, n => n.Replace('\\', '/').StartsWith("lib/", StringComparison.Ordinal)); - Assert.Contains(names, n => n.Replace('\\', '/').Contains("buildTransitive/Azure.Cli.targets", StringComparison.Ordinal)); + Assert.Contains(names, n => n.Replace('\\', '/').Contains("buildTransitive/azx.cli.targets", StringComparison.Ordinal)); Assert.DoesNotContain(names, n => n.Replace('\\', '/').StartsWith("az/", StringComparison.Ordinal)); } diff --git a/src/Tests/ReleaseTests.cs b/src/Tests/ReleaseTests.cs index 84c8198..034c16d 100644 --- a/src/Tests/ReleaseTests.cs +++ b/src/Tests/ReleaseTests.cs @@ -50,7 +50,9 @@ public void Publish_version_does_not_double_append_preview() Assert.DoesNotContain("Expand-Archive", publish); Assert.DoesNotContain("tar -xf $nupkg", publish); Assert.Contains("package-pointer", publish); - Assert.Contains("bin/Azure.Cli.${{ matrix.rid }}.*.nupkg", publish); + Assert.Contains("bin/azx.cli.${{ matrix.rid }}.*.nupkg", publish); + Assert.Contains("bin/azx.cli.${{ env.Version }}.nupkg", publish); + Assert.Contains("azx.cli.$env:Version.nupkg", publish); } static string FindRepoRoot() diff --git a/src/azx/azx.csproj b/src/azx/azx.csproj index 23db7b2..cf2242f 100644 --- a/src/azx/azx.csproj +++ b/src/azx/azx.csproj @@ -19,12 +19,12 @@ - + diff --git a/src/azx/nuget.config b/src/azx/nuget.config index a1042d4..43ebfaf 100644 --- a/src/azx/nuget.config +++ b/src/azx/nuget.config @@ -17,8 +17,8 @@ - - + + From 55296497ebc93a7e93069bd1480663d106ddafd9 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Fri, 28 Aug 2026 21:54:23 -0300 Subject: [PATCH 2/2] fix: address CI failure in build --- src/Azure.Cli/payload.functions.ps1 | 57 ++++++++++++++++++++++++++--- src/Azure.Cli/payload.ps1 | 40 +++++++++++++++++--- src/Tests/PackTests.cs | 26 +++++++++++++ 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/Azure.Cli/payload.functions.ps1 b/src/Azure.Cli/payload.functions.ps1 index df8d321..54162ef 100644 --- a/src/Azure.Cli/payload.functions.ps1 +++ b/src/Azure.Cli/payload.functions.ps1 @@ -1,17 +1,64 @@ #Requires -Version 7 # Download/extract helpers for payload.ps1. Get-SourcesRoot is the sources unpack unit. +function Get-GitHubAuthTokens { + # In GitHub Actions prefer GITHUB_TOKEN: per-repo quota, unlike a shared GH_TOKEN PAT. + $tokens = [System.Collections.Generic.List[string]]::new() + $order = if ($env:GITHUB_ACTIONS -eq 'true') { + @($env:GITHUB_TOKEN, $env:GH_TOKEN) + } + else { + @($env:GH_TOKEN, $env:GITHUB_TOKEN) + } + foreach ($t in $order) { + if (-not [string]::IsNullOrWhiteSpace($t) -and -not $tokens.Contains($t)) { + $tokens.Add($t) + } + } + return $tokens +} + function Get-GitHubHeaders { $headers = @{ 'User-Agent' = 'azx-payload' } - if ($env:GH_TOKEN) { - $headers['Authorization'] = "Bearer $($env:GH_TOKEN)" - } - elseif ($env:GITHUB_TOKEN) { - $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" + $token = @(Get-GitHubAuthTokens) | Select-Object -First 1 + if ($token) { + $headers['Authorization'] = "Bearer $token" } return $headers } +function Invoke-GitHubRestMethod([string] $Uri) { + $attempts = [System.Collections.Generic.List[string]]::new() + foreach ($t in @(Get-GitHubAuthTokens)) { + $attempts.Add($t) + } + $attempts.Add('') # unauthenticated last; public REST still has an IP quota + + $errors = [System.Collections.Generic.List[string]]::new() + foreach ($token in $attempts) { + $headers = @{ 'User-Agent' = 'azx-payload' } + if ($token) { + $headers['Authorization'] = "Bearer $token" + } + try { + return Invoke-RestMethod -Uri $Uri -Headers $headers + } + catch { + $status = $null + try { $status = [int]$_.Exception.Response.StatusCode } catch { } + $msg = [string]$_ + $errors.Add($msg) + $retry = ($status -in 401, 403, 429) -or ($msg -match 'rate limit') -or ($msg -match 'Bad credentials') + if ($retry) { + Write-Host "GitHub API rejected request$(if ($null -ne $status) { " ($status)" }); trying next credentials" + continue + } + throw + } + } + throw "GitHub API failed for $Uri. $($errors -join ' | ')" +} + function Save-Url([string] $Url, [string] $Dest) { if (Test-Path $Dest) { Write-Host "Cached $Dest" diff --git a/src/Azure.Cli/payload.ps1 b/src/Azure.Cli/payload.ps1 index 8f58b22..97c8ed4 100644 --- a/src/Azure.Cli/payload.ps1 +++ b/src/Azure.Cli/payload.ps1 @@ -60,12 +60,42 @@ function Get-PbsTriple([string] $PayloadRid) { } function Get-PbsAsset([string] $PythonVersion, [string] $Triple) { - $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/astral-sh/python-build-standalone/releases?per_page=15' -Headers (Get-GitHubHeaders) $pattern = "^cpython-$([regex]::Escape($PythonVersion))\+.*-$([regex]::Escape($Triple))-install_only_stripped\.tar\.gz$" - foreach ($release in $releases) { - $asset = @($release.assets | Where-Object { $_.name -match $pattern }) | Select-Object -First 1 - if ($asset) { - return $asset + try { + $releases = Invoke-GitHubRestMethod 'https://api.github.com/repos/astral-sh/python-build-standalone/releases?per_page=15' + foreach ($release in @($releases)) { + if (-not $release.assets) { continue } + $asset = @($release.assets | Where-Object { $_.name -match $pattern }) | Select-Object -First 1 + if ($asset) { + return $asset + } + } + Write-Host 'No PBS asset in GitHub API releases; trying releases.atom' + } + catch { + Write-Host "PBS GitHub API lookup failed: $_" + } + + # github.com HTML/atom does not consume REST API quota. + $atomHeaders = @{ 'User-Agent' = 'azx-payload' } + $atom = Invoke-WebRequest -Uri 'https://github.com/astral-sh/python-build-standalone/releases.atom' -Headers $atomHeaders + $tags = [regex]::Matches($atom.Content, 'python-build-standalone/releases/tag/([^<"\s]+)') | + ForEach-Object { [System.Uri]::UnescapeDataString($_.Groups[1].Value) } | + Select-Object -Unique + foreach ($tag in $tags) { + $name = "cpython-$PythonVersion+$tag-$Triple-install_only_stripped.tar.gz" + $url = "https://github.com/astral-sh/python-build-standalone/releases/download/$tag/$name" + try { + $head = Invoke-WebRequest -Uri $url -Method Head -SkipHttpErrorCheck -Headers $atomHeaders + if ($head.StatusCode -ge 200 -and $head.StatusCode -lt 400) { + return [pscustomobject]@{ + name = $name + browser_download_url = $url + } + } + } + catch { + # Tag does not contain this CPython build, or HEAD was not supported. } } throw "No python-build-standalone install_only_stripped asset for CPython $PythonVersion on $Triple." diff --git a/src/Tests/PackTests.cs b/src/Tests/PackTests.cs index 20016db..f01d65c 100644 --- a/src/Tests/PackTests.cs +++ b/src/Tests/PackTests.cs @@ -281,12 +281,38 @@ public void Payload_sources_use_github_tarball_not_zip_via_tar() Assert.DoesNotContain("tar -xf $zip", functions); Assert.Contains("githubusercontent", functions); Assert.DoesNotContain("Invoke-WebRequest -Uri $Url -OutFile $Dest -Headers (Get-GitHubHeaders)", functions); + Assert.Contains("function Get-GitHubAuthTokens", functions); + Assert.Contains("function Invoke-GitHubRestMethod", functions); + Assert.Contains("rate limit", functions); var payload = File.ReadAllText(Path.Combine(repo, "src", "Azure.Cli", "payload.ps1")); Assert.Contains("Expand-Zip $zip $OutDir", payload); Assert.DoesNotContain("tar -xf $zip", payload); Assert.Contains("Remove-UnusedPythonShare", payload); Assert.Contains("Install-CliFromPyPI", payload); + Assert.Contains("Invoke-GitHubRestMethod", payload); + Assert.Contains("releases.atom", payload); + } + + [Fact] + public void GitHubHeaders_prefer_actions_token_when_github_actions() + { + var repo = FindRepoRoot(); + var functions = Path.Combine(repo, "src", "Azure.Cli", "payload.functions.ps1"); + var functionsLit = functions.Replace("'", "''", StringComparison.Ordinal); + RunPwsh(repo, $$""" + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version Latest + . '{{functionsLit}}' + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_TOKEN = 'actions-token' + $env:GH_TOKEN = 'pat-token' + $h = Get-GitHubHeaders + if ($h['Authorization'] -ne 'Bearer actions-token') { throw "Actions: $($h['Authorization'])" } + $env:GITHUB_ACTIONS = 'false' + $h = Get-GitHubHeaders + if ($h['Authorization'] -ne 'Bearer pat-token') { throw "Local: $($h['Authorization'])" } + """); } [Fact]