From baa656303c9c9423a17fc1ab9b44f57497b48d77 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Thu, 18 Dec 2025 10:15:17 +0530 Subject: [PATCH 1/9] slack notification settings --- .../service/notificationService.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/notification/service/notificationService.ts b/src/notification/service/notificationService.ts index cce0936..aa282cd 100644 --- a/src/notification/service/notificationService.ts +++ b/src/notification/service/notificationService.ts @@ -76,9 +76,15 @@ class NotificationService { settings.pipeline_id = event.pipelineId settings.event_type_id = event.eventTypeId for (let h of this.handlers) { + // Handle email notifications (SES and SMTP) if ((h instanceof SESService) || (h instanceof SMTPService)) { await h.handle(event, templateResults, settings, configsMap, destinationMap) } + // Handle Slack notifications for approval events + if (h instanceof SlackService) { + this.logger.info("Processing Slack approval notification"); + await h.handle(event, templateResults, settings, configsMap, destinationMap) + } }} catch(err) { this.logger.error("err" + err) @@ -145,12 +151,19 @@ class NotificationService { this.logger.info(`Processing notification V2 for event type: ${event.eventTypeId}, correlationId: ${event.correlationId}`); this.logger.info(`Using ${notificationSettings.length} pre-provided notification settings`); - // Handle approval notifications - if (event.payload.providers && event.payload.providers.length > 0) { - this.logger.info(`Processing approval notification with ${event.payload.providers.length} providers`); - await this.sendApprovalNotification(event); - this.logger.info(`Approval notification sent successfully`); - return new CustomResponse("notification sent", 200); + // Handle approval notifications (eventTypeId 4 = Approval, 5 = ConfigApproval) + // Approval events use event.payload.providers instead of notificationSettings + if (event.eventTypeId === EVENT_TYPE.Approval || event.eventTypeId === EVENT_TYPE.ConfigApproval) { + this.logger.info(`Detected approval event type: ${event.eventTypeId}`); + if (event.payload.providers && event.payload.providers.length > 0) { + this.logger.info(`Processing approval notification with ${event.payload.providers.length} providers`); + await this.sendApprovalNotification(event); + this.logger.info(`Approval notification sent successfully`); + return new CustomResponse("notification sent", 200); + } else { + this.logger.warn(`Approval event received but no providers found in payload for event ${event.correlationId}`); + return new CustomResponse("", 0, new CustomError("no providers found in payload for approval event", 400)); + } } // Handle scoop notification events From 524818700afcd3b7faa127d64ec9f217a5dae06b Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Thu, 18 Dec 2025 10:23:22 +0530 Subject: [PATCH 2/9] version upgrade --- Dockerfile | 4 ++-- package-lock.json | 17 +++++++++++++---- package.json | 5 ++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 258bfaf..9c2a93e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node AS builder +FROM node:18 AS builder WORKDIR /app COPY package.json . @@ -7,7 +7,7 @@ RUN yarn install COPY /. . RUN yarn build-ts -FROM node:14.2.0 +FROM node:18-slim RUN groupadd -r devtron && useradd -r -g devtron devtron diff --git a/package-lock.json b/package-lock.json index 5b03fe4..fe833cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@types/express": "^4.17.21", "@types/mustache": "^0.8.32", - "@types/node": "^12.0.2", + "@types/node": "^18.0.0", "@types/request": "^2.48.1", "axios": "^1.7.7", "body-parser": "^1.20.3", @@ -723,9 +723,18 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, "node_modules/@types/qs": { diff --git a/package.json b/package.json index 665ea35..8456546 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "app for notification", "main": "dist/server.js", + "engines": { + "node": ">=18.0.0" + }, "scripts": { "start": "ts-node src/server.ts", "dev": "nodemon src/server.ts", @@ -29,7 +32,7 @@ "dependencies": { "@types/express": "^4.17.21", "@types/mustache": "^0.8.32", - "@types/node": "^12.0.2", + "@types/node": "^18.0.0", "@types/request": "^2.48.1", "axios": "^1.7.7", "body-parser": "^1.20.3", From e1abbe3308889344e6a9b0bdd653d68c53706c7d Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Thu, 18 Dec 2025 10:27:21 +0530 Subject: [PATCH 3/9] version upgrade --- Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c2a93e..63e8c8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,12 @@ FROM node:18-slim RUN groupadd -r devtron && useradd -r -g devtron devtron -ENV TINI_VERSION v0.18.0 -RUN arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && echo $arch && wget https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${arch} -O /tini -RUN chmod +x /tini +ENV TINI_VERSION=v0.18.0 +RUN apt-get update && apt-get install -y wget && \ + arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && echo $arch && \ + wget https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${arch} -O /tini && \ + chmod +x /tini && \ + apt-get purge -y wget && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* ENTRYPOINT ["/tini", "--"] WORKDIR /app From 2fd91401c7d30aa2bba664404f6f344409e33738 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Thu, 18 Dec 2025 10:31:04 +0530 Subject: [PATCH 4/9] version upgrade --- Dockerfile | 4 ++-- package-lock.json | 28 ++++++++-------------------- package.json | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63e8c8b..97d591b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18 AS builder +FROM node:20 AS builder WORKDIR /app COPY package.json . @@ -7,7 +7,7 @@ RUN yarn install COPY /. . RUN yarn build-ts -FROM node:18-slim +FROM node:20-slim RUN groupadd -r devtron && useradd -r -g devtron devtron diff --git a/package-lock.json b/package-lock.json index fe833cc..5797ee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@types/express": "^4.17.21", "@types/mustache": "^0.8.32", - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "@types/request": "^2.48.1", "axios": "^1.7.7", "body-parser": "^1.20.3", @@ -38,6 +38,9 @@ "ts-node": "^10.7.0", "tslint": "^5.16.0", "typescript": "^4.8.3" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@babel/code-frame": { @@ -723,20 +726,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -2230,15 +2227,6 @@ "@google-cloud/storage": "^7.7.0" } }, - "node_modules/firebase-admin/node_modules/@types/node": { - "version": "20.19.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.2.tgz", - "integrity": "sha512-9pLGGwdzOUBDYi0GNjM97FIA+f92fqSke6joWeBjWXllfNxZBs7qeMF7tvtOIsbY45xkWkxrdwUfUf3MnQa9gA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", diff --git a/package.json b/package.json index 8456546..a609f50 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "app for notification", "main": "dist/server.js", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "scripts": { "start": "ts-node src/server.ts", @@ -32,7 +32,7 @@ "dependencies": { "@types/express": "^4.17.21", "@types/mustache": "^0.8.32", - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "@types/request": "^2.48.1", "axios": "^1.7.7", "body-parser": "^1.20.3", From 3302746b1b18fed4ae0e22409195fe242bc52e85 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Thu, 18 Dec 2025 13:22:52 +0530 Subject: [PATCH 5/9] version upgrade --- src/destination/destinationHandlers/slackHandler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/destination/destinationHandlers/slackHandler.ts b/src/destination/destinationHandlers/slackHandler.ts index 0bdf44d..13d3b0d 100644 --- a/src/destination/destinationHandlers/slackHandler.ts +++ b/src/destination/destinationHandlers/slackHandler.ts @@ -148,10 +148,13 @@ export class SlackService implements Handler { jsons = Mustache.render(template, event.payload.scoopNotificationConfig.data); }else{ let parsedEvent = this.mh.parseEvent(event as Event, true); + this.logger.info('Parsed event data for Slack:', JSON.stringify(parsedEvent, null, 2)); jsons = Mustache.render(template, parsedEvent); } + this.logger.info('Rendered Mustache template (before JSON parse):', jsons); let j = JSON.parse(jsons) + this.logger.info('Final Slack payload (after JSON parse):', JSON.stringify(j, null, 2)); const res = await sdk.send( { slack: j From 7d210e481e9d5594cc12a795cc62b3887c9dc899 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Fri, 19 Dec 2025 10:18:35 +0530 Subject: [PATCH 6/9] version upgrade --- src/common/mustacheHelper.ts | 111 ++++---- .../destinationHandlers/slackHandler.ts | 11 +- src/tests/approvalTemplateTest.ts | 251 ++++++++++++++++++ 3 files changed, 318 insertions(+), 55 deletions(-) create mode 100644 src/tests/approvalTemplateTest.ts diff --git a/src/common/mustacheHelper.ts b/src/common/mustacheHelper.ts index 3a4fecc..200e613 100644 --- a/src/common/mustacheHelper.ts +++ b/src/common/mustacheHelper.ts @@ -77,7 +77,65 @@ export class MustacheHelper { if(event.eventTypeId===EVENT_TYPE.ScoopNotification){ return this.parseScoopNotification(event) } + + const date = moment(event.eventTime); + const timestamp = isSlackNotification + ? date.unix() + : date.format('dddd, MMMM Do YYYY hh:mm A [GMT]Z'); + let baseURL = event.baseUrl; + + // Handle approval events FIRST (before checking pipelineType) + if (event.eventTypeId===EVENT_TYPE.Approval){ + let imageTagNames,imageComment,imageLink,approvalLink; + let index = -1; + if (event.payload.dockerImageUrl) index = event.payload.dockerImageUrl.lastIndexOf(":"); + if (event.payload.imageTagNames) imageTagNames = event.payload.imageTagNames; + if (event.payload.imageComment) imageComment = event.payload.imageComment; + if (baseURL && event.payload.imageApprovalLink) imageLink =`${baseURL}${event.payload.imageApprovalLink}`; + if (baseURL && event.payload.approvalLink) approvalLink = `${baseURL}${event.payload.approvalLink}`; + + return { + eventTime: timestamp, + triggeredBy: event.payload.triggeredBy || "NA", + appName: event.payload.appName || "NA", + envName: event.payload.envName || "NA", + pipelineName: event.payload.pipelineName || "NA", + imageTag: index >= 0 ? event.payload.dockerImageUrl.substring(index + 1) : "NA", + comment:imageComment, + tags:imageTagNames, + imageApprovalLink:imageLink, + approvalLink:approvalLink, + } + } + + if (event.eventTypeId===EVENT_TYPE.ConfigApproval){ + let protectConfigFileType,protectConfigFileName,protectConfigComment,protectConfigLink,envName,approvalLink; + if (event.payload.protectConfigFileType) protectConfigFileType = event.payload.protectConfigFileType; + if (event.payload.protectConfigFileName) protectConfigFileName = event.payload.protectConfigFileName; + if (event.payload.protectConfigComment) protectConfigComment = event.payload.protectConfigComment.split("\n"); + if (baseURL && event.payload.protectConfigLink) protectConfigLink =`${baseURL}${event.payload.protectConfigLink}`; + if (baseURL && event.payload.approvalLink) approvalLink = `${baseURL}${event.payload.approvalLink}`; + if (!event.payload.envName){ + envName="Base configuration" + } + else{ + envName=event.payload.envName + } + return { + eventTime: timestamp, + triggeredBy: event.payload.triggeredBy || "NA", + appName: event.payload.appName || "NA", + envName: envName, + protectConfigFileType:protectConfigFileType, + protectConfigFileName:protectConfigFileName, + protectConfigComment:protectConfigComment, + protectConfigLink:protectConfigLink, + approvalLink:approvalLink, + } + } + + // Now handle CI/CD events (which need material parsing) let material = event.payload.material; let ciMaterials; if (event.eventTypeId!==EVENT_TYPE.Approval && event.eventTypeId!==EVENT_TYPE.ConfigApproval && event.eventTypeId!=EVENT_TYPE.ImagePromotion){ @@ -116,12 +174,6 @@ export class MustacheHelper { }) : []; } - - const date = moment(event.eventTime); - const timestamp = isSlackNotification - ? date.unix() - : date.format('dddd, MMMM Do YYYY hh:mm A [GMT]Z'); - if (event.pipelineType === "CI") { let buildHistoryLink; if (baseURL && event.payload.buildHistoryLink) buildHistoryLink = `${baseURL}${event.payload.buildHistoryLink}`; @@ -163,52 +215,7 @@ export class MustacheHelper { triggeredWithoutApprovalStyle: event.isDeploymentDoneWithoutApproval ? 'block' : 'none' } } - else if (event.eventTypeId===EVENT_TYPE.Approval){ - let imageTagNames,imageComment,imageLink,approvalLink; - let index = -1; - if (event.payload.dockerImageUrl) index = event.payload.dockerImageUrl.lastIndexOf(":"); - if (event.payload.imageTagNames) imageTagNames = event.payload.imageTagNames; - if (event.payload.imageComment) imageComment = event.payload.imageComment; - if (baseURL && event.payload.imageApprovalLink) imageLink =`${baseURL}${event.payload.imageApprovalLink}`; - if (baseURL && event.payload.approvalLink) approvalLink = `${baseURL}${event.payload.approvalLink}`; - - return { - eventTime: timestamp, - triggeredBy: event.payload.triggeredBy || "NA", - appName: event.payload.appName || "NA", - envName: event.payload.envName || "NA", - pipelineName: event.payload.pipelineName || "NA", - imageTag: index >= 0 ? event.payload.dockerImageUrl.substring(index + 1) : "NA", - comment:imageComment, - tags:imageTagNames, - imageApprovalLink:imageLink, - approvalLink:approvalLink, - } - - - } - else if (event.eventTypeId===EVENT_TYPE.ConfigApproval){ - let protectConfigFileType,protectConfigFileName,protectConfigComment,protectConfigLink,envName,approvalLink; - if (event.payload.protectConfigFileType) protectConfigFileType = event.payload.protectConfigFileType; - if (event.payload.protectConfigFileName) protectConfigFileName = event.payload.protectConfigFileName; - if (event.payload.protectConfigComment) protectConfigComment = event.payload.protectConfigComment.split("\n"); - if (baseURL && event.payload.protectConfigLink) protectConfigLink =`${baseURL}${event.payload.protectConfigLink}`; - if (baseURL && event.payload.approvalLink) approvalLink = `${baseURL}${event.payload.approvalLink}`; - if (!event.payload.envName){ - envName="Base configuration" - } - return { - eventTime: timestamp, - triggeredBy: event.payload.triggeredBy || "NA", - appName: event.payload.appName || "NA", - envName: event.payload.envName || envName, - protectConfigFileType:protectConfigFileType || "NA", - protectConfigFileName:protectConfigFileName || "NA", - protectConfigComment:protectConfigComment || [], - protectConfigLink:protectConfigLink, - approvalLink:approvalLink, - } - } + // Note: Approval and ConfigApproval events are now handled at the top of this function else if (event.eventTypeId === EVENT_TYPE.ImagePromotion ){ let artifactPromotionRequestViewLink : string = `${baseURL}${event.payload?.artifactPromotionRequestViewLink}` diff --git a/src/destination/destinationHandlers/slackHandler.ts b/src/destination/destinationHandlers/slackHandler.ts index 13d3b0d..e7c1b0b 100644 --- a/src/destination/destinationHandlers/slackHandler.ts +++ b/src/destination/destinationHandlers/slackHandler.ts @@ -148,13 +148,16 @@ export class SlackService implements Handler { jsons = Mustache.render(template, event.payload.scoopNotificationConfig.data); }else{ let parsedEvent = this.mh.parseEvent(event as Event, true); - this.logger.info('Parsed event data for Slack:', JSON.stringify(parsedEvent, null, 2)); + console.log('=== SLACK DEBUG: Parsed event data ==='); + console.log(JSON.stringify(parsedEvent, null, 2)); jsons = Mustache.render(template, parsedEvent); } - this.logger.info('Rendered Mustache template (before JSON parse):', jsons); + console.log('=== SLACK DEBUG: Rendered Mustache template ==='); + console.log(jsons); let j = JSON.parse(jsons) - this.logger.info('Final Slack payload (after JSON parse):', JSON.stringify(j, null, 2)); + console.log('=== SLACK DEBUG: Final Slack payload ==='); + console.log(JSON.stringify(j, null, 2)); const res = await sdk.send( { slack: j @@ -163,6 +166,8 @@ export class SlackService implements Handler { return res; } catch (error) { this.logger.error('slack sendNotification error', error) + console.log('=== SLACK DEBUG: Error details ==='); + console.log(error); throw new CustomError("Unable to send slack notification",500); } } diff --git a/src/tests/approvalTemplateTest.ts b/src/tests/approvalTemplateTest.ts new file mode 100644 index 0000000..87f1393 --- /dev/null +++ b/src/tests/approvalTemplateTest.ts @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MustacheHelper } from '../common/mustacheHelper'; +import { Event } from '../notification/service/notificationService'; +import { EVENT_TYPE } from '../common/types'; +import Mustache from 'mustache'; + +// Sample approval event based on the debug log +const approvalEvent: Event = { + eventTypeId: EVENT_TYPE.Approval, + pipelineId: 26, + pipelineType: "CD", + correlationId: "test-approval-correlation-id", + eventTime: "2025-12-19T04:36:12Z", + payload: { + appName: "pk-restart-devtron", + envName: "restart-env", + pipelineName: "cd-26-2nam", + triggeredBy: "admin", + dockerImageUrl: "registry.example.com/app:21609cce-18-38", + imageApprovalLink: "/dashboard/app/26/trigger?approval-node=11&search=&approval-state=pending", + approvalLink: "/dashboard/app/26/trigger?approval-node=11&search=&approval-state=pending", + imageTagNames: ["v1.0", "latest"], + imageComment: "Test approval comment", + providers: [ + { dest: 'slack', configId: 1 } + ] + }, + teamId: 1, + appId: 26, + envId: 1, + clusterId: 1, + isProdEnv: false, + baseUrl: "https://devtron-ent-2.devtron.info" +}; + +// Slack approval template (Block Kit format) +const slackApprovalTemplate = `{ + "text": "šŸ›Žļø Image approval requested for {{appName}}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "šŸ›Žļø Image Approval Request" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Application:*\\n{{appName}}" + }, + { + "type": "mrkdwn", + "text": "*Environment:*\\n{{envName}}" + }, + { + "type": "mrkdwn", + "text": "*Pipeline:*\\n{{pipelineName}}" + }, + { + "type": "mrkdwn", + "text": "*Requested by:*\\n{{triggeredBy}}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Image Tag:* \`{{imageTag}}\`\\n*Time:* " + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Request" + }, + "url": "{{{imageApprovalLink}}}", + "style": "primary" + } + ] + } + ] +}`; + +/** + * Validate Slack Block Kit structure + */ +function validateSlackBlockKit(payload: any): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + // Check required fields + if (!payload.text) { + errors.push('Missing required field: text'); + } + + if (!payload.blocks || !Array.isArray(payload.blocks)) { + errors.push('Missing or invalid blocks array'); + return { isValid: false, errors }; + } + + // Validate each block + payload.blocks.forEach((block: any, index: number) => { + if (!block.type) { + errors.push(`Block ${index}: Missing type`); + } + + // Validate button URLs are not HTML-encoded + if (block.type === 'actions' && block.elements) { + block.elements.forEach((element: any, elemIndex: number) => { + if (element.type === 'button' && element.url) { + if (element.url.includes('&#x') || element.url.includes('&')) { + errors.push(`Block ${index}, Element ${elemIndex}: URL is HTML-encoded: ${element.url}`); + } + // Validate URL format + if (!element.url.startsWith('http://') && !element.url.startsWith('https://')) { + errors.push(`Block ${index}, Element ${elemIndex}: Invalid URL format: ${element.url}`); + } + } + }); + } + }); + + return { + isValid: errors.length === 0, + errors + }; +} + +/** + * Test function to validate approval template parsing + */ +export function testApprovalTemplateParsing() { + console.log('\n=== APPROVAL TEMPLATE PARSING TEST ===\n'); + + const mh = new MustacheHelper(); + + // Parse the event + const parsedEvent = mh.parseEvent(approvalEvent, true); + console.log('1. Parsed Event Data:'); + console.log(JSON.stringify(parsedEvent, null, 2)); + + // Render the template with Mustache + console.log('\n2. Rendering Mustache Template...'); + const renderedTemplate = Mustache.render(slackApprovalTemplate, parsedEvent); + console.log('Rendered Template (raw string):'); + console.log(renderedTemplate); + + // Parse the JSON + console.log('\n3. Parsing JSON...'); + let slackPayload; + try { + slackPayload = JSON.parse(renderedTemplate); + console.log('āœ… JSON parsing successful'); + console.log('Final Slack Payload:'); + console.log(JSON.stringify(slackPayload, null, 2)); + } catch (error) { + console.error('āŒ JSON parsing failed:', error); + return false; + } + + // Validate Slack Block Kit structure + console.log('\n4. Validating Slack Block Kit Structure...'); + const validationResults = validateSlackBlockKit(slackPayload); + + if (validationResults.isValid) { + console.log('āœ… All validations passed!'); + return slackPayload; + } else { + console.log('āŒ Validation failed:'); + validationResults.errors.forEach(err => console.log(` - ${err}`)); + return false; + } +} + +/** + * Test sending notification to Slack + * Set SLACK_WEBHOOK_URL environment variable to test actual sending + */ +export async function testSendToSlack() { + const payload = testApprovalTemplateParsing(); + + if (!payload) { + console.log('\nāŒ Cannot send to Slack - template validation failed'); + return; + } + + const webhookUrl = process.env.SLACK_WEBHOOK_URL; + + if (!webhookUrl) { + console.log('\nāš ļø SLACK_WEBHOOK_URL not set. Skipping actual Slack send test.'); + console.log('To test sending to Slack, set SLACK_WEBHOOK_URL environment variable.'); + return; + } + + console.log('\n5. Sending to Slack...'); + + try { + const fetch = require('node-fetch'); + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); + + const responseText = await response.text(); + + if (response.ok) { + console.log('āœ… Successfully sent to Slack!'); + console.log('Response:', responseText); + } else { + console.log('āŒ Slack API error:'); + console.log('Status:', response.status); + console.log('Response:', responseText); + } + } catch (error) { + console.error('āŒ Error sending to Slack:', error); + } +} + +// Run tests if executed directly +if (require.main === module) { + console.log('Running Approval Template Tests...\n'); + testSendToSlack().then(() => { + console.log('\n=== Tests Complete ==='); + }); +} + From 7ebd90860fba00b830a5419714952fed40ae0d8e Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Fri, 19 Dec 2025 10:22:38 +0530 Subject: [PATCH 7/9] version upgrade --- src/tests/approvalTemplateTest.ts | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/tests/approvalTemplateTest.ts b/src/tests/approvalTemplateTest.ts index 87f1393..1cd2617 100644 --- a/src/tests/approvalTemplateTest.ts +++ b/src/tests/approvalTemplateTest.ts @@ -241,9 +241,106 @@ export async function testSendToSlack() { } } +/** + * Test the SQL template from approval-templates-fixed.sql + */ +export function testSQLTemplate() { + console.log('\n=== TESTING SQL TEMPLATE (Image Approval) ===\n'); + + const sqlTemplate = `{ + "text": "šŸ›Žļø Image approval requested for {{appName}}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "šŸ›Žļø Image Approval Request" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Application:*\\n{{appName}}" + }, + { + "type": "mrkdwn", + "text": "*Environment:*\\n{{envName}}" + }, + { + "type": "mrkdwn", + "text": "*Pipeline:*\\n{{pipelineName}}" + }, + { + "type": "mrkdwn", + "text": "*Requested by:*\\n{{triggeredBy}}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Image Tag:* \`{{imageTag}}\`\\n*Time:* {{#comment}}\\n*Comment:* {{comment}}{{/comment}}{{#tags}}\\n*Tags:* {{tags}}{{/tags}}" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Request" + }, + "url": "{{{imageApprovalLink}}}", + "style": "primary" + } + ] + } + ] +}`; + + const mh = new MustacheHelper(); + const parsedEvent = mh.parseEvent(approvalEvent, true); + + console.log('Rendering SQL template...'); + const rendered = Mustache.render(sqlTemplate, parsedEvent); + + try { + const payload = JSON.parse(rendered); + console.log('āœ… SQL template renders valid JSON'); + + const validation = validateSlackBlockKit(payload); + if (validation.isValid) { + console.log('āœ… SQL template passes Slack validation'); + return payload; + } else { + console.log('āŒ SQL template validation failed:'); + validation.errors.forEach(err => console.log(` - ${err}`)); + return false; + } + } catch (error) { + console.error('āŒ SQL template produces invalid JSON:', error); + return false; + } +} + // Run tests if executed directly if (require.main === module) { console.log('Running Approval Template Tests...\n'); + + // Test the SQL template first + const sqlPayload = testSQLTemplate(); + + if (sqlPayload) { + console.log('\nāœ… SQL template is valid and ready to use!\n'); + } else { + console.log('\nāŒ SQL template has issues that need to be fixed!\n'); + } + + // Then test sending to Slack testSendToSlack().then(() => { console.log('\n=== Tests Complete ==='); }); From db7394615ee34880fe693bc3c3ca77974c2c169d Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Mon, 22 Dec 2025 13:43:44 +0530 Subject: [PATCH 8/9] version upgrade --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1eb7269..0b3ad5c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 AS builder +FROM node:24 AS builder WORKDIR /app COPY package.json . From c65c4c844affb99c8e6985e25c5acfdd36f15672 Mon Sep 17 00:00:00 2001 From: prakhar katiyar Date: Mon, 22 Dec 2025 16:14:25 +0530 Subject: [PATCH 9/9] version upgrade --- src/destination/destinationHandlers/slackHandler.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/destination/destinationHandlers/slackHandler.ts b/src/destination/destinationHandlers/slackHandler.ts index e7c1b0b..aff3b7c 100644 --- a/src/destination/destinationHandlers/slackHandler.ts +++ b/src/destination/destinationHandlers/slackHandler.ts @@ -148,16 +148,9 @@ export class SlackService implements Handler { jsons = Mustache.render(template, event.payload.scoopNotificationConfig.data); }else{ let parsedEvent = this.mh.parseEvent(event as Event, true); - console.log('=== SLACK DEBUG: Parsed event data ==='); - console.log(JSON.stringify(parsedEvent, null, 2)); jsons = Mustache.render(template, parsedEvent); } - - console.log('=== SLACK DEBUG: Rendered Mustache template ==='); - console.log(jsons); let j = JSON.parse(jsons) - console.log('=== SLACK DEBUG: Final Slack payload ==='); - console.log(JSON.stringify(j, null, 2)); const res = await sdk.send( { slack: j @@ -166,8 +159,6 @@ export class SlackService implements Handler { return res; } catch (error) { this.logger.error('slack sendNotification error', error) - console.log('=== SLACK DEBUG: Error details ==='); - console.log(error); throw new CustomError("Unable to send slack notification",500); } }