Skip to content

Commit

Permalink
Update to do full creation +semver: major
Browse files Browse the repository at this point in the history
  • Loading branch information
MrHinsh committed Jun 29, 2024
1 parent 6bb2eb9 commit 7fd8b57
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 11,454 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class WorkItemToBuild
public int? templateId { get; set; }
public Dictionary<string, string> fields { get; set; }
public List<WorkItemToBuildRelation> relations { get; set; }

public bool hasComplexRelation { get; set; }
public string targetUrl { get; set; }
public int targetId { get; set; }
}
}
351 changes: 284 additions & 67 deletions ABB.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ namespace ABB.WorkItemClone.ConsoleUI.Commands
{
internal class WorkItemCloneCommandSettings : BaseCommandSettings
{
[CommandArgument(0, "[jsonFile]")]
[CommandOption("--NonInteractive")]
public bool NonInteractive { get; set; }
[CommandOption("--ClearCache")]
public bool ClearCache { get; set; }

[CommandOption("--outputPath")]
public string? OutputPath { get; set; }

[CommandOption("--jsonFile")]
public string? JsonFile { get; set; }

[CommandOption("--targetAccessToken")]
public string? targetAccessToken { get; set; }

[CommandOption("-p|--projectId")]
public int? projectId { get; set; }

}
}
153 changes: 129 additions & 24 deletions ABB.WorkItemClone.ConsoleUI/Commands/WorkItemMergeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
using Microsoft.Extensions.Hosting;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.Models.Process;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.TestManagement.TestPlanning.WebApi;
using Newtonsoft.Json;
using Spectre.Console;
using Spectre.Console.Cli;
using System;
using System.Collections.Generic;
using static System.Net.Mime.MediaTypeNames;

namespace ABB.WorkItemClone.ConsoleUI.Commands
{
Expand All @@ -27,7 +30,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, WorkItemMer
List<jsonWorkItem> jsonWorkItems = LoadJsonFile(settings.JsonFile);
var projectId = EnsureProjectIdAskIfMissing(settings.projectId);


// --------------------------------------------------------------
AnsiConsole.Write(
new Table()
.AddColumn(new TableColumn("Setting").Alignment(Justify.Right))
Expand All @@ -52,7 +55,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, WorkItemMer
.AddChoices(true, false)
);
}

// --------------------------------------------------------------
await AnsiConsole.Progress()
.AutoClear(false) // Do not remove the task list when done
.HideCompleted(false) // Hide tasks as they are completed
Expand All @@ -68,78 +71,129 @@ await AnsiConsole.Progress()
{
// Define tasks
var task1 = ctx.AddTask("[bold]Stage 1[/]: Get Template Items", false);

var task2 = ctx.AddTask("[bold]Stage 2[/]: Load Template Items", false);
var task3 = ctx.AddTask("[bold]Stage 3[/]: Get Target Project", false);
var task4 = ctx.AddTask("[bold]Stage 4[/]: Create Work Items (First Pass)", false);
var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Work Item (Second Pass) Relations ", false);

var task4 = ctx.AddTask("[bold]Stage 4[/]: Plan Work Item Creation (First Pass)", false);
var task5 = ctx.AddTask("[bold]Stage 5[/]: Plan Work Item Creation (Second Pass) Relations ", false);
var task6 = ctx.AddTask("[bold]Stage 6[/]: Create Work Items", false);

// --------------------------------------------------------------
// Task 1: query for template work items
task1.StartTask();
task1.MaxValue = 1;
AnsiConsole.WriteLine("Stage 1: Executing items from Query");
var fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults();
AnsiConsole.WriteLine($"Stage 1: Query returned {fakeItemsFromTemplateQuery.workItems.Count()} items id's from the template.");
string cacheQueryFile = $"{settings.OutputPath}\\Step1-TemplateQuery.json";
if (settings.ClearCache)
{
System.IO.File.Delete(cacheQueryFile);
}
QueryResults fakeItemsFromTemplateQuery;
if (System.IO.File.Exists(cacheQueryFile))
{
fakeItemsFromTemplateQuery = JsonConvert.DeserializeObject<QueryResults>(System.IO.File.ReadAllText(cacheQueryFile));
task1.Description = task1.Description + " (from cache)";
AnsiConsole.WriteLine($"Stage 1: Loaded {fakeItemsFromTemplateQuery.workItems.Count()} work items from cache.");
} else
{
fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults();
AnsiConsole.WriteLine($"Stage 1: Query returned {fakeItemsFromTemplateQuery.workItems.Count()} items id's from the template.");
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step1-TemplateQuery.json", JsonConvert.SerializeObject(fakeItemsFromTemplateQuery, Formatting.Indented));
}
task1.Increment(1);
task1.StopTask();



// --------------------------------------------------------------
// Task 2: getting work items and their full data
task2.MaxValue = fakeItemsFromTemplateQuery.workItems.Count();
task2.StartTask();
AnsiConsole.WriteLine($"Stage 2: Starting process of {task2.MaxValue} work items to get their full data ");
List<WorkItemFull> templateWorkItems = new List<WorkItemFull>();
await foreach (var workItem in templateApi.GetWorkItemsFullAsync(fakeItemsFromTemplateQuery.workItems))
string cachetemplateWorkItemsFile = $"{settings.OutputPath}\\Step2-TemplateItems.json";
if (settings.ClearCache)
{
//AnsiConsole.WriteLine($"Stage 2: Processing {workItem.id}:`{workItem.fields.SystemTitle}`");
templateWorkItems.Add(workItem);
task2.Increment(1);
System.IO.File.Delete(cachetemplateWorkItemsFile);
}
List<WorkItemFull> templateWorkItems;
if (System.IO.File.Exists(cachetemplateWorkItemsFile))
{
templateWorkItems = JsonConvert.DeserializeObject<List<WorkItemFull>>(System.IO.File.ReadAllText(cachetemplateWorkItemsFile));
task2.Increment(templateWorkItems.Count);
task2.Description = task2.Description + " (from cache)";
AnsiConsole.WriteLine($"Stage 2: Loaded {templateWorkItems.Count()} work items from cache.");
} else
{
templateWorkItems = new List<WorkItemFull>();
AnsiConsole.WriteLine($"Stage 2: Loading {fakeItemsFromTemplateQuery.workItems.Count()} work items from template.");
await foreach (var workItem in templateApi.GetWorkItemsFullAsync(fakeItemsFromTemplateQuery.workItems))
{
//AnsiConsole.WriteLine($"Stage 2: Processing {workItem.id}:`{workItem.fields.SystemTitle}`");
templateWorkItems.Add(workItem);
task2.Increment(1);
}
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step2-TemplateItems.json", JsonConvert.SerializeObject(templateWorkItems, Formatting.Indented));
}

AnsiConsole.WriteLine($"Stage 2: All {task2.MaxValue} work items loaded");
task2.StopTask();

// --------------------------------------------------------------
// Task 3: Load the Project work item
task3.StartTask();
task3.MaxValue = 1;
AnsiConsole.WriteLine($"Stage 3: Load the Project work item with ID {settings.projectId} from {configSettings.target.Organization} ");
AzureDevOpsApi targetApi = CreateAzureDevOpsConnection(settings.targetAccessToken, configSettings.target.Organization, configSettings.target.Project);
WorkItemFull projectItem = await targetApi.GetWorkItem((int)settings.projectId);
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step3-Project.json", JsonConvert.SerializeObject(projectItem, Formatting.Indented));
AnsiConsole.WriteLine($"Stage 3: Project `{projectItem.fields.SystemTitle}` loaded ");
task3.Increment(1);
task3.StopTask();

// --------------------------------------------------------------
// Task 4: First Pass generation of Work Items to build
task4.MaxValue = jsonWorkItems.Count();
task4.StartTask();
AnsiConsole.WriteLine($"Stage 4: First Pass generation of Work Items to build will merge the provided json work items with the data from the template.");
List<WorkItemToBuild> buildItems = new List<WorkItemToBuild>();
await foreach (WorkItemToBuild witb in generateWorkItemsToBuildList(jsonWorkItems, templateWorkItems, projectItem, configSettings.target.Project))
{
// AnsiConsole.WriteLine($"Stage 4: processing {witb.guid}");
// AnsiConsole.WriteLine($"Stage 4: processing {witb.guid}");
buildItems.Add(witb);
task4.Increment(1);
}
System.IO.File.WriteAllText($"{settings.OutputPath}\\WorkItemsToBuild-norelations.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented));
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step4-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented));
task4.StopTask();
AnsiConsole.WriteLine($"Stage 4: Completed first pass.");

// Task 5:
// --------------------------------------------------------------
// Task 5: Second Pass Add Relations
task5.MaxValue = jsonWorkItems.Count();
AnsiConsole.WriteLine($"Stage 5: Second Pass generate relations.");
task5.StartTask();
await foreach (WorkItemToBuild witb in generateWorkItemsToBuildRelations(buildItems, templateWorkItems))
{
// AnsiConsole.WriteLine($"Stage 5: processing {witb.guid} for output of {witb.relations.Count-1} relations");
//AnsiConsole.WriteLine($"Stage 5: processing {witb.guid} for output of {witb.relations.Count-1} relations");
task5.Increment(1);
}
System.IO.File.WriteAllText($"{settings.OutputPath}\\WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented));
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step5-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented));
task5.StopTask();
AnsiConsole.WriteLine($"Stage 5: Completed second pass.");

// --------------------------------------------------------------
// Task 6: Create work items in target
task6.MaxValue = buildItems.Count();
AnsiConsole.WriteLine($"Stage 6: Create Work Items in Target.");
task6.StartTask();
await foreach (WorkItemToBuild witb in CreateWorkItemsToBuild(buildItems, projectItem, targetApi))
{
//AnsiConsole.WriteLine($"Stage 6: Processing {witb.guid} for output of {witb.relations.Count - 1} relations");
task6.Increment(1);
}
System.IO.File.WriteAllText($"{settings.OutputPath}\\Step6-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented));
task6.StopTask();
AnsiConsole.WriteLine($"Stage 6: All Work Items Created.");


});


AnsiConsole.WriteLine($"Complete...");


////second pass, add relations
//foreach (var item in configWorkItems)
Expand Down Expand Up @@ -179,6 +233,7 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildList(Lis
}
WorkItemToBuild newItem = new WorkItemToBuild();
newItem.guid = Guid.NewGuid();
newItem.hasComplexRelation = false;
newItem.templateId = item.id;
newItem.fields = new Dictionary<string, string>()
{
Expand Down Expand Up @@ -211,16 +266,66 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildRelation
if (workItemToBuildToLinkTo != null)
{
item.relations.Add(new WorkItemToBuildRelation() { rel = relation.rel, templateId = templateIdToLinkTo, targetId = 0, guid = workItemToBuildToLinkTo.guid });
item.hasComplexRelation = true;
} else
{
AnsiConsole.WriteLine($"Relation {relation.rel} to {templateIdToLinkTo} not found in work items to build.");
//AnsiConsole.WriteLine($"Relation {relation.rel} to {templateIdToLinkTo} not found in work items to build.");
}
}
}
yield return item;
}
}

private async IAsyncEnumerable<WorkItemToBuild> CreateWorkItemsToBuild(List<WorkItemToBuild> workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi)
{
foreach (WorkItemToBuild item in workItemsToBuild)
{
if (item.targetId != 0) continue;
WorkItemAdd itemToAdd = CreateWorkItemAddOperation(item, workItemsToBuild, projectItem);
WorkItemFull newWorkItem = await targetApi.CreateWorkItem(itemToAdd, "Dependancy");
if (newWorkItem == null) {
throw new Exception("Failed to create work item");
}
item.targetUrl = newWorkItem.url;
item.targetId = newWorkItem.id;
yield return item;
}

}

private WorkItemAdd CreateWorkItemAddOperation(WorkItemToBuild item, List<WorkItemToBuild> workItemsToBuild, WorkItemFull projectItem)
{
WorkItemAdd itemAdd = new WorkItemAdd();

foreach (var field in item.fields)
{
if (field.Value != null)
{
itemAdd.Operations.Add(new FieldOperation() { op = "add", path = $"/fields/{field.Key}", value = field.Value });
}
}
foreach (var relation in item.relations)
{
if (relation.rel == "System.LinkTypes.Hierarchy-Reverse")
{
itemAdd.Operations.Add(new RelationOperation() { op = "add", path = "/relations/-", value = new RelationValue { rel = relation.rel, url = projectItem.url } });
}
else
{
var targetItem = workItemsToBuild.Find(x => x.guid == relation.guid);
if (targetItem.targetUrl == null)
{
//AnsiConsole.WriteLine($"SKIP: Relation on {item.guid} does not yet exist. This relation should be added from the other side when {relation.guid} is processed.");
}
else
{
itemAdd.Operations.Add(new RelationOperation() { op = "add", path = "/relations/-", value = new RelationValue { rel = relation.rel, url = targetItem.targetUrl } });
}

}
}
return itemAdd;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal class WorkItemMergeCommandSettings : WorkItemExportCommandSettings
{
[CommandOption("--NonInteractive")]
public bool NonInteractive { get; set; }
[CommandOption("--ClearCache")]
public bool ClearCache { get; set; }

[CommandOption("--outputPath")]
public string? OutputPath { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions ABB.WorkItemClone.ConsoleUI/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
},
"Clone": {
"commandName": "Project",
"commandLineArgs": "clone ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --projectId 540 --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya "
"commandLineArgs": "clone --outputPath ..\\..\\..\\..\\TestData --jsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --projectId 540 --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya --targetAccessToken ay5xc2kn7pka35nkx5coiey6fo4twjjc6yvtp5i3xcsmw5fu65ja "
},
"Export": {
"commandName": "Project",
"commandLineArgs": "export ..\\..\\..\\..\\TestData\\WorkItems --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya"
},
"Merge": {
"commandName": "Project",
"commandLineArgs": "merge --outputPath ..\\..\\..\\..\\TestData --jsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --projectId 540 --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya --targetAccessToken ay5xc2kn7pka35nkx5coiey6fo4twjjc6yvtp5i3xcsmw5fu65ja --NonInteractive"
"commandLineArgs": "merge --outputPath ..\\..\\..\\..\\TestData --jsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --projectId 540 --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya --targetAccessToken ay5xc2kn7pka35nkx5coiey6fo4twjjc6yvtp5i3xcsmw5fu65ja --NonInteractive --ClearCache"
}
}
}
Loading

0 comments on commit 7fd8b57

Please sign in to comment.