Skip to content

Commit 22d0f23

Browse files
Handle per-semester parents in Quest (#413)
* Set the state to "committed" after creation * First pass at a new config * Revert "First pass at a new config" This reverts commit 8e028fd45dd4b3ec35289dc60f1d7f303cf08325. * Code refactoring Move some code around to make it easier to add the config features without extensive copying. * Expand config object to include semesters Set the parent based on semester + parent config. * Apply suggestions from code review Co-authored-by: David Pine <[email protected]> * fix for production --------- Co-authored-by: David Pine <[email protected]>
1 parent 2da9209 commit 22d0f23

File tree

5 files changed

+214
-176
lines changed

5 files changed

+214
-176
lines changed

actions/sequester/Quest2GitHub/Models/QuestIteration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public class QuestIteration
77
public required string Name { get; init; }
88
public required string Path { get; init; }
99

10+
public bool IsInSemester(string semesterName) => Path.Contains(semesterName);
11+
1012
public static QuestIteration? CurrentIteration(IEnumerable<QuestIteration> iterations)
1113
{
1214
var currentYear = int.Parse(DateTime.Now.ToString("yyyy"));

actions/sequester/Quest2GitHub/Models/QuestWorkItem.cs

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DotNet.DocsTools.GitHubObjects;
22
using DotNet.DocsTools.Utility;
3+
using Microsoft.DotnetOrg.Ospo;
34

45
namespace Quest2GitHub.Models;
56

@@ -123,7 +124,6 @@ public static async Task<QuestWorkItem> QueryWorkItem(QuestClient client, int wo
123124
/// Create a work item from a GitHub issue.
124125
/// </summary>
125126
/// <param name="issue">The GitHub issue.</param>
126-
/// <param name="parentId">The ID of the parent ID</param>
127127
/// <param name="questClient">The quest client.</param>
128128
/// <param name="ospoClient">the MS open source programs office client.</param>
129129
/// <param name="path">The path component for the area path.</param>
@@ -139,16 +139,18 @@ public static async Task<QuestWorkItem> QueryWorkItem(QuestClient client, int wo
139139
/// Json element.
140140
/// </remarks>
141141
public static async Task<QuestWorkItem> CreateWorkItemAsync(QuestIssueOrPullRequest issue,
142-
int parentId,
143142
QuestClient questClient,
144143
OspoClient? ospoClient,
145144
string path,
146145
string? requestLabelNodeId,
147146
QuestIteration currentIteration,
148147
IEnumerable<QuestIteration> allIterations,
149-
IEnumerable<LabelToTagMap> tagMap)
148+
IEnumerable<LabelToTagMap> tagMap,
149+
IEnumerable<ParentForLabel> parentNodes,
150+
int defaultParentNode)
150151
{
151152
string areaPath = $"""{questClient.QuestProject}\{path}""";
153+
int parentId = ParentIdFromIssue(parentNodes, issue, defaultParentNode, allIterations);
152154

153155
List<JsonPatchDocument> patchDocument =
154156
[
@@ -399,6 +401,180 @@ public static string BuildDescriptionFromIssue(QuestIssueOrPullRequest issue, st
399401
}
400402
}
401403

404+
static internal async Task<QuestWorkItem?> UpdateWorkItemAsync(QuestWorkItem questItem,
405+
QuestIssueOrPullRequest ghIssue,
406+
QuestClient questClient,
407+
OspoClient? ospoClient,
408+
IEnumerable<QuestIteration> allIterations,
409+
IEnumerable<LabelToTagMap> tagMap,
410+
IEnumerable<ParentForLabel> parentNodes,
411+
int defaultParentNode)
412+
{
413+
int parentId = ParentIdFromIssue(parentNodes, ghIssue, defaultParentNode, allIterations);
414+
string? ghAssigneeEmailAddress = await ghIssue.QueryAssignedMicrosoftEmailAddressAsync(ospoClient);
415+
AzDoIdentity? questAssigneeID = default;
416+
var proposedQuestState = questItem.State;
417+
if (ghAssigneeEmailAddress?.EndsWith("@microsoft.com") == true)
418+
{
419+
questAssigneeID = await questClient.GetIDFromEmail(ghAssigneeEmailAddress);
420+
}
421+
List<JsonPatchDocument> patchDocument = [];
422+
if ((parentId != 0) && (parentId != questItem.ParentWorkItemId))
423+
{
424+
if (questItem.ParentWorkItemId != 0)
425+
{
426+
// Remove the existing parent relation.
427+
patchDocument.Add(new JsonPatchDocument
428+
{
429+
Operation = Op.Remove,
430+
Path = "/relations/" + questItem.ParentRelationIndex,
431+
});
432+
};
433+
var parentRelation = new Relation
434+
{
435+
RelationName = "System.LinkTypes.Hierarchy-Reverse",
436+
Url = $"https://dev.azure.com/{questClient.QuestOrg}/{questClient.QuestProject}/_apis/wit/workItems/{parentId}",
437+
Attributes =
438+
{
439+
["name"] = "Parent",
440+
["isLocked"] = false
441+
}
442+
};
443+
444+
patchDocument.Add(new JsonPatchDocument
445+
{
446+
Operation = Op.Add,
447+
Path = "/relations/-",
448+
From = default,
449+
Value = parentRelation
450+
});
451+
}
452+
if ((questAssigneeID is not null) && (questAssigneeID?.Id != questItem.AssignedToId))
453+
{
454+
// build patch document for assignment.
455+
JsonPatchDocument assignPatch = new()
456+
{
457+
Operation = Op.Add,
458+
Path = "/fields/System.AssignedTo",
459+
Value = questAssigneeID,
460+
};
461+
patchDocument.Add(assignPatch);
462+
}
463+
bool questItemOpen = questItem.State is not "Closed";
464+
proposedQuestState = ghIssue.IsOpen ? "Committed" : "Closed";
465+
if (ghIssue.IsOpen != questItemOpen)
466+
{
467+
468+
// When the issue is opened or closed,
469+
// update the description. That picks up any new
470+
// labels and comments.
471+
patchDocument.Add(new JsonPatchDocument
472+
{
473+
Operation = Op.Add,
474+
Path = "/fields/System.Description",
475+
From = default,
476+
Value = BuildDescriptionFromIssue(ghIssue, null)
477+
});
478+
}
479+
StoryPointSize? iterationSize = ghIssue.LatestStoryPointSize();
480+
QuestIteration? iteration = iterationSize?.ProjectIteration(allIterations);
481+
if (iterationSize != null)
482+
{
483+
Console.WriteLine($"Latest GitHub sprint project: {iterationSize?.Month}-{iterationSize?.CalendarYear}, size: {iterationSize?.Size}");
484+
if ((iterationSize?.IsPastIteration == true) && (ghIssue.IsOpen == true))
485+
{
486+
Console.WriteLine($"Moving to the backlog / future iteration.");
487+
iteration = QuestIteration.FutureIteration(allIterations);
488+
proposedQuestState = "New";
489+
}
490+
}
491+
else
492+
{
493+
Console.WriteLine("No GitHub sprint project found - using current iteration.");
494+
}
495+
if (proposedQuestState != questItem.State)
496+
{
497+
patchDocument.Add(new JsonPatchDocument
498+
{
499+
Operation = Op.Add,
500+
Path = "/fields/System.State",
501+
Value = proposedQuestState,
502+
});
503+
}
504+
if ((iteration is not null) && (iteration.Path != questItem.IterationPath))
505+
{
506+
patchDocument.Add(new JsonPatchDocument
507+
{
508+
Operation = Op.Add,
509+
Path = "/fields/System.IterationPath",
510+
Value = iteration.Path,
511+
});
512+
}
513+
if ((iterationSize?.QuestStoryPoint() is not null) && (iterationSize.QuestStoryPoint() != questItem.StoryPoints))
514+
{
515+
patchDocument.Add(new JsonPatchDocument
516+
{
517+
Operation = Op.Add,
518+
From = default,
519+
Path = "/fields/Microsoft.VSTS.Scheduling.StoryPoints",
520+
Value = iterationSize.QuestStoryPoint(),
521+
});
522+
}
523+
int? priority = ghIssue.GetPriority(iterationSize);
524+
if (priority.HasValue && priority != questItem.Priority)
525+
{
526+
patchDocument.Add(new JsonPatchDocument
527+
{
528+
Operation = Op.Add,
529+
Path = "/fields/Microsoft.VSTS.Common.Priority",
530+
Value = priority.Value
531+
});
532+
}
533+
var tags = from t in ghIssue.WorkItemTagsForIssue(tagMap)
534+
where !questItem.Tags.Contains(t)
535+
select t;
536+
if (tags.Any())
537+
{
538+
string azDoTags = string.Join(";", tags);
539+
patchDocument.Add(new JsonPatchDocument
540+
{
541+
Operation = Op.Add,
542+
Path = "/fields/System.Tags",
543+
Value = azDoTags
544+
});
545+
}
546+
547+
QuestWorkItem? newItem = default;
548+
if (patchDocument.Count != 0)
549+
{
550+
JsonElement jsonDocument = await questClient.PatchWorkItem(questItem.Id, patchDocument);
551+
newItem = QuestWorkItem.WorkItemFromJson(jsonDocument);
552+
}
553+
if (!ghIssue.IsOpen && (ghIssue.ClosingPRUrl is not null))
554+
{
555+
newItem = await questItem.AddClosingPR(questClient, ghIssue.ClosingPRUrl) ?? newItem;
556+
}
557+
return newItem;
558+
}
559+
560+
static private int ParentIdFromIssue(IEnumerable<ParentForLabel> parentNodes, QuestIssueOrPullRequest ghIssue, int defaultParentNode, IEnumerable<QuestIteration> allIterations)
561+
{
562+
var iteration = ghIssue.LatestStoryPointSize()?.ProjectIteration(allIterations);
563+
564+
foreach (ParentForLabel pair in parentNodes)
565+
{
566+
if (ghIssue.Labels.Any(l => l.Name == pair.Label) || (pair.Label is null))
567+
{
568+
if ((pair.Semester is null) || (iteration?.IsInSemester(pair.Semester) is true))
569+
{
570+
return pair.ParentNodeId;
571+
}
572+
}
573+
}
574+
return defaultParentNode;
575+
}
576+
577+
402578
/// <summary>
403579
/// Construct a work item from the JSON document.
404580
/// </summary>

actions/sequester/Quest2GitHub/Options/ImportOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace Quest2GitHub.Options;
22

3-
public record struct ParentForLabel(string Label, int ParentNodeId);
3+
public record struct ParentForLabel(string? Label, string? Semester, int ParentNodeId);
44

55
public record struct LabelToTagMap(string Label, string Tag);
66

0 commit comments

Comments
 (0)