testbeats 2.0.4 → 2.0.6
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/README.md +2 -0
- package/package.json +6 -2
- package/src/beats/beats.js +57 -33
- package/src/beats/beats.types.d.ts +19 -0
- package/src/cli.js +3 -0
- package/src/commands/publish.command.js +24 -7
- package/src/extensions/ai-failure-summary.extension.js +43 -0
- package/src/extensions/base.extension.js +91 -0
- package/src/extensions/ci-info.extension.js +81 -0
- package/src/extensions/extensions.d.ts +12 -0
- package/src/extensions/index.js +9 -6
- package/src/extensions/mentions.js +1 -1
- package/src/extensions/smart-analysis.extension.js +62 -0
- package/src/helpers/ci.js +3 -0
- package/src/helpers/constants.js +5 -1
- package/src/helpers/helper.js +3 -3
- package/src/helpers/metadata.helper.js +15 -1
- package/src/index.d.ts +7 -0
- package/src/extensions/ai-failure-summary.js +0 -72
- package/src/extensions/ci-info.js +0 -134
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|

|
|
14
14
|

|
|
15
15
|

|
|
16
|
+

|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
[](https://github.com/test-results-reporter/testbeats/stargazers)
|
|
18
20
|

|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testbeats",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -64,5 +64,9 @@
|
|
|
64
64
|
"mocha-multi-reporters": "^1.5.1",
|
|
65
65
|
"pactum": "^3.2.3",
|
|
66
66
|
"pkg": "^5.8.0"
|
|
67
|
-
}
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=14.0.0"
|
|
70
|
+
},
|
|
71
|
+
"engineStrict": true
|
|
68
72
|
}
|
package/src/beats/beats.js
CHANGED
|
@@ -16,6 +16,7 @@ class Beats {
|
|
|
16
16
|
this.result = result;
|
|
17
17
|
this.api = new BeatsApi(config);
|
|
18
18
|
this.test_run_id = '';
|
|
19
|
+
this.test_run = null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async publish() {
|
|
@@ -27,6 +28,7 @@ class Beats {
|
|
|
27
28
|
await this.#uploadAttachments();
|
|
28
29
|
this.#updateTitleLink();
|
|
29
30
|
await this.#attachFailureSummary();
|
|
31
|
+
await this.#attachSmartAnalysis();
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
#setCIInfo() {
|
|
@@ -47,7 +49,7 @@ class Beats {
|
|
|
47
49
|
|
|
48
50
|
async #publishTestResults() {
|
|
49
51
|
if (!this.config.api_key) {
|
|
50
|
-
logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal');
|
|
52
|
+
logger.warn('😿 No API key provided, skipping publishing results to TestBeats Portal...');
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
logger.info("🚀 Publishing results to TestBeats Portal...");
|
|
@@ -125,55 +127,77 @@ class Beats {
|
|
|
125
127
|
if (this.config.show_failure_summary === false) {
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
try {
|
|
131
|
+
logger.info('✨ Fetching AI Failure Summary...');
|
|
132
|
+
await this.#setTestRun(' AI Failure Summary', 'failure_summary_status');
|
|
133
|
+
this.config.extensions.push({
|
|
134
|
+
name: 'ai-failure-summary',
|
|
135
|
+
hook: HOOK.AFTER_SUMMARY,
|
|
136
|
+
inputs: {
|
|
137
|
+
data: this.test_run
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.error(`❌ Unable to attach failure summary: ${error.message}`, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async #attachSmartAnalysis() {
|
|
146
|
+
if (!this.test_run_id) {
|
|
130
147
|
return;
|
|
131
148
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
if (!this.config.targets) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.config.show_smart_analysis === false) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
logger.info('🤓 Fetching Smart Analysis...');
|
|
157
|
+
await this.#setTestRun('Smart Analysis', 'smart_analysis_status');
|
|
158
|
+
this.config.extensions.push({
|
|
159
|
+
name: 'smart-analysis',
|
|
160
|
+
hook: HOOK.AFTER_SUMMARY,
|
|
161
|
+
inputs: {
|
|
162
|
+
data: this.test_run
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error(`❌ Unable to attach smart analysis: ${error.message}`, error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#getDelay() {
|
|
171
|
+
if (process.env.TEST_BEATS_DELAY) {
|
|
172
|
+
return parseInt(process.env.TEST_BEATS_DELAY);
|
|
136
173
|
}
|
|
174
|
+
return 3000;
|
|
137
175
|
}
|
|
138
176
|
|
|
139
|
-
async #
|
|
140
|
-
|
|
177
|
+
async #setTestRun(text, wait_for = 'smart_analysis_status') {
|
|
178
|
+
if (this.test_run && this.test_run[wait_for] === 'COMPLETED') {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
141
181
|
let retry = 3;
|
|
142
182
|
while (retry >= 0) {
|
|
143
183
|
retry = retry - 1;
|
|
144
184
|
await new Promise(resolve => setTimeout(resolve, this.#getDelay()));
|
|
145
|
-
|
|
146
|
-
const status = test_run && test_run
|
|
185
|
+
this.test_run = await this.api.getTestRun(this.test_run_id);
|
|
186
|
+
const status = this.test_run && this.test_run[wait_for];
|
|
147
187
|
switch (status) {
|
|
148
188
|
case 'COMPLETED':
|
|
149
|
-
|
|
189
|
+
logger.debug(`☑️ ${text} generated successfully`);
|
|
190
|
+
return;
|
|
150
191
|
case 'FAILED':
|
|
151
|
-
logger.error(`❌ Failed to generate
|
|
192
|
+
logger.error(`❌ Failed to generate ${text}`);
|
|
152
193
|
return;
|
|
153
194
|
case 'SKIPPED':
|
|
154
|
-
logger.warn(`❗ Skipped generating
|
|
195
|
+
logger.warn(`❗ Skipped generating ${text}`);
|
|
155
196
|
return;
|
|
156
197
|
}
|
|
157
|
-
logger.info(`🔄
|
|
158
|
-
}
|
|
159
|
-
logger.warn(`🙈 AI Failure Summary not generated in given time`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
#getDelay() {
|
|
163
|
-
if (process.env.TEST_BEATS_DELAY) {
|
|
164
|
-
return parseInt(process.env.TEST_BEATS_DELAY);
|
|
198
|
+
logger.info(`🔄 ${text} not generated, retrying...`);
|
|
165
199
|
}
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
#getAIFailureSummaryExtension(text) {
|
|
170
|
-
return {
|
|
171
|
-
name: 'ai-failure-summary',
|
|
172
|
-
hook: HOOK.AFTER_SUMMARY,
|
|
173
|
-
inputs: {
|
|
174
|
-
failure_summary: text
|
|
175
|
-
}
|
|
176
|
-
};
|
|
200
|
+
logger.warn(`🙈 ${text} not generated in given time`);
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type IBeatExecutionMetric = {
|
|
2
|
+
id: string
|
|
3
|
+
created_at: string
|
|
4
|
+
updated_at: string
|
|
5
|
+
newly_failed: number
|
|
6
|
+
always_failing: number
|
|
7
|
+
recurring_failures: number
|
|
8
|
+
recovered: number
|
|
9
|
+
added: number
|
|
10
|
+
removed: number
|
|
11
|
+
flaky: number
|
|
12
|
+
failure_summary: any
|
|
13
|
+
failure_summary_provider: any
|
|
14
|
+
failure_summary_model: any
|
|
15
|
+
status: string
|
|
16
|
+
status_message: any
|
|
17
|
+
test_run_id: string
|
|
18
|
+
org_id: string
|
|
19
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,9 @@ prog
|
|
|
11
11
|
.version('2.0.4')
|
|
12
12
|
.option('-c, --config', 'path to config file')
|
|
13
13
|
.option('-l, --logLevel', 'Log Level', "INFO")
|
|
14
|
+
.option('--api-key', 'api key')
|
|
15
|
+
.option('--project', 'project name')
|
|
16
|
+
.option('--run', 'run name')
|
|
14
17
|
.option('--slack', 'slack webhook url')
|
|
15
18
|
.option('--teams', 'teams webhook url')
|
|
16
19
|
.option('--chat', 'chat webhook url')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const trp = require('test-results-parser');
|
|
3
3
|
const prp = require('performance-results-parser');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
|
|
5
6
|
const beats = require('../beats');
|
|
6
7
|
const { ConfigBuilder } = require('../utils/config.builder');
|
|
@@ -8,6 +9,7 @@ const target_manager = require('../targets');
|
|
|
8
9
|
const logger = require('../utils/logger');
|
|
9
10
|
const { processData } = require('../helpers/helper');
|
|
10
11
|
const pkg = require('../../package.json');
|
|
12
|
+
const { MIN_NODE_VERSION } = require('../helpers/constants');
|
|
11
13
|
|
|
12
14
|
class PublishCommand {
|
|
13
15
|
|
|
@@ -20,6 +22,8 @@ class PublishCommand {
|
|
|
20
22
|
|
|
21
23
|
async publish() {
|
|
22
24
|
logger.info(`🥁 TestBeats v${pkg.version}`);
|
|
25
|
+
|
|
26
|
+
this.#validateEnvDetails();
|
|
23
27
|
this.#buildConfig();
|
|
24
28
|
this.#validateOptions();
|
|
25
29
|
this.#setConfigFromFile();
|
|
@@ -30,6 +34,20 @@ class PublishCommand {
|
|
|
30
34
|
logger.info('✅ Results published successfully!');
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
#validateEnvDetails() {
|
|
38
|
+
try {
|
|
39
|
+
const current_major_version = parseInt(process.version.split('.')[0].replace('v', ''));
|
|
40
|
+
if (current_major_version >= MIN_NODE_VERSION) {
|
|
41
|
+
logger.info(`💻 NodeJS: ${process.version}, OS: ${os.platform()}, Version: ${os.release()}, Arch: ${os.machine()}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.warn(`⚠️ Unable to verify NodeJS version: ${error.message}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`❌ Supported NodeJS version is >= v${MIN_NODE_VERSION}. Current version is ${process.version}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
#buildConfig() {
|
|
34
52
|
const config_builder = new ConfigBuilder(this.opts);
|
|
35
53
|
config_builder.build();
|
|
@@ -52,8 +70,7 @@ class PublishCommand {
|
|
|
52
70
|
const config_json = require(path.join(cwd, this.opts.config));
|
|
53
71
|
this.opts.config = config_json;
|
|
54
72
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
throw new Error(`Config file not found: ${file_path}`);
|
|
73
|
+
throw new Error(`Failed to read config file: '${file_path}' with error: '${error.message}'`);
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
}
|
|
@@ -63,7 +80,7 @@ class PublishCommand {
|
|
|
63
80
|
/**@type {import('../index').PublishConfig[]} */
|
|
64
81
|
this.configs = [];
|
|
65
82
|
if (processed_config.reports) {
|
|
66
|
-
for (const report of
|
|
83
|
+
for (const report of processed_config.reports) {
|
|
67
84
|
this.configs.push(report);
|
|
68
85
|
}
|
|
69
86
|
} else {
|
|
@@ -72,7 +89,7 @@ class PublishCommand {
|
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
#validateConfig() {
|
|
75
|
-
logger.info("
|
|
92
|
+
logger.info("🚓 Validating configuration...")
|
|
76
93
|
for (const config of this.configs) {
|
|
77
94
|
this.#validateResults(config);
|
|
78
95
|
this.#validateTargets(config);
|
|
@@ -177,12 +194,12 @@ class PublishCommand {
|
|
|
177
194
|
for (const config of this.configs) {
|
|
178
195
|
for (let i = 0; i < this.results.length; i++) {
|
|
179
196
|
const result = this.results[i];
|
|
180
|
-
|
|
197
|
+
config.extensions = config.extensions || [];
|
|
181
198
|
await beats.run(config, result);
|
|
182
199
|
if (config.targets) {
|
|
183
200
|
for (const target of config.targets) {
|
|
184
201
|
target.extensions = target.extensions || [];
|
|
185
|
-
target.extensions =
|
|
202
|
+
target.extensions = config.extensions.concat(target.extensions);
|
|
186
203
|
await target_manager.run(target, result);
|
|
187
204
|
}
|
|
188
205
|
} else {
|
|
@@ -194,4 +211,4 @@ class PublishCommand {
|
|
|
194
211
|
|
|
195
212
|
}
|
|
196
213
|
|
|
197
|
-
module.exports = { PublishCommand }
|
|
214
|
+
module.exports = { PublishCommand }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { BaseExtension } = require('./base.extension');
|
|
2
|
+
const { STATUS, HOOK } = require("../helpers/constants");
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AIFailureSummaryExtension extends BaseExtension {
|
|
6
|
+
|
|
7
|
+
constructor(target, extension, result, payload, root_payload) {
|
|
8
|
+
super(target, extension, result, payload, root_payload);
|
|
9
|
+
this.#setDefaultOptions();
|
|
10
|
+
this.#setDefaultInputs();
|
|
11
|
+
this.updateExtensionInputs();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
run() {
|
|
15
|
+
this.#setText();
|
|
16
|
+
this.attach();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#setDefaultOptions() {
|
|
20
|
+
this.default_options.hook = HOOK.AFTER_SUMMARY,
|
|
21
|
+
this.default_options.condition = STATUS.PASS_OR_FAIL;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#setDefaultInputs() {
|
|
25
|
+
this.default_inputs.title = 'AI Failure Summary ✨';
|
|
26
|
+
this.default_inputs.title_link = '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#setText() {
|
|
30
|
+
const data = this.extension.inputs.data;
|
|
31
|
+
if (!data) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @type {import('../beats/beats.types').IBeatExecutionMetric}
|
|
37
|
+
*/
|
|
38
|
+
const execution_metrics = data.execution_metrics[0];
|
|
39
|
+
this.text = execution_metrics.failure_summary;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { AIFailureSummaryExtension }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
|
|
3
|
+
|
|
4
|
+
class BaseExtension {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {import('..').Target} target
|
|
9
|
+
* @param {import('..').Extension} extension
|
|
10
|
+
* @param {import('..').TestResult} result
|
|
11
|
+
* @param {any} payload
|
|
12
|
+
* @param {any} root_payload
|
|
13
|
+
*/
|
|
14
|
+
constructor(target, extension, result, payload, root_payload) {
|
|
15
|
+
this.target = target;
|
|
16
|
+
this.extension = extension;
|
|
17
|
+
this.result = result;
|
|
18
|
+
this.payload = payload;
|
|
19
|
+
this.root_payload = root_payload;
|
|
20
|
+
|
|
21
|
+
this.text = '';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @type {import('..').ExtensionInputs}
|
|
25
|
+
*/
|
|
26
|
+
this.default_inputs = {};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @type {import('..').IExtensionDefaultOptions}
|
|
30
|
+
*/
|
|
31
|
+
this.default_options = {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
updateExtensionInputs() {
|
|
35
|
+
this.extension.inputs = Object.assign({}, this.default_inputs, this.extension.inputs);
|
|
36
|
+
switch (this.target.name) {
|
|
37
|
+
case 'teams':
|
|
38
|
+
this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
|
|
39
|
+
break;
|
|
40
|
+
case 'slack':
|
|
41
|
+
this.extension.inputs = Object.assign({}, { separator: false }, this.extension.inputs);
|
|
42
|
+
break;
|
|
43
|
+
case 'chat':
|
|
44
|
+
this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
attach() {
|
|
52
|
+
if (!this.text) {
|
|
53
|
+
logger.debug(`⚠️ Extension '${this.extension.name}' has no text. Skipping.`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (this.target.name) {
|
|
58
|
+
case 'teams':
|
|
59
|
+
addTeamsExtension({ payload: this.payload, extension: this.extension, text: this.text });
|
|
60
|
+
break;
|
|
61
|
+
case 'slack':
|
|
62
|
+
addSlackExtension({ payload: this.payload, extension: this.extension, text: this.text });
|
|
63
|
+
break;
|
|
64
|
+
case 'chat':
|
|
65
|
+
addChatExtension({ payload: this.payload, extension: this.extension, text: this.text });
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string[]} texts
|
|
74
|
+
*/
|
|
75
|
+
mergeTexts(texts) {
|
|
76
|
+
const _texts = texts.filter(text => !!text);
|
|
77
|
+
switch (this.target.name) {
|
|
78
|
+
case 'teams':
|
|
79
|
+
return _texts.join('\n\n');
|
|
80
|
+
case 'slack':
|
|
81
|
+
return _texts.join('\n');
|
|
82
|
+
case 'chat':
|
|
83
|
+
return _texts.join('<br>');
|
|
84
|
+
default:
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { BaseExtension }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { BaseExtension } = require("./base.extension");
|
|
2
|
+
const { getCIInformation } = require('../helpers/ci');
|
|
3
|
+
const { getMetaDataText } = require("../helpers/metadata.helper");
|
|
4
|
+
const { STATUS, HOOK } = require("../helpers/constants");
|
|
5
|
+
|
|
6
|
+
class CIInfoExtension extends BaseExtension {
|
|
7
|
+
|
|
8
|
+
constructor(target, extension, result, payload, root_payload) {
|
|
9
|
+
super(target, extension, result, payload, root_payload);
|
|
10
|
+
this.#setDefaultOptions();
|
|
11
|
+
this.#setDefaultInputs();
|
|
12
|
+
this.updateExtensionInputs();
|
|
13
|
+
|
|
14
|
+
this.ci = null;
|
|
15
|
+
this.repository_elements = [];
|
|
16
|
+
this.build_elements = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#setDefaultOptions() {
|
|
20
|
+
this.default_options.hook = HOOK.AFTER_SUMMARY,
|
|
21
|
+
this.default_options.condition = STATUS.PASS_OR_FAIL;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#setDefaultInputs() {
|
|
25
|
+
this.default_inputs.title = '';
|
|
26
|
+
this.default_inputs.title_link = '';
|
|
27
|
+
this.default_inputs.show_repository = true;
|
|
28
|
+
this.default_inputs.show_repository_branch = true;
|
|
29
|
+
this.default_inputs.show_build = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run() {
|
|
33
|
+
this.ci = getCIInformation();
|
|
34
|
+
|
|
35
|
+
this.setRepositoryElements();
|
|
36
|
+
this.setBuildElements();
|
|
37
|
+
|
|
38
|
+
const repository_text = await getMetaDataText({ elements: this.repository_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
|
|
39
|
+
const build_text = await getMetaDataText({ elements: this.build_elements, target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
|
|
40
|
+
this.text = this.mergeTexts([repository_text, build_text]);
|
|
41
|
+
this.attach();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setRepositoryElements() {
|
|
45
|
+
if (!this.ci) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.extension.inputs.show_repository && this.ci.repository_url && this.ci.repository_name) {
|
|
50
|
+
this.repository_elements.push({ label: 'Repository', key: this.ci.repository_name, value: this.ci.repository_url, type: 'hyperlink' });
|
|
51
|
+
}
|
|
52
|
+
if (this.extension.inputs.show_repository_branch && this.ci.repository_ref) {
|
|
53
|
+
if (this.ci.repository_ref.includes('refs/pull')) {
|
|
54
|
+
const pr_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/pull/', '/pull/');
|
|
55
|
+
const pr_name = this.ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
|
|
56
|
+
this.repository_elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
|
|
57
|
+
} else {
|
|
58
|
+
const branch_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/heads/', '/tree/');
|
|
59
|
+
const branch_name = this.ci.repository_ref.replace('refs/heads/', '');
|
|
60
|
+
this.repository_elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setBuildElements() {
|
|
66
|
+
if (!this.ci) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.extension.inputs.show_build && this.ci.build_url) {
|
|
71
|
+
const name = (this.ci.build_name || 'Build') + (this.ci.build_number ? ` #${this.ci.build_number}` : '');
|
|
72
|
+
this.build_elements.push({ label: 'Build', key: name, value: this.ci.build_url, type: 'hyperlink' });
|
|
73
|
+
}
|
|
74
|
+
if (this.extension.inputs.data) {
|
|
75
|
+
this.build_elements = this.build_elements.concat(this.extension.inputs.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { CIInfoExtension };
|
package/src/extensions/index.js
CHANGED
|
@@ -6,8 +6,9 @@ const qc_test_summary = require('./quick-chart-test-summary');
|
|
|
6
6
|
const percy_analysis = require('./percy-analysis');
|
|
7
7
|
const custom = require('./custom');
|
|
8
8
|
const metadata = require('./metadata');
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const { AIFailureSummaryExtension } = require('./ai-failure-summary.extension');
|
|
10
|
+
const { SmartAnalysisExtension } = require('./smart-analysis.extension');
|
|
11
|
+
const { CIInfoExtension } = require('./ci-info.extension');
|
|
11
12
|
const { EXTENSION } = require('../helpers/constants');
|
|
12
13
|
const { checkCondition } = require('../helpers/helper');
|
|
13
14
|
const logger = require('../utils/logger');
|
|
@@ -17,7 +18,7 @@ async function run(options) {
|
|
|
17
18
|
const extensions = target.extensions || [];
|
|
18
19
|
for (let i = 0; i < extensions.length; i++) {
|
|
19
20
|
const extension = extensions[i];
|
|
20
|
-
const extension_runner = getExtensionRunner(extension);
|
|
21
|
+
const extension_runner = getExtensionRunner(extension, options);
|
|
21
22
|
const extension_options = Object.assign({}, extension_runner.default_options, extension);
|
|
22
23
|
if (extension_options.hook === hook) {
|
|
23
24
|
if (await checkCondition({ condition: extension_options.condition, result, target, extension })) {
|
|
@@ -35,7 +36,7 @@ async function run(options) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function getExtensionRunner(extension) {
|
|
39
|
+
function getExtensionRunner(extension, options) {
|
|
39
40
|
switch (extension.name) {
|
|
40
41
|
case EXTENSION.HYPERLINKS:
|
|
41
42
|
return hyperlinks;
|
|
@@ -54,9 +55,11 @@ function getExtensionRunner(extension) {
|
|
|
54
55
|
case EXTENSION.METADATA:
|
|
55
56
|
return metadata;
|
|
56
57
|
case EXTENSION.CI_INFO:
|
|
57
|
-
return
|
|
58
|
+
return new CIInfoExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
58
59
|
case EXTENSION.AI_FAILURE_SUMMARY:
|
|
59
|
-
return
|
|
60
|
+
return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
61
|
+
case EXTENSION.SMART_ANALYSIS:
|
|
62
|
+
return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
60
63
|
default:
|
|
61
64
|
return require(extension.name);
|
|
62
65
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { BaseExtension } = require('./base.extension');
|
|
2
|
+
const { STATUS, HOOK } = require("../helpers/constants");
|
|
3
|
+
|
|
4
|
+
class SmartAnalysisExtension extends BaseExtension {
|
|
5
|
+
|
|
6
|
+
constructor(target, extension, result, payload, root_payload) {
|
|
7
|
+
super(target, extension, result, payload, root_payload);
|
|
8
|
+
this.#setDefaultOptions();
|
|
9
|
+
this.#setDefaultInputs();
|
|
10
|
+
this.updateExtensionInputs();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
run() {
|
|
14
|
+
this.#setText();
|
|
15
|
+
this.attach();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#setDefaultOptions() {
|
|
19
|
+
this.default_options.hook = HOOK.AFTER_SUMMARY,
|
|
20
|
+
this.default_options.condition = STATUS.PASS_OR_FAIL;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#setDefaultInputs() {
|
|
24
|
+
this.default_inputs.title = 'Smart Analysis';
|
|
25
|
+
this.default_inputs.title_link = '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#setText() {
|
|
29
|
+
const data = this.extension.inputs.data;
|
|
30
|
+
|
|
31
|
+
if (!data) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @type {import('../beats/beats.types').IBeatExecutionMetric}
|
|
37
|
+
*/
|
|
38
|
+
const execution_metrics = data.execution_metrics[0];
|
|
39
|
+
|
|
40
|
+
const smart_analysis = [];
|
|
41
|
+
if (execution_metrics.newly_failed) {
|
|
42
|
+
smart_analysis.push(`⭕ NF: ${execution_metrics.newly_failed}`);
|
|
43
|
+
}
|
|
44
|
+
if (execution_metrics.always_failing) {
|
|
45
|
+
smart_analysis.push(`🔴 AF: ${execution_metrics.always_failing}`);
|
|
46
|
+
}
|
|
47
|
+
if (execution_metrics.recurring_failures) {
|
|
48
|
+
smart_analysis.push(`🟠 RF: ${execution_metrics.recurring_failures}`);
|
|
49
|
+
}
|
|
50
|
+
if (execution_metrics.flaky) {
|
|
51
|
+
smart_analysis.push(`🟡 FL: ${execution_metrics.flaky}`);
|
|
52
|
+
}
|
|
53
|
+
if (execution_metrics.recovered) {
|
|
54
|
+
smart_analysis.push(`🟢 RC: ${execution_metrics.recovered}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.text = smart_analysis.join(' | ');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { SmartAnalysisExtension };
|
package/src/helpers/ci.js
CHANGED
package/src/helpers/constants.js
CHANGED
|
@@ -21,6 +21,7 @@ const TARGET = Object.freeze({
|
|
|
21
21
|
|
|
22
22
|
const EXTENSION = Object.freeze({
|
|
23
23
|
AI_FAILURE_SUMMARY: 'ai-failure-summary',
|
|
24
|
+
SMART_ANALYSIS: 'smart-analysis',
|
|
24
25
|
HYPERLINKS: 'hyperlinks',
|
|
25
26
|
MENTIONS: 'mentions',
|
|
26
27
|
REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
|
|
@@ -37,10 +38,13 @@ const URLS = Object.freeze({
|
|
|
37
38
|
QUICK_CHART: 'https://quickchart.io'
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
const MIN_NODE_VERSION = 14;
|
|
42
|
+
|
|
40
43
|
module.exports = Object.freeze({
|
|
41
44
|
STATUS,
|
|
42
45
|
HOOK,
|
|
43
46
|
TARGET,
|
|
44
47
|
EXTENSION,
|
|
45
|
-
URLS
|
|
48
|
+
URLS,
|
|
49
|
+
MIN_NODE_VERSION
|
|
46
50
|
});
|
package/src/helpers/helper.js
CHANGED
|
@@ -31,7 +31,7 @@ function processText(raw) {
|
|
|
31
31
|
return raw;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
35
|
* @returns {import('../index').PublishConfig }
|
|
36
36
|
*/
|
|
37
37
|
function processData(data) {
|
|
@@ -72,9 +72,9 @@ function getResultText({ result }) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
*
|
|
76
76
|
* @param {object} param0
|
|
77
|
-
* @param {string | Function} param0.condition
|
|
77
|
+
* @param {string | Function} param0.condition
|
|
78
78
|
*/
|
|
79
79
|
async function checkCondition({ condition, result, target, extension }) {
|
|
80
80
|
if (typeof condition === 'function') {
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
const { checkCondition } = require('./helper')
|
|
1
|
+
const { checkCondition } = require('./helper');
|
|
2
|
+
|
|
3
|
+
function getMetaDataText(params) {
|
|
4
|
+
switch (params.target.name) {
|
|
5
|
+
case 'teams':
|
|
6
|
+
return getTeamsMetaDataText(params);
|
|
7
|
+
case 'slack':
|
|
8
|
+
return getSlackMetaDataText(params);
|
|
9
|
+
case 'chat':
|
|
10
|
+
return getChatMetaDataText(params);
|
|
11
|
+
default:
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
/**
|
|
4
17
|
* Asynchronously generates metadata text for slack.
|
|
@@ -109,6 +122,7 @@ function get_url({ url, target, extension, result}) {
|
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
module.exports = {
|
|
125
|
+
getMetaDataText,
|
|
112
126
|
getSlackMetaDataText,
|
|
113
127
|
getTeamsMetaDataText,
|
|
114
128
|
getChatMetaDataText
|
package/src/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ExtensionInputs {
|
|
|
25
25
|
title?: string;
|
|
26
26
|
title_link?: string;
|
|
27
27
|
separator?: boolean;
|
|
28
|
+
data?: any;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface ReportPortalAnalysisInputs extends ExtensionInputs {
|
|
@@ -227,6 +228,7 @@ export interface PublishReport {
|
|
|
227
228
|
project?: string;
|
|
228
229
|
run?: string;
|
|
229
230
|
show_failure_summary?: boolean;
|
|
231
|
+
show_smart_analysis?: boolean;
|
|
230
232
|
targets?: Target[];
|
|
231
233
|
extensions?: Extension[];
|
|
232
234
|
results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
|
|
@@ -265,5 +267,10 @@ export interface CommandLineOptions {
|
|
|
265
267
|
mstest?: string;
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
export type IExtensionDefaultOptions = {
|
|
271
|
+
hook: Hook
|
|
272
|
+
condition: Condition
|
|
273
|
+
}
|
|
274
|
+
|
|
268
275
|
export function publish(options: PublishOptions): Promise<any>
|
|
269
276
|
export function defineConfig(config: PublishConfig): PublishConfig
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
const { STATUS, HOOK } = require("../helpers/constants");
|
|
2
|
-
const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {object} param0
|
|
6
|
-
* @param {import('..').Target} param0.target
|
|
7
|
-
* @param {import('..').MetadataExtension} param0.extension
|
|
8
|
-
*/
|
|
9
|
-
async function run({ target, extension, result, payload, root_payload }) {
|
|
10
|
-
extension.inputs = Object.assign({}, default_inputs, extension.inputs);
|
|
11
|
-
if (target.name === 'teams') {
|
|
12
|
-
extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs);
|
|
13
|
-
await attachForTeams({ target, extension, payload, result });
|
|
14
|
-
} else if (target.name === 'slack') {
|
|
15
|
-
extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs);
|
|
16
|
-
await attachForSlack({ target, extension, payload, result });
|
|
17
|
-
} else if (target.name === 'chat') {
|
|
18
|
-
extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs);
|
|
19
|
-
await attachForChat({ target, extension, payload, result });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {object} param0
|
|
25
|
-
* @param {import('..').MetadataExtension} param0.extension
|
|
26
|
-
*/
|
|
27
|
-
async function attachForTeams({ target, extension, payload, result }) {
|
|
28
|
-
const text = extension.inputs.failure_summary
|
|
29
|
-
if (text) {
|
|
30
|
-
addTeamsExtension({ payload, extension, text });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function attachForSlack({ target, extension, payload, result }) {
|
|
35
|
-
const text = extension.inputs.failure_summary
|
|
36
|
-
if (text) {
|
|
37
|
-
addSlackExtension({ payload, extension, text });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function attachForChat({ target, extension, payload, result }) {
|
|
42
|
-
const text = extension.inputs.failure_summary
|
|
43
|
-
if (text) {
|
|
44
|
-
addChatExtension({ payload, extension, text });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const default_options = {
|
|
49
|
-
hook: HOOK.AFTER_SUMMARY,
|
|
50
|
-
condition: STATUS.FAIL,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const default_inputs = {
|
|
54
|
-
title: 'AI Failure Summary ✨'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const default_inputs_teams = {
|
|
58
|
-
separator: true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const default_inputs_slack = {
|
|
62
|
-
separator: false
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const default_inputs_chat = {
|
|
66
|
-
separator: true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = {
|
|
70
|
-
run,
|
|
71
|
-
default_options
|
|
72
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
const { STATUS, HOOK } = require("../helpers/constants");
|
|
2
|
-
const { getCIInformation } = require('../helpers/ci');
|
|
3
|
-
const { addTeamsExtension, addSlackExtension, addChatExtension } = require('../helpers/extension.helper');
|
|
4
|
-
const { getTeamsMetaDataText, getSlackMetaDataText, getChatMetaDataText } = require('../helpers/metadata.helper');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {object} param0 - the payload object
|
|
9
|
-
* @param {import('..').Extension} param0.extension - The result object
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
async function run({ target, extension, payload, result }) {
|
|
13
|
-
extension.inputs = Object.assign({}, default_inputs, extension.inputs);
|
|
14
|
-
if (target.name === 'teams') {
|
|
15
|
-
extension.inputs = Object.assign({}, default_inputs_teams, extension.inputs);
|
|
16
|
-
const text = await get_text({ target, extension, result });
|
|
17
|
-
if (text) {
|
|
18
|
-
addTeamsExtension({ payload, extension, text });
|
|
19
|
-
}
|
|
20
|
-
} else if (target.name === 'slack') {
|
|
21
|
-
extension.inputs = Object.assign({}, default_inputs_slack, extension.inputs);
|
|
22
|
-
const text = await get_text({ target, extension, result });
|
|
23
|
-
if (text) {
|
|
24
|
-
addSlackExtension({ payload, extension, text });
|
|
25
|
-
}
|
|
26
|
-
} else if (target.name === 'chat') {
|
|
27
|
-
extension.inputs = Object.assign({}, default_inputs_chat, extension.inputs);
|
|
28
|
-
const text = await get_text({ target, extension, result });
|
|
29
|
-
if (text) {
|
|
30
|
-
addChatExtension({ payload, extension, text });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
* @param {import('..').CIInfoInputs} inputs
|
|
38
|
-
*/
|
|
39
|
-
function get_repository_elements(inputs) {
|
|
40
|
-
const elements = [];
|
|
41
|
-
const ci = getCIInformation();
|
|
42
|
-
if (inputs.show_repository && ci && ci.repository_url && ci.repository_name) {
|
|
43
|
-
elements.push({ label: 'Repository', key: ci.repository_name, value: ci.repository_url, type: 'hyperlink' });
|
|
44
|
-
}
|
|
45
|
-
if (inputs.show_repository_branch && ci && ci.repository_ref) {
|
|
46
|
-
if (ci.repository_ref.includes('refs/pull')) {
|
|
47
|
-
const pr_url = ci.repository_url + ci.repository_ref.replace('refs/pull/', '/pull/');
|
|
48
|
-
const pr_name = ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
|
|
49
|
-
elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
|
|
50
|
-
} else {
|
|
51
|
-
const branch_url = ci.repository_url + ci.repository_ref.replace('refs/heads/', '/tree/');
|
|
52
|
-
const branch_name = ci.repository_ref.replace('refs/heads/', '');
|
|
53
|
-
elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return elements;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {import('..').CIInfoInputs} inputs
|
|
62
|
-
*/
|
|
63
|
-
function get_build_elements(inputs) {
|
|
64
|
-
let elements = [];
|
|
65
|
-
const ci = getCIInformation();
|
|
66
|
-
if (inputs.show_build && ci && ci.build_url) {
|
|
67
|
-
const name = (ci.build_name || 'Build') + (ci.build_number ? ` #${ci.build_number}` : '');
|
|
68
|
-
elements.push({ label: 'Build', key: name, value: ci.build_url, type: 'hyperlink' });
|
|
69
|
-
}
|
|
70
|
-
if (inputs.data) {
|
|
71
|
-
elements = elements.concat(inputs.data);
|
|
72
|
-
}
|
|
73
|
-
return elements;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function get_text({ target, extension, result }) {
|
|
77
|
-
const repository_elements = get_repository_elements(extension.inputs);
|
|
78
|
-
const build_elements = get_build_elements(extension.inputs);
|
|
79
|
-
if (target.name === 'teams') {
|
|
80
|
-
const repository_text = await getTeamsMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
|
|
81
|
-
const build_text = await getTeamsMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
|
|
82
|
-
if (build_text) {
|
|
83
|
-
return `${repository_text ? `${repository_text}\n\n` : '' }${build_text}`;
|
|
84
|
-
} else {
|
|
85
|
-
return repository_text;
|
|
86
|
-
}
|
|
87
|
-
} else if (target.name === 'slack') {
|
|
88
|
-
const repository_text = await getSlackMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
|
|
89
|
-
const build_text = await getSlackMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
|
|
90
|
-
if (build_text) {
|
|
91
|
-
return `${repository_text ? `${repository_text}\n` : '' }${build_text}`;
|
|
92
|
-
} else {
|
|
93
|
-
return repository_text;
|
|
94
|
-
}
|
|
95
|
-
} else if (target.name === 'chat') {
|
|
96
|
-
const repository_text = await getChatMetaDataText({ elements: repository_elements, target, extension, result, default_condition: default_options.condition });
|
|
97
|
-
const build_text = await getChatMetaDataText({ elements: build_elements, target, extension, result, default_condition: default_options.condition });
|
|
98
|
-
if (build_text) {
|
|
99
|
-
return `${repository_text ? `${repository_text}<br>` : '' }${build_text}`;
|
|
100
|
-
} else {
|
|
101
|
-
return repository_text;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const default_options = {
|
|
107
|
-
hook: HOOK.AFTER_SUMMARY,
|
|
108
|
-
condition: STATUS.PASS_OR_FAIL,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const default_inputs = {
|
|
112
|
-
title: '',
|
|
113
|
-
show_repository: true,
|
|
114
|
-
show_repository_branch: true,
|
|
115
|
-
show_build: true,
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const default_inputs_teams = {
|
|
119
|
-
|
|
120
|
-
separator: true
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const default_inputs_slack = {
|
|
124
|
-
separator: false
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const default_inputs_chat = {
|
|
128
|
-
separator: true
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
module.exports = {
|
|
132
|
-
run,
|
|
133
|
-
default_options
|
|
134
|
-
}
|