Looking for Confluence Data Center to Cloud Migration Tips?
Preparing
There is few thing to do before the first test migration:
- Disable mail
- Limit access
- Install Apps - Notice this is not like Data Center - You cant disable and enable them as You please....
Clean up
I do recommend to clean up first:
- Backup and delete projects - You can use Configuration Manager for Jira (CMJ)
Duplicates of objects
Ive seen that this does not function: https://confluence.atlassian.com/cloudkb/jira-cloud-migration-assistant-duplicates-and-tracking-entities-1063170066.html#:~:text=Possible%20causes%20for%20duplicates,3)%2C%20and%20so%20on.&text=One%20example%20use%20case%20is,a%20test%20instance%20in%20Server
A test - Migrate a project - Delete it - Migrate it again... Gave a lot of (migrated) fields, screens etc etc
Current recommendation seems to be a sire Reset (for Jira and Jira SErvice Management, and then a Big Bang migrate.
Dark Features
Disable Workflow migration errors
This Dark Feature will allow You to migrate Workflows:
com.atlassian.jira.migration.skip.invalid.workflow.rule
Migrate all boards and filters
https://jira.atlassian.com/browse/MIG-1668
Testing
You can test migrate project to Cloud as many times as needed.
Copy App/Speciel Fields to Text/Text Multiline field
As it is often nessesary to copy values to shadow fields, if the App has no migration or the App will not be used in cloud.
This script does not send mail to users!
Variables:
Variable | Explanation |
---|---|
BASEURL | The URI for the Jira instance |
REST | The Scriptrunner path to the REST service |
USERNAME | A username for a user with access to all issues. I use the "automation" user, and that user is a member of "automaticservices" - required for the REST |
PASSWORD | Password or token for the above user. |
FIELDMAP | 23321|29033|M - 23321 is the source field in DC, 29033 is the destination field in DC, M is for the source and destination fields are multivalue fields, the opposite of S for singlevalue. Like this S - Singlevalue: Or this M - Multivalue: |
DRYRUN | If DRYRUN="1" - no updates will be made in Jira |
EMPTYONLY | If EMPTYONLY="1" - the REST service will only select issues where the destination field is empty. This will be faster, but values that are updated will not be copied again. |
SKIPINDEX | If SKIPINDEX="1" - the Issue will not be reindexed, so f used - make sure to reindex Jira after the scriptrun. |
DAYSBACK | Will add to the JQL: and updated > -<DAYSBACK>d So, after a full run, a more partial run can be done. |
The script will loop all issuetypes for all projects and all field mappings and call a REST
PASSWORD=*******************
#!/bin/bash source password.txt IFS=$(echo -en ",") BASEURL="https://jira.server.dk" REST="/rest/scriptrunner/latest/custom/UpdateShadowFieldsForIssue" USERNAME=automation DRYRUN="" EMPTYONLY="1" SKIPINDEX="" DAYSBACK= #Loop projects curl -s -u "$USERNAME:$PASSWORD" -X GET -H 'Content-Type: application/json' $BASEURL/rest/api/2/project?maxResults=1000 > projects.json cat projects.json | jq '.[].key' | while read -r PROJECTKEY; do PROJECTKEY=$(echo $PROJECTKEY | sed 's/\"//g') #echo "$PROJECTKEY" #Loop Issuetypes for the Project curl -s -u "$USERNAME:$PASSWORD" -X GET -H 'Content-Type: application/json' $BASEURL/rest/api/2/issue/createmeta/$PROJECTKEY/issuetypes?maxResults=1000 > issuetypes.json cat issuetypes.json | jq '.values[].name' | while read -r ISSUETYPE; do ISSUETYPE=$(echo $ISSUETYPE | sed 's/\"//g') #echo $ISSUETYPE #Loop Fields declare -a FIELDMAP=("23321|29033|M" "23523|29020|S" "24523|29021|S" "23522|29022|S" "24623|29026|S" "24422|29023|S" "23726|29024|S" "23728|29034|M" "23729|29035|M" "23727|29025|S" "24920|29036|S" "24921|29037|S" "24624|29027|S" "24423|29028|S" "23320|29032|S" "24821|29031|S" "24625|29030|S" "24424|29029|S" "24922|29039|S" "24923|29038|S" "24924|29040|S" "24925|29041|S" "24926|29042|S" "25028|29043|S" "25027|29044|S") for FIELD in "${FIELDMAP[@]}" do #echo "Field $FIELD" SOURCEFIELDID=$(echo $FIELD | cut -d '|' -f 1) SHADOWFIELDID=$(echo $FIELD | cut -d '|' -f 2) TYPE=$(echo $FIELD | cut -d '|' -f 3) ISSUETYPE=$(echo $ISSUETYPE | sed 's/ /%20/g') QUERYSTRING="projectkey=$PROJECTKEY&issuetype=$ISSUETYPE&sourcefieldid=$SOURCEFIELDID&shadowfieldid=$SHADOWFIELDID&type=$TYPE&emptyonly=$EMPTYONLY&dryrun=$DRYRUN&skipindex=$SKIPINDEX&daysback=$DAYSBACK" echo "curling.... $QUERYSTRING" curl -u "$USERNAME:$PASSWORD" -X GET -H 'Content-Type: application/json' "$BASEURL$REST?$QUERYSTRING" done done done
The scriptrunner REST code:
import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.Issue import com.atlassian.jira.issue.IssueManager import com.atlassian.jira.user.util.UserUtil import com.atlassian.jira.issue.CustomFieldManager import com.atlassian.jira.issue.fields.CustomField import com.atlassian.jira.bc.issue.search.SearchService import com.atlassian.jira.issue.search.SearchProvider import com.atlassian.jira.issue.search.SearchResults import com.atlassian.jira.web.bean.PagerFilter import com.atlassian.jira.user.ApplicationUser import com.atlassian.jira.event.type.EventDispatchOption import groovy.json.JsonSlurper import com.atlassian.jira.issue.index.IssueIndexingService import com.atlassian.jira.issue.MutableIssue import com.atlassian.jira.issue.IssueManager import com.atlassian.jira.util.ImportUtils import groovy.transform.BaseScript import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate import javax.ws.rs.core.MultivaluedMap import javax.servlet.http.HttpServletRequest import javax.ws.rs.core.Response @BaseScript CustomEndpointDelegate delegate UpdateShadowFieldsForIssue(httpMethod: "GET", groups: ["automaticservices","jira-administrators"]) { MultivaluedMap queryParams -> Random random = new Random() String scriptRunIdent = Math.abs(random.nextInt() % 99999) + 1 String scriptName = "UpdateShadowFieldsForIssue.groovy" String projectKey = queryParams.getFirst("projectkey") as String String issueType = queryParams.getFirst("issuetype") as String String sourceFieldId = queryParams.getFirst("sourcefieldid") as String String shadowFieldId = queryParams.getFirst("shadowfieldid") as String String type = queryParams.getFirst("type") as String String emptyOnly = queryParams.getFirst("emptyonly") as String String dryRun = queryParams.getFirst("dryrun") as String String skipIndex = queryParams.getFirst("skipindex") as String String daysBack = queryParams.getFirst("daysback") as String Integer currentNumber = 0 def builder = new groovy.json.JsonBuilder() String jql="Project=" + projectKey + " and issuetype='" + issueType + "' and cf[" + sourceFieldId + "] is not empty" if (emptyOnly == "1") { jql=jql + " and cf[" + shadowFieldId + "] is empty" } if (daysBack != "") { jql=jql + " and updated > -" + daysBack + "d" } log.info "Script=" + scriptName + " ScriptRunIdent=" + scriptRunIdent + " Message='JQL: " + jql + "'" ApplicationUser currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser() CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager() IssueManager issueManager = ComponentAccessor.getIssueManager() def issueIndexingService = ComponentAccessor.getComponent(IssueIndexingService) CustomField sourceField = customFieldManager.getCustomFieldObject("customfield_" + sourceFieldId) String jiraSourceFieldType =sourceField.getCustomFieldType().getName() CustomField shadowField = customFieldManager.getCustomFieldObject("customfield_" + shadowFieldId) String jiraShadowFieldType = shadowField.getCustomFieldType().getName() log.info "Script=" + scriptName + " ScriptRunIdent=" + scriptRunIdent + " Message='Source field Jira type: " + jiraSourceFieldType + "'" log.info "Script=" + scriptName + " ScriptRunIdent=" + scriptRunIdent + " Message='Shadow field Jira type: " + jiraShadowFieldType + "'" if (jiraShadowFieldType == "Text Field (single line)" || jiraShadowFieldType == "Text Field (multi-line)") { SearchService searchService = ComponentAccessor.getComponent(SearchService.class) SearchService.ParseResult parseResult = searchService.parseQuery(currentUser, jql) if (parseResult.isValid()) { SearchResults results = searchService.search(currentUser, parseResult.getQuery(), PagerFilter.getUnlimitedFilter()) final List issues = results?.results totalIssues = issues.size() issues.each { theIssue -> log.info "Script=" + scriptName + " ScriptRunIdent=" + scriptRunIdent + " Message='Value " + theIssue.getCustomFieldValue(sourceField) + "'" //Extract value(s) here from Source field. Its possible to expand for another 'jiraSourceFieldType' if (jiraSourceFieldType.contains("Elements Connect")) { sourceFieldValue = extractElementsValues(theIssue.getCustomFieldValue(sourceField),type) } else { sourceFieldValue = theIssue.getCustomFieldValue(sourceField) } shadowFieldsValue = theIssue.getCustomFieldValue(shadowField) if (sourceFieldValue != shadowFieldsValue) { currentNumber = currentNumber + 1 if (dryRun != "1") { log.info "Script=" + scriptName + " IssueKey=" + theIssue.getKey() + " ScriptRunIdent=" + scriptRunIdent + " Message='Copying " + sourceFieldId + " to " + shadowFieldId + ", value: " + sourceFieldValue + "'" MutableIssue mIssue = issueManager.getIssueByCurrentKey(theIssue.getKey()) try { mIssue.setCustomFieldValue(shadowField,sourceFieldValue) ComponentAccessor.getIssueManager().updateIssue(currentUser, mIssue, EventDispatchOption.DO_NOT_DISPATCH, false) } catch (Exception ex) { log.info "Script=" + scriptName + " IssueKey=" + theIssue.getKey() + " ScriptRunIdent=" + scriptRunIdent + " Error='Error Updating: " + ex.getMessage() + "'" } //Reindex Issue if (skipIndex != "1") { boolean wasIndexing = ImportUtils.isIndexIssues() ImportUtils.setIndexIssues(true) issueIndexingService.reIndex(mIssue) ImportUtils.setIndexIssues(wasIndexing) } } else { log.info "Script=" + scriptName + " IssueKey=" + theIssue.getKey() + " ScriptRunIdent=" + scriptRunIdent + " Message='DryRun - Copying " + sourceFieldId + " to " + shadowFieldId + ", value: " + sourceFieldValue + "'" } } else { log.info "Script=" + scriptName + " IssueKey=" + theIssue.getKey() + " ScriptRunIdent=" + scriptRunIdent + " Message='Skipped copy due to identical values'" } } if (dryRun != "1") { if (currentNumber > 0) { builder {success "Fields copied successfully. Copied " + currentNumber + " field(s)."} } else { builder {success "No Fields copied."} } } else { builder {success "Dry Run. Woulf have copied " + currentNumber + " field(s)."} } Response.status(200).entity(builder.toString()).build() } else { builder {error "Not valid JQL"} Response.status(500).entity(builder.toString()).build() } } else { builder {error "Shadow field is not a Text Field."} Response.status(500).entity(builder.toString()).build() } } def extractElementsValues(theValue,fieldType) { if (theValue == null) { return null } def slurper = new JsonSlurper().parseText(theValue) if (fieldType == "M") { slurper.keys.join("\n").toString() } else { slurper.keys[0].toString() } }