diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs index b91d393d193..999694f1a06 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/DevOpsService.cs @@ -112,7 +112,7 @@ public interface IDevOpsService public partial class DevOpsService(ILogger logger, IDevOpsConnection connection) : IDevOpsService { - private static readonly string RELEASE_PLANER_APP_TEST = "Release Planner App Test"; + private static readonly string RELEASE_PLANNER_APP_TEST = "Release Planner App Test"; private List? _cachedRelationTypes; private static readonly string[] SUPPORTED_SDK_LANGUAGES = { "Dotnet", "JavaScript", "Python", "Java", "Go" }; @@ -128,7 +128,7 @@ public async Task> ListOverdueReleasePlansAsync() try { var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}'"; - query += $" AND [System.Tags] NOT CONTAINS '{RELEASE_PLANER_APP_TEST}'"; + query += $" AND [System.Tags] NOT CONTAINS '{RELEASE_PLANNER_APP_TEST}'"; query += " AND [System.WorkItemType] = 'Release Plan'"; query += " AND [System.State] IN ('In Progress','Not Started','New')"; query += " AND [Custom.SDKReleasemonth] <> ''"; @@ -186,7 +186,7 @@ public async Task> GetReleasePlansForProductAsync(stri try { var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}'"; - query += $" AND [System.Tags] {(isTestReleasePlan ? "CONTAINS" : "NOT CONTAINS")} '{RELEASE_PLANER_APP_TEST}'"; + query += $" AND [System.Tags] {(isTestReleasePlan ? "CONTAINS" : "NOT CONTAINS")} '{RELEASE_PLANNER_APP_TEST}'"; query += $" AND [Custom.ProductServiceTreeID] = '{productTreeId}'"; query += $" AND [Custom.SDKtypetobereleased] = '{sdkReleaseType}'"; query += " AND [System.WorkItemType] = 'Release Plan'"; @@ -225,7 +225,7 @@ public async Task> GetReleasePlansForPackageAsync(stri var languageId = MapLanguageToId(language); var escapedPackageName = packageName?.Replace("'", "''"); var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}'"; - query += $" AND [System.Tags] {(isTestReleasePlan ? "CONTAINS" : "NOT CONTAINS")} '{RELEASE_PLANER_APP_TEST}'"; + query += $" AND [System.Tags] {(isTestReleasePlan ? "CONTAINS" : "NOT CONTAINS")} '{RELEASE_PLANNER_APP_TEST}'"; query += $" AND [Custom.{languageId}PackageName] = '{escapedPackageName}'"; query += " AND [System.WorkItemType] = 'Release Plan'"; query += " AND [System.State] = 'In Progress'"; @@ -1164,7 +1164,7 @@ public async Task GetPackageWorkItemAsync(string packag throw new ArgumentException("Invalid data in one of the parameters."); } - var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}' AND [Custom.Package] = '{packageName}' AND [Custom.Language] = '{language}' AND [System.WorkItemType] = 'Package' AND [System.State] NOT IN ('Closed','Duplicate','Abandoned') AND [System.Tags] NOT CONTAINS '{RELEASE_PLANER_APP_TEST}'"; + var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}' AND [Custom.Package] = '{packageName}' AND [Custom.Language] = '{language}' AND [System.WorkItemType] = 'Package' AND [System.State] NOT IN ('Closed','Duplicate','Abandoned') AND [System.Tags] NOT CONTAINS '{RELEASE_PLANNER_APP_TEST}'"; if (!string.IsNullOrEmpty(packageVersion)) { query += $" AND [Custom.PackageVersion] = '{packageVersion}'"; @@ -1191,7 +1191,7 @@ public async Task> ListPartialPackageWorkItemAsync throw new ArgumentException("Invalid data in one of the parameters."); } - var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}' AND [Custom.Package] CONTAINS '{packageName}' AND [Custom.Language] = '{language}' AND [System.WorkItemType] = 'Package' AND [System.State] NOT IN ('Closed','Duplicate','Abandoned') AND [System.Tags] NOT CONTAINS '{RELEASE_PLANER_APP_TEST}'"; + var query = $"SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '{Constants.AZURE_SDK_DEVOPS_RELEASE_PROJECT}' AND [Custom.Package] CONTAINS '{packageName}' AND [Custom.Language] = '{language}' AND [System.WorkItemType] = 'Package' AND [System.State] NOT IN ('Closed','Duplicate','Abandoned') AND [System.Tags] NOT CONTAINS '{RELEASE_PLANNER_APP_TEST}'"; query += " ORDER BY [System.Id] DESC"; // Order by package work item to find the most recently created logger.LogInformation("Fetching package work item with package name {packageName} and language {language}.", packageName, language); @@ -1411,6 +1411,88 @@ public async Task UpdateWorkItemAsync(int workItemId, Dictionary FindOrCreateServiceParent(string serviceName, bool ignoreReleasePlannerTests = true, string? tag = null) + { + var serviceParent = await FindServiceWorkItem(serviceName, ignoreReleasePlannerTests, tag); + if (serviceParent != null) + { + logger.LogDebug("Found existing service work item [{workItemId}]", serviceParent.Id); + return serviceParent; + } + + var serviceWorkItem = new EpicWorkItem + { + ServiceName = serviceName, + EpicType = "Service" + }; + + if (!string.IsNullOrEmpty(tag)) + { + serviceWorkItem.Tag = tag; + } + + var workItem = await CreateWorkItemAsync(serviceWorkItem, "Epic", serviceName); + + logger.LogInformation("[{workItemId}] - Created service work item for {serviceName}", workItem.Id, serviceName); + return workItem; + } + + private async Task FindEpicWorkItem(string serviceName, string? packageDisplayName = null, bool ignoreReleasePlannerTests = true, string? tag = null) + { + var serviceCondition = new StringBuilder(); + + if (!string.IsNullOrEmpty(serviceName)) + { + serviceCondition.Append($"[Custom.ServiceName] = '{serviceName}'"); + + if (!string.IsNullOrEmpty(packageDisplayName)) + { + serviceCondition.Append($" AND [Custom.PackageDisplayName] = '{packageDisplayName}'"); + } + else + { + serviceCondition.Append(" AND [Custom.PackageDisplayName] = ''"); + } + } + else + { + serviceCondition.Append("[Custom.ServiceName] <> ''"); + } + + if (!string.IsNullOrEmpty(tag)) + { + serviceCondition.Append($" AND [System.Tags] CONTAINS '{tag}'"); + } + + if (ignoreReleasePlannerTests) + { + serviceCondition.Append($" AND [System.Tags] NOT CONTAINS '{RELEASE_PLANNER_APP_TEST}'"); + } + + var query = $"SELECT [System.Id], [Custom.ServiceName], [Custom.PackageDisplayName], [System.Parent], [System.Tags] FROM WorkItems WHERE [System.State] <> 'Duplicate' AND [System.WorkItemType] = 'Epic' AND {serviceCondition}"; + + logger.LogDebug("Finding parent work item with query: {query}", query); + + var workItems = await FetchWorkItemsAsync(query); + + if (workItems.Count > 0) + { + if (workItems.Count > 1) + { + logger.LogWarning("Found multiple parent work items matching criteria. Using first match with ID: {workItemId}", workItems[0].Id); + } + return workItems[0]; + } + + return null; + } + + private async Task FindProductWorkItem(string serviceName, string packageDisplayName, bool ignoreReleasePlannerTests = true, string? tag = null) + => await FindEpicWorkItem(serviceName, packageDisplayName, ignoreReleasePlannerTests, tag); + + private async Task FindServiceWorkItem(string serviceName, bool ignoreReleasePlannerTests = true, string? tag = null) + => await FindEpicWorkItem(serviceName, null, ignoreReleasePlannerTests, tag); + private async Task UpdateWorkItemParentAsync(WorkItemBase child, WorkItemBase parent) { if (child.ParentId == parent.WorkItemId)