Moving Tasks to the Next Sprint
  • 11 Jan 2020
  • 5 Minutes to read
  • Contributors
  • Comment
  • Dark
    Light
  • PDF

Moving Tasks to the Next Sprint

  • Comment
  • Dark
    Light
  • PDF

Article Summary

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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.

image.png

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 Story

  • Do 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 smaller

  • What 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

https://developercommunity.visualstudio.com/idea/673278/can-i-setup-azure-devops-to-automatically-move-any.html

Update

I was bored so I modified the logic app so it does the condition in a better way, try this instead.

image.png

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"
                }
            }
        }
    }
}

Was this article helpful?