How to restore Epic Link to Issues?

Yair Spolter
Contributor
August 19, 2018

I accidentally changed an Epic, which was linked in thousands of issues, to a Task. This un-linked all of the Issues. Yes, major face-palm, I know.

Is there any way to restore the Epic Link to all of the Issues that previously had them?

4 answers

1 accepted

4 votes
Answer accepted
Maarten Cautreels
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 19, 2018

Hi @Yair Spolter

I was able to reproduce the issue (obviously) but I don't really have good news. I couldn't find an easy way to restore the issues linked to that epic. However you can view the issues that were linked to the Epic in the history. Thus there's a way to figure our which issues were linked.

  1. Simply open the Epic (that was accidentally changed to a task)
  2. On the bottom of the issue open the History tab:
    Screenshot 2018-08-19 at 17.11.05.jpg
  3. I was able to copy and paste this list as a table out of the history. Pasted it into a Google sheet and converted it to a JQL function (using some Sheet formula magic). See the sheet here.
  4. You can then use the JQL function to query all of the previously linked issues and use the Bulk Edit feature in Jira to add them to the Epic again.

Hope this helps.

Best,

Maarten

Yair Spolter
Contributor
August 19, 2018

@Maarten Cautreels that was pure AWESOMENESS!

I can't thank you enough!

It was truly... Epic! (sorry, I could not resist).

Maarten Cautreels
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 19, 2018

@Yair Spolter Happy to help! :-)

0 votes
Joel Santos June 23, 2022

As the doc that @Maarten Cautreels linked is now offline, if someone stumbles upon this, my guess is that he suggested:

  1. copying the list of keys from the issues that got de-linked
  2. with some trickery, get a search query in JQL
  3. Execute that search query in the "issue" view of the project
  4. Run "Bulk Change" on the de-linked tasks
  5. Link all the tasks with the corresponding epic

If you only have one epic, is not difficult to do by hand. If your task keys are ABC-1, ABC-2 and ABC3 and your epic is ABC-4, search for

project = "ABC" and (key="ABC-1" or key="ABC-2" or key="ABC-3") ORDER BY created DESC

And run the bulk change on them.

 

In my case, I delinked ALL my epics (I changed their type to a custom "epic", only to discover that there's no such thing). With ~70 epics I needed more automation, so I wrote a script. Is super-tailored to what I needed, so I'll leave it here and then it's up to you to modify it accordingly. 

My workflow consisted in copying the history of an Epic into a text file called "History.txt" straight from my browser. Then the script will look for changes made by the specified user (as my mistake was caused by an automation, the user was "Automation for Jira"). The script will output the JQL query needed for Bulk Changing. As that was going to be a lot of work, I looked around and found GitHub - ankitpokhrel/jira-cli: 🔥 [WIP] Feature-rich interactive Jira command line., a Jira CLI interface. My script also will build the command that needs to be executed to link a number of issues to an epic, but if you don't want that, just comment the lines. It's based on Go, so you might need that too, or if you don't want to go through that hassle, use the JQL query.

To run it, for example, to search for changes made by "Automation by Jira" in the epic's history that is pasted on "History.txt", of an Epic with key "ABC-4" on a project with acronym "ABC", I would call it like this

python3 JiraLinker.py -p ABC -t History.txt -u "Automation for Jira" -e ABC-4
The script: JiraLinker.py PS: looks like this garbage doesn't like big code sections, and renders them as normal text!
****************** START OF THE SCRIPT ***************************
from pathlib import Path
from typing import Any
from re import findall
from enum import Enum
from subprocess import Popen, STDOUT, PIPE
class Lvl(str, Enum):
    """Debug log levels

    The different logging levels and the colours associated to the output
    """
    ERROR = "\x1b[0;31;22m"  # RED + normal mode
    OK = "\x1b[0;32;22m"  # GREEN
    WARNING = "\x1b[0;33;22m"  # YELLOW
    # YELLOW, but I can use in temporary debugs and then delete
    DEBUG = "\x1b[0;33;22m"
    INFO = "\x1b[0;36;22m"  # CYAN
    NORMAL = "\x1b[0;37;22m"  # NORMAL white text
    RESET = "\x1b[0m"       # Reset


def debugPrint(level: Lvl, msg: Any) -> None:
    """Prints coloured text

    Args:
        level: a member of the class lvl, that indicates the level of the messsage
        msg: a string to be printed with the selected colour
    """
    print(f"{level}" + f"{msg}" + f"{Lvl.RESET}")

def buildQuery(listOfIssues: list[str], projectAcronym: str) -> str:
    searchQuery: str = 'project = "' + projectAcronym + '" and ('
    for key in listOfIssues[:-1]:
        searchQuery += 'key="' + f'{key}' + '" or '
    searchQuery += 'key="' + listOfIssues[-1] + '") ORDER BY created DESC'
    return searchQuery


# Parse CLI arguments
parser = ArgumentParser()
parser.add_argument("-p", "--projectAcronym",
                    help="The acronym of the project containing the issues", type=str, required=True)
parser.add_argument("-t", "--history",
                    help="Path to the file that contains the pasted history of the epic", type=str, required=True)
parser.add_argument("-e", "--epicKey",
                    help="The issue number or key of the Epic that has had its children removed",
                    type=str, required=True)
parser.add_argument("-u", "--user",
                    help="The user that created all the havoc!",
                    type=str, required=True)

args = parser.parse_args()

filename: Path = Path(args.history)
project: str = args.projectAcronym
epicKey: str = args.epicKey
user: str = args.user

try:
    with open(filename, encoding="utf-8") as f:
        history = f.read()
        # This will need to be changed in order to match your case
        childs = findall(rf'({user})(\n )(updated the Epic Child)([\s\w\d,:]*)({project}-[\d]+)(\n)(None)', history)
        if childs:
            issueNumbers = [child[4] for child in childs]
            issueNumbers.sort()
            for child in childs:
                debugPrint(Lvl.OK, child[0] + " " + child [2] + " " + child[4] + " -> " + child[6])
            jqlQuery = buildQuery(issueNumbers, project)
            debugPrint(Lvl.OK, "JQL Search Query (for bulk editing)")
            debugPrint(Lvl.INFO, jqlQuery)

            jiraCliCommand = "jira epic add " + str(epicKey) + " " + " ".join(issueNumbers)
            debugPrint(Lvl.OK, "Jira CLI Command)")
            debugPrint(Lvl.INFO, jiraCliCommand)
            try:
                # Run the following command, redirecting stdin and stdout to be able to send input and display the output of the
                # command. Stderr is redirected to sdtout so it's displayed in context. Universal Newlines to true makes
                # Popen return text, instead of binary strings
                # Redirecting stdin allows us to send an enter, becuase the batch file ends in a "press ENTER to continue..."
                process = Popen(jiraCliCommand.split(), stdin=PIPE,
                                stdout=PIPE, stderr=STDOUT, universal_newlines=True)
                # Send the enter the script is waiting for to end execution
                process.communicate("\r\n")
                debugPrint(Lvl.INFO, f"Output of '{jiraCliCommand}'")
                # Print the output of the command. communicate() returns a string tuple with the stdout and stderr, although
                # stderr is redirected to stdout already, and will be empty
                (stdout, _) = process.communicate()
                if stdout:
                    debugPrint(Lvl.INFO, stdout)
                # If the command returns other than 0, exit
                if (process.returncode != 0):
                    exit(1)
            except FileNotFoundError as err:
                debugPrint(Lvl.ERROR, 'ERROR: ' + str(err))

        else:
            debugPrint(Lvl.WARNING, "No Epic Child updates found in the history")

except FileNotFoundError:
    debugPrint(Lvl.ERROR, "File not found?")
    raise
****************** END OF THE SCRIPT ***************************
I hope somebody founds this useful
0 votes
Aldo Botello Téllez
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
January 18, 2022

Does anyone have the google doc tamplate ? 

Joel Santos June 23, 2022

You probably won't need this anymore, but check my answer in case you still do

0 votes
Maarten Cautreels
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 19, 2018

Hi Yair,

Just so we are looking in the right direction a few questions:

  • Are you on Jira Server or Cloud? 
  • I'm also assuming (from the Epic Link) that you're using Jira Software instead of just Core? (Do you have Kanban/Scrum boards available on your Jira?)
  • How/what did you change on the Epic?
  • What kind of link do you want to restore? The issues being linked to the Epic through the Epic Link field or another type of link (relates to, is blocked by, ...) to this Epic?

I'd love to help search for an easy restore solution but I'm afraid restoring a backup will most likely be the easiest way.

Best,

Maarten

Yair Spolter
Contributor
August 19, 2018

Thanks for your help Maarten.

We are on Jira server.

We do have Kanban etc.

I changed the "Type" from Epic to Task

I want to restore the the issues being linked to the Epic through the Epic Link field.

Restoring a backup means going back to a previous state (which affects ALL changeds made since)?

Maarten Cautreels
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
August 19, 2018

Thanks for the info and quick response. I'm going to try and reproduce this on my test instance to see if I can find an easy way to restore it.

Best,

Maarten

Yair Spolter
Contributor
August 19, 2018

Thank you. That would be great!

Suggest an answer

Log in or Sign up to answer