Skip to content
Open
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
3 changes: 3 additions & 0 deletions .azure-pipelines/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ adding exact filenames from `devops/scripts/compliance/binskim-runtime-exclusion
- Some jobs also rely on **service connections** (`ame_esrp_connection` for ESRP, `AzureSubscription` for Key Vault) and **persisted checkout credentials** (`persistCredentials: true`, for checkout/fetch). Git tag/push and `gh` operations authenticate with the Key Vault `GH_TOKEN`, not the persisted checkout credentials — use these built-in mechanisms rather than embedding tokens.

## Packaging Roots (`packaging.yml`)

- Existing native installer lifecycle jobs also exercise public metadata/download and piped-invocation paths using local HTTP-command fixtures backed by the selected pipeline archive, never GitHub's latest release. Linux/macOS share `packaging/common/validate_public_cli_download.sh`, which rejects `jq`/`gh` use; no JSON parser installation is needed on glibc, musl, or macOS. Windows uses scoped HTTP fixtures, and signed PowerShell validation runs the new public bootstrap as well as the bundled helper in isolated processes. No additional test matrix, Pester dependency, or GitHub Actions wiring is required.
- `PackagingScriptsRoot = devops/scripts/packaging`
- Extension download-only helper checks run inside the same native CLI lifecycle jobs: Bash through the shared CLI fixture, and PowerShell through `common/test_public_extensions_download.ps1`. Both build fixture bundles from the downloaded pipeline extension packages and check pinned releases, failure preservation, and cleanup without live release downloads.
- `PackagingOutRoot = devops/out` (with `common/`, `linux/`, `windows/`, `macos/` subfolders)
- Default RIDs: `linux-x64`, `win-x64`, `osx-arm64`.
- Pipeline jobs shell out to `devops/scripts/` (`ci/`, `compliance/`, `packaging/`, `publishing/`). When changing a build step, update the script there rather than embedding logic in YAML.
Expand Down
8 changes: 8 additions & 0 deletions devops/scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ output/staging path, update this guide in the same PR so it stays accurate.
| `packaging/signing/` | ESRP input prep, repack, and verification of signed outputs, including collision-resistant macOS bundle ZIP mapping. |
| `publishing/` | Release publishing helpers (`Update-ReleaseChangelog.ps1`, `Update-ChangedExtensionVersions.ps1`). |

## Installer Download Validation

- The existing Linux/macOS lifecycle validators use `packaging/common/validate_public_cli_download.sh` for the public-download install, then continue their native runtime and extension checks. The shared fixture substitutes `curl` with local release metadata and the exact pipeline archive; it rejects `gh`/`jq` use and exercises missing/duplicate assets, invalid or truncated URLs, malformed metadata, HTTP failures, piped help, and truncated scripts. It preserves the existing local-archive safety coverage without requiring a JSON parser.
- Windows zip validation substitutes HTTP cmdlets in a child scope and installs the selected pipeline archive through `Invoke-Expression`; its existing offline and native lifecycle checks remain.
- `packaging/powershell/test_signed_azps_cmdlets.ps1` validates both the bundled helper and the public `install/azps-install.ps1` bootstrap in isolated PowerShell processes. Bootstrap HTTP fixtures copy the selected signed archive, exercise default and parameterized installation/import, and check error cleanup and existing-repository preservation.
- These checks run in the existing ADO validation jobs without Pester, GitHub authentication, or a live latest-release lookup. Keep shared Bash fixtures compatible with macOS Bash 3.2 and Alpine.
- The Bash fixture also validates `install/azcli-extensions-download.sh` against a bundle made from the pipeline's extension packages. Windows calls `packaging/common/test_public_extensions_download.ps1` for the PowerShell equivalent. These download-only helpers support a pinned release, save `azcli-extensions.tar.gz` in the working directory, preserve an existing bundle on failure, and never extract or install it.

## Credential Scanner Suppressions

`compliance/CredScanSuppressions.json` records file paths, scanner-provided SARIF
Expand Down
138 changes: 138 additions & 0 deletions devops/scripts/packaging/common/test_public_extensions_download.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ExtensionsDirectory
)

$ErrorActionPreference = 'Stop'
$downloaderPath = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../../../install/azcli-extensions-download.ps1'))
$testRoot = Join-Path ([IO.Path]::GetTempPath()) "azclips-extension-download-test-$([guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Path $testRoot | Out-Null
$fixture = @{
Scenario = 'valid'
Tag = 'v-fixture.timestamp'
Archive = (Join-Path $testRoot 'pipeline-bundle.tar.gz')
Endpoint = ''
}
try {
tar -czf $fixture.Archive -C $ExtensionsDirectory .
if ($LASTEXITCODE -ne 0) { throw 'Could not create the extensions fixture from pipeline packages.' }
Push-Location $testRoot
try {
function Invoke-RestMethod {
[CmdletBinding()]
param([string]$Uri)
if ($Uri -notin @('https://api.github.com/repos/Azure/azclips/releases/latest',
"https://api.github.com/repos/Azure/azclips/releases/tags/$($fixture.Tag)")) {
throw "Unexpected metadata URL: $Uri"
}
$fixture.Endpoint = $Uri
if ($fixture.Scenario -eq 'metadata-failure') { throw 'Fixture HTTP 403 rate limit' }
$asset = @{
name = 'azcli-extensions-0.0.0.tar.gz'
browser_download_url = "https://github.com/Azure/azclips/releases/download/$($fixture.Tag)/azcli-extensions-0.0.0.tar.gz"
}
if ($fixture.Scenario -eq 'invalid-url') { $asset.browser_download_url += '?download=1' }
if ($fixture.Scenario -eq 'wrong-repo') { $asset.browser_download_url = $asset.browser_download_url.Replace('/Azure/', '/Other/') }
if ($fixture.Scenario -eq 'unsafe-name') { $asset.name = '../azcli-extensions-0.0.0.tar.gz' }
$assets = @($asset, @{ name = 'unrelated.tar.gz'; browser_download_url = 'https://example.invalid/wrong' })
if ($fixture.Scenario -eq 'missing') { $assets = @() }
if ($fixture.Scenario -eq 'duplicate') { $assets = @($asset, $asset) }
$tag = switch ($fixture.Scenario) {
'malformed' { '' }
'mismatched-release' { 'v-other' }
default { $fixture.Tag }
}
@{ tag_name = $tag; assets = $assets }
}
function Invoke-WebRequest {
[CmdletBinding()]
param([string]$Uri, [string]$OutFile)
if ($Uri -ne "https://github.com/Azure/azclips/releases/download/$($fixture.Tag)/azcli-extensions-0.0.0.tar.gz") {
throw "Unexpected asset URL: $Uri"
}
if ($fixture.Scenario -eq 'download-failure') {
Set-Content -LiteralPath $OutFile -Value 'partial download'
throw 'Fixture HTTP 503 download failure'
}
if ($fixture.Scenario -ne 'missing-download') {
Copy-Item -LiteralPath $fixture.Archive -Destination $OutFile
}
}
function gh { throw 'Download must not invoke gh.' }
function jq { throw 'Download must not invoke jq.' }
function Assert-DownloadCleanup {
if (Get-ChildItem -Force -Filter '.azcli-extensions-download*') {
throw 'Extensions downloader left staging files.'
}
}
Set-Content -Path azcli-extensions.tar.gz -Value 'existing bundle sentinel'
$before = (Get-FileHash azcli-extensions.tar.gz).Hash
foreach ($scenario in 'missing', 'duplicate', 'malformed', 'mismatched-release', 'invalid-url', 'wrong-repo', 'unsafe-name', 'metadata-failure', 'download-failure', 'missing-download') {
$fixture.Scenario = $scenario
$expected = switch ($scenario) {
'malformed' { 'valid matching tag_name' }
'mismatched-release' { 'valid matching tag_name' }
'invalid-url' { 'invalid GitHub download URL' }
'wrong-repo' { 'invalid GitHub download URL' }
'metadata-failure' { 'Fixture HTTP 403' }
'download-failure' { 'Fixture HTTP 503' }
'missing-download' { 'Download did not produce' }
default { 'Expected exactly one' }
}
try {
if ($scenario -eq 'mismatched-release') {
& $downloaderPath -Release $fixture.Tag
}
else {
Get-Content -Raw $downloaderPath | Invoke-Expression
}
throw "FAIL: $scenario unexpectedly succeeded."
}
catch {
if ($_.Exception.Message -notlike "*$expected*") { throw }
}
if ((Get-FileHash azcli-extensions.tar.gz).Hash -ne $before) {
throw "FAIL: $scenario changed the existing bundle."
}
Assert-DownloadCleanup
}
$fixture.Scenario = 'valid'
$beforePath = $env:PATH
$ErrorActionPreference = 'Continue'
Get-Content -Raw $downloaderPath | Invoke-Expression
if ($ErrorActionPreference -ne 'Continue' -or $env:PATH -ne $beforePath -or
(Get-Variable metadata -Scope Local -ErrorAction SilentlyContinue)) {
throw 'Download helper leaked caller state.'
}
$ErrorActionPreference = 'Stop'
& $downloaderPath -Release $fixture.Tag
if ($fixture.Endpoint -notlike '*/tags/v-fixture.timestamp') { throw 'Download ignored the pinned release.' }
if ((Get-FileHash azcli-extensions.tar.gz).Hash -ne (Get-FileHash $fixture.Archive).Hash) {
throw 'Downloaded bundle differs from the pipeline packages.'
}
Assert-DownloadCleanup
if (@(Get-ChildItem -Force).Count -ne 2) { throw 'Helper extracted or created unexpected files.' }
try {
& $downloaderPath -Release '../invalid'
throw 'FAIL: Invalid release tag was accepted.'
}
catch {
if ($_.Exception.Message -notlike '*pattern*') { throw }
}
Remove-Item -LiteralPath azcli-extensions.tar.gz
New-Item -ItemType Directory -Path azcli-extensions.tar.gz | Out-Null
try {
Get-Content -Raw $downloaderPath | Invoke-Expression
throw 'FAIL: Directory destination was accepted.'
}
catch {
if ($_.Exception.Message -notlike '*non-file destination*') { throw }
}
Write-Host 'PASS: PowerShell download-only extensions helper, pinned release, scope isolation, failure preservation, and cleanup.'
}
finally { Pop-Location }
}
finally {
Remove-Item -LiteralPath $testRoot -Recurse -Force -ErrorAction Stop
}
Loading
Loading