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()
}
}

