- Print
- Comment
- DarkLight
- PDF
Moving Tasks to the Next Sprint
We work alot with Azure DevOps work items and one of the things that frustrates me is that when you get to the end of a sprint and move stories which are not complete to the next sprint you also need to move all of the tasks associated with the story to the next sprint too.
You may be in the situation where some of the tasks are complete and some need to be rolled over. Some people are really good at planning and may have planned tasks in different future sprints but the most common scenario I tend to see is that the stories are put into a sprint and as best as possible they are complete and then the ones outstanding are moved to the next sprint then someone also needs to move all of the tasks too.
I wanted to see if I could make a Logic App which would do this for me.
The below picture shows my task board with a user story in iteration 2 and it has some tasks associated with it.
Trigger Event
In the Logic App I used the Azure DevOps trigger which will fire when a work item is updated. Remember this will fire for every work item.
Below shows the trigger configured and I also put a condition in so that if the work item is not a user story I will ignore the event.
Get Child Work Items
Once I get an event for a user story the next thing I want to do is get all of the child work items of the one the event relates to. Below shows me doing this. I also ask for only the work items which are tasks.
Checking if the Child Work Item is of interest
In the next step I put a check of the work item type. To be honest its just now while writing the blog that ive noticed that i have 2 places ive specified tasks (1 in the condition and 1 where i asked for only tasks in the get children action). You could maybe remove this step of possibly enhance it if you need to consider other types of work item.
I am also checking the status of the task. If its removed or closed I want to leave it alone but for any other status id like to move it to the next iteration.
Has the Iteration Path Changed?
Next up I have a check to see if the iteration path has changed. If the task and user story have the same iteration path then I will ignore the event. I only want to do the update if the user story has changed to a different iteration.
Updating the User Story
To test this out I then go back to devops and change the user story so the iteration path is moved to iteration 3 which is the next sprint.
I then wait a short time (depending on your configuration in logic apps and for the event to fire from devops) and if I move the board to Iteration 3 I will see my user story then it will auto refresh and my tasks will appear.
Things to Watch for and think about
If you are going to try this out, below are a few things to consider:
Watch out for a recursive loop
Remember in this logic app you are triggering on a work item being updated and then going and updating other work items. These updates will cause another event to be triggered. In the logic app I am checking the type of work item the event relates to and am ignoring the event unless it is of type User StoryDo you only need tasks?
It might be that your board includes bugs and other stuff. In that case you may choose to extend the logic app to include moving other types of work items.Make a better condition
I just did this as a demo quick and dirty, thinking back I am sure I could probably have done a more complex condition which would have got rid of a few of the condition shapes making the logic app smallerWhat type of project have you got
In DevOps there are different types of process and work items. You might need to check the names for the work item types and statuses for your own instance when building your logic app if they are different.
Conclusion
The key thing here is that my proof of concept proves I can automate something that was annoying me in Azure Devops and this will do until the product includes this feature automatically. It seems this feature is not yet there according to this thread
Update
I was bored so I modified the logic app so it does the condition in a better way, try this instead.
Logic App Code
If you want a quick start to trying this out, below you will find the json from my logic app which you can modify as needed. Please note I have changed anything sensitive with the [Removed] tag.
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Condition_-_Is_User_Story": {
"actions": {
"For_each_2": {
"actions": {
"Condition_-_Should_Update_Work_Item": {
"actions": {
"Update_a_work_item_-_Task_iteration_path_moved_to_match_user_story": {
"inputs": {
"body": {
"iteration": "@triggerBody()?['fields']?['System_IterationPath']"
},
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices_1']['connectionId']"
}
},
"method": "patch",
"path": "/_apis/wit/workitems/@{encodeURIComponent(items('For_each_2')?['System.Id'])}",
"queries": {
"account": "[Replaced]"
}
},
"runAfter": {},
"type": "ApiConnection"
}
},
"expression": {
"and": [
{
"equals": [
"@items('For_each_2')?['System.WorkItemType']",
"Task"
]
},
{
"not": {
"equals": [
"@items('For_each_2')?['System.State']",
"Closed"
]
}
},
{
"not": {
"equals": [
"@items('For_each_2')?['System.State']",
"Removed"
]
}
},
{
"not": {
"equals": [
"@items('For_each_2')?['System.IterationPath']",
"@triggerBody()?['fields']?['System_IterationPath']"
]
}
}
]
},
"runAfter": {},
"type": "If"
}
},
"foreach": "@body('Get_work_item_children')?['value']",
"runAfter": {
"Get_work_item_children": [
"Succeeded"
]
},
"type": "Foreach"
},
"Get_work_item_children": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices_1']['connectionId']"
}
},
"method": "get",
"path": "/_apis/wit/workitems/@{encodeURIComponent(triggerBody()?['id'])}/children",
"queries": {
"account": "[Replaced]s",
"project": "[Replaced]",
"workItemType": "Task"
}
},
"runAfter": {},
"type": "ApiConnection"
}
},
"expression": {
"and": [
{
"equals": [
"@triggerBody()?['fields']?['System_WorkItemType']",
"User Story"
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"When_a_work_item_is_updated": {
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices_1']['connectionId']"
}
},
"method": "get",
"path": "/v2/workitemupdated_trigger/@{encodeURIComponent('[Replaced]')}/_apis/wit/wiql",
"queries": {
"account": "[Replaced]",
"areaPathComparison": "Equals",
"iterationPathComparison": "Equals"
}
},
"recurrence": {
"frequency": "Minute",
"interval": 3
},
"splitOn": "@triggerBody()?['value']",
"type": "ApiConnection"
}
}
},
"parameters": {
"$connections": {
"value": {
"visualstudioteamservices_1": {
"connectionId": "/subscriptions/[Replaced]/resourceGroups/[Replaced]/providers/Microsoft.Web/connections/visualstudioteamservices-3",
"connectionName": "visualstudioteamservices-3",
"id": "/subscriptions/[Replaced]/providers/Microsoft.Web/locations/northeurope/managedApis/visualstudioteamservices"
}
}
}
}
}