Миграция из jiracloud в существующий инстанс jiradc (обновленная версия)

Миграция будет проходить по статье https://confluence.atlassian.com/adminjiraserver/migrating-from-jira-cloud-to-data-center-applications-938846963.html Запускаем бэкап. Обращаем внимание что начиная с 30 ноября 2024, такой простоты не будет и придется мучить csv. project_configurator_2024-08-03_22-18 Скачиваем бэкап на развернутую jiradc по пути /var/atlassian/application-data/shared-home/import Он оказался размером 140GB, поэтому обращаем внимание на следующее:

If your backup is 2GB or more, import the attachments separately

Это говорит о том что нужно сделать unzip backup.zip

Затем zip newbackup.zip entities.xml activeobjects.xml

А папку attachments из папки data отправить по пути home-directory/data/attachments mv attachments ../../../data

После развертывания бэкапа столнулся с тем что не открывался workflow с ошибкой Error parsing workflow XML: Attribute "ruleKey" must be declared for element type "validator". Please see log for more details.

Решение описано здесь: https://confluence.atlassian.com/jirakb/attribute-rulekey-must-be-declared-for-element-type-validator-1354203361.html чтож нужно просто пересохранить wf в клауде и проделать все предыдущие операции заново.

Прогулявшись по админке увидел что не открываются кастомфилды. Посмотрел логи и увидел сдледующее: Caused by: java.lang.IllegalStateException: Duplicate key customfield_10066 (attempted merging values com.atlassian.jira.issue.fields.screen.FieldScreenLayoutItemImpl@6a1ada31 and com.atlassian.jira.issue.fields.screen.FieldScreenLayoutItemImpl@6a1add97)

Точно помню что уже решал такую проблему, но на момент текущего подхода на странице атласовской доки с bulk resolution было 504 поэтому пришлось доставать из вэбархива. Так же добавил эту статью себе с переводом. Если коротко то все решается вот таким запросом:

delete from fieldscreenlayoutitem where id in (select max(i.id) as deleteme
from fieldscreen f, fieldscreenlayoutitem i, fieldscreentab t
where f.id = t.fieldscreen
and i.fieldscreentab = t.id
group by f.name, i.fieldidentifier having count(*) > 1);

Ставим Project Configurator и при попытке экспортировать проект ловим такую ошибку:

Some errors occurred:

Found custom field with values for issues in exported project(s), with id: 10084, name: Issue color and type: com.pyxis.greenhopper.jira:jsw-issue-color. This field does not exist. Check if its type is defined in a plugin that has been disabled.

Found custom field with values for issues in exported project(s), with id: 10013, name: [CHART] Time in Status and type: com.atlassian.jira.ext.charting:timeinstatus. This field does not exist. Check if its type is defined in a plugin that has been disabled.

Решается так:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.ofbiz.OfBizDelegator
import groovy.sql.GroovyRowResult
import groovy.sql.Sql
import org.ofbiz.core.entity.ConnectionFactory
import org.ofbiz.core.entity.DelegatorInterface

import java.sql.Connection

//select("""select * from customfieldvalue where customfield = 10013;""")
//update("""DELETE FROM customfieldvalue WHERE customfield = 10013;""")
//select("""select * from customfieldvalue where customfield = 10084;""")
//update("""DELETE FROM customfieldvalue WHERE customfield = 10084;""")
//select("""select * from customfield where id = 10013;""")
//update("""DELETE FROM customfield where id = 10013;""")
//select("""select * from customfield where id = 10084;""")
update("""DELETE FROM customfield where id = 10084;""")
//update("delete/insert ...")
def select(String query) {
    OfBizDelegator delegator = ComponentAccessor.getOfBizDelegator();
    DelegatorInterface delegatorInterface = delegator.getDelegatorInterface();
    String helperName = delegatorInterface.getGroupHelperName("default");
    Connection connection = ConnectionFactory.getConnection(helperName);
    Sql sql = new Sql(connection);

    List<GroovyRowResult> resultRows = []
    try {
        resultRows.addAll(sql.rows(query));
    } finally {
        //sql.close()
        connection.close()
    }

    return resultRows
}

def update(String query) {
    OfBizDelegator delegator = ComponentAccessor.getOfBizDelegator();
    DelegatorInterface delegatorInterface = delegator.getDelegatorInterface();
    String helperName = delegatorInterface.getGroupHelperName("default");
    Connection connection = ConnectionFactory.getConnection(helperName);
    Sql sql = new Sql(connection);
    def numberOfUpdated
    try {
        numberOfUpdated = sql.executeUpdate(query);
    } finally {
        //sql.close()
        connection.close()
    }
    return numberOfUpdated
}

И сразу пофиксим

[In [Resolution Declined] ] Found two items with the same external identification 'Declined' and with ids: 10006 and 10005

Простым удалением из списка резолюции с более поздним ID

После этого проект успешно экспортировался но при импорте наблюдается следующая ошибка:

com.atlassian.jira.exception.ParseException: The entityId of entityName '835006' is missing at com.atlassian.jira.imports.project.parser.EntityPropertyParserImpl.verifyRequiredAttribute(EntityPropertyParserImpl.java:43) at com.atlassian.jira.imports.project.parser.EntityPropertyParserImpl.parse(EntityPropertyParserImpl.java:28) at com.atlassian.jira.imports.project.handler.IssueRelatedEntitiesPartitionHandler.handleEntityProperty(IssueRelatedEntitiesPartitionHandler.java:155)

Это фиксится простым удалением из entities.xml битой строки 2345-40f1-c358-c465

Далее столнулся со следующей ошибкой:

2024-08-04 19:05:38,157+0300 JiraTaskExecutionThread-12 ERROR admin_localhost 1141x6519x1 2u3go4 0:0:0:0:0:0:0:1 /secure/complete-project-import!resumeImportFromSimulation.jspa ...[c.a.j.imports.xml.DefaultBackupXmlParser] Unexpected import failure java.lang.NullPointerException at com.atlassian.greenhopper.imports.LexorankPreImportHandler.addFieldMapping(LexorankPreImportHandler.java:87) at com.atlassian.greenhopper.imports.LexorankPreImportHandler.handleEntity(LexorankPreImportHandler.java:73) at com.atlassian.jira.imports.project.ao.handler.ChainedAoSaxHandler.endRow(ChainedAoSaxHandler.java:289) at com.atlassian.jira.imports.project.ao.handler.ChainedAoSaxHandler.endElement(ChainedAoSaxHandler.java:193) at com.atlassian.jira.imports.project.ao.handler.ChainedAoSaxHandler.endElement(ChainedAoSaxHandler.java:151) at org.apache.xerces.parsers.AbstractSAXParser.endElement(Unknown Source) at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl.scanEndElement(Unknown Source) at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl$FragmentContentDispatcher.dispatch(Unknown Source) at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source) at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source) at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source) at org.apache.xerces.parsers.XMLParser.parse(Unknown Source)

Решается так:

DELETE FROM "AO_60DB71_LEXORANK";
SELECT id FROM customfield WHERE customfieldtypekey = 'com.pyxis.greenhopper.jira:gh-lexo-rank';
INSERT INTO "AO_60DB71_LEXORANK" ("ID", "FIELD_ID", "ISSUE_ID", "RANK", "TYPE") VALUES (DEFAULT, 10011, -9223372036854775808, '0|000000:', 0);
INSERT INTO "AO_60DB71_LEXORANK" ("ID", "FIELD_ID", "ISSUE_ID", "RANK", "TYPE") VALUES (DEFAULT, 10011, 9223372036854775807, '0|zzzzzz:', 2);

После этого экспортнул ошибка та же, ребутнул та же, реиндекснул та же. Забыл что в начале нужно было делать DELETE FROM "AO_60DB71_LEXORANK"; Решается по этой доке: https://confluence.atlassian.com/jirakb/project-import-of-software-project-fails-with-unexpected-import-failure-803603791.html

Затем нужно снова делать экспорт, и удалять из него строку с entityId '835006' В итоге пофиксилось:

select("""select * from ENTITY_PROPERTY where id = 835006;""")

update("""DELETE from ENTITY_PROPERTY where id = 835006;""")

Попробовал экспортировать сразу 10 проектов и столкнулся со следующей ошибкой:

Some errors occurred:

[Board SCRUM@@ABPLATFORM board@@6358ebcbd66d8108a123b7c7@@Filter for ABPLATFORM board] [Board SCRUM@@ABPLATFORM board@@6358ebcbd66d8108a123b7c7@@Filter for ABPLATFORM board] Trying to export Board found problem: Could not find object: User with key 557058:64d63ec4-f434-47e8-896d-8d4670c131df

[Board SCRUM@@AN board@@rk:821a1326-e179-4fba-bc79-b674adbe8dd9@@Filter for AN board] [Board SCRUM@@AN board@@rk:821a1326-e179-4fba-bc79-b674adbe8dd9@@Filter for AN board] Trying to export Board found problem: Could not find object: Field with id: customfield_10027 used as board statistic

[Board SCRUM@@(to delete) LUTP board@@ug:01cb188b-b27f-464e-9eb0-826197a33f7b@@Filter for LUTPS board] [Board SCRUM@@(to delete) LUTP board@@ug:01cb188b-b27f-464e-9eb0-826197a33f7b@@Filter for LUTPS board] Trying to export Board found problem: Could not find object: User with key 6152d955048360006ae4efa3

[Board KANBAN@@(to delete) 5-11 LUTP kanban@@634d50bed66d8108a1211988@@Filter for 5-11 LUTP kanban] [Board KANBAN@@(to delete) 5-11 LUTP kanban@@634d50bed66d8108a1211988@@Filter for 5-11 LUTP kanban] Trying to export Board found problem: Could not find object: User with key 6152d955048360006ae4efa3

At Most frequent errors found in export you can find advice to fix these problems

Здесь просто запускаем второй раз экспорт без досок, фильтров и дэшбордов.

И следующая ошибка:

Found custom field with values for issues in exported project(s), with id: 10368, name: Checklist Text (view-only) and type: com.atlassian.plugins.atlassian-connect-plugin:com.herocoders.plugins.jira.issuechecklist-free__checklist-text-view-only. This field does not exist. Check if its type is defined in a plugin that has been disabled.

Здесь кажется ругается на чеклист. Можно забэкапить данные в json а потом удалить:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.ofbiz.OfBizDelegator
import groovy.sql.GroovyRowResult
import groovy.sql.Sql
import org.ofbiz.core.entity.ConnectionFactory
import org.ofbiz.core.entity.DelegatorInterface
import groovy.json.*
import java.sql.Connection

def checklist = select("""SELECT (jp.pkey || '-' || ji.issuenum) AS issuekey, ji.*
FROM project jp
JOIN jiraissue ji ON jp.id = ji.project
WHERE ji.id in (select issue from customfieldvalue where customfield = 10368);""")//.issue
new JsonBuilder( checklist ).toPrettyString()
//select("""select issue from customfieldvalue where customfield = 10013;""")
//update("""DELETE FROM customfieldvalue WHERE customfield = 10013;""")
//update("delete/insert ...")
def select(String query) {
    OfBizDelegator delegator = ComponentAccessor.getOfBizDelegator();
    DelegatorInterface delegatorInterface = delegator.getDelegatorInterface();
    String helperName = delegatorInterface.getGroupHelperName("default");
    Connection connection = ConnectionFactory.getConnection(helperName);
    Sql sql = new Sql(connection);

    List<GroovyRowResult> resultRows = []
    try {
        resultRows.addAll(sql.rows(query));
    } finally {
        //sql.close()
        connection.close()
    }

    return resultRows
}

def update(String query) {
    OfBizDelegator delegator = ComponentAccessor.getOfBizDelegator();
    DelegatorInterface delegatorInterface = delegator.getDelegatorInterface();
    String helperName = delegatorInterface.getGroupHelperName("default");
    Connection connection = ConnectionFactory.getConnection(helperName);
    Sql sql = new Sql(connection);
    def numberOfUpdated
    try {
        numberOfUpdated = sql.executeUpdate(query);
    } finally {
        //sql.close()
        connection.close()
    }
    return numberOfUpdated
}

А затем нужно изменить учетки пользователей. Следующим скриптом

import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.bc.user.ApplicationUserBuilderImpl
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.util.AttachmentUtils
//import com.atlassian.jira.util.PathUtils
import org.apache.log4j.Level
import org.apache.log4j.Logger
//Logger.getLogger("com.onresolve.scriptrunner").setLevel(Level.DEBUG)

def myCSV = 'userRenames.csv' // MUST BE A TEXT FILE
def issue = ComponentAccessor.getIssueManager().getIssueObject("EM-1806") // Key for attachment issue
def aM= ComponentAccessor.getAttachmentManager()
def uM = ComponentAccessor.getUserManager()
def attachments = aM.getAttachments(issue)
def userSearchService = ComponentAccessor.getUserSearchService()

attachments.each{ a ->
    def fileName = a.filename
    log.warn fileName + ' is the user source'
// This method to get the file path is deprecated, but it works
    def filePath = AttachmentUtils.getAttachmentFile(a)
// Concatenation only works for projects which have never edited their project key
// Anyone know a way to get the original project key?
// def pathManager = ComponentAccessor.getAttachmentPathManager()
// def filePath = PathUtils.joinPaths(pathManager.attachmentPath, issue.projectObject.key, '10000', issue.key, a.id.toString())
    if (fileName.toString() == myCSV) {
        File fileContents = new File(filePath.toString())
        def mailChange = 0
        def displayNameChange = 0
        if (fileContents) {
            def lines = fileContents.readLines()
            for (item in lines) {
                def columns = item.split(",")
                def oldUserName =    columns[0]
                def newMail =        columns[2]
                def newUserName =    columns[3]
                def newDisplayName = columns[4]
// log.info "Old mail is " +oldMail + " and new mail is " +newMail // items before change

                def user = uM?.getUserByName(oldUserName) as ApplicationUser
                def userCheck = uM?.getUserByName(newMail) as ApplicationUser
                log.info user.toString()
// Excel compatible logging: item|action|result
                if ((! user) && (! userCheck)) {
                    log.info oldUserName + "|" + "No action|User does not exist" // log when no user exists
                }
                if ((userCheck) && (! user)) {
                    log.info oldUserName + "|" + "No action|Only new account " + newMail + " exists; nothing to do" // log when only new account exits
                }
                if ((userCheck) && (user)) {
                    log.info oldUserName + "|" + "Duplicate|Migration required for " + newMail // log when two accounts are present
                }

                if ((user) && (! userCheck)) {
                    // def check = uM.canRenameUser(user) // Boolean in case perms or directory type is in question
                    //log.info "Check was " +check
                    def nMail = new ApplicationUserBuilderImpl(user).emailAddress(newMail.toLowerCase()).build()
                    uM.updateUser(nMail)
                    mailChange = 1
                    log.info oldUserName + "|" + "Action|Email was uptated to " + newMail // log when user is updated
                }
            }
        }
        if (mailChange == 1) {
// Yes, you have to re-declare the user to perform a second update
            def lines = fileContents.readLines()
            for (item in lines) {
                def columns = item.split(",")
                def oldUserName =    columns[0]
                def oldMail =        columns[1]
                def newMail =        columns[2]
                def newUserName =    columns[3]
                def newDisplayName = columns[4]
                def user = uM?.getUserByName(oldUserName) as ApplicationUser
                   if (user) {
                    def nDisplayName = new ApplicationUserBuilderImpl(user).displayName(newDisplayName).build()
                    uM.updateUser(nDisplayName)
                    log.info oldMail + "|Action|Display name was updated to " + nDisplayName
                    displayNameChange = 1
                }
            }
        }
        if (displayNameChange == 1) {
            def lines = fileContents.readLines()
            for (item in lines) {
                def columns = item.split(",")
                def oldUserName =    columns[0]
                def oldMail =        columns[1]
                def newMail =        columns[2]
                def newUserName =    columns[3]
                def newDisplayName = columns[4]
                def user = uM?.getUserByName(oldUserName) as ApplicationUser
                def userCheck = uM?.getUserByName(newUserName) as ApplicationUser
                   if ((user) && (! userCheck)) {
                    def nName = new ApplicationUserBuilderImpl(user).name(newUserName.toLowerCase()).build()
                    uM.updateUser(nName)
                    log.info oldUserName + "|Action|User name was updated to " + nName
                }
            }
       }
    }
}

Так же потребовалось удалить все фильтры

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.issue.search.SearchRequestManager
import com.atlassian.jira.user.ApplicationUser

def userUtil = ComponentAccessor.getUserUtil()

SearchRequestManager searchRequestManager = ComponentAccessor.getComponent( SearchRequestManager )
def users = ComponentAccessor.userManager.getAllUsers()
def filters = users.collect{
  searchRequestManager.getAllOwnedSearchRequests(it)
}
filters.collect{it.id.collect{searchRequestManager.delete(it)}}

Следующая ошибка Data import for project with key: NLTCS Data import failed with exception: Entity com.atlassian.greenhopper.service.sprint.SprintAO does not have field CREATED_DATE java.lang.NullPointerException: Entity com.atlassian.greenhopper.service.sprint.SprintAO does not have field CREATED_DATE at java.base/java.util.Objects.requireNonNull(Unknown Source) at net.java.ao.EntityManager.create(EntityManager.java:342) at net.java.ao.EntityManager.create(EntityManager.java:402)

Решается так:

ALTER TABLE "AO_60DB71_SPRINT"
  DROP COLUMN "CREATED_DATE";

Еще одна ошибка: Data import failed with exception: Entity com.atlassian.greenhopper.service.sprint.SprintAO does not have field SPRINT_VERSION java.lang.NullPointerException: Entity com.atlassian.greenhopper.service.sprint.SprintAO does not have field SPRINT_VERSION at java.base/java.util.Objects.requireNonNull(Unknown Source) at net.java.ao.EntityManager.create(EntityManager.java:342)

update("""ALTER TABLE "AO_60DB71_SPRINT"
DROP COLUMN "SPRINT_VERSION";""")

Data import failed with exception: null java.lang.NullPointerException at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter.getProcessedCustomFieldMessageSet(JiraDataImporter.java:336) at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter.addMessagesFromMappingResult(JiraDataImporter.java:306) at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter.jiraImportData(JiraDataImporter.java:253) at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter.importData(JiraDataImporter.java:153) at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter$DataImportCallable.call(JiraDataImporter.java:410) at com.awnaba.projectconfigurator.transporter.engine.JiraDataImporter.importDataSynchronously(JiraDataImporter.java:380) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl.doDataLoadPhase(ProjectCompleteImporterImpl.java:427) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl.executeNextPhase(ProjectCompleteImporterImpl.java:316) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl.doCompleteImport(ProjectCompleteImporterImpl.java:281) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl.access$0(ProjectCompleteImporterImpl.java:274) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl$CompleteImporterTask.call(ProjectCompleteImporterImpl.java:546) at com.awnaba.projectconfigurator.operationsapi.impl.ProjectCompleteImporterImpl$CompleteImporterTask.call(ProjectCompleteImporterImpl.java:1) at com.awnaba.projectconfigurator.longrunningtasks.TaskHelperImpl$WrappingTask.safeCall(TaskHelperImpl.java:183) at com.awnaba.projectconfigurator.longrunningtasks.TaskHelperImpl$WrappingTask.call(TaskHelperImpl.java:132) at com.awnaba.projectconfigurator.longrunningtasks.TaskHelperImpl$WrappingTask.call(TaskHelperImpl.java:1) at com.atlassian.jira.task.TaskManagerImpl$TaskCallableDecorator.call(TaskManagerImpl.java:533) at com.atlassian.jira.task.TaskManagerImpl$TaskCallableDecorator.call(TaskManagerImpl.java:491) at java.base/java.util.concurrent.FutureTask.run(Unknown Source) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) at java.base/java.util.concurrent.FutureTask.run(Unknown Source) at com.atlassian.jira.task.ForkedThreadExecutor$ForkedRunnableDecorator.run(ForkedThreadExecutor.java:216) at java.base/java.lang.Thread.run(Unknown Source) https://appfire.atlassian.net/wiki/spaces/SUPPORT/pages/80884866/PCJ-Troubleshooting+a+NullPointerException+caused+due+to+a+null+text+value+for+a+custom+field+value Решается так:

 delete from customfieldvalue where id in(Select id from customfieldvalue where STRINGVALUE is null and NUMBERVALUE is null and TEXTVALUE is null and DATEVALUE is null and VALUETYPE is null);

Новая ошибка: ERRORS: The Issue Link Type 'Issue split' has style 'gh_split_issue' in the backup, but has no style in the current system. Решается так:

UPDATE issuelinktype
SET pstyle = null
WHERE id = 10212;