Skip to content

Commit

Permalink
Add telemitery, checks for field names, and valiation of area paths.
Browse files Browse the repository at this point in the history
  • Loading branch information
MrHinsh committed Jul 12, 2024
1 parent 2da9872 commit be4f259
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
<AssemblyName>WorkItemClone</AssemblyName>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>0.0.0.0</Version>
<FileVersion>0.0.0.0</FileVersion>
<InformationalVersion>0.0.0-local</InformationalVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/nkdAgility/Azure-DevOps-WorkItem-Clone</PackageProjectUrl>
<RepositoryUrl>https://github.com/nkdAgility/Azure-DevOps-WorkItem-Clone</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
Expand Down
138 changes: 111 additions & 27 deletions AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Azure.Pipelines.WebApi;
using Microsoft.VisualStudio.Services.CircuitBreaker;
using Newtonsoft.Json.Linq;
using System.Diagnostics.Eventing.Reader;

namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands
{
Expand All @@ -26,7 +27,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, WorkItemClo
}
CombineValuesFromConfigAndSettings(settingsFromCmd, config);

AnsiConsole.MarkupLine($"[red]Run: [/] {config.RunName}" );
AnsiConsole.MarkupLine($"[red]Run: [/] {config.RunName}");
string runCache = $"{config.CachePath}\\{config.RunName}";
DirectoryInfo outputPathInfo = CreateOutputPath(runCache);

Expand Down Expand Up @@ -100,20 +101,20 @@ await AnsiConsole.Progress()
QueryResults fakeItemsFromTemplateQuery;
fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults("Select [System.Id] From WorkItems Where [System.TeamProject] = '@project' AND [System.Parent] = @id AND [System.ChangedDate] > '@changeddate' order by [System.CreatedDate] desc", new Dictionary<string, string>() { { "@id", config.templateParentId.ToString() }, { "@changeddate", changedDate.ToString("yyyy-MM-dd") } });
if (fakeItemsFromTemplateQuery.workItems.Length == 0)
{
AnsiConsole.WriteLine($"Stage 1: Checked template for changes. None Detected. Loading Cache");

// Load from Cache
task1.Increment(1);
task1.Description = task1.Description + " (cache)";
await Task.Delay(250);
task1.StopTask();
//////////////////////
templateWorkItems = JsonConvert.DeserializeObject<List<WorkItemFull>>(System.IO.File.ReadAllText(cacheTemplateWorkItemsFile));
task2.Increment(templateWorkItems.Count);
task2.Description = task2.Description + " (cache)";
AnsiConsole.WriteLine($"Stage 2: Loaded {templateWorkItems.Count()} work items from cache.");
{
AnsiConsole.WriteLine($"Stage 1: Checked template for changes. None Detected. Loading Cache");

// Load from Cache

task1.Increment(1);
task1.Description = task1.Description + " (cache)";
await Task.Delay(250);
task1.StopTask();
//////////////////////
templateWorkItems = JsonConvert.DeserializeObject<List<WorkItemFull>>(System.IO.File.ReadAllText(cacheTemplateWorkItemsFile));
task2.Increment(templateWorkItems.Count);
task2.Description = task2.Description + " (cache)";
AnsiConsole.WriteLine($"Stage 2: Loaded {templateWorkItems.Count()} work items from cache.");
}
}

Expand All @@ -123,7 +124,7 @@ await AnsiConsole.Progress()
// --------------------------------------------------------------
// Task 1: query for template work items
task1.StartTask();

//AnsiConsole.WriteLine("Stage 1: Executing items from Query");
QueryResults fakeItemsFromTemplateQuery;
fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults("Select [System.Id] From WorkItems Where [System.TeamProject] = '@project' AND [System.Parent] = @id order by [System.CreatedDate] desc", new Dictionary<string, string>() { { "@id", config.templateParentId.ToString() } });
Expand Down Expand Up @@ -196,7 +197,8 @@ await AnsiConsole.Progress()
task5.MaxValue = 1;
task5.Increment(1);
task5.Description = task5.Description + " (run cache)";
} else
}
else
{
// Task 4: First Pass generation of Work Items to build
task4.MaxValue = inputWorkItems.Count();
Expand Down Expand Up @@ -234,14 +236,14 @@ await AnsiConsole.Progress()

//AnsiConsole.WriteLine($"Stage 5: Completed second pass.");




// --------------------------------------------------------------
// Task 6: Create work items in target

task6.MaxValue = buildItems.Count();
int taskCount = 1;
task6.MaxValue = buildItems.Count();
int taskCount = 1;
AnsiConsole.WriteLine($"Processing {buildItems.Count()} items");
task6.Description = $"[bold]Stage 6[/]: Create Work Items (0/{buildItems.Count()})";
task6.StartTask();
Expand All @@ -265,7 +267,7 @@ await AnsiConsole.Progress()
AnsiConsole.WriteLine($"Failed {result.witb.guid}");
break;
}

}
task6.StopTask();
//AnsiConsole.WriteLine($"Stage 6: All Work Items Created.");
Expand All @@ -283,7 +285,7 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildList(JAr
{
WorkItemFull templateWorkItem = null;
int jsonItemTemplateId = 0;
if (int.TryParse(item["id"].Value<string>(),out jsonItemTemplateId))
if (int.TryParse(item["id"].Value<string>(), out jsonItemTemplateId))
{
templateWorkItem = templateWorkItems.Find(x => x.id == jsonItemTemplateId);
}
Expand All @@ -299,7 +301,7 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildList(JAr
// { "System.Tags", string.Join(";" , item.tags, item.area, item.fields.product, templateWorkItem != null? templateWorkItem.fields.SystemTags : "") },
// { "System.AreaPath", string.Join("\\", targetTeamProject, item.area)},
//};
var fields = item["fields"].ToObject<Dictionary<string,string>>();
var fields = item["fields"].ToObject<Dictionary<string, string>>();
foreach (var field in fields)
{
switch (field.Key)
Expand Down Expand Up @@ -354,6 +356,8 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildRelation
}
}



private async IAsyncEnumerable<(WorkItemToBuild, string status, int skipped, int failed, int created)> CreateWorkItemsToBuild(List<WorkItemToBuild> workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi)
{
int skipped = 0;
Expand All @@ -364,10 +368,20 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildRelation
if (item.targetId != 0)
{
skipped++;
yield return (item, "skipped", skipped, failed,created);
} else
yield return (item, "skipped", skipped, failed, created);
}
else
{
WorkItemAdd itemToAdd = CreateWorkItemAddOperation(item, workItemsToBuild, projectItem);

if (!await ValidateOperations(targetApi, item, itemToAdd))
{
AnsiConsole.WriteLine($"[SKIP] {item.guid} As it does not pass field validation. issues listed above..");
skipped++;
yield return (item, "skipped", skipped, failed, created);
continue;
}

WorkItemFull newWorkItem = await targetApi.CreateWorkItem(itemToAdd, item.workItemType);
if (newWorkItem != null)
{
Expand All @@ -381,10 +395,80 @@ private async IAsyncEnumerable<WorkItemToBuild> generateWorkItemsToBuildRelation
failed++;
yield return (item, "failed", skipped, failed, created);
}
}

}

}

}

internal Dictionary<string, bool> foundFields = new Dictionary<string, bool>();
internal Dictionary<string, bool> foundAreaPaths = new Dictionary<string, bool>();
private async Task<bool> ValidateOperations(AzureDevOpsApi targetApi, WorkItemToBuild item, WorkItemAdd itemToAdd)
{
bool valid = true;
foreach (FieldOperation operation in itemToAdd.Operations.FindAll(p => p is FieldOperation))
{
valid = valid & await CheckFieldExists(targetApi, item, valid, operation);
switch (operation.path)
{
case "/fields/System.AreaPath":
valid = valid & await CheckAreaPathExists(targetApi, item, valid, operation);
break;
}
}
return valid;
}

private async Task<bool> CheckAreaPathExists(AzureDevOpsApi targetApi, WorkItemToBuild item, bool valid, FieldOperation operation)
{
if (!foundAreaPaths.ContainsKey(operation.value))
{
NodeClassification node = await targetApi.GetNodeClassification(operation.value.Replace($"{targetApi.Project}\\", ""));
if (node == null)
{
AnsiConsole.WriteLine($"[VALIDATE] {item.guid} has an invalid area path of {operation.value}. This is required to create a work item.");
valid = false;
foundAreaPaths.Add(operation.value, false);
}
else
{
foundAreaPaths.Add(operation.value, true);
}
}
else
{
if (!foundAreaPaths[operation.value])
{
valid = false;
}
}

return valid;
}

private async Task<bool> CheckFieldExists(AzureDevOpsApi targetApi, WorkItemToBuild item, bool valid, FieldOperation operation)
{
string uniqueFieldkey = $"{item.workItemType}{operation.path}";
if (foundFields.ContainsKey(uniqueFieldkey))
{
valid = valid && foundFields[uniqueFieldkey];
}
else
{
FieldItem field = await targetApi.GetFieldOnWorkItem(item.workItemType, operation.path);
if (field == null)
{
AnsiConsole.WriteLine($"[VALIDATE] Field {operation.path} does not exist on {item.workItemType} This is required to create a work item.");
valid = false;
foundFields.Add(uniqueFieldkey, false);
}
else
{
foundFields.Add(uniqueFieldkey, true);
}
}

return valid;
}

private WorkItemAdd CreateWorkItemAddOperation(WorkItemToBuild item, List<WorkItemToBuild> workItemsToBuild, WorkItemFull projectItem)
Expand Down
30 changes: 15 additions & 15 deletions AzureDevOps.WorkItemClone.ConsoleUI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@
using AzureDevOps.WorkItemClone.ConsoleUI.Commands;
using System.Text;
using System.Threading.Tasks;
using AzureDevOps.WorkItemClone;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace ABB.WorkItemClone.ConsoleUI
{
internal class Program
{


static async Task<int> Main(string[] args)
{
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
var runningVersion = GetRunningVersion();

Telemetry.Initialize(runningVersion.versionString);
AnsiConsole.Write(new FigletText("Azure DevOps").LeftJustified().Color(Color.Red));
AnsiConsole.Write(new FigletText("Work Item Clone").LeftJustified().Color(Color.Red));
AnsiConsole.MarkupLine($"[bold white]Azure DevOps Work Item Clone[/] [bold yellow]{GetVersionTextForLog()}[/]");
AnsiConsole.MarkupLine($"[bold white]Azure DevOps Work Item Clone[/] [bold yellow]{runningVersion.versionString}[/]");

var app = new CommandApp();
app.Configure(config =>
Expand All @@ -37,28 +44,21 @@ static async Task<int> Main(string[] args)
}
catch (Exception ex)
{
Telemetry.TrackException(ex);
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
return -99;
}
Console.WriteLine("finished");
}

public static string GetVersionTextForLog()
{
Version runningVersion = GetRunningVersion();
string debugVersion = (string.IsNullOrEmpty(ThisAssembly.Git.BaseTag) ? "v" + runningVersion + "-local" : ThisAssembly.Git.BaseTag + "-" + ThisAssembly.Git.Commits + "-local");
string textVersion = ((runningVersion.Major > 0) ? "v" + runningVersion : debugVersion);
return textVersion;
}

public static Version GetRunningVersion()
public static (Version version, string PreReleaseLabel, string versionString) GetRunningVersion()
{
Version assver = Assembly.GetEntryAssembly()?.GetName().Version;
if (assver == null)
{
return new Version("0.0.0");
}
return new Version(assver.Major, assver.Minor, assver.Build);
FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()?.Location);
var matches = Regex.Matches(myFileVersionInfo.ProductVersion, @"^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<build>0|[1-9]\d*)(?:-((?<label>:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<fullEnd>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$");
Version version = new Version(myFileVersionInfo.FileVersion);
string textVersion = "v" + version.Major + "." + version.Minor + "." + version.Build + "-" + matches[0].Groups[1].Value;
return (version, matches[0].Groups[1].Value, textVersion);
}
}
}
2 changes: 2 additions & 0 deletions AzureDevOps.WorkItemClone/AzureDevOps.WorkItemClone.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Elmah.Io.Client" Version="5.1.76" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
</ItemGroup>

</Project>
Loading

0 comments on commit be4f259

Please sign in to comment.