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.
- package/.github/CODEOWNERS +15 -0
- package/.xfi-config.json +23 -1
- package/CHANGELOG.md +28 -0
- package/canary-install-example.sh +44 -0
- package/dist/core/cli.js +2 -1
- package/dist/core/engine/analyzer.js +3 -1
- package/dist/demoConfig/node-fullstack.json +15 -1
- package/dist/facts/repoFilesystemFacts.js +7 -7
- package/dist/index.js +41 -0
- package/dist/notifications/index.d.ts +3 -0
- package/dist/notifications/index.js +120 -0
- package/dist/notifications/notificationManager.d.ts +19 -0
- package/dist/notifications/notificationManager.js +249 -0
- package/dist/notifications/notifications.test.d.ts +1 -0
- package/dist/notifications/notifications.test.js +100 -0
- package/dist/notifications/providers/emailProvider.d.ts +17 -0
- package/dist/notifications/providers/emailProvider.js +86 -0
- package/dist/notifications/providers/slackProvider.d.ts +11 -0
- package/dist/notifications/providers/slackProvider.js +67 -0
- package/dist/notifications/providers/teamsProvider.d.ts +10 -0
- package/dist/notifications/providers/teamsProvider.js +95 -0
- package/dist/types/notificationTypes.d.ts +23 -0
- package/dist/types/notificationTypes.js +2 -0
- package/dist/types/typeDefs.d.ts +15 -0
- package/dist/utils/jsonSchemas.js +53 -0
- package/dist/xfidelity +41 -0
- package/flagCheck.js +26 -0
- package/package.json +6 -3
- package/src/core/cli.ts +2 -1
- package/src/core/engine/analyzer.ts +3 -1
- package/src/demoConfig/node-fullstack.json +15 -1
- package/src/facts/repoFilesystemFacts.ts +7 -7
- package/src/index.ts +55 -1
- package/src/notifications/index.ts +119 -0
- package/src/notifications/notificationManager.ts +272 -0
- package/src/notifications/notifications.test.ts +116 -0
- package/src/notifications/providers/emailProvider.ts +90 -0
- package/src/notifications/providers/slackProvider.ts +63 -0
- package/src/notifications/providers/teamsProvider.ts +89 -0
- package/src/types/notificationTypes.ts +26 -0
- package/src/types/typeDefs.ts +15 -0
- 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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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,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;
|