How to query Work Item links with specified comment using WIQL - tfs

I need a fast way to get a list of links filtered by the link comment.
Have a code:
var linkCommentFilter = "Some link comment";
const string queryString = #"SELECT [System.Id]
FROM workItemlinks
WHERE [System.Links.LinkType] = 'Tested By'
AND [System.Links.Comment] = '{0}'
AND ([Source].[System.Id] IN ({1}))";
var query = new Query(store, string.Format(queryString,
linkCommentFilter,
string.Join(",", wiIds)));
var result = query.RunLinkQuery().ToArray();
When trying to run this code an exception occurred "field System.Links.Comment doesn't exist". How could I fix it?

You can't query Work Item links with specified comment using WIQL as there is no System.Links.Comment field in TFS.
You need to use TFS rest api to get a list of work items with links and attachments, check the sample code below:
public List<WorkItem> GetWorkItemsWithLinksAndAttachments()
{
int[] workitemIds = new int[] { 1, 5, 6, 10, 22, 50 };
VssConnection connection = Context.Connection;
WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient<WorkItemTrackingHttpClient>();
List<WorkItem> workitems = workItemTrackingClient.GetWorkItemsAsync(workitemIds, expand: WorkItemExpand.Links | WorkItemExpand.Relations).Result;
foreach(var workitem in workitems)
{
Console.WriteLine("Work item {0}", workitem.Id);
foreach (var relation in workitem.Relations)
{
Console.WriteLine(" {0} {1}", relation.Rel, relation.Url);
}
}
Then find the specified comment with relation.Attributes["comment"] in workitem.Relations.

Related

Azure DevOps Test API - How to create or get the value of actionPath

Clerify,
By get I mean to generate or get the number I need to provide, NOT the number already exists in the test results.
When creating tests result with steps information (manual run), need to provide value for a field called actionPath
https://learn.microsoft.com/en-us/rest/api/azure/devops/test/action%20results/list?view=azure-devops-rest-6.0#testactionresultmodel
Is there a way to find or generate this value so I can create tests results using the API? It seems there is no consistent or clear way of what or where.
After some try/error I have found the solution.
Phase One - Get the Step ID
You need to install HTML Agility Pack nuget to be able to extract data from the HTML.
When creating a test step, it will have an automatic ID which is represented in it's HTML. In order to get the test steps HTML and extract the IDs, need to use the following code (or similar):
// create a client (assuming you know how to create vss connection)
var client = vssConnction.GetClient<WorkItemTrackingHttpClient>();
// get the work item with all fields
var item = client.GetWorkItemAsync(<item id>, expand: WorkItemExpand.All).GetAwaiter().GetResult();
// get the HTML
var html = $"{item.Fields["Microsoft.VSTS.TCM.Steps"]}"
// load the HTML into DOM object
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(html);
// extract all IDs. The actionPath is the hex form of the step ID (.ToString("x"))
var ids = htmlDocument.DocumentNode.SelectNodes("//step").Select(i => int.Parse(i.GetAttributeValue("id", "0")).ToString("x"));
Phase Two - Get the Action Path
The actionPath is the hex form of the step ID in 8 digitis with leading zeros (e.g. id 10 will be 0000000a). In order to parse the id into action path use the following code (or similar):
var actionPath = ids.Select(i => new string('0', 8 - i.Length) + i);
Now you can tell the actionPath when creating an action test result
Full Code Workflow (was not validated for errors)
// credentials
var basicCredential = new VssBasicCredential("", personalAccessToken);
var credentials = new VssCredentials(basicCredential);
// connection
var connection = new VssConnection(new Uri("your collection URI"), credentials);
// clients
var testManagement = connection.GetClient<TestManagementHttpClient>();
// test points
var pointsFilter = new PointsFilter { TestcaseIds = new[] { <test_id>, <test_id>, ... } };
var pointsQuery = new TestPointsQuery() { PointsFilter = pointsFilter };
var points = testManagement.GetPointsByQueryAsync(query, project).GetAwaiter().GetResult().Points;
// test run
var runCreateModel = new RunCreateModel(name: "My Test Run", pointIds: points, plan: new ShallowReference(id: $"{<test_plan_id>}"));
var testRun = testManagement.CreateTestRunAsync(runCreateModel, "<project name>").GetAwaiter().GetResult();
// iteration
var dateTime = DateTime.Now;
var date = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Kind);
var iteration = new TestIterationDetailsModel
{
Id = 1,
StartedDate = date,
CompletedDate = date.AddMinutes(5),
Comment = "My Test Iteration"
}
// action
var actionResult = new TestActionResultModel
{
ActionPath = "<actionPath>", // the one we extracted from the HTML
StepIdentifier = "<the_test_step_id>", // the one we extracted from the HTML
IterationId = <the_iteration_id>,
StartedDate = date,
Outcome = TestOutcome.Passed,
CompletedDate = date.AddMinutes(5)
};
iteration.ActionResults = new List<TestActionResultModel> { actionResult };
// test result
var testCaseResult = testManagement
.GetTestResultByIdAsync("<project name>", testRun.Id, <test_results_id>, ResultDetails.Iterations)
.GetAwaiter()
.GetResult()
.First();
testCaseResult.IterationDetails.Add(iteration);
testManagement.UpdateTestResultsAsync(new[] { testCaseResult }, "<project name>", testRun.Id).GetAwaiter().GetResult();
How to create or get the value of actionPath
You can use the below rest api to get the value of actionPath.
https://dev.azure.com/{organization}/{project}/_apis/test/Runs/{runId}/results/{testCaseResultId}?detailsToInclude=iterations&api-version=6.0
Test in Postman:

Create team in GraphAPI returns always null

I am using GraphAPI SDK to create a new Team in Microsoft Teams:
var newTeam = new Team()
{
DisplayName = teamName,
Description = teamName,
AdditionalData = new Dictionary<string, object>()
{
{"template#odata.bind", "https://graph.microsoft.com/v1.0/teamsTemplates('standard')"}
},
Members = new TeamMembersCollectionPage()
{
new AadUserConversationMember
{
Roles = new List<String>()
{
"owner"
},
AdditionalData = new Dictionary<string, object>()
{
{"user#odata.bind", $"https://graph.microsoft.com/v1.0/users/{userId}"}
}
}
}
};
var team = await this.graphStableClient.Teams
.Request()
.AddAsync(newTeam);
The problem is that I get always null. According documentation this method returns a 202 response (teamsAsyncOperation), but the AddAsync method from SDK returns a Team object. Is there any way to get the tracking url to check if the team creation has been finished with the SDK?
Documentation and working SDK works different... As they wrote in microsoft-graph-docs/issues/10840, we can only get the teamsAsyncOperation header values if we use HttpRequestMessage as in contoso-airlines-teams-sample. They wrote to the people who asks this problem, look to the joined teams :)) :)
var newTeam = new Team()
{
DisplayName = model.DisplayName,
Description = model.Description,
AdditionalData = new Dictionary<string, object>
{
["template#odata.bind"] = $"{graph.BaseUrl}/teamsTemplates('standard')",
["members"] = owners.ToArray()
}
};
// we cannot use 'await client.Teams.Request().AddAsync(newTeam)'
// as we do NOT get the team ID back (object is always null) :(
BaseRequest request = (BaseRequest)graph.Teams.Request();
request.ContentType = "application/json";
request.Method = "POST";
string location;
using (HttpResponseMessage response = await request.SendRequestAsync(newTeam, CancellationToken.None))
location = response.Headers.Location.ToString();
// looks like: /teams('7070b1fd-1f14-4a06-8617-254724d63cde')/operations('c7c34e52-7ebf-4038-b306-f5af2d9891ac')
// but is documented as: /teams/7070b1fd-1f14-4a06-8617-254724d63cde/operations/c7c34e52-7ebf-4038-b306-f5af2d9891ac
// -> this split supports both of them
string[] locationParts = location.Split(new[] { '\'', '/', '(', ')' }, StringSplitOptions.RemoveEmptyEntries);
string teamId = locationParts[1];
string operationId = locationParts[3];
// before querying the first time we must wait some secs, else we get a 404
int delayInMilliseconds = 5_000;
while (true)
{
await Task.Delay(delayInMilliseconds);
// lets see how far the teams creation process is
TeamsAsyncOperation operation = await graph.Teams[teamId].Operations[operationId].Request().GetAsync();
if (operation.Status == TeamsAsyncOperationStatus.Succeeded)
break;
if (operation.Status == TeamsAsyncOperationStatus.Failed)
throw new Exception($"Failed to create team '{newTeam.DisplayName}': {operation.Error.Message} ({operation.Error.Code})");
// according to the docs, we should wait > 30 secs between calls
// https://learn.microsoft.com/en-us/graph/api/resources/teamsasyncoperation?view=graph-rest-1.0
delayInMilliseconds = 30_000;
}
// finally, do something with your team...
I found a solution from another question... Tried and saw that it's working...

'Create copy of work item' via REST API for Azure DevOps?

I'm wanting to 'Create copy of work item' which is available via the UI, ideally via the API.
I know how to create a new work item, but the feature in the UI to connect all current parent links / related links, and all other details is quite useful.
Creating via this API is here: https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work%20items/create?view=azure-devops-rest-5.1
Any help would be greatly appreciated.
We cannot just copy a work item because it contains system fields that we should skip. Additionally your process may have some rules that may block some fields on the creation step. Here is the small example to clone a work item through REST API with https://www.nuget.org/packages/Microsoft.TeamFoundationServer.Client:
class Program
{
static string[] systemFields = { "System.IterationId", "System.ExternalLinkCount", "System.HyperLinkCount", "System.AttachedFileCount", "System.NodeName",
"System.RevisedDate", "System.ChangedDate", "System.Id", "System.AreaId", "System.AuthorizedAs", "System.State", "System.AuthorizedDate", "System.Watermark",
"System.Rev", "System.ChangedBy", "System.Reason", "System.WorkItemType", "System.CreatedDate", "System.CreatedBy", "System.History", "System.RelatedLinkCount",
"System.BoardColumn", "System.BoardColumnDone", "System.BoardLane", "System.CommentCount", "System.TeamProject"}; //system fields to skip
static string[] customFields = { "Microsoft.VSTS.Common.ActivatedDate", "Microsoft.VSTS.Common.ActivatedBy", "Microsoft.VSTS.Common.ResolvedDate",
"Microsoft.VSTS.Common.ResolvedBy", "Microsoft.VSTS.Common.ResolvedReason", "Microsoft.VSTS.Common.ClosedDate", "Microsoft.VSTS.Common.ClosedBy",
"Microsoft.VSTS.Common.StateChangeDate"}; //unneeded fields to skip
const string ChildRefStr = "System.LinkTypes.Hierarchy-Forward"; //should be only one parent
static void Main(string[] args)
{
string pat = "<pat>"; //https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate
string orgUrl = "https://dev.azure.com/<org>";
string newProjectName = "";
int wiIdToClone = 0;
VssConnection connection = new VssConnection(new Uri(orgUrl), new VssBasicCredential(string.Empty, pat));
var witClient = connection.GetClient<WorkItemTrackingHttpClient>();
CloneWorkItem(witClient, wiIdToClone, newProjectName, true);
}
private static void CloneWorkItem(WorkItemTrackingHttpClient witClient, int wiIdToClone, string NewTeamProject = "", bool CopyLink = false)
{
WorkItem wiToClone = (CopyLink) ? witClient.GetWorkItemAsync(wiIdToClone, expand: WorkItemExpand.Relations).Result
: witClient.GetWorkItemAsync(wiIdToClone).Result;
string teamProjectName = (NewTeamProject != "") ? NewTeamProject : wiToClone.Fields["System.TeamProject"].ToString();
string wiType = wiToClone.Fields["System.WorkItemType"].ToString();
JsonPatchDocument patchDocument = new JsonPatchDocument();
foreach (var key in wiToClone.Fields.Keys) //copy fields
if (!systemFields.Contains(key) && !customFields.Contains(key))
if (NewTeamProject == "" ||
(NewTeamProject != "" && key != "System.AreaPath" && key != "System.IterationPath")) //do not copy area and iteration into another project
patchDocument.Add(new JsonPatchOperation()
{
Operation = Operation.Add,
Path = "/fields/" + key,
Value = wiToClone.Fields[key]
});
if (CopyLink) //copy links
foreach (var link in wiToClone.Relations)
{
if (link.Rel != ChildRefStr)
{
patchDocument.Add(new JsonPatchOperation()
{
Operation = Operation.Add,
Path = "/relations/-",
Value = new
{
rel = link.Rel,
url = link.Url
}
});
}
}
WorkItem clonedWi = witClient.CreateWorkItemAsync(patchDocument, teamProjectName, wiType).Result;
Console.WriteLine("New work item: " + clonedWi.Id);
}
}
Link to full project: https://github.com/ashamrai/AzureDevOpsExtensions/tree/master/CustomNetTasks/CloneWorkItem

TFS workItem.Validate() issue

Here is the case. I wrote a tool to create a task WorkItem (version 12.0.0.0), it can run normally and create TFS task last month. But Today, when I rerun it, it doesn't create TFS task and show error message: The field "Assigned To" contains the value "email address" that is not in the list of supported values.
NetworkCredential credential = new NetworkCredential("user", "password");
TfsConfigurationServer configurationServer = new TfsConfigurationServer(tfsUri, credential);
ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren(new[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
foreach (CatalogNode collectionNode in collectionNodes)
{
Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId);
ReadOnlyCollection<CatalogNode> projectNodes = collectionNode.QueryChildren(new[] { CatalogResourceTypes.TeamProject }, false, CatalogQueryOptions.None);
foreach (CatalogNode projectNode in projectNodes)
{
Console.WriteLine("Team Project: " + projectNode.Resource.DisplayName);
}
WorkItemStore workstore = teamProjectCollection.GetService<WorkItemStore>();
Project project = workstore.Projects["SpecifiedProject"];
WorkItemType itemtype = project.WorkItemTypes["Task"];
WorkItem workItem = new WorkItem(itemtype);
string assignedTo;
assignedTo = String.Format("{0}#***.com", task.User);
workItem.Fields["Assigned to"].Value = assignedTo;
workItem.Fields["Priority"].Value = task.Priority;
workItem.Fields["Area Path"].Value = AREAPATH;
workItem.Fields["Iteration path"].Value = task.IterationPath;
workItem.Title = task.Title;
workItem.Fields["DESCRIPTION"].Value = task.Description;
workItem.Fields["Original Estimate"].Value = task.EstimatedTime.ToString();
workItem.Fields["Completed Work"].Value = task.ActualTime.ToString();
workItem.Fields["Remaining Work"].Value = task.RemainTime.ToString();
// get the link type for hierarchical relationships
var linkType = workstore.WorkItemLinkTypes[CoreLinkTypeReferenceNames.Hierarchy];
//var linkType = workstore.WorkItemLinkTypes[CoreLinkTypeReferenceNames.Dependency];
RelatedLink rl = new RelatedLink(linkType.ReverseEnd, task.UserStoryID);
workItem.Links.Add(rl);
ArrayList ValidationResult = workItem.Validate();
I think this code block "ArrayList ValidationResult = workItem.Validate();" has some issues, for detail error: enter image description here

Fetch latest checked in information using TfvcHttpClient Class

Trying to fetch latest checked in information using TfvcHttpClient class from a specific folder in Team Foundation Server using its client API from a console application.
Please help how can I achieve it? I have personal access token and below mentioned is able to connect by using it:
Code:
string uri = _uri;
string personalAccessToken = _personalAccessToken;
string project = _project;
string credentials = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", personalAccessToken)));
//create wiql object
var wiql = new
{
query = "Select [State], [Title] " +
"From WorkItems " +
"Where [Work Item Type] = 'Bug' " +
"And [System.TeamProject] = '" + project + "' " +
"And [System.State] <> 'Closed' " +
"Order By [State] Asc, [Changed Date] Desc"
};
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(uri);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
//serialize the wiql object into a json string
var postValue = new StringContent(JsonConvert.SerializeObject(wiql), Encoding.UTF8, "application/json"); //mediaType needs to be application/json for a post call
//send query to REST endpoint to return list of id's from query
var method = new HttpMethod("POST");
var httpRequestMessage = new HttpRequestMessage(method, uri + "/_apis/wit/wiql?api-version=2.2") { Content = postValue };
var httpResponseMessage = client.SendAsync(httpRequestMessage).Result;
}
I have tried below mentioned code for achieving:
VssConnection connection = new VssConnection(serverUrl, new
VssBasicCredential(string.Empty, _personalAccessToken));
var buildServer = connection.GetClient<BuildHttpClient>(); // connect to the build server subpart
var sourceControlServer = connection.GetClient<Microsoft.TeamFoundation.SourceControl.WebApi.TfvcHttpClient>(); // connect to the TFS source control subpart
var changesets = buildServer.GetChangesBetweenBuildsAsync("client-rsa", 1, 5).Result;
foreach (var changeset in changesets)
{
var csDetail = sourceControlServer.GetChangesetAsync("client-rsa", Convert.ToInt32(changeset.Id.Replace("C", string.Empty).Trim()), includeDetails: true).Result;
var checkinNote = csDetail.CheckinNotes?.FirstOrDefault(_ => _.Name == "My check-in note");
if (checkinNote != null)
{
Console.WriteLine("{0}: {1}", changeset.Id, changeset.Message);
Console.WriteLine("Check-in note: {0}", checkinNote.Value);
}
else
Console.WriteLine("Warning: {0} has no check-in note", changeset.Id);
}
Here is the simple sample code which use TfvcHttpClient to get the latest changeset information:
string purl = "https://xxx.visualstudio.com";
string projectname = "projectname";
VssCredentials creds = new VssClientCredentials();
creds.Storage = new VssClientCredentialStorage();
VssConnection vc = new VssConnection(new Uri(purl),creds);
TfvcHttpClient thc = vc.GetClient<TfvcHttpClient>();
TfvcChangesetSearchCriteria tcsc = new TfvcChangesetSearchCriteria();
//Specify the server path of the folder
tcsc.ItemPath = "$/XXXX/XXXX";
//Get the entire history of the specified path
List<TfvcChangesetRef> changerefs = thc.GetChangesetsAsync(projectname,null,null,null,null,tcsc).Result;
//Get the latest changeset ref
TfvcChangesetRef changeref = changerefs.First();
//Get the changeset
TfvcChangeset changeset = thc.GetChangesetAsync(projectname,changeref.ChangesetId).Result;
//Get the detailed changes
List<TfvcChange> changes = thc.GetChangesetChangesAsync(changeref.ChangesetId).Result;
foreach (TfvcChange cg in changes)
{
//Code to read detail information
}

Resources