mirror of
https://github.com/microsoft/mcp.git
synced 2026-11-04 12:01:31 +00:00
Sync eng/common directory with azure-sdk-tools for PR 16660 (#3428)
* Add Get-PackageApprovalStatus script. * Sync eng/common directory with azure-sdk-tools repository for Tools PR 16660 * Sync eng/common directory with azure-sdk-tools repository for Tools PR 16660 * Sync eng/common directory with azure-sdk-tools repository for Tools PR 16660 * Sync eng/common directory with azure-sdk-tools repository for Tools PR 16660 * Sync eng/common directory with azure-sdk-tools repository for Tools PR 16660 --------- Co-authored-by: Travis Prescott <trpresco@microsoft.com> Co-authored-by: azure-sdk <azuresdk@microsoft.com>
This commit is contained in:
12 files changed
+1364
-2
No files matched your search
@@ -57,9 +57,23 @@ steps:
|
||||
-BuildId '$(Build.BuildId)'
|
||||
-RepoName '$(Build.Repository.Name)'
|
||||
-MarkPackageAsShipped $${{parameters.MarkPackageAsShipped}}
|
||||
displayName: Create API Review
|
||||
displayName: Create and Validate API Review
|
||||
condition: >-
|
||||
and(
|
||||
succeededOrFailed(),
|
||||
ne(variables['Skip.CreateApiReview'], 'true')
|
||||
not(
|
||||
and(
|
||||
eq(variables['Skip.CreateApiReview'], 'true'),
|
||||
in(
|
||||
lower(variables['Build.RequestedForEmail']),
|
||||
'bebroder@microsoft.com',
|
||||
'mharder@microsoft.com',
|
||||
'djurek@microsoft.com',
|
||||
'chononiw@microsoft.com',
|
||||
'raychen@microsoft.com',
|
||||
'trpresco@microsoft.com',
|
||||
'prmarott@microsoft.com'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
parameters:
|
||||
- name: ArtifactPath
|
||||
type: string
|
||||
default: $(Build.ArtifactStagingDirectory)
|
||||
- name: Artifacts
|
||||
type: object
|
||||
default: []
|
||||
- name: ConfigFileDir
|
||||
type: string
|
||||
default: $(Build.ArtifactStagingDirectory)/PackageInfo
|
||||
- name: GenerateApiReviewForManualOnly
|
||||
type: boolean
|
||||
default: false
|
||||
- name: ArtifactName
|
||||
type: string
|
||||
default: "packages"
|
||||
- name: PackageName
|
||||
type: string
|
||||
default: ""
|
||||
- name: SourceRootPath
|
||||
type: string
|
||||
default: $(Build.SourcesDirectory)
|
||||
- name: PackageInfoFiles
|
||||
type: object
|
||||
default: []
|
||||
|
||||
steps:
|
||||
# Automatic APIView revisions are created regardless of how the pipeline is triggered.
|
||||
# This condition limits revision creation to manual runs when GenerateApiReviewForManualOnly is true.
|
||||
- ${{ if or(ne(parameters.GenerateApiReviewForManualOnly, true), eq(variables['Build.Reason'], 'Manual')) }}:
|
||||
# Ideally this should be an initial step in the caller job.
|
||||
# Remove this step once all callers set the default branch themselves.
|
||||
- template: /eng/common/pipelines/templates/steps/set-default-branch.yml
|
||||
parameters:
|
||||
WorkingDirectory: ${{ parameters.SourceRootPath }}
|
||||
|
||||
- ${{ if and(eq(variables['System.TeamProject'], 'internal'), ne(variables['Build.Reason'], 'PullRequest'), not(endsWith(variables['Build.Repository.Name'], '-pr'))) }}:
|
||||
- task: AzureCLI@2
|
||||
inputs:
|
||||
azureSubscription: "APIView prod deployment"
|
||||
scriptType: pscore
|
||||
scriptLocation: scriptPath
|
||||
scriptPath: ${{ parameters.SourceRootPath }}/eng/common/scripts/Create-APIViewRevision.ps1
|
||||
# PackageInfoFiles example: @('a/file1.json','a/file2.json')
|
||||
arguments: >
|
||||
-PackageInfoFiles @('${{ join(''',''', parameters.PackageInfoFiles) }}')
|
||||
-ArtifactList ('${{ convertToJson(parameters.Artifacts) }}' | ConvertFrom-Json | Select-Object Name)
|
||||
-ArtifactPath '${{ parameters.ArtifactPath }}'
|
||||
-ArtifactName '${{ parameters.ArtifactName }}'
|
||||
-PackageName '${{ parameters.PackageName }}'
|
||||
-SourceBranch '$(Build.SourceBranchName)'
|
||||
-DefaultBranch '$(DefaultBranch)'
|
||||
-ConfigFileDir '${{ parameters.ConfigFileDir }}'
|
||||
-BuildId '$(Build.BuildId)'
|
||||
-RepoName '$(Build.Repository.Name)'
|
||||
displayName: Create APIView Revision
|
||||
condition: >-
|
||||
and(
|
||||
succeededOrFailed(),
|
||||
not(
|
||||
and(
|
||||
eq(variables['Skip.CreateApiReview'], 'true'),
|
||||
eq(variables['IsRequesterAuthorizedToSkipApiReview'], 'true')
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
parameters:
|
||||
- name: PackageInfoFiles
|
||||
type: object
|
||||
- name: RepoOwner
|
||||
type: string
|
||||
default: ""
|
||||
- name: SourceRootPath
|
||||
type: string
|
||||
default: $(Build.SourcesDirectory)
|
||||
|
||||
steps:
|
||||
# The consuming pipeline must install the Azure SDK Tools CLI before including this template.
|
||||
# Use install-azsdk-cli.yml or otherwise set $(AZSDK) to the azsdk executable path.
|
||||
- task: AzureCLI@2
|
||||
displayName: Check Package Approval Status
|
||||
# Keep this predicate aligned with the release-stage inclusion condition in archetype-python-release.yml.
|
||||
condition: >-
|
||||
and(
|
||||
succeeded(),
|
||||
eq(variables['System.TeamProject'], 'internal'),
|
||||
or(
|
||||
in(variables['Build.Reason'], 'Manual', ''),
|
||||
and(
|
||||
eq(variables['Build.Reason'], 'IndividualCI'),
|
||||
eq(variables['Build.SourceBranch'], 'refs/heads/main')
|
||||
)
|
||||
),
|
||||
not(
|
||||
and(
|
||||
eq(variables['Skip.CheckPackageApproval'], 'true'),
|
||||
eq(variables['IsRequesterAuthorizedToSkipApiReview'], 'true')
|
||||
)
|
||||
)
|
||||
)
|
||||
inputs:
|
||||
azureSubscription: "ADO to ARH Service Connection"
|
||||
scriptType: pscore
|
||||
scriptLocation: scriptPath
|
||||
scriptPath: ${{ parameters.SourceRootPath }}/eng/common/scripts/Get-PackageApprovalStatus.ps1
|
||||
# PackageInfoFiles example: @('a/file1.json','a/file2.json')
|
||||
arguments: >
|
||||
-PackageInfoFiles @('${{ join(''',''', parameters.PackageInfoFiles) }}')
|
||||
-RepoOwner '${{ parameters.RepoOwner }}'
|
||||
-AzSdkExePath '$(AZSDK)'
|
||||
workingDirectory: $(Pipeline.Workspace)
|
||||
@@ -0,0 +1,37 @@
|
||||
parameters:
|
||||
- name: PackageInfoFiles
|
||||
type: object
|
||||
- name: RepoOwner
|
||||
type: string
|
||||
default: ""
|
||||
- name: SourceRootPath
|
||||
type: string
|
||||
default: $(Build.SourcesDirectory)
|
||||
|
||||
steps:
|
||||
# Include this template only after package publishing in the release stage.
|
||||
# The consuming pipeline must install the Azure SDK Tools CLI before including this template.
|
||||
# Use install-azsdk-cli.yml or otherwise set $(AZSDK) to the azsdk executable path.
|
||||
- task: AzureCLI@2
|
||||
displayName: Mark Packages Released
|
||||
condition: >-
|
||||
and(
|
||||
succeeded(),
|
||||
not(
|
||||
and(
|
||||
eq(variables['Skip.MarkPackageReleased'], 'true'),
|
||||
eq(variables['IsRequesterAuthorizedToSkipApiReview'], 'true')
|
||||
)
|
||||
)
|
||||
)
|
||||
inputs:
|
||||
azureSubscription: "ADO to ARH Service Connection"
|
||||
scriptType: pscore
|
||||
scriptLocation: scriptPath
|
||||
scriptPath: ${{ parameters.SourceRootPath }}/eng/common/scripts/Mark-PackageReleased.ps1
|
||||
# PackageInfoFiles example: @('a/file1.json','a/file2.json')
|
||||
arguments: >
|
||||
-PackageInfoFiles @('${{ join(''',''', parameters.PackageInfoFiles) }}')
|
||||
-RepoOwner '${{ parameters.RepoOwner }}'
|
||||
-AzSdkExePath '$(AZSDK)'
|
||||
workingDirectory: $(Pipeline.Workspace)
|
||||
@@ -0,0 +1,12 @@
|
||||
variables:
|
||||
IsRequesterAuthorizedToSkipApiReview: >-
|
||||
$[in(
|
||||
lower(variables['Build.RequestedForEmail']),
|
||||
'bebroder@microsoft.com',
|
||||
'mharder@microsoft.com',
|
||||
'djurek@microsoft.com',
|
||||
'chononiw@microsoft.com',
|
||||
'raychen@microsoft.com',
|
||||
'trpresco@microsoft.com',
|
||||
'prmarott@microsoft.com'
|
||||
)]
|
||||
@@ -0,0 +1,285 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates APIView revisions for SDK packages.
|
||||
|
||||
.DESCRIPTION
|
||||
Uploads package artifacts directly to APIView or creates revisions from pre-generated review token files.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $false)]
|
||||
[array] $ArtifactList,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $ArtifactPath,
|
||||
|
||||
[string] $SourceBranch,
|
||||
[string] $DefaultBranch,
|
||||
[string] $RepoName,
|
||||
[string] $BuildId,
|
||||
[string] $PackageName = "",
|
||||
[string] $ConfigFileDir = "",
|
||||
[string] $APIViewUri = "https://apiview.dev/autoreview",
|
||||
[string] $ArtifactName = "packages",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[array] $PackageInfoFiles
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 4
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Loads LanguageShort from eng/scripts/Language-Settings.ps1.
|
||||
. (Join-Path $PSScriptRoot common.ps1)
|
||||
|
||||
function Get-ApiViewBearerToken {
|
||||
$tokenResponse = az account get-access-token --resource "api://apiview" --output json 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to acquire APIView access token: $tokenResponse"
|
||||
}
|
||||
|
||||
try {
|
||||
$accessToken = ($tokenResponse | ConvertFrom-Json -ErrorAction Stop).accessToken
|
||||
}
|
||||
catch {
|
||||
throw "Failed to parse APIView access token response: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($accessToken)) {
|
||||
throw "APIView access token response did not contain an access token."
|
||||
}
|
||||
|
||||
return $accessToken
|
||||
}
|
||||
|
||||
function Invoke-ApiViewSourceUpload(
|
||||
[string] $FilePath,
|
||||
[string] $ApiLabel,
|
||||
[string] $ReleaseStatus,
|
||||
[string] $PackageVersion,
|
||||
[string] $PackageType
|
||||
) {
|
||||
$fileName = Split-Path -Leaf $FilePath
|
||||
Write-Host "Uploading $FilePath to APIView."
|
||||
|
||||
$multipartContent = [System.Net.Http.MultipartFormDataContent]::new()
|
||||
$fileStream = $null
|
||||
try {
|
||||
$fileStream = [System.IO.FileStream]::new($FilePath, [System.IO.FileMode]::Open)
|
||||
$fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
|
||||
$fileHeader.Name = "file"
|
||||
$fileHeader.FileName = $fileName
|
||||
$fileContent = [System.Net.Http.StreamContent]::new($fileStream)
|
||||
$fileContent.Headers.ContentDisposition = $fileHeader
|
||||
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream")
|
||||
$multipartContent.Add($fileContent)
|
||||
|
||||
Add-MultipartString $multipartContent "label" $ApiLabel
|
||||
Add-MultipartString $multipartContent "packageVersion" $PackageVersion
|
||||
Add-MultipartString $multipartContent "setReleaseTag" "False"
|
||||
Add-MultipartString $multipartContent "packageType" $PackageType
|
||||
if ($ReleaseStatus -and $ReleaseStatus -ne "Unreleased") {
|
||||
Add-MultipartString $multipartContent "compareAllRevisions" "True"
|
||||
}
|
||||
|
||||
$headers = @{ Authorization = "Bearer $(Get-ApiViewBearerToken)" }
|
||||
$response = Invoke-WebRequest -Method Post -Uri "$APIViewUri/upload" -Body $multipartContent -Headers $headers -MaximumRetryCount 3
|
||||
Write-Host "API review: $($response.Content)"
|
||||
Write-Host "HTTP response code: $($response.StatusCode)"
|
||||
}
|
||||
finally {
|
||||
$multipartContent.Dispose()
|
||||
if ($null -ne $fileStream) {
|
||||
$fileStream.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Add-MultipartString(
|
||||
[System.Net.Http.MultipartFormDataContent] $MultipartContent,
|
||||
[string] $Name,
|
||||
[string] $Value
|
||||
) {
|
||||
$header = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
|
||||
$header.Name = $Name
|
||||
$content = [System.Net.Http.StringContent]::new($Value)
|
||||
$content.Headers.ContentDisposition = $header
|
||||
$MultipartContent.Add($content)
|
||||
Write-Host "Request param, ${Name}: $Value"
|
||||
}
|
||||
|
||||
function Invoke-ApiViewTokenCreation(
|
||||
[object] $PackageInfo,
|
||||
[string] $ApiLabel,
|
||||
[string] $ReviewFileName,
|
||||
[string] $PackagePath
|
||||
) {
|
||||
if ([string]::IsNullOrWhiteSpace($BuildId)) {
|
||||
throw "BuildId is required to create an APIView revision from a review token file."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($RepoName)) {
|
||||
throw "RepoName is required to create an APIView revision from a review token file."
|
||||
}
|
||||
|
||||
$fileName = Split-Path -Leaf $PackagePath
|
||||
$queryParameters = [ordered]@{
|
||||
buildId = $BuildId
|
||||
artifactName = $ArtifactName
|
||||
originalFilePath = $fileName
|
||||
reviewFilePath = $ReviewFileName
|
||||
label = $ApiLabel
|
||||
repoName = $RepoName
|
||||
packageName = $PackageInfo.ArtifactName
|
||||
project = "internal"
|
||||
packageVersion = $PackageInfo.Version
|
||||
packageType = $PackageInfo.SdkType
|
||||
}
|
||||
if ($PackageInfo.ReleaseStatus -and $PackageInfo.ReleaseStatus -ne "Unreleased") {
|
||||
$queryParameters["compareAllRevisions"] = "true"
|
||||
}
|
||||
|
||||
$query = @($queryParameters.GetEnumerator() | ForEach-Object {
|
||||
"$([System.Uri]::EscapeDataString($_.Key))=$([System.Uri]::EscapeDataString($_.Value))"
|
||||
}) -join "&"
|
||||
$uri = "$APIViewUri/create?$query"
|
||||
Write-Host "Creating APIView revision from review token file $ReviewFileName."
|
||||
Write-Host "Request to APIView: $uri"
|
||||
$headers = @{ Authorization = "Bearer $(Get-ApiViewBearerToken)" }
|
||||
$response = Invoke-WebRequest -Method Post -Uri $uri -Headers $headers -MaximumRetryCount 3
|
||||
Write-Host "API review: $($response.Content)"
|
||||
Write-Host "HTTP response code: $($response.StatusCode)"
|
||||
}
|
||||
|
||||
function Get-ApiReviewTokenFileName([string] $ArtifactName) {
|
||||
$reviewTokenFileName = "${ArtifactName}_${LanguageShort}.json"
|
||||
$tokenFilePath = Join-Path $ArtifactPath $ArtifactName $reviewTokenFileName
|
||||
if (Test-Path $tokenFilePath) {
|
||||
Write-Host "Review token file is present at $tokenFilePath"
|
||||
return $reviewTokenFileName
|
||||
}
|
||||
|
||||
Write-Host "Review token file is not present at $tokenFilePath"
|
||||
return $null
|
||||
}
|
||||
|
||||
function Submit-ApiViewRevision([object] $PackageInfo, [string] $PackagePath) {
|
||||
$apiLabel = "Source Branch:$SourceBranch"
|
||||
$reviewTokenFileName = Get-ApiReviewTokenFileName $PackageInfo.ArtifactName
|
||||
if ($reviewTokenFileName) {
|
||||
Invoke-ApiViewTokenCreation $PackageInfo $apiLabel $reviewTokenFileName $PackagePath
|
||||
return
|
||||
}
|
||||
|
||||
Invoke-ApiViewSourceUpload $PackagePath $apiLabel $PackageInfo.ReleaseStatus $PackageInfo.Version $PackageInfo.SdkType
|
||||
}
|
||||
|
||||
function Find-PackageArtifacts([object] $PackageInfo) {
|
||||
if (-not $FindArtifactForApiReviewFn -or -not (Test-Path "Function:$FindArtifactForApiReviewFn")) {
|
||||
throw "The function configured by 'FindArtifactForApiReviewFn' was not found."
|
||||
}
|
||||
|
||||
$artifactName = if ($PackageInfo.ArtifactName) { $PackageInfo.ArtifactName } else { $PackageInfo.Name }
|
||||
$functionInfo = Get-Command $FindArtifactForApiReviewFn -ErrorAction Stop
|
||||
if ($functionInfo.Parameters.Keys -contains "packageInfo") {
|
||||
return &$FindArtifactForApiReviewFn $ArtifactPath $PackageInfo
|
||||
}
|
||||
|
||||
return &$FindArtifactForApiReviewFn $ArtifactPath $artifactName
|
||||
}
|
||||
|
||||
function Process-Package([object] $PackageInfo) {
|
||||
$packages = Find-PackageArtifacts $PackageInfo
|
||||
if (-not $packages) {
|
||||
Write-Host "No package is found in artifact path to submit a review request for $($PackageInfo.ArtifactName)."
|
||||
return
|
||||
}
|
||||
|
||||
$version = [AzureEngSemanticVersion]::ParseVersionString($PackageInfo.Version)
|
||||
if ($null -eq $version) {
|
||||
throw "Version '$($PackageInfo.Version)' for package $($PackageInfo.ArtifactName) is invalid."
|
||||
}
|
||||
|
||||
Write-Host "Version: $version"
|
||||
Write-Host "SDK Type: $($PackageInfo.SdkType)"
|
||||
Write-Host "Release Status: $($PackageInfo.ReleaseStatus)"
|
||||
if ($SourceBranch -ne $DefaultBranch -and $version.IsPrerelease) {
|
||||
Write-Host "Build is triggered from $SourceBranch with a prerelease version. Skipping APIView revision creation."
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($packagePath in $packages.Values) {
|
||||
Write-Host "Submitting APIView revision for package $($PackageInfo.ArtifactName), file path: $packagePath"
|
||||
Submit-ApiViewRevision $PackageInfo $packagePath
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-PackageInfoFiles {
|
||||
if (-not $ConfigFileDir) {
|
||||
$script:ConfigFileDir = Join-Path $ArtifactPath "PackageInfo"
|
||||
}
|
||||
|
||||
if ($PackageName) {
|
||||
$packageInfoPath = Join-Path $ConfigFileDir "$PackageName.json"
|
||||
if (-not (Test-Path $packageInfoPath)) {
|
||||
throw "Package property file path $packageInfoPath is invalid."
|
||||
}
|
||||
return @($packageInfoPath)
|
||||
}
|
||||
|
||||
if ($ArtifactList -and $ArtifactList.Count -gt 0) {
|
||||
$files = @()
|
||||
foreach ($artifact in $ArtifactList) {
|
||||
$packageInfoPath = Join-Path $ConfigFileDir "$($artifact.Name).json"
|
||||
if (Test-Path $packageInfoPath) {
|
||||
$files += $packageInfoPath
|
||||
}
|
||||
else {
|
||||
Write-Warning "Package property file path $packageInfoPath is invalid."
|
||||
}
|
||||
}
|
||||
return $files
|
||||
}
|
||||
|
||||
if ($PackageInfoFiles -and $PackageInfoFiles.Count -gt 0) {
|
||||
return @($PackageInfoFiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
}
|
||||
|
||||
throw "No package information provided. Provide PackageName, ArtifactList, or PackageInfoFiles."
|
||||
}
|
||||
|
||||
Write-Host "Artifact path: $ArtifactPath"
|
||||
Write-Host "Source branch: $SourceBranch"
|
||||
Write-Host "Package name: $PackageName"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SourceBranch)) {
|
||||
throw "SourceBranch is required to determine APIView revision eligibility."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($DefaultBranch)) {
|
||||
throw "DefaultBranch is required to determine APIView revision eligibility."
|
||||
}
|
||||
if (-not (Test-Path $ArtifactPath -PathType Container)) {
|
||||
throw "Artifact path $ArtifactPath does not exist or is not a directory."
|
||||
}
|
||||
|
||||
$processedPackageInfoFiles = @(Resolve-PackageInfoFiles)
|
||||
if ($processedPackageInfoFiles.Count -eq 0) {
|
||||
throw "No package info files found after processing the supplied package inputs."
|
||||
}
|
||||
|
||||
$failures = @()
|
||||
foreach ($packageInfoFile in $processedPackageInfoFiles) {
|
||||
try {
|
||||
$packageInfo = Get-Content $packageInfoFile -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||
Write-Host "Processing $($packageInfo.ArtifactName)"
|
||||
Process-Package $packageInfo
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to create an APIView revision from ${packageInfoFile}: $($_.Exception.Message)" -ErrorAction Continue
|
||||
$failures += "${packageInfoFile}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
throw "APIView revision creation failed for $($failures.Count) package(s):`n$($failures -join "`n")"
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks whether a package is approved for release.
|
||||
|
||||
.DESCRIPTION
|
||||
Invokes the centralized Azure SDK CLI API review release gate and fails unless its structured result approves the package.
|
||||
|
||||
.PARAMETER PackageInfoFiles
|
||||
Package-info JSON files containing the package name, version, and optional API hash.
|
||||
|
||||
.PARAMETER RepoOwner
|
||||
The optional GitHub repository owner to query in API Review Hub.
|
||||
|
||||
.PARAMETER AzSdkExePath
|
||||
The path to the azsdk executable.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[array] $PackageInfoFiles,
|
||||
|
||||
[string] $RepoOwner = "",
|
||||
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $AzSdkExePath = "azsdk"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 4
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot common.ps1)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($LanguageShort) -or $LanguageShort -eq "Unknown") {
|
||||
throw "SDK language settings were not loaded."
|
||||
}
|
||||
Write-Host "SDK language: $LanguageShort"
|
||||
|
||||
function Write-BackendStatus([string] $Name, [object] $Status) {
|
||||
if ($null -eq $Status) {
|
||||
return
|
||||
}
|
||||
|
||||
$approval = if ($Status.isApproved) { "APPROVED" } else { "NOT APPROVED" }
|
||||
Write-Host $Name
|
||||
Write-Host " Status: $approval"
|
||||
if ($Status.PSObject.Properties["reason"]) {
|
||||
Write-Host " Reason: $($Status.reason)"
|
||||
}
|
||||
if ($Status.PSObject.Properties["statusCode"]) {
|
||||
Write-Host " HTTP status: $($Status.statusCode)"
|
||||
}
|
||||
|
||||
if ($Status.PSObject.Properties["details"]) {
|
||||
foreach ($detail in @($Status.details)) {
|
||||
Write-Host " Detail: $detail"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Write-ApprovalSummary([object] $Response) {
|
||||
$result = $Response.result
|
||||
$source = if ($result.PSObject.Properties["finalSource"]) { $result.finalSource } else { "unknown" }
|
||||
$reason = if ($result.PSObject.Properties["reason"]) { $result.reason } else { "none" }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Approval results"
|
||||
Write-Host "----------------"
|
||||
if ($result.PSObject.Properties["reviewHub"]) {
|
||||
Write-BackendStatus "API Review Hub" $result.reviewHub
|
||||
}
|
||||
if ($result.PSObject.Properties["apiView"]) {
|
||||
Write-Host ""
|
||||
Write-BackendStatus "APIView" $result.apiView
|
||||
}
|
||||
|
||||
$approval = if ($result.isApproved) { "APPROVED" } else { "NOT APPROVED" }
|
||||
Write-Host ""
|
||||
Write-Host "Overall"
|
||||
Write-Host " Status: $approval"
|
||||
Write-Host " Source: $source"
|
||||
Write-Host " Reason: $reason"
|
||||
}
|
||||
|
||||
function Test-PackageApproval([string] $PackageName, [string] $PackageVersion, [string] $ApiHash) {
|
||||
$arguments = @(
|
||||
"package",
|
||||
"get-approval-status",
|
||||
"--language", $LanguageShort,
|
||||
"--package-name", $PackageName,
|
||||
"--package-version", $PackageVersion,
|
||||
"--output", "json"
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ApiHash)) {
|
||||
$arguments += @("--api-hash", $ApiHash)
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($RepoOwner)) {
|
||||
$arguments += @("--repo-owner", $RepoOwner)
|
||||
}
|
||||
|
||||
$hashDescription = if ([string]::IsNullOrWhiteSpace($ApiHash)) { "not provided" } else { $ApiHash }
|
||||
Write-Host "Checking package approval: language=$LanguageShort, package=$PackageName, version=$PackageVersion, apiHash=$hashDescription"
|
||||
$formattedArguments = @($arguments | ForEach-Object { Format-CommandArgument $_ })
|
||||
Write-Host "Command: azsdk $($formattedArguments -join ' ')"
|
||||
|
||||
$commandResult = Invoke-AzSdkCliCommand $AzSdkExePath $arguments
|
||||
$exitCode = $commandResult.ExitCode
|
||||
|
||||
try {
|
||||
$response = $commandResult.Output | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
$capturedOutput = "stdout:`n$($commandResult.Stdout)`nstderr:`n$($commandResult.Stderr)"
|
||||
throw "Package approval check returned malformed JSON for $PackageName $PackageVersion (azsdk exit code $exitCode). Captured output:`n$capturedOutput"
|
||||
}
|
||||
|
||||
if ($response.PSObject.Properties["result"] -and
|
||||
$null -ne $response.result -and
|
||||
$response.result.PSObject.Properties["isApproved"] -and
|
||||
$response.result.isApproved -is [bool]) {
|
||||
Write-ApprovalSummary $response
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
$failureMessage = if ($response.PSObject.Properties["response_error"] -and
|
||||
-not [string]::IsNullOrWhiteSpace($response.response_error)) {
|
||||
$response.response_error
|
||||
} else {
|
||||
"azsdk exited with code $exitCode."
|
||||
}
|
||||
throw "Package approval check failed: $failureMessage"
|
||||
}
|
||||
|
||||
if (-not $response.PSObject.Properties["result"] -or
|
||||
$null -eq $response.result -or
|
||||
-not $response.result.PSObject.Properties["isApproved"] -or
|
||||
$response.result.isApproved -isnot [bool]) {
|
||||
throw "Package approval check returned an invalid response for $PackageName $PackageVersion."
|
||||
}
|
||||
|
||||
if (-not $response.result.isApproved) {
|
||||
throw "Package $PackageName $PackageVersion is not approved for release."
|
||||
}
|
||||
}
|
||||
|
||||
$packageInfoPaths = @($PackageInfoFiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($packageInfoPaths.Count -eq 0) {
|
||||
throw "At least one package-info file is required."
|
||||
}
|
||||
|
||||
Confirm-AzSdkCliMinimumVersion $AzSdkExePath ([version] "0.6.38")
|
||||
$failures = @()
|
||||
foreach ($packageInfoFile in $packageInfoPaths) {
|
||||
try {
|
||||
if (-not (Test-Path $packageInfoFile -PathType Leaf)) {
|
||||
throw "Package-info file does not exist."
|
||||
}
|
||||
|
||||
$packageInfo = Get-Content $packageInfoFile -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||
$packageName = if ($packageInfo.PSObject.Properties["Name"]) { [string] $packageInfo.Name } else { "" }
|
||||
$packageVersion = if ($packageInfo.PSObject.Properties["Version"]) { [string] $packageInfo.Version } else { "" }
|
||||
$apiHash = if ($packageInfo.PSObject.Properties["ApiHash"]) { [string] $packageInfo.ApiHash } else { "" }
|
||||
$releaseStatus = if ($packageInfo.PSObject.Properties["ReleaseStatus"]) { [string] $packageInfo.ReleaseStatus } else { "" }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($packageName)) {
|
||||
throw "Package-info file does not contain a package Name."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($packageVersion)) {
|
||||
throw "Package-info file does not contain a package Version."
|
||||
}
|
||||
|
||||
try {
|
||||
Test-PackageApproval $packageName $packageVersion $apiHash
|
||||
}
|
||||
catch {
|
||||
if ($releaseStatus -eq "Unreleased") {
|
||||
Write-Host "$packageName $packageVersion is not marked for release. Ignoring approval check failure: $($_.Exception.Message)"
|
||||
}
|
||||
else {
|
||||
throw
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Package approval failed for ${packageInfoFile}: $($_.Exception.Message)" -ErrorAction Continue
|
||||
$failures += "${packageInfoFile}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
throw "Package approval failed for $($failures.Count) package(s):`n$($failures -join "`n")"
|
||||
}
|
||||
@@ -1,5 +1,73 @@
|
||||
. $PSScriptRoot/../logging.ps1
|
||||
|
||||
function Format-CommandArgument([string] $Argument) {
|
||||
if ($Argument -match '[\s"'']') {
|
||||
return '"' + $Argument.Replace('"', '\"') + '"'
|
||||
}
|
||||
return $Argument
|
||||
}
|
||||
|
||||
function Invoke-AzSdkCliCommand([string] $Executable, [string[]] $Arguments) {
|
||||
$command = Get-Command $Executable -ErrorAction SilentlyContinue
|
||||
if (-not $command) {
|
||||
throw "The azsdk CLI executable was not found at '$Executable'. Install azsdk before continuing."
|
||||
}
|
||||
|
||||
if ($command.CommandType -ne [System.Management.Automation.CommandTypes]::Application) {
|
||||
$output = @(& $command @Arguments 2>&1)
|
||||
return [PSCustomObject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($output | ForEach-Object { "$_" }) -join [Environment]::NewLine
|
||||
Stdout = ($output | ForEach-Object { "$_" }) -join [Environment]::NewLine
|
||||
Stderr = ""
|
||||
}
|
||||
}
|
||||
|
||||
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $command.Source
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
$startInfo.CreateNoWindow = $true
|
||||
|
||||
if ($startInfo.PSObject.Properties["ArgumentList"]) {
|
||||
foreach ($argument in $Arguments) {
|
||||
$startInfo.ArgumentList.Add($argument)
|
||||
}
|
||||
}
|
||||
else {
|
||||
$formattedArguments = @($Arguments | ForEach-Object { Format-CommandArgument $_ })
|
||||
$startInfo.Arguments = $formattedArguments -join " "
|
||||
}
|
||||
|
||||
$process = [System.Diagnostics.Process]::Start($startInfo)
|
||||
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderrTask = $process.StandardError.ReadToEndAsync()
|
||||
$process.WaitForExit()
|
||||
$stdout = $stdoutTask.GetAwaiter().GetResult()
|
||||
$stderr = $stderrTask.GetAwaiter().GetResult()
|
||||
|
||||
return [PSCustomObject]@{
|
||||
ExitCode = $process.ExitCode
|
||||
Output = if (-not [string]::IsNullOrWhiteSpace($stdout)) { $stdout } else { $stderr }
|
||||
Stdout = $stdout
|
||||
Stderr = $stderr
|
||||
}
|
||||
}
|
||||
|
||||
function Confirm-AzSdkCliMinimumVersion([string] $Executable, [version] $MinimumVersion) {
|
||||
$commandResult = Invoke-AzSdkCliCommand $Executable @("--version")
|
||||
$versionMatch = [regex]::Match($commandResult.Output, '(?<!\d)\d+\.\d+\.\d+(?:\.\d+)?(?!\d)')
|
||||
if ($commandResult.ExitCode -ne 0 -or -not $versionMatch.Success) {
|
||||
throw "Unable to determine the azsdk CLI version. Run 'azsdk --version' to verify the installation."
|
||||
}
|
||||
|
||||
$installedVersion = [version] $versionMatch.Value
|
||||
if ($installedVersion -lt $MinimumVersion) {
|
||||
throw "azsdk CLI version $MinimumVersion or later is required; found $installedVersion."
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-LoggedMsbuildCommand
|
||||
{
|
||||
[CmdletBinding()]
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Marks a published package as released in API Review Hub and APIView.
|
||||
|
||||
.DESCRIPTION
|
||||
Invokes the centralized Azure SDK CLI release-completion command and surfaces each backend result.
|
||||
|
||||
.PARAMETER PackageInfoFiles
|
||||
Package-info JSON files containing the published package name, version, and API hash.
|
||||
|
||||
.PARAMETER AzSdkExePath
|
||||
The path to the azsdk executable.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[array] $PackageInfoFiles,
|
||||
|
||||
[string] $RepoOwner = "",
|
||||
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $AzSdkExePath = "azsdk"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 4
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot common.ps1)
|
||||
|
||||
function Write-BackendResult([string] $Name, [object] $Result) {
|
||||
Write-Host $Name
|
||||
Write-Host " Result: $($Result | ConvertTo-Json -Compress -Depth 20)"
|
||||
}
|
||||
|
||||
function Set-PackageReleased([string] $PackageName, [string] $PackageVersion, [string] $ApiHash) {
|
||||
$arguments = @(
|
||||
"package",
|
||||
"mark-released",
|
||||
"--language", $LanguageShort,
|
||||
"--package-name", $PackageName,
|
||||
"--package-version", $PackageVersion
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ApiHash)) {
|
||||
$arguments += @("--api-hash", $ApiHash)
|
||||
}
|
||||
|
||||
$arguments += @("--output", "json")
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($RepoOwner)) {
|
||||
$arguments += @("--repo-owner", $RepoOwner)
|
||||
}
|
||||
|
||||
$hashDescription = if ([string]::IsNullOrWhiteSpace($ApiHash)) { "not provided" } else { $ApiHash }
|
||||
Write-Host "Marking package released: language=$LanguageShort, package=$PackageName, version=$PackageVersion, apiHash=$hashDescription"
|
||||
$formattedArguments = @($arguments | ForEach-Object { Format-CommandArgument $_ })
|
||||
Write-Host "Command: azsdk $($formattedArguments -join ' ')"
|
||||
|
||||
$commandResult = Invoke-AzSdkCliCommand $AzSdkExePath $arguments
|
||||
$exitCode = $commandResult.ExitCode
|
||||
|
||||
try {
|
||||
$response = $commandResult.Output | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
$capturedOutput = "stdout:`n$($commandResult.Stdout)`nstderr:`n$($commandResult.Stderr)"
|
||||
throw "Mark released returned malformed JSON for $PackageName $PackageVersion (azsdk exit code $exitCode). Captured output:`n$capturedOutput"
|
||||
}
|
||||
|
||||
$hasReviewHubResult = $response.PSObject.Properties["api_review_hub"] -and $null -ne $response.api_review_hub
|
||||
$hasApiViewResult = $response.PSObject.Properties["api_view"] -and $null -ne $response.api_view
|
||||
if ($hasReviewHubResult) {
|
||||
Write-BackendResult "API Review Hub" $response.api_review_hub
|
||||
}
|
||||
if ($hasApiViewResult) {
|
||||
Write-BackendResult "APIView" $response.api_view
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
[array] $errors = if ($response.PSObject.Properties["response_errors"]) { @($response.response_errors) } else { @() }
|
||||
$failureMessage = if ($errors.Count -gt 0) { $errors -join "; " } else { "azsdk exited with code $exitCode." }
|
||||
throw "Mark released failed: $failureMessage"
|
||||
}
|
||||
}
|
||||
|
||||
$packageInfoPaths = @($PackageInfoFiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($packageInfoPaths.Count -eq 0) {
|
||||
throw "At least one package-info file is required."
|
||||
}
|
||||
|
||||
Confirm-AzSdkCliMinimumVersion $AzSdkExePath ([version] "0.6.38")
|
||||
|
||||
$failures = @()
|
||||
foreach ($packageInfoFile in $packageInfoPaths) {
|
||||
try {
|
||||
if (-not (Test-Path $packageInfoFile -PathType Leaf)) {
|
||||
throw "Package-info file does not exist."
|
||||
}
|
||||
|
||||
$packageInfo = Get-Content $packageInfoFile -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||
$packageName = if ($packageInfo.PSObject.Properties["Name"]) { [string] $packageInfo.Name } else { "" }
|
||||
$packageVersion = if ($packageInfo.PSObject.Properties["Version"]) { [string] $packageInfo.Version } else { "" }
|
||||
$apiHash = if ($packageInfo.PSObject.Properties["ApiHash"]) { [string] $packageInfo.ApiHash } else { "" }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($packageName)) {
|
||||
throw "Package-info file does not contain a package Name."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($packageVersion)) {
|
||||
throw "Package-info file does not contain a package Version."
|
||||
}
|
||||
|
||||
Set-PackageReleased $packageName $packageVersion $apiHash
|
||||
}
|
||||
catch {
|
||||
Write-Error "Mark released failed for ${packageInfoFile}: $($_.Exception.Message)" -ErrorAction Continue
|
||||
$failures += "${packageInfoFile}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
throw "Mark released failed for $($failures.Count) package(s):`n$($failures -join "`n")"
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
Describe "Create-APIViewRevision.ps1" {
|
||||
BeforeAll {
|
||||
$scriptPath = Join-Path (Join-Path $PSScriptRoot "..") "Create-APIViewRevision.ps1"
|
||||
|
||||
function global:Find-Unknown-Artifacts-For-Apireview {
|
||||
param ([string] $ArtifactPath, [string] $ArtifactName)
|
||||
return @{ $ArtifactName = $global:TestPackagePath }
|
||||
}
|
||||
|
||||
function global:az {
|
||||
$global:LASTEXITCODE = 0
|
||||
return '{"accessToken":"test-token"}'
|
||||
}
|
||||
|
||||
function global:Invoke-WebRequest {
|
||||
param (
|
||||
[string] $Method,
|
||||
[string] $Uri,
|
||||
[object] $Body,
|
||||
[hashtable] $Headers,
|
||||
[int] $MaximumRetryCount
|
||||
)
|
||||
|
||||
$global:ApiViewRequests += [PSCustomObject]@{
|
||||
Method = $Method
|
||||
Uri = $Uri
|
||||
Body = if ($null -ne $Body) { $Body.ReadAsStringAsync().GetAwaiter().GetResult() } else { "" }
|
||||
Headers = $Headers
|
||||
MaximumRetryCount = $MaximumRetryCount
|
||||
}
|
||||
return [PSCustomObject]@{ Content = "created"; StatusCode = 200 }
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item Function:\Find-Unknown-Artifacts-For-Apireview -ErrorAction SilentlyContinue
|
||||
Remove-Item Function:\az -ErrorAction SilentlyContinue
|
||||
Remove-Item Function:\Invoke-WebRequest -ErrorAction SilentlyContinue
|
||||
Remove-Variable TestPackagePath, ApiViewRequests, LanguageShort -Scope Global -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
$global:LanguageShort = "Python"
|
||||
$testRoot = Join-Path $TestDrive "artifacts"
|
||||
$packageName = "test-package"
|
||||
$packageDirectory = Join-Path $testRoot $packageName
|
||||
$packageInfoDirectory = Join-Path $testRoot "PackageInfo"
|
||||
New-Item -ItemType Directory -Path $packageDirectory, $packageInfoDirectory -Force | Out-Null
|
||||
|
||||
$global:TestPackagePath = Join-Path $packageDirectory "$packageName.zip"
|
||||
Set-Content -Path $global:TestPackagePath -Value "package"
|
||||
$packageInfoPath = Join-Path $packageInfoDirectory "$packageName.json"
|
||||
@{
|
||||
ArtifactName = $packageName
|
||||
Name = $packageName
|
||||
Version = "1.0.0"
|
||||
SdkType = "client"
|
||||
ReleaseStatus = "Unreleased"
|
||||
} | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
$global:ApiViewRequests = @()
|
||||
}
|
||||
|
||||
It "uploads the source artifact when no review token exists" {
|
||||
& $scriptPath -ArtifactPath $testRoot -PackageName $packageName -SourceBranch main -DefaultBranch main
|
||||
|
||||
$global:ApiViewRequests.Count | Should Be 1
|
||||
$global:ApiViewRequests[0].Uri | Should Be "https://apiview.dev/autoreview/upload"
|
||||
$global:ApiViewRequests[0].Headers.Authorization | Should Be "Bearer test-token"
|
||||
$global:ApiViewRequests[0].MaximumRetryCount | Should Be 3
|
||||
$global:ApiViewRequests[0].Body | Should Match '(?s)name="?label"?.*?Source Branch:main'
|
||||
$global:ApiViewRequests[0].Body | Should Match '(?s)name="?packageVersion"?.*?1.0.0'
|
||||
$global:ApiViewRequests[0].Body | Should Match '(?s)name="?setReleaseTag"?.*?False'
|
||||
$global:ApiViewRequests[0].Body | Should Match '(?s)name="?packageType"?.*?client'
|
||||
}
|
||||
|
||||
It "creates a revision from a review token when one exists" {
|
||||
Set-Content -Path (Join-Path $packageDirectory "${packageName}_Python.json") -Value "{}"
|
||||
|
||||
& $scriptPath -ArtifactPath $testRoot -PackageName $packageName -SourceBranch main -DefaultBranch main -BuildId 123 -RepoName Azure/test
|
||||
|
||||
$global:ApiViewRequests.Count | Should Be 1
|
||||
$global:ApiViewRequests[0].Uri | Should Match "/create\?"
|
||||
$global:ApiViewRequests[0].Uri | Should Match "buildId=123"
|
||||
$global:ApiViewRequests[0].Uri | Should Match "repoName=Azure%2ftest"
|
||||
$global:ApiViewRequests[0].Uri | Should Match "packageName=test-package"
|
||||
$global:ApiViewRequests[0].Uri | Should Match "reviewFilePath=test-package_Python.json"
|
||||
$global:ApiViewRequests[0].Uri | Should Not Match "setReleaseTag"
|
||||
$global:ApiViewRequests[0].MaximumRetryCount | Should Be 3
|
||||
}
|
||||
|
||||
It "requires pipeline metadata when creating from a review token" {
|
||||
Set-Content -Path (Join-Path $packageDirectory "${packageName}_Python.json") -Value "{}"
|
||||
|
||||
$caughtError = $null
|
||||
try {
|
||||
& $scriptPath -ArtifactPath $testRoot -PackageName $packageName -SourceBranch main -DefaultBranch main
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError.Exception.Message | Should Match "BuildId is required"
|
||||
$global:ApiViewRequests.Count | Should Be 0
|
||||
}
|
||||
|
||||
It "requires branch metadata before processing packages" {
|
||||
$caughtError = $null
|
||||
try {
|
||||
& $scriptPath -ArtifactPath $testRoot -PackageName $packageName -SourceBranch "" -DefaultBranch main
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError.Exception.Message | Should Match "SourceBranch is required"
|
||||
$global:ApiViewRequests.Count | Should Be 0
|
||||
}
|
||||
|
||||
It "skips prerelease revisions from feature branches" {
|
||||
$packageInfo = Get-Content $packageInfoPath -Raw | ConvertFrom-Json
|
||||
$packageInfo.Version = "1.0.0-beta.1"
|
||||
$packageInfo | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
|
||||
& $scriptPath -ArtifactPath $testRoot -PackageName $packageName -SourceBranch feature -DefaultBranch main
|
||||
|
||||
$global:ApiViewRequests.Count | Should Be 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
Describe "Get-PackageApprovalStatus.ps1" {
|
||||
BeforeAll {
|
||||
$scriptPath = Join-Path (Join-Path $PSScriptRoot "..") "Get-PackageApprovalStatus.ps1"
|
||||
|
||||
function global:azsdk {
|
||||
param (
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[object[]] $Arguments
|
||||
)
|
||||
|
||||
if ($Arguments.Count -eq 1 -and $Arguments[0] -eq "--version") {
|
||||
$global:LASTEXITCODE = 0
|
||||
return $global:AzSdkVersion
|
||||
}
|
||||
|
||||
$global:CapturedAzSdkArguments = @($Arguments)
|
||||
$global:CapturedAzSdkInvocations += ,@($Arguments)
|
||||
$global:LASTEXITCODE = $global:AzSdkExitCode
|
||||
return $global:AzSdkOutput
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item Function:\azsdk -ErrorAction SilentlyContinue
|
||||
Remove-Variable AzSdkExitCode, AzSdkOutput, AzSdkVersion, CapturedAzSdkArguments, CapturedAzSdkInvocations, LanguageShort -Scope Global -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
$global:LanguageShort = "python"
|
||||
$global:AzSdkExitCode = 0
|
||||
$global:AzSdkVersion = "0.6.38"
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","result":{"isApproved":true,"finalSource":"reviewHub","reason":"approved"}}'
|
||||
$global:CapturedAzSdkArguments = @()
|
||||
$global:CapturedAzSdkInvocations = @()
|
||||
$packageInfoPath = Join-Path $TestDrive "azure-test.json"
|
||||
@{
|
||||
Name = "azure-test"
|
||||
Version = "1.0.0"
|
||||
} | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
}
|
||||
|
||||
It "passes package coordinates, API hash, and repository owner to azsdk" {
|
||||
$packageInfo = Get-Content $packageInfoPath -Raw | ConvertFrom-Json
|
||||
$packageInfo | Add-Member -NotePropertyName ApiHash -NotePropertyValue abc123
|
||||
$packageInfo | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath -RepoOwner Contoso
|
||||
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Be (@(
|
||||
"package", "get-approval-status",
|
||||
"--language", "python",
|
||||
"--package-name", "azure-test",
|
||||
"--package-version", "1.0.0",
|
||||
"--output", "json",
|
||||
"--api-hash", "abc123",
|
||||
"--repo-owner", "Contoso"
|
||||
) -join "|")
|
||||
}
|
||||
|
||||
It "omits the API hash when it is unavailable" {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Not Match "--api-hash"
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Not Match "--repo-owner"
|
||||
}
|
||||
|
||||
It "fails without prompting when the azsdk executable is unavailable" {
|
||||
$missingExecutable = Join-Path $TestDrive "missing-azsdk.exe"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath -AzSdkExePath $missingExecutable
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "azsdk CLI executable was not found"
|
||||
}
|
||||
|
||||
It "fails when the azsdk version is unsupported" {
|
||||
$global:AzSdkVersion = "0.6.37"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError.Exception.Message | Should Match "version 0.6.38 or later is required"
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 0
|
||||
}
|
||||
|
||||
It "fails when azsdk returns a nonzero exit code" {
|
||||
$global:AzSdkExitCode = 1
|
||||
$global:AzSdkOutput = '{"operation_status":"Failed","response_error":"distinct raw command output"}'
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "distinct raw command output"
|
||||
}
|
||||
|
||||
It "logs a reproducible command invocation" {
|
||||
$packageInfo = Get-Content $packageInfoPath -Raw | ConvertFrom-Json
|
||||
$packageInfo.Name = "azure test"
|
||||
$packageInfo | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
$messages = @(& $scriptPath -PackageInfoFiles $packageInfoPath 6>&1)
|
||||
|
||||
($messages -join [Environment]::NewLine) | Should Match 'Command: azsdk package get-approval-status --language python --package-name "azure test" --package-version 1.0.0 --output json'
|
||||
}
|
||||
|
||||
It "shows Review Hub and APIView results before the overall result" {
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","result":{"isApproved":true,"finalSource":"APIView","reason":"approved","reviewHub":{"isApproved":false,"reason":"repositoryNotSupported","statusCode":200},"apiView":{"isApproved":true,"reason":"approved","statusCode":200,"details":["API review is approved."]}}}'
|
||||
|
||||
$messages = @(& $scriptPath -PackageInfoFiles $packageInfoPath 6>&1) |
|
||||
ForEach-Object { "$_" }
|
||||
|
||||
[Array]::IndexOf($messages, "API Review Hub") | Should BeLessThan ([Array]::IndexOf($messages, "APIView"))
|
||||
[Array]::IndexOf($messages, "APIView") | Should BeLessThan ([Array]::IndexOf($messages, "Overall"))
|
||||
($messages -join [Environment]::NewLine) | Should Match "Overall\r?\n Status: APPROVED\r?\n Source: APIView\r?\n Reason: approved"
|
||||
}
|
||||
|
||||
It "includes raw output when azsdk returns malformed output" {
|
||||
$global:AzSdkExitCode = 1
|
||||
$global:AzSdkOutput = "distinct raw command output"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "distinct raw command output"
|
||||
}
|
||||
|
||||
It "fails when the response is malformed" {
|
||||
$global:AzSdkOutput = "not json"
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles $packageInfoPath } |
|
||||
Should Throw
|
||||
}
|
||||
|
||||
It "fails when a successful CLI invocation reports an unapproved result" {
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","result":{"isApproved":false,"finalSource":"none","reason":"pending"}}'
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles $packageInfoPath } |
|
||||
Should Throw
|
||||
}
|
||||
|
||||
It "ignores a failed approval check for an unreleased package" {
|
||||
$packageInfo = Get-Content $packageInfoPath -Raw | ConvertFrom-Json
|
||||
$packageInfo | Add-Member -NotePropertyName ReleaseStatus -NotePropertyValue Unreleased
|
||||
$packageInfo | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
$global:AzSdkExitCode = 1
|
||||
$global:AzSdkOutput = '{"operation_status":"Failed","response_error":"Package is not approved."}'
|
||||
|
||||
$messages = @(& $scriptPath -PackageInfoFiles $packageInfoPath 6>&1)
|
||||
|
||||
($messages -join [Environment]::NewLine) | Should Match "azure-test 1.0.0 is not marked for release. Ignoring approval check failure"
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 1
|
||||
}
|
||||
|
||||
It "fails when the response contract is missing the result" {
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded"}'
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles $packageInfoPath } |
|
||||
Should Throw
|
||||
}
|
||||
|
||||
It "fails when the approval decision is not Boolean" {
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","result":{"isApproved":"false"}}'
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles $packageInfoPath } |
|
||||
Should Throw
|
||||
}
|
||||
|
||||
It "checks every explicitly supplied package-info file" {
|
||||
$secondPackageInfoPath = Join-Path $TestDrive "azure-test-two.json"
|
||||
@{
|
||||
Name = "azure-test-two"
|
||||
Version = "2.0.0"
|
||||
ApiHash = "def456"
|
||||
} | ConvertTo-Json | Set-Content $secondPackageInfoPath
|
||||
|
||||
& $scriptPath -PackageInfoFiles @($packageInfoPath, $secondPackageInfoPath)
|
||||
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 2
|
||||
($global:CapturedAzSdkInvocations[0] -join "|") | Should Match "--package-name\|azure-test\|--package-version\|1.0.0"
|
||||
($global:CapturedAzSdkInvocations[1] -join "|") | Should Match "--package-name\|azure-test-two\|--package-version\|2.0.0.*--api-hash\|def456"
|
||||
}
|
||||
|
||||
It "continues checking valid packages after invalid package info" {
|
||||
$invalidPackageInfoPath = Join-Path $TestDrive "invalid.json"
|
||||
Set-Content $invalidPackageInfoPath "not json"
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles @($invalidPackageInfoPath, $packageInfoPath) } |
|
||||
Should Throw
|
||||
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
Describe "Mark-PackageReleased.ps1" {
|
||||
BeforeAll {
|
||||
$scriptPath = Join-Path (Join-Path $PSScriptRoot "..") "Mark-PackageReleased.ps1"
|
||||
|
||||
function global:azsdk {
|
||||
param (
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[object[]] $Arguments
|
||||
)
|
||||
|
||||
if ($Arguments.Count -eq 1 -and $Arguments[0] -eq "--version") {
|
||||
$global:LASTEXITCODE = 0
|
||||
return $global:AzSdkVersion
|
||||
}
|
||||
|
||||
$global:CapturedAzSdkArguments = @($Arguments)
|
||||
$global:CapturedAzSdkInvocations += ,@($Arguments)
|
||||
$global:LASTEXITCODE = $global:AzSdkExitCode
|
||||
return $global:AzSdkOutput
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Item Function:\azsdk -ErrorAction SilentlyContinue
|
||||
Remove-Variable AzSdkExitCode, AzSdkOutput, AzSdkVersion, CapturedAzSdkArguments, CapturedAzSdkInvocations, LanguageShort -Scope Global -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
$global:LanguageShort = "python"
|
||||
$global:AzSdkExitCode = 0
|
||||
$global:AzSdkVersion = "0.6.38"
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","api_review_hub":{"packageVersionId":"version123","isReleased":true},"api_view":{"revisionId":"revision456","isReleased":true}}'
|
||||
$global:CapturedAzSdkArguments = @()
|
||||
$global:CapturedAzSdkInvocations = @()
|
||||
$packageInfoPath = Join-Path $TestDrive "azure-test.json"
|
||||
@{
|
||||
Name = "azure-test"
|
||||
Version = "1.0.0"
|
||||
ApiHash = "abc123"
|
||||
} | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
}
|
||||
|
||||
It "passes package-info release inputs to azsdk" {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath -RepoOwner Azure
|
||||
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Be (@(
|
||||
"package", "mark-released",
|
||||
"--language", "python",
|
||||
"--package-name", "azure-test",
|
||||
"--package-version", "1.0.0",
|
||||
"--api-hash", "abc123",
|
||||
"--output", "json",
|
||||
"--repo-owner", "Azure"
|
||||
) -join "|")
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Not Match "--dry-run"
|
||||
}
|
||||
|
||||
It "omits the optional repository owner" {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Not Match "--repo-owner"
|
||||
}
|
||||
|
||||
It "omits the optional ApiHash" {
|
||||
$packageInfo = Get-Content $packageInfoPath -Raw | ConvertFrom-Json
|
||||
$packageInfo.PSObject.Properties.Remove("ApiHash")
|
||||
$packageInfo | ConvertTo-Json | Set-Content $packageInfoPath
|
||||
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 1
|
||||
($global:CapturedAzSdkArguments -join "|") | Should Not Match "--api-hash"
|
||||
}
|
||||
|
||||
It "fails without prompting when the azsdk executable is unavailable" {
|
||||
$missingExecutable = Join-Path $TestDrive "missing-azsdk.exe"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath -AzSdkExePath $missingExecutable
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "azsdk CLI executable was not found"
|
||||
}
|
||||
|
||||
It "fails when the azsdk version is unsupported" {
|
||||
$global:AzSdkVersion = "0.6.37"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError.Exception.Message | Should Match "version 0.6.38 or later is required"
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 0
|
||||
}
|
||||
|
||||
It "shows both backend results" {
|
||||
$messages = @(& $scriptPath -PackageInfoFiles $packageInfoPath 6>&1) |
|
||||
ForEach-Object { "$_" }
|
||||
|
||||
[Array]::IndexOf($messages, "API Review Hub") | Should BeLessThan ([Array]::IndexOf($messages, "APIView"))
|
||||
($messages -join [Environment]::NewLine) | Should Match '"packageVersionId":"version123"'
|
||||
($messages -join [Environment]::NewLine) | Should Match '"revisionId":"revision456"'
|
||||
}
|
||||
|
||||
It "surfaces partial backend failure details from azsdk" {
|
||||
$global:AzSdkExitCode = 1
|
||||
$global:AzSdkOutput = '{"operation_status":"Failed","api_review_hub":{"packageVersionId":"version123"},"api_view":null,"response_errors":["APIView: APIView failed"]}'
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "APIView: APIView failed"
|
||||
}
|
||||
|
||||
It "includes raw output when azsdk returns malformed output" {
|
||||
$global:AzSdkExitCode = 1
|
||||
$global:AzSdkOutput = "distinct raw command output"
|
||||
$caughtError = $null
|
||||
|
||||
try {
|
||||
& $scriptPath -PackageInfoFiles $packageInfoPath
|
||||
}
|
||||
catch {
|
||||
$caughtError = $_
|
||||
}
|
||||
|
||||
$caughtError | Should Not BeNullOrEmpty
|
||||
$caughtError.Exception.Message | Should Match "distinct raw command output"
|
||||
}
|
||||
|
||||
It "accepts a successful response with a missing backend result" {
|
||||
$global:AzSdkOutput = '{"operation_status":"Succeeded","api_review_hub":{"packageVersionId":"version123"},"api_view":null}'
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles $packageInfoPath } | Should Not Throw
|
||||
}
|
||||
|
||||
It "marks every explicitly supplied package-info file" {
|
||||
$secondPackageInfoPath = Join-Path $TestDrive "azure-test-two.json"
|
||||
@{
|
||||
Name = "azure-test-two"
|
||||
Version = "2.0.0"
|
||||
ApiHash = "def456"
|
||||
} | ConvertTo-Json | Set-Content $secondPackageInfoPath
|
||||
|
||||
& $scriptPath -PackageInfoFiles @($packageInfoPath, $secondPackageInfoPath)
|
||||
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 2
|
||||
($global:CapturedAzSdkInvocations[0] -join "|") | Should Match "--package-name\|azure-test\|--package-version\|1.0.0.*--api-hash\|abc123"
|
||||
($global:CapturedAzSdkInvocations[1] -join "|") | Should Match "--package-name\|azure-test-two\|--package-version\|2.0.0.*--api-hash\|def456"
|
||||
}
|
||||
|
||||
It "continues marking valid packages after invalid package info" {
|
||||
$invalidPackageInfoPath = Join-Path $TestDrive "invalid.json"
|
||||
Set-Content $invalidPackageInfoPath "not json"
|
||||
|
||||
{ & $scriptPath -PackageInfoFiles @($invalidPackageInfoPath, $packageInfoPath) } |
|
||||
Should Throw
|
||||
|
||||
$global:CapturedAzSdkInvocations.Count | Should Be 1
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user