Template Content
Scripts
About the integration
How does the integration logic work?
Which fields are being synced?
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 your Jira Cloud project and Trello board in sync. The following data is exchanged between Jira Cloud and Trello:
Configure API connections and event listeners:
Add webhooks in Jira Cloud:
project = TEST
).Create a custom field in Jira Cloud:
ScriptRunner Connect Sync Issue Key
to keep items in sync.Fill out the ./Values
file:
https://trello.com/b/ICZWgsrc/test
, ICZWgsrc
is the board ID).async HTTP event
) and paste it here.createTrelloWebhook
script to create a Trello webhook. Then run the CreateTrelloSyncCustomField
script and copy the custom field ID from the console output (e.g., 64a686b1599cd0c5b5690bd0
). Paste this ID here.Sync existing Trello cards and Jira issues (optional):
.json
to the end of the URL (e.g., https://trello.com/c/0gkqqQDX/65-test-card.json
), and copy the first value of the id
field (e.g., 64b132ad42ec9562fbc04b2a
).import Trello from "./api/trello";
import { TRELLO_BOARD_ID, JIRA_SYNC_CUSTOM_FIELD_NAME } from './Values';
/**
* Creates a custom field in Trello when manually invoked
*/
export default async function (event: any, context: Context): Promise<void> {
try {
const boardId = (await Trello.Board.getBoard({ id: TRELLO_BOARD_ID })).id;
// Create custom field in Trello to store sync key value
const field = await Trello.CustomField.createCustomField({
body: {
idModel: boardId,
modelType: 'board',
name: JIRA_SYNC_CUSTOM_FIELD_NAME,
type: 'text',
pos: 'bottom',
display_cardFront: false,
}
});
console.log(`Custom field created in Trello with ID: ${field.id}`);
} catch (e) {
console.error('Failed to create custom field in Trello', e);
}
}
import Trello from "./api/trello";
import { TRELLO_BOARD_ID, TRELLO_CALLBACK_URL } from './Values';
/**
* Creates a webhook in trello when manually invoked.
*/
export default async function (event: any, context: Context): Promise<void> {
try {
// Retrieve board ID
const boardId = (await Trello.Board.getBoard({ id: TRELLO_BOARD_ID })).id;
// Create Trello webhook
const webhook = await Trello.Webhook.createWebhook({
idModel: boardId,
callbackURL: TRELLO_CALLBACK_URL,
description: "ScriptRunner Connect Sync webhook",
});
console.log('Webhook created, ID:', webhook.id);
} catch (e) {
console.error('Failed to create a webhook', e);
}
}
import JiraCloud from './api/jira/cloud';
import { IssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import { getDataFromEvent, processJiraComment } from './Utils';
export default async function (event: IssueCommentCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue does not match the user who set up the integration
if ((myself.accountId !== event.comment.author.accountId)) {
// Collect data to process comment
const { issueKey, commentId, syncKey, commentString } = await getDataFromEvent(event);
// Process comment data to create comment in Trello and sync both comments
await processJiraComment('create', issueKey, commentId, syncKey, commentString);
console.log(`Comment created in Trello: ${commentString}`);
}
}
import JiraCloud from './api/jira/cloud';
import { IssueCommentDeletedEvent } from '@sr-connect/jira-cloud/events';
import { getDataFromEvent, processJiraComment } from './Utils';
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;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue does not match the user who set up the integration
if ((myself.accountId !== event.comment.author.accountId)) {
// Collect data to process comment
const { issueKey, commentId, syncKey, commentString } = await getDataFromEvent(event);
// Process comment data to delete comment from Trello
await processJiraComment('delete', issueKey, commentId, syncKey, '');
console.log(`Comment deleted from Trello: ${commentString}`);
}
}
import JiraCloud from './api/jira/cloud';
import { IssueCommentUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { getDataFromEvent, processJiraComment } from './Utils';
export default async function (event: IssueCommentUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue does not match the user who set up the integration
if ((myself.accountId !== event.comment.author.accountId)) {
// Collect data to process comment
const { issueKey, commentId, syncKey, commentString } = await getDataFromEvent(event);
// Process comment data to update comment
await processJiraComment('update', issueKey, commentId, syncKey, commentString);
console.log(`Comment updated in Trello: ${commentString}`);
}
}
import JiraCloud from './api/jira/cloud';
import Trello from "./api/trello";
import { IssueCreatedEvent } from '@sr-connect/jira-cloud/events';
import { TRELLO_BOARD_ID, LABELS_MAPPING, TRELLO_SYNC_CUSTOM_FIELD_ID, JIRA_SYNC_CUSTOM_FIELD_NAME } from './Values';
import { getTrelloListIdFromJiraStatusName, getTrelloMemberIdFromJiraAssignee, processTrelloCard } from './Utils';
export default async function (event: IssueCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue does not match the user who set up the integration
if ((myself.accountId !== event.user.accountId)) {
const name = event.issue.fields.summary!;
const status = event.issue.fields.status?.name;
const labels = event.issue.fields.labels ?? [];
const key = event.issue.key;
const assigneeName = event.issue.fields.assignee?.displayName;
const reporter = event.issue.fields.reporter?.displayName;
const boardId = (await Trello.Board.getBoard({ id: TRELLO_BOARD_ID })).id;
// Collect data to create Trello card
const listId = await getTrelloListIdFromJiraStatusName(status ?? '', boardId);
const memberId = assigneeName ? await getTrelloMemberIdFromJiraAssignee(assigneeName, boardId) : undefined;
const trelloLabels = LABELS_MAPPING.filter(l => labels.some(la => la === l.jiraLabel)).map(l => l.trelloLabel);
// Create Trello card
const card = await processTrelloCard('create', name, listId, trelloLabels, memberId, boardId, key, '', reporter);
if (!card.id) {
throw new Error(`Card ID not found for the card: ${name}.`);
}
// Add sync key value to Trello card
await Trello.Card.CustomField.updateCustomFieldItem({
idCard: card.id,
idCustomField: TRELLO_SYNC_CUSTOM_FIELD_ID,
body: {
value: {
text: card.id,
},
},
});
const syncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!syncField || !syncField.id) {
throw Error('Sync custom field not found on Jira Cloud.');
}
// Add sync key to newly created Jira issue
await JiraCloud.Issue.editIssue({
issueIdOrKey: key,
body: {
fields: {
[syncField.id]: card.id
}
}
});
}
}
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import JiraCloud from "./api/jira/cloud";
import Trello from "./api/trello";
import { deleteTrelloAttachment, getTrelloListIdFromJiraStatusName, getTrelloMemberIdFromJiraAssignee, processTrelloCard } from './Utils';
import { FIELDS, JIRA_SYNC_CUSTOM_FIELD_NAME, LABELS_MAPPING, TRELLO_BOARD_ID } from './Values';
export default async function (event: IssueUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue does not match the user who set up the integration
if ((myself.accountId !== event.user.accountId) && event.changelog?.items.some(cl => FIELDS.includes(cl.field))) {
const issueSyncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
const boardId = (await Trello.Board.getBoard({ id: TRELLO_BOARD_ID })).id;
const issue = await JiraCloud.Issue.getIssue({
issueIdOrKey: event.issue.key,
fields: ['summary', 'status', 'labels', 'assignee', 'attachment', issueSyncField?.key ?? '']
});
const syncKey = issue.fields?.[issueSyncField?.id ?? ''];
if (!syncKey) {
throw Error(`Jira issue ${issue.key} is not synced.`);
}
// Process labels update
if (event.changelog?.items.some(cl => cl.field === 'labels')) {
const labels = issue.fields?.labels;
const trelloLabels = (labels ?? []).reduce((acc, curr) => {
const label = LABELS_MAPPING.find(lm => lm.jiraLabel === curr);
if (label) {
return [...acc, label.trelloLabel];
}
return acc;
}, [] as string[]);
// Update Trello card
const card = await processTrelloCard('update', '', '', trelloLabels, '', boardId, event.issue.key, syncKey);
console.log(`Labels processed for Trello card: ${card.id}`);
}
// Process assignee update
if (event.changelog?.items.some(cl => cl.field === 'assignee')) {
const assigneeName = issue.fields?.assignee?.displayName;
// In case the assignee was removed from the Trello card
if (issue.fields?.assignee === null) {
// Update Trello card
const card = await processTrelloCard('update', '', '', undefined, null, boardId, event.issue.key, syncKey);
console.log(`Assignee processed for Trello card: ${card.id}`);
} else { // In case assignee was added to the Jira issue
if (assigneeName === undefined) {
throw Error(`Assignee name not found in the event payload. ${event.issue?.fields?.assignee?.accountId}.`);
}
const memberId = await getTrelloMemberIdFromJiraAssignee(assigneeName, boardId);
// Update Trello card
const card = await processTrelloCard('update', '', '', undefined, memberId, boardId, event.issue.key, syncKey);
console.log(`Assignee processed for Trello card ${card.id}`);
}
}
// Process status update
if (event.changelog?.items.some(cl => cl.field === 'status')) {
const status = issue.fields?.status?.name;
if (!status) {
throw Error(`Status name not found in the event payload for Jira issue: ${issue.key}.`);
}
const listId = await getTrelloListIdFromJiraStatusName(status ?? '', boardId);
// Update Trello card
const card = await processTrelloCard('update', '', listId, undefined, '', boardId, event.issue.key, syncKey);
console.log(`List transition processed for Trello card: ${card.id}`);
}
// Process summary update
if (event.changelog?.items.some(cl => cl.field === 'summary')) {
const summary = issue.fields?.summary;
if (!summary) {
throw Error(`Summary not found in the event payload for Jira issue: ${issue.key}`);
}
// Update Trello card
const card = await processTrelloCard('update', summary, '', undefined, '', boardId, event.issue.key, syncKey);
console.log(`Card name processed for Trello card: ${card.id}`);
}
// Process attachment update
if (event.changelog?.items.some(cl => cl.field === 'Attachment')) {
const item = event.changelog?.items.find(cl => cl.field === 'Attachment');
if (!item?.fromString) {
const attachmentName = item?.toString;
const content = event.issue.fields?.attachment?.find(a => a.filename === attachmentName);
// Retrieving content of an attachment from Jira
const response = await JiraCloud.fetch(`/rest/api/2/attachment/content/${content?.id}`);
const contentArr = await response.arrayBuffer();
// Processing attachment
await Trello.Card.Attachment.createAttachment({
id: syncKey,
body: {
name: attachmentName ?? '',
file: contentArr,
mimeType: content?.mimeType ?? ''
},
setCover: false,
});
console.log(`Attachment added to Trello card: ${attachmentName}`);
} else {
const attachmentName = item?.fromString;
await deleteTrelloAttachment(syncKey, attachmentName);
console.log(`Attachment removed from Trello card: ${attachmentName}`);
}
}
}
}
import Trello from "./api/trello";
import JiraCloud from "./api/jira/cloud";
import { JIRA_ISSUE_TYPE, JIRA_PROJECT_KEY, TRELLO_SYNC_CUSTOM_FIELD_ID, JIRA_SYNC_CUSTOM_FIELD_NAME, STATUS_LIST_MAPPING, LABELS_MAPPING } from './Values';
import { getComments, TrelloEvent, StorageValue } from './Utils';
import { RecordStorage } from "@sr-connect/record-storage";
import { CreateIssueRequest } from "@managed-api/jira-cloud-v3-core/types/issue";
import { CardAsResponse } from "@managed-api/trello-core/definitions/CardAsResponse";
export default async function (event: { body: TrelloEvent }, 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;
}
if (!event.body) {
console.warn('Ping event while creating webhook.');
return;
}
const currentUser = await Trello.Member.getMember({ id: 'me' });
// Check if the user who triggered the event does not match the user who set up the integration
if (currentUser.id !== event.body.action.idMemberCreator) {
const eventType = event.body.action.type;
console.log('Triggered Trello event', eventType);
// Extract the data from the event payload
const cardId = event.body.action.data.card?.id;
const memberId = event.body.action.data.member?.id;
const creatorId = event.body.action.memberCreator?.id;
const attachmentId = event.body.action?.data?.attachment?.id;
const attachmentName = event.body.action?.data?.attachment?.name;
const comment = event.body.action.data?.text || event.body.action.data?.action?.text;
const name = event.body.action.data.card?.name;
const list = event.body.action.data.list?.name || event.body.action.data.listAfter?.name;
if (!cardId) {
throw Error('Event is not suitable for processing.');
}
// Collect sync data from created card
const creator = creatorId ? await Trello.Member.getMember({ id: creatorId }) : undefined;
const member = memberId ? await Trello.Member.getMember({ id: memberId }) : undefined;
const labels = (await Trello.Card.getCard({ id: cardId })).labels;
// Process event according to the event type
switch (eventType) {
case 'createCard':
// Add sync key value to Trello card
await Trello.Card.CustomField.updateCustomFieldItem({
idCard: cardId,
idCustomField: TRELLO_SYNC_CUSTOM_FIELD_ID,
body: {
value: {
text: cardId,
},
},
});
// Create Jira Issue from created card
await processJiraIssue('create', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, {
syncKey: cardId,
summary: name,
status: list,
assigneeFullName: member?.fullName,
reporterFullName: creator?.fullName,
labels,
});
break;
case 'removeLabelFromCard':
case 'addLabelToCard':
// Add labels to Jira issue
await processJiraIssue('update', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, { labels }, await checkSyncAndReturnJiraIssueKey(cardId));
break;
case 'addMemberToCard':
// Check if name is present
if (!member?.fullName) {
throw Error(`Member's name not found. ${memberId}.`);
}
// Add assignee to Jira issue
await processJiraIssue('update', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, { assigneeFullName: member.fullName }, await checkSyncAndReturnJiraIssueKey(cardId));
break;
case 'removeMemberFromCard':
// Remove assignee from Jira issue
await processJiraIssue('update', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, { assigneeFullName: null }, await checkSyncAndReturnJiraIssueKey(cardId));
break;
case 'updateCard':
const translationKey = event.body.action.display.translationKey;
if (translationKey === 'action_renamed_card') {
// Update summary for Jira issue
await processJiraIssue('update', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, { summary: name }, await checkSyncAndReturnJiraIssueKey(cardId));
} else if (translationKey === 'action_move_card_from_list_to_list') {
// Update status for Jira issue
await processJiraIssue('update', JIRA_PROJECT_KEY, JIRA_ISSUE_TYPE, { status: list }, await checkSyncAndReturnJiraIssueKey(cardId));
}
break;
case 'commentCard':
if (!comment) {
throw Error(`Comment not found in the event payload for card ${cardId}`);
}
// Add comment from the event to synched issue on Jira
await processTrelloComment('create', cardId, comment, event.body.action.id);
console.log('Comment created in Jira Cloud:', comment);
break;
case 'updateComment':
if (!comment) {
throw Error(`Comment not found in the event payload for card ${cardId}`);
}
// Add comment from the event to synched issue on Jira
await processTrelloComment('update', cardId, comment, event.body.action.data.action?.id);
console.log('Comment updated in Jira Cloud:', comment);
break;
case 'deleteComment':
// Delete comment from Jira issue
await processTrelloComment('delete', cardId, '', event.body.action.data?.action?.id);
console.log('Comment deleted from Jira Cloud');
break;
case 'addAttachmentToCard':
if (!attachmentId || !attachmentName) {
throw Error('No attachment ID found in event payload.');
}
// Create attachment on Jira issue
await JiraCloud.Issue.Attachment.addAttachments({
issueIdOrKey: await checkSyncAndReturnJiraIssueKey(cardId),
body: [
{
fileName: attachmentName,
content: await getTrelloAttachmentContent(cardId, attachmentId, attachmentName)
}
]
});
console.log('Attachment added to Jira', attachmentName);
break;
case 'deleteAttachmentFromCard':
if (!attachmentId || !attachmentName) {
throw Error('No attachment ID found in event payload.');
}
const issue = await JiraCloud.Issue.getIssue({
issueIdOrKey: await checkSyncAndReturnJiraIssueKey(cardId)
});
const attachment = issue.fields?.attachment?.find(a => a.filename === attachmentName);
if (!attachment || !attachment.id) {
throw Error(`Attachment with name ${attachmentName} not found in Jira.`);
}
// Delete attachment from Jira issue
await JiraCloud.Issue.Attachment.deleteAttachment({
id: attachment.id
});
console.log('Attachment deleted from Jira', attachmentName);
break;
default:
break;
}
}
}
/**
* Check if Trello card is in sync with Jira and returns synced Jira issue key
*/
async function checkSyncAndReturnJiraIssueKey(cardId: string): Promise<string> {
// Check if card is in sync
const sync = await getSyncCustomFieldValueFromTrello(cardId, TRELLO_SYNC_CUSTOM_FIELD_ID);
if (!sync) {
throw Error(`Card is not synced, ${cardId}`);
}
// Get synced Jira issue
const issueKey = await getSyncedJiraIssueKey(cardId);
if (!issueKey) {
throw Error(`Issue key not found for sync key ${cardId}`);
}
return issueKey;
}
/**
* Process creation and update of Jira issue
*/
async function processJiraIssue(
operation: 'create' | 'update',
projectKey: string,
typeName: string,
fieldsData: {
syncKey?: string,
summary?: string,
status?: string,
assigneeFullName?: string | null,
reporterFullName?: string,
labels?: CardAsResponse['labels']
}, issueKey?: string
) {
let users;
let jiraFields = {};
// Find the project to use based on pre-defined project key
const project = await JiraCloud.Project.getProject({
projectIdOrKey: projectKey,
});
// Check if the project was found
if (!project || !project.id) {
// If not, then throw an error
throw Error('Project not found');
}
// Find all the issue types for given project
const issueTypes = await JiraCloud.Issue.Type.getTypesForProject({
projectId: +(project.id ?? 0) // + sign converts the string to number
});
// Find the issue type to use based on pre-defined issue type name
const issueType = issueTypes.find(it => it.name === typeName);
// Check if the issue type was found
if (!issueType || !issueType.id) {
// If not, then throw an error
throw Error('Issue Type not found');
}
// Get sync key
const syncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!syncField || !syncField.id) {
throw Error('Sync custom field not found on Jira Cloud.');
}
// Check if the assignee value is passed
if (fieldsData.assigneeFullName) {
users = await JiraCloud.User.getUsers();
const assignee = users.find(u => u.displayName === fieldsData.assigneeFullName);
if (assignee) {
jiraFields = {
...jiraFields,
assignee: {
accountId: assignee.accountId
}
}
} else {
console.error(`Assignee user not found on Jira Cloud instance, ${fieldsData.assigneeFullName}.`);
}
} else if (fieldsData.assigneeFullName === null) { // If the assignee value is null, that means that assignee was removed and provide according value
jiraFields = {
...jiraFields,
assignee: {
name: null
}
}
}
// Check if labels data is present
if (fieldsData.labels) {
const filteredLabels = LABELS_MAPPING.filter(l => fieldsData.labels?.some(lm => l.trelloLabel === lm.name));
jiraFields = {
...jiraFields,
labels: filteredLabels.map(l => l.jiraLabel),
}
}
// Check if sync data is present
if (fieldsData.syncKey) {
jiraFields = {
...jiraFields,
[syncField.id]: fieldsData.syncKey,
}
}
// Check if summary data is present
if (fieldsData.summary) {
jiraFields = {
...jiraFields,
summary: fieldsData.summary,
}
}
// Check if reporter's full name is present
if (fieldsData.reporterFullName) {
jiraFields = {
...jiraFields,
description: {
version: 1,
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: `Trello Card created by: ${fieldsData.reporterFullName}`
}
]
}
]
},
}
}
const bodyData: {
fields: Record<string, any>
} = {
fields: {
project: {
id: project.id
},
issuetype: {
id: issueType.id
},
...jiraFields,
},
};
let issue;
// Check if the operation is for creation
if (operation === 'create' && bodyData.fields.summary) {
issue = await JiraCloud.Issue.createIssue({
body: bodyData as CreateIssueRequest['body']
});
}
// Check if the operation is for update
if (operation === 'update' && issueKey) {
await JiraCloud.Issue.editIssue({
issueIdOrKey: issueKey,
body: bodyData
});
issue = {
key: issueKey
}
}
if (!issue || !issue.key) {
throw Error('Failed to process Jira issue.');
}
// Find Jira status if Trello list value is passed
const jiraStatus = STATUS_LIST_MAPPING.find(s => s.trelloList === fieldsData.status)?.jiraStatus;
if (fieldsData.status && !jiraStatus) {
throw Error(`Failed to get correct Jira status for Trello list: ${fieldsData.status}.`);
}
if (jiraStatus) {
// Find Jira transitions
const transition = await JiraCloud.Issue.Transition.getTransitions({
issueIdOrKey: issue.key
});
// Find Jira transition ID
const id = transition?.transitions?.find(t => t.name?.localeCompare(jiraStatus, 'en', { sensitivity: 'base' }) === 0)?.id;
if (!id) {
throw Error('Failed to retrieve transition id for Jira Status.');
}
// Perform Jira issue transition according to status/list mapping
await JiraCloud.Issue.Transition.performTransition({
issueIdOrKey: issue.key,
body: {
transition: {
id
}
}
});
}
console.log(`Jira issue processed: ${issue.key}`);
return issue;
}
/**
* Find an issue that belongs to Trello card
*/
async function getSyncedJiraIssueKey(syncKey: string): Promise<string> {
// You may want to use another means of storing Incident number in Jira Cloud side, for example use a custom field
const issues = (await JiraCloud.Issue.Search.searchByJql({
body: {
jql: `"${JIRA_SYNC_CUSTOM_FIELD_NAME}" ~ "${syncKey}"`
}
})).issues ?? [];
// If no issues with that sync key is found we throw an Error
if (issues.length === 0) {
throw Error('No issue is found');
}
if (issues.length > 1) {
throw Error(`Found more than 1 matching issue with sync key (${syncKey}): ${issues.map(i => i.key).join(', ')}`)
}
if (!issues[0].key) {
throw Error('No issue key presented.');
}
return issues[0].key;
}
/**
* Create, update or delete Jira issue comment
*/
async function processTrelloComment(type: 'create' | 'update' | 'delete', cardId: string, comment: string, trelloCommentId?: string) {
const jiraIssueKey = await checkSyncAndReturnJiraIssueKey(cardId);
const storage = new RecordStorage();
switch (type) {
case 'create': {
// Create comment in Jira
const commentResponse = await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: comment
}
]
}
]
}
}
});
// Get existing comments from Record Storage with the same ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, cardId);
const commentIds = {
[jiraIssueKey]: commentResponse.id,
[cardId]: trelloCommentId
}
// Check if existing comments exist
if (!comments) {
// If they don't, create a new record
await storage.setValue(cardId, [commentIds]);
} else {
// Id they do, update the existing record
const updatedComments = [...comments, commentIds];
await storage.setValue(cardId, updatedComments);
}
break;
}
case 'update': {
const stored = await storage.getValue<StorageValue[]>(cardId);
const jiraCommentId = (stored?.find(x => x[cardId] === trelloCommentId))?.[jiraIssueKey];
if (!jiraCommentId) {
throw Error(`Comment ${trelloCommentId} not synced.`);
}
// Update comment on Jira
await JiraCloud.Issue.Comment.updateComment({
id: jiraCommentId,
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: comment
}
]
}
]
}
}
});
break;
}
case 'delete': {
const stored = await storage.getValue<StorageValue[]>(cardId);
const jiraCommentId = (stored?.find(x => x[cardId] === trelloCommentId))?.[jiraIssueKey];
if (!jiraCommentId) {
throw Error(`Comment ${trelloCommentId} not synced.`);
}
// Delete comment on Jira
await JiraCloud.Issue.Comment.deleteComment({
issueIdOrKey: jiraIssueKey,
id: jiraCommentId
});
break;
}
default:
throw Error(`Unknown type: ${type}`)
}
}
/**
* Function gets array buffer of the attachment from Trello
*/
async function getTrelloAttachmentContent(cardId: string, attachmentId: string, name: string): Promise<ArrayBuffer> {
const response = await Trello.fetch(`/1/cards/${cardId}/attachments/${attachmentId}/download/${name}`);
if (!response.ok) {
throw new Error(`Unexpected response while getting attachment ${attachmentId} content for card ${cardId} in Trello: ${response.status}`);
}
return await response.arrayBuffer();
}
/**
* Function gets sync key from Trello card
*/
async function getSyncCustomFieldValueFromTrello(cardId: string, customFieldId: string): Promise<string> {
const customFields = await Trello.Card.CustomField.getCustomFieldItems({
id: cardId,
});
const syncKey = customFields.find(f => f.idCustomField === customFieldId)?.value?.text;
if (!syncKey) {
throw Error(`Sync key not found for the card: ${cardId}`);
}
return syncKey;
}
import JiraCloud from './api/jira/cloud';
import Trello from "./api/trello";
import { RecordStorage } from '@sr-connect/record-storage';
import { JIRA_SYNC_CUSTOM_FIELD_NAME, STATUS_LIST_MAPPING } from "./Values";
import { IssueCommentCreatedEvent, IssueCommentDeletedEvent, IssueCommentUpdatedEvent } from "@sr-connect/jira-cloud/events";
import { CardAsResponse } from '@managed-api/trello-core/definitions/CardAsResponse';
/**
* Function that collects data from Jira event payload
*/
export async function getDataFromEvent(event: IssueCommentUpdatedEvent | IssueCommentDeletedEvent | IssueCommentCreatedEvent) {
const commentId = event.comment.id;
const commentString = event.comment.body;
const issueSyncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
const issue = await JiraCloud.Issue.getIssue({ issueIdOrKey: event.issue.key, fields: [issueSyncField?.key ?? ''] });
if (!issue.key) {
throw Error(`Jira issue not found.`);
}
const syncKey = issue.fields?.[issueSyncField?.id ?? ''];
if (!syncKey) {
throw Error(`Jira issue ${issue.key} is not synced.`);
}
return {
issueKey: issue.key,
commentId,
syncKey,
commentString
}
}
/**
* Function that processes Jira comment and performs creation, update or removal actions in Tello
*/
export async function processJiraComment(type: 'create' | 'update' | 'delete', jiraIssueKey: string, jiraCommentId: string, cardId: string, comment: string) {
const storage = new RecordStorage();
switch (type) {
case 'create': {
// Create comment in Trello
const createdComment = await Trello.Card.Comment.addComment({
id: cardId,
text: comment,
});
// Get existing comments from Record Storage with the same ScriptRunner Connect Sync Issue Key
const comments = await getComments(storage, cardId);
const commentIds = {
[jiraIssueKey]: jiraCommentId,
[cardId]: createdComment.id
}
// Check if the existing comments exist in the storage
if (!comments) {
// If they don't, create a new record
await storage.setValue(cardId, [commentIds]);
} else {
// If they do, update the existing record
await storage.setValue(cardId, [...comments, commentIds]);
}
break;
}
case 'update': {
const stored = await storage.getValue<StorageValue[]>(cardId);
const trelloCommentId = (stored?.find(x => x[jiraIssueKey] === jiraCommentId))?.[cardId];
if (!trelloCommentId) {
throw Error(`Jira comment ${jiraCommentId} not synced.`);
}
await Trello.Card.Comment.updateComment({
id: cardId,
idAction: trelloCommentId,
text: comment,
});
break;
}
case 'delete': {
const stored = await storage.getValue<StorageValue[]>(cardId);
const trelloCommentId = (stored?.find(x => x[jiraIssueKey] === jiraCommentId))?.[cardId];
if (!trelloCommentId) {
throw Error(`Jira comment ${jiraCommentId} not synced.`);
}
await Trello.Card.Comment.deleteComment({
id: cardId,
idAction: trelloCommentId,
});
break;
}
default:
throw Error(`Unknown type: ${type}`)
}
}
/**
* Deletes Trello attachment
*/
export async function deleteTrelloAttachment(cardId: string, attachmentName: string) {
const attachments = await Trello.Card.Attachment.getAttachments({ id: cardId });
const attachmentId = attachments.find(a => a.name === attachmentName)?.id;
if (!attachmentId) {
throw Error(`Attachment with name: ${attachmentName} not found in Trello.`);
}
await Trello.Card.Attachment.deleteAttachment({
id: cardId,
idAttachment: attachmentId,
});
}
/**
* Retrieves comments from cached storage
*/
export async function getComments(storage: RecordStorage, syncKey: string) {
return await storage.getValue<Record<string, string>[]>(syncKey) ?? [];
}
/**
* Creates or update Trello card with synced data
*/
export async function processTrelloCard(type: 'create' | 'update', name: string, idList: string, labels: string[] | undefined, memberId: string | undefined | null, boardId: string, jiraIssueKey: string, cardId?: string, reporter?: string): Promise<CardAsResponse> {
let bodyData: {
idList?: string;
name?: string;
idLabels?: string[];
idMembers?: string[];
desc?: string;
} = {};
if (name) {
bodyData = {
...bodyData,
name
}
}
if (idList) {
bodyData = {
...bodyData,
idList
}
}
if (labels) {
bodyData = {
...bodyData,
idLabels: await getTrelloLabelIds(labels, boardId),
}
}
if (memberId) {
bodyData = {
...bodyData,
idMembers: [memberId]
}
}
if (memberId === null) {
bodyData = {
...bodyData,
idMembers: []
}
}
if (reporter) {
bodyData = {
...bodyData,
desc: `Jira issue created by: ${reporter}`
}
}
switch (type) {
case 'create': {
if (!bodyData.idList) {
throw new Error(`idList field is required to create Trello Card.`);
}
return await Trello.Card.createCard({
idList: bodyData.idList,
...bodyData
});
}
case 'update': {
if (!cardId) {
throw new Error('Card ID is required to updated Trello Card.');
}
return await Trello.Card.updateCard({
id: cardId,
...bodyData
});
}
default:
throw Error(`Unknown type: ${type}`);
}
}
/**
* Get Trello label IDs for given label names
*/
export async function getTrelloLabelIds(labelNames: string[], boardId: string): Promise<string[]> {
const labels = await Trello.Board.getLabels({ id: boardId });
const labelIds = labels.reduce<string[]>((acc, curr) => {
if (labelNames.some(n => n === curr.name) && curr.id) {
acc.push(curr.id);
}
return acc;
}, []);
if (!labelIds) {
throw Error(`Trello IDs for labels not found for ${labelNames.join(', ')}.`);
}
return labelIds;
}
/**
* Get Trello list ID for a given Jira status name
*/
export async function getTrelloListIdFromJiraStatusName(jiraStatus: string, boardId: string): Promise<string> {
const trelloListName = STATUS_LIST_MAPPING.find(l => l.jiraStatus.localeCompare(jiraStatus, 'en', { sensitivity: 'base' }) === 0)?.trelloList;
if (!trelloListName) {
throw Error(`Trello list name not found for Jira issue type: ${jiraStatus}`);
}
const listsList = await Trello.Board.getLists({ id: boardId });
const id = listsList.find(l => l.name === trelloListName)?.id;
if (!id) {
throw Error(`Trello list ID not found for ${trelloListName}.`);
}
return id;
}
/**
* Get Trello member ID for a given Jira assignee display name
*/
export async function getTrelloMemberIdFromJiraAssignee(name: string, boardId: string): Promise<string> {
const members = await Trello.Board.Member.getMembers({ id: boardId });
const memberId = members.find(m => m.fullName === name)?.id;
if (!memberId) {
throw Error(`Trello member ID not found for Jira assignee: ${name}.`);
}
return memberId;
}
export interface TrelloEvent {
model: {
id: string;
name: string;
};
action: {
id: string;
idMemberCreator: string;
data: {
action?: {
id: string;
text: string;
};
attachment?: {
id: string;
name: string;
url: string;
};
listAfter?: {
id: string;
name: string;
};
old?: {
value: any;
};
card?: {
id: string;
name: string;
idShort: number;
shortLink: string;
};
list?: {
id: string;
name: string;
};
board?: {
id: string;
name: string;
shortLink: string;
};
customField?: {
id: string;
name: string;
};
customFieldItem?: {
id: string;
value: {
text?: string;
};
};
idMember?: string;
member?: {
id: string;
name: string;
};
value?: string;
text?: string; // Comment
label?: {
id: string;
name: string;
color: string;
};
};
type: string;
date: string;
display: {
translationKey: string;
entities: {
card: {
type: string;
id: string;
shortLink: string;
text: string;
},
list: {
type: string;
id: string;
text: string;
},
memberCreator: {
type: string;
id: string;
username: string;
text: string;
}
}
};
memberCreator: {
id: string;
fullName: string;
username: string;
};
}
}
export interface StorageValue {
[x: string]: string;
};
// Name of the custom field that is used to keep items in sync between Jira Cloud and Trello, use this name to create a text custom field in Jira Cloud
export const JIRA_SYNC_CUSTOM_FIELD_NAME = 'ScriptRunner Connect Sync Issue Key';
// Trello Board ID from the URL
export const TRELLO_BOARD_ID = 'bkQz7DZe';
// Callback URL from Generic Event Listener for Trello
export const TRELLO_CALLBACK_URL = 'https://event.scriptrunnerconnect.com/7dfs3dymp7kcrgllbgxpuc';
// ID of the field used to store sync key between Jira and Trello items in Trello side, you should get the ID by running CreateTrelloSyncCustomField script, copy it from console and paste it here
export const TRELLO_SYNC_CUSTOM_FIELD_ID = '6548e426582875da0f9d6313';
// List of labels that will be synced between Trello and Jira
export const LABELS_MAPPING = [
{
jiraLabel: 'warning',
trelloLabel: 'alert'
},
{
jiraLabel: 'good',
trelloLabel: 'average'
},
];
// List of statuses/lists that will be synced between Trello and Jira
export const STATUS_LIST_MAPPING = [
{
jiraStatus: 'To Do',
trelloList: 'To Do'
},
{
jiraStatus: 'In Progress',
trelloList: 'Doing'
},
{
jiraStatus: 'Done',
trelloList: 'Done'
},
];
// Jira Cloud project key, where issue will be created and synced
export const JIRA_PROJECT_KEY = 'DEMO';
// Type of Jira Cloud issues that will be created
export const JIRA_ISSUE_TYPE = 'Task';
// Jira Cloud fields to keep in sync (don't add more since they are not supported in this default implementation)
export const FIELDS = ['summary', 'assignee', 'status', 'Attachment', 'labels'];