Skip to content

Commit 3239615

Browse files
authored
Create a Query for the Run at the end. (#51)
Added the generation of a query for the work items that were just created. To do this we added the following additional parameters: - `--targetQuery` - The query to create in the target project. Default is `SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID`. - `--targetQueryTitle` - The title of the query to create in the target project. Default is `Project-@RunName - @projectTitle`. - `--targetQueryFolder` - The folder to create the query in the target project. Default is `Shared Queries`.
2 parents 097cafb + e2c3e78 commit 3239615

7 files changed

Lines changed: 222 additions & 54 deletions

File tree

AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommand.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Azure.Pipelines.WebApi;
99
using Microsoft.VisualStudio.Services.CircuitBreaker;
1010
using Newtonsoft.Json.Linq;
11+
using System.Threading.Tasks;
1112
using System.Diagnostics.Eventing.Reader;
1213
using AzureDevOps.WorkItemClone.Repositories;
1314

@@ -75,6 +76,7 @@ await AnsiConsole.Progress()
7576
var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Output Plan Relations ", false);
7677
//var task51 = ctx.AddTask("[bold]Stage 5.1[/]: Validate Data ", false);
7778
var task6 = ctx.AddTask("[bold]Stage 6[/]: Create Work Items", false);
79+
var task7 = ctx.AddTask("[bold]Stage 7[/]: Create Query", false);
7880

7981
string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}-{config.templateParentId}.json";
8082

@@ -218,8 +220,30 @@ await AnsiConsole.Progress()
218220
}
219221
task6.StopTask();
220222
//AnsiConsole.WriteLine($"Stage 6: All Work Items Created.");
223+
224+
225+
// --------------------------------------------------------------
226+
// Task 7: Create Query
227+
task7.MaxValue = 1;
228+
task7.StartTask();
229+
230+
Dictionary<string, string> queryParameters = new Dictionary<string, string>()
231+
{
232+
{ "@projectID", projectItem.id.ToString() },
233+
{ "@projectTitle", projectItem.fields.SystemTitle },
234+
{ "@projectTags", projectItem.fields.SystemTags },
235+
{ "@RunName", config.RunName }
236+
};
237+
var query = await targetApi.CreateProjectQuery(config.targetQueryTitle, config.targetQuery, queryParameters);
238+
task7.Increment(1);
239+
task7.StopTask();
240+
241+
242+
221243
});
222244

245+
246+
223247

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

AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCloneCommandSettings.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings
4242
[CommandOption("--targetFalbackWit")]
4343
[DefaultValue("Deliverable")]
4444
public string? targetFalbackWit { get; set; }
45+
46+
47+
[Description("The WIQL Query to use. You can use @projectID, @projectTitle, @projectTags to replace data from the project!")]
48+
[CommandOption("--targetQuery")]
49+
[DefaultValue("SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID")]
50+
public string? targetQuery { get; set; }
51+
52+
[Description("The title to use for the query. You can use @projectID, @projectTitle, @projectTags, @RunName to replace data from the project!")]
53+
[CommandOption("--targetQueryTitle")]
54+
[DefaultValue("Project-@RunName - @projectTitle")]
55+
public string? targetQueryTitle { get; set; }
56+
57+
[Description("Must already Exist and be in the form 'Shared Queries/Folder1/Folder2'!")]
58+
[CommandOption("--targetQueryFolder")]
59+
[DefaultValue("Shared Queries")]
60+
public string? targetQueryFolder { get; set; }
61+
4562
//------------------------------------------------
4663
[Description("The access token for the template location")]
4764
[CommandOption("--templateAccessToken")]

AzureDevOps.WorkItemClone.ConsoleUI/Commands/WorkItemCommandBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se
3737
config.targetParentId = EnsureIntAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId, "Provide the target parent?");
3838
config.targetFalbackWit = EnsureStringAskIfMissing(config.targetFalbackWit = settings.targetFalbackWit != null ? settings.targetFalbackWit : config.targetFalbackWit, "Provide the target fallback wit?");
3939

40+
config.targetQueryTitle = EnsureStringAskIfMissing(config.targetQueryTitle = settings.targetQueryTitle != null ? settings.targetQueryTitle : config.targetQueryTitle, "Provide the target query title?");
41+
config.targetQueryFolder = EnsureStringAskIfMissing(config.targetQueryFolder = settings.targetQueryFolder != null ? settings.targetQueryFolder : config.targetQueryFolder, "Provide the target query folder?");
42+
config.targetQuery = EnsureStringAskIfMissing(config.targetQuery = settings.targetQuery != null ? settings.targetQuery : config.targetQuery, "Provide the target WIQL query?");
4043
}
4144

4245

AzureDevOps.WorkItemClone.ConsoleUI/configuration.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
"templateAccessToken": null,
99
"templateOrganization": "ABB-MO-ATE",
1010
"templateProject": "ABB Traction Template",
11-
"templateParentId": 212315
11+
"templateParentId": 212315,
12+
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
13+
"targetQueryTitle": "Project-@RunName - @projectTitle",
14+
"targetQueryFolder": "Shared Queries"
15+
1216
}

AzureDevOps.WorkItemClone/AzureDevOpsApi.cs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ public async IAsyncEnumerable<WorkItemFull> GetWorkItemsFullAsync(Workitem[] ite
5151
}
5252

5353
public async Task<QueryResults?> GetWiqlQueryResults(string wiqlQuery, Dictionary<string, string> parameters)
54+
{
55+
wiqlQuery = GetQueryString(wiqlQuery, parameters);
56+
string post = JsonConvert.SerializeObject(new
57+
{
58+
query = wiqlQuery
59+
});
60+
string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2";
61+
var result = await GetObjectResult<QueryResults>(apiCallUrl, post);
62+
return result.result;
63+
}
64+
65+
private string GetQueryString( string wiqlQuery, Dictionary<string, string> parameters)
5466
{
5567
if (parameters == null)
5668
{
@@ -64,19 +76,23 @@ public async IAsyncEnumerable<WorkItemFull> GetWorkItemsFullAsync(Workitem[] ite
6476
{
6577
wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems Where [System.TeamProject] = '@project' order by [System.CreatedDate] desc";
6678
}
67-
foreach (var param in parameters)
79+
wiqlQuery = ReplaceParamsInString(wiqlQuery, parameters);
80+
return wiqlQuery;
81+
}
82+
private string ReplaceParamsInString(string text, Dictionary<string, string> parameters)
83+
{
84+
if (string.IsNullOrEmpty(text))
6885
{
69-
wiqlQuery = wiqlQuery.Replace(param.Key, param.Value);
86+
text = "Default";
7087
}
71-
string post = JsonConvert.SerializeObject(new
88+
foreach (var param in parameters)
7289
{
73-
query = wiqlQuery
74-
});
75-
string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2";
76-
var result = await GetObjectResult<QueryResults>(apiCallUrl, post);
77-
return result.result;
90+
text = text.Replace(param.Key, param.Value);
91+
}
92+
return text;
7893
}
7994

95+
8096
public async Task<QueryResults?> GetWiqlQueryResults()
8197
{
8298
string post = JsonConvert.SerializeObject(new {
@@ -230,6 +246,23 @@ public ValueTask DisposeAsync()
230246
{
231247
return new(Task.Delay(TimeSpan.FromSeconds(1)));
232248
}
249+
250+
public async Task<Query> CreateProjectQuery(string queryName, string wiqlQuery, Dictionary<string, string> parameters)
251+
{
252+
///POST https://dev.azure.com/{organization}/{project}/_apis/wit/queries/{query}?api-version=7.1-preview.2
253+
wiqlQuery = GetQueryString(wiqlQuery, parameters);
254+
queryName = ReplaceParamsInString(queryName, parameters);
255+
string post = JsonConvert.SerializeObject(new
256+
{
257+
isFolder = false,
258+
name = queryName,
259+
path = $"Shared Queries/{queryName}",
260+
wiql = wiqlQuery
261+
});
262+
string apiCallUrl = $"https://dev.azure.com/{_account}/{_project}/_apis/wit/queries/Shared Queries/?api-version=7.2-preview.2";
263+
var result = await GetObjectResult<Query>(apiCallUrl, post);
264+
return result.result;
265+
}
233266
}
234267

235268
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace AzureDevOps.WorkItemClone.DataContracts
8+
{
9+
10+
public class Query
11+
{
12+
public string id { get; set; }
13+
public string name { get; set; }
14+
public string path { get; set; }
15+
public QueryCreatedby createdBy { get; set; }
16+
public DateTime createdDate { get; set; }
17+
public QueryLastmodifiedby lastModifiedBy { get; set; }
18+
public DateTime lastModifiedDate { get; set; }
19+
public bool isFolder { get; set; }
20+
public bool hasChildren { get; set; }
21+
public bool isPublic { get; set; }
22+
public QueryLinks2 _links { get; set; }
23+
public string url { get; set; }
24+
}
25+
26+
public class QueryCreatedby
27+
{
28+
public string displayName { get; set; }
29+
public string url { get; set; }
30+
public QueryLinks _links { get; set; }
31+
public string id { get; set; }
32+
public string uniqueName { get; set; }
33+
public string imageUrl { get; set; }
34+
public string descriptor { get; set; }
35+
}
36+
37+
public class QueryLinks
38+
{
39+
public Avatar avatar { get; set; }
40+
}
41+
42+
public class QueryAvatar
43+
{
44+
public string href { get; set; }
45+
}
46+
47+
public class QueryLastmodifiedby
48+
{
49+
public string displayName { get; set; }
50+
public string url { get; set; }
51+
public QueryLinks1 _links { get; set; }
52+
public string id { get; set; }
53+
public string uniqueName { get; set; }
54+
public string imageUrl { get; set; }
55+
public string descriptor { get; set; }
56+
}
57+
58+
public class QueryLinks1
59+
{
60+
public QueryAvatar1 avatar { get; set; }
61+
}
62+
63+
public class QueryAvatar1
64+
{
65+
public string href { get; set; }
66+
}
67+
68+
public class QueryLinks2
69+
{
70+
public QuerySelf self { get; set; }
71+
public QueryHtml html { get; set; }
72+
public QueryParent parent { get; set; }
73+
}
74+
75+
public class QuerySelf
76+
{
77+
public string href { get; set; }
78+
}
79+
80+
public class QueryHtml
81+
{
82+
public string href { get; set; }
83+
}
84+
85+
public class QueryParent
86+
{
87+
public string href { get; set; }
88+
}
89+
90+
}

README.md

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ Clones work items from a template project to a target project incorproating a JS
3535
- `--targetProject` - The name of the prject to clone work items to.
3636
- `--targetParentId` - All cloned work items will be come a child of this work item
3737

38+
*Target Query* - The target query is used to create a query in the target project to show the cloned work items.
39+
40+
- `--targetQuery` - The query to create in the target project. Default is `SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID`.
41+
- `--targetQueryTitle` - The title of the query to create in the target project. Default is `Project-@RunName - @projectTitle`.
42+
- `--targetQueryFolder` - The folder to create the query in the target project. Default is `Shared Queries`.
43+
44+
You can use the following parameters:
45+
46+
- *@projectID* - The ID of the target parent item
47+
- *@projectTitle* - The title of the parent item
48+
- *@projectTags* - the tags of the item
49+
- *@RunName* - The name of the run
50+
3851
*Optional Parameters* - These are optional parameters that can be used to control the behaviour of the clone process.
3952

4053
- `--NonInteractive` - Disables interactive mode. Default is `false`.
@@ -61,7 +74,6 @@ Clones work items from a template project to a target project incorproating a JS
6174
```
6275

6376

64-
6577
### `init`
6678

6779
Leads you through the process of creating a configuration file.
@@ -83,66 +95,51 @@ Clones work items from a template project to a target project incorproating a JS
8395
```json
8496
{
8597
"CachePath": "./cache",
86-
"inputJsonFile": "ADO_TESTProjPipline_V03.json",
87-
"targetAccessToken": null,
98+
"inputJsonFile": "TESTProjPipline_V03.json",
99+
"targetAccessToken": "************************************",
88100
"targetOrganization": "nkdagility-preview",
89101
"targetProject": "Clone-Demo",
90102
"targetParentId": 540,
91-
"templateAccessToken": null,
92-
"templateOrganization": "Clone-MO-ATE",
93-
"templateProject": "Clone Template"
103+
"templateAccessToken": "************************************",
104+
"templateOrganization": "orgname",
105+
"templateProject": "template Project",
106+
"templateParentId": 212315,
107+
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
108+
"targetQueryTitle": "Project-@RunName - @projectTitle",
109+
"targetQueryFolder": "Shared Queries"
94110
}
95111
```
96112

97-
## inputJsonFile Example
113+
## Json Input File
114+
115+
116+
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.
117+
118+
The `fields` are the fields that will be used to create the work item. You can use any field ientifyer from Azure DevOps.
98119

99120
```json
100-
[
121+
[
101122
{
102123
"id": 213928,
103-
"area": "TPL",
104-
"tags": "Customer Document",
105124
"fields": {
106-
"title": "Technical specification",
107-
"product": "CC000_000A01"
125+
"System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1",
126+
"System.Tags": "Customer Document",
127+
"System.Title": "Technical specification",
128+
"Custom.Product": "CC",
129+
"Microsoft.VSTS.Scheduling.Effort": 12,
130+
"Custom.TRA_Milestone": "E0.1"
108131
}
109132
},
110133
{
111-
"id": 213928,
112-
"area": "TPL",
113-
"tags": "Customer Document",
134+
"id": "",
114135
"fields": {
115-
"title": "Technical specification",
116-
"product": "CC000_000A02"
136+
"System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1",
137+
"System.Tags": "",
138+
"System.Title": "E4.8 Assessment",
139+
"Custom.Product": "",
140+
"Microsoft.VSTS.Scheduling.Effort": 2,
141+
"Custom.TRA_Milestone": "E4.8"
117142
}
118-
}
119-
]
120-
```
121-
122-
proposed new format not yet adopted:
123-
124-
125-
```json
126-
[
127-
{
128-
"templateId": 213928,
129-
"fields": [
130-
{"System.Title": "Technical specification"},
131-
{"Custom.Project": "CC000_000A01"},
132-
{"System.Tags": "Customer Document"},
133-
{"System.AreaPath": "#{targetProject}#\\TPL"}
134-
]
135-
},
136-
{
137-
"templateId": 213928,
138-
"fields": [
139-
{"System.Title": "Technical specification"},
140-
{"Custom.Project": "CC000_000A02"},
141-
{"System.Tags": "Technical specification"},
142-
{"System.AreaPath": "#{targetProject}#\\TPL"}
143-
]
144-
},
145-
}
146143
]
147144
```
148145

0 commit comments

Comments
 (0)