Skip to content

Commit

Permalink
Handle per-semester parents in Quest (#413)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
BillWagner and IEvangelist authored Sep 26, 2024
1 parent 2da9209 commit 22d0f23
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 176 deletions.
2 changes: 2 additions & 0 deletions actions/sequester/Quest2GitHub/Models/QuestIteration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class QuestIteration
public required string Name { get; init; }
public required string Path { get; init; }

public bool IsInSemester(string semesterName) => Path.Contains(semesterName);

public static QuestIteration? CurrentIteration(IEnumerable<QuestIteration> iterations)
{
var currentYear = int.Parse(DateTime.Now.ToString("yyyy"));
Expand Down
182 changes: 179 additions & 3 deletions actions/sequester/Quest2GitHub/Models/QuestWorkItem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using DotNet.DocsTools.GitHubObjects;
using DotNet.DocsTools.Utility;
using Microsoft.DotnetOrg.Ospo;

namespace Quest2GitHub.Models;

Expand Down Expand Up @@ -123,7 +124,6 @@ public static async Task<QuestWorkItem> QueryWorkItem(QuestClient client, int wo
/// Create a work item from a GitHub issue.
/// </summary>
/// <param name="issue">The GitHub issue.</param>
/// <param name="parentId">The ID of the parent ID</param>
/// <param name="questClient">The quest client.</param>
/// <param name="ospoClient">the MS open source programs office client.</param>
/// <param name="path">The path component for the area path.</param>
Expand All @@ -139,16 +139,18 @@ public static async Task<QuestWorkItem> QueryWorkItem(QuestClient client, int wo
/// Json element.
/// </remarks>
public static async Task<QuestWorkItem> CreateWorkItemAsync(QuestIssueOrPullRequest issue,
int parentId,
QuestClient questClient,
OspoClient? ospoClient,
string path,
string? requestLabelNodeId,
QuestIteration currentIteration,
IEnumerable<QuestIteration> allIterations,
IEnumerable<LabelToTagMap> tagMap)
IEnumerable<LabelToTagMap> tagMap,
IEnumerable<ParentForLabel> parentNodes,
int defaultParentNode)
{
string areaPath = $"""{questClient.QuestProject}\{path}""";
int parentId = ParentIdFromIssue(parentNodes, issue, defaultParentNode, allIterations);

List<JsonPatchDocument> patchDocument =
[
Expand Down Expand Up @@ -399,6 +401,180 @@ public static string BuildDescriptionFromIssue(QuestIssueOrPullRequest issue, st
}
}

static internal async Task<QuestWorkItem?> UpdateWorkItemAsync(QuestWorkItem questItem,
QuestIssueOrPullRequest ghIssue,
QuestClient questClient,
OspoClient? ospoClient,
IEnumerable<QuestIteration> allIterations,
IEnumerable<LabelToTagMap> tagMap,
IEnumerable<ParentForLabel> parentNodes,
int defaultParentNode)
{
int parentId = ParentIdFromIssue(parentNodes, ghIssue, defaultParentNode, allIterations);
string? ghAssigneeEmailAddress = await ghIssue.QueryAssignedMicrosoftEmailAddressAsync(ospoClient);
AzDoIdentity? questAssigneeID = default;
var proposedQuestState = questItem.State;
if (ghAssigneeEmailAddress?.EndsWith("@microsoft.com") == true)
{
questAssigneeID = await questClient.GetIDFromEmail(ghAssigneeEmailAddress);
}
List<JsonPatchDocument> patchDocument = [];
if ((parentId != 0) && (parentId != questItem.ParentWorkItemId))
{
if (questItem.ParentWorkItemId != 0)
{
// Remove the existing parent relation.
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Remove,
Path = "/relations/" + questItem.ParentRelationIndex,
});
};
var parentRelation = new Relation
{
RelationName = "System.LinkTypes.Hierarchy-Reverse",
Url = $"https://dev.azure.com/{questClient.QuestOrg}/{questClient.QuestProject}/_apis/wit/workItems/{parentId}",
Attributes =
{
["name"] = "Parent",
["isLocked"] = false
}
};

patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/relations/-",
From = default,
Value = parentRelation
});
}
if ((questAssigneeID is not null) && (questAssigneeID?.Id != questItem.AssignedToId))
{
// build patch document for assignment.
JsonPatchDocument assignPatch = new()
{
Operation = Op.Add,
Path = "/fields/System.AssignedTo",
Value = questAssigneeID,
};
patchDocument.Add(assignPatch);
}
bool questItemOpen = questItem.State is not "Closed";
proposedQuestState = ghIssue.IsOpen ? "Committed" : "Closed";
if (ghIssue.IsOpen != questItemOpen)
{

// When the issue is opened or closed,
// update the description. That picks up any new
// labels and comments.
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/fields/System.Description",
From = default,
Value = BuildDescriptionFromIssue(ghIssue, null)
});
}
StoryPointSize? iterationSize = ghIssue.LatestStoryPointSize();
QuestIteration? iteration = iterationSize?.ProjectIteration(allIterations);
if (iterationSize != null)
{
Console.WriteLine($"Latest GitHub sprint project: {iterationSize?.Month}-{iterationSize?.CalendarYear}, size: {iterationSize?.Size}");
if ((iterationSize?.IsPastIteration == true) && (ghIssue.IsOpen == true))
{
Console.WriteLine($"Moving to the backlog / future iteration.");
iteration = QuestIteration.FutureIteration(allIterations);
proposedQuestState = "New";
}
}
else
{
Console.WriteLine("No GitHub sprint project found - using current iteration.");
}
if (proposedQuestState != questItem.State)
{
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/fields/System.State",
Value = proposedQuestState,
});
}
if ((iteration is not null) && (iteration.Path != questItem.IterationPath))
{
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/fields/System.IterationPath",
Value = iteration.Path,
});
}
if ((iterationSize?.QuestStoryPoint() is not null) && (iterationSize.QuestStoryPoint() != questItem.StoryPoints))
{
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
From = default,
Path = "/fields/Microsoft.VSTS.Scheduling.StoryPoints",
Value = iterationSize.QuestStoryPoint(),
});
}
int? priority = ghIssue.GetPriority(iterationSize);
if (priority.HasValue && priority != questItem.Priority)
{
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/fields/Microsoft.VSTS.Common.Priority",
Value = priority.Value
});
}
var tags = from t in ghIssue.WorkItemTagsForIssue(tagMap)
where !questItem.Tags.Contains(t)
select t;
if (tags.Any())
{
string azDoTags = string.Join(";", tags);
patchDocument.Add(new JsonPatchDocument
{
Operation = Op.Add,
Path = "/fields/System.Tags",
Value = azDoTags
});
}

QuestWorkItem? newItem = default;
if (patchDocument.Count != 0)
{
JsonElement jsonDocument = await questClient.PatchWorkItem(questItem.Id, patchDocument);
newItem = QuestWorkItem.WorkItemFromJson(jsonDocument);
}
if (!ghIssue.IsOpen && (ghIssue.ClosingPRUrl is not null))
{
newItem = await questItem.AddClosingPR(questClient, ghIssue.ClosingPRUrl) ?? newItem;
}
return newItem;
}

static private int ParentIdFromIssue(IEnumerable<ParentForLabel> parentNodes, QuestIssueOrPullRequest ghIssue, int defaultParentNode, IEnumerable<QuestIteration> allIterations)
{
var iteration = ghIssue.LatestStoryPointSize()?.ProjectIteration(allIterations);

foreach (ParentForLabel pair in parentNodes)
{
if (ghIssue.Labels.Any(l => l.Name == pair.Label) || (pair.Label is null))
{
if ((pair.Semester is null) || (iteration?.IsInSemester(pair.Semester) is true))
{
return pair.ParentNodeId;
}
}
}
return defaultParentNode;
}


/// <summary>
/// Construct a work item from the JSON document.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion actions/sequester/Quest2GitHub/Options/ImportOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Quest2GitHub.Options;

public record struct ParentForLabel(string Label, int ParentNodeId);
public record struct ParentForLabel(string? Label, string? Semester, int ParentNodeId);

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

Expand Down
Loading

0 comments on commit 22d0f23

Please sign in to comment.