Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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 @@ -202,6 +203,7 @@ private IEnumerable<TaskGroup> FilterOutExistingTaskGroups(IEnumerable<TaskGroup
/// <param name="availableTasks"></param>
/// <returns>List of filtered Definitions</returns>
private IEnumerable<TaskGroup> FilterOutIncompatibleTaskGroups(IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<TaskDefinition> availableTasks)
private IEnumerable<TaskGroup> FilterOutIncompatibleTaskGroups(IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<TaskDefinition> availableTasks, IEnumerable<Mapping> taskGroupMappings)
{
var objectsToMigrate = filteredTaskGroups.Where(g =>
{
Expand All @@ -212,6 +214,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 @@ -312,6 +323,7 @@ private async Task<IEnumerable<Mapping>> CreateBuildPipelinesAsync(IEnumerable<M
foreach (var step in phase.Steps)
{
if (step.Task.DefinitionType.ToLower() != "metaTask".ToLower())
if (step.Task.DefinitionType?.ToLower() != "metaTask".ToLower())
{
continue;
}
Expand Down Expand Up @@ -528,6 +540,7 @@ private void UpdateTaskGroupId(ReleaseDefinition definitionToBeMigrated, IEnumer
foreach (var WorkflowTask in deployPhase.WorkflowTasks)
{
if (WorkflowTask.DefinitionType != null && WorkflowTask.DefinitionType.ToLower() != "metaTask".ToLower())
if (WorkflowTask.DefinitionType?.ToLower() != "metaTask".ToLower())
{
continue;
}
Expand Down Expand Up @@ -616,6 +629,7 @@ private async Task<IEnumerable<Mapping>> CreateServiceConnectionsAsync()
}

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

Expand All @@ -625,9 +639,88 @@ private async Task<IEnumerable<Mapping>> CreateTaskGroupDefinitionsAsync()
var filteredTaskGroups = FilterOutExistingTaskGroups(sourceDefinitions, targetDefinitions);
filteredTaskGroups = FilterOutIncompatibleTaskGroups(filteredTaskGroups, availableTasks).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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good idea, but this method has only one reference and no data at this time.


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

Log.LogInformation($"Phase 2 - Nested Taskgroups");
var nestedTaskGroups = filteredTaskGroups.Where(g => g.Tasks.Any(t => t.Task.DefinitionType.ToLower() == "metaTask".ToLower())).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 => t.Task.DefinitionType.ToLower() == "metaTask".ToLower()).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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method should only return newly generatd task groups?

} while (nestedTaskGroups.Any() && taskGroupsToMigrate.Any());

return existingMappings;
}

private async Task<IEnumerable<Mapping>> CreateTaskGroupsAsync(IEnumerable<Mapping> serviceConnectionMappings, IEnumerable<TaskGroup> targetDefinitions, IEnumerable<TaskDefinition> availableTasks, IEnumerable<TaskGroup> filteredTaskGroups, IEnumerable<Mapping> existingMappings)
{
filteredTaskGroups = FilterOutIncompatibleTaskGroups(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);
}
}
}

if (existingMappings is not null)
{
foreach (var task in definitionToBeMigrated.Tasks)
{
if (task.Task.DefinitionType.ToLower() != "metaTask".ToLower())
{
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);
Expand All @@ -636,6 +729,7 @@ private async Task<IEnumerable<Mapping>> CreateTaskGroupDefinitionsAsync()

targetDefinitions = await Target.GetApiDefinitionsAsync<TaskGroup>(queryForDetails: false);
mappings.AddRange(FindExistingMappings(sourceDefinitions, 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
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mappings contain only new created task groups, for a complete list all data from Azure DevOps must be request or we can add the already existing mappings.

return mappings;
}

Expand Down