[CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [array] $ArtifactList, [Parameter(Mandatory=$False)] [string] $ArtifactPath, [string] $SourceBranch, [string] $DefaultBranch, [string] $RepoName, [string] $BuildId, [string] $PackageName = "", [string] $ConfigFileDir = "", [string] $APIViewUri = "https://apiview.dev/autoreview", [string] $ArtifactName = "packages", [bool] $MarkPackageAsShipped = $false, [Parameter(Mandatory=$False)] [array] $PackageInfoFiles ) Set-StrictMode -Version 3 . (Join-Path $PSScriptRoot common.ps1) . (Join-Path $PSScriptRoot Helpers ApiView-Helpers.ps1) # Get Bearer token for APIView authentication function Get-ApiViewBearerToken() { $audience = "api://apiview" try { $tokenResponse = az account get-access-token --resource $audience --output json 2>$null | ConvertFrom-Json if ($tokenResponse -and $tokenResponse.accessToken) { return $tokenResponse.accessToken } return $null } catch { return $null } } # Submit API review request and return status whether current revision is approved or pending or failed to create review function Upload-SourceArtifact($filePath, $apiLabel, $releaseStatus, $packageVersion, $packageType) { Write-Host "File path: $filePath" $fileName = Split-Path -Leaf $filePath Write-Host "File name: $fileName" $multipartContent = [System.Net.Http.MultipartFormDataContent]::new() $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) $stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $stringHeader.Name = "label" $StringContent = [System.Net.Http.StringContent]::new($apiLabel) $StringContent.Headers.ContentDisposition = $stringHeader $multipartContent.Add($stringContent) Write-Host "Request param, label: $apiLabel" $versionParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $versionParam.Name = "packageVersion" $versionContent = [System.Net.Http.StringContent]::new($packageVersion) $versionContent.Headers.ContentDisposition = $versionParam $multipartContent.Add($versionContent) Write-Host "Request param, packageVersion: $packageVersion" $releaseTagParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $releaseTagParam.Name = "setReleaseTag" $releaseTagParamContent = [System.Net.Http.StringContent]::new($MarkPackageAsShipped) $releaseTagParamContent.Headers.ContentDisposition = $releaseTagParam $multipartContent.Add($releaseTagParamContent) Write-Host "Request param, setReleaseTag: $MarkPackageAsShipped" $packageTypeParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $packageTypeParam.Name = "packageType" $packageTypeParamContent = [System.Net.Http.StringContent]::new($packageType) $packageTypeParamContent.Headers.ContentDisposition = $packageTypeParam $multipartContent.Add($packageTypeParamContent) Write-Host "Request param, packageType: $packageType" if ($releaseStatus -and ($releaseStatus -ne "Unreleased")) { $compareAllParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $compareAllParam.Name = "compareAllRevisions" $compareAllParamContent = [System.Net.Http.StringContent]::new($true) $compareAllParamContent.Headers.ContentDisposition = $compareAllParam $multipartContent.Add($compareAllParamContent) Write-Host "Request param, compareAllRevisions: true" } $uri = "${APIViewUri}/upload" # Get Bearer token for authentication $bearerToken = Get-ApiViewBearerToken if (-not $bearerToken) { Write-Error "Failed to acquire Bearer token for APIView authentication." return 401 } $headers = @{ "Authorization" = "Bearer $bearerToken"; "content-type" = "multipart/form-data" } try { $Response = Invoke-WebRequest -Method 'POST' -Uri $uri -Body $multipartContent -Headers $headers Write-Host "API review: $($Response.Content)" $StatusCode = $Response.StatusCode } catch { Write-Host "ERROR: API request failed" -ForegroundColor Red Write-Host "Status Code: $($_.Exception.Response.StatusCode.Value__)" -ForegroundColor Yellow Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Yellow if ($_.ErrorDetails.Message) { Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow } $StatusCode = $_.Exception.Response.StatusCode } return $StatusCode } function Upload-ReviewTokenFile($packageName, $apiLabel, $releaseStatus, $reviewFileName, $packageVersion, $filePath, $packageType) { Write-Host "Original File path: $filePath" $fileName = Split-Path -Leaf $filePath Write-Host "OriginalFile name: $fileName" $params = "buildId=${BuildId}&artifactName=${ArtifactName}&originalFilePath=${fileName}&reviewFilePath=${reviewFileName}" $params +="&label=${apiLabel}&repoName=${RepoName}&packageName=${packageName}&project=internal&packageVersion=${packageVersion}&packageType=${packageType}" if($MarkPackageAsShipped) { $params += "&setReleaseTag=true" } $uri = "${APIViewUri}/create?${params}" if ($releaseStatus -and ($releaseStatus -ne "Unreleased")) { $uri += "&compareAllRevisions=true" } Write-Host "Request to APIView: $uri" # Get Bearer token for authentication $bearerToken = Get-ApiViewBearerToken if (-not $bearerToken) { Write-Error "Failed to acquire Bearer token for APIView authentication." return 401 } $headers = @{ "Authorization" = "Bearer $bearerToken" } try { $Response = Invoke-WebRequest -Method 'POST' -Uri $uri -Headers $headers Write-Host "API review: $($Response.Content)" $StatusCode = $Response.StatusCode } catch { Write-Host "ERROR: API request failed" -ForegroundColor Red Write-Host "Status Code: $($_.Exception.Response.StatusCode.Value__)" -ForegroundColor Yellow Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Yellow if ($_.ErrorDetails.Message) { Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow } $StatusCode = $_.Exception.Response.StatusCode } return $StatusCode } function Get-APITokenFileName($packageName) { $reviewTokenFileName = "${packageName}_${LanguageShort}.json" $tokenFilePath = Join-Path $ArtifactPath $packageName $reviewTokenFileName if (Test-Path $tokenFilePath) { Write-Host "Review token file is present at $tokenFilePath" return $reviewTokenFileName } else { Write-Host "Review token file is not present at $tokenFilePath" return $null } } function Submit-APIReview($packageInfo, $packagePath) { $apiLabel = "Source Branch:${SourceBranch}" $packageType = $packageInfo.SdkType # Get generated review token file if present # APIView processes request using different API if token file is already generated $reviewTokenFileName = Get-APITokenFileName $packageInfo.ArtifactName if ($reviewTokenFileName) { Write-Host "Uploading review token file $reviewTokenFileName to APIView." return Upload-ReviewTokenFile $packageInfo.ArtifactName $apiLabel $packageInfo.ReleaseStatus $reviewTokenFileName $packageInfo.Version $packagePath $packageType } else { Write-Host "Uploading $packagePath to APIView." return Upload-SourceArtifact $packagePath $apiLabel $packageInfo.ReleaseStatus $packageInfo.Version $packageType } } function IsApiviewStatusCheckRequired($packageInfo) { if ($IsApiviewStatusCheckRequiredFn -and (Test-Path "Function:$IsApiviewStatusCheckRequiredFn")) { return &$IsApiviewStatusCheckRequiredFn $packageInfo } if ($packageInfo.SdkType -eq "client" -and $packageInfo.IsNewSdk) { return $true } return $false } function ProcessPackage($packageInfo) { $packages = @{} if ($FindArtifactForApiReviewFn -and (Test-Path "Function:$FindArtifactForApiReviewFn")) { $pkgArtifactName = $packageInfo.ArtifactName ?? $packageInfo.Name # Check if the function supports the packageInfo parameter $functionInfo = Get-Command $FindArtifactForApiReviewFn -ErrorAction SilentlyContinue $supportsPackageInfoParam = $false if ($functionInfo -and $functionInfo.Parameters) { # Check if function specifically supports packageInfo parameter $parameterNames = $functionInfo.Parameters.Keys $supportsPackageInfoParam = $parameterNames -contains 'packageInfo' } # Call function with appropriate parameters if ($supportsPackageInfoParam) { LogInfo "Calling $FindArtifactForApiReviewFn with packageInfo parameter" $packages = &$FindArtifactForApiReviewFn $ArtifactPath $packageInfo } else { LogInfo "Calling $FindArtifactForApiReviewFn with legacy parameters" $packages = &$FindArtifactForApiReviewFn $ArtifactPath $pkgArtifactName } } else { Write-Host "The function for 'FindArtifactForApiReviewFn' was not found.` Make sure it is present in eng/scripts/Language-Settings.ps1 and referenced in eng/common/scripts/common.ps1.` See https://github.com/Azure/azure-sdk-tools/blob/main/doc/common/common_engsys.md#code-structure" return 1 } if ($packages) { foreach($pkgPath in $packages.Values) { $pkg = Split-Path -Leaf $pkgPath $version = [AzureEngSemanticVersion]::ParseVersionString($packageInfo.Version) if ($null -eq $version) { Write-Host "Version info is not available for package $($packageInfo.ArtifactName), because version '$($packageInfo.Version)' is invalid. Please check if the version follows Azure SDK package versioning guidelines." return 1 } Write-Host "Version: $($version)" Write-Host "SDK Type: $($packageInfo.SdkType)" Write-Host "Release Status: $($packageInfo.ReleaseStatus)" # Run create review step only if build is triggered from main branch or if version is GA. # This is to avoid invalidating review status by a build triggered from feature branch if ( ($SourceBranch -eq $DefaultBranch) -or (-not $version.IsPrerelease) -or $MarkPackageAsShipped) { Write-Host "Submitting API Review request for package $($pkg), File path: $($pkgPath)" $respCode = Submit-APIReview $packageInfo $pkgPath Write-Host "HTTP Response code: $($respCode)" # no need to check API review status when marking a package as shipped if ($MarkPackageAsShipped) { if ($respCode -eq '500') { Write-Host "Failed to mark package $($packageInfo.ArtifactName) as released. Please reach out to Azure SDK engineering systems on teams channel." return 1 } Write-Host "Package $($packageInfo.ArtifactName) is marked as released." return 0 } $apiStatus = [PSCustomObject]@{ IsApproved = $false Details = "" } $pkgNameStatus = [PSCustomObject]@{ IsApproved = $false Details = "" } Process-ReviewStatusCode $respCode $packageInfo.ArtifactName $apiStatus $pkgNameStatus if ($apiStatus.IsApproved) { Write-Host "API status: $($apiStatus.Details)" } elseif (!$packageInfo.ReleaseStatus -or $packageInfo.ReleaseStatus -eq "Unreleased") { Write-Host "Release date is not set for current version in change log file for package. Ignoring API review approval status since package is not yet ready for release." } elseif ($version.IsPrerelease) { # Check if package name is approved. Preview version cannot be released without package name approval if (!$pkgNameStatus.IsApproved) { if (IsApiviewStatusCheckRequired $packageInfo) { Write-Error $($pkgNameStatus.Details) return 1 } else{ Write-Host "Package name is not approved for package $($packageInfo.ArtifactName), however it is not required for this package type so it can still be released without API review approval." } } # Ignore API review status for prerelease version Write-Host "Package version is not GA. Ignoring API view approval status" } else { # Return error code if status code is 201 for new data plane package # Temporarily enable API review for spring SDK types. Ideally this should be done be using 'IsReviewRequired' method in language side # to override default check of SDK type client if (IsApiviewStatusCheckRequired $packageInfo) { if (!$apiStatus.IsApproved) { Write-Host "Package version $($version) is GA and automatic API Review is not yet approved for package $($packageInfo.ArtifactName)." Write-Host "Build and release is not allowed for GA package without API review approval." Write-Host "You will need to queue another build to proceed further after API review is approved" Write-Host "You can check http://aka.ms/azsdk/engsys/apireview/faq for more details on API Approval." } return 1 } else { Write-Host "API review is not approved for package $($packageInfo.ArtifactName), however it is not required for this package type so it can still be released without API review approval." } } } else { Write-Host "Build is triggered from $($SourceBranch) with prerelease version. Skipping API review status check." } } } else { Write-Host "No package is found in artifact path to submit review request" } return 0 } Write-Host "Artifact path: $($ArtifactPath)" Write-Host "Source branch: $($SourceBranch)" Write-Host "Package Info Files: $($PackageInfoFiles)" Write-Host "Artifact List: $($ArtifactList)" Write-Host "Package Name: $($PackageName)" # Parameter priority and backward compatibility logic # Priority order: PackageName > Artifacts > PackageInfoFiles (for backward compatibility) if (-not $ConfigFileDir) { $ConfigFileDir = Join-Path -Path $ArtifactPath "PackageInfo" } Write-Host "Config File directory: $($ConfigFileDir)" # Initialize working variable $ProcessedPackageInfoFiles = @() if ($PackageName -and $PackageName -ne "") { # Highest Priority: Single package mode (existing usage) Write-Host "Using PackageName parameter: $PackageName" $pkgPropPath = Join-Path -Path $ConfigFileDir "$PackageName.json" if (Test-Path $pkgPropPath) { $ProcessedPackageInfoFiles = @($pkgPropPath) } else { Write-Error "Package property file path $pkgPropPath is invalid." exit 1 } } elseif ($ArtifactList -and $ArtifactList.Count -gt 0) { # Second Priority: Multiple artifacts mode (existing usage) Write-Host "Using ArtifactList parameter with $($ArtifactList.Count) artifacts" foreach ($artifact in $ArtifactList) { $pkgPropPath = Join-Path -Path $ConfigFileDir "$($artifact.name).json" if (Test-Path $pkgPropPath) { $ProcessedPackageInfoFiles += $pkgPropPath } else { Write-Warning "Package property file path $pkgPropPath is invalid." } } } elseif ($PackageInfoFiles -and $PackageInfoFiles.Count -gt 0) { # Lowest Priority: Direct PackageInfoFiles (new method) Write-Host "Using PackageInfoFiles parameter with $($PackageInfoFiles.Count) files" # Filter out empty strings or whitespace-only entries $ProcessedPackageInfoFiles = @($PackageInfoFiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } else { Write-Error "No package information provided. Please provide either 'PackageName', 'ArtifactList', or 'PackageInfoFiles' parameters." exit 1 } # Validate that we have package info files to process if (-not $ProcessedPackageInfoFiles -or $ProcessedPackageInfoFiles.Count -eq 0) { Write-Error "No package info files found after processing parameters. Or PackageInfoFiles parameter contains only empty or whitespace entries, please check the artifact settings." exit 1 } $responses = @{} Write-Host "Processed Package Info Files: $($ProcessedPackageInfoFiles -join ', ')" # Process all packages using the processed PackageInfoFiles array foreach ($packageInfoFile in $ProcessedPackageInfoFiles) { $packageInfo = Get-Content $packageInfoFile | ConvertFrom-Json Write-Host "Processing $($packageInfo.ArtifactName)" $result = ProcessPackage -packageInfo $packageInfo $responses[$packageInfo.ArtifactName] = $result } $exitCode = 0 foreach($pkg in $responses.keys) { if ($responses[$pkg] -eq 1) { Write-Host "API changes are not approved for $($pkg)" $exitCode = 1 } } exit $exitCode