Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0002-nupkg-version-is-upstream.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- #content -->
---
Expand Down
10 changes: 5 additions & 5 deletions src/Azure.Cli/Azure.Cli.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Azure.Cli</PackageId>
<PackageId>azx.cli</PackageId>
<RootNamespace>Azure</RootNamespace>
<AssemblyName>Azure.Cli</AssemblyName>
<Description>Azure CLI payload for .NET: PackageReference Azure.Cli and publish/pack for your RID to get az/ plus Azure.Cli.ResolvePath.</Description>
<Description>Azure CLI payload for .NET: PackageReference azx.cli and publish/pack for your RID to get az/ plus Azure.Cli.ResolvePath.</Description>
<IsAotCompatible>true</IsAotCompatible>
<IsPackable>true</IsPackable>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
Expand All @@ -30,10 +30,10 @@
</ItemGroup>

<Import Project="Azure.Cli.pack.targets" />
<Import Project="buildTransitive\Azure.Cli.targets" Condition="'$(AzureCliTargetsImported)' != 'true'" />
<Import Project="buildTransitive\azx.cli.targets" Condition="'$(AzureCliTargetsImported)' != 'true'" />

<ItemGroup>
<None Include="buildTransitive\Azure.Cli.targets" Pack="$(IncludeBuildOutput)" PackagePath="buildTransitive\Azure.Cli.targets" />
<None Include="build\Azure.Cli.targets" Pack="$(IncludeBuildOutput)" PackagePath="build\Azure.Cli.targets" />
<None Include="buildTransitive\azx.cli.targets" Pack="$(IncludeBuildOutput)" PackagePath="buildTransitive\$(PackageId).targets" />
<None Include="build\azx.cli.targets" Pack="$(IncludeBuildOutput)" PackagePath="build\$(PackageId).targets" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/Azure.Cli/Cli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
3 changes: 0 additions & 3 deletions src/Azure.Cli/build/Azure.Cli.targets

This file was deleted.

3 changes: 3 additions & 0 deletions src/Azure.Cli/build/azx.cli.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\buildTransitive\$(MSBuildThisFileName).targets" Condition="'$(AzureCliTargetsImported)' != 'true'" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<PropertyGroup Condition="'$(AzureCliPayloadRoot)' == ''">
<_AzureCliVersion>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))))</_AzureCliVersion>
<_AzureCliPackageFolder>$([System.String]::Copy('azure.cli.$(_AzureCliRid)').ToLowerInvariant())</_AzureCliPackageFolder>
<_AzureCliPackageFolder>$([System.String]::Copy('$(MSBuildThisFileName).$(_AzureCliRid)').ToLowerInvariant())</_AzureCliPackageFolder>
<AzureCliPayloadRoot>$(NuGetPackageRoot)$(_AzureCliPackageFolder)\$(_AzureCliVersion)\az</AzureCliPayloadRoot>
</PropertyGroup>

Expand Down
57 changes: 52 additions & 5 deletions src/Azure.Cli/payload.functions.ps1
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
40 changes: 35 additions & 5 deletions src/Azure.Cli/payload.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
49 changes: 40 additions & 9 deletions src/Tests/PackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<PackageId>Azure.Cli</PackageId>", azureCli);
Assert.Contains("<PackageId>azx.cli</PackageId>", azureCli);
Assert.Contains("<AssemblyName>Azure.Cli</AssemblyName>", azureCli);
Assert.Contains("<RootNamespace>Azure</RootNamespace>", azureCli);
Assert.Contains("<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>", azureCli);
Assert.DoesNotContain("NuGetizer", azureCli, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("<PackAsTool", azureCli);
Assert.Contains("Azure.Cli.pack.targets", azureCli);
Assert.Contains("buildTransitive\\Azure.Cli.targets", azureCli.Replace('/', '\\'));
Assert.Contains("buildTransitive\\azx.cli.targets", azureCli.Replace('/', '\\'));
Assert.Contains("buildTransitive\\$(PackageId).targets", azureCli.Replace('/', '\\'));
Assert.Contains("Readme", azureCli);
Assert.DoesNotContain("win-arm64", azureCli);

Expand Down Expand Up @@ -61,7 +64,7 @@ public void Pointer_and_rid_csproj_use_pack_split()
Assert.Contains("$(AzureCliPackageId).$(RuntimeIdentifier)", packTargets);
Assert.DoesNotContain("runtimes/$(RuntimeIdentifier)/native/", packTargets);

var consumer = File.ReadAllText(Path.Combine(repo, "src", "Azure.Cli", "buildTransitive", "Azure.Cli.targets"));
var consumer = File.ReadAllText(Path.Combine(repo, "src", "Azure.Cli", "buildTransitive", "azx.cli.targets"));
Assert.Contains("IncludeAzureCliPayload", consumer);
Assert.Contains(@"TargetPath>az\", consumer.Replace('/', '\\'));
Assert.DoesNotContain("runtimes/$(RuntimeIdentifier)/native/", consumer);
Expand All @@ -72,13 +75,15 @@ public void Pointer_and_rid_csproj_use_pack_split()
Assert.Contains("<PublishAot>true</PublishAot>", azx);
Assert.Contains("<ToolCommandName>azx</ToolCommandName>", azx);
Assert.Contains("<ToolPackageRuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64;osx-arm64</ToolPackageRuntimeIdentifiers>", azx);
Assert.Contains("""<PackageReference Include="Azure.Cli" Version="$(Version)" />""", azx);
Assert.Contains("""<PackageReference Include="azx.cli" Version="$(Version)" />""", 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);

Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -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));
}

Expand Down Expand Up @@ -276,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]
Expand Down
4 changes: 3 additions & 1 deletion src/Tests/ReleaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading