eng/scripts/Update-Spec-Versions.ps1 (325 lines of code) (raw):
[CmdletBinding()]
param (
[string]$specsRoot,
[bool]$mergeWithExisting = $false,
[string]$specValidationIssues = ""
)
Set-StrictMode -Version 3
$ProgressPreference = "SilentlyContinue"; # Disable invoke-webrequest progress dialog
. (Join-Path $PSScriptRoot PackageList-Helpers.ps1)
. (Join-Path $PSScriptRoot .. common scripts Helpers PSModule-Helpers.ps1)
Install-ModuleIfNotInstalled "powershell-yaml" "0.4.7" | Import-Module
$specsGitFolder = (Join-Path $specsRoot ".git")
$specsGitFolderExists = (Test-Path $specsGitFolder)
$specificationRoot = (Join-Path $specsRoot "specification") -replace "\\", "/"
$validationIssues = @()
function LogSpecValidationIssue($issue, $message)
{
$script:validationIssues += "${issue}: ${message}"
return $issue
}
function NormalizedParent($path)
{
return $path -replace "/[^/]+$"
}
function MakeRelativeToSpecFolder($path)
{
return ($path -replace "\\", "/") -replace "^$specificationRoot/", ""
}
function ResolveToSpecFolder($specPath, $jsonPath)
{
if ($jsonPath) { $specPath = Join-Path $specPath $jsonPath }
return Join-Path $specificationRoot $specPath
}
function GetServiceLifeCycle($allSpecs, $serviceFamily, $resourcePath)
{
$stableSpecs = $allSpecs.Where({
$_.ServiceFamily -eq $serviceFamily -and $_.ResourcePath -eq $resourcePath -and $_.VersionType -eq "stable" -and $_.IsTypeSpec -ne "True"
})
if ($stableSpecs.Count -eq 0) {
return "Greenfield"
}
return "Brownfield"
}
function UpdateSpecMetadata($spec)
{
$version = ""
$anyTypeSpec = $false
$inconsistentVersion = @()
$jsonFiles = @($spec.JsonFiles.Split("|") | ForEach-Object { ResolveToSpecFolder $spec.SpecPath $_ })
$specFolderPath = ""
$validationErrors = @()
foreach ($jsonFile in $jsonFiles)
{
if (-not (Test-Path $jsonFile)) {
$validationErrors += LogSpecValidationIssue "SpecFileDoesNotExist" "'$jsonFile' under '$($spec.SpecPath)' doesn't exist."
continue
}
$specContent = Get-Content $jsonFile | ConvertFrom-Json -AsHashtable
Write-Verbose "Reading '$jsonFile' for metadata"
$jsonFile = MakeRelativeToSpecFolder $jsonFile
$specPathInfo = ParseSpecPath $jsonFile
if (!$specPathInfo) {
$validationErrors += LogSpecValidationIssue "SpecFileIsNotUnderServiceFolder" "'$jsonFile' in '$($spec.SpecPath)' doesn't match standard path format."
continue
}
if ($spec.Type -ne $specPathInfo.Type) {
$validationErrors += LogSpecValidationIssue "SpecTypeMismatchBetweenSpecFiles" "'$($specPathInfo.Type)' != '$($spec.Type)' for '$jsonFile'"
}
if ($spec.VersionType -ne $specPathInfo.VersionType) {
$validationErrors += LogSpecValidationIssue "VersionTypeMismatchBetweenSpecFiles" "'$($specPathInfo.VersionType)' != '$($spec.VersionType)' for '$jsonFile'"
}
if (!$specFolderPath) { $specFolderPath = $specPathInfo.SpecFolderPath }
if ($specFolderPath -ne $specPathInfo.SpecFolderPath) {
$validationErrors += LogSpecValidationIssue "SpecPathMismatchBetweenSpecFiles" "'$specFolderPath' != '$($specPathInfo.SpecFolderPath)' for '$jsonFile'"
}
if (!$specContent) { Write-Verbose "Failed to read $jsonFile"; continue }
if (!$specContent.ContainsKey("info") -or !$specContent.info.ContainsKey("version")) { Write-Verbose "Skipping: '$jsonFile' doesn't contain info.version"; continue }
if ($spec.Type -eq "mgmt" -and $specContent.ContainsKey("host") -and $specContent.host -ne "management.azure.com") {
Write-Verbose "Found mgmt spec at $jsonFile without management.azure.com as the host"
}
if (!$version) { $version = $specContent.info.version }
if ($version -ne $specContent.info.version) {
$validationErrors += LogSpecValidationIssue "VersionMismatchBetweenSpecFiles" "'$($specContent.info.version)' != '$version' for '$jsonFile'."
$inconsistentVersion += $version
$inconsistentVersion += $specContent.info.version
}
# Ignore differences with version starting with v or ending in -preview
if (($spec.Version -replace "v|-preview") -ne ($specContent.info.version -replace "v|-preview")) {
$validationErrors += LogSpecValidationIssue "VersionMismatchBetweenPathAndSpec" "'$($spec.Version)' != '$($specContent.info.version)' for '$jsonFile'"
}
# TODO: Find TypeSpec project
if ($specContent.info.ContainsKey('x-typespec-generated')) {
$anyTypeSpec = $true
}
}
if ($inconsistentVersion) {
$version = "Varies: " + ($inconsistentVersion | Sort-Object -Unique | Join-String -Sep ",")
}
$spec.Version = $version
$spec.IsTypeSpec = $anyTypeSpec
$spec.SpecValidationErrors = ($validationErrors | Sort-Object -Unique | Join-String -Sep ",")
return $spec
}
function ParseSpecPath($specFilePath)
{
if ($specFilePath -notmatch "^(?<serviceFamily>[^/]+)/(?<type>data-plane|resource-manager)(?<rpPath>.+)?/(?<verType>preview|stable)/(?<version>[^/]+).*?/[^/]+\.json$") {
Write-Verbose "Skipping: '$specFilePath' doesn't match the standard directory path regex"
return $null
}
$serviceFamily = $matches["serviceFamily"]
$rpPath = $matches["rpPath"]?.Trim('/')
$verType = $matches["verType"]
$versionFromPath = $matches["version"]
$specType = $matches["type"]
if ($specType -eq "data-plane") { $specType = "data" }
if ($specType -eq "resource-manager") { $specType = "mgmt" }
return [PSCustomObject][ordered]@{
SpecFilePath = $specFilePath
SpecFolderPath = NormalizedParent $specFilePath
ServiceFamily = $serviceFamily
ResourcePath = $rpPath
Version = $versionFromPath
VersionType = $verType
Type = $specType
}
}
function DiscoverSpec($specPathInfo, $specConfig)
{
$specReadmeTag = ""
if ($specConfig)
{
$specPath = $specConfig.ReadmePath
$specReadmeTag = $specConfig.tag
$jsonFiles = $specConfig["input-file"]
}
else
{
$specPath = $relSpecPath
$jsonFiles = Get-ChildItem (ResolveToSpecFolder $specPath) *.json | Split-Path -Leaf
}
$jsonFilesString = $jsonFiles | Sort-Object | Join-String -Sep "|"
$spec = [PSCustomObject][ordered]@{
SpecPath = $specPath
SpecReadmeTag = $specReadmeTag
SpecValidationErrors = ""
ServiceFamily = $specPathInfo.ServiceFamily
ResourcePath = $specPathInfo.ResourcePath
Version = $specPathInfo.Version
VersionType = $specPathInfo.VersionType
Type = $specPathInfo.Type
IsTypeSpec = ""
ServiceLifeCycle = "" # Brownfield, Greenfield
DateCreated = ""
JsonFiles = $jsonFilesString
}
return UpdateSpecMetadata $spec
}
function FindAllSpecs($specsPath)
{
#TODO: Map to tspconfig
#$potentialTypeSpecs = Get-ChildItem -Recurse -Include tspconfig.yaml $specsPath
$potentialReadmes = @(Get-ChildItem -Recurse -Include "README.md" $specsPath)
Write-Host "Found $($potentialReadmes.Count) README.md files"
$specToReadmeMap = @{}
$readmeCount = 0
foreach ($readmeFile in $potentialReadmes)
{
$specConfig = ParseReadme $readmeFile
if (!$specConfig) { continue }
$readmeCount++
$readmePath = MakeRelativeToSpecFolder $readmeFile.DirectoryName
$specConfig = $specConfig | Add-Member -MemberType NoteProperty -Name "ReadmePath" -Value $readmePath -PassThru
#$specConfig = $specConfig | Add-Member -MemberType NoteProperty -Name "ReadmeFileName" -Value $readmeFile.Name -PassThru
foreach ($inputFile in $specConfig["input-file"])
{
$specRelPath = (Join-Path $readmePath $inputFile) -replace "\\", "/"
$specToReadmeMap[$specRelPath] = $specConfig
}
}
Write-Host "Found $readmeCount README.md files with yaml blocks"
$potentialSpecs = Get-ChildItem -Recurse -Include *.json $specsPath
Write-Host "Found $($potentialSpecs.Count) potential spec files"
$specCount = 0
$processedPaths = @{}
foreach ($potentialSpec in $potentialSpecs)
{
$specPath = MakeRelativeToSpecFolder $potentialSpec
# Skip files under common, examples, scenarios, restler, common-types
if ($specPath -match "/(examples|scenarios|restler|common|common-types)/") { continue }
$specPathInfo = ParseSpecPath $specPath
if (!$specPathInfo) { continue }
$specCount++
$relSpecPath = NormalizedParent $specPath
$specConfig = $specToReadmeMap[$specPath]
$pathKey = $relSpecPath
if ($specConfig) { $pathKey = $specConfig.ReadmePath }
if ($processedPaths.ContainsKey($pathKey)) { continue }
$processedPaths[$pathKey] = DiscoverSpec $specPathInfo $specConfig
}
Write-Host "Discovered $specCount spec files"
return $processedPaths.Values | Where-Object { $_ }
}
function CombineHashTables ($ht1, $ht2)
{
if (!$ht2) { return $ht1 }
#Write-Host $ht2
foreach ($key in $ht2.Keys)
{
if ($ht1.ContainsKey($key)) {
if ($ht1[$key] -is [Hashtable] -and $ht2[$key] -is [Hashtable]) {
$ht1[$key] = CombineHashTables $ht1[$key] $ht2[$key]
} else {
$ht1[$key] = $ht1[$key] + $ht2[$key]
}
} else {
$ht1[$key] = $ht2[$key]
}
}
return $ht1
}
function ParseReadme($readme)
{
try {
$readmeContent = Get-Content $readme -Raw
$yamlRegex = '(?s)```\s*yaml(?<con>.*?)\n(?<yaml>.*?)\s*```'
$yms = [regex]::Matches($readmeContent, $yamlRegex)
if ($yms.Count -eq 0) {
Write-Verbose "No yaml blocks found in $readme"
return $null
}
$yamlContent = @{}
$yamlBlocksNoConditions = $yms | Where-Object { $_.Groups["con"].Value -eq "" }
foreach ($yb in $yamlBlocksNoConditions) {
$ybc = $yb.Groups["yaml"].Value | ConvertFrom-Yaml
$yamlContent = CombineHashTables $yamlContent $ybc
}
if (!$yamlContent.ContainsKey("tag")) {
Write-Verbose "No default tag found in $readme"
return $null
}
$defaultTag = $yamlContent.tag
$yamlBlocksWitConditions = $yms | Where-Object { $_.Groups["con"].Value -ne "" }
foreach ($yb in $yamlBlocksWitConditions)
{
$condition = $yb.Groups["con"].Value.Trim()
if ($condition) {
$expectedCondition = "\`$\(tag\)\s*==\s*'${defaultTag}'"
#Write-Host "[$condition] == [$expectedCondition]"
if ($condition -notmatch $expectedCondition) {
continue
}
}
$ybc = $yb.Groups["yaml"].Value | ConvertFrom-Yaml
$yamlContent = CombineHashTables $yamlContent $ybc
}
}
catch {
Write-Host "Failed to parse $readme"
throw
return $null
}
return $yamlContent
}
function UpdateSpecIndex()
{
$newSpecsToWrite = @()
$discoveredSpecs = FindAllSpecs $specificationRoot
$speclistFile = Join-Path $releaseFolder "specs.csv"
$specs = Get-Content $speclistFile | ConvertFrom-Csv
Write-Host "Updating metadata for $($discoveredSpecs.Count) discovered specs"
foreach ($discoveredSpec in $discoveredSpecs)
{
$foundSpecs = $specs.Where({ $_.SpecPath -eq $discoveredSpec.SpecPath })
if ($foundSpecs.Count -gt 1) {
Write-Host "Found more than one spec with path $($discoveredSpec.SpecPath) that should never happen but only taking the first one."
}
if ($foundSpecs.Count -eq 1) {
$spec = $foundSpecs[0]
}
else {
# Add new one
$spec = $discoveredSpec
}
$spec.SpecValidationErrors = $discoveredSpec.SpecValidationErrors
$spec.SpecReadmeTag = $discoveredSpec.SpecReadmeTag
$spec.JsonFiles = $discoveredSpec.JsonFiles
$spec.Version = $discoveredSpec.Version
$spec.IsTypeSpec = $discoveredSpec.IsTypeSpec
if ($specsGitFolderExists -and !$spec.DateCreated -and !$spec.JsonFiles.Contains("/")) {
# Given files can be in different locations with different dates which aren't easy to reconcile we are only computing the date created
# if there is only one folder. If there is only one folder then we compute the commit date for the earliest file in that folder.
# This should handle all the new TypeSpec generated files which is what we are most interested in.
$spec.DateCreated = git --git-dir=$specsGitFolder log --diff-filter=A --pretty=format:'%cs' --reverse -- "specification/$($spec.SpecPath)" | Select-Object -First 1
}
$spec.ServiceLifeCycle = GetServiceLifeCycle $specs $spec.ServiceFamily $spec.ResourcePath
$newSpecsToWrite += $spec
}
if ($mergeWithExisting)
{
foreach ($existingSpec in $specs)
{
$foundSpecs = $newSpecsToWrite.Where({ $_.SpecPath -eq $existingSpec.SpecPath })
if ($foundSpecs.Count -eq 0) {
$newSpecsToWrite += $existingSpec
}
}
}
Write-Host "Writing $speclistFile"
$new = @($newSpecsToWrite | Where-Object { $_.IsTypeSpec -eq "True" } | Sort-Object ServiceFamily, ResourcePath, SpecPath)
$other = @($newSpecsToWrite | Where-Object { $_.IsTypeSpec -ne "True" } | Sort-Object ServiceFamily, ResourcePath, SpecPath)
$sortedSpecs = $new + $other
$sortedSpecs | ConvertTo-CSV -NoTypeInformation -UseQuotes Always | Out-File $speclistFile -encoding ascii
$specsFromReadme = $sortedSpecs | Where-Object { $_.SpecReadmeTag -ne "" }
Write-Host "Found $($specsFromReadme.Count) specs from readme files"
if ($validationIssues.Count -gt 0) {
$specsWithErrors = $sortedSpecs | Where-Object { $_.SpecValidationErrors }
Write-Host "Found $($validationIssues.Count) validation issues across $($specsWithErrors.Count) specs."
if ($specValidationIssues) {
Write-Host "See $specValidationIssues for all the details."
$validationIssues | Out-File $specValidationIssues
exit 1
}
}
}
UpdateSpecIndex