testbeats 2.1.6 → 2.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.1.6",
3
+ "version": "2.2.1",
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",
@@ -53,6 +53,7 @@
53
53
  "performance-results-parser": "latest",
54
54
  "phin-retry": "^1.0.3",
55
55
  "pretty-ms": "^7.0.1",
56
+ "prompts": "^2.4.2",
56
57
  "rosters": "0.0.1",
57
58
  "sade": "^1.8.1",
58
59
  "test-results-parser": "0.2.5"
@@ -139,7 +139,6 @@ class BeatsAttachments {
139
139
  */
140
140
  #compressAttachment(attachment_path) {
141
141
  return new Promise((resolve, _) => {
142
- console.log(attachment_path)
143
142
  if (attachment_path.endsWith('.br') || attachment_path.endsWith('.gz') || attachment_path.endsWith('.zst') || attachment_path.endsWith('.zip') || attachment_path.endsWith('.7z') || attachment_path.endsWith('.png') || attachment_path.endsWith('.jpg') || attachment_path.endsWith('.jpeg') || attachment_path.endsWith('.svg') || attachment_path.endsWith('.gif') || attachment_path.endsWith('.webp')) {
144
143
  resolve(attachment_path);
145
144
  return;
package/src/cli.js CHANGED
@@ -5,13 +5,18 @@ const sade = require('sade');
5
5
 
6
6
  const prog = sade('testbeats');
7
7
  const { PublishCommand } = require('./commands/publish.command');
8
+ const { GenerateConfigCommand } = require('./commands/generate-config.command');
8
9
  const logger = require('./utils/logger');
9
10
  const pkg = require('../package.json');
10
11
 
11
12
  prog
12
13
  .version(pkg.version)
13
- .option('-c, --config', 'path to config file')
14
14
  .option('-l, --logLevel', 'Log Level', "INFO")
15
+
16
+
17
+ // Command to publish test results
18
+ prog.command('publish')
19
+ .option('-c, --config', 'path to config file')
15
20
  .option('--api-key', 'api key')
16
21
  .option('--project', 'project name')
17
22
  .option('--run', 'run name')
@@ -27,9 +32,7 @@ prog
27
32
  .option('--xunit', 'xunit xml path')
28
33
  .option('--mstest', 'mstest xml path')
29
34
  .option('-ci-info', 'ci info extension')
30
- .option('-chart-test-summary', 'chart test summary extension');
31
-
32
- prog.command('publish')
35
+ .option('-chart-test-summary', 'chart test summary extension')
33
36
  .action(async (opts) => {
34
37
  try {
35
38
  logger.setLevel(opts.logLevel);
@@ -41,4 +44,22 @@ prog.command('publish')
41
44
  }
42
45
  });
43
46
 
47
+ // Command to initialize and generate TestBeats Configuration file
48
+ prog.command('init')
49
+ .describe('Generate a TestBeats configuration file')
50
+ .example('init')
51
+ .action(async (opts) => {
52
+ try {
53
+ const generate_command = new GenerateConfigCommand(opts);
54
+ await generate_command.execute();
55
+ } catch (error) {
56
+ if (error.name === 'ExitPromptError') {
57
+ logger.info('😿 Configuration generation was canceled by the user.');
58
+ } else {
59
+ throw new Error(`❌ Error in generating configuration file: ${error.message}`)
60
+ }
61
+ process.exit(1);
62
+ }
63
+ });
64
+
44
65
  prog.parse(process.argv);
@@ -0,0 +1,440 @@
1
+ const prompts = require('prompts');
2
+ const fs = require('fs/promises');
3
+ const logger = require('../utils/logger');
4
+ const pkg = require('../../package.json');
5
+
6
+
7
+ class GenerateConfigCommand {
8
+ /**
9
+ * TODO: [BETA / Experimental Mode]
10
+ * Generates initial TestBests configuration file
11
+ */
12
+ constructor(opts) {
13
+ this.opts = opts;
14
+ this.configPath = '.testbeats.json';
15
+ this.config = {};
16
+ }
17
+
18
+ async execute() {
19
+ logger.setLevel(this.opts.logLevel);
20
+ this.#printBanner();
21
+ logger.info(`🚧 Config generation is still in BETA mode, please report any issues at ${pkg.bugs.url}\n`);
22
+
23
+ await this.#buildConfigFilePath();
24
+ await this.#buildTestResultsConfig();
25
+ await this.#buildTargetsConfig();
26
+ await this.#buildGobalExtensionConfig();
27
+ await this.#buildTestBeatsPortalConfig();
28
+ await this.#saveConfigFile();
29
+ }
30
+
31
+ #printBanner() {
32
+ const banner = `
33
+ _____ _ ___ _
34
+ (_ _) ( )_ ( _'\\ ( )_
35
+ | | __ ___ | ,_)| (_) ) __ _ _ | ,_) ___
36
+ | | /'__'\\/',__)| | | _ <' /'__'\\ /'_' )| | /',__)
37
+ | |( ___/\\__, \\| |_ | (_) )( ___/( (_| || |_ \\__, \\
38
+ (_)'\\____)(____/'\\__)(____/''\\____)'\\__,_)'\\__)(____/
39
+
40
+ v${pkg.version}
41
+ Config Generation [BETA]
42
+ `;
43
+ console.log(banner);
44
+ }
45
+
46
+ async #buildConfigFilePath() {
47
+ const { configPath } = await prompts({
48
+ type: 'text',
49
+ name: 'configPath',
50
+ message: 'Enter path for configuration file :',
51
+ initial: '.testbeats.json'
52
+ });
53
+ this.configPath = configPath;
54
+ }
55
+
56
+ async #buildTestResultsConfig() {
57
+ const runnerChoices = [
58
+ { title: 'Mocha', value: 'mocha', selected: true },
59
+ { title: 'JUnit', value: 'junit' },
60
+ { title: 'TestNG', value: 'testng' },
61
+ { title: 'Cucumber', value: 'cucumber' },
62
+ { title: 'NUnit', value: 'nunit' },
63
+ { title: 'xUnit', value: 'xunit' },
64
+ { title: 'MSTest', value: 'mstest' }
65
+ ]
66
+ // Get test results details
67
+ const { testResults } = await prompts([{
68
+ type: 'toggle',
69
+ name: 'includeResults',
70
+ message: 'Do you want to configure test results?',
71
+ initial: true,
72
+ active: 'Yes',
73
+ inactive: 'No'
74
+ },
75
+ {
76
+ type: (prev) => (prev ? "multiselect" : null),
77
+ name: 'testResults',
78
+ message: 'Select test result types to include:',
79
+ choices: runnerChoices,
80
+ min: 1
81
+ }]);
82
+
83
+ if (!testResults) { return };
84
+
85
+ // Handle result paths
86
+ this.config.results = []
87
+ for (const resultType of testResults) {
88
+ const { path } = await prompts({
89
+ type: 'text',
90
+ name: 'path',
91
+ message: `Enter file path for ${resultType} results (.json, .xml etc):`,
92
+ initial: ""
93
+ });
94
+ this.config.results.push({
95
+ files: path,
96
+ type: resultType
97
+ });
98
+ }
99
+ }
100
+
101
+ async #buildTargetsConfig() {
102
+ const targetChoices = [
103
+ { title: 'Slack', value: 'slack' },
104
+ { title: 'Microsoft Teams', value: 'teams' },
105
+ { title: 'Google Chat', value: 'chat' }
106
+ ];
107
+
108
+ const { titleInput, targets } = await prompts([{
109
+ type: 'toggle',
110
+ name: 'includeTargets',
111
+ message: 'Do you want to configure notification targets (slack, teams, chat etc)?',
112
+ initial: true,
113
+ active: 'Yes',
114
+ inactive: 'No'
115
+ },
116
+ {
117
+ type: (prev) => (prev ? "text" : null),
118
+ name: 'titleInput',
119
+ message: 'Enter notification title (optional):',
120
+ initial: 'TestBeats Report'
121
+ },
122
+ {
123
+ type: (prev, values) => (values.includeTargets ? "multiselect" : null),
124
+ name: 'targets',
125
+ message: 'Select notification targets:',
126
+ choices: targetChoices,
127
+ min: 1
128
+ }]);
129
+
130
+ if (!targets) { return }
131
+
132
+ this.config.targets = []
133
+
134
+ // For each target, ask about target-specific extensions
135
+ for (const target of targets) {
136
+ const { webhookEnvVar, selectedExtensions } = await prompts([{
137
+ type: 'text',
138
+ name: 'webhookEnvVar',
139
+ message: `Enter environment variable name for ${target} webhook URL:`,
140
+ initial: `${target.toUpperCase()}_WEBHOOK_URL`
141
+ },
142
+ {
143
+ type: 'toggle',
144
+ name: 'useExtensions',
145
+ message: `Do you want to configure extensions for ${target}?`,
146
+ initial: true,
147
+ active: 'Yes',
148
+ inactive: 'No'
149
+ },
150
+ {
151
+ type: (prev, values) => (values.useExtensions ? 'multiselect' : null),
152
+ name: 'selectedExtensions',
153
+ message: `Select extensions for ${target}:`,
154
+ choices: this.#getExtensionsList(),
155
+ min: 1
156
+ }]);
157
+
158
+ const targetConfig = {
159
+ name: target,
160
+ inputs: {
161
+ title: titleInput,
162
+ url: `{${webhookEnvVar}}`,
163
+ publish: 'test-summary'
164
+ }
165
+ };
166
+
167
+ if (selectedExtensions) {
168
+ targetConfig.extensions = [];
169
+ // Configure extension-specific inputs
170
+ for (const ext of selectedExtensions) {
171
+ const extConfig = await this.#buildExtensionConfig(ext, target);
172
+ targetConfig.extensions.push(extConfig);
173
+ }
174
+ }
175
+ this.config.targets.push(targetConfig);
176
+ }
177
+ }
178
+
179
+ async #buildGobalExtensionConfig() {
180
+ const { globalExtensionsSelected } = await prompts([{
181
+ type: 'toggle',
182
+ name: 'includeGlobalExtensions',
183
+ message: 'Do you want to configure global extensions?',
184
+ initial: false,
185
+ active: 'Yes',
186
+ inactive: 'No'
187
+ },
188
+ {
189
+ type: (prev) => (prev ? 'multiselect' : null),
190
+ name: 'globalExtensionsSelected',
191
+ message: 'Select global extensions to enable:',
192
+ choices: this.#getExtensionsList()
193
+ }]);
194
+
195
+ if (!globalExtensionsSelected) { return };
196
+
197
+ this.config.extensions = [];
198
+
199
+ // Configure extension-specific inputs
200
+ for (const ext of globalExtensionsSelected) {
201
+ const extDetails = await this.#buildExtensionConfig(ext, null);
202
+ this.config.extensions.push(extDetails);
203
+ }
204
+ }
205
+
206
+ async #buildExtHyperlinks() {
207
+ const links = [];
208
+ const { addLink } = await prompts({
209
+ type: 'toggle',
210
+ name: 'addLink',
211
+ message: 'Do you want to add a hyperlink?',
212
+ initial: true,
213
+ active: 'Yes',
214
+ inactive: 'No'
215
+ });
216
+
217
+ while (addLink) {
218
+ const { text, url } = await prompts([
219
+ {
220
+ type: 'text',
221
+ name: 'text',
222
+ message: 'Enter link text:'
223
+ },
224
+ {
225
+ type: 'text',
226
+ name: 'url',
227
+ message: 'Enter link URL:'
228
+ }
229
+ ]);
230
+
231
+ links.push({ text, url });
232
+
233
+ const { addAnother } = await prompts({
234
+ type: 'toggle',
235
+ name: 'addAnother',
236
+ message: 'Add another link?',
237
+ initial: false,
238
+ active: 'Yes',
239
+ inactive: 'No'
240
+ });
241
+ if (!addAnother) break;
242
+ }
243
+ return { links };
244
+ }
245
+
246
+ async #buildExtMentions(targetName) {
247
+ const users = [];
248
+ const { addUser } = await prompts({
249
+ type: 'toggle',
250
+ name: 'addUser',
251
+ message: 'Do you want to add user mentions?',
252
+ initial: true,
253
+ active: 'Yes',
254
+ inactive: 'No'
255
+ });
256
+
257
+ while (addUser) {
258
+ const user = {};
259
+ const { name } = await prompts({
260
+ type: 'text',
261
+ name: 'name',
262
+ message: 'Enter user name:'
263
+ });
264
+ user.name = name;
265
+
266
+ if (targetName === 'teams') {
267
+ const { teams_upn } = await prompts({
268
+ type: 'text',
269
+ name: 'teams_upn',
270
+ message: 'Enter Teams UPN (user principal name):'
271
+ });
272
+ user.teams_upn = teams_upn;
273
+ } else if (targetName === 'slack') {
274
+ const { slack_uid } = await prompts({
275
+ type: 'text',
276
+ name: 'slack_uid',
277
+ message: 'Enter Slack user ID:'
278
+ });
279
+ user.slack_uid = slack_uid;
280
+ } else if (targetName === 'chat') {
281
+ const { chat_uid } = await prompts({
282
+ type: 'text',
283
+ name: 'chat_uid',
284
+ message: 'Enter Google Chat user ID:'
285
+ });
286
+ user.chat_uid = chat_uid;
287
+ }
288
+
289
+ users.push(user);
290
+
291
+ const { addAnother } = await prompts({
292
+ type: 'toggle',
293
+ name: 'addAnother',
294
+ message: 'Add another user?',
295
+ initial: false,
296
+ active: 'Yes',
297
+ inactive: 'No'
298
+ });
299
+ if (!addAnother) break;
300
+ }
301
+ return { users };
302
+ }
303
+
304
+ async #buildExtMetadata() {
305
+ const data = [];
306
+ const { addMetadata } = await prompts({
307
+ type: 'toggle',
308
+ name: 'addMetadata',
309
+ message: 'Do you want to add metadata?',
310
+ initial: true,
311
+ active: 'Yes',
312
+ inactive: 'No'
313
+ });
314
+
315
+ while (addMetadata) {
316
+ const { key, value } = await prompts([
317
+ {
318
+ type: 'text',
319
+ name: 'key',
320
+ message: 'Enter metadata key:'
321
+ },
322
+ {
323
+ type: 'text',
324
+ name: 'value',
325
+ message: 'Enter metadata value:'
326
+ }
327
+ ]);
328
+
329
+ data.push({ key, value });
330
+
331
+ const { addAnother } = await prompts({
332
+ type: 'toggle',
333
+ name: 'addAnother',
334
+ message: 'Add another metadata item?',
335
+ initial: false,
336
+ active: 'Yes',
337
+ inactive: 'No'
338
+ });
339
+ if (!addAnother) break;
340
+ }
341
+ return { data };
342
+ }
343
+
344
+ async #buildExtensionConfig(extension, target) {
345
+ const extConfig = {
346
+ name: extension
347
+ };
348
+
349
+ switch (extension) {
350
+ case 'hyperlinks':
351
+ extConfig.inputs = await this.#buildExtHyperlinks()
352
+ break;
353
+
354
+ case 'mentions':
355
+ extConfig.inputs = await this.#buildExtMentions(target);
356
+ break;
357
+
358
+ case 'metadata':
359
+ extConfig.inputs = await this.#buildExtMetadata();
360
+ break;
361
+
362
+ default:
363
+ // Add default configuration for other extensions
364
+ extConfig.inputs = {
365
+ title: '',
366
+ separator: target === 'slack' ? false : true
367
+ };
368
+ }
369
+ return extConfig;
370
+ }
371
+
372
+ async #buildTestBeatsPortalConfig() {
373
+ // TestBeats configuration
374
+ const { apiKey, project } = await prompts([{
375
+ type: 'toggle',
376
+ name: 'includeTestBeats',
377
+ message: 'Do you want to configure TestBeats API key (optional)?',
378
+ initial: false,
379
+ active: 'Yes',
380
+ inactive: 'No'
381
+ },
382
+ {
383
+ type: (prev) => (prev ? 'text' : null),
384
+ name: 'apiKey',
385
+ message: 'Enter environment variable name for API key (optional):',
386
+ initial: '{TEST_RESULTS_API_KEY}'
387
+ },
388
+ {
389
+ type: (prev, values) => (values.includeTestBeats ? 'text' : null),
390
+ name: 'project',
391
+ message: 'Enter project name (optional):'
392
+ }]);
393
+
394
+ if (apiKey) {
395
+ this.config.api_key = apiKey;
396
+ // Add optional fields only if they have values
397
+ if (project?.trim()) {
398
+ this.config.project = project.trim();
399
+ }
400
+ }
401
+ }
402
+
403
+ #sortConfig() {
404
+ // Sort keys alphabetically
405
+ this.config = Object.keys(this.config).sort()
406
+ .reduce((sortedConfig, key) => {
407
+ sortedConfig[key] = this.config[key];
408
+ return sortedConfig;
409
+ }, {});
410
+ }
411
+
412
+ async #saveConfigFile() {
413
+ // Write config to file
414
+ try {
415
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
416
+ logger.info(`✅ Configuration file successfully generated: ${this.configPath}`);
417
+ } catch (error) {
418
+ throw new Error(`Error: ${error.message}`)
419
+ }
420
+ }
421
+
422
+ #getExtensionsList() {
423
+ // List of Extensions supported
424
+ return [
425
+ { title: 'Quick Chart Test Summary', value: 'quick-chart-test-summary' },
426
+ { title: 'CI Information', value: 'ci-info' },
427
+ { title: 'Hyperlinks', value: 'hyperlinks' },
428
+ { title: 'Mentions', value: 'mentions' },
429
+ { title: 'Report Portal Analysis', value: 'report-portal-analysis' },
430
+ { title: 'Report Portal History', value: 'report-portal-history' },
431
+ { title: 'Percy Analysis', value: 'percy-analysis' },
432
+ { title: 'Metadata', value: 'metadata' },
433
+ { title: 'AI Failure Summary', value: 'ai-failure-summary' },
434
+ { title: 'Smart Analysis', value: 'smart-analysis' },
435
+ { title: 'Error Clusters', value: 'error-clusters' }
436
+ ];
437
+ }
438
+ }
439
+
440
+ module.exports = { GenerateConfigCommand };
@@ -8,6 +8,7 @@ const { ConfigBuilder } = require('../utils/config.builder');
8
8
  const target_manager = require('../targets');
9
9
  const logger = require('../utils/logger');
10
10
  const { processData } = require('../helpers/helper');
11
+ const { ExtensionsSetup } = require('../setups/extensions.setup');
11
12
  const pkg = require('../../package.json');
12
13
  const { MIN_NODE_VERSION } = require('../helpers/constants');
13
14
 
@@ -32,6 +33,7 @@ class PublishCommand {
32
33
  this.#processConfig();
33
34
  this.#validateConfig();
34
35
  this.#processResults();
36
+ await this.#setupExtensions();
35
37
  await this.#publishResults();
36
38
  await this.#publishErrors();
37
39
  }
@@ -209,6 +211,19 @@ class PublishCommand {
209
211
  }
210
212
  }
211
213
 
214
+ async #setupExtensions() {
215
+ logger.info('⚙️ Setting up extensions...');
216
+ try {
217
+ for (const config of this.configs) {
218
+ const extensions = config.extensions || [];
219
+ const setup = new ExtensionsSetup(extensions, this.results ? this.results[0] : null);
220
+ await setup.run();
221
+ }
222
+ } catch (error) {
223
+ logger.error(`❌ Error setting up extensions: ${error.message}`);
224
+ }
225
+ }
226
+
212
227
  async #publishResults() {
213
228
  if (!this.results.length) {
214
229
  logger.warn('⚠️ No results to publish');
@@ -1,3 +1,4 @@
1
+ const TestResult = require('test-results-parser/src/models/TestResult');
1
2
  const logger = require('../utils/logger');
2
3
  const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
4
 
@@ -7,13 +8,15 @@ class BaseExtension {
7
8
  *
8
9
  * @param {import('..').ITarget} target
9
10
  * @param {import('..').IExtension} extension
10
- * @param {import('..').TestResult} result
11
+ * @param {TestResult} result
11
12
  * @param {any} payload
12
13
  * @param {any} root_payload
13
14
  */
14
15
  constructor(target, extension, result, payload, root_payload) {
15
16
  this.target = target;
16
17
  this.extension = extension;
18
+
19
+ /** @type {TestResult} */
17
20
  this.result = result;
18
21
  this.payload = payload;
19
22
  this.root_payload = root_payload;
@@ -0,0 +1,30 @@
1
+ const { HOOK, STATUS } = require("../helpers/constants");
2
+ const { getMetaDataText } = require("../helpers/metadata.helper");
3
+ const { BaseExtension } = require("./base.extension");
4
+
5
+
6
+ class BrowserstackExtension extends BaseExtension {
7
+ constructor(target, extension, result, payload, root_payload) {
8
+ super(target, extension, result, payload, root_payload);
9
+ this.#setDefaultOptions();
10
+ }
11
+
12
+ #setDefaultOptions() {
13
+ this.default_options.hook = HOOK.AFTER_SUMMARY;
14
+ this.default_options.condition = STATUS.PASS_OR_FAIL;
15
+ }
16
+
17
+ async run() {
18
+ this.extension.inputs = Object.assign({}, this.extension.inputs);
19
+ /** @type {import('../index').BrowserstackInputs} */
20
+ const inputs = this.extension.inputs;
21
+ if (inputs.automation_build) {
22
+ const element = { label: 'Browserstack', key: inputs.automation_build.name, value: inputs.automation_build.public_url, type: 'hyperlink' }
23
+ const text = await getMetaDataText({ elements: [element], target: this.target, extension: this.extension, result: this.result, default_condition: this.default_options.condition });
24
+ this.text = text;
25
+ this.attach();
26
+ }
27
+ }
28
+ }
29
+
30
+ module.exports = { BrowserstackExtension };
@@ -56,19 +56,18 @@ class CIInfoExtension extends BaseExtension {
56
56
  this.#setRepositoryElement();
57
57
  }
58
58
  if (this.extension.inputs.show_repository_branch) {
59
- if (this.ci.repository_ref.includes('refs/pull')) {
59
+ if (this.ci.pull_request_name) {
60
60
  this.#setPullRequestElement();
61
61
  } else {
62
62
  this.#setRepositoryBranchElement();
63
63
  }
64
64
  }
65
65
  if (!this.extension.inputs.show_repository && !this.extension.inputs.show_repository_branch && this.extension.inputs.show_repository_non_common) {
66
- if (this.ci.repository_ref.includes('refs/pull')) {
66
+ if (this.ci.pull_request_name) {
67
67
  this.#setRepositoryElement();
68
68
  this.#setPullRequestElement();
69
69
  } else {
70
- const branch_name = this.ci.repository_ref.replace('refs/heads/', '');
71
- if (!COMMON_BRANCH_NAMES.includes(branch_name.toLowerCase())) {
70
+ if (!COMMON_BRANCH_NAMES.includes(this.ci.branch_name.toLowerCase())) {
72
71
  this.#setRepositoryElement();
73
72
  this.#setRepositoryBranchElement();
74
73
  }
@@ -81,15 +80,11 @@ class CIInfoExtension extends BaseExtension {
81
80
  }
82
81
 
83
82
  #setPullRequestElement() {
84
- const pr_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/pull/', '/pull/');
85
- const pr_name = this.ci.repository_ref.replace('refs/pull/', '').replace('/merge', '');
86
- this.repository_elements.push({ label: 'Pull Request', key: pr_name, value: pr_url, type: 'hyperlink' });
83
+ this.repository_elements.push({ label: 'Pull Request', key: this.ci.pull_request_name, value: this.ci.pull_request_url, type: 'hyperlink' });
87
84
  }
88
85
 
89
86
  #setRepositoryBranchElement() {
90
- const branch_url = this.ci.repository_url + this.ci.repository_ref.replace('refs/heads/', '/tree/');
91
- const branch_name = this.ci.repository_ref.replace('refs/heads/', '');
92
- this.repository_elements.push({ label: 'Branch', key: branch_name, value: branch_url, type: 'hyperlink' });
87
+ this.repository_elements.push({ label: 'Branch', key: this.ci.branch_name, value: this.ci.branch_url, type: 'hyperlink' });
93
88
  }
94
89
 
95
90
  #setBuildElements() {
@@ -5,6 +5,10 @@ export type ICIInfo = {
5
5
  repository_name: string
6
6
  repository_ref: string
7
7
  repository_commit_sha: string
8
+ branch_url: string
9
+ branch_name: string
10
+ pull_request_url: string
11
+ pull_request_name: string
8
12
  build_url: string
9
13
  build_number: string
10
14
  build_name: string
@@ -14,6 +14,7 @@ const { checkCondition } = require('../helpers/helper');
14
14
  const logger = require('../utils/logger');
15
15
  const { ErrorClustersExtension } = require('./error-clusters.extension');
16
16
  const { FailureAnalysisExtension } = require('./failure-analysis.extension');
17
+ const { BrowserstackExtension } = require('./browserstack.extension');
17
18
 
18
19
  async function run(options) {
19
20
  const { target, result, hook } = options;
@@ -72,6 +73,8 @@ function getExtensionRunner(extension, options) {
72
73
  return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
73
74
  case EXTENSION.ERROR_CLUSTERS:
74
75
  return new ErrorClustersExtension(options.target, extension, options.result, options.payload, options.root_payload);
76
+ case EXTENSION.BROWSERSTACK:
77
+ return new BrowserstackExtension(options.target, extension, options.result, options.payload, options.root_payload);
75
78
  default:
76
79
  return require(extension.name);
77
80
  }