Sync Jira Cloud with Jira Data Center


Get Started

Not the integration you're looking for? Browse more.

About the integration


How does the integration logic work?

When an issue is created in Jira Cloud, a corresponding issue is created in Jira DC, and vice versa. Updates on both sides will be kept in sync as well. Get started to learn more.

Which fields are being synced?

  • Summary
  • Issue Type
  • Reporter
  • Assignee
  • Status
  • Priority
  • Comments
  • Description
  • Impact
  • Change Reason
  • Change Risk
  • Change Type
  • Epic Name
  • Due Date
  • Labels
  • Issue Links
  • Attachments
  • Custom Fields

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?

ScriptRunner Connect is an AI assisted code-first (JavaScript/TypeScript) integration platform (iPaaS) for building complex integrations and automations.

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


README

Scripts

TypeScriptJiraCloud/OnJiraCloudCommentCreated
Comment Created

README


📋 Overview

This template synchronizes Jira Data Center (or server) and Jira Cloud issues, ensuring the following data is kept in sync between the two:

  • Summary
  • Issue Type
  • Reporter
  • Assignee
  • Status
  • Priority
  • Comments
  • Attachments
  • Description
  • Impact
  • Change Reason
  • Change Risk
  • Change Type
  • Epic Name
  • Custom Fields
  • Due Date
  • Labels
  • Issue Links

🖊️ Setup

  1. Configure API connections and event listeners:

    • Create connectors for both Jira On-Premise and Jira Cloud instances, or use existing ones.
    • Configure Event Listeners only for the events you want to track.
  2. Add webhooks:

    • In Jira Cloud and Jira On-Premise, add webhooks for issue-related events.
    • Use the project key in the form of a JQL expression (e.g., project = TEST) to listen to specific projects.
    • Optional: Add additional filters to the webhooks (e.g., project = TEST AND labels = test AND issueType = Task).
  3. Create a custom field:

    • In both Jira Cloud and Jira On-Premise, create a new text custom field called ScriptRunner Connect Sync Issue Key to track the issue key on the other side.
    • Add this custom field to all issue types used by the projects.
  4. Configure parameters:

    • Go to Parameters and configure parameters according to your Jira Cloud and Jira On-Premise projects.
  5. Create additional fields (optional):

    • If you want to sync fields like Impact, Change Reason, Change Type, and Change Risk, create corresponding single-option fields in Jira On-Premise with matching values.
    • Add these fields to the appropriate issue types or uncheck them from the FIELDS section of Parameters if not needed.
  6. Customize synced fields:

    • Remove any fields you don't want to sync by unchecking them in the FIELDS section of Parameters.
  7. Add Epic Name field:

    • In Jira Cloud, add the Epic Name field to the Epic issue type.
    • If missing, create a new short text custom field with that name and make it required.

🚀 Using the template

  • When a new issue is created, it will be automatically created in the other Jira instance.
  • Updates to an issue will be reflected in the other instance.
  • Run 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.

❗️ Considerations

  • Adding attachments to comments is not supported.
  • Description fields may not be accurate if special formatting is used.
  • This template is a great starting point for syncing issues between Jira Cloud and Jira On-Prem. While it covers the basics, it might not sync every field or perform all necessary checks. Feel free to customize it to fit your specific requirements.
  • Built with Jira DC version 9.12; there may be differences in older or newer versions.

API Connections


TypeScriptJiraCloud/OnJiraCloudCommentCreated

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);
    }
}
TypeScriptJiraCloud/OnJiraCloudCommentDeleted

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);
}
TypeScriptJiraCloud/OnJiraCloudCommentUpdated

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);
    }
}
TypeScriptJiraCloud/OnJiraCloudIssueCreated

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}`);
    }
}
TypeScriptJiraCloud/OnJiraCloudIssueDeleted

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);
    }
}
TypeScriptJiraCloud/OnJiraCloudIssueLinkCreated

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);
} 
TypeScriptJiraCloud/OnJiraCloudIssueLinkDeleted

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);
}
TypeScriptJiraCloud/OnJiraCloudIssueUpdated

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}`);
        }
    }
}
TypeScriptJiraOnPremise/OnJiraOnPremiseCommentCreated

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);
    }
}
TypeScriptJiraOnPremise/OnJiraOnPremiseCommentDeleted

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);
}
TypeScriptJiraOnPremise/OnJiraOnPremiseCommentUpdated

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);
    }
}
TypeScriptJiraOnPremise/OnJiraOnPremiseIssueCreated

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}`);
    }
}
TypeScriptJiraOnPremise/OnJiraOnPremiseIssueDeleted

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);
    }
}
TypeScriptJiraOnPremise/OnJiraOnPremiseIssueLinkCreated

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);
}
TypeScriptJiraOnPremise/OnJiraOnPremiseIssueLinkDeleted

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);
}
TypeScriptJiraOnPremise/OnJiraOnPremiseIssueUpdated

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}`);
        }
    }
}
TypeScriptPurgeCache

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);
}
TypeScriptUtils

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;
}
Documentation · Support · Suggestions & feature requests