jiraCloud/jobs/pendingApprovals.js
jmcqueen 0b056739e9 Add modular Jira-to-Webex approvals service with Docker deployment.
Stabilize logging, correlation IDs, service-account Jira auth, and direct approver reminder DMs while splitting the monolith into focused modules with Compose-based deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 08:34:47 -04:00

100 lines
4.6 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { formatDuration } from '../lib/util.js';
export function createPendingApprovalsJob({ jiraService, webexService, log }) {
const { logger, logWarn, logError, logDebug } = log;
function sendApprovalReminder(personEmail, approvals) {
return new Promise(function (resolve, reject) {
let msgText = 'You have the following pending approvals:\n';
for (const approval of approvals) {
msgText += `- [${approval.key}](https://aeo.atlassian.net/servicedesk/customer/portal/135/${approval.key}): ${approval.summary} ${approval.duration}\n`;
}
msgText += '\nYou can find these in the [Requests Pending Approvals](https://aeo.atlassian.net/servicedesk/customer/user/approvals?page=1) portal. If you have any questions or experience issues, please submit a [support request](https://aeo.atlassian.net/servicedesk/customer/portals).';
logDebug('sendApprovalReminder', `Sending direct reminder to ${personEmail}:\n${msgText}`);
webexService.sendWebexMessage({
toPersonEmail: personEmail,
markdown: msgText,
})
.then(result => resolve(result))
.catch(error => reject(error));
});
}
function runPendingApprovals() {
return jiraService.searchUnapprovedRequests()
.then(async function (tickets) {
if (!tickets || tickets.length === 0) {
logger('runPendingApprovals', 'No pending approval tickets found.');
return;
}
const needApproval = {};
for (const ticket of tickets) {
if (ticket.fields.customfield_10033) {
for (const approvals of ticket.fields.customfield_10033) {
if (approvals.approvers.length > 0 && approvals.finalDecision == 'pending') {
for (const approver of approvals.approvers) {
if (approver.approverDecision != 'approved' && new Date(approvals.createdDate.iso8601) < new Date(new Date() - (24 * 60 * 60 * 1000))) {
if (!needApproval[approver.approver.emailAddress]) {
needApproval[approver.approver.emailAddress] = [];
}
needApproval[approver.approver.emailAddress].push({
key: ticket.key,
summary: ticket.fields.summary,
reporter: ticket.fields.reporter.displayName,
duration: formatDuration(approvals.createdDate.iso8601),
});
}
}
}
}
} else {
logWarn('runPendingApprovals', `No approvals setup in customfield_10033 for ${ticket.key}.`);
}
}
for (const approver in needApproval) {
await sendApprovalReminder(approver, needApproval[approver])
.then(() => logger('runPendingApprovals', `Sent reminder to ${approver}.`))
.catch(error => logError('runPendingApprovals', `Failed to send reminder to ${approver}`, error));
}
})
.catch(error => logError('runPendingApprovals', 'Failed to run pending approvals job', error));
}
return { runPendingApprovals };
}
export async function cleanupOldLogs(log) {
const { logger, logError } = log;
const now = Date.now();
const directory = './logs';
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;
let files;
try {
files = await fs.promises.readdir(directory);
} catch (error) {
logError('cleanupOldLogs', `Failed to read log directory ${directory}`, error);
return;
}
for (const file of files) {
const filePath = path.join(directory, file);
try {
const stats = await fs.promises.stat(filePath);
if (stats.isFile() && (now - stats.mtimeMs) > sevenDaysInMs) {
await fs.promises.unlink(filePath);
logger('cleanupOldLogs', `Deleted old log file: ${file}`);
}
} catch (error) {
logError('cleanupOldLogs', `Failed to process log file ${file}`, error);
}
}
}