testbeats 2.2.2 → 2.2.4
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 +9 -4
- package/package.json +2 -2
- package/src/beats/beats.js +4 -0
- package/src/cli.js +1 -0
- package/src/commands/generate-config.command.js +7 -7
- package/src/commands/publish.command.js +2 -0
- package/src/helpers/constants.js +1 -0
- package/src/helpers/extension.helper.js +21 -1
- package/src/index.d.ts +10 -2
- package/src/platforms/github.platform.js +18 -0
- package/src/platforms/index.js +3 -0
- package/src/targets/github.js +310 -0
- package/src/targets/index.js +3 -0
- package/src/utils/config.builder.js +8 -1
package/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
> This npm package has been renamed from [test-results-reporter](https://www.npmjs.com/package/test-results-reporter) to [testbeats](https://www.npmjs.com/package/testbeats). test-results-reporter will soon be phased out, and users are encouraged to transition to testbeats.
|
|
2
|
-
|
|
3
1
|
<span align="center">
|
|
4
2
|
|
|
5
3
|

|
|
@@ -37,10 +35,12 @@ Read more about the project at [https://testbeats.com](https://testbeats.com)
|
|
|
37
35
|
|
|
38
36
|
#### Results in Portal
|
|
39
37
|
|
|
40
|
-

|
|
41
39
|
|
|
42
40
|
<br />
|
|
43
41
|
|
|
42
|
+
> Read more about the project at [https://testbeats.com](https://testbeats.com)
|
|
43
|
+
|
|
44
44
|
## Need Help
|
|
45
45
|
|
|
46
46
|
We use [Github Discussions](https://github.com/test-results-reporter/testbeats/discussions) to receive feedback, discuss ideas & answer questions. Head over to it and feel free to start a discussion. We are always happy to help 😊.
|
|
@@ -51,4 +51,9 @@ Like this project! Star it on [Github](https://github.com/test-results-reporter/
|
|
|
51
51
|
|
|
52
52
|
<br />
|
|
53
53
|
|
|
54
|
-
> Read more about the project at [https://testbeats.com](https://testbeats.com)
|
|
54
|
+
> Read more about the project at [https://testbeats.com](https://testbeats.com)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
This npm package has been renamed from [test-results-reporter](https://www.npmjs.com/package/test-results-reporter) to [testbeats](https://www.npmjs.com/package/testbeats). test-results-reporter will soon be phased out, and users are encouraged to transition to testbeats.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testbeats",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prompts": "^2.4.2",
|
|
57
57
|
"rosters": "0.0.1",
|
|
58
58
|
"sade": "^1.8.1",
|
|
59
|
-
"test-results-parser": "0.2.
|
|
59
|
+
"test-results-parser": "0.2.8"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"c8": "^10.1.2",
|
package/src/beats/beats.js
CHANGED
|
@@ -128,6 +128,7 @@ class Beats {
|
|
|
128
128
|
this.config.extensions.push({
|
|
129
129
|
name: 'ai-failure-summary',
|
|
130
130
|
hook: HOOK.AFTER_SUMMARY,
|
|
131
|
+
order: 100,
|
|
131
132
|
inputs: {
|
|
132
133
|
data: this.test_run
|
|
133
134
|
}
|
|
@@ -151,6 +152,7 @@ class Beats {
|
|
|
151
152
|
this.config.extensions.push({
|
|
152
153
|
name: 'failure-analysis',
|
|
153
154
|
hook: HOOK.AFTER_SUMMARY,
|
|
155
|
+
order: 200,
|
|
154
156
|
inputs: {
|
|
155
157
|
data: metrics
|
|
156
158
|
}
|
|
@@ -170,6 +172,7 @@ class Beats {
|
|
|
170
172
|
this.config.extensions.push({
|
|
171
173
|
name: 'smart-analysis',
|
|
172
174
|
hook: HOOK.AFTER_SUMMARY,
|
|
175
|
+
order: 300,
|
|
173
176
|
inputs: {
|
|
174
177
|
data: this.test_run
|
|
175
178
|
}
|
|
@@ -225,6 +228,7 @@ class Beats {
|
|
|
225
228
|
this.config.extensions.push({
|
|
226
229
|
name: 'error-clusters',
|
|
227
230
|
hook: HOOK.AFTER_SUMMARY,
|
|
231
|
+
order: 400,
|
|
228
232
|
inputs: {
|
|
229
233
|
data: res.values
|
|
230
234
|
}
|
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 };
|
|
@@ -11,6 +11,7 @@ const { processData } = require('../helpers/helper');
|
|
|
11
11
|
const { ExtensionsSetup } = require('../setups/extensions.setup');
|
|
12
12
|
const pkg = require('../../package.json');
|
|
13
13
|
const { MIN_NODE_VERSION } = require('../helpers/constants');
|
|
14
|
+
const { sortExtensionsByOrder } = require('../helpers/extension.helper');
|
|
14
15
|
|
|
15
16
|
class PublishCommand {
|
|
16
17
|
|
|
@@ -242,6 +243,7 @@ class PublishCommand {
|
|
|
242
243
|
}
|
|
243
244
|
target.extensions = target.extensions || [];
|
|
244
245
|
target.extensions = config.extensions.concat(target.extensions);
|
|
246
|
+
target.extensions = sortExtensionsByOrder(target.extensions);
|
|
245
247
|
await target_manager.run(target, result);
|
|
246
248
|
}
|
|
247
249
|
} else {
|
package/src/helpers/constants.js
CHANGED
|
@@ -100,8 +100,28 @@ function addChatExtension({ payload, extension, text }) {
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Sort extensions by their order property.
|
|
105
|
+
* Extensions without order will appear last, maintaining their original relative order.
|
|
106
|
+
*
|
|
107
|
+
* @param {import("..").IExtension[]} extensions - the extensions array to sort
|
|
108
|
+
* @return {import("..").IExtension[]} sorted extensions array
|
|
109
|
+
*/
|
|
110
|
+
function sortExtensionsByOrder(extensions) {
|
|
111
|
+
if (!extensions || !Array.isArray(extensions)) {
|
|
112
|
+
return extensions;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return extensions.slice().sort((a, b) => {
|
|
116
|
+
const orderA = typeof a.order === 'number' ? a.order : 1000;
|
|
117
|
+
const orderB = typeof b.order === 'number' ? b.order : 1000;
|
|
118
|
+
return orderA - orderB;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
module.exports = {
|
|
104
123
|
addSlackExtension,
|
|
105
124
|
addTeamsExtension,
|
|
106
|
-
addChatExtension
|
|
125
|
+
addChatExtension,
|
|
126
|
+
sortExtensionsByOrder
|
|
107
127
|
}
|
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
|
|
|
@@ -19,12 +19,13 @@ export interface IExtension {
|
|
|
19
19
|
enable?: string | boolean;
|
|
20
20
|
condition?: Condition;
|
|
21
21
|
hook?: Hook;
|
|
22
|
+
order?: number;
|
|
22
23
|
inputs?: ReportPortalAnalysisInputs | ReportPortalHistoryInputs | HyperlinkInputs | MentionInputs | QuickChartTestSummaryInputs | PercyAnalysisInputs | CustomExtensionInputs | MetadataInputs | CIInfoInputs | AIFailureSummaryInputs | BrowserstackInputs;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom' | 'ai-failure-summary';
|
|
26
27
|
export type Hook = 'start' | 'end' | 'after-summary';
|
|
27
|
-
export type TargetName = 'slack' | 'teams' | 'chat' | 'custom' | 'delay';
|
|
28
|
+
export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'custom' | 'delay';
|
|
28
29
|
export type PublishReportType = 'test-summary' | 'test-summary-slim' | 'failure-details';
|
|
29
30
|
|
|
30
31
|
export interface ConditionFunctionContext {
|
|
@@ -241,6 +242,13 @@ export interface TeamsInputs extends TargetInputs {
|
|
|
241
242
|
|
|
242
243
|
export interface ChatInputs extends TargetInputs { }
|
|
243
244
|
|
|
245
|
+
export interface GitHubInputs extends TargetInputs {
|
|
246
|
+
token?: string;
|
|
247
|
+
owner?: string;
|
|
248
|
+
repo?: string;
|
|
249
|
+
pull_number?: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
244
252
|
export interface InfluxDBTargetInputs {
|
|
245
253
|
url: string;
|
|
246
254
|
version?: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { BasePlatform } = require('./base.platform');
|
|
2
|
+
|
|
3
|
+
class GitHubPlatform extends BasePlatform {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string|number} text
|
|
7
|
+
*/
|
|
8
|
+
bold(text) {
|
|
9
|
+
return `**${text}**`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
break() {
|
|
13
|
+
return '\n';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { GitHubPlatform };
|
package/src/platforms/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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 github_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, github_token: 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, github_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 ${github_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
|
+
github_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
|
+
};
|
package/src/targets/index.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|