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"
}
}
}