UNPKG

testbeats

Version:

Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB

316 lines (269 loc) 10.1 kB
const request = require('phin-retry'); const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper'); const extension_manager = require('../extensions'); const { HOOK, STATUS } = require('../helpers/constants'); const logger = require('../utils/logger'); const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult'); const { getValidMetrics, getMetricValuesText } = require('../helpers/performance'); const { BaseTarget } = require('./base.target'); const STATUSES = { GOOD: '✅', WARNING: '⚠️', DANGER: '❌' } const DEFAULT_INPUTS = { token: undefined, comment_title: undefined, update_comment: false, owner: undefined, repo: undefined, pull_number: undefined, title: undefined, title_suffix: undefined, title_link: undefined, include_suites: true, include_failure_details: false, only_failures: false, max_suites: 10, duration: 'long', publish: 'test-summary' }; const default_options = { condition: STATUS.PASS_OR_FAIL }; class GitHubTarget extends BaseTarget { constructor({ target }) { super({ target }); } async run({ result, target }) { this.result = result; this.setTargetInputs(target); const payload = this.getMainPayload(); if (result instanceof PerformanceTestResult) { await this.setPerformancePayload({ result, target, payload }); } else { await this.setFunctionalPayload({ result, target, payload }); } const message = this.getMarkdownMessage({ result, target, payload }); logger.info(`🔔 Publishing results to GitHub PR...`); return await this.publishToGitHub({ target, message }); } async setFunctionalPayload({ result, target, payload }) { await extension_manager.run({ result, target, payload, hook: HOOK.START }); this.setMainContent({ result, target, payload }); await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY }); this.setSuiteContent({ result, target, payload }); await extension_manager.run({ result, target, payload, hook: HOOK.END }); } setTargetInputs(target) { target.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs); if (target.inputs.publish === 'test-summary-slim') { target.inputs.include_suites = false; } if (target.inputs.publish === 'failure-details') { target.inputs.include_failure_details = true; } } getMainPayload() { return { content: [] }; } setMainContent({ result, target, payload }) { const titleText = this.getTitleText(result, target); const resultText = this.getResultText(result); const durationText = getPrettyDuration(result.duration, target.inputs.duration); let content = `## ${titleText}\n\n`; content += `**Results**: ${resultText}\n`; content += `**Duration**: ${durationText}\n\n`; payload.content.push(content); } getTitleText(result, target) { let text = target.inputs.title ? target.inputs.title : result.name; if (target.inputs.title_suffix) { text = `${text} ${target.inputs.title_suffix}`; } if (target.inputs.title_link) { text = `[${text}](${target.inputs.title_link})`; } const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD; return `${status} ${text}`; } getResultText(result) { const percentage = getPercentage(result.passed, result.total); return `${result.passed} / ${result.total} Passed (${percentage}%)`; } setSuiteContent({ result, target, payload }) { let suite_count = 0; if (target.inputs.include_suites) { for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) { const suite = result.suites[i]; if (target.inputs.only_failures && suite.status !== 'FAIL') { continue; } // if suites length eq to 1 then main content will include suite summary if (result.suites.length > 1) { payload.content.push(this.getSuiteSummary({ target, suite })); suite_count += 1; } if (target.inputs.include_failure_details) { // Only attach failure details if there were failures if (suite.failed > 0) { payload.content.push(this.getFailureDetails(suite)); } } } } } getSuiteSummary({ target, suite }) { const text = this.getSuiteSummaryText(target, suite); return `### ${suite.name}\n${text}\n\n`; } getFailureDetails(suite) { let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`; const cases = suite.cases; for (let i = 0; i < cases.length; i++) { const test_case = cases[i]; if (test_case.status === 'FAIL') { content += `**Test**: ${test_case.name}\n`; content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`; } } content += `</details>\n\n`; return content; } getMarkdownMessage({ result, target, payload }) { return payload.content.join(''); } async publishToGitHub({ target, message }) { const { url, repo, owner, pull_number } = this.extractGitHubInfo(target); const token = target.inputs.token || process.env.GITHUB_TOKEN; if (!token) { throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.'); } if (!pull_number) { throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.'); } const comment_body = target.inputs.comment_title ? `${target.inputs.comment_title}\n\n${message}` : message; const headers = { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'testbeats' }; if (target.inputs.update_comment) { // Try to find existing comment and update it const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title }); if (existingComment) { return request.patch({ url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`, headers, body: { body: comment_body } }); } } // Create new comment return request.post({ url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`, headers, body: { body: comment_body } }); } async findExistingComment({ owner, repo, pull_number, token, comment_title }) { if (!comment_title) return null; try { const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`; const headers = { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'testbeats' }; const response = await request.get({ url, headers }); const comments = JSON.parse(response.body); return comments.find(comment => comment.body.includes(comment_title)); } catch (error) { logger.warn('Failed to find existing comment:', error.message); return null; } } extractGitHubInfo(target) { let url = target.inputs.url; let owner = target.inputs.owner; let repo = target.inputs.repo; let pull_number = target.inputs.pull_number; if (!owner || !repo) { const repository = process.env.GITHUB_REPOSITORY; const ref = process.env.GITHUB_REF; [owner, repo] = repository.split('/'); if (ref && ref.includes('refs/pull/')) { pull_number = ref.replace('refs/pull/', '').replace('/merge', ''); } } if (!url) { url = `https://api.github.com`; } return { url, owner, repo, pull_number }; } async setPerformancePayload({ result, target, payload }) { await extension_manager.run({ result, target, payload, hook: HOOK.START }); await this.setPerformanceMainContent({ result, target, payload }); await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY }); await this.setTransactionContent({ result, target, payload }); await extension_manager.run({ result, target, payload, hook: HOOK.END }); } async setPerformanceMainContent({ result, target, payload }) { const titleText = this.getTitleText(result, target); let content = `## ${titleText}\n\n`; const metrics = getValidMetrics(result.metrics); if (metrics.length > 0) { content += `**Performance Metrics**:\n`; content += getMetricValuesText(metrics); content += '\n\n'; } content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`; payload.content.push(content); } async setTransactionContent({ result, target, payload }) { let transaction_count = 0; if (target.inputs.include_suites) { for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) { const transaction = result.transactions[i]; if (target.inputs.only_failures && transaction.status !== 'FAIL') { continue; } payload.content.push(getTransactionSummary({ target, transaction })); transaction_count += 1; } } } getTransactionSummary({ target, transaction }) { let content = `### ${transaction.name}\n`; content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`; content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`; if (transaction.metrics && transaction.metrics.length > 0) { const metrics = getValidMetrics(transaction.metrics); if (metrics.length > 0) { content += `**Metrics**: ${getMetricValuesText(metrics)}\n`; } } content += '\n'; return content; } async handleErrors({ target, errors }) { logger.error('GitHub target errors:', errors); } } module.exports = { // name: 'GitHub', // run, // handleErrors, // default_inputs, // default_options, GitHubTarget };