diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3781bf4..7004e42 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,7 +91,7 @@ jobs: } if ( "${{needs.Setup.outputs.GitVersion_PreReleaseLabel}}" -eq "" ) { echo "Creating a release." - gh release create v${{needs.Setup.outputs.GitVersion_SemVer}} ./AzureDevOpsWorkItemClone-v${{needs.Setup.outputs.GitVersion_SemVer}}-win-x64.zip --generate-notes --discussion-category "General" + gh release create v${{needs.Setup.outputs.GitVersion_SemVer}} ./AzureDevOpsWorkItemClone-v${{needs.Setup.outputs.GitVersion_SemVer}}-win-x64.zip --generate-notes --discussion-category "Announcements" exit 0 } shell: pwsh diff --git a/AzureDevOps-workitem-clone.sln b/AzureDevOps-workitem-clone.sln index cdccff5..96eed66 100644 --- a/AzureDevOps-workitem-clone.sln +++ b/AzureDevOps-workitem-clone.sln @@ -10,6 +10,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F583DC39-F8DC-46A8-80E8-AC0458DEE366}" ProjectSection(SolutionItems) = preProject TestData\ADO_TESTProjPipline_V03.json = TestData\ADO_TESTProjPipline_V03.json + GitVersion.yml = GitVersion.yml + .github\workflows\main.yml = .github\workflows\main.yml EndProjectSection EndProject Global diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs index e389e20..1c875dd 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs @@ -5,6 +5,8 @@ using Spectre.Console; using Spectre.Console.Cli; using System.Linq; +using Microsoft.Azure.Pipelines.WebApi; +using Microsoft.VisualStudio.Services.CircuitBreaker; namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands { @@ -15,7 +17,7 @@ public override async Task ExecuteAsync(CommandContext context, WorkItemClo WorkItemCloneCommandSettings config = null; if (File.Exists(settingsFromCmd.configFile)) { - config = LoadWorkItemCloneCommandSettingsFromFile(settingsFromCmd.configFile); + config = LoadWorkItemCloneCommandSettingsFromFile(settingsFromCmd.configFile); } if (config == null) { @@ -23,9 +25,15 @@ public override async Task ExecuteAsync(CommandContext context, WorkItemClo } CombineValuesFromConfigAndSettings(settingsFromCmd, config); - DirectoryInfo outputPathInfo = CreateOutputPath(config.CachePath); + AnsiConsole.MarkupLine($"[red]Run: [/] {config.RunName}" ); + string runCache = $"{config.CachePath}\\{config.RunName}"; + DirectoryInfo outputPathInfo = CreateOutputPath(runCache); + AzureDevOpsApi templateApi = CreateAzureDevOpsConnection(config.templateAccessToken, config.templateOrganization, config.templateProject); - List jsonWorkItems = LoadJsonFile(config.inputJsonFile); + AzureDevOpsApi targetApi = CreateAzureDevOpsConnection(config.targetAccessToken, config.targetOrganization, config.targetProject); + + List inputWorkItems = DeserializeWorkItemList(config); + // -------------------------------------------------------------- WriteOutSettings(config); @@ -59,58 +67,54 @@ 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[/]: Plan Work Item Creation (First Pass)", false); - var task5 = ctx.AddTask("[bold]Stage 5[/]: Plan Work Item Creation (Second Pass) Relations ", false); + var task4 = ctx.AddTask("[bold]Stage 4[/]: Create Output Plan", false); + var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Output Plan 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"); - string cacheQueryFile = $"{config.CachePath}\\Step1-TemplateQuery.json"; - if (config.ClearCache) - { - System.IO.File.Delete(cacheQueryFile); - } - QueryResults fakeItemsFromTemplateQuery; - if (System.IO.File.Exists(cacheQueryFile)) - { - fakeItemsFromTemplateQuery = JsonConvert.DeserializeObject(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($"{config.CachePath}\\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 "); - string cachetemplateWorkItemsFile = $"{config.CachePath}\\Step2-TemplateItems.json"; - if (config.ClearCache) + string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}.json"; + + if (config.ClearCache && System.IO.File.Exists(cacheTemplateWorkItemsFile)) { - System.IO.File.Delete(cachetemplateWorkItemsFile); + System.IO.File.Delete(cacheTemplateWorkItemsFile); } + + task1.MaxValue = 1; List templateWorkItems; - if (System.IO.File.Exists(cachetemplateWorkItemsFile)) + if (System.IO.File.Exists(cacheTemplateWorkItemsFile)) { - templateWorkItems = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(cachetemplateWorkItemsFile)); + // Load from Cache + task1.StartTask(); + task1.Increment(1); + task1.Description = task1.Description + " (cache)"; + await Task.Delay(250); + task1.StopTask(); + ////////////////////// + templateWorkItems = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(cacheTemplateWorkItemsFile)); task2.Increment(templateWorkItems.Count); - task2.Description = task2.Description + " (from cache)"; + task2.Description = task2.Description + " (cache)"; AnsiConsole.WriteLine($"Stage 2: Loaded {templateWorkItems.Count()} work items from cache."); } else { + // Get From Server + // -------------------------------------------------------------- + // Task 1: query for template work items + task1.StartTask(); + + //AnsiConsole.WriteLine("Stage 1: Executing items from Query"); + QueryResults fakeItemsFromTemplateQuery; + fakeItemsFromTemplateQuery = await templateApi.GetWiqlQueryResults(); + AnsiConsole.WriteLine($"Stage 1: Query returned {fakeItemsFromTemplateQuery.workItems.Count()} items id's from the template."); + task1.Increment(1); + task1.StopTask(); + // -------------------------------------------------------------- + // Task 2: getting work items and their full data + task2.MaxValue = fakeItemsFromTemplateQuery.workItems.Count(); + task2.StartTask(); + await Task.Delay(250); + //AnsiConsole.WriteLine($"Stage 2: Starting process of {task2.MaxValue} work items to get their full data "); templateWorkItems = new List(); //AnsiConsole.WriteLine($"Stage 2: Loading {fakeItemsFromTemplateQuery.workItems.Count()} work items from template."); await foreach (var workItem in templateApi.GetWorkItemsFullAsync(fakeItemsFromTemplateQuery.workItems)) @@ -119,66 +123,128 @@ await AnsiConsole.Progress() templateWorkItems.Add(workItem); task2.Increment(1); } - System.IO.File.WriteAllText($"{config.CachePath}\\Step2-TemplateItems.json", JsonConvert.SerializeObject(templateWorkItems, Formatting.Indented)); + System.IO.File.WriteAllText(cacheTemplateWorkItemsFile, JsonConvert.SerializeObject(templateWorkItems, Formatting.Indented)); + //AnsiConsole.WriteLine($"Stage 2: All {task2.MaxValue} work items loaded"); + await Task.Delay(250); + task2.StopTask(); } + await Task.Delay(250); - //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 {config.targetParentId} from {config.targetOrganization} "); - AzureDevOpsApi targetApi = CreateAzureDevOpsConnection(config.targetAccessToken, config.targetOrganization, config.targetProject); - WorkItemFull projectItem = await targetApi.GetWorkItem((int)config.targetParentId); - System.IO.File.WriteAllText($"{config.CachePath}\\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 buildItems = new List(); - await foreach (WorkItemToBuild witb in generateWorkItemsToBuildList(jsonWorkItems, templateWorkItems, projectItem, config.targetProject)) + string targetProjectRunFile = $"{runCache}\\targetProject.json"; + WorkItemFull projectItem; + if (System.IO.File.Exists(targetProjectRunFile)) { - // AnsiConsole.WriteLine($"Stage 4: processing {witb.guid}"); - buildItems.Add(witb); - task4.Increment(1); + task3.StartTask(); + task3.MaxValue = 1; + // Load from Cache + projectItem = JsonConvert.DeserializeObject(System.IO.File.ReadAllText(targetProjectRunFile)); + task3.Increment(1); + await Task.Delay(250); + task3.StopTask(); + task3.Description = task3.Description + " (run cache)"; + } - System.IO.File.WriteAllText($"{config.CachePath}\\Step4-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented)); - task4.StopTask(); - //AnsiConsole.WriteLine($"Stage 4: Completed first pass."); + else + { + // -------------------------------------------------------------- + // Task 3: Load the Project work item + task3.StartTask(); + await Task.Delay(250); + task3.MaxValue = 1; + projectItem = await targetApi.GetWorkItem((int)config.targetParentId); + System.IO.File.WriteAllText(targetProjectRunFile, JsonConvert.SerializeObject(projectItem, Formatting.Indented)); + //AnsiConsole.WriteLine($"Stage 3: Project `{projectItem.fields.SystemTitle}` loaded "); + task3.Increment(1); + await Task.Delay(250); + task3.StopTask(); + // -------------------------------------------------------------- + } + await Task.Delay(250); // -------------------------------------------------------------- - // 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)) + + string workItemsToBuildRunFile = $"{runCache}\\output.json"; + List buildItems; + if (System.IO.File.Exists(workItemsToBuildRunFile)) { - //AnsiConsole.WriteLine($"Stage 5: processing {witb.guid} for output of {witb.relations.Count-1} relations"); + buildItems = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText(workItemsToBuildRunFile)); + // Update lists + task4.MaxValue = 1; + task4.Increment(1); + task4.Description = task4.Description + " (run cache)"; + task5.MaxValue = 1; task5.Increment(1); + task5.Description = task5.Description + " (run cache)"; + } else + { + // Task 4: First Pass generation of Work Items to build + task4.MaxValue = inputWorkItems.Count(); + task4.StartTask(); + await Task.Delay(250); + //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."); + buildItems = new List(); + await foreach (WorkItemToBuild witb in generateWorkItemsToBuildList(inputWorkItems, templateWorkItems, projectItem, config.targetProject)) + { + // AnsiConsole.WriteLine($"Stage 4: processing {witb.guid}"); + buildItems.Add(witb); + task4.Increment(1); + } + await Task.Delay(250); + task4.StopTask(); + //AnsiConsole.WriteLine($"Stage 4: Completed first pass."); + // -------------------------------------------------------------- + // Task 5: Second Pass Add Relations + task5.MaxValue = inputWorkItems.Count(); + //AnsiConsole.WriteLine($"Stage 5: Second Pass generate relations."); + task5.StartTask(); + await Task.Delay(250); + await foreach (WorkItemToBuild witb in generateWorkItemsToBuildRelations(buildItems, templateWorkItems)) + { + //AnsiConsole.WriteLine($"Stage 5: processing {witb.guid} for output of {witb.relations.Count-1} relations"); + task5.Increment(1); + } + System.IO.File.WriteAllText(workItemsToBuildRunFile, JsonConvert.SerializeObject(buildItems, Formatting.Indented)); + await Task.Delay(250); + task5.StopTask(); } - System.IO.File.WriteAllText($"{config.CachePath}\\Step5-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented)); - task5.StopTask(); + await Task.Delay(250); + // -------------------------------------------------------------- + + //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.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(); - await foreach (WorkItemToBuild witb in CreateWorkItemsToBuild(buildItems, projectItem, targetApi)) + await foreach ((WorkItemToBuild witb, string status, int skipped, int failed, int created) result in CreateWorkItemsToBuild(buildItems, projectItem, targetApi)) { //AnsiConsole.WriteLine($"Stage 6: Processing {witb.guid} for output of {witb.relations.Count - 1} relations"); task6.Increment(1); + taskCount++; + task6.Description = $"[bold]Stage 6[/]: Create Work Items ({taskCount}/{buildItems.Count()} c:{result.created}, s:{result.skipped}, f:{result.failed})"; + switch (result.status) + { + case "created": + //AnsiConsole.WriteLine($"Created {result.witb.guid}"); + System.IO.File.WriteAllText(workItemsToBuildRunFile, JsonConvert.SerializeObject(buildItems, Formatting.Indented)); + break; + case "skipped": + //AnsiConsole.WriteLine($"Skipped {result.witb.guid}"); + await Task.Delay(50); + break; + case "failed": + AnsiConsole.WriteLine($"Failed {result.witb.guid}"); + break; + } + } - System.IO.File.WriteAllText($"{config.CachePath}\\Step6-WorkItemsToBuild.json", JsonConvert.SerializeObject(buildItems, Formatting.Indented)); task6.StopTask(); //AnsiConsole.WriteLine($"Stage 6: All Work Items Created."); - - }); @@ -243,20 +309,35 @@ private async IAsyncEnumerable generateWorkItemsToBuildRelation } } - private async IAsyncEnumerable CreateWorkItemsToBuild(List workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi) + private async IAsyncEnumerable<(WorkItemToBuild, string status, int skipped, int failed, int created)> CreateWorkItemsToBuild(List workItemsToBuild, WorkItemFull projectItem, AzureDevOpsApi targetApi) { + int skipped = 0; + int failed = 0; + int created = 0; 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) + if (item.targetId != 0) { - throw new Exception("Failed to create work item"); - } - item.targetUrl = newWorkItem.url; - item.targetId = newWorkItem.id; - yield return item; + skipped++; + yield return (item, "skipped", skipped, failed,created); + } else + { + WorkItemAdd itemToAdd = CreateWorkItemAddOperation(item, workItemsToBuild, projectItem); + WorkItemFull newWorkItem = await targetApi.CreateWorkItem(itemToAdd, "Dependancy"); + if (newWorkItem != null) + { + created++; + item.targetUrl = newWorkItem.url; + item.targetId = newWorkItem.id; + yield return (item, "created", skipped, failed, created); + } + else + { + failed++; + yield return (item, "failed", skipped, failed, created); + } + } + } } diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs index 6f16a5b..dace613 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs @@ -14,6 +14,10 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings [CommandOption("--ClearCache")] [JsonIgnore] public bool ClearCache { get; set; } + [Description("Use this run name to execute. This will create a unique folder under the CachePath for storing run specific data and status. Defaults to yyyyyMMddHHmmss.")] + [CommandOption("--RunName")] + [JsonIgnore] + public string? RunName { get; set; } //------------------------------------------------ [CommandOption("--outputPath|--cachePath")] [DefaultValue("./cache")] diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs index 13e45a4..b16e363 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs @@ -18,6 +18,7 @@ internal abstract class WorkItemCommandBase : AsyncCommand internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings settings, WorkItemCloneCommandSettings config) { + config.RunName = settings.RunName != null ? settings.RunName : DateTime.Now.ToString("yyyyyMMddHHmmss"); config.configFile = EnsureConfigFileAskIfMissing(config.configFile = settings.configFile != null ? settings.configFile : config.configFile); config.inputJsonFile = EnsureJsonFileAskIfMissing(config.inputJsonFile = settings.inputJsonFile != null ? settings.inputJsonFile : config.inputJsonFile); config.CachePath = EnsureCachePathAskIfMissing(config.CachePath = settings.CachePath != null ? settings.CachePath : config.CachePath); @@ -29,7 +30,7 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se config.targetOrganization = EnsureOrganizationAskIfMissing(config.targetOrganization = settings.targetOrganization != null ? settings.targetOrganization : config.targetOrganization); config.targetProject = EnsureProjectAskIfMissing(config.targetProject = settings.targetProject != null ? settings.targetProject : config.targetProject, config.targetOrganization); config.targetAccessToken = EnsureAccessTokenAskIfMissing(settings.targetAccessToken != null ? settings.targetAccessToken : config.targetAccessToken, config.targetOrganization); - config.targetParentId = EnsureParentIdAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId); + config.targetParentId = EnsureParentIdAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId); } internal int EnsureParentIdAskIfMissing(int? parentId) { @@ -45,14 +46,8 @@ internal int EnsureParentIdAskIfMissing(int? parentId) return parentId.Value; } - internal List LoadJsonFile(string? jsonFile) + private List DeserializeWorkItemList(string jsonFile) { - jsonFile = EnsureJsonFileAskIfMissing(jsonFile); - if (!System.IO.File.Exists(jsonFile)) - { - AnsiConsole.MarkupLine("[red]Error:[/] No JSON file was found."); - throw new Exception(jsonFile + " not found."); - } List configWorkItems; try { @@ -72,6 +67,31 @@ internal List LoadJsonFile(string? jsonFile) return configWorkItems; } + internal List DeserializeWorkItemList(WorkItemCloneCommandSettings config) + { + string CachedRunJson = System.IO.Path.Combine(config.CachePath, config.RunName, "input.json"); + if (System.IO.File.Exists(CachedRunJson)) + { + // Load From Run Cache + config.inputJsonFile = CachedRunJson; + return DeserializeWorkItemList(CachedRunJson); + } else + { + // Load new + config.inputJsonFile = EnsureJsonFileAskIfMissing(config.inputJsonFile); + if (!System.IO.File.Exists(config.inputJsonFile)) + { + AnsiConsole.MarkupLine("[red]Error:[/] No JSON file was found."); + throw new Exception(config.inputJsonFile + " not found."); + } + + List inputWorkItems; + inputWorkItems= DeserializeWorkItemList(config.inputJsonFile); + System.IO.File.WriteAllText(CachedRunJson, JsonConvert.SerializeObject(inputWorkItems, Formatting.Indented)); + return inputWorkItems; + } + } + internal string EnsureJsonFileAskIfMissing(string? jsonFile) { if (jsonFile == null) @@ -203,6 +223,7 @@ internal void WriteOutSettings(WorkItemCloneCommandSettings config) .AddColumn(new TableColumn("Value")) .AddEmptyRow() .AddRow("configFile", config.configFile != null ? config.configFile : "NOT SET") + .AddRow("runName", config.RunName != null ? config.RunName : "NOT SET") .AddRow("CachePath", config.CachePath != null ? config.CachePath : "NOT SET") .AddRow("inputJsonFile", config.inputJsonFile != null ? config.inputJsonFile : "NOT SET") .AddEmptyRow() diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json index 2c719fa..92178e9 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Properties/launchSettings.json @@ -6,7 +6,7 @@ }, "Clone": { "commandName": "Project", - "commandLineArgs": "clone --jsonFile ..\\..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --templateAccessToken tqvemdfaucnfhr7fwr2f5ebunncgfxeps5owjmsriu6e3uti7dya --targetAccessToken ay5xc2kn7pka35nkx5coiey6fo4twjjc6yvtp5i3xcsmw5fu65ja " + "commandLineArgs": "clone --RunName Run1 --jsonFile ..\\..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json " }, "empty": { "commandName": "Project" diff --git a/AzureDevOps.WorkItemClone/DataContracts/WorkItemAdd.cs b/AzureDevOps.WorkItemClone/DataContracts/WorkItemAdd.cs index dfa9f69..d37899f 100644 --- a/AzureDevOps.WorkItemClone/DataContracts/WorkItemAdd.cs +++ b/AzureDevOps.WorkItemClone/DataContracts/WorkItemAdd.cs @@ -10,8 +10,6 @@ namespace AzureDevOps.WorkItemClone.DataContracts public class WorkItemAdd { public List Operations { get; set; } = new List(); - public WorkItemFull? ItemFromtemplate { get; set; } - public jsonWorkItem ItemFromConfig { get; set; } } public abstract class Operation diff --git a/README.md b/README.md index 928a92b..9c0c5c0 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Clones work items from a template project to a target project incorproating a JS - `--config` - The path to the configuration file. Default is `.\configuration.json`. This can be used to had code paramiters... Command line params overrides the configuration file. - `--CachePath` - Folder used to cache data like the template. Default is `.\cache`. - `--inputJsonFile` - The path to the JSON file that instructs the creation of the work items + - `--runname` - The name of the run. Default is the current DateTime. Use this to rerun a creation that failed or was interupted. *Template Parameters* - The template contains the Descrition, Acceptance Criteria and relationship to other work itsm that we will clone. @@ -45,6 +46,22 @@ Clones work items from a template project to a target project incorproating a JS clone --inputJsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --targetParentId 540 --templateAccessToken tqvemdfaucsriu6e3uti7dya --targetAccessToken ay5xc2kn5i3xcsmw5fu65ja ``` + #### Runs + + The consept of runs is to allow users to restart a failed or interupted run. The run name is used to identify the run and the cache is used to store the state of the run. + + The example below will create a subfolder to the cache called `Bob` where it will store the state of the run. If the run fails or is interupted you can restart the run by using the same run name. Rerunning the same run will not create duplicate work items and will not rebuild the output file that is generated in steps 4 and 5. It will reuse the existing one. If you need to change the input file then you will need to create a new run. + + When using the `--runname` parameter the `--inputJsonFile ` will not be used if a cache exists for the run. The input file will be read from the cache. + + *Typical usage*: + + ```powershell + clone --runname Bob --inputJsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --targetParentId 540 --templateAccessToken tqvemdfaucsriu6e3uti7dya --targetAccessToken ay5xc2kn5i3xcsmw5fu65ja + ``` + + + ### `init` Leads you through the process of creating a configuration file. diff --git a/docs/Home.md b/docs/Home.md index 928a92b..9c0c5c0 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -20,6 +20,7 @@ Clones work items from a template project to a target project incorproating a JS - `--config` - The path to the configuration file. Default is `.\configuration.json`. This can be used to had code paramiters... Command line params overrides the configuration file. - `--CachePath` - Folder used to cache data like the template. Default is `.\cache`. - `--inputJsonFile` - The path to the JSON file that instructs the creation of the work items + - `--runname` - The name of the run. Default is the current DateTime. Use this to rerun a creation that failed or was interupted. *Template Parameters* - The template contains the Descrition, Acceptance Criteria and relationship to other work itsm that we will clone. @@ -45,6 +46,22 @@ Clones work items from a template project to a target project incorproating a JS clone --inputJsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --targetParentId 540 --templateAccessToken tqvemdfaucsriu6e3uti7dya --targetAccessToken ay5xc2kn5i3xcsmw5fu65ja ``` + #### Runs + + The consept of runs is to allow users to restart a failed or interupted run. The run name is used to identify the run and the cache is used to store the state of the run. + + The example below will create a subfolder to the cache called `Bob` where it will store the state of the run. If the run fails or is interupted you can restart the run by using the same run name. Rerunning the same run will not create duplicate work items and will not rebuild the output file that is generated in steps 4 and 5. It will reuse the existing one. If you need to change the input file then you will need to create a new run. + + When using the `--runname` parameter the `--inputJsonFile ` will not be used if a cache exists for the run. The input file will be read from the cache. + + *Typical usage*: + + ```powershell + clone --runname Bob --inputJsonFile ..\\..\\..\\..\\TestData\\ADO_TESTProjPipline_V03.json --targetParentId 540 --templateAccessToken tqvemdfaucsriu6e3uti7dya --targetAccessToken ay5xc2kn5i3xcsmw5fu65ja + ``` + + + ### `init` Leads you through the process of creating a configuration file.