microsoft/typespec
Publicmirrored fromhttps://github.com/microsoft/typespecAvailable
eng/emitters/scripts/Create-APIReview.ps1
380lines · modecode
| 1 | [CmdletBinding()] |
| 2 | Param ( |
| 3 | [Parameter(Mandatory=$True)] |
| 4 | [array] $ArtifactList, |
| 5 | [Parameter(Mandatory=$True)] |
| 6 | [string] $ArtifactPath, |
| 7 | [string] $SourceBranch, |
| 8 | [string] $DefaultBranch, |
| 9 | [string] $RepoName, |
| 10 | [string] $BuildId, |
| 11 | [string] $PackageName = "", |
| 12 | [string] $ConfigFileDir = "", |
| 13 | [string] $APIViewUri = "https://apiview.dev/autoreview", |
| 14 | [string] $ArtifactName = "packages", |
| 15 | [bool] $MarkPackageAsShipped = $false, |
| 16 | [string] $LanguageShortName = "Unknown" |
| 17 | ) |
| 18 | |
| 19 | Set-StrictMode -Version 3 |
| 20 | . (Join-Path $PSScriptRoot ApiView-Helpers.ps1) |
| 21 | . (Join-Path $PSScriptRoot SemVer.ps1) |
| 22 | |
| 23 | # Get Bearer token for APIView authentication |
| 24 | # In Azure DevOps, this uses the service connection's Managed Identity/Service Principal |
| 25 | function Get-ApiViewBearerToken() |
| 26 | { |
| 27 | try { |
| 28 | $tokenResponse = az account get-access-token --resource "api://apiview" --output json 2>&1 |
| 29 | if ($LASTEXITCODE -ne 0) { |
| 30 | Write-Error "Failed to acquire access token. Please ensure Azure CLI is authenticated and has access to the APIView resource." |
| 31 | return $null |
| 32 | } |
| 33 | return ($tokenResponse | ConvertFrom-Json).accessToken |
| 34 | } |
| 35 | catch { |
| 36 | Write-Error "Failed to acquire access token: $($_.Exception.Message)" |
| 37 | return $null |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | if ($LanguageShortName -eq "Unknown") |
| 42 | { |
| 43 | Write-Host "Language short name is not provided. Please provide the language short name." |
| 44 | exit 1 |
| 45 | } |
| 46 | else |
| 47 | { |
| 48 | $functionScriptPath = Join-Path $PSScriptRoot "/../../../packages/http-client-$LanguageShortName/eng/scripts/Functions.ps1" |
| 49 | if (!(Test-Path $functionScriptPath)) |
| 50 | { |
| 51 | Write-Host "Functions script path $($functionScriptPath) is invalid." |
| 52 | exit 1 |
| 53 | } |
| 54 | . ($functionScriptPath) |
| 55 | } |
| 56 | |
| 57 | # Submit API review request and return status whether current revision is approved or pending or failed to create review |
| 58 | function Upload-SourceArtifact($filePath, $apiLabel, $releaseStatus, $packageVersion) |
| 59 | { |
| 60 | Write-Host "File path: $filePath" |
| 61 | $fileName = Split-Path -Leaf $filePath |
| 62 | Write-Host "File name: $fileName" |
| 63 | $multipartContent = [System.Net.Http.MultipartFormDataContent]::new() |
| 64 | $FileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open) |
| 65 | $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") |
| 66 | $fileHeader.Name = "file" |
| 67 | $fileHeader.FileName = $fileName |
| 68 | $fileContent = [System.Net.Http.StreamContent]::new($FileStream) |
| 69 | $fileContent.Headers.ContentDisposition = $fileHeader |
| 70 | $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream") |
| 71 | $multipartContent.Add($fileContent) |
| 72 | |
| 73 | |
| 74 | $stringHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") |
| 75 | $stringHeader.Name = "label" |
| 76 | $StringContent = [System.Net.Http.StringContent]::new($apiLabel) |
| 77 | $StringContent.Headers.ContentDisposition = $stringHeader |
| 78 | $multipartContent.Add($stringContent) |
| 79 | Write-Host "Request param, label: $apiLabel" |
| 80 | |
| 81 | $versionParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") |
| 82 | $versionParam.Name = "packageVersion" |
| 83 | $versionContent = [System.Net.Http.StringContent]::new($packageVersion) |
| 84 | $versionContent.Headers.ContentDisposition = $versionParam |
| 85 | $multipartContent.Add($versionContent) |
| 86 | Write-Host "Request param, packageVersion: $packageVersion" |
| 87 | |
| 88 | $releaseTagParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") |
| 89 | $releaseTagParam.Name = "setReleaseTag" |
| 90 | $releaseTagParamContent = [System.Net.Http.StringContent]::new($MarkPackageAsShipped) |
| 91 | $releaseTagParamContent.Headers.ContentDisposition = $releaseTagParam |
| 92 | $multipartContent.Add($releaseTagParamContent) |
| 93 | Write-Host "Request param, setReleaseTag: $MarkPackageAsShipped" |
| 94 | |
| 95 | if ($releaseStatus -and ($releaseStatus -ne "Unreleased")) |
| 96 | { |
| 97 | $compareAllParam = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") |
| 98 | $compareAllParam.Name = "compareAllRevisions" |
| 99 | $compareAllParamContent = [System.Net.Http.StringContent]::new($true) |
| 100 | $compareAllParamContent.Headers.ContentDisposition = $compareAllParam |
| 101 | $multipartContent.Add($compareAllParamContent) |
| 102 | Write-Host "Request param, compareAllRevisions: true" |
| 103 | } |
| 104 | |
| 105 | $uri = "${APIViewUri}/upload" |
| 106 | |
| 107 | # Get Bearer token for authentication |
| 108 | $bearerToken = Get-ApiViewBearerToken |
| 109 | if (-not $bearerToken) { |
| 110 | Write-Error "Failed to acquire Bearer token for APIView authentication." |
| 111 | return [System.Net.HttpStatusCode]::Unauthorized |
| 112 | } |
| 113 | |
| 114 | $headers = @{ |
| 115 | "Authorization" = "Bearer $bearerToken"; |
| 116 | "content-type" = "multipart/form-data" |
| 117 | } |
| 118 | |
| 119 | try |
| 120 | { |
| 121 | $Response = Invoke-WebRequest -Method 'POST' -Uri $uri -Body $multipartContent -Headers $headers |
| 122 | Write-Host "API review: $($Response.Content)" |
| 123 | $StatusCode = $Response.StatusCode |
| 124 | } |
| 125 | catch |
| 126 | { |
| 127 | Write-Host "ERROR: API request failed" -ForegroundColor Red |
| 128 | Write-Host "Status Code: $($_.Exception.Response.StatusCode.Value__)" -ForegroundColor Yellow |
| 129 | Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Yellow |
| 130 | if ($_.ErrorDetails.Message) { |
| 131 | Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow |
| 132 | } |
| 133 | $StatusCode = $_.Exception.Response.StatusCode |
| 134 | } |
| 135 | |
| 136 | return $StatusCode |
| 137 | } |
| 138 | |
| 139 | function Upload-ReviewTokenFile($packageName, $apiLabel, $releaseStatus, $reviewFileName, $packageVersion, $filePath) |
| 140 | { |
| 141 | Write-Host "Original File path: $filePath" |
| 142 | $fileName = Split-Path -Leaf $filePath |
| 143 | Write-Host "OriginalFile name: $fileName" |
| 144 | |
| 145 | $params = "buildId=${BuildId}&artifactName=${ArtifactName}&originalFilePath=${fileName}&reviewFilePath=${reviewFileName}" |
| 146 | $params += "&label=${apiLabel}&repoName=${RepoName}&packageName=${packageName}&project=internal&packageVersion=${packageVersion}" |
| 147 | if($MarkPackageAsShipped) { |
| 148 | $params += "&setReleaseTag=true" |
| 149 | } |
| 150 | $uri = "${APIViewUri}/create?${params}" |
| 151 | if ($releaseStatus -and ($releaseStatus -ne "Unreleased")) |
| 152 | { |
| 153 | $uri += "&compareAllRevisions=true" |
| 154 | } |
| 155 | |
| 156 | Write-Host "Request to APIView: $uri" |
| 157 | |
| 158 | # Get Bearer token for authentication |
| 159 | $bearerToken = Get-ApiViewBearerToken |
| 160 | if (-not $bearerToken) { |
| 161 | Write-Error "Failed to acquire Bearer token for APIView authentication." |
| 162 | return [System.Net.HttpStatusCode]::Unauthorized |
| 163 | } |
| 164 | |
| 165 | $headers = @{ |
| 166 | "Authorization" = "Bearer $bearerToken" |
| 167 | } |
| 168 | |
| 169 | try |
| 170 | { |
| 171 | $Response = Invoke-WebRequest -Method 'POST' -Uri $uri -Headers $headers |
| 172 | Write-Host "API review: $($Response.Content)" |
| 173 | $StatusCode = $Response.StatusCode |
| 174 | } |
| 175 | catch |
| 176 | { |
| 177 | Write-Host "ERROR: API request failed" -ForegroundColor Red |
| 178 | Write-Host "Status Code: $($_.Exception.Response.StatusCode.Value__)" -ForegroundColor Yellow |
| 179 | Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Yellow |
| 180 | if ($_.ErrorDetails.Message) { |
| 181 | Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow |
| 182 | } |
| 183 | $StatusCode = $_.Exception.Response.StatusCode |
| 184 | } |
| 185 | |
| 186 | return $StatusCode |
| 187 | } |
| 188 | |
| 189 | function Get-APITokenFileName($packageName) |
| 190 | { |
| 191 | $reviewTokenFileName = ${packageName}.StartsWith("typespec-") ? "${packageName}_js.json" : "${packageName}_${LanguageShortName}.json" |
| 192 | $tokenFilePath = Join-Path $ArtifactPath $packageName $reviewTokenFileName |
| 193 | if (Test-Path $tokenFilePath) { |
| 194 | Write-Host "Review token file is present at $tokenFilePath" |
| 195 | return $reviewTokenFileName |
| 196 | } |
| 197 | else { |
| 198 | Write-Host "Review token file is not present at $tokenFilePath" |
| 199 | return $null |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | function Submit-APIReview($packageInfo, $packagePath, $packageArtifactName) |
| 204 | { |
| 205 | $packageName = $packageInfo.Name |
| 206 | $apiLabel = "Source Branch:${SourceBranch}" |
| 207 | |
| 208 | # Get generated review token file if present |
| 209 | # APIView processes request using different API if token file is already generated |
| 210 | $reviewTokenFileName = Get-APITokenFileName $packageArtifactName |
| 211 | if ($reviewTokenFileName) { |
| 212 | Write-Host "Uploading review token file $reviewTokenFileName to APIView." |
| 213 | return Upload-ReviewTokenFile $packageArtifactName $apiLabel $packageInfo.ReleaseStatus $reviewTokenFileName $packageInfo.Version $packagePath |
| 214 | } |
| 215 | else { |
| 216 | Write-Host "Uploading $packagePath to APIView." |
| 217 | return Upload-SourceArtifact $packagePath $apiLabel $packageInfo.ReleaseStatus $packageInfo.Version |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | function IsApiviewStatusCheckRequired($packageInfo) |
| 222 | { |
| 223 | if (($packageInfo.SdkType -eq "client" -or $packageInfo.SdkType -eq "spring") -and $packageInfo.IsNewSdk) { |
| 224 | return $true |
| 225 | } |
| 226 | return $false |
| 227 | } |
| 228 | |
| 229 | function ProcessPackage($packageName) |
| 230 | { |
| 231 | $packages = Find-Artifacts-For-Apireview $ArtifactPath $packageName |
| 232 | if ($packages) |
| 233 | { |
| 234 | foreach($pkgPath in $packages.Values) |
| 235 | { |
| 236 | $pkg = Split-Path -Leaf $pkgPath |
| 237 | $pkgPropPath = Join-Path -Path $ConfigFileDir "$packageName.json" |
| 238 | if (-Not (Test-Path $pkgPropPath)) |
| 239 | { |
| 240 | Write-Host " Package property file path $($pkgPropPath) is invalid." |
| 241 | continue |
| 242 | } |
| 243 | # Get package info from json file created before updating version to daily dev |
| 244 | $pkgInfo = Get-Content $pkgPropPath | ConvertFrom-Json |
| 245 | Write-Host "Processing package: $($pkgInfo)" |
| 246 | $version = [AzureEngSemanticVersion]::ParseVersionString($pkgInfo.Version) |
| 247 | if ($null -eq $version) |
| 248 | { |
| 249 | Write-Host "Version info is not available for package $packageName, because version '$(pkgInfo.Version)' is invalid. Please check if the version follows Azure SDK package versioning guidelines." |
| 250 | return 1 |
| 251 | } |
| 252 | |
| 253 | Write-Host "Version: $($version)" |
| 254 | Write-Host "SDK Type: $($pkgInfo.SdkType)" |
| 255 | Write-Host "Release Status: $($pkgInfo.ReleaseStatus)" |
| 256 | |
| 257 | # Run create review step only if build is triggered from main branch or if version is GA. |
| 258 | # This is to avoid invalidating review status by a build triggered from feature branch |
| 259 | if ( ($SourceBranch -eq $DefaultBranch) -or (-not $version.IsPrerelease) -or $MarkPackageAsShipped) |
| 260 | { |
| 261 | Write-Host "Submitting API Review request for package $($pkg), File path: $($pkgPath)" |
| 262 | $respCode = Submit-APIReview $pkgInfo $pkgPath $packageName |
| 263 | Write-Host "HTTP Response code: $($respCode)" |
| 264 | |
| 265 | # no need to check API review status when marking a package as shipped |
| 266 | if ($MarkPackageAsShipped) |
| 267 | { |
| 268 | if ($respCode -eq '500') |
| 269 | { |
| 270 | Write-Host "Failed to mark package ${packageName} as released. Please reach out to Azure SDK engineering systems on teams channel." |
| 271 | return 1 |
| 272 | } |
| 273 | Write-Host "Package ${packageName} is marked as released." |
| 274 | return 0 |
| 275 | } |
| 276 | |
| 277 | $apiStatus = [PSCustomObject]@{ |
| 278 | IsApproved = $false |
| 279 | Details = "" |
| 280 | } |
| 281 | $pkgNameStatus = [PSCustomObject]@{ |
| 282 | IsApproved = $false |
| 283 | Details = "" |
| 284 | } |
| 285 | Process-ReviewStatusCode $respCode $packageName $apiStatus $pkgNameStatus |
| 286 | |
| 287 | if ($apiStatus.IsApproved) { |
| 288 | Write-Host "API status: $($apiStatus.Details)" |
| 289 | } |
| 290 | elseif (!$pkgInfo.ReleaseStatus -or $pkgInfo.ReleaseStatus -eq "Unreleased") { |
| 291 | 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." |
| 292 | } |
| 293 | elseif ($version.IsPrerelease) |
| 294 | { |
| 295 | # Check if package name is approved. Preview version cannot be released without package name approval |
| 296 | if (!$pkgNameStatus.IsApproved) |
| 297 | { |
| 298 | if (IsApiviewStatusCheckRequired $pkgInfo) |
| 299 | { |
| 300 | Write-Error $($pkgNameStatus.Details) |
| 301 | return 1 |
| 302 | } |
| 303 | else{ |
| 304 | Write-Host "Package name is not approved for package $($packageName), however it is not required for this package type so it can still be released without API review approval." |
| 305 | } |
| 306 | } |
| 307 | # Ignore API review status for prerelease version |
| 308 | Write-Host "Package version is not GA. Ignoring API view approval status" |
| 309 | } |
| 310 | else |
| 311 | { |
| 312 | # Return error code if status code is 201 for new data plane package |
| 313 | # Temporarily enable API review for spring SDK types. Ideally this should be done be using 'IsReviewRequired' method in language side |
| 314 | # to override default check of SDK type client |
| 315 | if (IsApiviewStatusCheckRequired $pkgInfo) |
| 316 | { |
| 317 | if (!$apiStatus.IsApproved) |
| 318 | { |
| 319 | Write-Host "Package version $($version) is GA and automatic API Review is not yet approved for package $($packageName)." |
| 320 | Write-Host "Build and release is not allowed for GA package without API review approval." |
| 321 | Write-Host "You will need to queue another build to proceed further after API review is approved" |
| 322 | Write-Host "You can check http://aka.ms/azsdk/engsys/apireview/faq for more details on API Approval." |
| 323 | } |
| 324 | return 1 |
| 325 | } |
| 326 | else { |
| 327 | Write-Host "API review is not approved for package $($packageName), however it is not required for this package type so it can still be released without API review approval." |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | else { |
| 332 | Write-Host "Build is triggered from $($SourceBranch) with prerelease version. Skipping API review status check." |
| 333 | } |
| 334 | } |
| 335 | } |
| 336 | else { |
| 337 | Write-Host "No package is found in artifact path to submit review request" |
| 338 | } |
| 339 | return 0 |
| 340 | } |
| 341 | |
| 342 | $responses = @{} |
| 343 | # Check if package config file is present. This file has package version, SDK type etc info. |
| 344 | if (-not $ConfigFileDir) |
| 345 | { |
| 346 | $ConfigFileDir = Join-Path -Path $ArtifactPath "PackageInfo" |
| 347 | } |
| 348 | |
| 349 | Write-Host "Artifact path: $($ArtifactPath)" |
| 350 | Write-Host "Source branch: $($SourceBranch)" |
| 351 | Write-Host "Config File directory: $($ConfigFileDir)" |
| 352 | |
| 353 | # if package name param is not empty then process only that package |
| 354 | if ($PackageName) |
| 355 | { |
| 356 | Write-Host "Processing $($PackageName)" |
| 357 | $result = ProcessPackage -packageName $PackageName |
| 358 | $responses[$PackageName] = $result |
| 359 | } |
| 360 | else |
| 361 | { |
| 362 | # process all packages in the artifact |
| 363 | foreach ($artifact in $ArtifactList) |
| 364 | { |
| 365 | Write-Host "Processing $($artifact.name)" |
| 366 | $result = ProcessPackage -packageName $artifact.name |
| 367 | $responses[$artifact.name] = $result |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | $exitCode = 0 |
| 372 | foreach($pkg in $responses.keys) |
| 373 | { |
| 374 | if ($responses[$pkg] -eq 1) |
| 375 | { |
| 376 | Write-Host "API changes are not approved for $($pkg)" |
| 377 | $exitCode = 1 |
| 378 | } |
| 379 | } |
| 380 | exit $exitCode |
| 381 | |