eng/scripts/TypeSpec-Requirement.ps1 (172 lines of code) (raw):

[CmdletBinding()] param ( [Parameter(Position = 0)] [string] $BaseCommitish = "HEAD^", [Parameter(Position = 1)] [string] $HeadCommitish = "HEAD", [Parameter(Position = 2)] [string] $SpecType = "data-plane|resource-manager", [string] $CheckAllUnder, # Reserved for testing. Call using: # $ pwsh -Command '... -_ResponseCache @{"url"=200}' [Parameter(DontShow)] [hashtable] $_ResponseCache = @{} ) Set-StrictMode -Version 3 . $PSScriptRoot/ChangedFiles-Functions.ps1 . $PSScriptRoot/../common/scripts/logging.ps1 . $PSScriptRoot/Suppressions-Functions.ps1 function Get-ValidatedSuppression { param ( [string]$fileInSpecFolder ) $suppression = Get-Suppression "TypeSpecRequirement" $fileInSpecFolder if ($suppression) { # Each path must specify a single version (without wildcards) under "preview|stable" # # Allowed: data-plane/Azure.Contoso.WidgetManager/preview/2022-11-01-preview/**/*.json # Disallowed: data-plane/Azure.Contoso.WidgetManager/preview/**/*.json # Disallowed: data-plane/**/*.json # # Include "." since a few specs use versions like "X.Y" instead of "YYYY-MM-DD" $singleVersionPattern = "/(preview|stable)/[A-Za-z0-9._-]+/" $paths = $suppression["paths"] foreach ($path in $paths) { if ($path -notmatch $singleVersionPattern) { LogError ("Invalid path '$path'. Path must only include one version per suppression.") LogJobFailure exit 1 } } } return $suppression } $repoPath = Resolve-Path "$PSScriptRoot/../.." $pathsWithErrors = @() $filesToCheck = $CheckAllUnder ? (Get-ChildItem -Path $CheckAllUnder -Recurse -File | Resolve-Path -Relative -RelativeBasePath $repoPath | ForEach-Object { $_ -replace '\\', '/' }) : (Get-ChangedSwaggerFiles (Get-ChangedFiles $BaseCommitish $HeadCommitish)) $filesToCheck = $filesToCheck.Where({ ($_ -notmatch "/(examples|scenarios|restler|common|common-types)/") -and ($_ -match "specification/[^/]+/($SpecType).*?/(preview|stable)/[^/]+/[^/]+\.json$") }) if (!$filesToCheck) { LogInfo "No OpenAPI files found to check" } else { # Cache responses to GitHub web requests, for efficiency and to prevent rate limiting $responseCache = $_ResponseCache # - Forward slashes on both Linux and Windows # - May be nested 4 or 5 levels deep, perhaps even deeper # - Examples # - specification/foo/data-plane/Foo/stable/2023-01-01/Foo.json # - specification/foo/data-plane/Foo/bar/stable/2023-01-01/Foo.json # - specification/foo/resource-manager/Microsoft.Foo/stable/2023-01-01/Foo.json # - Doc: https://github.com/Azure/azure-rest-api-specs/blob/main/README.md#directory-structure foreach ($file in $filesToCheck) { LogInfo "Checking $file" $fullPath = (Join-Path $repoPath $file) $suppression = Get-ValidatedSuppression $fullPath if ($suppression) { $reason = $suppression["reason"] ?? "<no reason specified>" LogInfo " Suppressed: $reason" # Skip further checks, to avoid potential errors on files already suppressed continue } try { $jsonContent = Get-Content $fullPath | ConvertFrom-Json -AsHashtable } catch { LogWarning " OpenAPI cannot be parsed as JSON, so assuming not generated from TypeSpec" LogWarning " $_" } if ($jsonContent) { if ($null -ne ${jsonContent}?["info"]?["x-typespec-generated"]) { LogInfo " OpenAPI was generated from TypeSpec (contains '/info/x-typespec-generated')" if ($file -match "^.*specification/[^/]+/") { $rpFolder = $Matches[0]; $tspConfigs = @(Get-ChildItem -Path $rpFolder -Recurse -File | Where-Object { $_.Name -eq "tspconfig.yaml" }) if ($tspConfigs) { LogInfo " Folder '$rpFolder' contains $($tspConfigs.Count) file(s) named 'tspconfig.yaml'" } else { LogError ("OpenAPI was generated from TypeSpec, but folder '$rpFolder' contains no files named 'tspconfig.yaml'." ` + " The TypeSpec used to generate OpenAPI must be added to this folder.") LogJobFailure exit 1 } } else { LogError "Path to OpenAPI did not match expected regex. Unable to extract RP folder." LogJobFailure exit 1 } # Skip further checks, since spec is already using TypeSpec continue } else { LogInfo " OpenAPI was not generated from TypeSpec (missing '/info/x-typespec-generated')" } } # Extract path between "specification/" and "/(preview|stable)" if ($file -match "specification/(?<servicePath>[^/]+/($SpecType).*?)/(preview|stable)/[^/]+/[^/]+\.json$") { $servicePath = $Matches["servicePath"] } else { LogError "Path to OpenAPI did not match expected regex. Unable to extract service path." LogJobFailure exit 1 } $urlToStableFolder = "https://github.com/Azure/azure-rest-api-specs/tree/main/specification/$servicePath/stable" # Avoid conflict with pipeline secret $logUrlToStableFolder = $urlToStableFolder -replace '^https://', '' LogInfo " Checking $logUrlToStableFolder" $responseStatus = $responseCache[$urlToStableFolder]; if ($null -ne $responseStatus) { LogInfo " Found in cache" } else { LogInfo " Not found in cache, making web request" try { $response = Invoke-WebRequest -Uri $urlToStableFolder -Method Head -SkipHttpErrorCheck $responseStatus = $response.StatusCode $responseCache[$urlToStableFolder] = $responseStatus } catch { LogError "Exception making web request to ${logUrlToStableFolder}: $_" LogJobFailure exit 1 } } LogInfo " Status: $responseStatus" if ($responseStatus -eq 200) { LogInfo " Branch 'main' contains path '$servicePath/stable', so spec already exists and is not required to use TypeSpec" $notice = "Brownfield services will soon be required to convert from OpenAPI to TypeSpec. See https://aka.ms/azsdk/typespec." LogNoticeForFile $file $notice if ($env:GITHUB_OUTPUT) { # Set output to be used later in /.github/workflows/TypeSpec-Requirement.yaml Add-Content -Path $env:GITHUB_OUTPUT -Value "brownfield=true" } } elseif ($responseStatus -eq 404) { LogInfo " Branch 'main' does not contain path '$servicePath/stable', so spec is new and must use TypeSpec" $pathsWithErrors += $file } else { LogError "Unexpected response from ${logUrlToStableFolder}: ${responseStatus}" LogJobFailure exit 1 } } } if ($pathsWithErrors.Count -gt 0) { # DevOps only adds the first 4 errors to the github checks list so lets always add the generic one first # and then as many of the individual ones as can be found afterwards LogError "New specs must use TypeSpec. For more detailed docs see https://aka.ms/azsdk/typespec" LogJobFailure foreach ($path in $pathsWithErrors) { LogErrorForFile $path "OpenAPI was not generated from TypeSpec, and spec appears to be new" } exit 1 } exit 0