Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private async System.Threading.Tasks.Task MigratePipelinesAsync()
}
if (Options.MigrateTaskGroups)
{
taskGroupMappings = await CreateTaskGroupDefinitionsAsync();
taskGroupMappings = await CreateTaskGroupDefinitionsAsync(serviceConnectionMappings);
}
if (Options.MigrateBuildPipelines)
{
Expand Down Expand Up @@ -200,8 +200,9 @@ private IEnumerable<TaskGroup> FilterOutExistingTaskGroups(IEnumerable<TaskGroup
/// </summary>
/// <param name="filteredTaskGroups"></param>
/// <param name="availableTasks"></param>
/// <param name="taskGroupMappings"></param>
/// <returns>List of filtered Definitions</returns>
private IEnumerable<TaskGroup> FilterOutIncompatibleTaskGroups(IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<TaskDefinition> availableTasks)
private IEnumerable<TaskGroup> FilterOutIncompatibleTaskGroupsWithMappings(IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<TaskDefinition> availableTasks, IEnumerable<Mapping> taskGroupMappings)
{
var objectsToMigrate = filteredTaskGroups.Where(g =>
{
Expand All @@ -212,6 +213,15 @@ private IEnumerable<TaskGroup> FilterOutIncompatibleTaskGroups(IEnumerable<TaskG
{
return true;
}

if (taskGroupMappings is not null)
{
if (taskGroupMappings.Any(m => m.SourceId == t.Task.Id))
{
return true;
}
}

missingTasksNames.Add(t.DisplayName);
return false;
});
Expand Down Expand Up @@ -277,6 +287,11 @@ not null when typeof(DefinitionType) == typeof(ReleaseDefinition) => definitionN
.ToList();
}

private static bool IsMetaTask(string definitionType)
{
return string.Equals(definitionType, "metaTask", StringComparison.OrdinalIgnoreCase);
}

private async Task<IEnumerable<Mapping>> CreateBuildPipelinesAsync(IEnumerable<Mapping> TaskGroupMapping = null, IEnumerable<Mapping> VariableGroupMapping = null, IEnumerable<Mapping> serviceConnectionMappings = null)
{
Log.LogInformation("Processing Build Pipelines..");
Expand Down Expand Up @@ -311,7 +326,7 @@ private async Task<IEnumerable<Mapping>> CreateBuildPipelinesAsync(IEnumerable<M
{
foreach (var step in phase.Steps)
{
if (step.Task.DefinitionType.ToLower() != "metaTask".ToLower())
if (!IsMetaTask(step.Task.DefinitionType))
{
continue;
}
Expand Down Expand Up @@ -527,7 +542,7 @@ private void UpdateTaskGroupId(ReleaseDefinition definitionToBeMigrated, IEnumer
{
foreach (var WorkflowTask in deployPhase.WorkflowTasks)
{
if (WorkflowTask.DefinitionType != null && WorkflowTask.DefinitionType.ToLower() != "metaTask".ToLower())
if (!IsMetaTask(WorkflowTask.DefinitionType))
{
continue;
}
Expand Down Expand Up @@ -615,27 +630,107 @@ private async Task<IEnumerable<Mapping>> CreateServiceConnectionsAsync()
return mappings;
}

private async Task<IEnumerable<Mapping>> CreateTaskGroupDefinitionsAsync()
private async Task<IEnumerable<Mapping>> CreateTaskGroupDefinitionsAsync(IEnumerable<Mapping> serviceConnectionMappings)
{
Log.LogInformation($"Processing Taskgroups..");

var sourceDefinitions = await Source.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false);
var targetDefinitions = await Target.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false);
var availableTasks = await Target.GetApiDefinitionsAsync<TaskDefinition>(queryForDetails: false);
var filteredTaskGroups = FilterOutExistingTaskGroups(sourceDefinitions, targetDefinitions);
filteredTaskGroups = FilterOutIncompatibleTaskGroups(filteredTaskGroups, availableTasks).ToList();
filteredTaskGroups = FilterOutIncompatibleTaskGroupsWithMappings(filteredTaskGroups, availableTasks, null).ToList();

var existingMappings = FindExistingMappings(sourceDefinitions, targetDefinitions, new List<Mapping>());
Copy link
Collaborator

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


Log.LogInformation($"Phase 1 - Unnested Taskgroups");
var unnestedTaskGroups = filteredTaskGroups.Where(g => g.Tasks.All(t => !IsMetaTask(t.Task.DefinitionType)));
existingMappings = await CreateTaskGroupsAsync(serviceConnectionMappings, targetDefinitions, availableTasks, unnestedTaskGroups, existingMappings);

Log.LogInformation($"Phase 2 - Nested Taskgroups");
var nestedTaskGroups = filteredTaskGroups.Where(g => g.Tasks.Any(t => IsMetaTask(t.Task.DefinitionType))).ToList();
var taskGroupsToMigrate = new List<TaskGroup>();

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);
Copy link
Collaborator

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

} while (nestedTaskGroups.Any() && taskGroupsToMigrate.Any());
Comment on lines +653 to +668
Copy link

Copilot AI Oct 17, 2025

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.

Copilot uses AI. Check for mistakes.

return existingMappings;
}

private async Task<IEnumerable<Mapping>> CreateTaskGroupsAsync(IEnumerable<Mapping> serviceConnectionMappings, IEnumerable<TaskGroup> targetDefinitions, IEnumerable<TaskDefinition> availableTasks, IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<Mapping> existingMappings)
{
filteredTaskGroups = FilterOutIncompatibleTaskGroupsWithMappings(filteredTaskGroups, availableTasks, existingMappings).ToList();

var rootSourceDefinitions = SortDefinitionsByVersion(filteredTaskGroups).First();
var updatedSourceDefinitions = SortDefinitionsByVersion(filteredTaskGroups).Last();

foreach (var definitionToBeMigrated in rootSourceDefinitions)
{
if (serviceConnectionMappings is not null)
{
foreach (var task in definitionToBeMigrated.Tasks)
{
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());
Copy link

Copilot AI Oct 17, 2025

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()

Suggested change
var mapping = serviceConnectionMappings.FirstOrDefault(d => d.SourceId == input.Value.ToString());
var mapping = serviceConnectionMappings.FirstOrDefault(d => d.SourceId == input.Value?.ToString());

Copilot uses AI. Check for mistakes.
if (mapping != null)
{
newInputs.Add(input.Key, mapping.TargetId);
}
}

foreach (var input in newInputs)
{
((IDictionary<String, Object>)task.Inputs).Remove(input.Key);
((IDictionary<String, Object>)task.Inputs).Add(input.Key, input.Value);
Comment on lines +698 to +699
Copy link

Copilot AI Oct 17, 2025

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;

Suggested change
((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;

Copilot uses AI. Check for mistakes.
}
}
}

if (existingMappings is not null)
{
foreach (var task in definitionToBeMigrated.Tasks)
{
if (!IsMetaTask(task.Task.DefinitionType))
{
continue;
}
var mapping = existingMappings.FirstOrDefault(d => d.SourceId == task.Task.Id);
if (mapping == null)
{
Log.LogWarning("Can't find taskgroup {MissingTaskGroupId} in the target collection.", task.Task.Id);
}
else
{
task.Task.Id = mapping.TargetId;
}
}
}
}

var mappings = await Target.CreateApiDefinitionsAsync(rootSourceDefinitions);

targetDefinitions = await Target.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false);
var rootTargetDefinitions = SortDefinitionsByVersion(targetDefinitions).First();
await Target.UpdateTaskGroupsAsync(rootTargetDefinitions, updatedSourceDefinitions);

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);
Copy link
Collaborator

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

Copy link

Copilot AI Oct 17, 2025

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.

Suggested change
mappings.AddRange(existingMappings);
mappings = mappings.Union(existingMappings).ToList();

Copilot uses AI. Check for mistakes.
return mappings;
}

Expand Down