Template Content
Not the template you're looking for? Browse more.
About the template
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.
This integration automatically backs up Jira Cloud attachments to Google Drive whenever they are uploaded to Jira issues. When an attachment is added to an issue, the integration:
JiraCloudAttachmentsBackup/PROJ-123/
)This provides an additional layer of data protection, organizes attachments by issue, and makes it easy to access Jira attachments through Google Drive.
This template was vibe coded with AI agent.
You'll need two Connectors configured in ScriptRunner Connect:
Jira Cloud Connector
Google Drive Connector
Configure the following API Connections in the ScriptRunner Connect web UI:
Jira Cloud API Connection
Google Drive API Connection
Configure the following Parameters in the ScriptRunner Connect web UI:
JiraCloudAttachmentsBackup
JiraCloudAttachmentsBackup/PROJ-123/
)Configure an Event Listener for Jira Cloud issue updates:
Issue Updated
OnJiraCloudIssueUpdated
Webhook Configuration in Jira Cloud:
Note: The Issue Updated event will trigger for all issue changes, but the script filters to only process attachment uploads by checking the changelog for field === 'Attachment'
.
To test the integration:
ATTACHMENTS_FOLDER
parameter is configured in ScriptRunner ConnectATTACHMENTS_FOLDER
parameter)JiraCloudAttachmentsBackup/PROJ-123/
)Once configured, the integration runs automatically whenever:
The script uses the changelog to detect attachment additions, ensuring it only processes relevant updates.
Monitor the integration through ScriptRunner Connect web UI:
ATTACHMENTS_FOLDER
parameter is configured in the ScriptRunner Connect web UIJiraCloudAttachmentsBackup
(or use your preferred folder name)import GoogleDrive from './api/google/drive';
import JiraCloud from './api/jira/cloud';
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { FileFields } from '@managed-api/google-drive-v3-core/definitions/File';
import { convertBase64ToBuffer } from '@sr-connect/convert';
/**
* Backs up Jira Cloud attachments to Google Drive when they are uploaded to issues.
* Listens to issue update events and filters for attachment uploads using the changelog.
* Creates a root attachments folder in Google Drive if it doesn't exist,
* then creates an issue-specific subfolder, downloads the attachment from Jira,
* and uploads it to the issue subfolder.
*
* @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<EV>): Promise<void> {
const attachmentsFolderName = context.environment.vars.ATTACHMENTS_FOLDER;
const issueKey = event.issue.key;
// Check if this update includes attachment changes
if (!event.changelog?.items) {
console.log('No changelog items found, skipping');
return;
}
// Filter for attachment additions
const attachmentChanges = event.changelog.items.filter((item) => item.field === 'Attachment');
if (attachmentChanges.length === 0) {
console.log('No attachment changes detected, skipping');
return;
}
console.log(`Processing ${attachmentChanges.length} attachment(s) for issue ${issueKey}`);
// Find or create the root attachments folder in Google Drive
const rootFolderId = await findOrCreateFolder(attachmentsFolderName, null);
// Find or create the issue-specific subfolder
const issueFolderId = await findOrCreateFolder(issueKey, rootFolderId);
// Process each attachment change
for (const change of attachmentChanges) {
// The 'to' field contains the attachment ID
if (change.to) {
await backupAttachment(change.to, issueFolderId, issueKey, attachmentsFolderName);
}
}
console.log(`Successfully processed all attachments for issue ${issueKey}`);
}
/**
* Backs up a single attachment to Google Drive.
*
* @param attachmentId The ID of the attachment to backup
* @param issueFolderId The Google Drive folder ID where the attachment should be stored
* @param issueKey The issue key for logging purposes
* @param attachmentsFolderName The root folder name for logging purposes
*/
async function backupAttachment(
attachmentId: string,
issueFolderId: string,
issueKey: string,
attachmentsFolderName: string,
): Promise<void> {
console.log(`Backing up attachment ID: ${attachmentId}`);
// Get attachment metadata to retrieve filename and MIME type
const attachmentMetadata = await JiraCloud.Issue.Attachment.Metadata.getMetadata({
id: attachmentId,
});
console.log(`Attachment filename: ${attachmentMetadata.filename}`);
// Download the attachment content
const attachmentContent = await JiraCloud.Issue.Attachment.getAttachmentContent({
id: attachmentId,
});
// Convert to buffer if it's a string (base64)
const contentBuffer =
typeof attachmentContent === 'string' ? convertBase64ToBuffer(attachmentContent) : attachmentContent;
// Upload to Google Drive
await uploadToGoogleDrive(
issueFolderId,
attachmentMetadata.filename || `attachment-${attachmentId}`,
contentBuffer,
attachmentMetadata.mimeType || 'application/octet-stream',
);
console.log(
`Successfully backed up attachment ${attachmentMetadata.filename} to Google Drive in ${attachmentsFolderName}/${issueKey}`,
);
}
/**
* Finds a folder in Google Drive by name, or creates it if it doesn't exist.
* Uses Google Drive API to search for the folder by name and MIME type.
*
* @param folderName The name of the folder to find or create
* @param parentFolderId The ID of the parent folder, or null for root
* @returns The ID of the folder
*/
async function findOrCreateFolder(folderName: string, parentFolderId: string | null): Promise<string> {
// Build search query
let query = `name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
if (parentFolderId) {
query += ` and '${parentFolderId}' in parents`;
}
// Search for existing folder
const searchResults = await GoogleDrive.File.getFiles({
q: query,
fields: 'files(id, name)',
});
// If folder exists, return its ID
if (searchResults.files && searchResults.files.length > 0) {
const folderId = searchResults.files[0].id!;
console.log(`Found existing folder '${folderName}': ${folderId}`);
return folderId;
}
// Create the folder if it doesn't exist
console.log(`Folder '${folderName}' not found, creating new folder`);
const createBody: { name: string; mimeType: string; parents?: string[] } = {
name: folderName,
mimeType: 'application/vnd.google-apps.folder',
};
if (parentFolderId) {
createBody.parents = [parentFolderId];
}
const newFolder = await GoogleDrive.File.createFile({
body: createBody,
fields: 'id',
});
// Type guard to check if response is FileFields
if ('id' in newFolder) {
const folderId = (newFolder as FileFields).id!;
console.log(`Created folder '${folderName}': ${folderId}`);
return folderId;
}
throw new Error(`Failed to create folder '${folderName}': unexpected response type`);
}
/**
* Uploads an attachment to Google Drive in the specified folder.
* Uses multipart upload to send both metadata and file content in a single request.
*
* @param folderId The ID of the parent folder in Google Drive
* @param filename The name to use for the uploaded file
* @param content The file content as an ArrayBuffer or Uint8Array
* @param mimeType The MIME type of the file
*/
async function uploadToGoogleDrive(
folderId: string,
filename: string,
content: ArrayBuffer | Uint8Array,
mimeType: string,
): Promise<void> {
console.log(`Uploading ${filename} (${content.byteLength} bytes) to Google Drive folder ${folderId}`);
// Google Drive API accepts string | ArrayBuffer, so we pass Uint8Array as-is (it's compatible at runtime)
await GoogleDrive.File.createFile({
uploadType: 'multipart',
body: {
name: filename,
parents: [folderId],
mimeType: mimeType,
content: content as ArrayBuffer,
},
});
console.log(`Upload completed for ${filename}`);
}
ยฉ 2025 ScriptRunner ยท Terms and Conditions ยท Privacy Policy ยท Legal Notice ยท Cookie Preferences