-
-
Couldn't load subscription status.
- Fork 372
add support for nested task groups #3007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds support for nested task groups in the Azure DevOps Pipeline Processor, enabling migration of task groups that contain other task groups. The implementation uses a multi-phase approach where unnested task groups are migrated first, followed by iterative processing of nested task groups until all dependencies are resolved.
Key changes:
- Introduces multi-phase task group migration with dependency resolution
- Adds service connection mapping support for task groups
- Implements null-safe checks for task definition types
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
src/MigrationTools.Clients.AzureDevops.Rest/Processors/AzureDevOpsPipelineProcessor.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of this PR 🙂
I'll take a more in depth look at the changes in the next days as I'm currently on vacation.
Until then, fix the comments Copilot made and try to get a successful build/test run. Copilot's requested changes look good to me. Additionally it would be nice if you introduce a helper function for this check:
t.Task.DefinitionType?.ToLower() == "metaTask".ToLower()We call it many times and I think it makes sense that it is executed only in one single method.
…vOpsPipelineProcessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…vOpsPipelineProcessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…vOpsPipelineProcessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…vOpsPipelineProcessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
add IsMetaTask method for multiple used checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.
| do | ||
| { | ||
| // We need to process the nested taskgroups in a loop, because they can contain other nested taskgroups. | ||
| taskGroupsToMigrate.Clear(); | ||
| foreach (var taskGroup in nestedTaskGroups) | ||
| { | ||
| var nestedTaskGroup = taskGroup.Tasks.Where(t => IsMetaTask(t.Task.DefinitionType)).Select(t => t.Task).ToList(); | ||
| if (nestedTaskGroup.All(t => existingMappings.Any(m => t.Id == m.SourceId))) | ||
| { | ||
| taskGroupsToMigrate.Add(taskGroup); | ||
| } | ||
| } | ||
|
|
||
| nestedTaskGroups = nestedTaskGroups.Where(g => !taskGroupsToMigrate.Any(t => t.Id == g.Id)).ToList(); | ||
| existingMappings = await CreateTaskGroupsAsync(serviceConnectionMappings, targetDefinitions, availableTasks, taskGroupsToMigrate, existingMappings); | ||
| } while (nestedTaskGroups.Any() && taskGroupsToMigrate.Any()); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Infinite loop potential exists when nested task groups have circular dependencies or missing dependencies. The loop continues while nestedTaskGroups.Any() is true, but if taskGroupsToMigrate is empty (no progress made), it should exit to prevent indefinite execution. Consider adding a check to break when taskGroupsToMigrate is empty before awaiting CreateTaskGroupsAsync, or add iteration limit with appropriate logging.
| var newInputs = new Dictionary<string, object>(); | ||
| foreach (var input in (IDictionary<String, Object>)task.Inputs) | ||
| { | ||
| var mapping = serviceConnectionMappings.FirstOrDefault(d => d.SourceId == input.Value.ToString()); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential NullReferenceException if input.Value is null. The code calls .ToString() on input.Value without null checking. Add a null check before calling ToString() or use null-conditional operator: input.Value?.ToString()
| var mapping = serviceConnectionMappings.FirstOrDefault(d => d.SourceId == input.Value.ToString()); | |
| var mapping = serviceConnectionMappings.FirstOrDefault(d => d.SourceId == input.Value?.ToString()); |
| ((IDictionary<String, Object>)task.Inputs).Remove(input.Key); | ||
| ((IDictionary<String, Object>)task.Inputs).Add(input.Key, input.Value); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code removes and re-adds dictionary entries unnecessarily. Instead of removing and adding, directly update the value: ((IDictionary<String, Object>)task.Inputs)[input.Key] = input.Value;
| ((IDictionary<String, Object>)task.Inputs).Remove(input.Key); | |
| ((IDictionary<String, Object>)task.Inputs).Add(input.Key, input.Value); | |
| ((IDictionary<String, Object>)task.Inputs)[input.Key] = input.Value; |
| targetDefinitions = await Target.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false); | ||
| mappings.AddRange(FindExistingMappings(sourceDefinitions, targetDefinitions.Where(d => d.Name != null), mappings)); | ||
| mappings.AddRange(FindExistingMappings(rootSourceDefinitions, targetDefinitions.Where(d => d.Name != null), mappings)); | ||
| mappings.AddRange(existingMappings); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate mappings may be added to the list. The existingMappings are being added after potentially creating new mappings from the same source definitions in line 732. This could result in duplicate entries if the same mapping exists in both collections. Consider using mappings = mappings.Union(existingMappings).ToList() or check for duplicates before adding.
| mappings.AddRange(existingMappings); | |
| mappings = mappings.Union(existingMappings).ToList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new suggested changes by Copilot are valid. Please apply them and respond to by existing comments 😉
|
|
||
| targetDefinitions = await Target.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false); | ||
| mappings.AddRange(FindExistingMappings(sourceDefinitions, targetDefinitions.Where(d => d.Name != null), mappings)); | ||
| mappings.AddRange(existingMappings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this? As far as i see, there are no changes to this list. Therefore we can just reuse it in the method calling this method
| } | ||
|
|
||
| nestedTaskGroups = nestedTaskGroups.Where(g => !taskGroupsToMigrate.Any(t => t.Id == g.Id)).ToList(); | ||
| existingMappings = await CreateTaskGroupsAsync(serviceConnectionMappings, targetDefinitions, availableTasks, taskGroupsToMigrate, existingMappings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to apped the new mappings here instead of overriding the whole list
| var filteredTaskGroups = FilterOutExistingTaskGroups(sourceDefinitions, targetDefinitions); | ||
| filteredTaskGroups = FilterOutIncompatibleTaskGroups(filteredTaskGroups, availableTasks).ToList(); | ||
|
|
||
| var existingMappings = FindExistingMappings(sourceDefinitions, targetDefinitions, new List<Mapping>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of inititalizing a new list here, amke the parameter optional and initialize it in the method itself if not set
For our currently running migration with many classical (and somtime very old) pipelines I added some features to this project. The first is supporting task groups in task groups (nested task groups). With this change the task group migration run multiple times. In the first step all task groups with out task groups are migrated. In the second the task groups related on the migrated task groups. This will be done until no task group is missing or there was a problem.