Template Content
Scripts
About the integration
How does the integration logic work?
Which fields are being synced?
Does it work with older Jira Server instances too?
Yes. Functionally Jira Data Center and older server instances are the same.
Are sub-tasks synced too?
Yes.
Are the issue move action changes synced as well?
Yes.
Can I configure which fields are being synced?
Yes. You can change which fields are being synced and control quite many other things via the configuration.
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
Template Content
Scripts
This template keeps issues in sync between 2 Jira Data Center (or server) instances by exchanging the following data:
Configure API connections and event listeners:
Add webhooks in Jira DC:
project = TEST
, which will only listen to the "TEST" project.project in (TEST, SP)
, which listens to both TEST and
SP projects. You can also add filters like labels, issueType, status, priority
, and more. For example, project = TEST AND labels = test AND issueType = Task
filters issues with a test
label and Task
type in the TEST
project.Create custom text fields for syncing:
ScriptRunner Connect Sync Issue Key
. This will track the synced issue key from the other project.Set environment variables:
Configure the environment variables based on your project's needs.
FIELDS
environment variable, uncheck any fields you don't want to sync.MOVE_ISSUES_BETWEEN_PROJECTS
to false if you don't want issues to move between projects. 💡 Tip: You can run the PurgeCache
script to clear cached data (after testing, etc.).
ℹ️ Note: Updates made by the user who set up the integration will be ignored to prevent an endless update loop.
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueCommentCreatedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import {
extractIssueId,
getComments,
getEnvVars,
getMatchingValue,
getProjectIdentifier,
getScriptRunnerConnectSyncIssueKey,
searchIssue
} from './Utils';
/**
* This function creates a new comment in matching issue when a comment is created.
*/
export default async function createComment(context: Context, event: IssueCommentCreatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
console.log('Comment Created event:', event);
// Get the current user
const myself = await sourceInstance.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_PROJECTS, CUSTOM_FIELD_NAME } = getEnvVars(context);
const userDisplayName = event.comment.updateAuthor?.displayName;
const email = event.comment.updateAuthor?.emailAddress;
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
user: `${userDisplayName} (${email})`,
commentId: event.comment.id
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
// Get the issue ID for the created comment
const createdCommentIssueId = extractIssueId(event.comment.self ?? '0');
// Get the issue
const issue = await sourceInstance.Issue.getIssue({
issueIdOrKey: createdCommentIssueId ?? '0',
fields: 'issuetype, project'
})
// Extract the source project key
const sourceProjectKey = issue.fields?.project?.key ?? '0';
// Get target issue project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(issue.key ?? '0', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, issue.fields?.issuetype?.id ?? '0');
// Check if ScriptRunner Connect Sync Issue Key exists on the issue
if (scriptRunnerConnectSyncIssueKey === null) {
throw new Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from the other project
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, false)
// Check if issue with a matching ScriptRunner Connect Sync Issue Key exists
if (issues.total === 0) {
// If not, throw an error
throw new Error(`Issue with the matching ScriptRunner Connect Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Extract the matching issue issue key
const issueKey = issues.issues?.[0].key ?? '0';
// Construct the original user
const originalUser = `Original comment by: ${event.comment.author?.displayName}, (${event.comment.author?.emailAddress})`
// Clone the existing comment body and add original user
const updatedCommentBody = `${event.comment.body}\n\n${originalUser}`
// Create a new comment in target instance
const createdComment = await targetInstance.Issue.Comment.addComment({
issueIdOrKey: issueKey,
body: {
body: updatedCommentBody
}
})
const storage = new RecordStorage();
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Get existing comments from Record Storage with the same ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, scriptRunnerConnectSyncIssueKey);
// Save the comment ID's
const commentIds = {
[sourceProjectNumberWithKey]: event.comment.id,
[targetProjectNumberWithKey]: 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}`)
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from '@managed-api/jira-on-prem-v8-sr-connect';
import { IssueCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { FieldsCreateIssue } from '@managed-api/jira-on-prem-v8-core/definitions/issue';
import {
checkUserFieldOption,
getCustomField,
getEnvVars,
getJiraIssueType,
getJiraPriority,
getMatchingValue,
getScriptRunnerConnectSyncIssueKey,
handleCustomFieldsForCreatingIssue,
searchIssue,
uploadAttachment,
} from './Utils';
/**
* This function creates a new Jira DC issue when an issue is created.
*/
export default async function createIssue(context: Context, event: IssueCreatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi, targetProjectPredifinedUserEmail?: string): Promise<void> {
console.log('Issue Created event:', event);
// Get the current user
let myself = await sourceInstance.Myself.getCurrentUser();
// Check if the current user does not match the person who committed the update
if (myself.emailAddress !== event.user.emailAddress) {
const sourceProjectKey = event.issue.fields?.project?.key ?? '';
const userDisplayName = event.user.displayName;
const accountEmail = event.user.emailAddress;
// Extract the created issue issue key and issue type
const eventIssueKey = event.issue.key ?? '';
const eventIssueType = event.issue.fields?.issuetype;
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
project: sourceProjectKey,
issueKey: eventIssueKey,
issueType: eventIssueType?.name,
user: `${userDisplayName} (${accountEmail})`,
issueId: event.issue.id
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
// Get the environment variables
const { JIRA_PROJECTS, STATUS, PRIORITY, ISSUE_TYPES, FIELDS, CUSTOM_FIELDS, CUSTOM_FIELD_NAME } = getEnvVars(context);
// Find matching target project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key custom field
const sourceCustomField = await getCustomField(sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, eventIssueType?.id ?? '0');
// Check if ScriptRunner Connect Sync Issue Key custom field was found
if (!sourceCustomField) {
// If not, then throw an error
throw Error('ScriptRunner Connect Sync Issue Key custom field not found')
}
// Add issue key to the ScriptRunner Connect Sync Issue Key custom field
await sourceInstance.Issue.editIssue({
issueIdOrKey: eventIssueKey,
body: {
fields: {
[sourceCustomField]: eventIssueKey
}
}
})
// Find the project from target instance based on the matching project key
const project = await targetInstance.Project.getProject({
projectIdOrKey: targetProjectKey
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${targetProjectKey}`);
}
// Find the matching issue type name for the target project
const issueTypeName = getMatchingValue(sourceInstance, eventIssueType?.name ?? '', ISSUE_TYPES);
// Get the issue type from target instance
const issueType = await getJiraIssueType(issueTypeName, targetInstance);
// Get the ScriptRunner Connect Sync Issue Key custom field from target instance
const targetCustomField = (await getCustomField(targetInstance, CUSTOM_FIELD_NAME, targetProjectKey, issueType.id ?? '0')) ?? '';
// 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') {
// Get the Epic Name custom field ID from source Instance
const sourceInstanceEpicNameCustomField = await getCustomField(sourceInstance, 'Epic Name', sourceProjectKey, eventIssueType?.id ?? '0');
// Check if Epic Name custom field ID from source Instance was found
if (!sourceInstanceEpicNameCustomField) {
// If not, then throw an error
throw Error('Epic Name custom field ID from source Instance was not found');
}
// Get the Epic Name custom field ID from target Instance
const epicNameCustomField = await getCustomField(sourceInstance, 'Epic Name', sourceProjectKey, issueType.id ?? '0');
// Check if Epic Name custom field ID from target Instance was found
if (!epicNameCustomField) {
// If not, then throw an error
throw Error('Epic Name custom field ID from target Instance was not found');
}
// Add the Epic Name value to request body
requestBody[epicNameCustomField] = event.issue.fields?.[sourceInstanceEpicNameCustomField];
};
// Get the current user for target Instance
const targetInstanceIntegrationUser = await targetInstance.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 environment variables and handle the field appropriately
const reporterUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
reporter?.emailAddress ?? '',
targetInstanceIntegrationUser.name ?? '',
'reporter',
reporter?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.id ?? '0'
);
// 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 environment variables and handle the field appropriately
const assigneeUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
assignee?.emailAddress ?? '',
targetInstanceIntegrationUser.name ?? '',
'assignee', assignee?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.id ?? ''
);
// 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')) {
// Get the matching priority name
const priorityName = getMatchingValue(sourceInstance, event.issue.fields?.priority?.name ?? '', PRIORITY);
const priority = await getJiraPriority(priorityName, targetInstance);
// Add priority ID to issue fields
requestBody.priority = { id: priority.id ?? '' }
}
// Check if description field exists in FIELDS array and description has been added
if (FIELDS.includes('description') && event.issue.fields?.description !== null) {
// If it does, add description to issue fields
requestBody.description = event.issue.fields?.description
}
// 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 due date to issue fields
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 issue fields
requestBody.labels = event.issue.fields?.labels;
}
// Check if Sub-task was created
if (FIELDS.includes('issuetype') && issueType.name === 'Sub-task') {
// Extract parent issue key
const parentIssueKey = event.issue.fields?.parent?.key
// Get the Sync Key for parent issue
const parentSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(parentIssueKey ?? '', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, event.issue.fields?.parent?.fields?.issuetype?.id ?? '0');
// Check if Sync Key for parent issue was found
if (!parentSyncIssueKey) {
throw Error('ScriptRunner Connect Sync Issue Key is missing on parent issue');
}
// Search matching parent issue from target Instance
const matchingIssue = await searchIssue(context, parentSyncIssueKey ?? '', targetInstance, false);
// Check if a matching parent issue was found
if (matchingIssue.total === 0) {
// If not, throw an error
throw Error(`Matching parent issue with sync key ${parentSyncIssueKey} missing`);
}
// Add parent issue key to issue fields
requestBody.parent = {
key: matchingIssue?.issues?.[0].key
}
}
// Check if Epic Links is checked in FIELDS array
if (FIELDS.includes('Epic Link')) {
// Get the custom field for Epic Link in source instance
const customField = await getCustomField(sourceInstance, 'Epic Link', sourceProjectKey, eventIssueType?.id ?? '')
// Extract the Epic key
const fieldValue = event.issue.fields?.[customField ?? ''];
// If there is a value, find matching epic from target instance
if (fieldValue) {
// Get the Sync Key for Epic issue
const epicSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(fieldValue ?? '', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, event.issue.fields?.parent?.fields?.issuetype?.id ?? '0');
// Check if Sync Key for Epic issue was found
if (!epicSyncIssueKey) {
throw Error('ScriptRunner Connect Sync Issue Key is missing on Epic issue');
}
// Search matching Epic issue from target Instance
const matchingIssue = await searchIssue(context, epicSyncIssueKey ?? '', targetInstance, false);
// Check if a matching Epic issue was found
if (matchingIssue.total === 0) {
// If not, throw an error
throw Error(`Matching Epic issue with sync key ${epicSyncIssueKey} missing`);
}
// Get the Epic Link custom field in target instance
const epicLinkCustomField = await getCustomField(targetInstance, 'Epic Link', targetProjectKey, eventIssueType?.id ?? '')
// Add Epic issue key to issue fields
requestBody[epicLinkCustomField ?? ''] = matchingIssue.issues?.[0].key;
}
}
// Check if custom fields have been added to CUSTOM_FIELDS in environment variables
if (CUSTOM_FIELDS.length) {
// Check for values and add the custom fields to issue fields
const customFieldsToBeAdded = await handleCustomFieldsForCreatingIssue(sourceInstance, targetInstance, targetProjectKey, CUSTOM_FIELDS, event.issue, issueType.id ?? '');
// Add custom fields
requestBody = { ...requestBody, ...customFieldsToBeAdded }
}
console.log(`Issue fields`, {
requestBody
})
// Create a new issue in target instance
const issue = await targetInstance.Issue.createIssue({
body: {
fields: requestBody,
}
});
// Extract the newly created Issue key
const issueKey = issue.key ?? '';
console.log(`Issue created: ${issueKey}`);
// Get the newly created issue
const createdTargetIssue = await targetInstance.Issue.getIssue({
issueIdOrKey: issueKey
})
// Extract attachments from the issue that triggered the event
const attachments = event.issue.fields?.attachment ?? [];
// Check if attachments were added to the issue
if (FIELDS.includes('Attachment') && attachments.length > 0) {
// Loop through attachments and add them to the array
for (const attachment of attachments) {
// Upload attachment to target instance
await uploadAttachment(sourceInstance, targetInstance, attachment, issueKey);
}
}
// Check if newly created Issue has the correct status and find the matching status
const status = getMatchingValue(sourceInstance, event.issue.fields?.status?.name ?? '', STATUS);
// Check if the status is incorrect, then update it
if (createdTargetIssue.fields?.status?.name !== status) {
const transitions = (await targetInstance.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
// Find the correct transition ID
const transitionId = transitions.find(t => t.to?.name === status || t.name === status)?.id ?? ''
// Check if transition ID was found
if (!transitionId) {
// If not, throw an Error
throw Error(`Transition for status not found in target instance: ${status}`);
}
// Change the status of the issue (workflow transition)
await targetInstance.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
}
}
});
};
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueLinkCreatedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import {
getEnvVars,
getIssueLinks,
getMatchingValue,
getProjectIdentifier,
retrySearchIssueInTargetInstance,
retrySyncIssueKeyForIssueLinks,
setIssueLinks
} from "./Utils";
/**
* This function adds an issue link to the corresponding issue when an issue link is created
*/
export default async function createIssueLink(context: Context, event: IssueLinkCreatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
console.log('Issue Link Created:', event);
const { JIRA_PROJECTS, CUSTOM_FIELD_NAME, RetryConfigurations, FIELDS } = getEnvVars(context);
const storage = new RecordStorage();
// Check if FIELDS array includes issue links
if (FIELDS.includes('issue links')) {
// Skip sub-tasks and Epic-Story links since they are managed by other scripts
if (event.issueLink.issueLinkType?.name === 'jira_subtask_link' || event.issueLink.issueLinkType?.name === 'Epic-Story Link') {
return;
}
// Extract inward and destination issue ID's
const inwardIssueId = event.issueLink.sourceIssueId ?? 0;
const destinationIssueId = event.issueLink.destinationIssueId ?? 0;
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
inwardIssueId: inwardIssueId,
destinationIssueId: destinationIssueId,
issueLinkType: event.issueLink.issueLinkType?.name,
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
// Get the Issue Sync Issue Keys for both inward and outward linked issue
let [inwardIssueSyncIssueKey, outwardIssueSyncIssueKey] = await Promise.all([
retrySyncIssueKeyForIssueLinks(context, inwardIssueId, sourceInstance, CUSTOM_FIELD_NAME),
retrySyncIssueKeyForIssueLinks(context, destinationIssueId, sourceInstance, CUSTOM_FIELD_NAME)
]);
// Check if inward and outward Issue Sync Issue Keys are the same
if (inwardIssueSyncIssueKey && outwardIssueSyncIssueKey && inwardIssueSyncIssueKey === outwardIssueSyncIssueKey) {
// Retry getting sync issue keys if they are the same, until they are different or max retries reached
for (let i = 0; i < RetryConfigurations.MAX_RETRY_ATTEMPTS; i++) {
console.log(`Sync issue keys are the same (Attempt ${i + 1}/${RetryConfigurations.MAX_RETRY_ATTEMPTS}). Retrying...`);
// Retry fetching both sync issue keys
[inwardIssueSyncIssueKey, outwardIssueSyncIssueKey] = await Promise.all([
retrySyncIssueKeyForIssueLinks(context, inwardIssueId, sourceInstance, CUSTOM_FIELD_NAME),
retrySyncIssueKeyForIssueLinks(context, destinationIssueId, sourceInstance, CUSTOM_FIELD_NAME)
]);
// If the keys are now different, exit the loop
if (inwardIssueSyncIssueKey !== outwardIssueSyncIssueKey) {
console.log('Sync issue keys are now different. Exiting retry loop.');
break;
}
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
}
// If the keys are still the same after max retries, throw an error
if (inwardIssueSyncIssueKey === outwardIssueSyncIssueKey) {
throw Error('Failed to retrieve different sync issue keys after max retries.');
}
}
// Check if sync issue keys for both issues exist
if (inwardIssueSyncIssueKey && outwardIssueSyncIssueKey) {
// Get source project key and find matching target project key
const sourceProjectKey = (await sourceInstance.Issue.getIssue({
issueIdOrKey: inwardIssueId
})).fields?.project?.key ?? '';
// Target project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Search for matching inward and outward issues
const [matchingInwardIssue, matchingOutwardIssue] = await Promise.all([
retrySearchIssueInTargetInstance(context, inwardIssueSyncIssueKey, targetInstance, false),
retrySearchIssueInTargetInstance(context, outwardIssueSyncIssueKey, targetInstance, false)
])
// Get issue links related to inward issue
const savedIssueLinks = await getIssueLinks(storage, inwardIssueSyncIssueKey);
// Add project identifiers for the record storage
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Check if Issue Link has already been added
const found = savedIssueLinks.some((issueLinks) => issueLinks[sourceProjectNumberWithKey] === event.issueLink.id?.toString());
// Check if this issue link is already present in target instance
if (found) {
return console.log('Issue Link already exists')
}
// Create a new issue link in the target instance
await targetInstance.Issue.Link.createLink({
body: {
inwardIssue: {
id: matchingInwardIssue.issues?.[0].id
},
outwardIssue: {
id: matchingOutwardIssue.issues?.[0].id
},
type: {
name: event.issueLink.issueLinkType?.name
}
}
})
// Get the updated matching inward issue
const targetInwardIssue = await targetInstance.Issue.getIssue({
issueIdOrKey: matchingInwardIssue.issues?.[0].id ?? '0'
})
// Get the newly create issue link ID
const createdIssueLinkId = targetInwardIssue?.fields?.issuelinks?.find(link => link.outwardIssue?.key === matchingOutwardIssue?.issues?.[0].key && link.type?.name === event.issueLink.issueLinkType?.name)?.id
// Save the issue link ID's
const issueLink = {
[sourceProjectNumberWithKey]: event.issueLink.id?.toString() ?? '0',
[targetProjectNumberWithKey]: createdIssueLinkId ?? '0'
}
// Add created issue link to Record Storage
const updatedIssueLinks = [...savedIssueLinks, issueLink];
await setIssueLinks(storage, inwardIssueSyncIssueKey, updatedIssueLinks);
console.log(`Issue link created between ${matchingInwardIssue.issues?.[0].key} and ${matchingOutwardIssue.issues?.[0].key}`);
}
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueCommentDeletedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import {
extractIssueId,
getComments,
getEnvVars,
getMatchingValue,
getProjectIdentifier,
getScriptRunnerConnectSyncIssueKey,
searchIssue
} from './Utils';
/**
* This function deletes a corresponding comment when a comment has been deleted.
*/
export default async function deleteComment(context: Context, event: IssueCommentDeletedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
console.log('Comment Deleted event: ', event);
// Extract the comments author
const userDisplayName = event.comment.author?.displayName;
const userEmail = event.comment.author?.emailAddress;
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
commentCreatedBy: `${userDisplayName} (${userEmail})`,
commentId: event.comment.id
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
// Extract the deleted comment issue ID
const deletedCommentIssueId = extractIssueId(event.comment.self ?? '0');
// Check if Issue ID extraction was successful
if (!deletedCommentIssueId) {
// If not, throw an error
throw Error("Issue Id extraction failed");
}
// Search for the issue that event was triggered from
const issue = await sourceInstance.Issue.getIssue<null>({
issueIdOrKey: deletedCommentIssueId,
errorStrategy: {
handleHttp404Error: () => null
}
})
// Check if the issue still exists
if (issue) {
const storage = new RecordStorage();
const { JIRA_PROJECTS, CUSTOM_FIELD_NAME } = getEnvVars(context);
// Extract the source issue project key
const sourceProjectKey = issue.fields?.project?.key ?? '0';
// Find matching target project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(issue.key ?? '0', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, issue.fields?.issuetype?.id ?? '0');
// Check is Sync Issue Key is present
if (scriptRunnerConnectSyncIssueKey === null) {
// If not, throw an error
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Get comments from Record Storage with the ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, scriptRunnerConnectSyncIssueKey);
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Find the comment ID for the target instance that matches the comment that got deleted
const commentId = comments.find(c => c[sourceProjectNumberWithKey] === event.comment.id)?.[targetProjectNumberWithKey];
// Check if the matching comment ID was found
if (!commentId) {
// If not, throw an error
throw Error(`Couldn't find matching comment ID from Record Storage: ${event.comment.id}. Comment might already be deleted`);
};
// Find the matching issue from the target instance
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, false);
// Check if issue with a matching ScriptRunner Connect Issue Key exists
if (issues.total === 0) {
// 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 ?? '';
// Delete the comment
await targetInstance.Issue.Comment.deleteComment({
issueIdOrKey: issueKey,
id: commentId ?? '0'
})
// Check if issue has some comments left
if (comments.length > 1) {
// If it has then remove the deleted comment from Record Storage
const updatedComments = comments.filter(c => c[sourceProjectNumberWithKey] !== 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}`);
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueDeletedEvent } from "@sr-connect/jira-on-premise/events";
import { getCustomField, getEnvVars, searchIssue } from './Utils';
import { RecordStorage } from '@sr-connect/record-storage';
/**
* This function deletes a corresponding issue when an issue in Jira DC is deleted.
*/
export default async function deleteIssue(context: Context, event: IssueDeletedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
console.log('Issue Deleted event: ', event);
// Get the current user
let myself = await sourceInstance.Myself.getCurrentUser();
// Check if the current user does not match the person who committed the update
if (myself.emailAddress !== event.user.emailAddress) {
const userDisplayName = event.user.displayName;
const email = event.user.emailAddress;
const sourceProjectKey = event.issue.fields?.project?.key ?? '';
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
project: sourceProjectKey,
issueKey: event.issue.key,
issueType: event.issue.fields?.issuetype?.name,
user: `${userDisplayName} (${email})`,
issueId: event.issue.id
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
const { FIELDS, CUSTOM_FIELD_NAME } = getEnvVars(context);
// Get the ScriptRunner Connect Sync Issue Key custom field in source instance
const customField = await getCustomField(sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, event.issue.fields?.issuetype?.id ?? '0');
// Get the ScriptRunner Connect Sync Issue Key from deleted issue
const scriptRunnerConnectSyncIssueKey = event.issue.fields?.[customField ?? ''];
// Check if the deleted issue had ScriptRunner Connect Sync Issue Key
if (!scriptRunnerConnectSyncIssueKey) {
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Find the matching issue from the target project
const issues = await searchIssue(context, scriptRunnerConnectSyncIssueKey, targetInstance, false);
// Check if any matching issues were found
if (issues.total === 0) {
// 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 matching issue key
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();
// Check FIELDS array includes comments
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)
}
}
// Check FIELDS array includes issue links
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}`)
}
}
console.log(`Deleted Issue: ${issueKey}`)
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueLinkDeletedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import {
getEnvVars,
getIssueLinks,
getMatchingValue,
getProjectIdentifier,
getScriptRunnerConnectSyncIssueKeyForIssueLink,
setIssueLinks
} from './Utils';
/**
* This function deletes a corresponding issue link when an issue link in Jira DC is deleted.
*/
export default async function deleteIssueLink(context: Context, event: IssueLinkDeletedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
console.log('Issue Link Deleted event: ', event);
// Skip sub-tasks and Epic-Story links since they are managed by other scripts
if (event.issueLink.issueLinkType?.name === 'jira_subtask_link' || event.issueLink.issueLinkType?.name === 'Epic-Story Link') {
return;
}
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
inwardIssueId: event.issueLink.sourceIssueId,
destinationIssueId: event.issueLink.destinationIssueId,
issueLinkType: event.issueLink.issueLinkType?.name,
linkId: event.issueLink.id,
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
const storage = new RecordStorage();
const { JIRA_PROJECTS, CUSTOM_FIELD_NAME } = getEnvVars(context);
// Retrieve the list of issue link IDs to ignore during deletion.
// This helps prevent accidental deletion of issue links that are currently being processed.
// This is useful in scenarios where moving issues results in multiple issue links being processed simultaneously.
const ignore = await storage.getValue('ISSUE_MOVED') as string[] | undefined;
// If the `ignore` array includes the current event's issue link ID, stop the script execution.
// This ensures that any issue link that still exists aren't deleted.
if (ignore && ignore.includes((event.issueLink?.id ?? 0).toString())) {
return;
}
// Extract deleted issue link ID
const eventIssueLinkId = event.issueLink.id?.toString();
// Extract the source Issue ID
const sourceIssueId = event.issueLink.sourceIssueId?.toString() ?? '0';
// Search for the issue Sync Key from the inward issue
const issueSyncKeyForInwardIssue = await getScriptRunnerConnectSyncIssueKeyForIssueLink(context, sourceIssueId, sourceInstance, CUSTOM_FIELD_NAME);
if (!issueSyncKeyForInwardIssue) {
return;
}
// Check if Record Storage contains an issue link entry associated with the specified ScriptRunner Connect Sync Issue Key
const valueExists = await storage.valueExists(`issue_link_${issueSyncKeyForInwardIssue}`);
// Procceed if value exists
if (valueExists) {
// Get source project key and find matching target project key
const sourceProjectKey = (await sourceInstance.Issue.getIssue({
issueIdOrKey: sourceIssueId
})).fields?.project?.key ?? '';
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Get all the saved issue links from Record Storage with that Sync Key
const savedIssueLinks = await getIssueLinks(storage, issueSyncKeyForInwardIssue);
// Find the matching issue link ID
const issueLinkId = savedIssueLinks.find(il => il[sourceProjectNumberWithKey] === eventIssueLinkId)?.[targetProjectNumberWithKey];
// 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. Issue Link might 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 deleted issue link from Record Storage
const updatedIssueLinks = savedIssueLinks.filter(il => il[sourceProjectNumberWithKey] !== 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`);
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { GetCurrentUserResponseOK } from "@managed-api/jira-on-prem-v8-core/types/myself";
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueUpdatedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import { FieldsCreateIssue, FieldsEditIssue } from '@managed-api/jira-on-prem-v8-core/definitions/issue';
import { SearchIssuesByJqlResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue/search';
import {
checkUserFieldOption,
getCreateMetaData,
getCustomField,
getEnvVars,
getIssueLinks,
getJiraIssueType,
getJiraPriority,
getMatchingValue,
getProjectIdentifier,
getScriptRunnerConnectSyncIssueKey,
getScriptRunnerConnectSyncIssueKeyForIssueLink,
handleCustomFieldsForCreatingIssue,
retrySearchIssueInTargetInstance,
retrySyncIssueKeyForIssueLinks,
searchIssue,
setIssueLinks,
uploadAttachment,
} from './Utils';
/**
* This function creates a new matching issue and deletes the old one when issue gets moved.
*/
export default async function moveIssue(context: Context, event: IssueUpdatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi, myself: GetCurrentUserResponseOK, targetProjectPredifinedUserEmail?: string): Promise<void> {
console.log('Issue Moved event: ', event);
const { JIRA_PROJECTS, STATUS, PRIORITY, ISSUE_TYPES, FIELDS, CUSTOM_FIELDS, CUSTOM_FIELD_NAME, RetryConfigurations } = getEnvVars(context);
const eventIssueType = event.issue.fields?.issuetype;
const changeLogItems = event.changelog?.items ?? [];
const sourceProjectKey = event.issue.fields?.project?.key ?? '';
const keyChange = changeLogItems.find(item => item.field === 'Key');
const projectChange = changeLogItems.find(item => item.field === 'project');
const issueTypeChange = changeLogItems.find(item => item.field === 'issuetype');
const oldIssueKey = keyChange?.fromString ?? '';
const newIssueKey = keyChange?.toString ?? '';
const oldProjectKey = projectChange?.fromString ?? '';
const newProjectKey = projectChange?.toString ?? '';
const oldIssueType = issueTypeChange ? issueTypeChange.fromString ?? '' : eventIssueType?.name;
const newIssueType = issueTypeChange ? issueTypeChange?.toString ?? '' : eventIssueType?.name;
const userDisplayName = event.user.displayName;
const userEmail = event.user.emailAddress;
const storage = new RecordStorage();
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
project: sourceProjectKey,
summary: event.issue.fields?.summary,
oldIssueKey: oldIssueKey,
newIssueKey: newIssueKey,
oldProject: oldProjectKey,
newProject: newProjectKey,
oldIssueType: oldIssueType,
newIssueType: newIssueType,
user: `${userDisplayName} (${userEmail})`,
};
console.log('Going to perform', event.webhookEvent, event.issue_event_type_name, 'event:', metaData);
// Find target Project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key custom field
const sourceCustomField = (await getCustomField(sourceInstance, CUSTOM_FIELD_NAME, newProjectKey, eventIssueType?.id ?? '0')) ?? '';
// Extract the previous Script Runner Connect Sync Issue Key
const previousScriptRunnerConnectSyncIssueKey = event.issue?.fields?.[sourceCustomField];
if (!previousScriptRunnerConnectSyncIssueKey) {
throw Error('Missing ScriptRunner Connect Sync Issue Key');
}
// Update the ScriptRunner Connect Sync Issue Key custom field with the new issue key
await sourceInstance.Issue.editIssue({
issueIdOrKey: newIssueKey,
body: {
fields: {
[sourceCustomField]: newIssueKey,
}
}
})
// Find the project from target instance
const project = await targetInstance.Project.getProject({
projectIdOrKey: targetProjectKey
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${targetProjectKey}`);
}
// Find the matching issue type for the target project
const issueTypeName = getMatchingValue(sourceInstance, eventIssueType?.name ?? '', ISSUE_TYPES);
// Get the issue type from target instance
const issueType = await getJiraIssueType(issueTypeName, targetInstance);
// Get the ScriptRunner Connect Sync Issue Key custom field from target instance
const targetCustomField = (await getCustomField(targetInstance, CUSTOM_FIELD_NAME, targetProjectKey, eventIssueType?.id ?? '')) ?? '';
// Fields to be updated in target instance
let requestBody: FieldsCreateIssue = {
summary: event.issue.fields?.summary ?? '',
project: {
id: project.id,
},
issuetype: {
id: issueType.id
},
[targetCustomField]: newIssueKey,
};
// Issue fields we can't add when creating the issue
let requestBodyForTransition: FieldsEditIssue = {};
// Get editMetadata for the created Issue
const editMetadata = await sourceInstance.Issue.Metadata.getEditMetadata({
issueIdOrKey: newIssueKey,
})
// Map through fields the moved issue supports
const fieldsThatIssueSupports = Object.entries(editMetadata.fields ?? {})
.map(([key, field]) => key.startsWith('customfield_') ? field.name : key);
// Get fields that issue creations supports in target project
const createIssueMetadata = await getCreateMetaData(targetInstance, targetProjectKey, issueType.id ?? '0')
// Add fields that issue creation supports into an array
const createIssueFields = (createIssueMetadata?.values ?? []).map(fieldMetadata =>
fieldMetadata.fieldId.startsWith('customfield_') ? fieldMetadata.name : fieldMetadata.fieldId
);
// Get target instance integration user
const targetInstanceIntegrationUser = await targetInstance.Myself.getCurrentUser();
// Epic Issue Type, this will be used
const epicIssueType = await getJiraIssueType('Epic', targetInstance);
// 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 environment variables and handle the field appropriately
const reporterUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
reporter?.emailAddress ?? '',
targetInstanceIntegrationUser.name ?? '',
'reporter',
reporter?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.name ?? ''
);
// If a value is returned, add it to the issue fields
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 environtment variables and handle the field appropriately
const assigneeUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
assignee?.emailAddress ?? '',
targetInstanceIntegrationUser.name ?? '',
'assignee',
assignee?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.name ?? ''
);
// If a value is returned, add it to the issue fields
if (assigneeUserFieldOption) {
if (createIssueFields.includes('assignee')) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
} else if (fieldsThatIssueSupports.includes('assignee')) {
requestBodyForTransition = { ...requestBodyForTransition, ...assigneeUserFieldOption }
}
}
}
// Check if field exists in FIELDS array
if (FIELDS.includes('priority')) {
// Find the matching priority name
const priorityName = getMatchingValue(sourceInstance, event.issue.fields?.priority?.name ?? '', PRIORITY);
// Get the priority
const priority = await getJiraPriority(priorityName, targetInstance);
if (createIssueFields.includes('priority')) {
// Add priority name to issue fields
requestBody.priority = { id: priority.id }
} else if (fieldsThatIssueSupports.includes('priority')) {
requestBodyForTransition.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 issue fields
if (createIssueFields.includes('description')) {
requestBody.description = event.issue.fields?.description
} else if (fieldsThatIssueSupports.includes('description')) {
requestBodyForTransition.description = event.issue.fields?.description
}
}
// 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 (createIssueFields.includes('duedate')) {
// Add the duedate to issue fields
requestBody.duedate = event.issue.fields?.duedate;
} else if (fieldsThatIssueSupports.includes('duedate')) {
requestBodyForTransition.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 issue fields
if (createIssueFields.includes('labels')) {
requestBody.labels = event.issue.fields?.labels;
} else if (fieldsThatIssueSupports.includes('labels')) {
requestBodyForTransition.labels = event.issue.fields?.labels;
}
}
// Check if issue is a sub-task
if (FIELDS.includes('issuetype') && issueType.name === 'Sub-task') {
// Extract parent issue key
const parentIssueKey = event.issue.fields?.parent?.key;
let parentSyncIssueKey: string | null = null;
let matchingIssue: SearchIssuesByJqlResponseOK | undefined;
let attempts = 0;
do {
// Get ScriptRunner Connect Sync Issue Key for parent
parentSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(
parentIssueKey ?? '',
sourceInstance,
CUSTOM_FIELD_NAME,
sourceProjectKey,
event.issue.fields?.parent?.fields?.issuetype?.id ?? '0'
);
// Search for parent issue in target instance
matchingIssue = await searchIssue(context, parentSyncIssueKey ?? '0', targetInstance, false);
// Check if we found a matching issue and it's in the correct project
if (matchingIssue.total && matchingIssue.total > 0 && parentSyncIssueKey && matchingIssue.issues?.[0].fields?.project?.key === targetProjectKey) {
break; // Exit loop if matching issue is found
}
console.log('No matching issue found. Retrying...');
attempts++;
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
} while (attempts < RetryConfigurations.MAX_RETRY_ATTEMPTS); // Continue until max attempts reached
// Check if a matching issue was found
if (matchingIssue?.total === 0) {
// If not, throw an error
throw Error(`Issue with the matching Issue Parent ScriptRunner Connect Sync Issue Key does not exist in target instance: ${parentSyncIssueKey}`);
};
// Add matching parent issue key to issue fields
requestBody.parent = {
key: matchingIssue?.issues?.[0].key
}
// Check if the matching issue has any inward issues linked
const linkedInwardIssues = matchingIssue.issues?.[0].fields?.issuelinks?.filter(link => link.inwardIssue) ?? [];
// Iterate through each inward issue link found
for (const issueLink of linkedInwardIssues) {
// Search for the issue Sync Key from the inward issue
const issueSyncKeyForInwardIssue = await getScriptRunnerConnectSyncIssueKeyForIssueLink(context, issueLink?.inwardIssue?.id ?? '0', targetInstance, CUSTOM_FIELD_NAME);
// If the sync key is missing, skip to the next inward issue link
if (!issueSyncKeyForInwardIssue) {
continue;
}
// 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 source project key
const targetIssueProjectKey = (await targetInstance.Issue.getIssue({
issueIdOrKey: issueLink?.inwardIssue?.id ?? '0'
})).fields?.project?.key ?? '';
// Get all the saved issue links from Record Storage with that Sync Key
const savedIssueLinks = await getIssueLinks(storage, issueSyncKeyForInwardIssue);
// Find the matching issue link ID
const issueLinkId = savedIssueLinks.filter(il => il[targetIssueProjectKey] !== issueLink.id);
// Update the Record Storage
await setIssueLinks(storage, issueSyncKeyForInwardIssue, issueLinkId);
}
}
}
// Check if issue type name is Epic
if (FIELDS.includes('issuetype') && issueTypeName === 'Epic') {
// Get the Epic Name custom field ID from source Instance
const sourceInstanceEpicNameCustomField = await getCustomField(sourceInstance, 'Epic Name', sourceProjectKey, eventIssueType?.id ?? '0');
// Check if Epic Name custom field ID from source Instance was found
if (!sourceInstanceEpicNameCustomField) {
// If not, then throw an error
throw Error('Epic Name custom field ID from source Instance was not found');
}
// Get the Epic Name custom field ID from target Instance
const epicNameCustomField = await getCustomField(sourceInstance, 'Epic Name', sourceProjectKey, issueType.id ?? '0');
// Check if Epic Name custom field ID from target Instance was found
if (!epicNameCustomField) {
// If not, then throw an error
throw Error('Epic Name custom field ID from target Instance was not found');
}
// Add the Epic Name value to issue fields
requestBody[epicNameCustomField] = event.issue.fields?.[sourceInstanceEpicNameCustomField];
};
// Check if Epic Links is checked in FIELDS array
if (FIELDS.includes('Epic Link') && issueTypeName !== 'Epic') {
// Get the custom field for Epic Link in source instance
const customField = await getCustomField(sourceInstance, 'Epic Link', sourceProjectKey, eventIssueType?.id ?? '0')
// Extract the Epic key
const fieldValue = event.issue.fields?.[customField ?? ''];
// If there is a value, find matching epic from target instance
if (fieldValue) {
// Get the ScriptRunner Connect Sync Key for Epic issue
const epicSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(fieldValue ?? '', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, event.issue.fields?.parent?.fields?.issuetype?.id ?? '0');
// Check if Sync Key for Epic issue was found
if (!epicSyncIssueKey) {
throw Error('ScriptRunner Connect Sync Issue Key is missing on Epic issue');
}
// Search matching Epic issue from target Instance
const matchingIssue = await searchIssue(context, epicSyncIssueKey ?? '', targetInstance, false);
// Check if a matching Epic issue was found
if (matchingIssue.total === 0) {
// If not, throw an error
throw Error(`Matching Epic issue with sync key ${epicSyncIssueKey} missing`);
}
// Get the Epic Link custom field in target instance
const epicLinkCustomField = await getCustomField(targetInstance, 'Epic Link', targetProjectKey, epicIssueType.id ?? '0')
// Check if the field was found
if (!epicLinkCustomField) {
// If not, throw an error
throw Error("Coudn't find Epic Link custom field in target instance")
}
// If it does, add it to issue fields
if (createIssueFields.includes('Epic Link')) {
requestBody[epicLinkCustomField ?? ''] = matchingIssue.issues?.[0].key;
} else if (fieldsThatIssueSupports.includes('Epic Link')) {
requestBodyForTransition[epicLinkCustomField ?? ''] = matchingIssue.issues?.[0].key;
}
}
}
// Check if custom fields have been added to CUSTOM_FIELDS in Environment variables
if (CUSTOM_FIELDS.length) {
// Filter custom fields that can be added when creating an issue
const customFields = CUSTOM_FIELDS.filter(field => createIssueFields.includes(field));
// Filter custom fields that should be present but cannot be added during issue creation
const requiredCustomFieldsForTranstition = CUSTOM_FIELDS.filter(field =>
!createIssueFields.includes(field) && fieldsThatIssueSupports.includes(field)
);
// If there are custom fields available for issue creation, process and add them to the request body
if (customFields.length > 0) {
const customFieldsBody = await handleCustomFieldsForCreatingIssue(
sourceInstance,
targetInstance,
targetProjectKey,
customFields,
event.issue,
issueType.id ?? '0'
);
requestBody = { ...requestBody, ...customFieldsBody }
}
// If there are required custom fields for transition, process and add them to the transition request body
if (requiredCustomFieldsForTranstition.length > 0) {
const transitionCustomFieldsBody = await handleCustomFieldsForCreatingIssue(
sourceInstance,
targetInstance,
targetProjectKey,
requiredCustomFieldsForTranstition,
event.issue,
issueType?.id ?? '0'
);
requestBodyForTransition = { ...requestBodyForTransition, ...transitionCustomFieldsBody }
}
}
console.log('Issue fields:', requestBody);
// Create the new matching issue
const issue = await targetInstance.Issue.createIssue({
body: {
fields: requestBody,
}
})
// Extract the newly created issue key
const issueKey = issue.key ?? '';
// Extract event issue attachments
const attachments = event.issue.fields?.attachment ?? [];
// Check if there are any attachments
if (FIELDS.includes('Attachment') && attachments.length > 0) {
// Loop through attachments and upload them to target issue
for (const attachment of attachments) {
await uploadAttachment(sourceInstance, targetInstance, attachment, issueKey);
}
}
// CHeck if moved issue has comments and if does, add them to the new issue
if (FIELDS.includes('comments')) {
const issueComments = event.issue.fields?.comment;
// If comments exist, proceed to iterate through each comment
if (issueComments?.total && issueComments.total > 0) {
// Get the project identifiers for the source and target instances
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Array to hold mappings of original comment IDs to created comment IDs
const matchingComments = [];
// Iterate through each comment from the original issue
for (const comment of issueComments.comments ?? []) {
let issueCommentBody = comment.body;
// Check if "Original comment by" is already in the comment body
const hasOriginalUserText = issueCommentBody?.includes('Original comment by');
// Check if the original commentator is different from the current user
if (comment.author?.emailAddress !== myself.emailAddress && !hasOriginalUserText) {
// Add the original commentator
const originalUser = `Original comment by: ${comment.author?.displayName}, (${comment.author?.emailAddress})`;
// Append the original commentator's information to the comment body
issueCommentBody = `${comment.body}\n\n${originalUser}`
}
// Create the comment in target issue
const createdComment = await targetInstance.Issue.Comment.addComment({
issueIdOrKey: issueKey,
body: {
body: issueCommentBody
}
})
// Create a mapping of the original comment ID to the new comment ID
const commentIds = {
[sourceProjectNumberWithKey]: comment.id,
[targetProjectNumberWithKey]: createdComment.id
}
// Store the mapping in the array
matchingComments.push(commentIds)
}
// Save the new comment ID's to Record Storage
await storage.setValue(newIssueKey, matchingComments)
// Delete comment ID's from Record Storage with old ScriptRunner Connect Issue Sync Key
await storage.deleteValue(previousScriptRunnerConnectSyncIssueKey);
}
}
// Check if newly created Issue has the correct status and find the matching status
const createdTargetIssue = await targetInstance.Issue.getIssue({
issueIdOrKey: issueKey
})
const status = getMatchingValue(sourceInstance, event.issue.fields?.status?.name ?? '', STATUS);
// Check if status is incorrect, and then change it
if (createdTargetIssue.fields?.status?.name !== status) {
const transitions = (await targetInstance.Issue.Transition.getTransitions({
issueIdOrKey: issueKey
})).transitions ?? [];
const transitionId = transitions.find(t => t.to?.name === status || t.name === status)?.id ?? ''
// Check if transition ID was found for target instance
if (!transitionId) {
throw Error(`Transition for status not found in target instance: ${status}`);
}
// Change the status of the issue (workflow transition)
await targetInstance.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
},
fields: requestBodyForTransition
}
});
console.log(`Issue ${issueKey} was transitioned`)
};
// Check if the issue is Epic
if (FIELDS.includes('issuetype') && issueTypeName === 'Epic') {
// Retrieve issues linked to the current Epic in the source instance
const epicLinkedIssues = await sourceInstance.Issue.Search.searchByJql({
body: {
jql: `"Epic Link" = ${newIssueKey}`
}
})
// Check if any issues were found linked to this Epic
if (epicLinkedIssues.total && epicLinkedIssues.total > 0) {
// Iterate over each linked issue in the source instance
for (const issue of epicLinkedIssues.issues ?? []) {
// Extract ScriptRunner Connect Sync Issue Key
const issueSyncKey = issue.fields?.[sourceCustomField];
// Find the matching target issue
const targetIssue = await searchIssue(context, issueSyncKey, targetInstance, false);
// Check if a matching target issue was found
if (targetIssue.total === 0) {
// If not, throw an error
throw Error('Matching issue for epic link not found');
}
// Get the Epic Link custom field in target instance
const epicLinkCustomField = (await getCustomField(targetInstance, 'Epic Link', targetProjectKey, epicIssueType.id ?? '0')) ?? '';
// Check if the field was found
if (!epicLinkCustomField) {
// If not, throw an error
throw Error('Epic Link custom field in target instance not found');
}
// Add the Epic Link to the linked issue in the target instance
await targetInstance.Issue.editIssue({
issueIdOrKey: targetIssue.issues?.[0].key ?? '',
body: {
fields: {
[epicLinkCustomField]: issueKey
}
}
})
console.log(`Epic Link added to issue ${targetIssue.issues?.[0].key}`)
}
}
};
// Check if issue links exist in FIELDS array and if the event issue has issue links added
if (FIELDS.includes('issue links') && event.issue.fields?.issuelinks?.length) {
// Check if there are any issue links stored in Record Storage with the old Sync Key
const oldIssueLinks = await storage.valueExists(`issue_link_${previousScriptRunnerConnectSyncIssueKey}`);
// If there issue links with old Sync Key, delete them
if (oldIssueLinks) {
await storage.deleteValue(`issue_link_${previousScriptRunnerConnectSyncIssueKey}`)
}
// Extract the issue links
const issueLinks = event.issue.fields.issuelinks ?? [];
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Iterate over the issue links
for (const issueLink of issueLinks) {
const outwardLinkedIssueKey = issueLink.outwardIssue?.key;
const inwardLinkedIssueKey = issueLink.inwardIssue?.key;
const inwardLinkedIssueId = (issueLink.inwardIssue?.id ?? 0).toString();
// Check for existing issue links from Record Storage
const existingIssueLinks = await getIssueLinks(storage, newIssueKey);
// Check target instance has valid issue link type
const issueLinkTypes = await targetInstance.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 retrySyncIssueKeyForIssueLinks(context, +(issueLink.outwardIssue?.id ?? '0'), sourceInstance, CUSTOM_FIELD_NAME);
if (syncIssueKey) {
// Find the matching issue from target instance
const targetIssue = await retrySearchIssueInTargetInstance(context, syncIssueKey, targetInstance, false);
// Create issue link in target instance
await targetInstance.Issue.Link.createLink({
body: {
outwardIssue: {
key: targetIssue.issues?.[0].key,
},
type: {
name: issueLinkType.name
},
inwardIssue: {
key: issue.key
}
},
})
// Get the issue link id
const createdIssueLinkId = (await targetInstance.Issue.getIssue({
issueIdOrKey: issue.key ?? '0'
})).fields?.issuelinks?.find(link => link.outwardIssue?.key === targetIssue.issues?.[0].key && link.type?.name === issueLink.type?.name)?.id
// Save issue link mapping into Record Storage
const newIssueLink = {
[sourceProjectNumberWithKey]: issueLink.id ?? '0',
[targetProjectNumberWithKey]: createdIssueLinkId ?? '0',
}
// Save issue links to Record Storage
const updatedIssueLinks = [...existingIssueLinks, newIssueLink];
await setIssueLinks(storage, newIssueKey, updatedIssueLinks);
console.log(`Issue link created between ${targetIssue.issues?.[0].key} and ${issue.key}`);
}
}
// Handle inward issue link
if (inwardLinkedIssueKey) {
// Find the Sync key for inward issue
const inwardIssueSyncIssueKey = await retrySyncIssueKeyForIssueLinks(context, inwardLinkedIssueId, sourceInstance, CUSTOM_FIELD_NAME);
if (inwardIssueSyncIssueKey) {
// Search for the inward issue in target instance
const targetIssue = await retrySearchIssueInTargetInstance(context, inwardIssueSyncIssueKey, targetInstance, false);
// Create issue link in target instance
await targetInstance.Issue.Link.createLink({
body: {
outwardIssue: {
key: issue.key
},
type: {
name: issueLink.type?.name
},
inwardIssue: {
key: targetIssue.issues?.[0].key,
}
},
})
// Get the updated target inward issue
const targetIssueWithInward = await targetInstance.Issue.getIssue({
issueIdOrKey: targetIssue.issues?.[0].key ?? '0'
})
// Find the create issue link ID
const createdIssueLinkId = targetIssueWithInward?.fields?.issuelinks?.find(link => link.outwardIssue?.key === issue.key && link.type?.name === issueLink.type?.name)?.id;
// Get inward issue for source instance
const inwardIssue = await sourceInstance.Issue.getIssue({
issueIdOrKey: inwardLinkedIssueKey
})
// Retrieve the project identifier for the inward issue in the source instance
// and the target inward issue in the target instance using their project keys
const inwardIssueProjectNumberWithKey = getProjectIdentifier(sourceInstance, inwardIssue.fields?.project?.key ?? '');
const targetInwardIssueProjectNumberWithKey = getProjectIdentifier(targetInstance, targetIssueWithInward.fields?.project?.key ?? '');
// Get existing inward issue links from storage
const existingInwardIssueLinks = await getIssueLinks(storage, inwardIssueSyncIssueKey);
// Save issue link mapping into Record Storage
const newIssueLink = {
[inwardIssueProjectNumberWithKey]: issueLink.id ?? '0',
[targetInwardIssueProjectNumberWithKey]: createdIssueLinkId ?? '0'
};
// Create updated issue links by filtering out the existing link mapping and adding the new link
const updatedIssueLinks = [
newIssueLink,
...existingInwardIssueLinks.filter(il => il[inwardIssueProjectNumberWithKey] !== issueLink.id)
];
// Save issue links to Record Storage
await setIssueLinks(storage, inwardIssueSyncIssueKey, updatedIssueLinks);
console.log(`Issue link created between ${targetIssue.issues?.[0].key} and ${issue.key}`);
}
}
};
};
// Search for the matching issue again, in case it has been deleted already
const issueToBeDeleted = await searchIssue(context, previousScriptRunnerConnectSyncIssueKey, targetInstance, false)
// Check if the old issue was found
if (issueToBeDeleted.total && issueToBeDeleted.total > 0) {
// If found, check for any sub-tasks associated with the issue
if ((issueToBeDeleted.issues?.[0].fields?.subtasks ?? []).length > 0) {
const subTasks = issueToBeDeleted.issues?.[0].fields?.subtasks ?? [];
for (const subTask of subTasks) {
// Get the sub-task
const issue = await targetInstance.Issue.getIssue({
issueIdOrKey: subTask.key ?? '0',
errorStrategy: {
handleHttp404Error: () => null // Returns null if the sub-task does not exist
}
})
// If the sub-task does not exist, skip to the next iteration
if (!issue) {
continue;
}
const issueLinks = issue.fields?.issuelinks ?? [];
const inwardIssueLinks = issueLinks.filter(link => link.inwardIssue);
const ignoreIssueLinks = []
// Add inward issue link IDs to ignore if they exist
if (inwardIssueLinks.length > 0) {
for (const issueLink of inwardIssueLinks) {
const linkId = issueLink.id
ignoreIssueLinks.push(linkId)
}
}
// If there are issue links to ignore, store them in Record Storage with a TTL
// This is used so we don't end up accidentallly deleting still valid issue links when multiple issues are being moved
if (ignoreIssueLinks.length > 0) {
await storage.setValue('ISSUE_MOVED', ignoreIssueLinks, {
ttl: 5
});
}
}
}
// Proceed to delete the old issue, along with its sub-tasks if any
await targetInstance.Issue.deleteIssue({
issueIdOrKey: issueToBeDeleted.issues?.[0].key ?? '0',
deleteSubtasks: 'true',
})
console.log(`Deleted issue ${issueToBeDeleted.issues?.[0].key}`);
}
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-on-premise/events';
import createComment from '../CreateComment';
/**
* 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;
}
await createComment(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteComment from '../DeleteComment';
/**
* 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;
}
await deleteComment(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import updateComment from '../UpdateComment';
/**
* 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;
}
await updateComment(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { getEnvVars } from '../Utils';
import createIssue from '../CreateIssue';
/**
* 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;
}
const { PredefinedUser } = getEnvVars(context);
await createIssue(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2, PredefinedUser.JIRA_PROJECT_2_EMAIL);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteIssue from '../DeleteIssue';
/**
* 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;
}
await deleteIssue(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2)
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueLinkCreatedEvent } from '@sr-connect/jira-on-premise/events';
import createIssueLink from '../CreateIssueLink';
/**
* 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;
}
await createIssueLink(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueLinkDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteIssueLink from '../DeleteIssueLink';
/**
* 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;
}
await deleteIssueLink(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2)
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import { getEnvVars } from '../Utils';
import updateIssue from '../UpdateIssue';
/**
* 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;
}
const { PredefinedUser } = getEnvVars(context);
await updateIssue(context, event, JiraOnPremiseProject1, JiraOnPremiseProject2, PredefinedUser.JIRA_PROJECT_1_EMAIL);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-on-premise/events';
import createComment from '../CreateComment';
/**
* 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;
}
await createComment(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteComment from '../DeleteComment';
/**
* 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;
}
await deleteComment(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import updateComment from '../UpdateComment';
/**
* 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;
}
await updateComment(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueCreatedEvent } from '@sr-connect/jira-on-premise/events';
import { getEnvVars } from '../Utils';
import createIssue from '../CreateIssue';
/**
* 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;
}
const { PredefinedUser } = getEnvVars(context);
await createIssue(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1, PredefinedUser.JIRA_PROJECT_1_EMAIL);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteIssue from '../DeleteIssue';
/**
* 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;
}
await deleteIssue(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1)
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueLinkCreatedEvent } from '@sr-connect/jira-on-premise/events';
import createIssueLink from '../CreateIssueLink';
/**
* 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;
}
await createIssueLink(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1);
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueLinkDeletedEvent } from '@sr-connect/jira-on-premise/events';
import deleteIssueLink from '../DeleteIssueLink';
/**
* 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;
}
await deleteIssueLink(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1)
}
import JiraOnPremiseProject2 from '../api/jira/on-premise/2';
import JiraOnPremiseProject1 from '../api/jira/on-premise/1';
import { IssueUpdatedEvent } from '@sr-connect/jira-on-premise/events';
import { getEnvVars } from '../Utils';
import updateIssue from '../UpdateIssue';
/**
* 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;
}
const { PredefinedUser } = getEnvVars(context);
await updateIssue(context, event, JiraOnPremiseProject2, JiraOnPremiseProject1, PredefinedUser.JIRA_PROJECT_2_EMAIL);
}
import { RecordStorage } from '@sr-connect/record-storage';
import { GetKeysOfAllRecordsResponse } from '@sr-connect/record-storage/types';
import { throttleAll } from 'promise-throttle-all';
// How many concurrent deletion jobs to maintain
const CONCURRENCY = 3;
/**
* This script purges all cached data.
*/
export default async function (event: any, context: Context): Promise<void> {
const storage = new RecordStorage();
let lastEvaluatedKey: string | undefined;
do {
const keys: GetKeysOfAllRecordsResponse = await storage.getAllKeys();
lastEvaluatedKey = keys.lastEvaluatedKey;
await throttleAll(CONCURRENCY, keys.keys.map((key) => async () => await storage.deleteValue(key)));
} while (lastEvaluatedKey !== undefined);
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueCommentUpdatedEvent } from "@sr-connect/jira-on-premise/events";
import { RecordStorage } from '@sr-connect/record-storage';
import {
extractIssueId,
getComments,
getEnvVars,
getMatchingValue,
getProjectIdentifier,
getScriptRunnerConnectSyncIssueKey,
removeOriginalUserParagraph,
searchIssue
} from './Utils';
/**
* This function updates a corresponding comment when a comment has been updated.
*/
export default async function updateComment(context: Context, event: IssueCommentUpdatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi): Promise<void> {
// Get the current user
const myself = await sourceInstance.Myself.getCurrentUser();
const myselfInTargetInstance = await targetInstance.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 userEmail = event.comment.updateAuthor?.emailAddress;
const userDisplayName = event.comment.updateAuthor?.displayName;
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
user: `${userDisplayName} (${userEmail})`,
commentId: event.comment.id
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
const { JIRA_PROJECTS, CUSTOM_FIELD_NAME } = getEnvVars(context);
// Get the issue ID for the updated comment
const updatedCommentIssueId = extractIssueId(event.comment.self ?? '0');
// Check if Issue ID extraction was successful
if (!updatedCommentIssueId) {
throw new Error("Issue Id extraction failed");
}
// Get the source issue
const issue = await sourceInstance.Issue.getIssue({
issueIdOrKey: updatedCommentIssueId ?? '0',
fields: 'issuetype, project, attachment'
})
// Extract source project key
const sourceProjectKey = issue.fields?.project?.key ?? '0';
// Find matching target project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key from the issue
const scriptRunnerConnectSyncIssueKey = await getScriptRunnerConnectSyncIssueKey(issue.key ?? '0', sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, issue.fields?.issuetype?.id ?? '0');
// Check if ScriptRunner Connect Sync Issue Key is missing
if (scriptRunnerConnectSyncIssueKey === null) {
throw new Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
const storage = new RecordStorage();
const sourceProjectNumberWithKey = getProjectIdentifier(sourceInstance, sourceProjectKey);
const targetProjectNumberWithKey = getProjectIdentifier(targetInstance, targetProjectKey);
// Get matching comment IDs from Record Storage
const commentIds = await getComments(storage, scriptRunnerConnectSyncIssueKey);
// Find the matching comment ID
const commentId = commentIds.find(ci => ci[sourceProjectNumberWithKey] === event.comment.id)?.[targetProjectNumberWithKey];
// Check if matching comment ID was found
if (!commentId) {
throw new 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, true)
// Check if issue with a matching ScriptRunner Connect Sync Issue Key exists
if (issues.total === 0) {
// If not, throw an error
throw Error(`Issue with the matching ScriptRunner Connect Sync Issue Key does not exist in target instance: ${scriptRunnerConnectSyncIssueKey}`);
};
// Save the issue key
const issueKey = issues.issues?.[0].key ?? '';
// Add orignal user text
const originalUserText = `Original comment by: ${event.comment.author?.displayName}, (${event.comment.author?.emailAddress})`;
let issueCommentBody: string = event.comment.body || '';
// Check if "Original comment by" is already in the comment body
const hasOriginalUserText = issueCommentBody.includes('Original comment by');
const matchingCommentAuthor = (await targetInstance.Issue.Comment.getComment({
issueIdOrKey: issueKey,
id: commentId
})).author?.emailAddress;
// Determine if current user is the author of the comment
const isCurrentUserComment = myself.emailAddress === event.comment.author?.emailAddress;
const isMatchingCommentAuthor = myselfInTargetInstance.emailAddress === matchingCommentAuthor;
if (isCurrentUserComment) {
// If the current user is the author
if (isMatchingCommentAuthor) {
// Append original user info if missing
if (!hasOriginalUserText) {
issueCommentBody = `${issueCommentBody}\n\n${originalUserText}`;
}
} else {
// Remove original user info if the author is different
if (hasOriginalUserText) {
issueCommentBody = removeOriginalUserParagraph(issueCommentBody, 'Original comment by');
}
}
} else {
// If the comment author is somebody else
if (hasOriginalUserText && !isMatchingCommentAuthor) {
// Remove original user info if the current user is not the matching author
issueCommentBody = removeOriginalUserParagraph(issueCommentBody, 'Original comment by');
}
// Append original user info if the current user matches the matching comment author
if (isMatchingCommentAuthor && !hasOriginalUserText) {
issueCommentBody = `${issueCommentBody}\n\n${originalUserText}`;
}
}
// Update the comment in target instance
await targetInstance.Issue.Comment.updateComment({
id: commentId,
issueIdOrKey: issueKey,
body: {
body: issueCommentBody
}
});
console.log(`Comment updated for Issue: ${issueKey}`)
}
}
import JiraOnPremise from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { IssueUpdatedEvent } from "@sr-connect/jira-on-premise/events";
import { GetIssueResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue';
import { FieldsEditIssue } from '@managed-api/jira-on-prem-v8-core/definitions/issue';
import { UserFull } from '@managed-api/jira-on-prem-v8-core/definitions/user';
import { Attachment } from '@managed-api/jira-on-prem-v8-core/definitions/attachment';
import moveIssue from './MoveIssue';
import {
checkAccountsByKey,
checkAccountsOnJira,
checkUserFieldOption,
findAssignableUserOnJiraOnPrem,
getCustomField,
getEnvVars,
getJiraCustomFieldIdAndType,
getJiraIssueType,
getJiraPriority,
getMatchingValue,
getOriginalUserToCustomFieldId,
retrySearchIssueInTargetInstance,
searchIssue,
stringToArray,
uploadAttachment
} from "./Utils";
/**
* This function updates a corresponding issue when an issue has been updated.
*/
export default async function updateIssue(context: Context, event: IssueUpdatedEvent, sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi, targetProjectPredifinedUserEmail?: string): Promise<void> {
console.log('Issue Updated event: ', event);
// Get the current user
let myself = await sourceInstance.Myself.getCurrentUser();
const { JIRA_PROJECTS, STATUS, PRIORITY, ISSUE_TYPES, FIELDS, CUSTOM_FIELDS, RetryConfigurations, MOVE_ISSUES_BETWEEN_PROJECTS, UserFieldOptions, CUSTOM_FIELD_NAME } = 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 ?? '') || event.issue_event_type_name === 'issue_moved' && MOVE_ISSUES_BETWEEN_PROJECTS))) {
// Check the update is triggered by issue being moved
if (event.issue_event_type_name === 'issue_moved') {
// Check if we MOVE_ISSUES_BETWEEN_PROJECTS is set to true
if (MOVE_ISSUES_BETWEEN_PROJECTS) {
// If issue got moved and MOVE_ISSUES_BETWEEN_PROJECTS environment variable is true, run moveIssue script
return await moveIssue(context, event, sourceInstance, targetInstance, myself, targetProjectPredifinedUserEmail);
}
// If not, stop the update
return;
}
const userDisplayName = event.user.displayName;
const userEmail = event.user.emailAddress;
const sourceProjectKey = event.issue.fields?.project?.key ?? '';
const updatedIssue = event.issue.key ?? '';
const metaData = {
instance: sourceInstance === JiraOnPremise ? 1 : 2,
project: sourceProjectKey,
issueKey: updatedIssue,
issueId: event.issue.id,
issueType: event.issue.fields?.issuetype?.name,
user: `${userDisplayName} (${userEmail})`,
};
console.log('Going to perform', event.webhookEvent, 'event:', metaData);
// Find matching target project key
const targetProjectKey = getMatchingValue(sourceInstance, sourceProjectKey, JIRA_PROJECTS);
// Get the ScriptRunner Connect Sync Issue Key custom field
const customField = await getCustomField(sourceInstance, CUSTOM_FIELD_NAME, sourceProjectKey, event.issue.fields?.issuetype?.id ?? '0');
// Check if the custom field was found
if (!customField) {
// If not, throw an error
throw Error('No ScriptRunner Connect Sync Issue Key custom field found on the updated issue.')
}
let issue: GetIssueResponseOK | undefined;
let scriptRunnerConnectSyncIssueKey = null;
let statusChanged = false;
let scriptRunnerConnectSyncIssueKeyRetry = 0;
do {
// Get the updated issue
issue = await sourceInstance.Issue.getIssue({
issueIdOrKey: updatedIssue,
});
// Check if the custom field has a value
if (issue?.fields?.[customField] !== null) {
// Extract the ScriptRunner Connect Sync Issue Key
scriptRunnerConnectSyncIssueKey = issue?.fields?.[customField];
break;
} else {
console.log('No ScriptRunner Connect Sync Issue Key found on the updated issue. Retrying...');
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
}
scriptRunnerConnectSyncIssueKeyRetry++;
} while (scriptRunnerConnectSyncIssueKeyRetry < RetryConfigurations.MAX_RETRY_ATTEMPTS);
// Check if ScriptRunner Connect Sync Issue Key custom field is still missing a value after retry logic
if (scriptRunnerConnectSyncIssueKey === null) {
// Throw and error
throw Error('Issue is missing ScriptRunner Connect Sync Issue Key');
}
// Search for the matching issue in the target instance
const issues = await retrySearchIssueInTargetInstance(context, scriptRunnerConnectSyncIssueKey, targetInstance, true)
// Extract the issue that needs updating
const matchingIssueDetails = issues?.issues?.[0];
const issueKey = matchingIssueDetails?.key ?? '0';
// 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 = {};
// Object that contains changes that need to be updated on the source intance
let sourceInstanceRequestBody: FieldsEditIssue = {};
// Find the project to use based on pre-defined project key
const project = await targetInstance.Project.getProject({
projectIdOrKey: targetProjectKey
});
// Check if the project was found
if (!project) {
// If not, then throw an error
throw Error(`Target project not found: ${targetProjectKey}`);
}
// Get the matching issue
const matchingIssue = await targetInstance.Issue.getIssue({
issueIdOrKey: issueKey
});
// Get the matching issue type for target instance
const updatedIssueTypeName = event.issue.fields?.issuetype?.name ?? ''
const mappedIssueType = getMatchingValue(sourceInstance, updatedIssueTypeName, ISSUE_TYPES);
// Get the issue type from target instance
const issueType = await getJiraIssueType(mappedIssueType, targetInstance);
// Get the current user in target instance
const targetInstanceIntegrationUser = await targetInstance.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':
// Add issue type to request body
requestBody.issuetype = {
id: issueType.id ?? ''
};
break;
case 'reporter':
// Extract the reporter email
const reporterEmail = event.issue.fields?.reporter?.emailAddress ?? '';
// Function that check USER_FIELD_OPTION value and handles the field appropriately
const reporterUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
reporterEmail ?? '',
targetInstanceIntegrationUser.name ?? '',
eventItem.field,
event.issue.fields?.reporter?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.id ?? '0',
matchingIssue
);
// If value is returned, add it to the issue fields
if (reporterUserFieldOption) {
requestBody = { ...requestBody, ...reporterUserFieldOption }
}
// Check if COPY_ORIGINAL_USER_TO_CUSTOM_FIELD option is being used
if (UserFieldOptions.USER_FIELD_FALLBACK_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD' ||
UserFieldOptions.USER_FIELD_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD') {
// Find the custom field for storing reporter
const customFieldId = await getOriginalUserToCustomFieldId(
context,
sourceInstance,
eventItem.field,
sourceProjectKey,
event.issue.fields?.issuetype?.id ?? '0'
);
// Check if the field has a value in the source issue, and if so, remove it
const originFieldValue = event.issue.fields?.[customFieldId];
if (originFieldValue) {
sourceInstanceRequestBody[customFieldId] = null;
}
}
break;
case 'assignee':
// Extract the assignee email
const assigneeEmail = event.issue.fields?.assignee?.emailAddress ?? '';
// Check if the field has a value
if (assigneeEmail) {
// Function that check USER_FIELD_OPTION value and handles the field appropriately
const assigneeUserFieldOption = await checkUserFieldOption(
context,
targetInstance,
targetProjectKey,
assigneeEmail ?? '',
targetInstanceIntegrationUser.name ?? '',
eventItem.field,
event.issue.fields?.assignee?.displayName ?? '',
targetProjectPredifinedUserEmail ?? '',
issueType.id ?? '0',
matchingIssue
)
// Add the result from checkUserFieldOption to issue fields
if (assigneeUserFieldOption) {
requestBody = { ...requestBody, ...assigneeUserFieldOption }
}
// Check if COPY_ORIGINAL_USER_TO_CUSTOM_FIELD option is being used
if (UserFieldOptions.USER_FIELD_FALLBACK_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD' ||
UserFieldOptions.USER_FIELD_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD') {
// Find the custom field for storing assignee
const sourceCustomFieldId = await getOriginalUserToCustomFieldId(
context,
sourceInstance,
eventItem.field,
sourceProjectKey,
event.issue.fields?.issuetype?.id ?? '0'
);
// Check if the field has a value in the source issue, and if so, remove it
const originFieldValue = event.issue.fields?.[sourceCustomFieldId];
if (originFieldValue) {
sourceInstanceRequestBody[sourceCustomFieldId] = null;
}
}
} else {
// Check if COPY_ORIGINAL_USER_TO_CUSTOM_FIELD option is being used
if (UserFieldOptions.USER_FIELD_FALLBACK_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD' ||
UserFieldOptions.USER_FIELD_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD') {
// Find the custom field for storing assignee
const customFieldId = await getOriginalUserToCustomFieldId(
context,
targetInstance,
eventItem.field,
targetProjectKey,
issueType?.id ?? '0'
);
requestBody[customFieldId] = null;
}
// Remove user from field
requestBody.assignee = {
name: ''
}
}
break;
case 'priority':
// Extract the new priority
const updateIssuePriority = eventItem.toString ?? '';
// Find the matching priority
const matchingPiority = getMatchingValue(sourceInstance, updateIssuePriority, PRIORITY);
// Find the correct priority
const priority = await getJiraPriority(matchingPiority, targetInstance);
// Add the priority to request body
requestBody.priority = {
id: priority.id
}
break;
case 'labels':
// Add labels new value
requestBody.labels = event.issue.fields?.labels;
break;
case 'duedate':
// Add due date new value
requestBody.duedate = event.issue.fields?.duedate;
break;
case 'description':
// Add new description
requestBody.description = eventItem.toString ?? '';
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) {
// Upload attachment to target instance
await uploadAttachment(sourceInstance, targetInstance, attachment, issueKey);
}
} else {
// Extract issue attachments
const attachments = issues?.issues?.[0].fields?.attachment as Attachment[] ?? [];
// Find matching attachment ID
const attachmentId = (attachments?.find(a => a.filename === eventItem.fromString))?.id ?? '';
// Check if matching attachment ID was found
if (!attachmentId) {
throw Error('Matching attachment ID was not found from target instance');
}
// Delete the attachment
await targetInstance.Issue.Attachment.removeAttachment({
id: attachmentId
})
}
break;
case 'Epic Name':
// Find the custom field from target instance and save the updated Epic Name
const epicNameField = await getCustomField(targetInstance, eventItem.field, targetProjectKey, issueType.id ?? '');
// Check if custom field was found
if (!epicNameField) {
// If not, throw an error
throw Error('Epic Name custom field was not found on issue');
}
// Add the updated Epic Name to request body
requestBody[epicNameField ?? ''] = eventItem.toString
break;
case 'Epic Link':
let value = null;
// Find the Epic Link custom field
const epicLinkCustomField = await getCustomField(targetInstance, eventItem.field, targetProjectKey, issueType.id ?? '');
if (eventItem.toString) {
// Get the linked Epic issue
const epicLinkIssue = await sourceInstance.Issue.getIssue({
issueIdOrKey: eventItem.toString ?? '',
fields: customField
})
// Extract the epic issue sync key
const syncKey = epicLinkIssue.fields?.[customField];
// Find the matching epic issue from target instance
const matchingEpicLinkIssue = await searchIssue(context, syncKey, targetInstance, false)
// Check if matching Epic Issue was found
if (matchingEpicLinkIssue.total === 0) {
// If not, throw an error
throw new Error('Could not find matching Epic Link Issue')
}
// Extract the issue key for matching Epic
value = matchingEpicLinkIssue.issues?.[0].key
}
// Add the updated Epic Name to request body
requestBody[epicLinkCustomField ?? ''] = value;
break;
default:
break;
}
}
// Filter custom fields that were updated and are added in the CUSTOM_FIELDS environment variable
const updatedCustomFields = event.changelog?.items.filter(cl => CUSTOM_FIELDS.includes(cl.field ?? ''));
// Check if any custom fields got updated
if (updatedCustomFields.length) {
// Iterate through updated custom fields
for (const customField of updatedCustomFields) {
// Get custom field ID and type
const jiraCustomField = await getJiraCustomFieldIdAndType(sourceInstance, customField.field ?? '');
// Check if custom field was found
if (!jiraCustomField) {
throw Error(`Field ${customField} not found on Jsource instance`);
}
// Extract the custom field value
const fieldValue = event.issue.fields?.[jiraCustomField.id];
// Find the custom field in target instance
const customFieldId = await getCustomField(targetInstance, customField.field ?? '', project.key ?? '', issueType.id ?? '0');
// Check if custom field was found
if (!customFieldId) {
throw new Error(`Could not find ${customField.field} from target instance`);
}
// Check the type and value of each custom field to appropriately update the issue fields
switch (jiraCustomField.type) {
case 'Text Field (single line)':
case 'Number Field':
case 'URL Field':
case 'Date Picker':
case 'Date Time Picker':
case 'Labels':
case 'Text Field (multi-line)':
case 'Job Checkbox':
case 'Group Picker (single group)':
requestBody[customFieldId] = fieldValue;
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
if (!fieldValue) {
requestBody[customFieldId] = null;
} else {
requestBody[customFieldId] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
}
break;
case 'Select List (cascading)':
if (!fieldValue) {
requestBody[customFieldId] = null;
} else {
requestBody[customFieldId] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
}
break;
case 'Select List (single choice)':
case 'Radio Buttons':
if (!fieldValue) {
requestBody[customFieldId] = null;
} else {
requestBody[customFieldId] = {
value: fieldValue.value
}
}
break;
case 'User Picker (multiple users)':
// Get the target Issue
const targetIssue = await targetInstance.Issue.getIssue({
issueIdOrKey: issueKey,
})
// Extract users that are added on the issue
const currentlyAddedUsersOnJira: UserFull[] | null = targetIssue.fields?.[customFieldId];
// Verify whether any users have been added and if the fieldValue is not null
if (currentlyAddedUsersOnJira === null && fieldValue !== null) {
// Iterate through added users and extract their email
const users = (fieldValue as { emailAddress: string }[])?.map(field => ({
emailAddress: field.emailAddress ?? '',
}));
// Check if accounts can be added to the matching issue on Jira
const accountsToAdd = await checkAccountsOnJira(targetInstance, targetProjectKey, users)
if (accountsToAdd) {
// Adds valid accounts to the custom field
requestBody[customFieldId] = accountsToAdd.map(user => ({ name: user.name }));
}
} else {
// Extract the original Accounts from the issue
const originalListOfAccounts = stringToArray(customField.from ?? '')
// Extract the updated Accounts from the issue
const listOfAccounts = stringToArray(customField.to ?? '');
// Filter which accounts got removed and which added
const removedAccounts = originalListOfAccounts.filter(id => !listOfAccounts.includes(id));
const addedAccounts = listOfAccounts.filter(id => !originalListOfAccounts.includes(id));
// Map through currently added accounts on Jira and save their email and account name
const currentlyAddedUsers = (currentlyAddedUsersOnJira?.map(field => ({
emailAddress: field.emailAddress,
name: field.name
}))) ?? [];
let accountsToRemove: string[] = [];
let accountsToAdd: string[] = []
// If any accounts got removed, add them to the accountsToRemove array
if (removedAccounts.length > 0) {
// Get the account email of removed users
const usersThatGotRemoved = await checkAccountsByKey(sourceInstance, sourceProjectKey, removedAccounts);
// Check if the removed accounts are valid Jira accounts and add them to the accountsToRemove array
const validJiraAccounts = await checkAccountsOnJira(targetInstance, targetProjectKey, usersThatGotRemoved);
if (validJiraAccounts.length > 0) {
accountsToRemove = validJiraAccounts.map(user => user.name ?? '');
}
}
// If any account got added, add them to the accountsToAdd array
if (addedAccounts.length > 0) {
const addedUsers = (fieldValue as UserFull[]).filter(u => addedAccounts.includes(u.key ?? '')).map(user => ({
emailAddress: user.emailAddress ?? '',
}));
// Check if added accounts are valid Jira accounts and add them to the accountsToAdd array
const validJiraAccounts = await checkAccountsOnJira(targetInstance, targetProjectKey, addedUsers);
if (validJiraAccounts.length > 0) {
accountsToAdd = validJiraAccounts.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[customFieldId] = accounts;
}
break;
case 'User Picker (single user)':
// Check if user got added or removed
if (!fieldValue) {
requestBody[customFieldId] = null;
} else {
// Find account for user using email
const account = await findAssignableUserOnJiraOnPrem(targetInstance, targetProjectKey, fieldValue.emailAddress);
// If account is found, add it to the request body
if (account) {
requestBody[customFieldId] = {
name: account
}
} else {
// If not, change the field value to null
requestBody[customFieldId] = null
// Log out a warning
console.warn(`User ${fieldValue.displayName}, (${fieldValue.emailAddress}) is not assignable to the field ${customField.field}.`)
}
}
break;
case 'Group Picker (multiple groups)':
if (!fieldValue) {
requestBody[customFieldId] = null;
} else {
requestBody[customFieldId] = (fieldValue as {
name: string, self: string
}[]).map(group => ({ name: group.name }));
}
break;
default:
break;
}
}
}
console.log('Updated issue fields', requestBody);
// Check if issue status was changed
const updatedStatus = eventItems.find(change => change.field === 'status');
if (updatedStatus) {
const updateIssueStatus = event.issue.fields?.status?.name ?? ''
// Find the matching state
const status = getMatchingValue(sourceInstance, updateIssueStatus, STATUS);
const transitions = (await targetInstance.Issue.Transition.getTransitions({
issueIdOrKey: issueKey,
expand: ['transitions.fields']
})).transitions ?? [];
// Check for correct transition Id
const correctTransition = transitions.find(t => t.to?.name === status || t.name === status);
const transitionId = correctTransition?.id;
// Check if transition ID was found
if (!transitionId) {
throw Error(`Transition ID not found in target instance for status: ${status}. Check the STATUS mapping or the transition might not
be available for the current status`);
}
// Finally change the issue status (workflow transition)
await targetInstance.Issue.Transition.performTransition({
issueIdOrKey: issueKey,
body: {
transition: {
id: transitionId
},
fields: requestBody
}
});
console.log(`Updated issue: ${issueKey}`);
}
// If there are any fields in requestBody, update the issue in target instance
if (Object.keys(requestBody).length > 0 && !statusChanged) {
await targetInstance.Issue.editIssue({
issueIdOrKey: issueKey,
body: {
fields: requestBody,
}
});
console.log(`Updated issue: ${issueKey}`);
}
// If there are any fields in source instance requestBody, update the issue in source instance
if (Object.keys(sourceInstanceRequestBody).length > 0) {
await sourceInstance.Issue.editIssue({
issueIdOrKey: updatedIssue,
body: {
fields: sourceInstanceRequestBody,
}
});
console.log(`Updated source issue: ${updatedIssue}`);
}
}
}
import JiraOnPremise1 from './api/jira/on-premise/1';
import { JiraOnPremApi } from "@managed-api/jira-on-prem-v8-sr-connect";
import { GetIssueResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue';
import { RecordStorage } from '@sr-connect/record-storage';
import { GetIssueCustomFieldsResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue/field/custom';
import { UserFull } from '@managed-api/jira-on-prem-v8-core/definitions/user';
import { FindAssignableUsersResponseOK } from '@managed-api/jira-on-prem-v8-core/types/user/search';
import { Issue } from '@sr-connect/jira-on-premise/types';
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 { SearchIssuesByJqlResponseOK } from '@managed-api/jira-on-prem-v8-core/types/issue/search';
/**
* Function that finds matching value for PROJECT, STATUS, PRIORITY, ISSUE TYPE
*/
export function getMatchingValue(instance: JiraOnPremApi, value: string, attribute: Record<string, string>) {
return instance === JiraOnPremise1 ? attribute[value] : Object.keys(attribute).find(key => attribute[key] === value) ?? '';
}
/**
* Function that finds the priority from Jira
*/
export async function getJiraPriority(value: string, instance: JiraOnPremApi) {
const priorities = await instance.Issue.Priority.getPriorities();
const priority = priorities.find(p => p.name === value);
if (!priority) {
throw new Error(`Could not find priority ${value} on target instance`)
}
return priority
}
/**
* Function that finds issue type from Jira
*/
export async function getJiraIssueType(value: string, instance: JiraOnPremApi) {
const issueTypes = await instance.Issue.Type.getTypes();
const issueType = issueTypes.find(p => p.name === value);
if (!issueType) {
throw new Error(`Could not find issue type ${value} on target instance`)
}
return issueType
}
/**
* Function to fetch create metadata for a specific project and issue type from Jira instance
*/
export async function getCreateMetaData(instance: JiraOnPremApi, projectKey: string, issueTypeId: string) {
// Fetch create metadata for the given project and issue type
const response = await instance.fetch(`/rest/api/2/issue/createmeta/${projectKey}/issuetypes/${issueTypeId}`);
if (!response.ok) {
throw new Error(`Failed to fetch create metadata for project: ${projectKey}, issue type ID: ${issueTypeId}`);
}
const createMetaData: CreateMetaData = await response.json();
if (!createMetaData) {
throw new Error(`Create metadata not found for project: ${projectKey}, issue type: ${issueTypeId}`);
}
return createMetaData;
}
/**
* Function that finds the ID for custom field
*/
export async function getCustomField(instance: JiraOnPremApi, customFieldName: string, projectKey: string, issueTypeId: string) {
const customField = (await instance.Issue.Field.getFields()).filter(f => f.name === customFieldName);
if (customField.length === 0) {
throw Error(`Custom field for instance not found: ${customFieldName}`)
}
if (customField.length > 1) {
const createMetaData = await getCreateMetaData(instance, projectKey, issueTypeId);
let matchingFields: any[] = [];
if (createMetaData.total > 0) {
matchingFields = createMetaData.values.filter(field => field.name === customFieldName);
}
if (matchingFields.length > 1) {
throw Error(`More than one custom field was found with this name: ${customFieldName}`);
}
if (matchingFields.length === 0) {
console.log(`Custom field ${customFieldName} is not assignable to project ${projectKey}, issue type ID: ${issueTypeId}.`);
return
}
return matchingFields?.[0].fieldId as string
}
return customField?.[0].id as string;
}
/**
* Retrieves user information based on the provided account key
*/
export async function checkAccountsByKey(instance: JiraOnPremApi, projectKey: string, accountKeys: string[]): Promise<UserFull[]> {
const maxResults = 50;
let startAt = 0;
let validUsers: UserFull[] = [];
do {
const response = await instance.User.Search.findAssignableUsers({
startAt,
maxResults,
project: projectKey,
});
for (const key of accountKeys) {
const validUser = response.find(u => u.key === key)
if (validUser) {
validUsers.push(validUser);
}
}
startAt = response.length === maxResults ? startAt + maxResults : 0;
} while (startAt > 0);
return validUsers;
}
/**
* Function that checks the value of the USER_FIELD_OPTION and updates the field accordingly
*/
export async function checkUserFieldOption(
context: Context,
targetInstance: JiraOnPremApi,
targetProjectKey: string,
email: string,
integrationUser: string,
fieldName: string,
displayName: string,
targetProjectPredifinedUser: string,
issueType: string,
matchingIssue?: GetIssueResponseOK
) {
const { UserFieldOptions } = getEnvVars(context);
switch (UserFieldOptions.USER_FIELD_OPTION) {
case 'ORIGINAL_USER':
const assignableUser = await findAssignableUserOnJiraOnPrem(targetInstance, targetProjectKey, email);
// Check if user is assignable or not
if (!assignableUser) {
// Handle fallback for non-assignable user
return await checkUserFieldFallbackValue(
context,
targetInstance,
targetProjectKey,
integrationUser,
fieldName,
displayName,
targetProjectPredifinedUser,
issueType
);
}
// Check if USER_FIELD_FALLBACK_OPTION is COPY_ORIGINAL_USER_TO_CUSTOM_FIELD and if matching issue is passed
if (UserFieldOptions.USER_FIELD_FALLBACK_OPTION === 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD' && matchingIssue) {
const customFieldId = await getOriginalUserToCustomFieldId(context, targetInstance, fieldName, targetProjectKey, issueType);
const customFieldValue = matchingIssue?.fields?.[customFieldId];
return customFieldValue
? {
[fieldName]: { name: assignableUser },
[customFieldId]: ''
}
: { [fieldName]: { name: assignableUser } };
}
return { [fieldName]: { name: assignableUser } };
case 'REMAIN_UNASSIGNED':
return fieldName === 'reporter' ? { [fieldName]: { name: integrationUser } } : { [fieldName]: { name: null } }
case 'INTEGRATION_USER':
return {
[fieldName]: { name: integrationUser }
}
case 'PREDEFINED_USER':
const isPredefinedUserAssignable = await findAssignableUserOnJiraOnPrem(
targetInstance,
targetProjectKey,
targetProjectPredifinedUser
);
return isPredefinedUserAssignable ?
await handlePredefinedUser(
targetInstance,
targetProjectKey,
fieldName,
targetProjectPredifinedUser
) : await checkUserFieldFallbackValue(
context,
targetInstance,
targetProjectKey,
integrationUser,
fieldName,
displayName,
targetProjectPredifinedUser,
issueType
);
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserToCustomFieldId(context, targetInstance, fieldName, targetProjectKey, issueType);
return {
[fieldName]: {
name: integrationUser
},
[customFieldId]: displayName
};
default:
return;
}
}
/**
* Function that checks the value of USER_FIELD_FALLBACK_OPTION and updates the field accordingly
*/
export async function checkUserFieldFallbackValue(
context: Context,
targetInstance: JiraOnPremApi,
targetProjectKey: string,
integrationUser: string,
fieldName: string,
displayName: string,
targetProjectPredifinedUser: string,
issueType: string
) {
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, this can be changed to predifined user instead
return fieldName === 'reporter' ? { [fieldName]: { name: integrationUser } } : { [fieldName]: { name: null } }
case 'INTEGRATION_USER':
return { [fieldName]: { name: integrationUser } };
case 'PREDEFINED_USER':
return await handlePredefinedUser(targetInstance, targetProjectKey, fieldName, targetProjectPredifinedUser);
case 'COPY_ORIGINAL_USER_TO_CUSTOM_FIELD':
const customFieldId = await getOriginalUserToCustomFieldId(context, targetInstance, fieldName, targetProjectKey, issueType);
const account = fieldName === 'reporter' ? { [fieldName]: { name: integrationUser } } : { [fieldName]: { name: null } }
return {
[customFieldId]: displayName,
...account
};
case 'HALT_SYNC':
throw Error(`Script halted because user for field ${fieldName} was not found.`)
default:
return;
}
}
/**
* Function that copies original user to custom field
*/
export async function getOriginalUserToCustomFieldId(context: Context, targetInstance: JiraOnPremApi, fieldName: string, projectKey: string, issueType: 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 ?? '', projectKey, issueType);
if (!customFieldId) {
throw Error("Couldn't find Original user custom field for issue.")
}
return customFieldId;
}
/**
* Function that handles predefined user
*/
export async function handlePredefinedUser(targetInstance: JiraOnPremApi, targetProjectKey: string, fieldName: string, targetProjectPredifinedUser: string) {
if (!targetProjectPredifinedUser) {
throw Error('Missing predifined username')
};
const predifinedUser = await findAssignableUserOnJiraOnPrem(targetInstance, targetProjectKey, targetProjectPredifinedUser);
if (!predifinedUser) {
throw Error('Predifined user cannot be set')
}
return {
[fieldName]: { name: predifinedUser }
};
}
/**
* 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 finds the ID and type for Jira custom field
*/
export async function getJiraCustomFieldIdAndType(
instance: JiraOnPremApi,
customFieldName: string
): Promise<{ id: string, type: string } | undefined> {
const maxResults = 50;
let startAt = 0;
let customFields: GetIssueCustomFieldsResponseOK;
do {
customFields = await instance.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 handles finding the ScriptRunner Connect Sync Issue Key custom field value from the issue for Issue Links
*/
export async function getScriptRunnerConnectSyncIssueKeyForIssueLink(
context: Context,
issueKeyOrId: string | number,
instance: JiraOnPremApi,
customFieldName: string
): Promise<string | null> {
const { JIRA_PROJECTS } = getEnvVars(context);
const issue = (await instance.Issue.getIssue({
issueIdOrKey: issueKeyOrId,
errorStrategy: {
handleHttp404Error: () => null
}
}))
if (!issue) {
console.warn(`Issue not found for key/ID: ${issueKeyOrId}.`);
return null;
}
// Check if the linked issue belongs to a project added in the JIRA_PROJECTS array
const isProjectInScope = instance === JiraOnPremise1
? JIRA_PROJECTS.hasOwnProperty(issue.fields?.project?.key ?? '')
: Object.values(JIRA_PROJECTS).includes(issue.fields?.project?.key ?? '');
// Stop issue link creation if issue doesn't belong to synced projects
if (!isProjectInScope) {
throw new Error(`Issue ${issueKeyOrId} does not belong in to a synced project.`)
}
// Get the custom field
const customField = (await getCustomField(instance, customFieldName, issue.fields?.project?.key ?? '', issue.fields?.issuetype?.id ?? '0')) ?? '';
if (!customField) {
console.warn(`Custom field ${customFieldName} not found for issue ${issueKeyOrId}.`);
return null;
}
// Check if the field value exists
const syncIssueKey = issue.fields?.[customField];
if (!syncIssueKey) {
console.warn(`Issue ${issueKeyOrId} is missing the ScriptRunner Connect Sync Issue Key.`);
return null;
}
return syncIssueKey;
}
/**
* Retrieves the ScriptRunner Connect Sync Issue Key from an issue link, with multiple retry attempts
*/
export async function retrySyncIssueKeyForIssueLinks(
context: Context,
issueIdOrKey: number | string,
sourceInstance: JiraOnPremApi,
customFieldName: string
): Promise<string | null> {
const { RetryConfigurations } = getEnvVars(context);
let syncIssueKey: string | null = null;
let attempts = 0;
do {
syncIssueKey = await getScriptRunnerConnectSyncIssueKeyForIssueLink(context, issueIdOrKey, sourceInstance, customFieldName);
if (syncIssueKey) {
break;
}
attempts++;
console.log(`No Sync Issue Key found for issue ${issueIdOrKey}. Retrying attempt ${attempts}/${RetryConfigurations.MAX_RETRY_ATTEMPTS}...`);
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
} while (attempts < RetryConfigurations.MAX_RETRY_ATTEMPTS);
if (!syncIssueKey) {
console.log(`Failed to find Sync Issue Key after all retry attempts.`);
}
return syncIssueKey;
}
/**
* Searches for an issue using the provided Sync Issue Key in the target instance.
*/
export async function retrySearchIssueInTargetInstance(
context: Context,
syncIssueKey: string,
targetInstance: JiraOnPremApi,
includeAttachments: boolean,
): Promise<SearchIssuesByJqlResponseOK> {
const { RetryConfigurations } = getEnvVars(context);
let targetIssue: SearchIssuesByJqlResponseOK;
let attempts = 0;
do {
targetIssue = await searchIssue(context, syncIssueKey, targetInstance, includeAttachments);
if (targetIssue.total !== 0) {
return targetIssue;
}
attempts++;
console.log(`No matching issue found for key ${syncIssueKey}. Retrying attempt ${attempts}/${RetryConfigurations.MAX_RETRY_ATTEMPTS}...`);
await new Promise(resolve => setTimeout(resolve, RetryConfigurations.RETRY_DELAY_SECONDS * 1000));
} while (attempts < RetryConfigurations.MAX_RETRY_ATTEMPTS);
// Throw error if no matching issue was found after max attempts
throw Error(`Issue with the matching ScriptRunner Connect Sync Issue Key does not exist in target instance: ${syncIssueKey}`);
}
/**
* Function that finds the issue using ScriptRunner Connect Sync Issue Key custom field
*/
export async function searchIssue(context: Context, issueKey: string, instance: JiraOnPremApi, includeAttachments: boolean) {
const { CUSTOM_FIELD_NAME } = getEnvVars(context);
const includedFields = includeAttachments ? ['attachment'] : [];
const issues = await instance.Issue.Search.searchByJql({
body: {
jql: `"${CUSTOM_FIELD_NAME}" ~ "${issueKey}"`,
fields: includedFields
}
})
return issues;
}
/**
* Determine project number or identifier based on instance type
*/
export const getProjectIdentifier = (instance: JiraOnPremApi, projectKey: string): string => {
const instanceId = instance === JiraOnPremise1 ? 1 : 2;
return `${instanceId}-${projectKey}`;
};
/**
* 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 checks accounts on Jira using email
*/
export async function checkAccountsOnJira(instance: JiraOnPremApi, projectKey: string, users: {
emailAddress?: string,
}[]): Promise<UserFull[]> {
const maxResults = 50;
let startAt = 0;
let validUsers: UserFull[] = [];
do {
const response = await instance.User.Search.findAssignableUsers({
startAt,
maxResults,
project: projectKey
});
for (const user of users) {
const validUser = response.find(u => u.emailAddress === user.emailAddress)
if (validUser) {
validUsers.push(validUser);
}
}
startAt = response.length === maxResults ? startAt + maxResults : 0;
} while (startAt > 0);
return validUsers;
}
/**
* Function that checks if user can be added to the issue on Jira
*/
export async function findAssignableUserOnJiraOnPrem(instance: JiraOnPremApi, projectKey: string, email: string): Promise<string | null> {
const maxResults = 50;
let startAt = 0;
let users: FindAssignableUsersResponseOK;
do {
users = await instance.User.Search.findAssignableUsers({
startAt,
maxResults,
project: projectKey
});
let user: UserFull | undefined;
user = users.find(u => u.emailAddress === email);
if (user) {
return user.name ?? '';
}
if (users.length === maxResults) {
startAt = startAt + maxResults;
} else {
startAt = 0;
}
} while (startAt > 0);
return null;
}
/**
* Function that extracts issue ID from URL
*/
export function extractIssueId(url: string) {
const match = url.match(/\/issue\/(\d+)/);
return match ? match[1] : null;
}
/**
* Function that handles string value and changes it to string array
*/
export function stringToArray(originalString: string) {
if (originalString.trim() === "" || originalString.trim() === "[]") {
return [];
}
const trimmedString = originalString.trim().replace(/^\[|\]$/g, '');
return trimmedString.split(',').map(item => item.trim());
}
/**
* Function that finds the ScriptRunner Connect Sync Issue Key custom field value from the issue
*/
export async function getScriptRunnerConnectSyncIssueKey(
issueKeyOrId: string,
instance: JiraOnPremApi,
customFieldName: string,
projectKey: string,
issueTypeId: string
): Promise<string | null> {
const customField = (await getCustomField(instance, customFieldName, projectKey, issueTypeId)) ?? '';
const scriptRunnerConnectSyncIssueKey = (await instance.Issue.getIssue({
issueIdOrKey: issueKeyOrId,
fields: customField,
})).fields?.[customField]
return scriptRunnerConnectSyncIssueKey
}
/**
* 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 handles custom fields for issue creation
*/
export async function handleCustomFieldsForCreatingIssue(
sourceInstance: JiraOnPremApi,
targetInstance: JiraOnPremApi,
targetProjectKey: string,
customFields: string[],
eventIssue: Issue,
issueTypeId: string
): Promise<FieldsEditIssue> {
const requestBody: FieldsEditIssue = {};
for (const customField of customFields) {
// Get custom field
const sourceInstanceCustomFieldId = await getCustomField(sourceInstance, customField, targetProjectKey, issueTypeId);
// Save its value
const fieldValue = eventIssue.fields?.[sourceInstanceCustomFieldId ?? ''];
// If the custom field has a value
if (fieldValue) {
// Find the custom field from target instance
const jiraCustomField = await getJiraCustomFieldIdAndType(targetInstance, customField);
switch (jiraCustomField?.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':
case 'Job Checkbox':
case 'Group Picker (single group)':
// Add the value to the request body
requestBody[jiraCustomField.id] = fieldValue;
break;
case 'Select List (multiple choices)':
case 'Checkboxes':
requestBody[jiraCustomField.id] = (fieldValue as { value: string }[]).map(field => ({ value: field.value }));
break;
case 'Select List (cascading)':
requestBody[jiraCustomField.id] = fieldValue?.child
? {
value: fieldValue.value,
child: { value: fieldValue.child.value }
}
: { value: fieldValue.value };
break;
case 'Select List (single choice)':
case 'Radio Buttons':
requestBody[jiraCustomField.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 checkAccountsOnJira(targetInstance, targetProjectKey, users);
if (validAccounts) {
// Adds valid account IDs to the request body
requestBody[jiraCustomField.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(targetInstance, targetProjectKey, fieldValue.emailAddress);
if (user) {
requestBody[jiraCustomField.id] = {
name: user
}
}
break;
case 'Group Picker (multiple groups)':
requestBody[jiraCustomField.id] = (fieldValue as {
name: string, self: string
}[]).map(group => ({ name: group.name }));
break;
default:
break;
}
}
}
// Return the updated requestBody
return requestBody;
}
/**
* Function that handles uploading attachment to target instance
*/
export async function uploadAttachment(sourceInstance: JiraOnPremApi, targetInstance: JiraOnPremApi, attachment: Attachment, issueKey: string) {
if (attachment.content && attachment.filename) {
// Fetch the source issue attachment from the source instance
const storedAttachment = await sourceInstance.fetch(`/secure/attachment/${attachment.id}/${attachment.filename}`, {
headers: {
'x-stitch-store-body': 'true'
}
});
// Check if the fetch request was successful
if (!storedAttachment.ok) {
// Throw an error if the response status indicates a failure
throw Error(`Unexpected response while downloading attachment: ${storedAttachment.status}`);
}
// Retrieve the stored body ID from the response headers
const storedAttachmentId = storedAttachment.headers.get('x-stitch-stored-body-id');
// Check if the stored body ID was returned
if (!storedAttachmentId) {
// Throw an error if the stored body ID is not present
throw new Error('The attachment stored body is was not returned');
}
// Upload the attachment to the target issue using the stored body ID
await targetInstance.fetch(`/rest/api/2/issue/${issueKey}/attachments`, {
method: 'POST',
headers: {
// Include necessary headers for the upload
'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}`);
}
}
/*
* Function to remove the "Original comment by" paragraph from the comment body
*/
export const removeOriginalUserParagraph = (content: string, searchText: string) => {
// Escape any special characters in the searchText to ensure an exact match
const escapedSearchText = searchText.replace(/([.*+?^${}()|[\]\\])/g, '\\$1');
// Create a regular expression to match the escaped search text and everything following it until the end of the line
const regex = new RegExp(`${escapedSearchText}.*`, 'g');
// Replace the matched text with an empty string and trim any extra spaces
return content.replace(regex, '').trim();
};
export function getEnvVars(context: Context) {
return context.environment.vars as EnvVars;
}
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 JiraProject1Value = string;
type JiraProject2Value = string;
interface EnvVars {
JIRA_PROJECTS: Record<JiraProject1Value, JiraProject2Value>;
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_PROJECT_1_EMAIL?: string;
JIRA_PROJECT_2_EMAIL?: string;
};
OriginalUserFields: {
CUSTOM_FIELD_FOR_ASSIGNEE?: string;
CUSTOM_FIELD_FOR_REPORTER?: string;
};
PRIORITY: Record<JiraProject1Value, JiraProject2Value>;
STATUS: Record<JiraProject1Value, JiraProject2Value>;
ISSUE_TYPES: Record<JiraProject1Value, JiraProject2Value>;
MOVE_ISSUES_BETWEEN_PROJECTS: boolean;
}
export interface CreateMetaData {
maxResults: number,
startAt: number,
total: number,
isLast: boolean,
values: {
fieldId: string;
name: string;
[x: string]: any;
}[]
}