diff --git a/eng/scripts/Automation-Sdk-UpdateMetadata.ps1 b/eng/scripts/Automation-Sdk-UpdateMetadata.ps1 new file mode 100644 index 000000000000..2cc88bce84d0 --- /dev/null +++ b/eng/scripts/Automation-Sdk-UpdateMetadata.ps1 @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Updates parent-level and root-level pom.xml files, and ci.yml for a Java SDK package. + +.DESCRIPTION + This script handles two cases: + Case 1 - Service exists, new resourcemanager package: + - Skips root pom (service already listed) + - Updates service-level pom.xml with new module + - Updates ci.yml with new artifact entry + Case 2 - Brand new service: + - Updates root pom.xml with new service module + - Creates service-level pom.xml from template + - Creates ci.yml from template with new artifact entry + +.PARAMETER PackagePath + Absolute path to the root folder of the local SDK project (containing pom.xml). + +.PARAMETER SdkRepoPath + Absolute path to the root folder of the local SDK repository. + +.EXAMPLE + .\Automation-Sdk-UpdateMetadata.ps1 -PackagePath "C:\repos\azure-sdk-for-java\sdk\network\azure-resourcemanager-network" -SdkRepoPath "C:\repos\azure-sdk-for-java" +#> + +param( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path $_})] + [string]$PackagePath, + + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path $_})] + [string]$SdkRepoPath +) + +$ErrorActionPreference = "Stop" + +# Import common scripts for logging functions +$commonScriptPath = Join-Path $PSScriptRoot ".." "common" "scripts" "common.ps1" +. $commonScriptPath + +# Import metadata helper functions +$helperPath = Join-Path $PSScriptRoot "helpers" "Metadata-Helpers.ps1" +. $helperPath + +try { + LogInfo "========================================" + LogInfo "Azure SDK Metadata Update Tool" + LogInfo "========================================" + LogInfo "" + + LogInfo "Step 1: Deriving service and module from package path..." + $pathInfo = Get-ServiceAndModuleFromPath -PackagePath $PackagePath -SdkRepoPath $SdkRepoPath + $service = $pathInfo.Service + $module = $pathInfo.Module + LogInfo " Service: $service" + LogInfo " Module: $module" + LogInfo "" + + LogInfo "Step 2: Updating root pom.xml..." + Update-RootPom -SdkRepoPath $SdkRepoPath -Service $service + LogInfo "" + + LogInfo "Step 3: Updating service-level pom.xml..." + Update-ServicePom -SdkRepoPath $SdkRepoPath -Service $service -Module $module + LogInfo "" + + LogInfo "Step 4: Updating ci.yml..." + $packagePomPath = Join-Path $PackagePath "pom.xml" + if (Test-Path $packagePomPath) { + $groupId = Get-GroupIdFromPom -PomPath $packagePomPath + LogInfo " GroupId: $groupId" + + $supportedGroupIds = @("com.azure", "com.azure.resourcemanager") + if ($groupId -notin $supportedGroupIds) { + LogError "Unsupported SDK type with groupId '$groupId' at '$PackagePath'. This script only supports: $($supportedGroupIds -join ', ')." + exit 1 + } + + Update-CiYml -SdkRepoPath $SdkRepoPath -Service $service -Module $module -GroupId $groupId + } + else { + LogError "No pom.xml found at '$packagePomPath'. Cannot determine groupId for ci.yml update." + } + LogInfo "" + + LogInfo "✅ Metadata updated successfully!" + exit 0 +} +catch { + LogError "An error occurred: $_" + LogError "Stack trace: $($_.ScriptStackTrace)" + exit 1 +} diff --git a/eng/scripts/helpers/Metadata-Helpers.ps1 b/eng/scripts/helpers/Metadata-Helpers.ps1 new file mode 100644 index 000000000000..8aa5b0c1bd93 --- /dev/null +++ b/eng/scripts/helpers/Metadata-Helpers.ps1 @@ -0,0 +1,687 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Helper functions for metadata update automation. + +.DESCRIPTION + This file provides helper functions for updating parent-level and root-level + pom.xml files, as well as ci.yml files for Java SDK packages. + Logic is ported from eng/automation/utils.py and parameters.py (Python). +#> + +function Get-ServiceAndModuleFromPath { + <# + .SYNOPSIS + Derives service name and module (artifact) name from a package path. + + .DESCRIPTION + Given a package path like "C:\repos\azure-sdk-for-java\sdk\network\azure-resourcemanager-network", + extracts the service name ("network") and module name ("azure-resourcemanager-network"). + + .PARAMETER PackagePath + Absolute path to the package directory. + + .PARAMETER SdkRepoPath + Absolute path to the SDK repository root. + + .OUTPUTS + Hashtable with Service and Module properties. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$PackagePath, + + [Parameter(Mandatory = $true)] + [string]$SdkRepoPath + ) + + # Normalize paths to forward slashes for consistent comparison + $normalizedPackagePath = $PackagePath.TrimEnd('\', '/').Replace('\', '/') + $normalizedSdkRepoPath = $SdkRepoPath.TrimEnd('\', '/').Replace('\', '/') + + $sdkDir = "$normalizedSdkRepoPath/sdk/" + if (-not $normalizedPackagePath.StartsWith($sdkDir)) { + throw "PackagePath '$PackagePath' is not under sdk/ directory of '$SdkRepoPath'. Expected format: {SdkRepoPath}/sdk/{service}/{module}" + } + + $relativePath = $normalizedPackagePath.Substring($sdkDir.Length) + $parts = $relativePath.Split('/') + if ($parts.Count -lt 2) { + throw "Cannot determine service and module from path: $PackagePath. Expected format: sdk/{service}/{module}" + } + + return @{ + Service = $parts[0] + Module = $parts[1] + } +} + +function Get-GroupIdFromPom { + <# + .SYNOPSIS + Extracts groupId from a Maven POM file. + + .PARAMETER PomPath + The absolute path to the pom.xml file. + + .OUTPUTS + String representing the groupId. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path $_})] + [string]$PomPath + ) + + [xml]$pomXml = Get-Content $PomPath + $groupId = $pomXml.project.groupId + + # If groupId is not in the current project, check parent + if ([string]::IsNullOrEmpty($groupId)) { + $groupId = $pomXml.project.parent.groupId + } + + if ([string]::IsNullOrEmpty($groupId)) { + throw "Could not extract groupId from POM file: $PomPath" + } + + return $groupId +} + +# Equivalent of Python add_module_to_modules() +function Add-ModuleToModulesBlock { + <# + .SYNOPSIS + Adds a module entry to a XML block, sorted alphabetically. + + .PARAMETER ModulesBlock + The existing ... XML string. + + .PARAMETER Module + The module name to add. + + .OUTPUTS + String representing the updated block. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ModulesBlock, + + [Parameter(Mandatory = $true)] + [string]$Module + ) + + # Extract indent from the closing tag (matches Python: re.search(r"([^\S\n\r]*)", modules)) + if ($ModulesBlock -match '([^\S\n\r]*)') { + $closingIndent = $matches[1] + $indent = $closingIndent + " " + } + else { + $closingIndent = " " + $indent = " " + } + + # Collect all existing modules into a set and add new one + $allModules = [System.Collections.Generic.HashSet[string]]::new() + $moduleMatches = [regex]::Matches($ModulesBlock, '(.*?)') + foreach ($m in $moduleMatches) { + [void]$allModules.Add($m.Groups[1].Value) + } + [void]$allModules.Add($Module) + + # Sort and build module lines (matches Python: indent + POM_MODULE_FORMAT.format(module)) + $sortedModules = $allModules | Sort-Object + $moduleLines = ($sortedModules | ForEach-Object { "${indent}$_" }) -join "`n" + + return "`n${moduleLines}`n${closingIndent}" +} + +# Equivalent of Python add_module_to_default_profile() +function Add-ModuleToDefaultProfile { + <# + .SYNOPSIS + Adds a module to the default profile's section in a POM with profiles. + + .PARAMETER PomContent + The full content of the POM file. + + .PARAMETER Module + The module name to add. + + .OUTPUTS + Hashtable with Success (boolean) and Content (string) properties. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$PomContent, + + [Parameter(Mandatory = $true)] + [string]$Module + ) + + $profileMatches = [regex]::Matches($PomContent, '(?s).*?') + foreach ($profileMatch in $profileMatches) { + $profileValue = $profileMatch.Value + if ($profileValue -match 'default') { + if (([regex]::Matches($profileValue, '')).Count -gt 1) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Profile][Skip] find more than one in default" + } + return @{ Success = $false; Content = "" } + } + + $modulesMatch = [regex]::Match($profileValue, '(?s).*') + if (-not $modulesMatch.Success) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Profile][Skip] Cannot find in default" + } + return @{ Success = $false; Content = "" } + } + + $updatedModules = Add-ModuleToModulesBlock -ModulesBlock $modulesMatch.Value -Module $Module + + # Calculate absolute position (matches Python: pom[: profile.start() + modules.start()]) + $absStart = $profileMatch.Index + $modulesMatch.Index + $absEnd = $absStart + $modulesMatch.Length + $updatedPom = $PomContent.Substring(0, $absStart) + $updatedModules + $PomContent.Substring($absEnd) + + return @{ Success = $true; Content = $updatedPom } + } + } + + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Profile][Skip] cannot find with default" + } + return @{ Success = $false; Content = "" } +} + +# Equivalent of Python add_module_to_pom() +function Add-ModuleToPom { + <# + .SYNOPSIS + Adds a module entry to a POM file's section. + + .DESCRIPTION + Handles POMs with single blocks and POMs with containing + a default profile with its own block. + + .PARAMETER PomContent + The full content of the POM file as a string. + + .PARAMETER Module + The module name to add. + + .OUTPUTS + Hashtable with Success (boolean) and Content (string) properties. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$PomContent, + + [Parameter(Mandatory = $true)] + [string]$Module + ) + + # Check if module already exists + if ($PomContent.Contains("$Module")) { + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Skip] pom already has module $Module" + } + return @{ Success = $true; Content = $PomContent } + } + + # Count blocks + $modulesCount = ([regex]::Matches($PomContent, '')).Count + + if ($modulesCount -gt 1) { + if ($PomContent.Contains('')) { + return Add-ModuleToDefaultProfile -PomContent $PomContent -Module $Module + } + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Skip] find more than one in pom" + } + return @{ Success = $false; Content = "" } + } + + # Find the single block + $modulesMatch = [regex]::Match($PomContent, '(?s).*?') + if (-not $modulesMatch.Success) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Skip] Cannot find in pom" + } + return @{ Success = $false; Content = "" } + } + + $updatedModules = Add-ModuleToModulesBlock -ModulesBlock $modulesMatch.Value -Module $Module + $updatedPom = $PomContent.Substring(0, $modulesMatch.Index) + $updatedModules + $PomContent.Substring($modulesMatch.Index + $modulesMatch.Length) + + return @{ Success = $true; Content = $updatedPom } +} + +# Equivalent of Python update_root_pom() +function Update-RootPom { + <# + .SYNOPSIS + Adds sdk/{service} as a module to the root pom.xml. + + .PARAMETER SdkRepoPath + Absolute path to the SDK repository root. + + .PARAMETER Service + The service directory name (e.g., "network"). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$SdkRepoPath, + + [Parameter(Mandatory = $true)] + [string]$Service + ) + + $pomFile = Join-Path $SdkRepoPath "pom.xml" + if (-not (Test-Path $pomFile)) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[POM][Skip] cannot find root pom" + } + return + } + + $module = "sdk/$Service" + $pomContent = Get-Content -Path $pomFile -Raw + + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Process] dealing with root pom" + } + + $result = Add-ModuleToPom -PomContent $pomContent -Module $module + if ($result.Success) { + Set-Content -Path $pomFile -Value $result.Content -NoNewline + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Success] Write to root pom" + } + } +} + +# Equivalent of the POM update part of Python update_service_files_for_new_lib() +function Update-ServicePom { + <# + .SYNOPSIS + Adds a module to the service-level pom.xml, creating it if it doesn't exist. + + .PARAMETER SdkRepoPath + Absolute path to the SDK repository root. + + .PARAMETER Service + The service directory name. + + .PARAMETER Module + The artifact/module name to add. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$SdkRepoPath, + + [Parameter(Mandatory = $true)] + [string]$Service, + + [Parameter(Mandatory = $true)] + [string]$Module + ) + + $pomFile = Join-Path $SdkRepoPath "sdk" $Service "pom.xml" + + if (Test-Path $pomFile) { + $pomContent = Get-Content -Path $pomFile -Raw + } + else { + # Create from template (matches Python POM_FORMAT) + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Process] creating new service pom.xml" + } + $pomContent = @" + + + 4.0.0 + com.azure + azure-${Service}-service + pom + 1.0.0 + + + ${Module} + + +"@ + $pomDir = Split-Path $pomFile -Parent + if (-not (Test-Path $pomDir)) { + New-Item -ItemType Directory -Path $pomDir -Force | Out-Null + } + Set-Content -Path $pomFile -Value $pomContent -NoNewline + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Success] Created new service pom.xml at: $pomFile" + } + return + } + + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Process] dealing with service pom.xml" + } + + $result = Add-ModuleToPom -PomContent $pomContent -Module $Module + if ($result.Success) { + Set-Content -Path $pomFile -Value $result.Content -NoNewline + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[POM][Success] Write to service pom.xml" + } + } +} + +# ============================================================================ +# CI.yml update functions +# Ported from eng/automation/utils.py update_service_files_for_new_lib() and +# eng/automation/parameters.py CI_FORMAT / CI_HEADER +# ============================================================================ + +function New-CiYmlContent { + <# + .SYNOPSIS + Generates a new ci.yml content string from template. + + .PARAMETER Service + The service directory name (e.g., "network"). + + .PARAMETER Module + The artifact/module name (e.g., "azure-resourcemanager-network"). + + .OUTPUTS + String representing the ci.yml content. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Service, + + [Parameter(Mandatory = $true)] + [string]$Module + ) + + # Matches Python CI_HEADER + CI_FORMAT from parameters.py + return @" +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/${Service}/ci.yml + - sdk/${Service}/${Module}/ + exclude: + - sdk/${Service}/pom.xml + - sdk/${Service}/${Module}/pom.xml + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/${Service}/ci.yml + - sdk/${Service}/${Module}/ + exclude: + - sdk/${Service}/pom.xml + - sdk/${Service}/${Module}/pom.xml + +parameters: [] + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: ${Service} + Artifacts: [] +"@ +} + +function Add-ArtifactToCiYml { + <# + .SYNOPSIS + Adds an artifact entry to a parsed ci.yml object. + + .DESCRIPTION + Handles two modes based on whether the ci.yml has a parameters list: + - With parameters: adds artifact with releaseInBatch and a release parameter + - Without parameters: adds artifact without releaseInBatch + + .PARAMETER CiYml + The parsed ci.yml as a hashtable/ordered dictionary (from ConvertFrom-Yaml). + + .PARAMETER Module + The artifact/module name. + + .PARAMETER GroupId + The Maven groupId. + + .OUTPUTS + Boolean indicating whether the artifact was added. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $CiYml, + + [Parameter(Mandatory = $true)] + [string]$Module, + + [Parameter(Mandatory = $true)] + [string]$GroupId + ) + + # Validate structure + $extends = $CiYml["extends"] + if (-not ($extends -is [System.Collections.IDictionary])) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[CI][Skip] Unexpected ci.yml format: missing 'extends'" + } + return $false + } + $params = $extends["parameters"] + if (-not ($params -is [System.Collections.IDictionary])) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[CI][Skip] Unexpected ci.yml format: missing 'extends.parameters'" + } + return $false + } + $artifacts = $params["Artifacts"] + if (-not ($artifacts -is [System.Collections.IList])) { + if (Get-Command LogError -ErrorAction SilentlyContinue) { + LogError "[CI][Skip] Unexpected ci.yml format: 'Artifacts' is not a list" + } + return $false + } + + # Check if module already exists + foreach ($artifact in $artifacts) { + if ($artifact["name"] -eq $Module -and $artifact["groupId"] -eq $GroupId) { + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[CI][Skip] ci.yml already has module $Module" + } + return $false + } + } + + $safeName = $Module.Replace("-", "") + + # Check if ci.yml has a parameters list (not just empty/null) + $ciParameters = $CiYml["parameters"] + $hasParametersList = ($ciParameters -is [System.Collections.IList]) -and ($ciParameters.Count -gt 0) + + if ($hasParametersList) { + # Add artifact with releaseInBatch reference + $releaseParameterName = "release_$safeName" + $releaseInBatchRef = "`${{ parameters.$releaseParameterName }}" + + $newArtifact = [ordered]@{ + name = $Module + groupId = $GroupId + safeName = $safeName + releaseInBatch = $releaseInBatchRef + } + $artifacts.Add($newArtifact) + + # True for data-plane, False for management-plane + $releaseInBatchDefault = -not ($Module -match "-resourcemanager-") + + $newParam = [ordered]@{ + name = $releaseParameterName + displayName = $Module + type = "boolean" + default = $releaseInBatchDefault + } + $ciParameters.Add($newParam) + } + else { + # Add artifact without releaseInBatch + $newArtifact = [ordered]@{ + name = $Module + groupId = $GroupId + safeName = $safeName + } + $artifacts.Add($newArtifact) + } + + return $true +} + +function ConvertTo-CiYmlString { + <# + .SYNOPSIS + Converts a ci.yml object back to a YAML string with proper formatting. + + .DESCRIPTION + Uses powershell-yaml to serialize, then applies formatting fixes to match + the expected ci.yml style (blank lines between top-level sections). + + .PARAMETER CiYml + The ci.yml object to serialize. + + .OUTPUTS + String representing the formatted YAML content. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $CiYml + ) + + $yamlStr = ConvertTo-Yaml $CiYml + + # Add blank line before each top-level key (matches Python: re.sub(r"(\n\S)", r"\n\1", ci_yml_str)) + $yamlStr = $yamlStr -replace '(\n)(\S)', "`$1`n`$2" + + # Add CI header + $header = "# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.`n`n" + + return $header + $yamlStr +} + +function Update-CiYml { + <# + .SYNOPSIS + Updates or creates the ci.yml file for a service. + + .DESCRIPTION + Ported from Python update_service_files_for_new_lib() CI logic. + Cases: + 1. ci.yml doesn't exist → create from template, add artifact + 2. ci.yml exists with SDKType=data → rename to ci.data.yml, create new, add artifact + 3. ci.yml exists, module already present → skip + 4. ci.yml exists, module not present → add artifact (with or without release param) + + .PARAMETER SdkRepoPath + Absolute path to the SDK repository root. + + .PARAMETER Service + The service directory name. + + .PARAMETER Module + The artifact/module name. + + .PARAMETER GroupId + The Maven groupId for the artifact. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$SdkRepoPath, + + [Parameter(Mandatory = $true)] + [string]$Service, + + [Parameter(Mandatory = $true)] + [string]$Module, + + [Parameter(Mandatory = $true)] + [string]$GroupId + ) + + $ciYmlFile = Join-Path $SdkRepoPath "sdk" $Service "ci.yml" + + if (Test-Path $ciYmlFile) { + $ciYmlContent = Get-Content -Path $ciYmlFile -Raw + $ciYml = ConvertFrom-Yaml $ciYmlContent -Ordered + + # Check for SDKType=data → rename and create new + $sdkType = "" + try { + $sdkType = $ciYml["extends"]["parameters"]["SDKType"] + } catch {} + + if ($sdkType -is [string] -and $sdkType.ToLower() -eq "data") { + $ciDataFile = Join-Path $SdkRepoPath "sdk" $Service "ci.data.yml" + Move-Item -Path $ciYmlFile -Destination $ciDataFile -Force + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[CI][Process] Renamed existing ci.yml (SDKType=data) to ci.data.yml" + } + $ciYmlContent = New-CiYmlContent -Service $Service -Module $Module + $ciYml = ConvertFrom-Yaml $ciYmlContent -Ordered + } + } + else { + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[CI][Process] Creating new ci.yml for service: $Service" + } + $ciYmlContent = New-CiYmlContent -Service $Service -Module $Module + $ciYml = ConvertFrom-Yaml $ciYmlContent -Ordered + } + + $added = Add-ArtifactToCiYml -CiYml $ciYml -Module $Module -GroupId $GroupId + if ($added) { + $outputStr = ConvertTo-CiYmlString -CiYml $ciYml + $ciDir = Split-Path $ciYmlFile -Parent + if (-not (Test-Path $ciDir)) { + New-Item -ItemType Directory -Path $ciDir -Force | Out-Null + } + Set-Content -Path $ciYmlFile -Value $outputStr -NoNewline + if (Get-Command LogInfo -ErrorAction SilentlyContinue) { + LogInfo "[CI][Success] Write to ci.yml" + } + } +} diff --git a/eng/scripts/tests/Automation-Sdk-UpdateMetadata.tests.ps1 b/eng/scripts/tests/Automation-Sdk-UpdateMetadata.tests.ps1 new file mode 100644 index 000000000000..5c9ea8ef1665 --- /dev/null +++ b/eng/scripts/tests/Automation-Sdk-UpdateMetadata.tests.ps1 @@ -0,0 +1,1061 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Pester tests for Automation-Sdk-UpdateMetadata.ps1 + +.DESCRIPTION + This file contains unit tests for the metadata update automation functions + using the Pester testing framework. + +.NOTES + How to run: + 1. Install Pester if not already installed: + Install-Module Pester -Force -MinimumVersion 5.3.3 + + 2. Run the tests: + Invoke-Pester ./Automation-Sdk-UpdateMetadata.tests.ps1 +#> + +BeforeAll { + # Import YAML module for CI tests + Import-Module powershell-yaml -ErrorAction Stop + + # Import metadata helper functions + $helperPath = Join-Path $PSScriptRoot ".." "helpers" "Metadata-Helpers.ps1" + . $helperPath + + # Create a test directory structure + $script:TestRoot = Join-Path ([System.IO.Path]::GetTempPath()) "MetadataAutomationTests_$(New-Guid)" + New-Item -ItemType Directory -Path $script:TestRoot -Force | Out-Null + + # Sample POM with direct groupId + $script:SamplePomXml = @" + + + 4.0.0 + com.azure.resourcemanager + azure-resourcemanager-network + 2.0.0 + +"@ + + # Sample POM with groupId inherited from parent + $script:SamplePomWithParentXml = @" + + + 4.0.0 + + com.azure + azure-client-sdk-parent + 1.7.0 + + azure-storage-blob + 12.0.0 + +"@ + + # Sample service aggregator POM + $script:SampleServicePomXml = @" + + + 4.0.0 + com.azure + azure-network-service + pom + 1.0.0 + + + azure-resourcemanager-network + + +"@ + + # Sample root POM + $script:SampleRootPomXml = @" + + + sdk/advisor + sdk/network + sdk/storage + + +"@ + + # Sample POM with profiles (as in the root pom) + $script:SamplePomWithProfiles = @" + + + + default + + sdk/advisor + sdk/storage + + + + other + + sdk/other + + + + +"@ + + # Sample ci.yml with no parameters (simple artifact) + $script:SampleCiYmlNoParams = @" +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/communication/azure-resourcemanager-communication/ + exclude: + - sdk/communication/azure-resourcemanager-communication/pom.xml + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/communication/azure-resourcemanager-communication/ + exclude: + - sdk/communication/azure-resourcemanager-communication/pom.xml + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: communication/azure-resourcemanager-communication + Artifacts: + - name: azure-resourcemanager-communication + groupId: com.azure.resourcemanager + safeName: azureresourcemanagercommunication +"@ + + # Sample ci.yml with release parameters + $script:SampleCiYmlWithParams = @" +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - hotfix/* + - release/* + paths: + include: + - sdk/network/ + exclude: + - sdk/network/pom.xml + - sdk/network/azure-resourcemanager-network/pom.xml + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/network/ + exclude: + - sdk/network/pom.xml + - sdk/network/azure-resourcemanager-network/pom.xml + +parameters: +- name: release_azureresourcemanagernetwork + displayName: 'azure-resourcemanager-network' + type: boolean + default: false + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: network + Artifacts: + - name: azure-resourcemanager-network + groupId: com.azure.resourcemanager + safeName: azureresourcemanagernetwork + releaseInBatch: `${{ parameters.release_azureresourcemanagernetwork }} +"@ + + # Sample ci.yml with SDKType=data + $script:SampleCiYmlDataType = @" +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: myservice + SDKType: data + Artifacts: + - name: azure-myservice + groupId: com.azure + safeName: azuremyservice +"@ +} + +AfterAll { + # Clean up test directory + if (Test-Path $script:TestRoot) { + Remove-Item -Path $script:TestRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# ============================================================================ +# Get-ServiceAndModuleFromPath tests +# ============================================================================ +Describe "Get-ServiceAndModuleFromPath" { + It "Should extract service and module from Windows-style path" { + $result = Get-ServiceAndModuleFromPath ` + -PackagePath "C:\repos\azure-sdk-for-java\sdk\network\azure-resourcemanager-network" ` + -SdkRepoPath "C:\repos\azure-sdk-for-java" + + $result.Service | Should -Be "network" + $result.Module | Should -Be "azure-resourcemanager-network" + } + + It "Should extract service and module from forward-slash path with different repo name" { + $result = Get-ServiceAndModuleFromPath ` + -PackagePath "C:/repos/my-java-sdk/sdk/storage/azure-storage-blob" ` + -SdkRepoPath "C:/repos/my-java-sdk" + + $result.Service | Should -Be "storage" + $result.Module | Should -Be "azure-storage-blob" + } + + It "Should handle trailing slashes" { + $result = Get-ServiceAndModuleFromPath ` + -PackagePath "C:\repos\azure-sdk-for-java\sdk\network\azure-resourcemanager-network\" ` + -SdkRepoPath "C:\repos\azure-sdk-for-java\" + + $result.Service | Should -Be "network" + $result.Module | Should -Be "azure-resourcemanager-network" + } + + It "Should throw when path is not under sdk/ directory" { + { Get-ServiceAndModuleFromPath ` + -PackagePath "C:\repos\other-repo\lib\something" ` + -SdkRepoPath "C:\repos\azure-sdk-for-java" } | Should -Throw "*not under sdk/ directory*" + } + + It "Should throw when path has insufficient parts" { + { Get-ServiceAndModuleFromPath ` + -PackagePath "C:\repos\azure-sdk-for-java\sdk\network" ` + -SdkRepoPath "C:\repos\azure-sdk-for-java" } | Should -Throw "*Cannot determine service and module*" + } +} + +# ============================================================================ +# Get-GroupIdFromPom tests +# ============================================================================ +Describe "Get-GroupIdFromPom" { + BeforeEach { + $script:TestPomPath = Join-Path $script:TestRoot "pom.xml" + } + + AfterEach { + if (Test-Path $script:TestPomPath) { + Remove-Item $script:TestPomPath -Force + } + } + + It "Should extract direct groupId" { + Set-Content -Path $script:TestPomPath -Value $script:SamplePomXml + $result = Get-GroupIdFromPom -PomPath $script:TestPomPath + $result | Should -Be "com.azure.resourcemanager" + } + + It "Should extract groupId from parent when not in project" { + Set-Content -Path $script:TestPomPath -Value $script:SamplePomWithParentXml + $result = Get-GroupIdFromPom -PomPath $script:TestPomPath + $result | Should -Be "com.azure" + } + + It "Should throw when groupId is missing entirely" { + $invalidPom = @" + + + 4.0.0 + some-artifact + +"@ + Set-Content -Path $script:TestPomPath -Value $invalidPom + { Get-GroupIdFromPom -PomPath $script:TestPomPath } | Should -Throw "*Could not extract groupId*" + } +} + +# ============================================================================ +# Add-ModuleToModulesBlock tests +# ============================================================================ +Describe "Add-ModuleToModulesBlock" { + It "Should add a new module in sorted order" { + $block = @" + + sdk/advisor + sdk/storage + +"@ + $result = Add-ModuleToModulesBlock -ModulesBlock $block -Module "sdk/network" + $result | Should -Match "sdk/advisor" + $result | Should -Match "sdk/network" + $result | Should -Match "sdk/storage" + + # Verify order: advisor < network < storage + $advisorIdx = $result.IndexOf("sdk/advisor") + $networkIdx = $result.IndexOf("sdk/network") + $storageIdx = $result.IndexOf("sdk/storage") + $networkIdx | Should -BeGreaterThan $advisorIdx + $storageIdx | Should -BeGreaterThan $networkIdx + } + + It "Should not duplicate an existing module" { + $block = @" + + sdk/network + sdk/storage + +"@ + $result = Add-ModuleToModulesBlock -ModulesBlock $block -Module "sdk/network" + $count = ([regex]::Matches($result, 'sdk/network')).Count + $count | Should -Be 1 + } + + It "Should preserve indentation from the source block" { + $block = " `n sdk/a`n " + $result = Add-ModuleToModulesBlock -ModulesBlock $block -Module "sdk/b" + # The closing had indent " " (4 spaces), so module indent should be " " (6 spaces) + $result | Should -Match " sdk/b" + } +} + +# ============================================================================ +# Add-ModuleToPom tests +# ============================================================================ +Describe "Add-ModuleToPom" { + It "Should add module to a simple POM" { + $pom = @" + + + azure-resourcemanager-network + + +"@ + $result = Add-ModuleToPom -PomContent $pom -Module "azure-resourcemanager-compute" + $result.Success | Should -Be $true + $result.Content | Should -Match "azure-resourcemanager-compute" + $result.Content | Should -Match "azure-resourcemanager-network" + } + + It "Should skip when module already exists (idempotent)" { + $pom = @" + + + azure-resourcemanager-network + + +"@ + $result = Add-ModuleToPom -PomContent $pom -Module "azure-resourcemanager-network" + $result.Success | Should -Be $true + $result.Content | Should -Be $pom + } + + It "Should add module to root POM style (sdk/ prefixed)" { + $result = Add-ModuleToPom -PomContent $script:SampleRootPomXml -Module "sdk/newservice" + $result.Success | Should -Be $true + $result.Content | Should -Match "sdk/newservice" + + # Verify sorted order + $networkIdx = $result.Content.IndexOf("sdk/network") + $newserviceIdx = $result.Content.IndexOf("sdk/newservice") + $storageIdx = $result.Content.IndexOf("sdk/storage") + $newserviceIdx | Should -BeGreaterThan $networkIdx + $storageIdx | Should -BeGreaterThan $newserviceIdx + } + + It "Should fail when no block exists" { + $pom = "com.azure" + $result = Add-ModuleToPom -PomContent $pom -Module "new-module" + $result.Success | Should -Be $false + } + + It "Should fail when multiple blocks without profiles" { + $pom = @" + + + a + + + b + + +"@ + $result = Add-ModuleToPom -PomContent $pom -Module "c" + $result.Success | Should -Be $false + } +} + +# ============================================================================ +# Add-ModuleToDefaultProfile tests +# ============================================================================ +Describe "Add-ModuleToDefaultProfile" { + It "Should add module to the default profile's modules" { + $result = Add-ModuleToDefaultProfile -PomContent $script:SamplePomWithProfiles -Module "sdk/network" + $result.Success | Should -Be $true + $result.Content | Should -Match "sdk/network" + + # Verify it was added to the default profile, not the other + # Check order in default profile: advisor < network < storage + $defaultProfileMatch = [regex]::Match($result.Content, '(?s)default.*?') + $defaultProfile = $defaultProfileMatch.Value + $defaultProfile | Should -Match "sdk/advisor" + $defaultProfile | Should -Match "sdk/network" + $defaultProfile | Should -Match "sdk/storage" + } + + It "Should not modify other profiles" { + $result = Add-ModuleToDefaultProfile -PomContent $script:SamplePomWithProfiles -Module "sdk/network" + $result.Success | Should -Be $true + + # The "other" profile should still only have sdk/other + $otherProfileMatch = [regex]::Match($result.Content, '(?s)other.*?') + $otherProfile = $otherProfileMatch.Value + $otherProfile | Should -Not -Match "sdk/network" + $otherProfile | Should -Match "sdk/other" + } + + It "Should fail when no default profile exists" { + $pom = @" + + + + custom + + sdk/a + + + + +"@ + $result = Add-ModuleToDefaultProfile -PomContent $pom -Module "sdk/b" + $result.Success | Should -Be $false + } +} + +# ============================================================================ +# Add-ModuleToPom with profiles tests +# ============================================================================ +Describe "Add-ModuleToPom with profiles" { + It "Should delegate to default profile when multiple and exist" { + $result = Add-ModuleToPom -PomContent $script:SamplePomWithProfiles -Module "sdk/network" + $result.Success | Should -Be $true + $result.Content | Should -Match "sdk/network" + } +} + +# ============================================================================ +# Update-RootPom tests +# ============================================================================ +Describe "Update-RootPom" { + BeforeEach { + $script:TestSdkRoot = Join-Path $script:TestRoot "sdk-root-$(New-Guid)" + New-Item -ItemType Directory -Path $script:TestSdkRoot -Force | Out-Null + } + + AfterEach { + if (Test-Path $script:TestSdkRoot) { + Remove-Item -Path $script:TestSdkRoot -Recurse -Force + } + } + + It "Should add new service module to root pom" { + Set-Content -Path (Join-Path $script:TestSdkRoot "pom.xml") -Value $script:SampleRootPomXml + + Update-RootPom -SdkRepoPath $script:TestSdkRoot -Service "compute" + + $content = Get-Content (Join-Path $script:TestSdkRoot "pom.xml") -Raw + $content | Should -Match "sdk/compute" + } + + It "Should skip when service already exists" { + Set-Content -Path (Join-Path $script:TestSdkRoot "pom.xml") -Value $script:SampleRootPomXml + + Update-RootPom -SdkRepoPath $script:TestSdkRoot -Service "network" + + $content = Get-Content (Join-Path $script:TestSdkRoot "pom.xml") -Raw + $count = ([regex]::Matches($content, 'sdk/network')).Count + $count | Should -Be 1 + } + + It "Should not throw when root pom does not exist" { + { Update-RootPom -SdkRepoPath $script:TestSdkRoot -Service "newservice" } | Should -Not -Throw + } +} + +# ============================================================================ +# Update-ServicePom tests +# ============================================================================ +Describe "Update-ServicePom" { + BeforeEach { + $script:TestSdkRoot = Join-Path $script:TestRoot "sdk-svc-$(New-Guid)" + New-Item -ItemType Directory -Path (Join-Path $script:TestSdkRoot "sdk" "network") -Force | Out-Null + } + + AfterEach { + if (Test-Path $script:TestSdkRoot) { + Remove-Item -Path $script:TestSdkRoot -Recurse -Force + } + } + + It "Should create new service pom when it does not exist" { + Update-ServicePom -SdkRepoPath $script:TestSdkRoot -Service "newservice" -Module "azure-resourcemanager-newservice" + + $pomFile = Join-Path $script:TestSdkRoot "sdk" "newservice" "pom.xml" + Test-Path $pomFile | Should -Be $true + + $content = Get-Content $pomFile -Raw + $content | Should -Match "azure-newservice-service" + $content | Should -Match "azure-resourcemanager-newservice" + } + + It "Should add module to existing service pom" { + $existingPom = Join-Path $script:TestSdkRoot "sdk" "network" "pom.xml" + Set-Content -Path $existingPom -Value $script:SampleServicePomXml + + Update-ServicePom -SdkRepoPath $script:TestSdkRoot -Service "network" -Module "azure-resourcemanager-network-extra" + + $content = Get-Content $existingPom -Raw + $content | Should -Match "azure-resourcemanager-network" + $content | Should -Match "azure-resourcemanager-network-extra" + } + + It "Should skip when module already exists in service pom" { + $existingPom = Join-Path $script:TestSdkRoot "sdk" "network" "pom.xml" + Set-Content -Path $existingPom -Value $script:SampleServicePomXml + + Update-ServicePom -SdkRepoPath $script:TestSdkRoot -Service "network" -Module "azure-resourcemanager-network" + + $content = Get-Content $existingPom -Raw + $count = ([regex]::Matches($content, 'azure-resourcemanager-network')).Count + $count | Should -Be 1 + } +} + +# ============================================================================ +# Script integration tests +# ============================================================================ +Describe "Script Integration" { + It "Should verify the main script imports the helper correctly" { + $scriptPath = Join-Path $PSScriptRoot ".." "Automation-Sdk-UpdateMetadata.ps1" + $scriptContent = Get-Content $scriptPath -Raw + + $scriptContent | Should -Match '\. \$helperPath' + $scriptContent | Should -Match 'Metadata-Helpers\.ps1' + } + + It "Should verify the main script does not duplicate function definitions" { + $scriptPath = Join-Path $PSScriptRoot ".." "Automation-Sdk-UpdateMetadata.ps1" + $scriptContent = Get-Content $scriptPath -Raw + + $scriptContent | Should -Not -Match 'function Get-ServiceAndModuleFromPath' + $scriptContent | Should -Not -Match 'function Add-ModuleToPom' + $scriptContent | Should -Not -Match 'function Update-RootPom' + $scriptContent | Should -Not -Match 'function Update-ServicePom' + } + + It "Should verify the helper file exports all required functions" { + $helperPath = Join-Path $PSScriptRoot ".." "helpers" "Metadata-Helpers.ps1" + + { . $helperPath } | Should -Not -Throw + + Get-Command Get-ServiceAndModuleFromPath -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Get-GroupIdFromPom -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Add-ModuleToModulesBlock -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Add-ModuleToPom -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Add-ModuleToDefaultProfile -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Update-RootPom -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Update-ServicePom -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command New-CiYmlContent -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Add-ArtifactToCiYml -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command ConvertTo-CiYmlString -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + Get-Command Update-CiYml -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + + It "Should verify swagger_to_sdk_config.json references the metadata script" { + $configPath = Join-Path $PSScriptRoot ".." ".." "swagger_to_sdk_config.json" + $config = Get-Content $configPath -Raw | ConvertFrom-Json + + $config.packageOptions.updateMetadataScript.path | Should -Be "./eng/scripts/Automation-Sdk-UpdateMetadata.ps1" + } +} + +# ============================================================================ +# End-to-end: Case 1 - Existing service, new resourcemanager package +# ============================================================================ +Describe "End-to-End: Existing Service, New Package" { + BeforeEach { + $script:E2ERoot = Join-Path $script:TestRoot "e2e-case1-$(New-Guid)" + New-Item -ItemType Directory -Path $script:E2ERoot -Force | Out-Null + + # Set up SDK repo with existing service (already in root pom, has service pom) + Set-Content -Path (Join-Path $script:E2ERoot "pom.xml") -Value $script:SampleRootPomXml + $svcDir = Join-Path $script:E2ERoot "sdk" "network" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "pom.xml") -Value $script:SampleServicePomXml + + # Package directory exists + $pkgDir = Join-Path $svcDir "azure-resourcemanager-network-extra" + New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null + Set-Content -Path (Join-Path $pkgDir "pom.xml") -Value $script:SamplePomXml + } + + AfterEach { + if (Test-Path $script:E2ERoot) { + Remove-Item -Path $script:E2ERoot -Recurse -Force + } + } + + It "Should update service pom but skip root pom" { + $service = "network" + $module = "azure-resourcemanager-network-extra" + + $rootPomBefore = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + # Root pom unchanged (service already existed) + $rootPomAfter = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $rootPomAfter | Should -Be $rootPomBefore + + # Service pom has the new module + $svcPomContent = Get-Content (Join-Path $script:E2ERoot "sdk" "network" "pom.xml") -Raw + $svcPomContent | Should -Match "azure-resourcemanager-network" + $svcPomContent | Should -Match "azure-resourcemanager-network-extra" + } + + It "Should be idempotent when run twice" { + $service = "network" + $module = "azure-resourcemanager-network-extra" + + # First run + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + $rootPom1 = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $svcPom1 = Get-Content (Join-Path $script:E2ERoot "sdk" "network" "pom.xml") -Raw + + # Second run + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + $rootPom2 = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $svcPom2 = Get-Content (Join-Path $script:E2ERoot "sdk" "network" "pom.xml") -Raw + + $rootPom2 | Should -Be $rootPom1 + $svcPom2 | Should -Be $svcPom1 + } +} + +# ============================================================================ +# End-to-end: Case 2 - Brand new service +# ============================================================================ +Describe "End-to-End: New Service" { + BeforeEach { + $script:E2ERoot = Join-Path $script:TestRoot "e2e-case2-$(New-Guid)" + New-Item -ItemType Directory -Path $script:E2ERoot -Force | Out-Null + + # Set up minimal SDK repo structure (root pom only, no service dir) + Set-Content -Path (Join-Path $script:E2ERoot "pom.xml") -Value $script:SampleRootPomXml + $pkgDir = Join-Path $script:E2ERoot "sdk" "compute" "azure-resourcemanager-compute" + New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null + Set-Content -Path (Join-Path $pkgDir "pom.xml") -Value $script:SamplePomXml + } + + AfterEach { + if (Test-Path $script:E2ERoot) { + Remove-Item -Path $script:E2ERoot -Recurse -Force + } + } + + It "Should update root pom and create service pom" { + $service = "compute" + $module = "azure-resourcemanager-compute" + + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + # Verify root pom has new service + $rootPom = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $rootPom | Should -Match "sdk/compute" + + # Verify service pom was created + $svcPom = Join-Path $script:E2ERoot "sdk" "compute" "pom.xml" + Test-Path $svcPom | Should -Be $true + $svcPomContent = Get-Content $svcPom -Raw + $svcPomContent | Should -Match "azure-resourcemanager-compute" + } + + It "Should be idempotent when run twice" { + $service = "compute" + $module = "azure-resourcemanager-compute" + + # First run + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + $rootPom1 = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $svcPom1 = Get-Content (Join-Path $script:E2ERoot "sdk" "compute" "pom.xml") -Raw + + # Second run + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + + $rootPom2 = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $svcPom2 = Get-Content (Join-Path $script:E2ERoot "sdk" "compute" "pom.xml") -Raw + + $rootPom2 | Should -Be $rootPom1 + $svcPom2 | Should -Be $svcPom1 + } +} + +# ============================================================================ +# New-CiYmlContent tests +# ============================================================================ +Describe "New-CiYmlContent" { + It "Should generate valid YAML with correct service and module" { + $result = New-CiYmlContent -Service "network" -Module "azure-resourcemanager-network" + + $result | Should -Match "sdk/network/ci.yml" + $result | Should -Match "sdk/network/azure-resourcemanager-network/" + $result | Should -Match "sdk/network/azure-resourcemanager-network/pom.xml" + $result | Should -Match "ServiceDirectory: network" + $result | Should -Match "Artifacts: \[\]" + } + + It "Should be parseable YAML" { + $content = New-CiYmlContent -Service "compute" -Module "azure-resourcemanager-compute" + $parsed = ConvertFrom-Yaml $content -Ordered + $parsed["trigger"] | Should -Not -BeNullOrEmpty + $parsed["pr"] | Should -Not -BeNullOrEmpty + $parsed["extends"]["parameters"]["ServiceDirectory"] | Should -Be "compute" + } +} + +# ============================================================================ +# Add-ArtifactToCiYml tests +# ============================================================================ +Describe "Add-ArtifactToCiYml" { + It "Should add artifact without releaseInBatch when no parameters exist" { + $content = New-CiYmlContent -Service "newservice" -Module "azure-resourcemanager-newservice" + $ciYml = ConvertFrom-Yaml $content -Ordered + + $result = Add-ArtifactToCiYml -CiYml $ciYml -Module "azure-resourcemanager-newservice" -GroupId "com.azure.resourcemanager" + + $result | Should -Be $true + $artifacts = $ciYml["extends"]["parameters"]["Artifacts"] + $artifacts.Count | Should -Be 1 + $artifacts[0]["name"] | Should -Be "azure-resourcemanager-newservice" + $artifacts[0]["groupId"] | Should -Be "com.azure.resourcemanager" + $artifacts[0]["safeName"] | Should -Be "azureresourcemanagernewservice" + $artifacts[0].Keys | Should -Not -Contain "releaseInBatch" + } + + It "Should add artifact with releaseInBatch when parameters list exists" { + $ciYml = ConvertFrom-Yaml $script:SampleCiYmlWithParams -Ordered + + $result = Add-ArtifactToCiYml -CiYml $ciYml -Module "azure-resourcemanager-network-extra" -GroupId "com.azure.resourcemanager" + + $result | Should -Be $true + $artifacts = $ciYml["extends"]["parameters"]["Artifacts"] + $artifacts.Count | Should -Be 2 + $newArtifact = $artifacts[1] + $newArtifact["name"] | Should -Be "azure-resourcemanager-network-extra" + $newArtifact["releaseInBatch"] | Should -Not -BeNullOrEmpty + + # Check release parameter was added + $params = $ciYml["parameters"] + $params.Count | Should -Be 2 + $newParam = $params[1] + $newParam["name"] | Should -Be "release_azureresourcemanagernetworkextra" + $newParam["default"] | Should -Be $false # management-plane + } + + It "Should set release param default=true for data-plane packages" { + $ciYml = ConvertFrom-Yaml $script:SampleCiYmlWithParams -Ordered + + $result = Add-ArtifactToCiYml -CiYml $ciYml -Module "azure-storage-blob" -GroupId "com.azure" + + $result | Should -Be $true + $params = $ciYml["parameters"] + $newParam = $params | Where-Object { $_["name"] -eq "release_azurestorageblob" } + $newParam["default"] | Should -Be $true # data-plane + } + + It "Should skip when module already exists" { + $ciYml = ConvertFrom-Yaml $script:SampleCiYmlWithParams -Ordered + + $result = Add-ArtifactToCiYml -CiYml $ciYml -Module "azure-resourcemanager-network" -GroupId "com.azure.resourcemanager" + + $result | Should -Be $false + $ciYml["extends"]["parameters"]["Artifacts"].Count | Should -Be 1 + } + + It "Should return false for unexpected format" { + $ciYml = [ordered]@{ "trigger" = @{} } + + $result = Add-ArtifactToCiYml -CiYml $ciYml -Module "azure-test" -GroupId "com.azure" + + $result | Should -Be $false + } +} + +# ============================================================================ +# Update-CiYml tests +# ============================================================================ +Describe "Update-CiYml" { + BeforeEach { + $script:CiTestRoot = Join-Path $script:TestRoot "ci-test-$(New-Guid)" + New-Item -ItemType Directory -Path $script:CiTestRoot -Force | Out-Null + } + + AfterEach { + if (Test-Path $script:CiTestRoot) { + Remove-Item -Path $script:CiTestRoot -Recurse -Force + } + } + + It "Should create ci.yml when it does not exist" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "newservice" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "newservice" -Module "azure-resourcemanager-newservice" -GroupId "com.azure.resourcemanager" + + $ciFile = Join-Path $svcDir "ci.yml" + Test-Path $ciFile | Should -Be $true + + $content = Get-Content $ciFile -Raw + $content | Should -Match "azure-resourcemanager-newservice" + $content | Should -Match "com.azure.resourcemanager" + $content | Should -Match "azureresourcemanagernewservice" + } + + It "Should add artifact to existing ci.yml without parameters" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "communication" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlNoParams + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "communication" -Module "azure-resourcemanager-communication-extra" -GroupId "com.azure.resourcemanager" + + $content = Get-Content (Join-Path $svcDir "ci.yml") -Raw + $content | Should -Match "azure-resourcemanager-communication" + $content | Should -Match "azure-resourcemanager-communication-extra" + } + + It "Should add artifact to existing ci.yml with release parameters" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "network" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlWithParams + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "network" -Module "azure-resourcemanager-network-extra" -GroupId "com.azure.resourcemanager" + + $content = Get-Content (Join-Path $svcDir "ci.yml") -Raw + $content | Should -Match "azure-resourcemanager-network-extra" + $content | Should -Match "release_azureresourcemanagernetworkextra" + } + + It "Should skip when module already exists in ci.yml" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "network" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlWithParams + + $before = Get-Content (Join-Path $svcDir "ci.yml") -Raw + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "network" -Module "azure-resourcemanager-network" -GroupId "com.azure.resourcemanager" + + $after = Get-Content (Join-Path $svcDir "ci.yml") -Raw + $after | Should -Be $before + } + + It "Should rename ci.yml to ci.data.yml when SDKType=data and create new ci.yml" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "myservice" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlDataType + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "myservice" -Module "azure-resourcemanager-myservice" -GroupId "com.azure.resourcemanager" + + # ci.data.yml should exist + Test-Path (Join-Path $svcDir "ci.data.yml") | Should -Be $true + + # New ci.yml should have the new module + $content = Get-Content (Join-Path $svcDir "ci.yml") -Raw + $content | Should -Match "azure-resourcemanager-myservice" + $content | Should -Not -Match "SDKType" + } + + It "Should be idempotent when run twice" { + $svcDir = Join-Path $script:CiTestRoot "sdk" "network" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlWithParams + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "network" -Module "azure-resourcemanager-network-extra" -GroupId "com.azure.resourcemanager" + $content1 = Get-Content (Join-Path $svcDir "ci.yml") -Raw + + Update-CiYml -SdkRepoPath $script:CiTestRoot -Service "network" -Module "azure-resourcemanager-network-extra" -GroupId "com.azure.resourcemanager" + $content2 = Get-Content (Join-Path $svcDir "ci.yml") -Raw + + $content2 | Should -Be $content1 + } +} + +# ============================================================================ +# End-to-end: CI.yml with POM - Existing service, new package +# ============================================================================ +Describe "End-to-End: Existing Service with CI update" { + BeforeEach { + $script:E2ERoot = Join-Path $script:TestRoot "e2e-ci-case1-$(New-Guid)" + New-Item -ItemType Directory -Path $script:E2ERoot -Force | Out-Null + + Set-Content -Path (Join-Path $script:E2ERoot "pom.xml") -Value $script:SampleRootPomXml + $svcDir = Join-Path $script:E2ERoot "sdk" "network" + New-Item -ItemType Directory -Path $svcDir -Force | Out-Null + Set-Content -Path (Join-Path $svcDir "pom.xml") -Value $script:SampleServicePomXml + Set-Content -Path (Join-Path $svcDir "ci.yml") -Value $script:SampleCiYmlWithParams + + $pkgDir = Join-Path $svcDir "azure-resourcemanager-network-extra" + New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null + Set-Content -Path (Join-Path $pkgDir "pom.xml") -Value $script:SamplePomXml + } + + AfterEach { + if (Test-Path $script:E2ERoot) { + Remove-Item -Path $script:E2ERoot -Recurse -Force + } + } + + It "Should update service pom, ci.yml, and skip root pom" { + $service = "network" + $module = "azure-resourcemanager-network-extra" + $groupId = "com.azure.resourcemanager" + + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + Update-CiYml -SdkRepoPath $script:E2ERoot -Service $service -Module $module -GroupId $groupId + + # Service pom has the new module + $svcPomContent = Get-Content (Join-Path $script:E2ERoot "sdk" "network" "pom.xml") -Raw + $svcPomContent | Should -Match "azure-resourcemanager-network-extra" + + # ci.yml has the new artifact + $ciContent = Get-Content (Join-Path $script:E2ERoot "sdk" "network" "ci.yml") -Raw + $ciContent | Should -Match "azure-resourcemanager-network-extra" + $ciContent | Should -Match "release_azureresourcemanagernetworkextra" + } +} + +# ============================================================================ +# End-to-end: CI.yml with POM - Brand new service +# ============================================================================ +Describe "End-to-End: New Service with CI update" { + BeforeEach { + $script:E2ERoot = Join-Path $script:TestRoot "e2e-ci-case2-$(New-Guid)" + New-Item -ItemType Directory -Path $script:E2ERoot -Force | Out-Null + + Set-Content -Path (Join-Path $script:E2ERoot "pom.xml") -Value $script:SampleRootPomXml + $pkgDir = Join-Path $script:E2ERoot "sdk" "compute" "azure-resourcemanager-compute" + New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null + Set-Content -Path (Join-Path $pkgDir "pom.xml") -Value $script:SamplePomXml + } + + AfterEach { + if (Test-Path $script:E2ERoot) { + Remove-Item -Path $script:E2ERoot -Recurse -Force + } + } + + It "Should create ci.yml, service pom, and update root pom" { + $service = "compute" + $module = "azure-resourcemanager-compute" + $groupId = "com.azure.resourcemanager" + + Update-RootPom -SdkRepoPath $script:E2ERoot -Service $service + Update-ServicePom -SdkRepoPath $script:E2ERoot -Service $service -Module $module + Update-CiYml -SdkRepoPath $script:E2ERoot -Service $service -Module $module -GroupId $groupId + + # Root pom updated + $rootPom = Get-Content (Join-Path $script:E2ERoot "pom.xml") -Raw + $rootPom | Should -Match "sdk/compute" + + # Service pom created + $svcPom = Get-Content (Join-Path $script:E2ERoot "sdk" "compute" "pom.xml") -Raw + $svcPom | Should -Match "azure-resourcemanager-compute" + + # ci.yml created with artifact + $ciFile = Join-Path $script:E2ERoot "sdk" "compute" "ci.yml" + Test-Path $ciFile | Should -Be $true + $ciContent = Get-Content $ciFile -Raw + $ciContent | Should -Match "azure-resourcemanager-compute" + $ciContent | Should -Match "com.azure.resourcemanager" + $ciContent | Should -Match "ServiceDirectory: compute" + } +} + +# ============================================================================ +# End-to-end: Unsupported SDK type should fail the run +# ============================================================================ +Describe "End-to-End: Unsupported SDK Type" { + BeforeEach { + $script:E2ERoot = Join-Path $script:TestRoot "e2e-unsupported-$(New-Guid)" + New-Item -ItemType Directory -Path $script:E2ERoot -Force | Out-Null + + Set-Content -Path (Join-Path $script:E2ERoot "pom.xml") -Value $script:SampleRootPomXml + $pkgDir = Join-Path $script:E2ERoot "sdk" "spring" "spring-cloud-azure-core" + New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null + + $springPom = @" + + + 4.0.0 + com.azure.spring + spring-cloud-azure-core + 4.0.0 + +"@ + Set-Content -Path (Join-Path $pkgDir "pom.xml") -Value $springPom + } + + AfterEach { + if (Test-Path $script:E2ERoot) { + Remove-Item -Path $script:E2ERoot -Recurse -Force + } + } + + It "Should fail for unsupported groupId (e.g., com.azure.spring)" { + $scriptPath = Join-Path $PSScriptRoot ".." "Automation-Sdk-UpdateMetadata.ps1" + $pkgPath = Join-Path $script:E2ERoot "sdk" "spring" "spring-cloud-azure-core" + + $output = pwsh -NoProfile -File $scriptPath -PackagePath $pkgPath -SdkRepoPath $script:E2ERoot 2>&1 + $LASTEXITCODE | Should -Be 1 + $output | Out-String | Should -Match "Unsupported SDK type" + } +} diff --git a/eng/swagger_to_sdk_config.json b/eng/swagger_to_sdk_config.json index 505259f7bf6f..346af09f88be 100644 --- a/eng/swagger_to_sdk_config.json +++ b/eng/swagger_to_sdk_config.json @@ -31,6 +31,9 @@ }, "updateChangelogContentScript": { "path": "./eng/scripts/Automation-Sdk-UpdateChangelog.ps1" + }, + "updateMetadataScript": { + "path": "./eng/scripts/Automation-Sdk-UpdateMetadata.ps1" } } }