-
-
Notifications
You must be signed in to change notification settings - Fork 373
Add patching for ClosedDate when migrating work items #2991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -876,6 +876,7 @@ private WorkItemData ReplayRevisions(List<RevisionItem> revisionsToMigrate, Work | |||||
targetWorkItem.ToWorkItem().History = history.ToString(); | ||||||
} | ||||||
targetWorkItem.SaveToAzureDevOps(); | ||||||
PatchClosedDate(sourceWorkItem, targetWorkItem); | ||||||
|
||||||
CommonTools.Attachment.CleanUpAfterSave(); | ||||||
TraceWriteLine(LogEventLevel.Information, "...Saved as {TargetWorkItemId}", new Dictionary<string, object> { { "TargetWorkItemId", targetWorkItem.Id } }); | ||||||
|
@@ -961,6 +962,65 @@ private void CheckClosedDateIsValid(WorkItemData sourceWorkItem, WorkItemData ta | |||||
} | ||||||
} | ||||||
|
||||||
private void PatchClosedDate(WorkItemData sourceWorkItem, WorkItemData targetWorkItem) | ||||||
{ | ||||||
var targetItem = targetWorkItem.ToWorkItem(); | ||||||
var state = targetItem.Fields["System.State"].Value?.ToString(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will only work with Closed or Done. It will not work with a custom closed state name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for pointing that out. For now, the intention was to only support "Closed" and "Done". If we want to handle custom closed state names, could you point me to where we define or retrieve the custom field/state mappings in this project? I’d be happy to extend the implementation to use that instead of hardcoding values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its something like:
|
||||||
if (!(state == "Closed" || state == "Done")) | ||||||
Comment on lines
+967
to
+969
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The targetItem.ToWorkItem() call is repeated multiple times throughout the method. Consider storing it in a variable at the beginning of the method to improve readability and avoid redundant calls. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
{ | ||||||
return; | ||||||
} | ||||||
|
||||||
object srcClosedDate = null; | ||||||
if (sourceWorkItem.ToWorkItem().Fields.Contains("Microsoft.VSTS.Common.ClosedDate")) | ||||||
{ | ||||||
srcClosedDate = sourceWorkItem.ToWorkItem().Fields["Microsoft.VSTS.Common.ClosedDate"].Value; | ||||||
} | ||||||
if (srcClosedDate == null && sourceWorkItem.ToWorkItem().Fields.Contains("System.ClosedDate")) | ||||||
{ | ||||||
srcClosedDate = sourceWorkItem.ToWorkItem().Fields["System.ClosedDate"].Value; | ||||||
} | ||||||
if (srcClosedDate == null) | ||||||
{ | ||||||
return; | ||||||
} | ||||||
|
||||||
try | ||||||
{ | ||||||
ValidatePatTokenRequirement(); | ||||||
Uri collectionUri = Target.Options.Collection; | ||||||
string token = Target.Options.Authentication.AccessToken; | ||||||
VssConnection connection = new(collectionUri, new VssBasicCredential(string.Empty, token)); | ||||||
WorkItemTrackingHttpClient workItemTrackingClient = connection.GetClient<WorkItemTrackingHttpClient>(); | ||||||
JsonPatchDocument patchDocument = []; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] Consider using explicit constructor syntax
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
|
||||||
var closedDateFieldRef = targetItem.Fields.Contains("Microsoft.VSTS.Common.ClosedDate") | ||||||
? "Microsoft.VSTS.Common.ClosedDate" | ||||||
: (targetItem.Fields.Contains("System.ClosedDate") ? "System.ClosedDate" : null); | ||||||
|
||||||
if (closedDateFieldRef == null) | ||||||
{ | ||||||
Log.LogWarning("Cannot patch ClosedDate: no appropriate field on target."); | ||||||
return; | ||||||
} | ||||||
|
||||||
patchDocument.Add(new JsonPatchOperation | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The patch needs an additional date field set immediately after the previous operation. Without it, any subsequent revisions created after the patch will be unable to migrate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now I’m only patching ClosedDate. Could you clarify which additional date field you mean should also be set? Is it ChangedDate, StateChangeDate, or another system field? Once I know the exact field, I can update the patch accordingly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When patching a work item to fix a field (for example, If later revisions already exist, this causes a problem: those revisions may no longer be migratable because the revision history is no longer consistent. To avoid this, the patched revision must:
For example, if revision 4 is the one being corrected, the patch must produce a revision between 4 and 5, not at the end of the chain. Revision history:
Without this ordering, any future migration will fail because the timeline is broken. |
||||||
{ | ||||||
Operation = Operation.Add, | ||||||
Path = $"/fields/{closedDateFieldRef}", | ||||||
Value = srcClosedDate | ||||||
}); | ||||||
|
||||||
int id = int.Parse(targetWorkItem.Id); | ||||||
var result = workItemTrackingClient.UpdateWorkItemAsync(patchDocument, id, bypassRules: true).Result; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using .Result on async methods can cause deadlocks in certain contexts. Consider making the PatchClosedDate method async and using await, or use ConfigureAwait(false) if the method must remain synchronous. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
Log.LogInformation("Patched ClosedDate for work item {id}.", id); | ||||||
} | ||||||
catch (Exception ex) | ||||||
{ | ||||||
Log.LogWarning(ex, "Failed to patch ClosedDate for {id}.", targetWorkItem.Id); | ||||||
} | ||||||
} | ||||||
|
||||||
private bool SkipRevisionWithInvalidIterationPath(WorkItemData targetWorkItemData) | ||||||
{ | ||||||
if (!Options.SkipRevisionWithInvalidIterationPath) | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call needs the rest API and needs to be wrapped in a ProductVersion check for not being OnPremisesClassic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve tested this with TFS and confirmed it works. I’ll also run a DevOps → DevOps test to make sure the patch behaves correctly in that environment. That way we’ll cover both major product versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We support TFS 2013 which has no rest API's at all.