diff --git a/AzureDevOps-workitem-clone.sln b/AzureDevOps-workitem-clone.sln index 96eed66..c7ede15 100644 --- a/AzureDevOps-workitem-clone.sln +++ b/AzureDevOps-workitem-clone.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution TestData\ADO_TESTProjPipline_V03.json = TestData\ADO_TESTProjPipline_V03.json GitVersion.yml = GitVersion.yml .github\workflows\main.yml = .github\workflows\main.yml + README.md = README.md EndProjectSection EndProject Global diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs index 3132ae3..80a2a73 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Diagnostics.Eventing.Reader; using AzureDevOps.WorkItemClone.Repositories; +using System.Text.RegularExpressions; namespace AzureDevOps.WorkItemClone.ConsoleUI.Commands { @@ -267,31 +268,80 @@ private async IAsyncEnumerable generateWorkItemsToBuildList(JAr newItem.templateId = jsonItemTemplateId; newItem.workItemType = templateWorkItem != null ? templateWorkItem.fields["System.WorkItemType"].ToString() : "Deliverable"; newItem.fields = new Dictionary(); - var fields = controlItem["fields"].ToObject>(); - foreach (var field in fields) + var controlFields = controlItem["fields"].ToObject>(); + foreach (var field in controlFields) { - switch (field.Key) + var fieldKey = field.Key; + var fieldValue = field.Value; + fieldValue = ProcessFieldValue(templateWorkItem, controlFields, field, fieldKey, fieldValue); + switch (fieldKey) { case "System.AreaPath": - newItem.fields.Add(field.Key, string.Join("\\", targetTeamProject, field.Value)); + newItem.fields.Add(fieldKey, string.Join("\\", targetTeamProject, field.Value)); break; default: - if (templateWorkItem != null && templateWorkItem.fields.ContainsKey(field.Key) && (field.Value.Contains("${valuefromtemplate}") || field.Value.Contains("${fromtemplate}"))) + newItem.fields.Add(fieldKey, fieldValue); + break; + } + } + newItem.relations = new List() { new WorkItemToBuildRelation() { rel = "System.LinkTypes.Hierarchy-Reverse", targetId = projectItem.id } }; + yield return newItem; + } + } + + private static string ProcessFieldValue(WorkItemFull templateWorkItem, Dictionary? controlFields, KeyValuePair field, string fieldKey, string fieldValue) + { + var fieldMatches = Regex.Matches(field.Value, @"\$\[([^\|\]]+)(?:\|([^\]]+))?\]"); + if (templateWorkItem != null) + { + // Check and replace all template field names + foreach (Match match in fieldMatches) + { + var key = match.Groups[1].Value; + var option = match.Groups[2].Value; + switch (option) + { + case "control": + if (controlFields.ContainsKey(key)) { - /// Add the value from the template - newItem.fields.Add(field.Key, templateWorkItem.fields[field.Key].ToString()); + fieldValue = fieldValue.Replace(match.Value, controlFields[key].ToString()); } - else + break; + case "template": + default: + if (templateWorkItem.fields.ContainsKey(key)) { - /// add value from control file - newItem.fields.Add(field.Key, field.Value); + fieldValue = fieldValue.Replace(match.Value, templateWorkItem.fields[key].ToString()); } break; } } - newItem.relations = new List() { new WorkItemToBuildRelation() { rel = "System.LinkTypes.Hierarchy-Reverse", targetId = projectItem.id } }; - yield return newItem; + // Check and replace old school replace flags + if (templateWorkItem.fields.ContainsKey(fieldKey) && (fieldValue.Contains("${valuefromtemplate}") || fieldValue.Contains("${fromtemplate}"))) + { + fieldValue = field.Value + .Replace("${valuefromtemplate}", templateWorkItem.fields[fieldKey].ToString()) + .Replace("${fromtemplate}", templateWorkItem.fields[fieldKey].ToString()); + } } + else + { + // Check and replace all template field names + foreach (Match match in fieldMatches) + { + var key = match.Groups[1].Value; + if (templateWorkItem.fields.ContainsKey(key)) + { + fieldValue = fieldValue.Replace($"$[{key}]", ""); + } + } + // Ensure we remove the template values + fieldValue = field.Value + .Replace("${valuefromtemplate}", "") + .Replace("${fromtemplate}", ""); + } + + return fieldValue; } private async IAsyncEnumerable generateWorkItemsToBuildRelations(List workItemsToBuild, List templateWorkItems) diff --git a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs index 1bc5a06..3da3011 100644 --- a/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs +++ b/AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs @@ -23,7 +23,7 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings [DefaultValue("./cache")] public string? CachePath { get; set; } //------------------------------------------------ - [CommandOption("--jsonFile|--inputJsonFile")] + [CommandOption("--jsonFile|--inputJsonFile|--controlFile")] public string? inputJsonFile { get; set; } //------------------------------------------------ [Description("The access token for the target location")] diff --git a/README.md b/README.md index 986e70b..5a08db4 100644 --- a/README.md +++ b/README.md @@ -110,14 +110,15 @@ Clones work items from a template project to a target project incorproating a JS } ``` -## Json Input File +## Control File (--inputFile) +The control file consists of a list of work items that you want to create. Each work item has an `id` and a list of `fields`. -The `id` is the ID of the template item. This will be used to specifiy Description, Acceptance Criteria, and dependancy relationsips. I the `id` is not specified a new work item will be created. -The `fields` are the fields that will be used to create the work item. You can use any field ientifyer from Azure DevOps. +- `id` - The `id` is the optional ID of a template item. If there is an ID, and there is a template item, then the tool will load that template and allow you to select values from it to add to the work item that you create. This template item will also be used to load relationships to other work items and if both ends of the relationship are part of the control file it will wire them up as expected. Not specifying an D wil result in a new work item based only on the control file. +- `fields` - These `fields` are the fields that will be used to create the work item. You can use any field `Refname` from the target Azure DevOps work item. -Use the `${fromtemplate}` to specify that the value should be taken from the template. This is used here for the Description and Acceptance Criteria, but can be used to pull data from any field. +example: ```json [ @@ -129,9 +130,7 @@ Use the `${fromtemplate}` to specify that the value should be taken from the tem "System.Title": "Technical specification", "Custom.Product": "CC", "Microsoft.VSTS.Scheduling.Effort": 12, - "Custom.TRA_Milestone": "E0.1", - "System.Description": "${fromtemplate}", - "Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}" + "Custom.TRA_Milestone": "E0.1" } }, { @@ -143,9 +142,54 @@ Use the `${fromtemplate}` to specify that the value should be taken from the tem "Custom.Product": "", "Microsoft.VSTS.Scheduling.Effort": 2, "Custom.TRA_Milestone": "E4.8" - "System.Description": "${fromtemplate}", + } +] +``` + +### Fields Manipulation + +If you wish to pull fields from the template work item you can use the following syntax: + +- `$[System.Description|template]` - this will pull the `System.Description` field from the template work item. Since template is the default you can also use `$[System.Description]`. +- `$[System.Description|control]` - this will pull the `System.Description` field from the control item data. + +This would make the following valid: + + ```json +[ + { + "id": 213928, + "fields": { + "System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1", + "System.Tags": "Customer Document", + "System.Title": "Technical specification [ $[Custom.Product|control] ]", + "Custom.Product": "CC", + "Microsoft.VSTS.Scheduling.Effort": 12, + "Custom.TRA_Milestone": "E0.1", + "System.Description": "$[System.Description] for $[Custom.Product|control]", + "Microsoft.VSTS.Common.AcceptanceCriteria": "$[Microsoft.VSTS.Common.AcceptanceCriteria]" + } + } +] +``` + +You can also specify `${fromtemplate}` or `${valuefromtemplate}` to pull just the value from the template that is the same name as the control field in focus. + + ```json +[ + { + "id": 213928, + "fields": { + "System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1", + "System.Tags": "Customer Document", + "System.Title": "Technical specification", + "Custom.Product": "CC", + "Microsoft.VSTS.Scheduling.Effort": 12, + "Custom.TRA_Milestone": "E0.1", + "System.Description": "The description: ${fromtemplate}", "Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}" } + } ] ``` diff --git a/TestData/tst_jsonj_export_v20-lite.json b/TestData/tst_jsonj_export_v20-lite.json index 9e76a24..6c78d7c 100644 --- a/TestData/tst_jsonj_export_v20-lite.json +++ b/TestData/tst_jsonj_export_v20-lite.json @@ -8,8 +8,22 @@ "Custom.Product": "CC", "Microsoft.VSTS.Scheduling.Effort": 12, "Custom.TRA_Milestone": "E0.1", - "System.Description": "${fromtemplate}", - "Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}" + "System.Description": "here is the descrption: ${fromtemplate}", + "Microsoft.VSTS.Common.AcceptanceCriteria": "${fromtemplate}", + "Custom.CombineDA": "

Description>

$[System.Description]

Acceptance

$[Microsoft.VSTS.Common.AcceptanceCriteria]

Entra

$[Custom.Product|control]" + } + }, + { + "id": 213928, + "fields": { + "System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1", + "System.Tags": "Customer Document", + "System.Title": "Technical specification", + "Custom.Product": "CC", + "Microsoft.VSTS.Scheduling.Effort": 12, + "Custom.TRA_Milestone": "E0.1", + "System.Description": "here is the descrption: $[System.Description] for $[Custom.Product|control]", + "Microsoft.VSTS.Common.AcceptanceCriteria": "$[Microsoft.VSTS.Common.AcceptanceCriteria]" } } ] \ No newline at end of file