Template Content
Scripts
About the integration
How does the integration logic work?
Which fields are being synced?
Does it work with older Jira Server instances too?
Yes. Functionally Jira Data Center and older server instances are the same.
Can I configure which fields are being synced?
Yes. You can change which fields are being synced and control quite many other things via the configuration.
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
Template Content
Scripts
This template synchronizes Jira Data Center (or server) and Jira Cloud issues, ensuring the following data is kept in sync between the two:
Configure API connections and event listeners:
Add webhooks:
project = TEST
) to listen to specific projects.project = TEST AND labels = test AND issueType = Task
).Create a custom field:
Configure parameters:
Parameters
and configure parameters according to your Jira Cloud and Jira On-Premise projects.Create additional fields (optional):
FIELDS
section of Parameters if not needed.Customize synced fields:
FIELDS
section of Parameters.Add Epic Name field:
PurgeCache
script to clear cached data, especially after testing.ℹ️ Note: To avoid infinite update loops, updates triggered by the user who authorized the connector(s) will be ignored.
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import { createComment, getEnvVars } from '../Utils';
/**
* Entry point to Comment Created event
*
* @param event Object that holds Comment Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check that the comment was created by a different user than the one who set up the integration
if (myself.accountId !== event.comment.author?.accountId) {
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await createComment(context, event, JiraCloud, JiraOnPremise, JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentDeletedEvent } from '@sr-connect/jira-cloud/events';
import { deleteComment, getEnvVars } from '../Utils';
/**
* Entry point to Comment Deleted event
*
* @param event Object that holds Comment Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await deleteComment(context, event, JiraCloud, JiraOnPremise, JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { getEnvVars, updateComment } from '../Utils';
/**
* Entry point to Comment Updated event
*
* @param event Object that holds Comment Updated event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check that the comment was updated by a different user than the one who set up the integration
if (myself.accountId !== event.comment.updateAuthor.accountId) {
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await updateComment(context, event, JiraCloud, JiraOnPremise, JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCreatedEvent } from '@sr-connect/jira-cloud/events';
import {
checkAccountsOnJiraOnPrem,
findAssignableUserOnJiraOnPrem,
getCustomField,
getEnvVars,
getEpicNameCustomFieldFromJiraCloud,
getFieldAndMatchingValue,
getIssueLinks,
getJiraOnPremCustomFieldIdAndType,
getMatchingValue,
getScriptRunnerConnectSyncIssueKey,
handleUserFieldOptionForJiraOnPrem,
searchIssue,
setIssueLinks
} from '../Utils';
import { FieldsCreateIssue } from '@managed-api/jira-on-prem-v8-core/definitions/issue';
import { RecordStorage } from '@sr-connect/record-storage';
/**
* Entry point to Issue Created event
*
* @param event Object that holds Issue Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraCloud.Myself.getCurrentUser();
// Check if the current user does not match the person who committed the update
if (myself.accountId !== event.user.accountId) {
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY, CUSTOM_FIELD_NAME, ISSUE_TYPES, PRIORITY, IMPACT, CHANGE_REASON, CHANGE_RISK, CHANGE_TYPE, CUSTOM_FIELDS, FIELDS, STATUS } = getEnvVars(context);
// Extract the issue issue key
const eventIssueKey = event.issue.key;
// Get the ScriptRunner Connect Sync Issue Key custom field
const sourceCustomField = await getCustomField(JiraCloud, CUSTOM_FIELD_NAME);
// Add issue key to the ScriptRunner Connect Sync Issue Key custom field
await JiraCloud.Issue.editIssue({
issueIdOrKey: eventIssueKey,
body: {
fields: {
[sourceCustomField]: eventIssueKey
}
}
})
// Find the project from target instance based on pre-defined project key
const project = await JiraOnPremise.Project.getProject({
projectIdOrKey: JIRA_ON_PREM_PROJECT_KEY
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${JIRA_ON_PREM_PROJECT_KEY}`);
}
// Find the matching issue type for the other project
const issueTypeName = await getMatchingValue(JiraCloud, event.issue.fields.issuetype?.name ?? '', ISSUE_TYPES);
// Find all the issue types for given project
const issueTypes = await JiraOnPremise.Issue.Type.getTypes();
// Find the issue type to use based on pre-defined issue type name
const issueType = issueTypes.find(it => it.name === issueTypeName);
// Check if the issue type was found
if (!issueType) {
// If not, then throw an error
throw Error(`Issue Type not found in target instance: ${issueTypeName}`);
}
// Get the ScriptRunner Connect Sync Issue Key custom field from target instance
const targetCustomField = await getCustomField(JiraOnPremise, CUSTOM_FIELD_NAME);
// Fields to be updated in target instance
let requestBody: FieldsCreateIssue = {
summary: event.issue.fields.summary ?? '',
project: {
id: project.id ?? ''
},
issuetype: {
id: issueType.id ?? ''
},
[targetCustomField]: eventIssueKey,
};
// Check if issue type name is epic
if (issueTypeName === 'Epic') {
// If it is then find the epic name custom field
const epicNameCustomField = await getEpicNameCustomFieldFromJiraCloud(event.issue.fields.project?.id ?? '');
// Find custom field from Jira On-Premise
const jiraOnPremEpicNameCustomField = await getCustomField(JiraOnPremise, 'Epic Name');
// Add the Epic Name value to request body
requestBody[jiraOnPremEpicNameCustomField] = event.issue.fields[epicNameCustomField];
};
// Get integration user account on Jira On-Prem
const integrationUserOnJiraPrem = await JiraOnPremise.Myself.getCurrentUser();
// Check if field exists in FIELDS array
if (FIELDS.includes('reporter')) {
// Extract the reporter
const reporter = event.issue.fields.reporter
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const reporterUserFieldOption = await handleUserFieldOptionForJiraOnPrem(context, 'reporter', reporter?.emailAddress ?? '', reporter?.displayName ?? '', integrationUserOnJiraPrem);
// If a value is returned add it to the request body
if (reporterUserFieldOption) {
requestBody = { ...requestBody, ...reporterUserFieldOption }
}
}
// Check if field exists in FIELDS array and assignee has been assigned
if (FIELDS.includes('assignee') && event.issue.fields.assignee !== null) {
// Extract the assignee
const assignee = event.issue.fields.assignee;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const assigneeUserFieldOption = await handleUserFieldOptionForJiraOnPrem(context, 'assignee', assignee?.emailAddress ?? '', assignee?.displayName ?? '', integrationUserOnJiraPrem);
// If a value is returned add it to the request body
if (assigneeUserFieldOption) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
}
}
// Check if field exists in FIELDS array
if (FIELDS.includes('priority')) {
// Find the matching Jira Cloud priority name
const priorityName = await getMatchingValue(JiraCloud, event.issue.fields.priority?.name ?? '', PRIORITY);
// Find priorities from target instance
const priorities = await JiraOnPremise.Issue.Priority.getPriorities();
const priority = priorities.find(p => p.name === priorityName);
// Check if correct priority was found
if (!priority) {
// If not, throw an error
throw Error(`Priority not found in target instance: ${priority}`);
}
// Add priority Id to issue fields
requestBody.priority = { id: priority.id ?? '' }
}
// Check if field exists in FIELDS array and description has been added
if (FIELDS.includes('description') && event.issue.fields.description !== null) {
// Add description to request body
requestBody.description = event.issue.fields.description as string;
}
// Check if field exists in FIELDS array
if (FIELDS.includes('Impact')) {
const impactField = await getCustomField(JiraCloud, 'Impact');
const impact = event.issue.fields[impactField];
if (impact !== null) {
// Find the Impact field and matching value in target instance
const impactValues = await getFieldAndMatchingValue(JiraOnPremise, impact.value, IMPACT, 'Impact');
// Add the Impact field to request body
requestBody[impactValues.field] = {
value: impactValues.matchingValue
};
}
}
// Check if field exists in FIELDS array
if (FIELDS.includes('Change reason')) {
const changeReasonField = await getCustomField(JiraCloud, 'Change reason');
const changeReason = event.issue.fields[changeReasonField];
if (changeReason !== null) {
// Find the Change reason and matching value in target instance
const changeReasonValues = await getFieldAndMatchingValue(JiraOnPremise, changeReason.value, CHANGE_REASON, 'Change reason');
// Add the Change reason field to request body
requestBody[changeReasonValues.field] = {
value: changeReasonValues.matchingValue
};
}
}
// Check if field exists in FIELDS array
if (FIELDS.includes('Change type')) {
const changeTypeField = await getCustomField(JiraCloud, 'Change type');
const changeType = event.issue.fields[changeTypeField];
if (changeType !== null) {
// Find the Change type and matching value in target instance
const changeTypeValues = await getFieldAndMatchingValue(JiraOnPremise, changeType.value, CHANGE_TYPE, 'Change type');
// Add the Change type field to request body
requestBody[changeTypeValues.field] = {
value: changeTypeValues.matchingValue
};
}
}
// Check if field exists in FIELDS array
if (FIELDS.includes('Change risk')) {
const changeRiskField = await getCustomField(JiraCloud, 'Change risk');
const changeRisk = event.issue.fields[changeRiskField];
if (changeRisk !== null) {
// Find the Change risk and matching value in target instance
const changeRiskValues = await getFieldAndMatchingValue(JiraOnPremise, changeRisk.value, CHANGE_RISK, 'Change risk');
// Add the Change risk field to request body
requestBody[changeRiskValues.field] = {
value: changeRiskValues.matchingValue
};
}
}
// Check if duedate field exists in FIELDS array and if issue has due date added
if (FIELDS.includes('duedate') && event.issue.fields.duedate !== null) {
// If it does, add it to request body
requestBody.duedate = event.issue.fields.duedate;
}
// Check if labels field exist in FIELDS array and if issue has labels added
if (FIELDS.includes('labels') && event.issue.fields.labels?.length !== 0) {
// If it does, add it to request body
requestBody.labels = event.issue.fields.labels;
}
// Check if Sub-task was created
if (FIELDS.includes('issuetype') && event.changelog.items.some(item => item.field === 'IssueParentAssociation')) {
const parentIssueKey = event.issue.fields.parent.key
const parentSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, parentIssueKey ?? '', JiraCloud);
const matchingIssue = await searchIssue(context, parentSyncIssueKey, JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY);
requestBody.parent = {
key: matchingIssue?.issues?.[0].key
}
}
// Get the created issue
const createdIssue = await JiraCloud.Issue.getIssue({
issueIdOrKey: eventIssueKey
});
// Check if custom fields have been added to CUSTOM_FIELDS array in Values script
if (CUSTOM_FIELDS.length) {
// If field names have been added there, we will add them to the request body
for (const customField of CUSTOM_FIELDS) {
// Get custom field
const sourceInstanceCustomFieldId = await getCustomField(JiraCloud, customField);
// Save its value
const fieldValue = event.issue.fields[sourceInstanceCustomFieldId];
if (fieldValue) {
// Find the custom field from target instance
const jiraOnPremCustomField = await getJiraOnPremCustomFieldIdAndType(customField);
switch (jiraOnPremCustomField?.type) {
// Check if custom field is a string or a number
case 'Text Field (single line)':
case 'Text Field (multi-line)':
case 'Number Field':
case 'URL Field':
case 'Date Picker':
case 'Date Time Picker':
case 'Labels':
// Add the value to the request body
requestBody[jiraOnPremCustomField.id] = fieldValue;
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
requestBody[jiraOnPremCustomField.id] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
break;
case 'Select List (cascading)':
requestBody[jiraOnPremCustomField.id] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
break;
case 'Select List (single choice)':
requestBody[jiraOnPremCustomField.id] = {
value: fieldValue.value
}
break;
case 'User Picker (multiple users)':
const users = (fieldValue as { emailAddress?: string, displayName: string }[]).map(field => ({
emailAddress: field.emailAddress ?? '',
displayName: field.displayName
}));
// Check if the account can be added to the issue
const validAccounts = await checkAccountsOnJiraOnPrem(context, users);
if (validAccounts) {
// Adds valid account IDs to the request body
requestBody[jiraOnPremCustomField.id] = validAccounts.map(user => ({ name: user.name }));
}
break;
case 'User Picker (single user)':
// Check if user is assignable to the target project
const user = await findAssignableUserOnJiraOnPrem(fieldValue.emailAddress, fieldValue.displayName);
if (user) {
requestBody[jiraOnPremCustomField.id] = {
name: user.name
}
}
break;
default:
break;
}
}
}
}
// Create a new Issue in Jira On-Premise
const issue = await JiraOnPremise.Issue.createIssue({
body: {
fields: requestBody,
}
})
// Extract the newly created Issue key
const issueKey = issue.key ?? '';
const attachments = event.issue.fields.attachment ?? [];
// Check if attachments were added to the issue
if (FIELDS.includes('Attachment') && attachments.length > 0) {
// Get the attachments from the issue
const issueAttachments = (await JiraCloud.Issue.getIssue({
issueIdOrKey: eventIssueKey
})).fields?.attachment ?? [];
// Loop through attachments and add them to the array
for (const attachment of issueAttachments) {
if (attachment.content && attachment.filename) {
const storedAttachment = await JiraCloud.fetch(attachment.content, {
headers: {
'x-stitch-store-body': 'true'
}
});
// Check if the attachment content response is OK
if (!storedAttachment.ok) {
// If not, then throw an error
throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
}
const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');
if (!storedAttachmentId) {
throw new Error('The attachment stored body was not returned');
}
await JiraOnPremise.fetch(`/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'X-Atlassian-Token': 'no-check',
'x-stitch-stored-body-id': storedAttachmentId,
'x-stitch-stored-body-form-data-file-name': attachment.filename
}
});
}
}
}
// Check if newly created Issue has the correct status and find the matching status
const status = await getMatchingValue(JiraCloud, event.issue.fields.status?.name ?? '', STATUS);
// Check if status is incorrect, and then change it
if (createdIssue.fields?.status?.name !== status) {
const transitions = (await JiraOnPremise.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
const transitionId = transitions.find(t => t.name === status)?.id ?? ''
if (!transitionId) {
throw Error(`Transition for status not found in target instance: ${status}`);
}
// Change the status of the issue (workflow transition)
await JiraOnPremise.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
}
}
});
};
// Check if issue links exist in FIELDS array and if issue has issue links added
if (FIELDS.includes('issue links') && createdIssue.fields?.issuelinks?.length) {
const issueLinks = event.issue.fields.issuelinks ?? [];
// Go over created issue links
for (const issueLink of issueLinks) {
// Extract issue keys
const outwardLinkedIssueKey = issueLink.outwardIssue?.key;
const inwardLinkedIssueKey = issueLink.inwardIssue?.key;
const storage = new RecordStorage();
// Check for existing issue links
const existingIssueLinks = await getIssueLinks(storage, event.issue.key);
// Check target instance has valid issue link type
const issueLinkTypes = await JiraOnPremise.Issue.Link.Type.getTypes();
const issueLinkType = (issueLinkTypes.issueLinkTypes?.find((types) => types.name === issueLink.type.name));
if (!issueLinkType) {
throw Error(`Issue Link Type ${issueLink.type.name} doesn't exist in the target project`);
}
// Handle outward issue link
if (outwardLinkedIssueKey) {
// Find the Sync key for outward issue
const syncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, outwardLinkedIssueKey ?? '', JiraCloud);
if (syncIssueKey) {
// Find the matching issue from target instance
const targetIssue = (await searchIssue(context, syncIssueKey ?? '', JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY, false))
// Create issue link in target instance
await JiraOnPremise.Issue.Link.createLink({
body: {
outwardIssue: {
key: targetIssue?.issues?.[0].key ?? '0',
},
type: {
name: issueLink.type.name
},
inwardIssue: {
key: issue.key
}
},
})
// Get the issue link id
const createdIssueLinkId = (await JiraOnPremise.Issue.getIssue({
issueIdOrKey: issue.key ?? '0'
})).fields?.issuelinks?.find(l => l.outwardIssue?.key === targetIssue?.issues?.[0].key && l.type?.name === issueLink.type.name)?.id
// Save issue link mapping into Record Storage
const newIssueLink = {
[JIRA_CLOUD_PROJECT_KEY]: issueLink.id,
[JIRA_ON_PREM_PROJECT_KEY]: createdIssueLinkId ?? '0',
}
// Save issue links to Record Storage
const updatedIssueLinks = [...existingIssueLinks, newIssueLink];
await setIssueLinks(storage, eventIssueKey, updatedIssueLinks);
console.log(`Issue link created bewteen ${targetIssue?.issues?.[0].key} and ${issue.key}`);
}
}
// Handle inward issue link
if (inwardLinkedIssueKey) {
// Find the Sync key for inward issue
const syncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, inwardLinkedIssueKey ?? '', JiraCloud);
if (syncIssueKey) {
// Find the matching issue from target instance
const targetIssue = (await searchIssue(context, syncIssueKey ?? '', JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY))
// Create issue link in target instance
await JiraOnPremise.Issue.Link.createLink({
body: {
outwardIssue: {
key: issue.key
},
type: {
name: issueLink.type.name
},
inwardIssue: {
key: targetIssue?.issues?.[0].key,
}
},
})
// Get the created issue link id
const createdIssueLinkId = (await JiraOnPremise.Issue.getIssue({
issueIdOrKey: targetIssue?.issues?.[0].key ?? '0'
})).fields?.issuelinks?.find(l => l.outwardIssue?.key === issue.key && l.type?.name === issueLink.type.name)?.id
// Save issue link mapping into Record Storage
const newIssueLink = {
[JIRA_CLOUD_PROJECT_KEY]: issueLink.id,
[JIRA_ON_PREM_PROJECT_KEY]: createdIssueLinkId ?? '0'
}
// Save issue links to Record Storage
const updatedIssueLinks = [...existingIssueLinks, newIssueLink];
await setIssueLinks(storage, syncIssueKey, updatedIssueLinks);
console.log(`Issue link created bewteen ${targetIssue?.issues?.[0].key} and ${issue.key}`);
}
}
};
};
console.log(`Issue created: ${issueKey}`);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueDeletedEvent } from '@sr-connect/jira-cloud/events';
import { deleteIssue, getEnvVars, } from '../Utils';
/**
* Entry point to Issue Deleted event
*
* @param event Object that holds Issue Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraCloud.Myself.getCurrentUser();
//Check if the current user does not match the person who committed the update
if (myself.accountId !== event.user.accountId) {
const { JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await deleteIssue(context, event, JiraCloud, JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueLinkCreatedEvent } from '@sr-connect/jira-cloud/events';
import { createIssueLink, getEnvVars } from '../Utils';
/**
* Entry point to Issue Link Created event
*
* @param event Object that holds Issue Link Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueLinkCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await createIssueLink(context, event, JiraCloud, JiraOnPremise, JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueLinkDeletedEvent } from '@sr-connect/jira-cloud/events';
import { deleteIssueLink, getEnvVars } from '../Utils';
/**
* Entry point to Issue Link Deleted event
*
* @param event Object that holds Issue Link Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueLinkDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
await deleteIssueLink(context, event, JiraCloud, JiraOnPremise, JIRA_CLOUD_PROJECT_KEY, JIRA_ON_PREM_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { checkAccountsOnJiraOnPrem, findAssignableUserOnJiraOnPrem, getCustomField, getEnvVars, getEpicLink, getFieldAndMatchingValue, getJiraOnPremCustomFieldIdAndType, getMatchingValue, getScriptRunnerConnectSyncIssueKey, getUsersFromJiraCloud, handleUserFieldOptionForJiraOnPrem, searchIssue, setEpicLink, stringToArray } from '../Utils';
import { FieldsEditIssue } from '@managed-api/jira-on-prem-v8-core/definitions/issue';
import { Attachment } from '@managed-api/jira-on-prem-v8-core/definitions/attachment';
import { UserFull } from '@managed-api/jira-on-prem-v8-core/definitions/user';
import { UserDetailsAsResponse } from '@managed-api/jira-cloud-v3-core/definitions/UserDetailsAsResponse';
import { RecordStorage } from '@sr-connect/record-storage';
import { GetIssueResponseOK } from '@managed-api/jira-cloud-v3-core/types/issue';
/**
* Entry point to Issue Updated event
*
* @param event Object that holds Issue Updated event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraCloud.Myself.getCurrentUser();
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY, FIELDS, CHANGE_REASON, CHANGE_RISK, CHANGE_TYPE, CUSTOM_FIELDS, CUSTOM_FIELD_NAME, IMPACT, ISSUE_TYPES, PRIORITY, STATUS, RetryConfigurations } = getEnvVars(context);
// Check if the current user does not match the person who committed the update and the changelog includes one of the fields that we're interested in
if ((myself.accountId !== event.user.accountId) && event.changelog?.items.some(cl => (FIELDS.includes(cl.field) || CUSTOM_FIELDS.includes(cl.field)))) {
const customField = await getCustomField(JiraCloud, CUSTOM_FIELD_NAME);
let issue: GetIssueResponseOK | undefined;
let scriptRunnerConnectSyncIssueKey = null;
// Retry logic for finding ScriptRunner Connect Sync Issue Key
for (let i = 0; i < RetryConfigurations.MAX_RETRY_ATTEMPTS + 1; i++) {
// Get the updated issue
issue = await JiraCloud.Issue.getIssue({
issueIdOrKey: event.issue.key,
});
if (issue?.fields?.[customField] !== null) {
// Extract the ScriptRunner Connect Sync Issue Key
scriptRunnerConnectSyncIssueKey = issue?.fields?.[customField];
break;
} else {
console.log('No sync issue key found. Retrying...');
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
}
}
if (scriptRunnerConnectSyncIssueKey === null) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from the target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY, true)
if (issues === null) {
throw Error(`Issue with the matching Stich It Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Extract the issue key of the matching issue that was updated
const issueKey = issues?.issues?.[0].key ?? '';
// Extract the fields that were updated
const eventItems = event.changelog?.items.filter(item => FIELDS.includes(item.field)) ?? [];
// Object that contains changes that need to be updated on the target instance
let requestBody: FieldsEditIssue = {};
// Find the project to use based on pre-defined project key
const project = await JiraOnPremise.Project.getProject({
projectIdOrKey: JIRA_ON_PREM_PROJECT_KEY
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${JIRA_ON_PREM_PROJECT_KEY}`);
}
// Get integration user account on Jira On-Prem
const integrationUserOnJiraPrem = await JiraOnPremise.Myself.getCurrentUser();
// Go over the updated fields and add their values to the request body
for (const eventItem of eventItems) {
switch (eventItem.field) {
case 'summary':
// Add summary to request body
requestBody.summary = eventItem.toString ?? '';
break;
case 'issuetype':
// Extract the updated issue type name
const updatedIssueType = event.issue.fields.issuetype?.name ?? ''
// Find the matching issue type name
const mappedIssueType = await getMatchingValue(JiraCloud, updatedIssueType, ISSUE_TYPES);
// Find all the issue types in target instance
const issueTypes = await JiraOnPremise.Issue.Type.getTypes();
// Find the issue type
const issueType = issueTypes.find(it => it.name === mappedIssueType);
// Check if the issue type was found
if (!issueType) {
// If not, throw an error
throw Error(`Issue type not found in target instance: ${mappedIssueType}`);
}
// Add issue type to request body
requestBody.issuetype = {
id: issueType.id ?? ''
};
break;
case 'reporter':
// Extract the reporter
const reporter = event.issue.fields.reporter
// Function that check USER_FIELD_OPTION value and handles the field appropriately
const reporterUserFieldOption = await handleUserFieldOptionForJiraOnPrem(context, eventItem.field, reporter?.emailAddress ?? '', reporter?.displayName ?? '', integrationUserOnJiraPrem);
if (reporterUserFieldOption) {
requestBody = { ...requestBody, ...reporterUserFieldOption }
}
break;
case 'assignee':
if (eventItem.to) {
// Extract the assignee
const assignee = event.issue.fields.assignee;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const assigneeUserFieldOption = await handleUserFieldOptionForJiraOnPrem(context, eventItem.field, assignee?.emailAddress ?? '', assignee?.displayName ?? '', integrationUserOnJiraPrem);
if (assigneeUserFieldOption) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
}
} else {
requestBody.assignee = {
name: ''
}
}
break;
case 'status':
// Extract the updated status
const updateIssueStatus = event.issue.fields.status?.name ?? ''
// Find the matching state
const status = await getMatchingValue(JiraCloud, updateIssueStatus, STATUS);
// Get project transitions
const transitions = (await JiraOnPremise.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
const transitionId = transitions.find(t => t.name === status)?.id ?? ''
if (!transitionId) {
throw Error(`Transition ID not found in target instance for status: ${status}`);
}
// Finally change the issue status (workflow transition)
await JiraOnPremise.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
}
}
});
break;
case 'priority':
// Extract priority
const updateIssuePriority = eventItem.toString ?? '';
// Find the matching priority name
const matchingPiority = await getMatchingValue(JiraCloud, updateIssuePriority, PRIORITY);
// Find priorities from target instance
const priorities = await JiraOnPremise.Issue.Priority.getPriorities();
const priority = priorities.find(p => p.name === matchingPiority);
// Check if priority was found
if (!priority) {
// If not, throw an error
throw Error(`Priority not found in target instance: ${priority}`)
}
// Add the priority to request body
requestBody.priority = {
id: priority.id ?? '0'
}
break;
case 'Attachment':
// Check if attachment was added or deleted
if (eventItem.to) {
// Extract attachment ID
const attachmentId = eventItem.to ?? '';
// Find the added attachment from issue
const attachment = issue?.fields?.attachment?.find(a => a.id === attachmentId) ?? {}
if (attachment.content && attachment.filename) {
// Add the attachment
const storedAttachment = await JiraCloud.fetch(attachment.content, {
headers: {
'x-stitch-store-body': 'true'
}
});
// Check if the attachment content response is OK
if (!storedAttachment.ok) {
// If not, then throw an error
throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
}
const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');
if (!storedAttachmentId) {
throw new Error('The attachment stored body was not returned');
}
await JiraOnPremise.fetch(`/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'X-Atlassian-Token': 'no-check',
'x-stitch-stored-body-id': storedAttachmentId,
'x-stitch-stored-body-form-data-file-name': attachment.filename
}
});
console.log(`Attachment ${attachment.filename} added to issue: ${issueKey}`);
}
} else {
const attachments = issues?.issues?.[0].fields?.attachment as Attachment[] ?? [];
// Find matching attachment ID
const attachmentId = (attachments?.find(a => a.filename === eventItem.fromString))?.id ?? '';
if (!attachmentId) {
throw Error('Matching attachment ID was not found from target instance');
}
// Delete the attachment
await JiraOnPremise.Issue.Attachment.removeAttachment({
id: attachmentId
})
}
break;
case 'description':
// Get the description from the issue
if (eventItem.toString) {
// Add description to issue fields
requestBody.description = eventItem.toString
} else {
requestBody.description = '';
}
break;
case 'Impact':
// Find the field and matching value in target instance
const impactValues = await getFieldAndMatchingValue(JiraOnPremise, eventItem.toString, IMPACT, eventItem.field);
if (eventItem.to) {
// Add the Impact field to request body and update the field value
requestBody[impactValues.field] = {
value: impactValues.matchingValue
};
} else {
requestBody[impactValues.field] = null;
}
break;
case 'Change reason':
// Find the field and matching value in target instance
const changeReasonValues = await getFieldAndMatchingValue(JiraOnPremise, eventItem.toString, CHANGE_REASON, eventItem.field);
if (eventItem.to) {
// Add the Change reason field to request body and update the field value
requestBody[changeReasonValues.field] = {
value: changeReasonValues.matchingValue
};
} else {
requestBody[changeReasonValues.field] = null;
}
break;
case 'Change type':
// Find the field and matching value in target instance
const changeTypeValues = await getFieldAndMatchingValue(JiraOnPremise, eventItem.toString, CHANGE_TYPE, eventItem.field);
if (eventItem.to) {
// Add the Change type field to request body and update the field value
requestBody[changeTypeValues.field] = {
value: changeTypeValues.matchingValue
};
} else {
requestBody[changeTypeValues.field] = null;
}
break;
case 'Change risk':
// Find the field and matching value in target instance
const changeRiskValues = await getFieldAndMatchingValue(JiraOnPremise, eventItem.toString, CHANGE_RISK, eventItem.field);
if (eventItem.to) {
// Add the Change risk field to request body and update the field value
requestBody[changeRiskValues.field] = {
value: changeRiskValues.matchingValue
};
} else {
requestBody[changeRiskValues.field] = null;
}
break;
case 'labels':
// Add updated labels to request body
requestBody.labels = event.issue.fields.labels;
break;
case 'duedate':
// Add updated due date to request body
requestBody.duedate = event.issue.fields.duedate;
break;
case 'IssueParentAssociation':
const storage = new RecordStorage();
// Check if epic link has been added to Record Storage
const epicLink = await getEpicLink(storage, scriptRunnerConnectSyncIssueKey);
// Check if epic got added or removed
if (eventItem.toString) {
const epicSyncKey = await getScriptRunnerConnectSyncIssueKey(context, eventItem.toString, JiraCloud);
// Find the matching epic issue from target instance
const matchingEpicIssue = await searchIssue(context, epicSyncKey ?? '0', JiraOnPremise, JIRA_ON_PREM_PROJECT_KEY, false);
// Check if subtask has been added to the issue already
if (matchingEpicIssue.issues?.[0].fields?.subtasks?.some(s => s.key === issues.issues?.[0].key)) {
console.log('Sub-task is already present');
break;
}
if (matchingEpicIssue?.issues?.length === 0) {
throw Error('Matching Epic Issue not found')
}
// If epic link ID exists, delete the link before creating a new one
if (epicLink?.id) {
await JiraOnPremise.Issue.Link.deleteLink({
linkId: epicLink.id
});
// Delete epic link key from Record Storage
await storage.deleteValue(`epic_${scriptRunnerConnectSyncIssueKey}`)
}
// Create Epic Link between issues on Jira On-Prem
await JiraOnPremise.Issue.Link.createLink({
body: {
inwardIssue: {
key: matchingEpicIssue?.issues?.[0].key
},
outwardIssue: {
key: issueKey
},
type: {
name: 'Epic-Story Link'
}
}
})
// Update Record Storage to keep track of Epic Link
await setEpicLink(storage, scriptRunnerConnectSyncIssueKey, { jiraCloudIssue: event.issue.key });
} else {
if (!epicLink) {
throw Error('Matching Epic Link not found')
}
// Delete epic Link from Jira On-Premise
await JiraOnPremise.Issue.Link.deleteLink({
linkId: epicLink?.id ?? '0'
})
}
break;
case 'Epic Name':
// Find the custom field from target instance and save the updated Epic Name
const epicNameField = await getJiraOnPremCustomFieldIdAndType(eventItem.field);
if (epicNameField) {
requestBody[epicNameField.id] = eventItem.toString;
}
break;
default:
break;
}
}
// Filter custom fields that were updated and exist in the CUSTOM_FIELDS array in Values script
const updatedCustomFields = event.changelog?.items.filter(cl => CUSTOM_FIELDS.includes(cl.field));
// Check if any custom field got updated
if (updatedCustomFields.length) {
// Map through updated custom fields
for (const customField of updatedCustomFields) {
// Extract the custom field ID
const jiraCloudCustomFieldId = customField.fieldId;
// Find the custom field from target instance
const jiraOnPremCustomField = await getJiraOnPremCustomFieldIdAndType(customField.field);
if (!jiraOnPremCustomField) {
throw Error(`Field ${customField} not found on Jira On-Premise instance`);
}
// Extract the field value
const fieldValue = event.issue.fields?.[jiraCloudCustomFieldId];
// Check the type and value of each custom field to appropriately update the request body
switch (jiraOnPremCustomField?.type) {
case 'Text Field (single line)':
case 'Text Field (multi-line)':
case 'Number Field':
case 'URL Field':
case 'Date Picker':
case 'Date Time Picker':
case 'Labels':
requestBody[jiraOnPremCustomField.id] = fieldValue;
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
if (!customField.toString) {
requestBody[jiraOnPremCustomField.id] = null
} else {
requestBody[jiraOnPremCustomField.id] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
}
break;
case 'Select List (cascading)':
if (!customField.toString) {
requestBody[jiraOnPremCustomField.id] = null
} else {
requestBody[jiraOnPremCustomField.id] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
}
break;
case 'Select List (single choice)':
if (!customField.toString) {
requestBody[jiraOnPremCustomField.id] = null
} else {
requestBody[jiraOnPremCustomField.id] = {
value: fieldValue.value
}
}
break;
case 'User Picker (multiple users)':
// Get the target issue in Jira On-Premise
const targetIssue = await JiraOnPremise.Issue.getIssue({
issueIdOrKey: issueKey,
})
// Extract users that are added on the Jira On-Premise issue
const currentlyAddedUsersOnJiraOnPrem: UserFull[] | null = targetIssue.fields?.[jiraOnPremCustomField.id];
// Verify whether any users have been added and if the fieldValue is not null
if (currentlyAddedUsersOnJiraOnPrem === null && fieldValue !== null) {
// Iterate through added users and extract their email and display name
const users = (fieldValue as { emailAddress?: string, displayName: string }[])?.map(field => ({
emailAddress: field.emailAddress ?? '',
displayName: field.displayName
}));
// Check if accounts can be added to the matching issue on Jira On-Premise
const accountsToAdd = await checkAccountsOnJiraOnPrem(context, users)
if (accountsToAdd) {
// Adds valid accounts to the custom field
requestBody[jiraOnPremCustomField.id] = accountsToAdd.map(user => ({ name: user.name }));
}
} else {
// Extract the original Account IDs from the issue
const originalListOfAccountIds = await stringToArray(customField.from ?? '')
// Extract the updated Account IDs from the issue
const listOfAccountIds = await stringToArray(customField.to ?? '');
// Filter which accounts got removed and which added
const removedAccountIds = originalListOfAccountIds.filter(id => !listOfAccountIds.includes(id));
const addedAccountIds = listOfAccountIds.filter(id => !originalListOfAccountIds.includes(id));
// Map through currently added accounts on Jira On-Premise and save their email and account name
const currentlyAddedUsers = currentlyAddedUsersOnJiraOnPrem?.map(field => ({
emailAddress: field.emailAddress,
name: field.name
}));
let accountsToRemove: string[] = [];
let accountsToAdd: string[] = []
// If any account IDs got removed, add them to the accountsToRemove array
if (removedAccountIds.length > 0) {
// Get the account email and display name of removed users using accountId
const usersThatGotRemoved = await getUsersFromJiraCloud(JIRA_CLOUD_PROJECT_KEY, removedAccountIds);
// Check if the removed accounts are valid Jira On-Premise accounts and add them to the accountsToRemove array
const validJiraOnPremAccounts = await checkAccountsOnJiraOnPrem(context, usersThatGotRemoved);
accountsToRemove = validJiraOnPremAccounts.map(user => user.name ?? '');
}
// If any account IDs got added, add them to the accountsToAdd array
if (addedAccountIds.length > 0) {
const addedUsers = (fieldValue as UserDetailsAsResponse[]).filter(u => addedAccountIds.includes(u.accountId ?? '0')).map(user => ({
emailAddress: user.emailAddress ?? '',
displayName: user.displayName ?? ''
}));
// Check if added accounts are valid Jira On-Premise accounts and add them to the accountsToAdd array
const validJiraOnPremAccounts = await checkAccountsOnJiraOnPrem(context, addedUsers);
accountsToAdd = validJiraOnPremAccounts.map(user => user.name ?? '');
}
// New list of accounts, filtering out accounts that need to be removed and adding new ones
const newList = currentlyAddedUsers?.filter(item => !accountsToRemove.includes(item.name ?? '')).map(item => item.name ?? '');
newList?.push(...accountsToAdd);
const accounts = newList?.map(value => ({ name: value }));
// Add necessary accounts to the request body
requestBody[jiraOnPremCustomField.id] = accounts;
}
break;
case 'User Picker (single user)':
// Check if user got added or removed
if (!customField.toString) {
requestBody[jiraOnPremCustomField.id] = null
} else {
// Check user can be added on Jira On-Premise
const user = await findAssignableUserOnJiraOnPrem(fieldValue.emailAddress, fieldValue.displayName);
if (user) {
requestBody[jiraOnPremCustomField.id] = {
name: user.name
}
} else {
requestBody[jiraOnPremCustomField.id] = null
}
}
break;
default:
break;
}
}
}
// If there is any fields in requestBody, update the issue in target instance
if (Object.keys(requestBody).length > 0) {
await JiraOnPremise.Issue.editIssue({
issueIdOrKey: issueKey,
body: {
fields: requestBody,
}
});
console.log(`Updated issue: ${issueKey}`);
}
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { createComment, getEnvVars } from '../Utils';
/**
* Entry point to Comment Created event
*
* @param event Object that holds Comment Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraOnPremise.Myself.getCurrentUser();
// Check that the comment was created by a different user than the one who set up the integration
if (myself.emailAddress !== event.comment.author?.emailAddress) {
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
// Create comment in matching issue
await createComment(context, event, JiraOnPremise, JiraCloud, JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentDeletedEvent } from '@sr-connect/jira-on-premise/events';
import { deleteComment, getEnvVars } from '../Utils';
/**
* Entry point to Comment Deleted event
*
* @param event Object that holds Comment Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
// Delete comment from matching issue
await deleteComment(context, event, JiraOnPremise, JiraCloud, JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import { getEnvVars, updateComment } from '../Utils';
/**
* Entry point to Comment Updated event
*
* @param event Object that holds Comment Updated event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCommentUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraOnPremise.Myself.getCurrentUser();
// Check that the comment was updated by a different user than the one who set up the integration
if (myself.emailAddress !== event.comment.updateAuthor?.emailAddress) {
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
// Update comment in matching issue
await updateComment(context, event, JiraOnPremise, JiraCloud, JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY);
}
}
import JiraCloud from '../api/jira/cloud';
import JiraOnPremise from '../api/jira/on-premise';
import { IssueCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { createParagraph, findAssignableUserOnJiraCloud, getCustomField, getEnvVars, getEpicNameCustomFieldFromJiraCloud, getFieldAndMatchingValue, getIssueLinks, getJiraCloudPriority, getJiraOnPremCustomFieldIdAndType, getMatchingValue, getScriptRunnerConnectSyncIssueKey, handleUserFieldOptionForJiraCloud, searchIssue, setIssueLinks } from '../Utils';
import { IssueFieldsCreate } from '@managed-api/jira-cloud-v3-core/definitions/IssueFields';
import { RecordStorage } from '@sr-connect/record-storage';
/**
* Entry point to Issue Created event
*
* @param event Object that holds Issue Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraOnPremise.Myself.getCurrentUser();
// Check if the current user does not match the person who committed the update
if (myself.emailAddress !== event.user.emailAddress) {
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY, CUSTOM_FIELD_NAME, CHANGE_REASON, CHANGE_RISK, CHANGE_TYPE, CUSTOM_FIELDS, FIELDS, IMPACT, ISSUE_TYPES, PRIORITY, STATUS } = getEnvVars(context);
// Extract the issue issue key
const eventIssueKey = event.issue.key ?? '';
// Extract the issue fields
const eventIssueFields = event.issue.fields;
// Get the ScriptRunner Connect Sync Issue Key custom field
const sourceCustomField = await getCustomField(JiraOnPremise, CUSTOM_FIELD_NAME);
// Add issue key to the ScriptRunner Connect Sync Issue Key custom field
await JiraOnPremise.Issue.editIssue({
issueIdOrKey: eventIssueKey,
body: {
fields: {
[sourceCustomField]: eventIssueKey
}
}
})
// Find the project from target instance based on pre-defined project key
const project = await JiraCloud.Project.getProject({
projectIdOrKey: JIRA_CLOUD_PROJECT_KEY
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${JIRA_CLOUD_PROJECT_KEY}`);
}
// Find the matching issue type for Jira Cloud project
const issueTypeName = await getMatchingValue(JiraOnPremise, eventIssueFields?.issuetype?.name ?? '', ISSUE_TYPES);
// Find all the issue types for Jira Cloud project
const issueTypes = await JiraCloud.Issue.Type.getTypesForProject({
projectId: +(project.id ?? 0) // + sign converts the string to number
});
// Find the issue type to use based on pre-defined issue type name
const issueType = issueTypes.find(it => it.name === issueTypeName);
// Check if the issue type was found
if (!issueType) {
// If not, then throw an error
throw Error(`Issue Type not found in target instance: ${issueTypeName}`);
}
// Get the ScriptRunner Connect Sync Issue Key custom field from target instance
const targetCustomField = await getCustomField(JiraCloud, CUSTOM_FIELD_NAME);
//Fields to be updated in target instance
let requestBody: IssueFieldsCreate = {
summary: eventIssueFields?.summary ?? '',
project: {
key: project.key ?? ''
},
issuetype: {
name: issueType.name ?? ''
},
[targetCustomField]: eventIssueKey,
}
// Check if issue type name is Epic
if (issueTypeName === 'Epic') {
// Get the Epic Name custom field ID from Jira On-Premise
const jiraOnPremEpicNameCustomField = await getCustomField(JiraOnPremise, 'Epic Name');
// Find the Epic Name custom field ID from Jira Cloud
const epicNameCustomField = await getEpicNameCustomFieldFromJiraCloud(project?.id ?? '');
// Add the Epic Name value to request body
requestBody[epicNameCustomField] = eventIssueFields?.[jiraOnPremEpicNameCustomField];
};
// Get the user who set up the integration on Jira Cloud
const integrationUserOnJiraCloud = await JiraCloud.Myself.getCurrentUser();
// Check if reporter field is added in FIELDS array
if (FIELDS.includes('reporter')) {
// Extract the reporter
const reporter = eventIssueFields?.reporter;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const reporterUserFieldOption = await handleUserFieldOptionForJiraCloud(context, 'reporter', reporter?.emailAddress ?? '', reporter?.displayName ?? '', integrationUserOnJiraCloud.accountId ?? '');
// If a value is returned add it to the request body
if (reporterUserFieldOption) {
requestBody = { ...requestBody, ...reporterUserFieldOption }
}
}
// Check if assignee field is added in FIELDS array and assignee got added
if (FIELDS.includes('assignee') && event.issue?.fields?.assignee !== null) {
// Extract the assignee
const assignee = eventIssueFields?.assignee;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const assigneeUserFieldOption = await handleUserFieldOptionForJiraCloud(context, 'assignee', assignee?.emailAddress ?? '', assignee?.displayName ?? '', integrationUserOnJiraCloud.accountId ?? '');
// If a value is returned add it to the request body
if (assigneeUserFieldOption) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
}
}
// Check if priority field exists FIELDS array
if (FIELDS.includes('priority')) {
// Find the matching Jira Cloud priority name
const priorityName = await getMatchingValue(JiraOnPremise, eventIssueFields?.priority?.name ?? '', PRIORITY);
// Find priority from Jira Cloud
const priority = await getJiraCloudPriority(priorityName);
// Check if correct priority was found
if (!priority) {
// If not, throw an error
throw Error(`Priority not found in target instance: ${priority}`);
}
// Add priority ID to request body
requestBody.priority = { id: priority.id ?? '' }
}
// Check if description field is added in FIELDS array and if description got added
if (FIELDS.includes('description') && eventIssueFields?.description !== null) {
requestBody.description = createParagraph(eventIssueFields?.description ?? '')
}
// Check if duedate field is added in FIELDS array and if issue has due date added
if (FIELDS.includes('duedate') && eventIssueFields?.duedate !== null) {
// If it does, add it to request body
requestBody.duedate = eventIssueFields?.duedate;
}
// Check if labels field is added in FIELDS array and if issue has labels added
if (FIELDS.includes('labels') && eventIssueFields?.labels?.length !== 0) {
// If it does, add it to request body
requestBody.labels = eventIssueFields?.labels;
}
// // Get the created issue
const createdIssue = await JiraOnPremise.Issue.getIssue({
issueIdOrKey: eventIssueKey
});
// Check if Impact field is added in FIELDS array
if (FIELDS.includes('Impact')) {
const fieldName = 'Impact';
// Get the custom field from Jira On-Premise
const sourceInstanceCustomFieldId = await getCustomField(JiraOnPremise, fieldName);
// Save its value
const value = eventIssueFields?.[sourceInstanceCustomFieldId]?.value;
// Check if the field has a value added
if (value) {
// Find the Impact field and matching value in target instance
const impact = await getFieldAndMatchingValue(JiraCloud, value, IMPACT, fieldName);
requestBody[impact.field] = {
value: impact.matchingValue
};
}
}
// Check if Change reason field is added in FIELDS array
if (FIELDS.includes('Change reason')) {
const fieldName = 'Change reason';
// Get the custom field from Jira On-Premise
const sourceInstanceCustomFieldId = await getCustomField(JiraOnPremise, fieldName);
// Save its value
const value = eventIssueFields?.[sourceInstanceCustomFieldId]?.value;
// Check if the field has a value added
if (value) {
// Find the Change reason field and matching value in target instance
const changeReason = await getFieldAndMatchingValue(JiraCloud, value, CHANGE_REASON, fieldName);
requestBody[changeReason.field] = {
value: changeReason.matchingValue
};
}
}
// Check if Change risk field is added in FIELDS array
if (FIELDS.includes('Change risk')) {
const fieldName = 'Change risk';
// Get the custom field from Jira On-Premise
const sourceInstanceCustomFieldId = await getCustomField(JiraOnPremise, fieldName);
// Save its value
const value = eventIssueFields?.[sourceInstanceCustomFieldId]?.value;
// Check if the field has a value added
if (value) {
// Find the Change reason field and matching value in target instance
const changeRisk = await getFieldAndMatchingValue(JiraCloud, value, CHANGE_RISK, fieldName);
requestBody[changeRisk.field] = {
value: changeRisk.matchingValue
};
}
}
// Check if Change type field is added in FIELDS array
if (FIELDS.includes('Change type')) {
const fieldName = 'Change type';
// Get the custom field from Jira On-Premise
const sourceInstanceCustomFieldId = await getCustomField(JiraOnPremise, fieldName);
// Save its value
const value = eventIssueFields?.[sourceInstanceCustomFieldId]?.value;
// Check if the field has a value added
if (value) {
// Find the Change reason field and matching value in target instance
const changeType = await getFieldAndMatchingValue(JiraCloud, value, CHANGE_TYPE, fieldName);
requestBody[changeType.field] = {
value: changeType.matchingValue
};
}
}
if (FIELDS.includes('issuetype') && eventIssueFields.issuetype.name === 'Sub-task') {
const parentIssueKey = eventIssueFields.parent.key;
const parentSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, parentIssueKey ?? '', JiraOnPremise);
const matchingIssue = await searchIssue(context, parentSyncIssueKey, JiraCloud, JIRA_CLOUD_PROJECT_KEY);
requestBody.parent = {
key: matchingIssue?.issues?.[0].key
}
}
// Check if any custom fields have been added to CUSTOM_FIELDS array in Values script
if (CUSTOM_FIELDS.length) {
// If field names have been added, iterate through them and check if they have any values
for (const customField of CUSTOM_FIELDS) {
// Get the custom field
const jiraOnPremCustomField = await getJiraOnPremCustomFieldIdAndType(customField);
if (!jiraOnPremCustomField) {
throw Error(`Field ${customField} not found on Jira On-Premise instance`);
}
// Save its value
const fieldValue = eventIssueFields?.[jiraOnPremCustomField.id];
// Check if the custom field has a value
if (fieldValue) {
// Find the custom field in target instance
const jiraCloudCustomFieldId = await getCustomField(JiraCloud, customField);
// Check the type and value of each custom field to appropriately update the request body
switch (jiraOnPremCustomField.type) {
case 'Text Field (single line)':
case 'Number Field':
case 'URL Field':
case 'Date Picker':
case 'Date Time Picker':
case 'Labels':
requestBody[jiraCloudCustomFieldId] = createdIssue.fields?.[jiraOnPremCustomField.id];
break;
case 'Text Field (multi-line)':
requestBody[jiraCloudCustomFieldId] = createParagraph(createdIssue.fields?.[jiraOnPremCustomField.id]);
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
requestBody[jiraCloudCustomFieldId] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
break;
case 'Select List (cascading)':
requestBody[jiraCloudCustomFieldId] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
break;
case 'Select List (single choice)':
requestBody[jiraCloudCustomFieldId] = {
value: fieldValue.value
}
break;
case 'User Picker (multiple users)':
const validAccountIds = await Promise.all((fieldValue as { emailAddress: string }[]).map(async (acc) => {
const accountId = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, acc.emailAddress);
return accountId;
}));
requestBody[jiraCloudCustomFieldId] = validAccountIds.map(value => ({ accountId: value }));
break;
case 'User Picker (single user)':
const accountId = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, fieldValue.emailAddress);
if (accountId) {
requestBody[jiraCloudCustomFieldId] = {
accountId: accountId
}
}
break;
default:
break;
}
}
}
}
// Create a new Issue in Jira Cloud
const issue = await JiraCloud.Issue.createIssue({
body: {
fields: requestBody,
}
})
// Extract the newly created Issue key
const issueKey = issue.key ?? '';
const attachments = eventIssueFields?.attachment ?? [];
// Check if attachments were added to the issue
if (FIELDS.includes('Attachment') && attachments.length > 0) {
// Get the attachments from the issue
const issueAttachments = (await JiraOnPremise.Issue.getIssue({
issueIdOrKey: eventIssueKey
})).fields?.attachment ?? [];
// Loop through attachments and add them to the array
for (const attachment of issueAttachments) {
if (attachment.content && attachment.filename) {
const storedAttachment = await JiraOnPremise.fetch(`/secure/attachment/${attachment.id}/${attachment.filename}`, {
headers: {
'x-stitch-store-body': 'true'
}
});
if (!storedAttachment.ok) {
throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
}
const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');
if (!storedAttachmentId) {
throw new Error('The attachment stored body is was not returned');
}
await JiraCloud.fetch(`/rest/api/3/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'X-Atlassian-Token': 'no-check',
'x-stitch-stored-body-id': storedAttachmentId,
'x-stitch-stored-body-form-data-file-name': attachment.filename
}
})
}
}
}
// Check if newly created issue has the correct status and find the matching status
const status = await getMatchingValue(JiraOnPremise, eventIssueFields?.status?.name ?? '', STATUS);
// Check if status is incorrect, and then change it
if (createdIssue.fields?.status?.name !== status) {
const transitions = (await JiraCloud.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
const transitionId = transitions.find(t => t.name === status)?.id ?? ''
if (!transitionId) {
throw Error(`Transition for status not found in target instance: ${status}`);
}
// Change the status of the issue (workflow transition)
await JiraCloud.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
}
}
});
};
// Check if issue links is added in FIELDS array and if issue has issue links added
if (FIELDS.includes('issue links') && createdIssue.fields?.issuelinks?.length) {
const issueLinks = eventIssueFields?.issuelinks ?? [];
// Go over created issue links
for (const issueLink of issueLinks) {
// Extract issue keys
const outwardLinkedIssueKey = issueLink.outwardIssue?.key;
const inwardLinkedIssueKey = issueLink.inwardIssue?.key;
const storage = new RecordStorage();
// Check for existing issue links
const existingIssueLinks = await getIssueLinks(storage, eventIssueKey);
// Check target instance has valid issue link type
const issueLinkTypes = await JiraOnPremise.Issue.Link.Type.getTypes();
const issueLinkType = (issueLinkTypes.issueLinkTypes?.find((types) => types.name === issueLink.type?.name));
if (!issueLinkType) {
throw Error(`Issue Link Type ${issueLink.type?.name} doesn't exist in the target project`);
}
// Handle outward issue link
if (outwardLinkedIssueKey) {
// Find the Sync key for outward issue
const syncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, outwardLinkedIssueKey ?? '', JiraOnPremise);
if (syncIssueKey) {
// Find the matching issue from target instance
const targetIssue = (await searchIssue(context, syncIssueKey ?? '', JiraCloud, JIRA_CLOUD_PROJECT_KEY, false))
// Create issue link in target instance
await JiraCloud.Issue.Link.createLink({
body: {
outwardIssue: {
key: targetIssue?.issues?.[0].key,
},
type: {
name: issueLink.type?.name
},
inwardIssue: {
key: issue.key
}
},
})
// Get the issue link ID
const createdIssueLinkId = (await JiraCloud.Issue.getIssue({
issueIdOrKey: issue.key ?? '0'
})).fields?.issuelinks?.find(x => x.outwardIssue?.key === targetIssue?.issues?.[0].key && x.type?.name === issueLink.type?.name)?.id
// Save issue link mapping into Record Storage
const newIssueLink = {
[JIRA_ON_PREM_PROJECT_KEY]: issueLink.id ?? '0',
[JIRA_CLOUD_PROJECT_KEY]: createdIssueLinkId ?? '0',
}
// Save issue links to Record Storage
const updatedIssueLinks = [...existingIssueLinks, newIssueLink];
await setIssueLinks(storage, eventIssueKey, updatedIssueLinks);
console.log(`Issue link created bewteen ${targetIssue?.issues?.[0].key} and ${issue.key}`);
}
}
// Handle inward issue link
if (inwardLinkedIssueKey) {
// Find the Sync key for inward issue
const syncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, inwardLinkedIssueKey ?? '', JiraOnPremise);
if (syncIssueKey) {
// Find the matching issue from target instance
const targetIssue = (await searchIssue(context, syncIssueKey ?? '', JiraCloud, JIRA_CLOUD_PROJECT_KEY))
// Create issue link in target instance
await JiraCloud.Issue.Link.createLink({
body: {
outwardIssue: {
key: issue.key
},
type: {
name: issueLink.type?.name
},
inwardIssue: {
key: targetIssue?.issues?.[0].key,
}
},
})
// Get the created issue link id
const createdIssueLinkId = (await JiraCloud.Issue.getIssue({
issueIdOrKey: targetIssue?.issues?.[0].key ?? '0'
})).fields?.issuelinks?.find(l => l.outwardIssue?.key === issue.key && l.type?.name === issueLink.type?.name)?.id
// Save issue link mapping into Record Storage
const newIssueLink = {
[JIRA_ON_PREM_PROJECT_KEY]: issueLink.id ?? '0',
[JIRA_CLOUD_PROJECT_KEY]: createdIssueLinkId ?? '0'
}
// Save issue links to Record Storage
const updatedIssueLinks = [...existingIssueLinks, newIssueLink];
await setIssueLinks(storage, syncIssueKey, updatedIssueLinks);
console.log(`Issue link created bewteen ${targetIssue?.issues?.[0].key} and ${issue.key}`);
}
}
};
};
console.log(`Issue created: ${issueKey}`);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueDeletedEvent } from '@sr-connect/jira-on-premise/events';
import { deleteIssue, getEnvVars } from '../Utils';
/**
* Entry point to Issue Deleted event
*
* @param event Object that holds Issue Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraOnPremise.Myself.getCurrentUser();
// Check if the current user does not match the person who committed the update
if (myself.emailAddress !== event.user.emailAddress) {
const { JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
await deleteIssue(context, event, JiraOnPremise, JiraCloud, JIRA_CLOUD_PROJECT_KEY);
}
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueLinkCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { createIssueLink, getEnvVars } from '../Utils';
/**
* Entry point to Issue Link Created event
*
* @param event Object that holds Issue Link Created event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueLinkCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
await createIssueLink(context, event, JiraOnPremise, JiraCloud, JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueLinkDeletedEvent } from '@sr-connect/jira-on-premise/events';
import { deleteIssueLink, getEnvVars } from '../Utils';
/**
* Entry point to Issue Link Deleted event
*
* @param event Object that holds Issue Link Deleted event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueLinkDeletedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY } = getEnvVars(context);
await deleteIssueLink(context, event, JiraOnPremise, JiraCloud, JIRA_ON_PREM_PROJECT_KEY, JIRA_CLOUD_PROJECT_KEY);
}
import JiraOnPremise from '../api/jira/on-premise';
import JiraCloud from '../api/jira/cloud';
import { IssueUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import { checkAccountsOnJiraOnPrem, createParagraph, findAssignableUserOnJiraCloud, getCustomField, getEnvVars, getEpicNameCustomFieldFromJiraCloud, getFieldAndMatchingValue, getJiraCloudPriority, getJiraOnPremCustomFieldIdAndType, getMatchingValue, handleUserFieldOptionForJiraCloud, searchIssue, stringToArray } from '../Utils';
import { IssueFieldsUpdate } from '@managed-api/jira-cloud-v3-core/definitions/IssueFields';
import { Attachment } from '@managed-api/jira-on-prem-v8-core/definitions/attachment';
import { AttachmentAsResponse } from '@managed-api/jira-cloud-v3-core/definitions/AttachmentAsResponse';
import { UserDetailsAsResponse } from '@managed-api/jira-cloud-v3-core/definitions/UserDetailsAsResponse';
import { UserFull } from '@managed-api/jira-on-prem-v8-core/definitions/user';
import { GetIssueResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue';
/**
* Entry point to Issue Updated event
*
* @param event Object that holds Issue Updated event data
* @param context Object that holds function invocation context data
*/
export default async function (event: IssueUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
let myself = await JiraOnPremise.Myself.getCurrentUser();
const { JIRA_CLOUD_PROJECT_KEY, FIELDS, CUSTOM_FIELDS, CUSTOM_FIELD_NAME, CHANGE_REASON, CHANGE_RISK, CHANGE_TYPE, IMPACT, ISSUE_TYPES, PRIORITY, STATUS, RetryConfigurations } = getEnvVars(context);
// Check if the current user does not match the person who committed the update and the changelog includes one of the fields that we're interested in
if ((myself.emailAddress !== event.user.emailAddress) && event.changelog?.items?.some(cl => (FIELDS.includes(cl.field ?? '') || CUSTOM_FIELDS.includes(cl.field ?? '')))) {
const syncKeyCustomField = await getCustomField(JiraOnPremise, CUSTOM_FIELD_NAME);
let issue: GetIssueResponseOK | undefined;
let scriptRunnerConnectSyncIssueKey = null;
// Retry logic for finding matching issue
for (let i = 0; i < RetryConfigurations.MAX_RETRY_ATTEMPTS + 1; i++) {
// Get the updated issue
issue = await JiraOnPremise.Issue.getIssue({
issueIdOrKey: event.issue.key ?? '0',
});
if (issue?.fields?.[syncKeyCustomField] !== null) {
// Extract the ScriptRunner Connect Sync Issue Key
scriptRunnerConnectSyncIssueKey = issue?.fields?.[syncKeyCustomField];
break;
} else {
console.log('No sync issue key found. Retrying...');
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
}
}
// Check if the sync key exists
if (scriptRunnerConnectSyncIssueKey === null) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from the target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, JiraCloud, JIRA_CLOUD_PROJECT_KEY, true)
if (issues === null) {
throw Error(`Issue with the matching Stich It Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Extract the issue key that was updated
const issueKey = issues?.issues?.[0].key ?? '';
// Extract the fields that were updated
const eventItems = event.changelog?.items.filter(item => FIELDS.includes(item.field ?? '')) ?? [];
// Object that contains changes that need to be updated on the target instance
let requestBody: IssueFieldsUpdate = {};
// Find the project to use based on pre-defined project key
const project = await JiraCloud.Project.getProject({
projectIdOrKey: JIRA_CLOUD_PROJECT_KEY
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${JIRA_CLOUD_PROJECT_KEY}`);
}
// Get integration user on Jira Cloud
const integrationUserOnJiraCloud = await JiraCloud.Myself.getCurrentUser();
// Add fields and their values to the request body
for (const eventItem of eventItems) {
switch (eventItem.field) {
case 'summary':
// Add summary to request body
requestBody.summary = eventItem.toString ?? '';
break;
case 'issuetype':
// Extract the updated issue type name
const updatedIssueType = event.issue.fields?.issuetype?.name ?? ''
// Find the matching issue type name
const mappedIssueType = await getMatchingValue(JiraOnPremise, updatedIssueType, ISSUE_TYPES);
// Find all the issue types in target instance
const issueTypes = await JiraCloud.Issue.Type.getTypesForProject({
projectId: +(project.id ?? 0) // + sign converts the string to number
});
// Find the issue type
const issueType = issueTypes.find(it => it.name === mappedIssueType);
// Check if the issue type was found
if (!issueType) {
// If not, throw an error
throw Error(`Issue type not found in target instance: ${mappedIssueType}`);
}
// Add issue type to request body
requestBody.issuetype = {
id: issueType.id ?? '0'
};
break;
case 'reporter':
// Extract the reporter
const reporter = event.issue?.fields?.reporter;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const reporterUserFieldOption = await handleUserFieldOptionForJiraCloud(context, 'reporter', reporter?.emailAddress ?? '', reporter?.displayName ?? '', integrationUserOnJiraCloud.accountId ?? '');
// If a value is returned add it to the request body
if (reporterUserFieldOption) {
requestBody = { ...requestBody, ...reporterUserFieldOption }
}
break;
case 'assignee':
// Check if assignee got added
if (eventItem.to) {
// Extract the assignee
const assignee = event.issue?.fields?.assignee;
// Check the user field option value and user field fallback value from Values script and handle the field appropriately
const assigneeUserFieldOption = await handleUserFieldOptionForJiraCloud(context, 'assignee', assignee?.emailAddress, assignee?.displayName ?? '', integrationUserOnJiraCloud.accountId ?? '');
// If a value is returned add it to the request body
if (assigneeUserFieldOption) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
}
} else {
requestBody.assignee = null;
}
break;
case 'status':
// Extract the updated status
const updateIssueStatus = event.issue.fields?.status?.name ?? ''
// Find the matching status
const status = await getMatchingValue(JiraOnPremise, updateIssueStatus, STATUS);
// Get project transitions
const transitions = (await JiraCloud.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
// Find the correct transition ID
const transitionId = transitions.find(t => t.name === status)?.id ?? ''
if (!transitionId) {
throw Error(`Transition ID not found in target instance for status: ${status}`);
}
// Finally change the issue status (workflow transition)
await JiraCloud.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
}
}
});
break;
case 'priority':
// Extract priority
const updateIssuePriority = eventItem.toString ?? '';
// Find the matching priority
const matchingPiority = await getMatchingValue(JiraOnPremise, updateIssuePriority, PRIORITY);
// Find priority from Jira Cloud
const priority = await getJiraCloudPriority(matchingPiority);
// Check if priority was found
if (!priority) {
// If not, throw an error
throw Error(`Priority not found in target instance: ${priority}`)
}
// Add the priority to request body
requestBody.priority = {
id: priority.id ?? '0'
}
break;
case 'Attachment':
// Check if attachment was added or deleted
if (eventItem.to) {
// Extract attachment ID
const attachmentId = eventItem.to ?? '';
// Find the added attachment from issue
const attachment: Attachment | undefined = issue?.fields?.attachment?.find(a => a.id === attachmentId);
if (attachment?.id && attachment.filename) {
// Add the attachment
const storedAttachment = await JiraOnPremise.fetch(`/secure/attachment/${attachment.id}/${attachment.filename}`, {
headers: {
'x-stitch-store-body': 'true'
}
});
if (!storedAttachment.ok) {
throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
}
const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');
if (!storedAttachmentId) {
throw new Error('The attachment stored body is was not returned');
}
await JiraCloud.fetch(`/rest/api/3/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
'X-Atlassian-Token': 'no-check',
'x-stitch-stored-body-id': storedAttachmentId,
'x-stitch-stored-body-form-data-file-name': attachment.filename
}
})
console.log(`Attachment ${attachment.filename} added to issue: ${issueKey}`);
}
} else {
// Find matching attachment ID
const attachmentId = ((issues?.issues?.[0].fields?.attachment as AttachmentAsResponse[])?.find(a => a.filename === eventItem.fromString))?.id ?? '';
if (!attachmentId) {
throw Error('Matching attachment ID was not found from target instance');
}
// Delete the attachment
await JiraCloud.Issue.Attachment.deleteAttachment({
id: attachmentId
})
}
break;
case 'description':
// Check if description got added or removed
if (eventItem.toString) {
requestBody.description = createParagraph(eventItem.toString)
} else {
requestBody.description = null
}
break;
case 'Impact':
// Find the field and matching value in Jira Cloud
const impactValues = await getFieldAndMatchingValue(JiraCloud, eventItem.toString ?? '', IMPACT, eventItem.field);
// Add the Impact field to request body
if (eventItem.to) {
requestBody[impactValues.field] = {
value: impactValues.matchingValue
};
} else {
requestBody[impactValues.field] = null;
}
break;
case 'Change reason':
// Find the field and matching value in Jira Cloud
const changeReasonValues = await getFieldAndMatchingValue(JiraCloud, eventItem.toString ?? '', CHANGE_REASON, eventItem.field);
if (eventItem.to) {
// Add the Change reason field to request body
requestBody[changeReasonValues.field] = {
value: changeReasonValues.matchingValue
};
} else {
requestBody[changeReasonValues.field] = null;
}
break;
case 'Change type':
// Find the field and matching value in Jira Cloud
const changeTypeValues = await getFieldAndMatchingValue(JiraCloud, eventItem.toString ?? '', CHANGE_TYPE, eventItem.field);
if (eventItem.to) {
// Add the Change type field to request body
requestBody[changeTypeValues.field] = {
value: changeTypeValues.matchingValue
};
} else {
requestBody[changeTypeValues.field] = null;
}
break;
case 'Change risk':
// Find the field and matching value in Jira Cloud
const changeRiskValues = await getFieldAndMatchingValue(JiraCloud, eventItem.toString ?? '', CHANGE_RISK, eventItem.field);
if (eventItem.to) {
// Add the Change risk field to request body
requestBody[changeRiskValues.field] = {
value: changeRiskValues.matchingValue
};
} else {
requestBody[changeRiskValues.field] = null;
}
break;
case 'labels':
// Add updated labels to request body
requestBody.labels = event.issue.fields?.labels;
break;
case 'duedate':
// Add updated due date to request body
requestBody.duedate = event.issue.fields?.duedate ?? null;
break;
case 'Epic Name':
// Find the custom field from target instance and save the updated Epic Name
const jiraCloudCustomFieldId = await getEpicNameCustomFieldFromJiraCloud(project.id ?? '0');
// Add the updated Epic Name to request body
requestBody[jiraCloudCustomFieldId] = eventItem.toString
break;
default:
break;
}
}
// Filter custom fields that were updated and exist in the CUSTOM_FIELDS array in Values script
const updatedCustomFields = event.changelog?.items.filter(cl => CUSTOM_FIELDS.includes(cl.field ?? ''));
// Check if any custom field got updated
if (updatedCustomFields.length) {
// Iterate through updated custom fields
for (const customField of updatedCustomFields) {
// Get custom field id and type
const jiraOnPremCustomField = await getJiraOnPremCustomFieldIdAndType(customField.field ?? '');
if (!jiraOnPremCustomField) {
throw Error(`Field ${customField} not found on Jira On-Premise instance`);
}
// Extract the custom field value
const fieldValue = event.issue.fields?.[jiraOnPremCustomField.id];
// Find the custom field in target instance
const jiraCloudCustomFieldId = await getCustomField(JiraCloud, customField.field ?? '');
// Check the type and value of each custom field to appropriately update the request body
switch (jiraOnPremCustomField.type) {
case 'Text Field (single line)':
case 'Number Field':
case 'URL Field':
case 'Date Picker':
case 'Date Time Picker':
case 'Labels':
requestBody[jiraCloudCustomFieldId] = fieldValue;
break;
case 'Text Field (multi-line)':
if (!fieldValue) {
requestBody[jiraCloudCustomFieldId] = null;
} else {
requestBody[jiraCloudCustomFieldId] = createParagraph(fieldValue);
}
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
if (!fieldValue) {
requestBody[jiraCloudCustomFieldId] = null;
} else {
requestBody[jiraCloudCustomFieldId] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
}
break;
case 'Select List (cascading)':
if (!fieldValue) {
requestBody[jiraCloudCustomFieldId] = null;
} else {
requestBody[jiraCloudCustomFieldId] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
}
break;
case 'Select List (single choice)':
if (!fieldValue) {
requestBody[jiraCloudCustomFieldId] = null;
} else {
requestBody[jiraCloudCustomFieldId] = {
value: fieldValue.value
}
}
break;
case 'User Picker (multiple users)':
// Get the target Issue in Jira Cloud
const targetIssue = await JiraCloud.Issue.getIssue({
issueIdOrKey: issueKey,
})
// Extract users that are added on the Jira Cloud issue
const currentlyAddedUsersOnJiraCloud: UserDetailsAsResponse[] | null = targetIssue.fields?.[jiraCloudCustomFieldId];
let accountsToAdd: string[] = [];
let accountsToRemove: string[] = [];
// Verify whether any users have been added and if the fieldValue is not null
if (currentlyAddedUsersOnJiraCloud === null && fieldValue !== null) {
// Iterate through added users and extract their email
const users = (fieldValue as { emailAddress?: string }[]).map(field => ({
emailAddress: field.emailAddress ?? '',
}));
// Check if user with that email is assignable in Jira Cloud
await Promise.all(users.map(async (user) => {
const assignableAccount = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, user.emailAddress);
accountsToAdd.push(assignableAccount ?? '')
}));
// If theres valid accounts add them on Jira Cloud issue
if (accountsToAdd.length > 0) {
requestBody[jiraCloudCustomFieldId] = accountsToAdd.map(acc => ({
accountId: acc
}));
}
} else {
// Extract the original accounts from the issue
const originalListOfAccountNames = await stringToArray(customField.fromString ?? '')
// Extract the updated Accounts from the issue
const listOfAccountNames = await stringToArray(customField.toString ?? '');
// Filter which accounts got removed and which added
const removedAccounts = originalListOfAccountNames.filter(name => !listOfAccountNames.includes(name));
const addedAccounts = listOfAccountNames.filter(name => !originalListOfAccountNames.includes(name));
// Check which accounts are currently added in the target instance
const currentlyAddedAccountIds = currentlyAddedUsersOnJiraCloud?.map(field => ({
accountId: field.accountId,
}));
// If any accounts got removed, add them to the accountsToRemove array
if (removedAccounts.length > 0) {
const userDisplayName = removedAccounts.map(acc => ({
displayName: acc
}))
// Get Jira On-Premise users using displayName
const usersThatGotRemoved = await checkAccountsOnJiraOnPrem(context, userDisplayName);
// Iterate through all the users that got removed and use their email to search valid accounts on Jira Cloud
await Promise.all(usersThatGotRemoved.map(async (user) => {
const validUser = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, user.emailAddress ?? '');
// Add valid users to accountsToRemove array
if (validUser) {
accountsToRemove.push(validUser);
}
}));
}
// If any users got added, add them to the accountsToAdd array
if (addedAccounts.length > 0) {
const addedUsers = (fieldValue as UserFull[])?.filter(u => addedAccounts.includes(u.displayName ?? '0')).map(user => ({
emailAddress: user.emailAddress ?? '',
}));
// Iterate through all the users that got added and use their email to search valid accounts on Jira Cloud
await Promise.all(addedUsers.map(async (user) => {
const validUser = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, user.emailAddress);
// Add valid users to accountsToAdd array
if (validUser) {
accountsToAdd.push(validUser);
}
}));
}
// New list of accounts, filtering out accounts that need to be removed and adding new ones
const newList = currentlyAddedAccountIds?.filter(item => !accountsToRemove.includes(item.accountId ?? '')).map(item => item.accountId ?? '');
newList?.push(...accountsToAdd);
const accounts = newList?.map(value => ({ accountId: value }));
// Add necessary accounts to the request body
requestBody[jiraCloudCustomFieldId] = accounts;
}
break;
case 'User Picker (single user)':
// Check if user got added or removed
if (!fieldValue) {
requestBody[jiraCloudCustomFieldId] = null;
} else {
// Find accountId for user using email
const accountId = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, fieldValue.emailAddress);
// If accountId is found, add it to the request body
if (accountId) {
requestBody[jiraCloudCustomFieldId] = {
accountId: accountId
}
} else {
// If not, change the field value to null
requestBody[jiraCloudCustomFieldId] = null
}
}
break;
default:
break;
}
}
}
// If there is any fields in requestBody, update the issue in target instance
if (Object.keys(requestBody).length > 0) {
await JiraCloud.Issue.editIssue({
issueIdOrKey: issueKey,
body: {
fields: requestBody,
}
});
console.log(`Updated issue: ${issueKey}`);
}
}
}
import { RecordStorage } from '@sr-connect/record-storage';
import { GetKeysOfAllRecordsResponse } from '@sr-connect/record-storage/types';
import { throttleAll } from 'promise-throttle-all';
// How many concurrent deletion jobs to maintain
const CONCURRENCY = 3;
/**
* This script purges all cached data.
*/
export default async function (event: any, context: Context): Promise<void> {
const storage = new RecordStorage();
let lastEvaluatedKey: string | undefined;
do {
const keys: GetKeysOfAllRecordsResponse = await storage.getAllKeys();
lastEvaluatedKey = keys.lastEvaluatedKey;
await throttleAll(CONCURRENCY, keys.keys.map((key) => async () => await storage.deleteValue(key)));
} while (lastEvaluatedKey !== undefined);
}
import JiraOnPremise from './api/jira/on-premise';
import JiraCloud from './api/jira/cloud';
import { RecordStorage } from '@sr-connect/record-storage';
import { JiraOnPremApi } from '@managed-api/jira-on-prem-v8-sr-connect';
import { FindAssignableUsersResponseOK } from '@managed-api/jira-on-prem-v8-core/types/user/search';
import { UserFull } from '@managed-api/jira-on-prem-v8-core/definitions/user';
import { JiraCloudApi } from '@managed-api/jira-cloud-v3-sr-connect';
import { SearchIssuePrioritiesResponseOK } from '@managed-api/jira-cloud-v3-core/types/issue/priority';
import { GetIssueCustomFieldsResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue/field/custom';
import { IssueDeletedEvent as JiraCloudIssueDeletedEvent } from "@sr-connect/jira-cloud/events";
import { IssueDeletedEvent as JiraOnPremIssueDeletedEvent } from "@sr-connect/jira-on-premise/events";
import { IssueCommentCreatedEvent as JiraCloudIssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import { IssueCommentCreatedEvent as JiraOnPremIssueCommentCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { AddIssueCommentResponseOK as JiraOnPremAddIssueComment } from '@managed-api/jira-on-prem-v8-core/types/issue/comment';
import { AddIssueCommentResponseOK as JiraCloudAddIssueComment } from '@managed-api/jira-cloud-v3-core/types/issue/comment';
import { IssueCommentDeletedEvent as JiraCloudIssueCommentDeletedEvent } from '@sr-connect/jira-cloud/events';
import { IssueCommentDeletedEvent as JiraOnPremIssueCommentDeletedEvent } from '@sr-connect/jira-on-premise/events';
import { IssueCommentUpdatedEvent as JiraCloudIssueCommentUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { IssueCommentUpdatedEvent as JiraOnPremIssueCommentUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import { IssueLinkCreatedEvent as JiraCloudIssueLinkCreatedEvent } from '@sr-connect/jira-cloud/events';
import { IssueLinkCreatedEvent as JiraOnPremIssueLinkCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { IssueLinkDeletedEvent as JiraCloudIssueLinkDeletedEvent } from '@sr-connect/jira-cloud/events';
import { IssueLinkDeletedEvent as JiraOnPremIssueLinkDeletedEvent } from '@sr-connect/jira-on-premise/events';
/**
* Function that finds matching value for STATUS, PRIORITY, ISSUE TYPE, IMPACT, CHANGE_REASON, CHANGE_TYPE, CHANGE_RISK
*/
export async function getMatchingValue(sourceInstance: JiraOnPremApi | JiraCloudApi, value: string, property: Record<string, string>) {
const matchingValue = sourceInstance === JiraCloud ? property[value] : Object.keys(property).find(key => property[key] === value) ?? '';
return matchingValue
}
/**
* Function that finds the priority from Jira Cloud (traverses paginated results)
*/
export async function getJiraCloudPriority(value: string) {
const maxResults = '50';
let startAt = '0';
let priorities: SearchIssuePrioritiesResponseOK;
do {
priorities = await JiraCloud.Issue.Priority.searchPriorities({
startAt,
maxResults
});
const priority = priorities.values?.find(p => p.name === value);
if (priority) {
return priority;
}
if (priorities.values?.length === +maxResults) {
startAt = (+startAt + +maxResults).toString();
} else {
startAt = '0'
}
} while (+startAt > 0);
}
/**
* Function that tries to retrieve issue comments from the cache (using RecordStorage)
*/
export async function getComments(storage: RecordStorage, scriptRunnerConnectIssueKey: string) {
return await storage.getValue<Record<string, string>[]>(scriptRunnerConnectIssueKey) ?? [];
}
/**
* Function that tries to retrieve issue links from the cache (using RecordStorage)
*/
export async function getIssueLinks(storage: RecordStorage, scriptRunnerConnectIssueKey: string) {
return await storage.getValue<Record<string, string>[]>(`issue_link_${scriptRunnerConnectIssueKey}`) ?? [];
}
/**
* Function that tries to set issue links to the cache (using RecordStorage)
*/
export async function setIssueLinks(storage: RecordStorage, scriptRunnerConnectIssueKey: string, values: { [key: string]: string }[]) {
return await storage.setValue(`issue_link_${scriptRunnerConnectIssueKey}`, values);
}
/**
* Function that tries to retrieve epic links from the cache (using RecordStorage)
*/
export async function getEpicLink(storage: RecordStorage, scriptRunnerConnectIssueKey: string) {
return await storage.getValue<EpicLink>(`epic_${scriptRunnerConnectIssueKey}`);
}
/**
* Function that tries to set epic links to the cache (using RecordStorage)
*/
export async function setEpicLink(storage: RecordStorage, scriptRunnerConnectIssueKey: string, value: EpicLink) {
return await storage.setValue(`epic_${scriptRunnerConnectIssueKey}`, value);
}
/**
* Function that finds the ScriptRunner Connect Sync Issue Key custom field value from the issue
*/
export async function getScriptRunnerConnectSyncIssueKey(context: Context, issueIdOrKey: string, instance: JiraOnPremApi | JiraCloudApi): Promise<string | null> {
const { CUSTOM_FIELD_NAME, RetryConfigurations } = getEnvVars(context);
const customField = await getCustomField(instance, CUSTOM_FIELD_NAME);
for (let i = 0; i < RetryConfigurations.MAX_RETRY_ATTEMPTS + 1; i++) {
const scriptRunnerConnectSyncIssueKey = (await instance.Issue.getIssue({
issueIdOrKey: issueIdOrKey,
})).fields?.[customField];
if (scriptRunnerConnectSyncIssueKey !== null) {
return scriptRunnerConnectSyncIssueKey;
} else {
console.log('No sync issue key found. Retrying...');
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
}
}
return null;
}
/**
* Function that finds the issue using ScriptRunner Connect Sync Issue Key custom field
*/
export async function searchIssue(context: Context, issueKey: string, instance: JiraOnPremApi | JiraCloudApi, projectKey: string, includeAttachments?: boolean) {
const { CUSTOM_FIELD_NAME, RetryConfigurations } = getEnvVars(context);
const includedFields = includeAttachments ? ['attachment'] : [];
for (let i = 0; i < RetryConfigurations.MAX_RETRY_ATTEMPTS + 1; i++) {
const issues = await instance.Issue.Search.searchByJql({
body: {
jql: `project = ${projectKey} AND "${CUSTOM_FIELD_NAME}" ~ "${issueKey}"`,
fields: includedFields
}
})
if (issues?.issues?.length === 0) {
console.log('No matching issue found. Retrying...');
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
} else {
return issues;
}
}
return null;
}
/**
* Function that finds the custom fields ID
*/
export async function getCustomField(instance: JiraOnPremApi | JiraCloudApi, customFieldName: string): Promise<string> {
const customField = (await instance.Issue.Field.getFields()).filter(f => f.name === customFieldName);
if (customField.length === 0) {
throw Error(`Custom field '${customFieldName}' was not found for this instance`)
}
if (customField.length > 1) {
throw Error(`More than one custom field was found with this name: ${customFieldName}`)
}
return customField?.[0].id ?? '';
}
/**
* Function that finds the ID and type for Jira On-Premise custom field
*/
export async function getJiraOnPremCustomFieldIdAndType(customFieldName: string): Promise<{ id: string, type: string } | undefined> {
const maxResults = 50;
let startAt = 0;
let customFields: GetIssueCustomFieldsResponseOK;
do {
customFields = await JiraOnPremise.Issue.Field.Custom.getFields({
startAt,
maxResults
});
const customField = customFields.values?.find(f => f.name === customFieldName);
if (customField) {
return {
id: customField.id ?? '',
type: customField.type ?? ''
}
}
if (customFields.values?.length === maxResults) {
startAt = startAt + maxResults
} else {
startAt = 0
}
} while (startAt > 0);
}
/**
* Function that finds 'Epic Name' custom field from Jira Cloud
*/
export async function getEpicNameCustomFieldFromJiraCloud(projectId: string): Promise<string> {
const customField = (await JiraCloud.Issue.Field.getFields()).filter(f => f.name === 'Epic Name');
if (customField.length === 0) {
throw Error('Epic Name field for instance not found')
}
if (customField.length > 1) {
const epicNameCustomField = customField.find(field => field.scope?.project?.id === projectId)?.id ?? '';
if (!epicNameCustomField) {
throw Error('Epic Name field for project not found')
}
return epicNameCustomField;
}
return customField?.[0].id ?? '0';
}
/**
* Function that finds the ID for custom fields and matching value in target instance
*/
export async function getFieldAndMatchingValue(targetInstance: JiraOnPremApi | JiraCloudApi, eventValue: string | null, valuesType: ValuesType, fieldName: string): Promise<{ field: string; matchingValue: string; }> {
const matchingValue = await getMatchingValue(targetInstance === JiraOnPremise ? JiraCloud : JiraOnPremise, eventValue ?? '', valuesType);
// Find field name from target instance
const field = await getCustomField(targetInstance, fieldName);
if (!field) {
throw Error(`${fieldName} field does not exist`)
}
return {
field,
matchingValue
}
}
/**
* Function that handles string value and changes it to string array
*/
export async function stringToArray(originalString: string): Promise<string[]> {
if (originalString.trim() === "" || originalString.trim() === "[]") {
return [];
}
const trimmedString = originalString.trim().replace(/^\[|\]$/g, '');
return trimmedString.split(',').map(item => item.trim());
}
/**
* Function that checks accounts on Jira On-Premise
*/
export async function checkAccountsOnJiraOnPrem(context: Context, users: {
emailAddress?: string,
displayName: string
}[]): Promise<UserFull[]> {
const { JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
const maxResults = 50;
let startAt = 0;
let validUsers: UserFull[] = [];
do {
const response = await JiraOnPremise.User.Search.findAssignableUsers({
startAt,
maxResults,
project: JIRA_ON_PREM_PROJECT_KEY
});
for (const user of users) {
const validUser = user.emailAddress ?
response.find(u => u.emailAddress === user.emailAddress) :
response.find(u => u.displayName === user.displayName);
if (validUser) {
validUsers.push(validUser);
}
}
startAt = response.length === maxResults ? startAt + maxResults : 0;
} while (startAt > 0);
return validUsers;
}
/**
* Function that checks userFieldOption value and changes the field accordingly for Jira On-Premise
*/
export async function handleUserFieldOptionForJiraOnPrem(context: Context, fieldName: string, userEmail: string | undefined, userDisplayName: string, integrationUser: UserFull): Promise<UserField | undefined> {
const { PredefinedUser, UserFieldOptions } = getEnvVars(context);
switch (UserFieldOptions.USER_FIELD_OPTION) {
case 'ORIGINAL_USER':
const assignableUser = await findAssignableUserOnJiraOnPrem(context, userEmail ?? '', userDisplayName);
return assignableUser ? { [fieldName]: { name: assignableUser.name ?? '' } } : await handleUserFieldFallbackValueForJiraOnPrem(context, fieldName, userDisplayName, integrationUser.name ?? '');
case 'REMAIN_UNASSIGNED':
return fieldName === 'reporter' ? { [fieldName]: { name: integrationUser.name } } : { [fieldName]: null }
case 'INTEGRATION_USER':
return {
[fieldName]: { name: integrationUser.name }
}
case 'PREDEFINED_USER':
const isPredefinedUserAssignable = await findAssignableUserOnJiraOnPrem(context, PredefinedUser.JIRA_ON_PREM_PREDEFINED_USER_EMAIL);
return isPredefinedUserAssignable ? await handlePredefinedUserForJiraOnPrem(context, fieldName) : handleUserFieldFallbackValueForJiraOnPrem(context, fieldName, userDisplayName, integrationUser.name ?? '');
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserCustomFieldId(context, JiraOnPremise, fieldName);
return {
[customFieldId]: userDisplayName
};
default:
return;
}
}
/**
* Function that checks userFieldFallbackOption value and changes the field accordingly for Jira On-Premise
*/
export async function handleUserFieldFallbackValueForJiraOnPrem(context: Context, fieldName: string, user: string, integrationUser: string): Promise<UserField | undefined> {
const { UserFieldOptions } = getEnvVars(context);
switch (UserFieldOptions.USER_FIELD_FALLBACK_OPTION) {
case 'REMAIN_UNASSIGNED':
// If fallback value is REMAIN_UNASSIGNED, reporter field will be integration user
return fieldName === 'reporter' ? { [fieldName]: { name: integrationUser } } : { [fieldName]: null }
case 'INTEGRATION_USER':
return { [fieldName]: { name: integrationUser } };
case 'PREDEFINED_USER':
return await handlePredefinedUserForJiraOnPrem(context, fieldName);
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserCustomFieldId(context, JiraOnPremise, fieldName);
return {
[customFieldId]: user
};
case 'HALT_SYNC':
throw Error(`Script halted because user for field ${fieldName} was not found.`)
default:
return;
}
}
/**
* Function that checks if user can be added to the issue on Jira On-Prem
*/
export async function findAssignableUserOnJiraOnPrem(context: Context, email?: string, displayName?: string): Promise<UserFull | null> {
const maxResults = 50;
let startAt = 0;
let users: FindAssignableUsersResponseOK;
const { JIRA_ON_PREM_PROJECT_KEY } = getEnvVars(context);
do {
users = await JiraOnPremise.User.Search.findAssignableUsers({
startAt,
maxResults,
project: JIRA_ON_PREM_PROJECT_KEY
});
let user: UserFull | undefined;
email ? user = users.find(u => u.emailAddress === email) : user = users.find(u => u.displayName === displayName);
if (user) {
return user;
}
if (users.length === maxResults) {
startAt = startAt + maxResults;
} else {
startAt = 0;
}
} while (startAt > 0);
return null;
}
/**
* Function that checks if account can be added to issue on Jira Cloud
*/
export async function findAssignableUserOnJiraCloud(jiraCloudProjectKey: string, email: string): Promise<string | undefined> {
const user = await JiraCloud.User.Search.findUsersAssignableToIssues({
project: jiraCloudProjectKey,
query: `${email}`
})
return user[0]?.accountId
}
/**
* Function that copies original user to custom field
*/
export async function getOriginalUserCustomFieldId(context: Context, targetInstance: JiraOnPremApi | JiraCloudApi, fieldName: string): Promise<string> {
const { OriginalUserFields } = getEnvVars(context);
const customFieldName = fieldName === 'assignee' ? OriginalUserFields.CUSTOM_FIELD_FOR_ASSIGNEE : OriginalUserFields.CUSTOM_FIELD_FOR_REPORTER;
const customFieldId = await getCustomField(targetInstance, customFieldName ?? '');
return customFieldId
}
/**
* Function that handles predefined user for Jira On-Premise
*/
export async function handlePredefinedUserForJiraOnPrem(context: Context, fieldName: string): Promise<UserField> {
const { PredefinedUser } = getEnvVars(context);
if (!PredefinedUser.JIRA_ON_PREM_PREDEFINED_USER_EMAIL) {
throw Error('Missing predifined user email')
};
const user = await findAssignableUserOnJiraOnPrem(context, PredefinedUser.JIRA_ON_PREM_PREDEFINED_USER_EMAIL);
if (!user) {
throw Error('Predifined user cannot be set')
}
return {
[fieldName]: { name: user.name }
};
}
/**
* Function that checks userFieldOption value and changes the field accordingly for Jira Cloud
*/
export async function handleUserFieldOptionForJiraCloud(context: Context, fieldName: string, userEmail: string | undefined, userDisplayName: string, integrationUserAccountId: string): Promise<UserField | undefined> {
const { UserFieldOptions, JIRA_CLOUD_PROJECT_KEY, PredefinedUser } = getEnvVars(context);
switch (UserFieldOptions.USER_FIELD_OPTION) {
case 'ORIGINAL_USER':
const assignableUser = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, userEmail ?? '');
return assignableUser ? { [fieldName]: { accountId: assignableUser } } : await handleUserFieldFallbackValueForJiraCloud(context, fieldName, userDisplayName, integrationUserAccountId);
case 'REMAIN_UNASSIGNED':
return fieldName === 'reporter' ? { [fieldName]: { accountId: integrationUserAccountId } } : { [fieldName]: null }
case 'INTEGRATION_USER':
return {
[fieldName]: { accountId: integrationUserAccountId }
}
case 'PREDEFINED_USER':
const isPredefinedUserAssignable = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, PredefinedUser.JIRA_CLOUD_PREDEFINED_USER_EMAIL ?? '');
return isPredefinedUserAssignable ? await handlePredefinedUserForJiraCloud(context, fieldName) : handleUserFieldFallbackValueForJiraOnPrem(context, fieldName, userDisplayName, integrationUserAccountId ?? '');
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserCustomFieldId(context, JiraCloud, fieldName);
return {
[customFieldId]: userDisplayName
};
default:
return;
}
}
/**
* Function that checks userFieldFallbackOption value and changes the field accordingly for Jira Cloud
*/
export async function handleUserFieldFallbackValueForJiraCloud(context: Context, fieldName: string, userDisplayName: string, integrationUserAccountId: string): Promise<UserField | undefined> {
const { UserFieldOptions } = getEnvVars(context);
switch (UserFieldOptions.USER_FIELD_FALLBACK_OPTION) {
case 'REMAIN_UNASSIGNED':
// If fallback value is REMAIN_UNASSIGNED, reporter field will be integration user
return fieldName === 'reporter' ? { [fieldName]: { accountId: integrationUserAccountId } } : { [fieldName]: null }
case 'INTEGRATION_USER':
return { [fieldName]: { accountId: integrationUserAccountId } };
case 'PREDEFINED_USER':
return await handlePredefinedUserForJiraCloud(context, fieldName);
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserCustomFieldId(context, JiraCloud, fieldName);
return {
[customFieldId]: userDisplayName
};
case 'HALT_SYNC':
throw Error(`Script halted because user for field ${fieldName} was not found.`)
default:
return;
}
}
/**
* Function that handles predefined user for Jira Cloud
*/
export async function handlePredefinedUserForJiraCloud(context: Context, fieldName: string): Promise<UserField> {
const { JIRA_CLOUD_PROJECT_KEY, PredefinedUser } = getEnvVars(context);
if (!PredefinedUser.JIRA_CLOUD_PREDEFINED_USER_EMAIL) {
throw Error('Missing predifined user email')
};
const accountId = await findAssignableUserOnJiraCloud(JIRA_CLOUD_PROJECT_KEY, PredefinedUser.JIRA_CLOUD_PREDEFINED_USER_EMAIL);
if (!accountId) {
throw Error('Predifined user cannot be set')
}
return {
[fieldName]: { accountId: accountId }
};
}
/**
* Function that creates a paragraph for Jira Cloud
*/
export const createParagraph = (text: string) => {
return {
type: 'doc' as const,
version: 1 as const,
content: [
{
type: 'paragraph' as const,
content: [
{
type: 'text' as const,
text: text
}
]
}
]
}
}
/**
* Function that extracts issue ID from URL
*/
export function extractIssueId(url: string): string | null {
const match = url.match(/\/issue\/(\d+)/);
return match ? match[1] : null;
}
/**
* Checks if users are assignable to the project and saves their email and display name.
*/
export async function getUsersFromJiraCloud(jiraCloudProjectKey: string, accountIds: string[]): Promise<JiraCloudUserInfo[]> {
let validUsers: JiraCloudUserInfo[] = [];
await Promise.all(accountIds.map(async (id) => {
const user = await JiraCloud.User.Search.findUsersAssignableToProjects({
projectKeys: jiraCloudProjectKey,
accountId: id
})
if (user.length > 0) {
validUsers.push({
emailAddress: user[0].emailAddress ?? '',
displayName: user[0].displayName ?? ''
});
};
}));
return validUsers
}
/**
* Function that handles deleted issue
*/
export async function deleteIssue(context: Context, event: JiraCloudIssueDeletedEvent | JiraOnPremIssueDeletedEvent, sourceInstance: JiraOnPremApi | JiraCloudApi, targetInstance: JiraOnPremApi | JiraCloudApi, targetProjectKey: string): Promise<void> {
const { CUSTOM_FIELD_NAME, FIELDS } = getEnvVars(context);
const customField = await getCustomField(sourceInstance, CUSTOM_FIELD_NAME);
// Get the ScriptRunner Connect Sync Issue Key from deleted issue
const scriptRunnerConnectSyncIssueKey = event.issue.fields?.[customField] as string | undefined;
if (!scriptRunnerConnectSyncIssueKey) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from the other project
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, targetProjectKey);
if (issues === null) {
throw Error(`Issue with the matching Stich It Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
}
const issueKey = issues?.issues?.[0].key ?? '';
// Delete the issue from the target instance
await targetInstance.Issue.deleteIssue({
issueIdOrKey: issueKey,
deleteSubtasks: 'true'
});
const storage = new RecordStorage();
if (FIELDS.includes('comments')) {
// Check if there are any comments cached with this ScriptRunnerConnect Sync Issue Key
const comments = await storage.valueExists(scriptRunnerConnectSyncIssueKey);
// If there are then delete them
if (comments) {
await storage.deleteValue(scriptRunnerConnectSyncIssueKey)
}
}
if (FIELDS.includes('issue links')) {
// Check if there are any issue links cached with this ScriptRunnerConnect Sync Issue Key
const issueLinks = await storage.valueExists(`issue_link_${scriptRunnerConnectSyncIssueKey}`);
// If there are then delete them
if (issueLinks) {
await storage.deleteValue(`issue_link_${scriptRunnerConnectSyncIssueKey}`)
}
}
if (FIELDS.includes('IssueParentAssociation')) {
// Check if there are any issue links cached with this ScriptRunnerConnect Sync Issue Key
const epicLink = await storage.valueExists(`epic_${scriptRunnerConnectSyncIssueKey}`);
// If there are then delete them
if (epicLink) {
await storage.deleteValue(`epic_${scriptRunnerConnectSyncIssueKey}`)
}
}
console.log(`Deleted Issue: ${issueKey}`)
}
/**
* Function that handles created comment
*/
export async function createComment(context: Context, event: JiraCloudIssueCommentCreatedEvent | JiraOnPremIssueCommentCreatedEvent, sourceInstance: JiraCloudApi | JiraOnPremApi, targetInstance: JiraCloudApi | JiraOnPremApi, sourceProjectKey: string, targetProjectKey: string,): Promise<void> {
// Extract issue ID or Key from self property
const issueIdOrKey = sourceInstance === JiraOnPremise ? extractIssueId((event as JiraOnPremIssueCommentCreatedEvent).comment.self ?? '0') : (event as JiraCloudIssueCommentCreatedEvent).issue.key;
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, issueIdOrKey ?? '0', sourceInstance);
if (scriptRunnerConnectSyncIssueKey === null) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, targetProjectKey);
// Check if issue with a matching ScriptRunner Connect Sync Issue Key exists
if (issues === null) {
// If not, throw an error
throw Error(`Issue with the matching ScriptRunner Connect Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Extract the issue key
const issueKey = issues?.issues?.[0].key ?? '';
// Create a new comment in target instance
let createdComment: JiraOnPremAddIssueComment | JiraCloudAddIssueComment;
if (sourceInstance === JiraCloud) {
createdComment = await JiraOnPremise.Issue.Comment.addComment({
issueIdOrKey: issueKey,
body: {
body: event.comment.body
}
})
} else {
createdComment = await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: issueKey,
body: {
body: createParagraph(event.comment.body ?? '')
}
})
}
const storage = new RecordStorage();
// Get existing comments from Record Storage with the same ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, scriptRunnerConnectSyncIssueKey);
const commentIds = {
[sourceProjectKey]: event.comment.id,
[targetProjectKey]: createdComment.id
}
// Check if existing comments exist
if (!comments) {
// If they don't, create a new record
await storage.setValue(scriptRunnerConnectSyncIssueKey, commentIds);
} else {
// Id they do, update the existing record
const updatedComments = [...comments, commentIds];
await storage.setValue(scriptRunnerConnectSyncIssueKey, updatedComments);
}
console.log(`Comment created for Issue: ${issueKey}`)
}
/**
* Function that handles deleted comment
*/
export async function deleteComment(context: Context, event: JiraCloudIssueCommentDeletedEvent | JiraOnPremIssueCommentDeletedEvent, sourceInstance: JiraCloudApi | JiraOnPremApi, targetInstance: JiraCloudApi | JiraOnPremApi, sourceProjectKey: string, targetProjectKey: string): Promise<void> {
// Extract the issue ID or Key
const issueIdOrKey = sourceInstance === JiraOnPremise ? extractIssueId((event as JiraOnPremIssueCommentDeletedEvent).comment.self ?? '0') : (event as JiraCloudIssueCommentDeletedEvent).issue.key;
// Search for the updated issue
const issueExists = await sourceInstance.Issue.getIssue<null>({
issueIdOrKey: issueIdOrKey ?? '0',
errorStrategy: {
handleHttp404Error: () => null
}
})
// Check if the issue still exists
if (issueExists) {
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, issueIdOrKey ?? '0', sourceInstance);
if (scriptRunnerConnectSyncIssueKey === null) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
const storage = new RecordStorage();
// Get comments from Record Storage with the ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, scriptRunnerConnectSyncIssueKey);
// Find the comment ID for the target instance that matches the comment that got deleted
const commentId = comments.find(c => c[sourceProjectKey] === event.comment.id)?.[targetProjectKey];
// Check if the matching comment ID was found
if (!commentId) {
// If not, throw an error
throw Error(`Couldn't find comment ID from Record Storage: ${commentId}. Comment might already be deleted`);
};
// Find the matching issue from the target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, targetProjectKey);
// Check if issue with a matching ScriptRunner Connect Issue Key exists
if (issues === null) {
// If not, throw an error
throw Error(`Issue with the matching Stich It Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Extract the issue key
const issueKey = issues?.issues?.[0].key ?? '';
// Delete the comment
await targetInstance.Issue.Comment.deleteComment({
issueIdOrKey: issueKey,
id: commentId ?? '0'
})
// Check if issue has any comments left
if (comments.length > 1) {
// If it has, then remove the deleted comment from Record Storage
const updatedComments = comments.filter(c => c[sourceProjectKey] !== event.comment.id);
await storage.setValue(scriptRunnerConnectSyncIssueKey, updatedComments);
} else {
// If not, then delete the value from Record Storage since we don't need to keep it there anymore
await storage.deleteValue(scriptRunnerConnectSyncIssueKey);
}
console.log(`Comment deleted for Issue: ${issueKey}`);
}
}
/**
* Function that handles updated comment
*/
export async function updateComment(context: Context, event: JiraCloudIssueCommentUpdatedEvent | JiraOnPremIssueCommentUpdatedEvent, sourceInstance: JiraCloudApi | JiraOnPremApi, targetInstance: JiraCloudApi | JiraOnPremApi, sourceProjectKey: string, targetProjectKey: string): Promise<void> {
// Extract the issue ID or key
const issueIdOrKey = sourceInstance === JiraOnPremise ? extractIssueId((event as JiraOnPremIssueCommentUpdatedEvent).comment.self ?? '0') : (event as JiraCloudIssueCommentUpdatedEvent).issue.key;
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, issueIdOrKey ?? '0', sourceInstance);
if (scriptRunnerConnectSyncIssueKey === null) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
const storage = new RecordStorage();
// Get matching comment IDs from Record Storage
const commentIds = await getComments(storage, scriptRunnerConnectSyncIssueKey);
const commentId = commentIds.find(ci => ci[sourceProjectKey] === event.comment.id)?.[targetProjectKey];
if (!commentId) {
throw Error(`Couldn't find correct comment ID from Record Storage`);
};
// Find the matching issue from the target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, targetProjectKey);
// Check if issue with a matching ScriptRunner Connect Sync Issue Key exists
if (issues === null) {
// If not, throw an error
throw Error(`Issue with the matching Stich It Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Save the issue key
const issueKey = issues?.issues?.[0].key ?? '';
// Update the comment
if (sourceInstance === JiraCloud) {
await JiraOnPremise.Issue.Comment.updateComment({
id: commentId,
issueIdOrKey: issueKey,
body: {
body: event.comment.body
}
});
} else {
await JiraCloud.Issue.Comment.updateComment({
id: commentId,
issueIdOrKey: issueKey,
body: {
body: createParagraph(event.comment.body ?? '')
}
});
}
console.log(`Comment updated for Issue: ${issueKey}`)
}
/**
* Function that handles created issue link
*/
export async function createIssueLink(context: Context, event: JiraCloudIssueLinkCreatedEvent | JiraOnPremIssueLinkCreatedEvent, sourceInstance: JiraCloudApi | JiraOnPremApi, targetInstance: JiraCloudApi | JiraOnPremApi, sourceProjectKey: string, targetProjectKey: string): Promise<void> {
// Get the Issue Sync Issue Keys for both inward and outward linked issue
const inwardIssueSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, (event.issueLink.sourceIssueId ?? 0).toString(), sourceInstance);
const outwardIssueSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(context, (event.issueLink.destinationIssueId ?? 0).toString(), sourceInstance);
// Check if sync issue keys for both issues exist
if (inwardIssueSyncIssueKey && outwardIssueSyncIssueKey) {
// Check if the matching issue has been created
const matchingInwardIssue = await searchIssue(context, inwardIssueSyncIssueKey, targetInstance, targetProjectKey, false);
const matchingOutwardIssue = await searchIssue(context, outwardIssueSyncIssueKey, targetInstance, targetProjectKey, false);
if (!matchingInwardIssue?.issues?.length || !matchingOutwardIssue?.issues?.length) {
console.log('Issue link cannot be created, matching issue is missing.')
return;
}
// Ignore sub-tasks
if (event.issueLink.issueLinkType.name === 'jira_subtask_link') {
return;
}
if (matchingInwardIssue.issues.length && matchingOutwardIssue.issues.length) {
const storage = new RecordStorage();
// Check the issue link type is Epic Link
if (event.issueLink.issueLinkType?.name === 'Epic-Story Link' && sourceInstance === JiraOnPremise) {
//Check if existing epic link with outward issue sync key exists in Record Storage
const epicLink = await getEpicLink(storage, outwardIssueSyncIssueKey);
// If epic link doesn't exist in Record Storage, add parent to Jira Cloud matching issue
if (!epicLink) {
await JiraCloud.Issue.editIssue({
issueIdOrKey: matchingOutwardIssue.issues?.[0].key ?? '0',
body: {
fields: {
parent: {
key: matchingInwardIssue.issues?.[0].key
}
}
}
})
console.log(`Parent added for issue ${matchingOutwardIssue.issues?.[0].key}`)
}
const updatedEpicLink: EpicLink = epicLink
? { ...epicLink, id: (event.issueLink.id ?? 0).toString() }
: {
jiraCloudIssue: matchingOutwardIssue.issues?.[0].key ?? '0',
id: (event.issueLink.id ?? 0).toString()
};
// Add the necessary epic link information to Record Storage
await setEpicLink(storage, outwardIssueSyncIssueKey, updatedEpicLink)
return;
}
// Get issue links related to inward issue
const savedIssueLinks = await getIssueLinks(storage, inwardIssueSyncIssueKey);
const found = savedIssueLinks.some((issueLinks) => issueLinks[sourceProjectKey] === (event.issueLink.id ?? 0).toString());
if (found) {
console.log('Issue Link already exists')
} else {
// Create new issue link
await targetInstance.Issue.Link.createLink({
body: {
inwardIssue: {
id: matchingInwardIssue.issues?.[0].id
},
outwardIssue: {
id: matchingOutwardIssue.issues?.[0].id
},
type: {
name: event.issueLink.issueLinkType?.name
}
}
})
let createdIssueLinkId: string | undefined;
if (sourceInstance === JiraCloud) {
createdIssueLinkId = (await JiraOnPremise.Issue.getIssue({
issueIdOrKey: matchingInwardIssue.issues?.[0].id ?? '0'
})).fields?.issuelinks?.find(link => link.outwardIssue?.key === matchingOutwardIssue.issues?.[0].key && link.type?.name === event.issueLink.issueLinkType?.name)?.id
} else {
createdIssueLinkId = (await JiraCloud.Issue.getIssue({
issueIdOrKey: matchingInwardIssue.issues?.[0].id ?? '0'
})).fields?.issuelinks?.find(link => link.outwardIssue?.key === matchingOutwardIssue.issues?.[0].key && link.type?.name === event.issueLink.issueLinkType?.name)?.id
}
const issueLink = {
[sourceProjectKey]: (event.issueLink.id ?? 0).toString(),
[targetProjectKey]: createdIssueLinkId ?? '0'
}
// Add created issue link to Record Storage
const updatedIssueLinks = [...savedIssueLinks, issueLink];
await setIssueLinks(storage, inwardIssueSyncIssueKey, updatedIssueLinks);
console.log(`Issue link created bewteen ${matchingInwardIssue.issues?.[0].key} and ${matchingOutwardIssue.issues?.[0].key}`);
}
}
}
}
/**
* Function that handles deleted issue link
*/
export async function deleteIssueLink(context: Context, event: JiraCloudIssueLinkDeletedEvent | JiraOnPremIssueLinkDeletedEvent, sourceInstance: JiraCloudApi | JiraOnPremApi, targetInstance: JiraCloudApi | JiraOnPremApi, sourceProjectKey: string, targetProjectKey: string): Promise<void> {
const storage = new RecordStorage();
// Search for the issues that event was triggered from
const issueExists = await sourceInstance.Issue.getIssue<null>({
issueIdOrKey: (event.issueLink.sourceIssueId ?? 0).toString(),
errorStrategy: {
handleHttp404Error: () => null
}
})
const epicIssueExists = await sourceInstance.Issue.getIssue<null>({
issueIdOrKey: (event.issueLink.destinationIssueId ?? 0).toString(),
errorStrategy: {
handleHttp404Error: () => null
}
})
// Check if either the main issue or the associated epic issue exists
if (issueExists === null || epicIssueExists === null) {
return;
};
// Check if the issues belong to the correct project, ignore them if they don't
if (issueExists.fields?.project?.key !== sourceProjectKey || epicIssueExists.fields?.project?.key !== sourceProjectKey) {
return;
}
// Extract deleted issue link id
const eventIssueLinkId = (event.issueLink.id ?? 0).toString();
// Check if Epic Link got removed
if (event.issueLink.issueLinkType?.name === 'Epic-Story Link' && sourceInstance === JiraOnPremise) {
// Search for the issue Sync Key for the outward issue
const issueSyncKeyForOutwardIssue = await getScriptRunnerConnectSyncIssueKey(context, (event.issueLink.destinationIssueId ?? 0).toString(), JiraOnPremise) ?? '';
// Check if a Record Storage has epic value with that Sync Key
const epicLink = await getEpicLink(storage, issueSyncKeyForOutwardIssue);
// Check epic link id matches the one saved in Record Storage
if (epicLink && epicLink.jiraCloudIssue && epicLink.id === eventIssueLinkId) {
// Remove the matching epic from the issue in Jira Cloud
await JiraCloud.Issue.editIssue({
issueIdOrKey: epicLink.jiraCloudIssue,
body: {
fields: {
parent: null
}
}
})
// Delete the value from Record Storage
storage.deleteValue(`epic_${issueSyncKeyForOutwardIssue}`);
return;
}
}
// Search for the issue Sync Key from the inward issue
const issueSyncKeyForInwardIssue = await getScriptRunnerConnectSyncIssueKey(context, (event.issueLink.sourceIssueId ?? 0).toString(), sourceInstance) ?? '';
// Check if a Record Storage has issue link key value with that Sync Key
const valueExists = await storage.valueExists(`issue_link_${issueSyncKeyForInwardIssue}`);
if (valueExists) {
// Get all the saved issue links from Record Storage with that Sync Key
const savedIssueLinks = await getIssueLinks(storage, issueSyncKeyForInwardIssue);
const issueLinkId = savedIssueLinks.find(il => il[sourceProjectKey] === eventIssueLinkId)?.[targetProjectKey];
// Check if the matching Issue Link ID was found
if (!issueLinkId) {
// If not, throw an error
throw Error(`Couldn't find issue link ID from Record Storage: ${issueLinkId}. Issue Link might already be deleted`);
};
// Delete the link from target instance
await targetInstance.Issue.Link.deleteLink({
linkId: issueLinkId
})
// Check if there are any issue links left with that key
if (savedIssueLinks.length > 1) {
// If it has then remove the delete issue link from Record Storage
const updatedIssueLinks = savedIssueLinks.filter(il => il[sourceProjectKey] !== eventIssueLinkId);
await setIssueLinks(storage, issueSyncKeyForInwardIssue, updatedIssueLinks);
} else {
// If not, then delete the value from Record Storage since we don't need to keep it there anymore
await storage.deleteValue(`issue_link_${issueSyncKeyForInwardIssue}`);
}
console.log(`Issue link deleted`);
}
}
export type ValuesType = {
[key: string]: string;
};
interface UserField {
[key: string]: {
name?: string | null;
accountId?: string | null;
} | string | null;
};
export interface AttachmentBody {
fileName: string;
content: ArrayBuffer;
};
export interface MatchingValue {
field: string;
matchingValue: string;
}
export interface JiraCloudUserInfo {
emailAddress?: string;
displayName: string;
};
export interface EpicLink {
jiraCloudIssue: string,
id?: string,
}
export type UserFieldOptionType = 'ORIGINAL_USER' | 'REMAIN_UNASSIGNED' | 'INTEGRATION_USER' | 'PREDEFINED_USER' | 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD';
export type UserFieldFallbackOptionType = 'REMAIN_UNASSIGNED' | 'INTEGRATION_USER' | 'PREDEFINED_USER' | 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD' | 'HALT_SYNC';
type JiraCloudValue = string;
type JiraOnPremiseValue = string;
interface EnvVars {
JIRA_CLOUD_PROJECT_KEY: string;
JIRA_ON_PREM_PROJECT_KEY: string;
CUSTOM_FIELD_NAME: string;
RetryConfigurations: {
MAX_RETRY_ATTEMPTS: number;
RETRY_DELAY_SECONDS: number;
};
FIELDS: string[];
CUSTOM_FIELDS: string[];
UserFieldOptions: {
USER_FIELD_OPTION: UserFieldOptionType;
USER_FIELD_FALLBACK_OPTION: UserFieldFallbackOptionType;
};
PredefinedUser: {
JIRA_CLOUD_PREDEFINED_USER_EMAIL?: string;
JIRA_ON_PREM_PREDEFINED_USER_EMAIL?: string;
};
OriginalUserFields: {
CUSTOM_FIELD_FOR_ASSIGNEE?: string;
CUSTOM_FIELD_FOR_REPORTER?: string;
};
PRIORITY: Record<JiraCloudValue, JiraOnPremiseValue>;
STATUS: Record<JiraCloudValue, JiraOnPremiseValue>;
ISSUE_TYPES: Record<JiraCloudValue, JiraOnPremiseValue>;
IMPACT: Record<JiraCloudValue, JiraOnPremiseValue>;
CHANGE_REASON: Record<JiraCloudValue, JiraOnPremiseValue>;
CHANGE_TYPE: Record<JiraCloudValue, JiraOnPremiseValue>;
CHANGE_RISK: Record<JiraCloudValue, JiraOnPremiseValue>;
}
export function getEnvVars(context: Context) {
return context.environment.vars as EnvVars;
}