x-fidelity 3.9.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/CODEOWNERS +15 -0
  2. package/.xfi-config.json +23 -1
  3. package/CHANGELOG.md +28 -0
  4. package/canary-install-example.sh +44 -0
  5. package/dist/core/cli.js +2 -1
  6. package/dist/core/engine/analyzer.js +3 -1
  7. package/dist/demoConfig/node-fullstack.json +15 -1
  8. package/dist/facts/repoFilesystemFacts.js +7 -7
  9. package/dist/index.js +41 -0
  10. package/dist/notifications/index.d.ts +3 -0
  11. package/dist/notifications/index.js +120 -0
  12. package/dist/notifications/notificationManager.d.ts +19 -0
  13. package/dist/notifications/notificationManager.js +249 -0
  14. package/dist/notifications/notifications.test.d.ts +1 -0
  15. package/dist/notifications/notifications.test.js +100 -0
  16. package/dist/notifications/providers/emailProvider.d.ts +17 -0
  17. package/dist/notifications/providers/emailProvider.js +86 -0
  18. package/dist/notifications/providers/slackProvider.d.ts +11 -0
  19. package/dist/notifications/providers/slackProvider.js +67 -0
  20. package/dist/notifications/providers/teamsProvider.d.ts +10 -0
  21. package/dist/notifications/providers/teamsProvider.js +95 -0
  22. package/dist/types/notificationTypes.d.ts +23 -0
  23. package/dist/types/notificationTypes.js +2 -0
  24. package/dist/types/typeDefs.d.ts +15 -0
  25. package/dist/utils/jsonSchemas.js +53 -0
  26. package/dist/xfidelity +41 -0
  27. package/flagCheck.js +26 -0
  28. package/package.json +6 -3
  29. package/src/core/cli.ts +2 -1
  30. package/src/core/engine/analyzer.ts +3 -1
  31. package/src/demoConfig/node-fullstack.json +15 -1
  32. package/src/facts/repoFilesystemFacts.ts +7 -7
  33. package/src/index.ts +55 -1
  34. package/src/notifications/index.ts +119 -0
  35. package/src/notifications/notificationManager.ts +272 -0
  36. package/src/notifications/notifications.test.ts +116 -0
  37. package/src/notifications/providers/emailProvider.ts +90 -0
  38. package/src/notifications/providers/slackProvider.ts +63 -0
  39. package/src/notifications/providers/teamsProvider.ts +89 -0
  40. package/src/types/notificationTypes.ts +26 -0
  41. package/src/types/typeDefs.ts +15 -0
  42. package/src/utils/jsonSchemas.ts +53 -0
@@ -0,0 +1,15 @@
1
+ # Default owners for everything in the repo
2
+ * @zotoio/core-team
3
+
4
+ # Specific ownership rules
5
+ /src/core/ @zotoio/core-team
6
+ /src/facts/ @zotoio/rules-team
7
+ /src/operators/ @zotoio/rules-team
8
+ /src/plugins/ @zotoio/plugin-team
9
+ /src/server/ @zotoio/api-team
10
+ /src/types/ @zotoio/core-team
11
+ /src/utils/ @zotoio/core-team
12
+ /website/ @zotoio/docs-team
13
+
14
+ # Notification test team
15
+ /src/notifications/ @zotoio/notification-team
package/.xfi-config.json CHANGED
@@ -34,5 +34,27 @@
34
34
  ],
35
35
  "additionalFacts": ["customFact"],
36
36
  "additionalOperators": ["customOperator"],
37
- "additionalPlugins": ["xfiPluginSimpleExample"]
37
+ "additionalPlugins": ["xfiPluginSimpleExample"],
38
+ "notifications": {
39
+ "recipients": {
40
+ "email": [
41
+ "io@zoto.io"
42
+ ],
43
+ "slack": [
44
+ "U123456",
45
+ "U789012"
46
+ ],
47
+ "teams": [
48
+ "user1@example.com",
49
+ "user2@example.com"
50
+ ]
51
+ },
52
+ "codeOwners": true,
53
+ "notifyOnSuccess": false,
54
+ "notifyOnFailure": true,
55
+ "customTemplates": {
56
+ "success": "# Success! 🎉\n\nYour codebase passed all X-Fidelity checks.\n\n- Archetype: ${archetype}\n- Files analyzed: ${fileCount}\n- Execution time: ${executionTime} seconds\n\nGreat job keeping the code clean!",
57
+ "failure": "# Issues Detected ⚠️\n\nX-Fidelity found issues in your codebase:\n\n- Archetype: ${archetype}\n- Files analyzed: ${fileCount}\n- Total issues: ${totalIssues}\n - Warnings: ${warningCount}\n - Errors: ${errorCount}\n - Fatalities: ${fatalityCount}\n\n## Affected Files\n${affectedFiles}\n\nPlease address these issues as soon as possible."
58
+ }
59
+ }
38
60
  }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [3.11.0](https://github.com/zotoio/x-fidelity/compare/v3.10.0...v3.11.0) (2025-03-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Remove duplicate import statement in notifications test ([f5c6468](https://github.com/zotoio/x-fidelity/commit/f5c6468063c1575610cd4b54aad84b97d4884437))
7
+ * **results:** fix missing filesystem match details ([bc62faf](https://github.com/zotoio/x-fidelity/commit/bc62faff20e69fa6977e1ceee1bc22ad578cf2ba))
8
+ * update NotificationManager mock to properly handle registerProvider ([6102837](https://github.com/zotoio/x-fidelity/commit/6102837f206938500885faf33560a91235a41885))
9
+
10
+
11
+ ### Features
12
+
13
+ * add CODEOWNERS and notification test config ([5dc2904](https://github.com/zotoio/x-fidelity/commit/5dc29047d86f33b27fc9603077884b4df3f37330))
14
+ * add debug logging for notification flow ([0e6af6b](https://github.com/zotoio/x-fidelity/commit/0e6af6b1583636935561f85a33c0bbea830905a5))
15
+ * add detailed debug logging for email notifications ([18d929b](https://github.com/zotoio/x-fidelity/commit/18d929b3cc49b538c6ea3330d51d2670b090f7ba))
16
+ * add email notification configuration to node-fullstack archetype ([424c5a0](https://github.com/zotoio/x-fidelity/commit/424c5a007c1b9eabeaffbca375cac131142ff7c9))
17
+ * add notification system with email, slack and teams providers ([95d4ade](https://github.com/zotoio/x-fidelity/commit/95d4ade7de8db30c1592087ffa8dd78845c4ee47))
18
+ * add notification team to CODEOWNERS file ([edb648e](https://github.com/zotoio/x-fidelity/commit/edb648e7378239ae7b6f672b9ce73c97e820a82a))
19
+ * add package version to XFI_RESULT metadata ([1ee061a](https://github.com/zotoio/x-fidelity/commit/1ee061a971bbf63d18014f529a3353d4efb63482))
20
+ * add xfiVersion field to ResultMetadata interface ([e59b187](https://github.com/zotoio/x-fidelity/commit/e59b18725b98781f547e58c5d9581d2ad7d5949f))
21
+
22
+ # [3.10.0](https://github.com/zotoio/x-fidelity/compare/v3.9.1...v3.10.0) (2025-03-08)
23
+
24
+
25
+ ### Features
26
+
27
+ * **canary:** add example canary rollout script using launchdarkly ([73fbcfb](https://github.com/zotoio/x-fidelity/commit/73fbcfb4eca11be8af7eb9fca20f7ed132898a83))
28
+
1
29
  ## [3.9.1](https://github.com/zotoio/x-fidelity/compare/v3.9.0...v3.9.1) (2025-03-02)
2
30
 
3
31
 
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ #
3
+ # This script uses the LaunchDarkly Node SDK to evaluate a feature flag
4
+ # and conditionally run a command based on the flag’s value.
5
+ #
6
+ # Requirements:
7
+ # - Node.js installed.
8
+ # - LaunchDarkly Node SDK installed:
9
+ # yarn global add launchdarkly-node-client-sdk
10
+ #
11
+ # Set your LaunchDarkly clientid and feature flag key here.
12
+ # note that the clientid is not a secret, as it is public in the client-side SDKs
13
+ #
14
+ # export XFI_LD_CLIENT_ID="YOUR_LAUNCHDARKLY_CLIENT_ID"
15
+ # export XFI_VERSION_FLAG_KEY="your-feature-flag-key"
16
+
17
+ # get the first commit on this repo to use as the context key
18
+ XFI_CONTEXT_KEY=$(git rev-list --max-parents=0 HEAD | head -n 1)
19
+
20
+ # print config
21
+ echo "XFI_CONTEXT_KEY: $XFI_CONTEXT_KEY"
22
+ echo "XFI_LD_CLIENT_ID: $XFI_LD_CLIENT_ID"
23
+ echo "XFI_VERSION_FLAG_KEY: $XFI_VERSION_FLAG_KEY"
24
+
25
+ # Run a Node.js script to evaluate the flag.
26
+ FLAG_VALUE=$(node ./flagCheck.js $XFI_CONTEXT_KEY)
27
+ echo "FLAG_VALUE: $FLAG_VALUE"
28
+
29
+ # Check if the Node.js command succeeded.
30
+ if [ $? -ne 0 ]; then
31
+ echo 'Error: Failed to evaluate the feature flag.'
32
+ exit 0
33
+ fi
34
+
35
+ # Decide which command to run based on the flag value.
36
+ if [ "$FLAG_VALUE" == "true" ]; then
37
+ echo "Installing the NEW version of xfi"
38
+ # add your canary CI install command here
39
+ # eg. yarn global add x-fidelity@3.9.1 --ignore-engines
40
+ else
41
+ echo "Installing the current version of xfi"
42
+ # add your current CI install command here
43
+ # eg. yarn global add x-fidelity@2.17.2 --ignore-engines
44
+ fi
package/dist/core/cli.js CHANGED
@@ -35,7 +35,8 @@ function initCLI() {
35
35
  info: (obj) => console.log(JSON.stringify(obj)),
36
36
  error: (obj) => console.error(JSON.stringify(obj)),
37
37
  warn: (obj) => console.warn(JSON.stringify(obj)),
38
- debug: (obj) => console.debug(JSON.stringify(obj))
38
+ debug: (obj) => console.debug(JSON.stringify(obj)),
39
+ trace: (obj) => console.trace(JSON.stringify(obj))
39
40
  };
40
41
  global.logger = fallbackLogger;
41
42
  }
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.analyzeCodebase = analyzeCodebase;
13
13
  const logger_1 = require("../../utils/logger");
14
14
  const configManager_1 = require("../configManager");
15
+ const package_json_1 = require("../../../package.json");
15
16
  const openaiUtils_1 = require("../../utils/openaiUtils");
16
17
  const telemetry_1 = require("../../utils/telemetry");
17
18
  const repoFilesystemFacts_1 = require("../../facts/repoFilesystemFacts");
@@ -172,7 +173,8 @@ function analyzeCodebase(params) {
172
173
  exemptCount: exemptCount,
173
174
  options: cli_1.options,
174
175
  repoPath,
175
- repoUrl
176
+ repoUrl,
177
+ xfiVersion: package_json_1.version
176
178
  }
177
179
  };
178
180
  // Send telemetry for analysis end
@@ -59,6 +59,20 @@
59
59
  ".*\\.(ts|tsx|js|jsx)$",
60
60
  ".*\\/xfiTestMatch\\.json$",
61
61
  ".*\\/README\\.md$"
62
- ]
62
+ ],
63
+ "notifications": {
64
+ "enabled": true,
65
+ "providers": ["email"],
66
+ "recipients": {
67
+ "email": ["io@zoto.io"]
68
+ },
69
+ "codeOwners": true,
70
+ "notifyOnSuccess": true,
71
+ "notifyOnFailure": true,
72
+ "customTemplates": {
73
+ "success": "All checks passed successfully! 🎉\n\nArchetype: ${archetype}\nFiles analyzed: ${fileCount}\nExecution time: ${executionTime}s",
74
+ "failure": "Issues found in codebase:\n\nArchetype: ${archetype}\nTotal issues: ${totalIssues}\n- Warnings: ${warningCount}\n- Errors: ${errorCount}\n- Fatalities: ${fatalityCount}\n\nAffected files:\n${affectedFiles}"
75
+ }
76
+ }
63
77
  }
64
78
  }
@@ -130,11 +130,11 @@ function repoFileAnalysis(params, almanac) {
130
130
  return result;
131
131
  }
132
132
  //if there is already a resultFact for this file, we need to append
133
- // const existingResult = almanac.factValue(params.resultFact);
134
- // if (Object.keys(existingResult).includes('result')) {
135
- // logger.error(JSON.stringify(existingResult));
136
- // result.result = existingResult.result;
137
- // }
133
+ const existingResult = almanac.factValue(params.resultFact);
134
+ if (Object.keys(existingResult).includes('result')) {
135
+ logger_1.logger.error(JSON.stringify(existingResult));
136
+ result.result = existingResult.result;
137
+ }
138
138
  const analysis = [];
139
139
  const lines = fileContent.split('\n');
140
140
  logger_1.logger.debug({ lineCount: lines.length }, 'Processing file lines');
@@ -196,8 +196,8 @@ function repoFileAnalysis(params, almanac) {
196
196
  result.result[resultLength + i] = fileAnalysis[i];
197
197
  }
198
198
  }
199
- //result.result. = analysis;
200
- almanac.addRuntimeFact(params.resultFact, result);
199
+ result.result = analysis;
200
+ almanac.addRuntimeFact(params.resultFact, result.result);
201
201
  return result;
202
202
  // testing match on 'oracle'
203
203
  // testing match on 'declare'
package/dist/index.js CHANGED
@@ -62,6 +62,7 @@ const prettyjson_1 = __importDefault(require("prettyjson"));
62
62
  const analyzer_1 = require("./core/engine/analyzer");
63
63
  const configServer_1 = require("./server/configServer");
64
64
  const telemetry_1 = require("./utils/telemetry");
65
+ const notifications_1 = require("./notifications");
65
66
  // Function to handle errors and send telemetry
66
67
  const handleError = (error) => __awaiter(void 0, void 0, void 0, function* () {
67
68
  yield (0, telemetry_1.sendTelemetry)({
@@ -85,6 +86,16 @@ logger_1.logger.debug({ options: cli_1.options }, 'Startup options');
85
86
  function main() {
86
87
  return __awaiter(this, void 0, void 0, function* () {
87
88
  try {
89
+ // Initialize notification system
90
+ const notificationConfig = {
91
+ enabled: process.env.NOTIFICATIONS_ENABLED === 'true',
92
+ providers: (process.env.NOTIFICATION_PROVIDERS || '').split(',').filter(Boolean),
93
+ codeOwnersPath: process.env.CODEOWNERS_PATH || '.github/CODEOWNERS',
94
+ codeOwnersEnabled: process.env.CODEOWNERS_ENABLED !== 'false', // Default to true
95
+ notifyOnSuccess: process.env.NOTIFY_ON_SUCCESS === 'true',
96
+ notifyOnFailure: process.env.NOTIFY_ON_FAILURE !== 'false', // Default to true
97
+ };
98
+ const notificationManager = yield (0, notifications_1.initializeNotifications)(notificationConfig);
88
99
  if (cli_1.options.examine && process.env.NODE_ENV !== 'test') {
89
100
  const { validateArchetypeConfig } = yield Promise.resolve().then(() => __importStar(require('./core/validateConfig')));
90
101
  validateArchetypeConfig();
@@ -103,6 +114,24 @@ function main() {
103
114
  });
104
115
  const resultString = JSON.stringify(resultMetadata);
105
116
  const prettyResult = prettyjson_1.default.render(resultMetadata.XFI_RESULT);
117
+ // Add debug logging before notification check
118
+ logger_1.logger.debug({
119
+ notificationsEnabled: process.env.NOTIFICATIONS_ENABLED,
120
+ notificationConfig: notificationConfig,
121
+ hasNotificationManager: !!notificationManager
122
+ }, 'Checking notification status');
123
+ // Send notifications if enabled
124
+ if (notificationConfig.enabled) {
125
+ logger_1.logger.debug('Notifications are enabled, preparing to send report');
126
+ // Get list of affected files (those with issues)
127
+ const affectedFiles = getAffectedFiles(resultMetadata);
128
+ logger_1.logger.debug({
129
+ affectedFilesCount: affectedFiles.length,
130
+ hasRepoConfig: !!resultMetadata.XFI_RESULT.repoXFIConfig
131
+ }, 'Preparing notification data');
132
+ // Pass the repo config to the notification manager
133
+ yield notificationManager.sendReport(resultMetadata, affectedFiles, resultMetadata.XFI_RESULT.repoXFIConfig);
134
+ }
106
135
  // if results are found, there were issues found in the codebase
107
136
  if (resultMetadata.XFI_RESULT.totalIssues > 0) {
108
137
  logger_1.logger.warn(`WARNING: lo-fi attributes detected in codebase. ${resultMetadata.XFI_RESULT.warningCount} are warnings, ${resultMetadata.XFI_RESULT.fatalityCount} are fatal.`);
@@ -142,6 +171,18 @@ function main() {
142
171
  }
143
172
  });
144
173
  }
174
+ // Helper function to extract affected files from results
175
+ function getAffectedFiles(resultMetadata) {
176
+ const affectedFiles = [];
177
+ if (resultMetadata.XFI_RESULT.issueDetails) {
178
+ for (const issue of resultMetadata.XFI_RESULT.issueDetails) {
179
+ if (issue.filePath && !affectedFiles.includes(issue.filePath)) {
180
+ affectedFiles.push(issue.filePath);
181
+ }
182
+ }
183
+ }
184
+ return affectedFiles;
185
+ }
145
186
  var configManager_1 = require("./core/configManager");
146
187
  Object.defineProperty(exports, "repoDir", { enumerable: true, get: function () { return configManager_1.repoDir; } });
147
188
  __exportStar(require("./utils/logger"), exports);
@@ -0,0 +1,3 @@
1
+ import { NotificationManager } from './notificationManager';
2
+ import { NotificationConfig } from '../types/notificationTypes';
3
+ export declare function initializeNotifications(config: NotificationConfig): Promise<NotificationManager>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.initializeNotifications = initializeNotifications;
13
+ const notificationManager_1 = require("./notificationManager");
14
+ const emailProvider_1 = require("./providers/emailProvider");
15
+ const slackProvider_1 = require("./providers/slackProvider");
16
+ const teamsProvider_1 = require("./providers/teamsProvider");
17
+ const logger_1 = require("../utils/logger");
18
+ function initializeNotifications(config) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ if (!config.enabled) {
21
+ logger_1.logger.info('Notifications are disabled');
22
+ return notificationManager_1.NotificationManager.getInstance(config);
23
+ }
24
+ const notificationManager = notificationManager_1.NotificationManager.getInstance(config);
25
+ // Register configured providers
26
+ for (const providerName of config.providers) {
27
+ switch (providerName) {
28
+ case 'email':
29
+ const emailConfig = loadEmailConfig();
30
+ if (emailConfig) {
31
+ notificationManager.registerProvider(new emailProvider_1.EmailProvider(emailConfig));
32
+ }
33
+ break;
34
+ case 'slack':
35
+ const slackConfig = loadSlackConfig();
36
+ if (slackConfig) {
37
+ notificationManager.registerProvider(new slackProvider_1.SlackProvider(slackConfig));
38
+ }
39
+ break;
40
+ case 'teams':
41
+ const teamsConfig = loadTeamsConfig();
42
+ if (teamsConfig) {
43
+ notificationManager.registerProvider(new teamsProvider_1.TeamsProvider(teamsConfig));
44
+ }
45
+ break;
46
+ default:
47
+ logger_1.logger.warn(`Unknown notification provider: ${providerName}`);
48
+ }
49
+ }
50
+ return notificationManager;
51
+ });
52
+ }
53
+ function loadEmailConfig() {
54
+ try {
55
+ const config = {
56
+ host: process.env.NOTIFICATION_EMAIL_HOST || '',
57
+ port: parseInt(process.env.NOTIFICATION_EMAIL_PORT || '587'),
58
+ secure: process.env.NOTIFICATION_EMAIL_SECURE === 'true',
59
+ auth: {
60
+ user: process.env.NOTIFICATION_EMAIL_USER || '',
61
+ pass: process.env.NOTIFICATION_EMAIL_PASS || '',
62
+ },
63
+ from: process.env.NOTIFICATION_EMAIL_FROM || 'x-fidelity@noreply.com',
64
+ };
65
+ // Add debug logging
66
+ logger_1.logger.debug({
67
+ emailConfig: Object.assign(Object.assign({}, config), { auth: {
68
+ user: config.auth.user,
69
+ pass: '****' // Mask password
70
+ } })
71
+ }, 'Email configuration loaded');
72
+ // Validate required fields
73
+ if (!config.host || !config.auth.user || !config.auth.pass) {
74
+ logger_1.logger.warn('Missing required email configuration fields', {
75
+ hasHost: !!config.host,
76
+ hasUser: !!config.auth.user,
77
+ hasPass: !!config.auth.pass
78
+ });
79
+ return null;
80
+ }
81
+ return config;
82
+ }
83
+ catch (error) {
84
+ logger_1.logger.error(error, 'Failed to load email configuration');
85
+ return null;
86
+ }
87
+ }
88
+ function loadSlackConfig() {
89
+ try {
90
+ const webhookUrl = process.env.NOTIFICATION_SLACK_WEBHOOK;
91
+ if (!webhookUrl) {
92
+ logger_1.logger.warn('Slack webhook URL not configured');
93
+ return null;
94
+ }
95
+ return {
96
+ webhookUrl,
97
+ channel: process.env.NOTIFICATION_SLACK_CHANNEL,
98
+ };
99
+ }
100
+ catch (error) {
101
+ logger_1.logger.error(error, 'Failed to load Slack configuration');
102
+ return null;
103
+ }
104
+ }
105
+ function loadTeamsConfig() {
106
+ try {
107
+ const webhookUrl = process.env.NOTIFICATION_TEAMS_WEBHOOK;
108
+ if (!webhookUrl) {
109
+ logger_1.logger.warn('Teams webhook URL not configured');
110
+ return null;
111
+ }
112
+ return {
113
+ webhookUrl
114
+ };
115
+ }
116
+ catch (error) {
117
+ logger_1.logger.error(error, 'Failed to load Teams configuration');
118
+ return null;
119
+ }
120
+ }
@@ -0,0 +1,19 @@
1
+ import { ResultMetadata, RepoXFIConfig } from '../types/typeDefs';
2
+ import { NotificationProvider, NotificationConfig } from '../types/notificationTypes';
3
+ export declare class NotificationManager {
4
+ private static instance;
5
+ private providers;
6
+ private config;
7
+ private codeOwners;
8
+ private constructor();
9
+ static getInstance(config: NotificationConfig): NotificationManager;
10
+ registerProvider(provider: NotificationProvider): void;
11
+ sendReport(results: ResultMetadata, affectedFiles: string[], repoXFIConfig?: RepoXFIConfig): Promise<void>;
12
+ private mergeNotificationConfig;
13
+ private getRecipients;
14
+ private loadCodeOwners;
15
+ private getCodeOwnersForFiles;
16
+ private matchesGlob;
17
+ private applyTemplate;
18
+ private generateReportContent;
19
+ }
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.NotificationManager = void 0;
16
+ const logger_1 = require("../utils/logger");
17
+ const fs_1 = __importDefault(require("fs"));
18
+ class NotificationManager {
19
+ constructor(config) {
20
+ this.providers = new Map();
21
+ this.codeOwners = [];
22
+ this.config = config;
23
+ this.loadCodeOwners();
24
+ }
25
+ static getInstance(config) {
26
+ if (!NotificationManager.instance) {
27
+ NotificationManager.instance = new NotificationManager(config);
28
+ }
29
+ else {
30
+ // Update config if it changes
31
+ NotificationManager.instance.config = Object.assign(Object.assign({}, NotificationManager.instance.config), config);
32
+ }
33
+ return NotificationManager.instance;
34
+ }
35
+ registerProvider(provider) {
36
+ this.providers.set(provider.name, provider);
37
+ logger_1.logger.info(`Registered notification provider: ${provider.name}`);
38
+ }
39
+ sendReport(results, affectedFiles, repoXFIConfig) {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ var _a, _b;
42
+ if (!this.config.enabled) {
43
+ logger_1.logger.debug('Notifications are disabled');
44
+ return;
45
+ }
46
+ // Merge global config with repo-specific config
47
+ const notifyConfig = this.mergeNotificationConfig(repoXFIConfig);
48
+ // Determine if we should send notification based on results
49
+ const hasIssues = results.XFI_RESULT.totalIssues > 0;
50
+ if (hasIssues && !notifyConfig.notifyOnFailure) {
51
+ logger_1.logger.debug('Skipping notification for failure as notifyOnFailure is disabled');
52
+ return;
53
+ }
54
+ if (!hasIssues && !notifyConfig.notifyOnSuccess) {
55
+ logger_1.logger.debug('Skipping notification for success as notifyOnSuccess is disabled');
56
+ return;
57
+ }
58
+ // Get recipients from multiple sources
59
+ const recipients = this.getRecipients(affectedFiles, repoXFIConfig, notifyConfig);
60
+ if (Object.values(recipients).every(list => list.length === 0)) {
61
+ logger_1.logger.warn('No recipients found for notification');
62
+ return;
63
+ }
64
+ // Generate report content
65
+ const subject = hasIssues
66
+ ? `[X-Fidelity] Issues found in your codebase (${results.XFI_RESULT.totalIssues})`
67
+ : '[X-Fidelity] Your codebase passed all checks';
68
+ // Use custom template if available
69
+ const templateKey = hasIssues ? 'failure' : 'success';
70
+ const customTemplate = (_b = (_a = repoXFIConfig === null || repoXFIConfig === void 0 ? void 0 : repoXFIConfig.notifications) === null || _a === void 0 ? void 0 : _a.customTemplates) === null || _b === void 0 ? void 0 : _b[templateKey];
71
+ const content = customTemplate
72
+ ? this.applyTemplate(customTemplate, results, affectedFiles)
73
+ : this.generateReportContent(results, affectedFiles);
74
+ // Send through all configured providers
75
+ for (const providerName of this.config.providers) {
76
+ const provider = this.providers.get(providerName);
77
+ if (provider) {
78
+ try {
79
+ // Only send to recipients configured for this provider
80
+ const providerRecipients = recipients[providerName] || [];
81
+ if (providerRecipients.length === 0) {
82
+ logger_1.logger.debug(`No recipients configured for ${providerName}`);
83
+ continue;
84
+ }
85
+ const notification = {
86
+ recipients: providerRecipients,
87
+ subject,
88
+ content,
89
+ metadata: {
90
+ results,
91
+ ciRunUrl: process.env.CI_RUN_URL || process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
92
+ ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
93
+ : undefined
94
+ }
95
+ };
96
+ yield provider.send(notification);
97
+ logger_1.logger.info(`Sent notification via ${providerName} to ${providerRecipients.join(', ')}`);
98
+ }
99
+ catch (error) {
100
+ logger_1.logger.error(error, `Failed to send notification via ${providerName}`);
101
+ }
102
+ }
103
+ else {
104
+ logger_1.logger.warn(`Notification provider not found: ${providerName}`);
105
+ }
106
+ }
107
+ });
108
+ }
109
+ mergeNotificationConfig(repoXFIConfig) {
110
+ if (!(repoXFIConfig === null || repoXFIConfig === void 0 ? void 0 : repoXFIConfig.notifications)) {
111
+ return this.config;
112
+ }
113
+ return Object.assign(Object.assign({}, this.config), { notifyOnSuccess: repoXFIConfig.notifications.notifyOnSuccess !== undefined
114
+ ? repoXFIConfig.notifications.notifyOnSuccess
115
+ : this.config.notifyOnSuccess, notifyOnFailure: repoXFIConfig.notifications.notifyOnFailure !== undefined
116
+ ? repoXFIConfig.notifications.notifyOnFailure
117
+ : this.config.notifyOnFailure,
118
+ // Use codeOwners setting from repo config if specified
119
+ codeOwnersEnabled: repoXFIConfig.notifications.codeOwners !== undefined
120
+ ? repoXFIConfig.notifications.codeOwners
121
+ : this.config.codeOwnersEnabled });
122
+ }
123
+ getRecipients(affectedFiles, repoXFIConfig, notifyConfig) {
124
+ var _a;
125
+ const result = {
126
+ email: [],
127
+ slack: [],
128
+ teams: []
129
+ };
130
+ // Add recipients from repo config
131
+ if ((_a = repoXFIConfig === null || repoXFIConfig === void 0 ? void 0 : repoXFIConfig.notifications) === null || _a === void 0 ? void 0 : _a.recipients) {
132
+ const configRecipients = repoXFIConfig.notifications.recipients;
133
+ if (configRecipients.email) {
134
+ result.email.push(...configRecipients.email);
135
+ }
136
+ if (configRecipients.slack) {
137
+ result.slack.push(...configRecipients.slack);
138
+ }
139
+ if (configRecipients.teams) {
140
+ result.teams.push(...configRecipients.teams);
141
+ }
142
+ }
143
+ // Add code owners if enabled
144
+ if (notifyConfig === null || notifyConfig === void 0 ? void 0 : notifyConfig.codeOwnersEnabled) {
145
+ const codeOwners = this.getCodeOwnersForFiles(affectedFiles);
146
+ // Add code owners to all provider types
147
+ for (const key of Object.keys(result)) {
148
+ result[key].push(...codeOwners);
149
+ }
150
+ }
151
+ // Deduplicate recipients
152
+ for (const key of Object.keys(result)) {
153
+ result[key] = [...new Set(result[key])];
154
+ }
155
+ return result;
156
+ }
157
+ loadCodeOwners() {
158
+ if (!this.config.codeOwnersPath) {
159
+ logger_1.logger.warn('No CODEOWNERS file path specified');
160
+ return;
161
+ }
162
+ try {
163
+ const codeOwnersContent = fs_1.default.readFileSync(this.config.codeOwnersPath, 'utf8');
164
+ const lines = codeOwnersContent.split('\n');
165
+ for (const line of lines) {
166
+ // Skip comments and empty lines
167
+ if (line.trim().startsWith('#') || line.trim() === '')
168
+ continue;
169
+ const parts = line.trim().split(/\s+/);
170
+ if (parts.length >= 2) {
171
+ const path = parts[0];
172
+ const owners = parts.slice(1).map(owner => {
173
+ // Remove @ from GitHub usernames if present
174
+ return owner.startsWith('@') ? owner.substring(1) : owner;
175
+ });
176
+ this.codeOwners.push({ path, owners });
177
+ }
178
+ }
179
+ logger_1.logger.info(`Loaded ${this.codeOwners.length} code owner entries`);
180
+ }
181
+ catch (error) {
182
+ logger_1.logger.error(error, `Failed to load CODEOWNERS file from ${this.config.codeOwnersPath}`);
183
+ }
184
+ }
185
+ getCodeOwnersForFiles(files) {
186
+ const owners = new Set();
187
+ for (const file of files) {
188
+ for (const codeOwner of this.codeOwners) {
189
+ // Simple glob matching (can be enhanced with proper glob matching)
190
+ if (this.matchesGlob(file, codeOwner.path)) {
191
+ codeOwner.owners.forEach(owner => owners.add(owner));
192
+ }
193
+ }
194
+ }
195
+ return Array.from(owners);
196
+ }
197
+ matchesGlob(filePath, pattern) {
198
+ // Simple implementation - can be replaced with a proper glob matching library
199
+ if (pattern === '*')
200
+ return true;
201
+ if (pattern.endsWith('/*')) {
202
+ const dir = pattern.slice(0, -2);
203
+ return filePath.startsWith(dir + '/');
204
+ }
205
+ if (pattern.endsWith('/**')) {
206
+ const dir = pattern.slice(0, -3);
207
+ return filePath.startsWith(dir + '/');
208
+ }
209
+ return filePath === pattern;
210
+ }
211
+ applyTemplate(template, results, affectedFiles) {
212
+ // Replace template variables with actual values
213
+ return template
214
+ .replace(/\${archetype}/g, results.XFI_RESULT.archetype)
215
+ .replace(/\${fileCount}/g, String(results.XFI_RESULT.fileCount))
216
+ .replace(/\${totalIssues}/g, String(results.XFI_RESULT.totalIssues))
217
+ .replace(/\${warningCount}/g, String(results.XFI_RESULT.warningCount))
218
+ .replace(/\${errorCount}/g, String(results.XFI_RESULT.errorCount))
219
+ .replace(/\${fatalityCount}/g, String(results.XFI_RESULT.fatalityCount))
220
+ .replace(/\${exemptCount}/g, String(results.XFI_RESULT.exemptCount))
221
+ .replace(/\${affectedFiles}/g, affectedFiles.map(file => `- ${file}`).join('\n'))
222
+ .replace(/\${date}/g, new Date().toISOString())
223
+ .replace(/\${executionTime}/g, String(results.XFI_RESULT.durationSeconds));
224
+ }
225
+ generateReportContent(results, affectedFiles) {
226
+ // Basic template - can be enhanced or made configurable
227
+ return `
228
+ # X-Fidelity Analysis Report
229
+
230
+ ## Summary
231
+ - Archetype: ${results.XFI_RESULT.archetype}
232
+ - Total files analyzed: ${results.XFI_RESULT.fileCount}
233
+ - Issues found: ${results.XFI_RESULT.totalIssues}
234
+ - Warnings: ${results.XFI_RESULT.warningCount}
235
+ - Errors: ${results.XFI_RESULT.errorCount}
236
+ - Fatalities: ${results.XFI_RESULT.fatalityCount}
237
+ - Exemptions: ${results.XFI_RESULT.exemptCount}
238
+
239
+ ## Affected Files
240
+ ${affectedFiles.map(file => `- ${file}`).join('\n')}
241
+
242
+ ## Execution Time
243
+ ${results.XFI_RESULT.durationSeconds} seconds
244
+
245
+ For detailed information, please check the CI logs.
246
+ `;
247
+ }
248
+ }
249
+ exports.NotificationManager = NotificationManager;