Skip to content

Commit

Permalink
Create a Query for the Run at the end. (#51)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
MrHinsh authored Jul 17, 2024
2 parents 097cafb + e2c3e78 commit 3239615
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 54 deletions.
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.Threading.Tasks;
using System.Diagnostics.Eventing.Reader;
using AzureDevOps.WorkItemClone.Repositories;

Expand Down Expand Up @@ -75,6 +76,7 @@ await AnsiConsole.Progress()
var task5 = ctx.AddTask("[bold]Stage 5[/]: Create Output Plan Relations ", false);
//var task51 = ctx.AddTask("[bold]Stage 5.1[/]: Validate Data ", false);
var task6 = ctx.AddTask("[bold]Stage 6[/]: Create Work Items", false);
var task7 = ctx.AddTask("[bold]Stage 7[/]: Create Query", false);
string cacheTemplateWorkItemsFile = $"{config.CachePath}\\templateCache-{config.templateOrganization}-{config.templateProject}-{config.templateParentId}.json";
Expand Down Expand Up @@ -218,8 +220,30 @@ await AnsiConsole.Progress()
}
task6.StopTask();
//AnsiConsole.WriteLine($"Stage 6: All Work Items Created.");
// --------------------------------------------------------------
// Task 7: Create Query
task7.MaxValue = 1;
task7.StartTask();
Dictionary<string, string> queryParameters = new Dictionary<string, string>()
{
{ "@projectID", projectItem.id.ToString() },
{ "@projectTitle", projectItem.fields.SystemTitle },
{ "@projectTags", projectItem.fields.SystemTags },
{ "@RunName", config.RunName }
};
var query = await targetApi.CreateProjectQuery(config.targetQueryTitle, config.targetQuery, queryParameters);
task7.Increment(1);
task7.StopTask();
});




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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ internal class WorkItemCloneCommandSettings : BaseCommandSettings
[CommandOption("--targetFalbackWit")]
[DefaultValue("Deliverable")]
public string? targetFalbackWit { get; set; }


[Description("The WIQL Query to use. You can use @projectID, @projectTitle, @projectTags to replace data from the project!")]
[CommandOption("--targetQuery")]
[DefaultValue("SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID")]
public string? targetQuery { get; set; }

[Description("The title to use for the query. You can use @projectID, @projectTitle, @projectTags, @RunName to replace data from the project!")]
[CommandOption("--targetQueryTitle")]
[DefaultValue("Project-@RunName - @projectTitle")]
public string? targetQueryTitle { get; set; }

[Description("Must already Exist and be in the form 'Shared Queries/Folder1/Folder2'!")]
[CommandOption("--targetQueryFolder")]
[DefaultValue("Shared Queries")]
public string? targetQueryFolder { get; set; }

//------------------------------------------------
[Description("The access token for the template location")]
[CommandOption("--templateAccessToken")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ internal void CombineValuesFromConfigAndSettings(WorkItemCloneCommandSettings se
config.targetParentId = EnsureIntAskIfMissing(config.targetParentId = settings.targetParentId != null ? settings.targetParentId : config.targetParentId, "Provide the target parent?");
config.targetFalbackWit = EnsureStringAskIfMissing(config.targetFalbackWit = settings.targetFalbackWit != null ? settings.targetFalbackWit : config.targetFalbackWit, "Provide the target fallback wit?");

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


Expand Down
6 changes: 5 additions & 1 deletion AzureDevOps.WorkItemClone.ConsoleUI/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
"templateAccessToken": null,
"templateOrganization": "ABB-MO-ATE",
"templateProject": "ABB Traction Template",
"templateParentId": 212315
"templateParentId": 212315,
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
"targetQueryTitle": "Project-@RunName - @projectTitle",
"targetQueryFolder": "Shared Queries"

}
49 changes: 41 additions & 8 deletions AzureDevOps.WorkItemClone/AzureDevOpsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ public async IAsyncEnumerable<WorkItemFull> GetWorkItemsFullAsync(Workitem[] ite
}

public async Task<QueryResults?> GetWiqlQueryResults(string wiqlQuery, Dictionary<string, string> parameters)
{
wiqlQuery = GetQueryString(wiqlQuery, parameters);
string post = JsonConvert.SerializeObject(new
{
query = wiqlQuery
});
string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2";
var result = await GetObjectResult<QueryResults>(apiCallUrl, post);
return result.result;
}

private string GetQueryString( string wiqlQuery, Dictionary<string, string> parameters)
{
if (parameters == null)
{
Expand All @@ -64,19 +76,23 @@ public async IAsyncEnumerable<WorkItemFull> GetWorkItemsFullAsync(Workitem[] ite
{
wiqlQuery = "Select [System.Id], [System.Title], [System.State] From WorkItems Where [System.TeamProject] = '@project' order by [System.CreatedDate] desc";
}
foreach (var param in parameters)
wiqlQuery = ReplaceParamsInString(wiqlQuery, parameters);
return wiqlQuery;
}
private string ReplaceParamsInString(string text, Dictionary<string, string> parameters)
{
if (string.IsNullOrEmpty(text))
{
wiqlQuery = wiqlQuery.Replace(param.Key, param.Value);
text = "Default";
}
string post = JsonConvert.SerializeObject(new
foreach (var param in parameters)
{
query = wiqlQuery
});
string apiCallUrl = $"https://dev.azure.com/{_account}/_apis/wit/wiql?api-version=7.2-preview.2";
var result = await GetObjectResult<QueryResults>(apiCallUrl, post);
return result.result;
text = text.Replace(param.Key, param.Value);
}
return text;
}


public async Task<QueryResults?> GetWiqlQueryResults()
{
string post = JsonConvert.SerializeObject(new {
Expand Down Expand Up @@ -230,6 +246,23 @@ public ValueTask DisposeAsync()
{
return new(Task.Delay(TimeSpan.FromSeconds(1)));
}

public async Task<Query> CreateProjectQuery(string queryName, string wiqlQuery, Dictionary<string, string> parameters)
{
///POST https://dev.azure.com/{organization}/{project}/_apis/wit/queries/{query}?api-version=7.1-preview.2
wiqlQuery = GetQueryString(wiqlQuery, parameters);
queryName = ReplaceParamsInString(queryName, parameters);
string post = JsonConvert.SerializeObject(new
{
isFolder = false,
name = queryName,
path = $"Shared Queries/{queryName}",
wiql = wiqlQuery
});
string apiCallUrl = $"https://dev.azure.com/{_account}/{_project}/_apis/wit/queries/Shared Queries/?api-version=7.2-preview.2";
var result = await GetObjectResult<Query>(apiCallUrl, post);
return result.result;
}
}

}
90 changes: 90 additions & 0 deletions AzureDevOps.WorkItemClone/DataContracts/Queries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AzureDevOps.WorkItemClone.DataContracts
{

public class Query
{
public string id { get; set; }
public string name { get; set; }
public string path { get; set; }
public QueryCreatedby createdBy { get; set; }
public DateTime createdDate { get; set; }
public QueryLastmodifiedby lastModifiedBy { get; set; }
public DateTime lastModifiedDate { get; set; }
public bool isFolder { get; set; }
public bool hasChildren { get; set; }
public bool isPublic { get; set; }
public QueryLinks2 _links { get; set; }
public string url { get; set; }
}

public class QueryCreatedby
{
public string displayName { get; set; }
public string url { get; set; }
public QueryLinks _links { get; set; }
public string id { get; set; }
public string uniqueName { get; set; }
public string imageUrl { get; set; }
public string descriptor { get; set; }
}

public class QueryLinks
{
public Avatar avatar { get; set; }
}

public class QueryAvatar
{
public string href { get; set; }
}

public class QueryLastmodifiedby
{
public string displayName { get; set; }
public string url { get; set; }
public QueryLinks1 _links { get; set; }
public string id { get; set; }
public string uniqueName { get; set; }
public string imageUrl { get; set; }
public string descriptor { get; set; }
}

public class QueryLinks1
{
public QueryAvatar1 avatar { get; set; }
}

public class QueryAvatar1
{
public string href { get; set; }
}

public class QueryLinks2
{
public QuerySelf self { get; set; }
public QueryHtml html { get; set; }
public QueryParent parent { get; set; }
}

public class QuerySelf
{
public string href { get; set; }
}

public class QueryHtml
{
public string href { get; set; }
}

public class QueryParent
{
public string href { get; set; }
}

}
87 changes: 42 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ Clones work items from a template project to a target project incorproating a JS
- `--targetProject` - The name of the prject to clone work items to.
- `--targetParentId` - All cloned work items will be come a child of this work item

*Target Query* - The target query is used to create a query in the target project to show the cloned work items.

- `--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`.

You can use the following parameters:

- *@projectID* - The ID of the target parent item
- *@projectTitle* - The title of the parent item
- *@projectTags* - the tags of the item
- *@RunName* - The name of the run

*Optional Parameters* - These are optional parameters that can be used to control the behaviour of the clone process.

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



### `init`

Leads you through the process of creating a configuration file.
Expand All @@ -83,66 +95,51 @@ Clones work items from a template project to a target project incorproating a JS
```json
{
"CachePath": "./cache",
"inputJsonFile": "ADO_TESTProjPipline_V03.json",
"targetAccessToken": null,
"inputJsonFile": "TESTProjPipline_V03.json",
"targetAccessToken": "************************************",
"targetOrganization": "nkdagility-preview",
"targetProject": "Clone-Demo",
"targetParentId": 540,
"templateAccessToken": null,
"templateOrganization": "Clone-MO-ATE",
"templateProject": "Clone Template"
"templateAccessToken": "************************************",
"templateOrganization": "orgname",
"templateProject": "template Project",
"templateParentId": 212315,
"targetQuery": "SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AreaPath],[System.AssignedTo],[System.State] FROM workitems WHERE [System.Parent] = @projectID",
"targetQueryTitle": "Project-@RunName - @projectTitle",
"targetQueryFolder": "Shared Queries"
}
```

## inputJsonFile Example
## Json Input File


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.

```json
[
[
{
"id": 213928,
"area": "TPL",
"tags": "Customer Document",
"fields": {
"title": "Technical specification",
"product": "CC000_000A01"
"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"
}
},
{
"id": 213928,
"area": "TPL",
"tags": "Customer Document",
"id": "",
"fields": {
"title": "Technical specification",
"product": "CC000_000A02"
"System.AreaPath": "Engineering Group\\ECH Group\\ECH TPL 1",
"System.Tags": "",
"System.Title": "E4.8 Assessment",
"Custom.Product": "",
"Microsoft.VSTS.Scheduling.Effort": 2,
"Custom.TRA_Milestone": "E4.8"
}
}
]
```

proposed new format not yet adopted:


```json
[
{
"templateId": 213928,
"fields": [
{"System.Title": "Technical specification"},
{"Custom.Project": "CC000_000A01"},
{"System.Tags": "Customer Document"},
{"System.AreaPath": "#{targetProject}#\\TPL"}
]
},
{
"templateId": 213928,
"fields": [
{"System.Title": "Technical specification"},
{"Custom.Project": "CC000_000A02"},
{"System.Tags": "Technical specification"},
{"System.AreaPath": "#{targetProject}#\\TPL"}
]
},
}
]
```

0 comments on commit 3239615

Please sign in to comment.