testbeats 2.2.3 → 2.2.5

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.2.3",
3
+ "version": "2.2.5",
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",
package/src/cli.js CHANGED
@@ -23,6 +23,7 @@ prog.command('publish')
23
23
  .option('--slack', 'slack webhook url')
24
24
  .option('--teams', 'teams webhook url')
25
25
  .option('--chat', 'chat webhook url')
26
+ .option('--github', 'github token')
26
27
  .option('--title', 'title of the test run')
27
28
  .option('--junit', 'junit xml path')
28
29
  .option('--testng', 'testng xml path')
@@ -30,14 +30,14 @@ class GenerateConfigCommand {
30
30
 
31
31
  #printBanner() {
32
32
  const banner = `
33
- _____ _ ___ _
34
- (_ _) ( )_ ( _'\\ ( )_
35
- | | __ ___ | ,_)| (_) ) __ _ _ | ,_) ___
33
+ _____ _ ___ _
34
+ (_ _) ( )_ ( _'\\ ( )_
35
+ | | __ ___ | ,_)| (_) ) __ _ _ | ,_) ___
36
36
  | | /'__'\\/',__)| | | _ <' /'__'\\ /'_' )| | /',__)
37
37
  | |( ___/\\__, \\| |_ | (_) )( ___/( (_| || |_ \\__, \\
38
38
  (_)'\\____)(____/'\\__)(____/''\\____)'\\__,_)'\\__)(____/
39
-
40
- v${pkg.version}
39
+
40
+ v${pkg.version}
41
41
  Config Generation [BETA]
42
42
  `;
43
43
  console.log(banner);
@@ -102,7 +102,7 @@ class GenerateConfigCommand {
102
102
  const targetChoices = [
103
103
  { title: 'Slack', value: 'slack' },
104
104
  { title: 'Microsoft Teams', value: 'teams' },
105
- { title: 'Google Chat', value: 'chat' }
105
+ { title: 'Google Chat', value: 'chat' },
106
106
  ];
107
107
 
108
108
  const { titleInput, targets } = await prompts([{
@@ -437,4 +437,4 @@ class GenerateConfigCommand {
437
437
  }
438
438
  }
439
439
 
440
- module.exports = { GenerateConfigCommand };
440
+ module.exports = { GenerateConfigCommand };
@@ -1,6 +1,7 @@
1
1
  const TestResult = require('test-results-parser/src/models/TestResult');
2
2
  const logger = require('../utils/logger');
3
- const { addChatExtension, addSlackExtension, addTeamsExtension } = require('../helpers/extension.helper');
3
+ const { addChatExtension, addSlackExtension, addTeamsExtension, addGithubExtension } = require('../helpers/extension.helper');
4
+ const { getPlatform } = require('../platforms');
4
5
 
5
6
  class BaseExtension {
6
7
 
@@ -32,6 +33,11 @@ class BaseExtension {
32
33
  * @type {import('..').IExtensionDefaultOptions}
33
34
  */
34
35
  this.default_options = {};
36
+
37
+ /**
38
+ * @type {import('../platforms').BasePlatform}
39
+ */
40
+ this.platform = getPlatform(this.target.name);
35
41
  }
36
42
 
37
43
  updateExtensionInputs() {
@@ -46,6 +52,9 @@ class BaseExtension {
46
52
  case 'chat':
47
53
  this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
48
54
  break;
55
+ case 'github':
56
+ this.extension.inputs = Object.assign({}, { separator: true }, this.extension.inputs);
57
+ break;
49
58
  default:
50
59
  break;
51
60
  }
@@ -67,6 +76,9 @@ class BaseExtension {
67
76
  case 'chat':
68
77
  addChatExtension({ payload: this.payload, extension: this.extension, text: this.text });
69
78
  break;
79
+ case 'github':
80
+ addGithubExtension({ payload: this.payload, extension: this.extension, text: this.text });
81
+ break;
70
82
  default:
71
83
  break;
72
84
  }
@@ -84,6 +96,8 @@ class BaseExtension {
84
96
  return _texts.join('\n');
85
97
  case 'chat':
86
98
  return _texts.join('<br>');
99
+ case 'github':
100
+ return _texts.join('\n');
87
101
  default:
88
102
  break;
89
103
  }
@@ -100,6 +114,8 @@ class BaseExtension {
100
114
  return `*${text}*`;
101
115
  case 'chat':
102
116
  return `<b>${text}</b>`;
117
+ case 'github':
118
+ return `**${text}**`;
103
119
  default:
104
120
  break;
105
121
  }
@@ -38,7 +38,7 @@ class ErrorClustersExtension extends BaseExtension {
38
38
  for (const cluster of clusters) {
39
39
  texts.push(`${truncate(cluster.failure, 150)} - ${this.bold(`(x${cluster.count})`)}`);
40
40
  }
41
- this.text = this.mergeTexts(texts);
41
+ this.text = this.platform.bullets(texts);
42
42
  }
43
43
  }
44
44
 
@@ -14,6 +14,7 @@ const TARGET = Object.freeze({
14
14
  SLACK: 'slack',
15
15
  TEAMS: 'teams',
16
16
  CHAT: 'chat',
17
+ GITHUB: 'github',
17
18
  CUSTOM: 'custom',
18
19
  DELAY: 'delay',
19
20
  INFLUX: 'influx',
@@ -100,6 +100,15 @@ function addChatExtension({ payload, extension, text }) {
100
100
  });
101
101
  }
102
102
 
103
+ function addGithubExtension({ payload, extension, text }) {
104
+ if (extension.inputs.title) {
105
+ const title = extension.inputs.title_link ? `[${extension.inputs.title}](${extension.inputs.title_link})` : extension.inputs.title;
106
+ payload.content.push(`**${title}**\n${text}\n\n`);
107
+ } else {
108
+ payload.content.push(`${text}\n\n`);
109
+ }
110
+ }
111
+
103
112
  /**
104
113
  * Sort extensions by their order property.
105
114
  * Extensions without order will appear last, maintaining their original relative order.
@@ -123,5 +132,6 @@ module.exports = {
123
132
  addSlackExtension,
124
133
  addTeamsExtension,
125
134
  addChatExtension,
135
+ addGithubExtension,
126
136
  sortExtensionsByOrder
127
137
  }
package/src/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface ITarget {
10
10
  name: TargetName;
11
11
  enable?: string | boolean;
12
12
  condition?: Condition;
13
- inputs?: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs | InfluxDBTargetInputs;
13
+ inputs?: SlackInputs | TeamsInputs | ChatInputs | GitHubInputs | CustomTargetInputs | InfluxDBTargetInputs;
14
14
  extensions?: IExtension[];
15
15
  }
16
16
 
@@ -25,7 +25,7 @@ export interface IExtension {
25
25
 
26
26
  export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom' | 'ai-failure-summary';
27
27
  export type Hook = 'start' | 'end' | 'after-summary';
28
- export type TargetName = 'slack' | 'teams' | 'chat' | 'custom' | 'delay';
28
+ export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'custom' | 'delay';
29
29
  export type PublishReportType = 'test-summary' | 'test-summary-slim' | 'failure-details';
30
30
 
31
31
  export interface ConditionFunctionContext {
@@ -242,6 +242,13 @@ export interface TeamsInputs extends TargetInputs {
242
242
 
243
243
  export interface ChatInputs extends TargetInputs { }
244
244
 
245
+ export interface GitHubInputs extends TargetInputs {
246
+ token?: string;
247
+ owner?: string;
248
+ repo?: string;
249
+ pull_number?: string;
250
+ }
251
+
245
252
  export interface InfluxDBTargetInputs {
246
253
  url: string;
247
254
  version?: string;
@@ -6,11 +6,29 @@ class BasePlatform {
6
6
  * @param {string|number} text
7
7
  */
8
8
  bold(text) {
9
- throw new Error('Not Implemented');
9
+ return `**${text}**`;
10
10
  }
11
11
 
12
12
  break() {
13
- throw new Error('Not Implemented');
13
+ return '\n';
14
+ }
15
+
16
+ /**
17
+ * @param {string[]} values
18
+ */
19
+ merge(values) {
20
+ return this.getValidTexts(values).join(this.break());
21
+ }
22
+
23
+ /**
24
+ * @param {string[]} items - Array of strings to convert to bullet points
25
+ * @returns {string} - Formatted bullet points as a string
26
+ */
27
+ bullets(items) {
28
+ if (!items || !Array.isArray(items) || items.length === 0) {
29
+ return '';
30
+ }
31
+ return this.merge(items.map(item => `- ${item}`));
14
32
  }
15
33
 
16
34
  /**
@@ -123,6 +141,14 @@ class BasePlatform {
123
141
 
124
142
  return texts.join(' • ');
125
143
  }
144
+
145
+ /**
146
+ * @param {string[]} texts
147
+ * @returns {string[]}
148
+ */
149
+ getValidTexts(texts) {
150
+ return texts.filter(text => !!text);
151
+ }
126
152
  }
127
153
 
128
154
  module.exports = { BasePlatform }
@@ -13,6 +13,17 @@ class ChatPlatform extends BasePlatform {
13
13
  return '<br>';
14
14
  }
15
15
 
16
+ /**
17
+ * @param {string[]} items - Array of strings to convert to bullet points
18
+ * @returns {string} - Formatted bullet points as a string
19
+ */
20
+ bullets(items) {
21
+ if (!items || !Array.isArray(items) || items.length === 0) {
22
+ return '';
23
+ }
24
+ return this.merge(items.map(item => `• ${item}`));
25
+ }
26
+
16
27
  }
17
28
 
18
29
  module.exports = { ChatPlatform }
@@ -0,0 +1,7 @@
1
+ const { BasePlatform } = require('./base.platform');
2
+
3
+ class GitHubPlatform extends BasePlatform {
4
+
5
+ }
6
+
7
+ module.exports = { GitHubPlatform };
@@ -2,6 +2,7 @@ const { TARGET } = require("../helpers/constants");
2
2
  const { SlackPlatform } = require('./slack.platform');
3
3
  const { TeamsPlatform } = require('./teams.platform');
4
4
  const { ChatPlatform } = require('./chat.platform');
5
+ const { GitHubPlatform } = require('./github.platform');
5
6
 
6
7
  /**
7
8
  *
@@ -15,6 +16,8 @@ function getPlatform(name) {
15
16
  return new TeamsPlatform();
16
17
  case TARGET.CHAT:
17
18
  return new ChatPlatform();
19
+ case TARGET.GITHUB:
20
+ return new GitHubPlatform();
18
21
  default:
19
22
  throw new Error('Invalid Platform');
20
23
  }
@@ -9,8 +9,15 @@ class SlackPlatform extends BasePlatform {
9
9
  return `*${text}*`;
10
10
  }
11
11
 
12
- break() {
13
- return '\n';
12
+ /**
13
+ * @param {string[]} items - Array of strings to convert to bullet points
14
+ * @returns {string} - Formatted bullet points as a string
15
+ */
16
+ bullets(items) {
17
+ if (!items || !Array.isArray(items) || items.length === 0) {
18
+ return '';
19
+ }
20
+ return this.merge(items.map(item => `• ${item}`));
14
21
  }
15
22
  }
16
23
 
@@ -1,16 +1,21 @@
1
1
  const { BasePlatform } = require("./base.platform");
2
2
 
3
3
  class TeamsPlatform extends BasePlatform {
4
- /**
5
- * @param {string|number} text
6
- */
7
- bold(text) {
8
- return `**${text}**`;
9
- }
10
4
 
11
5
  break() {
12
6
  return '\n\n';
13
7
  }
8
+
9
+ /**
10
+ * @param {string[]} items - Array of strings to convert to bullet points
11
+ * @returns {string} - Formatted bullet points as a string
12
+ */
13
+ bullets(items) {
14
+ if (!items || !Array.isArray(items) || items.length === 0) {
15
+ return '';
16
+ }
17
+ return this.merge(items.map(item => `- ${item}`));
18
+ }
14
19
  }
15
20
 
16
21
  module.exports = { TeamsPlatform }
@@ -0,0 +1,310 @@
1
+ const request = require('phin-retry');
2
+ const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
+ const extension_manager = require('../extensions');
4
+ const { HOOK, STATUS, TARGET } = require('../helpers/constants');
5
+ const logger = require('../utils/logger');
6
+
7
+ const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
8
+ const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
9
+ const TestResult = require('test-results-parser/src/models/TestResult');
10
+ const { getPlatform } = require('../platforms');
11
+
12
+ const STATUSES = {
13
+ GOOD: '✅',
14
+ WARNING: '⚠️',
15
+ DANGER: '❌'
16
+ }
17
+
18
+ async function run({ result, target }) {
19
+ setTargetInputs(target);
20
+ const payload = getMainPayload();
21
+ if (result instanceof PerformanceTestResult) {
22
+ await setPerformancePayload({ result, target, payload });
23
+ } else {
24
+ await setFunctionalPayload({ result, target, payload });
25
+ }
26
+ const message = getMarkdownMessage({ result, target, payload });
27
+ logger.info(`🔔 Publishing results to GitHub PR...`);
28
+ return await publishToGitHub({ target, message });
29
+ }
30
+
31
+ async function setFunctionalPayload({ result, target, payload }) {
32
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
33
+ setMainContent({ result, target, payload });
34
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
35
+ setSuiteContent({ result, target, payload });
36
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
37
+ }
38
+
39
+ function setTargetInputs(target) {
40
+ target.inputs = Object.assign({}, default_inputs, target.inputs);
41
+ if (target.inputs.publish === 'test-summary-slim') {
42
+ target.inputs.include_suites = false;
43
+ }
44
+ if (target.inputs.publish === 'failure-details') {
45
+ target.inputs.include_failure_details = true;
46
+ }
47
+ }
48
+
49
+ function getMainPayload() {
50
+ return {
51
+ content: []
52
+ };
53
+ }
54
+
55
+ function setMainContent({ result, target, payload }) {
56
+ const titleText = getTitleText(result, target);
57
+ const resultText = getResultText(result);
58
+ const durationText = getPrettyDuration(result.duration, target.inputs.duration);
59
+
60
+ let content = `## ${titleText}\n\n`;
61
+ content += `**Results**: ${resultText}\n`;
62
+ content += `**Duration**: ${durationText}\n\n`;
63
+
64
+ payload.content.push(content);
65
+ }
66
+
67
+ function getTitleText(result, target) {
68
+ let text = target.inputs.title ? target.inputs.title : result.name;
69
+ if (target.inputs.title_suffix) {
70
+ text = `${text} ${target.inputs.title_suffix}`;
71
+ }
72
+ if (target.inputs.title_link) {
73
+ text = `[${text}](${target.inputs.title_link})`;
74
+ }
75
+
76
+ const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD;
77
+ return `${status} ${text}`;
78
+ }
79
+
80
+ function getResultText(result) {
81
+ const percentage = getPercentage(result.passed, result.total);
82
+ return `${result.passed} / ${result.total} Passed (${percentage}%)`;
83
+ }
84
+
85
+ function setSuiteContent({ result, target, payload }) {
86
+ let suite_count = 0;
87
+ if (target.inputs.include_suites) {
88
+ for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) {
89
+ const suite = result.suites[i];
90
+ if (target.inputs.only_failures && suite.status !== 'FAIL') {
91
+ continue;
92
+ }
93
+
94
+ // if suites length eq to 1 then main content will include suite summary
95
+ if (result.suites.length > 1) {
96
+ payload.content.push(getSuiteSummary({ target, suite }));
97
+ suite_count += 1;
98
+ }
99
+
100
+ if (target.inputs.include_failure_details) {
101
+ // Only attach failure details if there were failures
102
+ if (suite.failed > 0) {
103
+ payload.content.push(getFailureDetails(suite));
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ function getSuiteSummary({ target, suite }) {
111
+ const platform = getPlatform(TARGET.GITHUB);
112
+ const text = platform.getSuiteSummaryText(target, suite);
113
+ return `### ${suite.name}\n${text}\n\n`;
114
+ }
115
+
116
+ function getFailureDetails(suite) {
117
+ let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`;
118
+ const cases = suite.cases;
119
+ for (let i = 0; i < cases.length; i++) {
120
+ const test_case = cases[i];
121
+ if (test_case.status === 'FAIL') {
122
+ content += `**Test**: ${test_case.name}\n`;
123
+ content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`;
124
+ }
125
+ }
126
+ content += `</details>\n\n`;
127
+ return content;
128
+ }
129
+
130
+ function getMarkdownMessage({ result, target, payload }) {
131
+ return payload.content.join('');
132
+ }
133
+
134
+ async function publishToGitHub({ target, message }) {
135
+ const { url, repo, owner, pull_number } = extractGitHubInfo(target);
136
+ const token = target.inputs.token || process.env.GITHUB_TOKEN;
137
+
138
+ if (!token) {
139
+ throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
140
+ }
141
+
142
+ if (!pull_number) {
143
+ throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.');
144
+ }
145
+
146
+ const comment_body = target.inputs.comment_title ?
147
+ `${target.inputs.comment_title}\n\n${message}` :
148
+ message;
149
+
150
+ const headers = {
151
+ 'Authorization': `token ${token}`,
152
+ 'Accept': 'application/vnd.github.v3+json',
153
+ 'User-Agent': 'testbeats'
154
+ };
155
+
156
+ if (target.inputs.update_comment) {
157
+ // Try to find existing comment and update it
158
+ const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
159
+ if (existingComment) {
160
+ return request.patch({
161
+ url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
162
+ headers,
163
+ body: { body: comment_body }
164
+ });
165
+ }
166
+ }
167
+
168
+ // Create new comment
169
+ return request.post({
170
+ url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`,
171
+ headers,
172
+ body: { body: comment_body }
173
+ });
174
+ }
175
+
176
+ async function findExistingComment({ owner, repo, pull_number, token, comment_title }) {
177
+ if (!comment_title) return null;
178
+
179
+ try {
180
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
181
+ const headers = {
182
+ 'Authorization': `token ${token}`,
183
+ 'Accept': 'application/vnd.github.v3+json',
184
+ 'User-Agent': 'testbeats'
185
+ };
186
+
187
+ const response = await request.get({ url, headers });
188
+ const comments = JSON.parse(response.body);
189
+
190
+ return comments.find(comment => comment.body.includes(comment_title));
191
+ } catch (error) {
192
+ logger.warn('Failed to find existing comment:', error.message);
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function extractGitHubInfo(target) {
198
+ let url = target.inputs.url;
199
+ let owner = target.inputs.owner;
200
+ let repo = target.inputs.repo;
201
+ let pull_number = target.inputs.pull_number;
202
+
203
+ if (!owner || !repo) {
204
+ const repository = process.env.GITHUB_REPOSITORY;
205
+ const ref = process.env.GITHUB_REF;
206
+ [owner, repo] = repository.split('/');
207
+ if (ref && ref.includes('refs/pull/')) {
208
+ pull_number = ref.replace('refs/pull/', '').replace('/merge', '');
209
+ }
210
+ }
211
+
212
+ if (!url) {
213
+ url = `https://api.github.com`;
214
+ }
215
+
216
+ return {
217
+ url,
218
+ owner,
219
+ repo,
220
+ pull_number
221
+ };
222
+ }
223
+
224
+ async function setPerformancePayload({ result, target, payload }) {
225
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
226
+ await setPerformanceMainContent({ result, target, payload });
227
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
228
+ await setTransactionContent({ result, target, payload });
229
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
230
+ }
231
+
232
+ async function setPerformanceMainContent({ result, target, payload }) {
233
+ const titleText = getTitleText(result, target);
234
+ let content = `## ${titleText}\n\n`;
235
+
236
+ const metrics = getValidMetrics(result.metrics);
237
+ if (metrics.length > 0) {
238
+ content += `**Performance Metrics**:\n`;
239
+ content += getMetricValuesText(metrics);
240
+ content += '\n\n';
241
+ }
242
+
243
+ content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`;
244
+ payload.content.push(content);
245
+ }
246
+
247
+ async function setTransactionContent({ result, target, payload }) {
248
+ let transaction_count = 0;
249
+ if (target.inputs.include_suites) {
250
+ for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) {
251
+ const transaction = result.transactions[i];
252
+ if (target.inputs.only_failures && transaction.status !== 'FAIL') {
253
+ continue;
254
+ }
255
+
256
+ payload.content.push(getTransactionSummary({ target, transaction }));
257
+ transaction_count += 1;
258
+ }
259
+ }
260
+ }
261
+
262
+ function getTransactionSummary({ target, transaction }) {
263
+ let content = `### ${transaction.name}\n`;
264
+ content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`;
265
+ content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`;
266
+
267
+ if (transaction.metrics && transaction.metrics.length > 0) {
268
+ const metrics = getValidMetrics(transaction.metrics);
269
+ if (metrics.length > 0) {
270
+ content += `**Metrics**: ${getMetricValuesText(metrics)}\n`;
271
+ }
272
+ }
273
+
274
+ content += '\n';
275
+ return content;
276
+ }
277
+
278
+ async function handleErrors({ target, errors }) {
279
+ logger.error('GitHub target errors:', errors);
280
+ }
281
+
282
+ const default_inputs = {
283
+ token: undefined,
284
+ comment_title: undefined,
285
+ update_comment: false,
286
+ owner: undefined,
287
+ repo: undefined,
288
+ pull_number: undefined,
289
+ title: undefined,
290
+ title_suffix: undefined,
291
+ title_link: undefined,
292
+ include_suites: true,
293
+ include_failure_details: false,
294
+ only_failures: false,
295
+ max_suites: 10,
296
+ duration: 'long',
297
+ publish: 'test-summary'
298
+ };
299
+
300
+ const default_options = {
301
+ condition: STATUS.PASS_OR_FAIL
302
+ };
303
+
304
+ module.exports = {
305
+ name: 'GitHub',
306
+ run,
307
+ handleErrors,
308
+ default_inputs,
309
+ default_options
310
+ };
@@ -1,6 +1,7 @@
1
1
  const teams = require('./teams');
2
2
  const slack = require('./slack');
3
3
  const chat = require('./chat');
4
+ const github = require('./github');
4
5
  const custom = require('./custom');
5
6
  const delay = require('./delay');
6
7
  const influx = require('./influx');
@@ -15,6 +16,8 @@ function getTargetRunner(target) {
15
16
  return slack;
16
17
  case TARGET.CHAT:
17
18
  return chat;
19
+ case TARGET.GITHUB:
20
+ return github;
18
21
  case TARGET.CUSTOM:
19
22
  return custom;
20
23
  case TARGET.DELAY:
@@ -91,11 +91,18 @@ class ConfigBuilder {
91
91
  if (this.opts.chat) {
92
92
  this.#addTarget('chat', this.opts.chat);
93
93
  }
94
+ if (this.opts.github) {
95
+ this.#addTarget('github', this.opts.github);
96
+ }
94
97
  }
95
98
 
96
99
  #addTarget(name, url) {
97
100
  this.config.targets = this.config.targets || [];
98
- this.config.targets.push({ name, inputs: { url, title: this.opts.title || '', only_failures: true } })
101
+ if (name === 'github') {
102
+ this.config.targets.push({ name, inputs: { token: this.opts.github, title: this.opts.title || '', only_failures: true } })
103
+ } else {
104
+ this.config.targets.push({ name, inputs: { url, title: this.opts.title || '', only_failures: true } })
105
+ }
99
106
  }
100
107
 
101
108
  #buildExtensions() {