Восстановление связей задач из истории после случайного удаления типа связи из админки

При импорте проектов с использованием project configurator могут создаваться дубли типов линков. Это вызывает проблемы с расширенным JQL поиском. Например функция linkedIssuesOf перестает работать с такой ошибкой: photo_2021-12-04_20-11-10

При случайном удалении из админки не приехавшего линка, а того который был в jira изначально, все связи сделанные в задачах с этим линком так же удаляются из задач. Чтобы восстановить такие связи можно использовать скрипт приведенный в листинге ниже.

При удалении линка об этом делается запись в историю каждой задачи - за это и зацепимся. Хоть затрагивается и большое количество задач и история линковок может содержать не относящиеся к удалению типы линков, окно записей при удалении линка в историю небольшое, зададим в JQL все затронутые задачи а все остальные линковки фильтранём по признаку непопадания в окно задав startTS и endTS. Так же учтем направление линковки и можно запускать скрипт.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.MutableIssue
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.bc.issue.link.IssueLinkService
import com.atlassian.jira.bc.issue.link.IssueLinkService.IssueLinkValidationResult
import com.atlassian.jira.util.ErrorCollection

def issueManager = ComponentAccessor.getIssueManager()
def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

String ALL_TASK_JQL = '''issue IN updatedBy("userwhodeletedtype") and updated >= "2021/12/03 00:20")'''

def runJQL(String jql, ApplicationUser user) {
    def issuesSearch
    def searchService = ComponentAccessor.getComponent(SearchService.class)
    def issueManager = ComponentAccessor.getIssueManager()
    def parseJQLResult = searchService.parseQuery(user, jql)
    if (parseJQLResult.isValid()) {
        def result = searchService.search(user, parseJQLResult.getQuery(), PagerFilter.getUnlimitedFilter())
        def documentIssues = result.results
        return documentIssues.collect { issueManager.getIssueObject(it.id) }
    } else {
        log.error("Invalid JQL: " + jql);
        log.debug("SendRFCReportToConfluenceSRV out")
    }
    return issuesSearch
}

Timestamp startTS = new Timestamp(1638480000000L)
Timestamp endTS = new Timestamp(1638481800000L)

def arrayIssues = runJQL(ALL_TASK_JQL, currentUser) as List<Issue>

for (issue in arrayIssues) {
    def thisIssueIsBlockedBy = ComponentAccessor.changeHistoryManager.getChangeItemsForField(issue, "Link")
                               .findAll{it.created > startTS && it.created < endTS && it.getFromString()?.contains("This issue is blocked by ") }
                               ?.collect{it?.from}?.collect{issueManager.getIssueObject(it)}
    if (thisIssueIsBlockedBy) thisIssueIsBlockedBy.collect{createLink(it, issue, 11000L, 0, currentUser)}
    def thisIssueIsBlocks = ComponentAccessor.changeHistoryManager.getChangeItemsForField(issue, "Link")
                            .findAll{it.created > startTS && it.created < endTS && it.getFromString()?.contains("This issue blocks ")}
                            ?.collect{it?.from}?.collect{issueManager.getIssueObject(it)}
    if (thisIssueIsBlocks) thisIssueIsBlocks.collect{createLink(issue, it, 11000L, 0, currentUser)}
}

void createLink(Issue fromIssue, Issue toIssue, Long issueLinkTypeId, Long sequence, ApplicationUser user) {
    def issueLinkManager = ComponentAccessor.getIssueLinkManager()
    issueLinkManager.createIssueLink(fromIssue.id, toIssue.id, issueLinkTypeId, sequence, user)
}