@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
278 lines (220 loc) • 8.65 kB
JavaScript
/**
* GitHub Step Summary Generator
* Generate rich markdown summary for GitHub Actions workflow
*/
/**
* Generate comprehensive GitHub Step Summary
* @param {Array} violations - Array of violations
* @param {Object} stats - Statistics object
* @param {Object} metadata - Additional metadata
* @returns {string} Markdown summary
*/
function generateStepSummary(violations, stats, metadata = {}) {
const {
totalViolations,
errorCount,
warningCount,
filesWithIssues,
fileGroups,
ruleGroups
} = stats;
const { prNumber, artifactUrl, score } = metadata;
let summary = '';
// Header
const emoji = errorCount > 0 ? '❌' : warningCount > 0 ? '⚠️' : '✅';
const status = errorCount > 0 ? 'Failed' : warningCount > 0 ? 'Warning' : 'Passed';
summary += `# ${emoji} SunLint Quality Report\n\n`;
// Status badges
if (errorCount > 0) {
summary += `> **Status:** 🔴 ${status} - ${errorCount} error(s) must be fixed\n\n`;
} else if (warningCount > 0) {
summary += `> **Status:** 🟡 ${status} - ${warningCount} warning(s) found\n\n`;
} else {
summary += `> **Status:** ✅ ${status} - No violations found!\n\n`;
}
// Quick Stats
summary += `## 📊 Summary\n\n`;
summary += `| Metric | Value |\n`;
summary += `|--------|-------|\n`;
summary += `| Total Violations | **${totalViolations}** |\n`;
summary += `| Errors | **${errorCount}** 🔴 |\n`;
summary += `| Warnings | **${warningCount}** 🟡 |\n`;
summary += `| Files with Issues | **${filesWithIssues}** |\n`;
if (score && score.score !== undefined) {
summary += `| Quality Score | **${score.score}/100** (${score.grade || 'N/A'}) |\n`;
}
summary += `\n`;
if (totalViolations === 0) {
summary += `---\n\n`;
summary += `### 🎉 Excellent Work!\n\n`;
summary += `No coding standard violations detected in this PR.\n\n`;
if (artifactUrl) {
summary += `📥 [Download detailed HTML report](${artifactUrl}#artifacts)\n\n`;
}
return summary;
}
// Top Files with Issues
summary += `## 📁 Top Files with Issues\n\n`;
const sortedFiles = Object.entries(fileGroups)
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 15); // Top 15 files
summary += `| File | Errors | Warnings | Total |\n`;
summary += `|------|--------|----------|-------|\n`;
for (const [file, fileViolations] of sortedFiles) {
const fileErrors = fileViolations.filter(v => v.severity === 'error').length;
const fileWarnings = fileViolations.filter(v => v.severity === 'warning').length;
const total = fileViolations.length;
const errorBadge = fileErrors > 0 ? '🔴' : '';
const warningBadge = fileWarnings > 0 && fileErrors === 0 ? '🟡' : '';
summary += `| \`${truncatePath(file, 60)}\` ${errorBadge}${warningBadge} | ${fileErrors} | ${fileWarnings} | **${total}** |\n`;
}
if (Object.keys(fileGroups).length > 15) {
summary += `\n_... and ${Object.keys(fileGroups).length - 15} more file(s)_\n`;
}
summary += `\n`;
// All Files (Collapsible)
if (Object.keys(fileGroups).length > 15) {
summary += `<details>\n`;
summary += `<summary>📋 View all ${Object.keys(fileGroups).length} files</summary>\n\n`;
const allFiles = Object.entries(fileGroups)
.sort((a, b) => b[1].length - a[1].length);
summary += `| File | Errors | Warnings | Total |\n`;
summary += `|------|--------|----------|-------|\n`;
for (const [file, fileViolations] of allFiles) {
const fileErrors = fileViolations.filter(v => v.severity === 'error').length;
const fileWarnings = fileViolations.filter(v => v.severity === 'warning').length;
const total = fileViolations.length;
summary += `| \`${truncatePath(file, 60)}\` | ${fileErrors} | ${fileWarnings} | ${total} |\n`;
}
summary += `\n</details>\n\n`;
}
// Top Violations by Rule
summary += `## 🔍 Top Violations by Rule\n\n`;
const sortedRules = Object.entries(ruleGroups)
.sort((a, b) => b[1].length - a[1].length)
.slice(0, 10); // Top 10 rules
summary += `| Rule | Errors | Warnings | Total | Locations |\n`;
summary += `|------|--------|----------|-------|------------|\n`;
for (const [ruleId, ruleViolations] of sortedRules) {
const ruleErrors = ruleViolations.filter(v => v.severity === 'error').length;
const ruleWarnings = ruleViolations.filter(v => v.severity === 'warning').length;
const total = ruleViolations.length;
const badge = ruleErrors > 0 ? '🔴' : '🟡';
// Sample locations
const sampleLocations = ruleViolations.slice(0, 3).map(v =>
`\`${truncatePath(v.file, 30)}:${v.line}\``
).join(', ');
const more = ruleViolations.length > 3 ? `, +${ruleViolations.length - 3} more` : '';
summary += `| **${badge} ${ruleId}** | ${ruleErrors} | ${ruleWarnings} | **${total}** | ${sampleLocations}${more} |\n`;
}
if (Object.keys(ruleGroups).length > 10) {
summary += `\n_... and ${Object.keys(ruleGroups).length - 10} more rule(s)_\n`;
}
summary += `\n`;
// All Rules (Collapsible)
if (Object.keys(ruleGroups).length > 10) {
summary += `<details>\n`;
summary += `<summary>📜 View all ${Object.keys(ruleGroups).length} rules</summary>\n\n`;
const allRules = Object.entries(ruleGroups)
.sort((a, b) => b[1].length - a[1].length);
for (const [ruleId, ruleViolations] of allRules) {
const ruleErrors = ruleViolations.filter(v => v.severity === 'error').length;
const ruleWarnings = ruleViolations.filter(v => v.severity === 'warning').length;
const badge = ruleErrors > 0 ? '🔴' : '🟡';
summary += `### ${badge} ${ruleId} (${ruleViolations.length} violations)\n\n`;
// Show first 5 locations
const locationsToShow = ruleViolations.slice(0, 5);
for (const v of locationsToShow) {
summary += `- \`${v.file}:${v.line}\``;
if (v.message && v.message.length < 100) {
summary += ` - ${v.message}`;
}
summary += `\n`;
}
if (ruleViolations.length > 5) {
summary += `\n_... and ${ruleViolations.length - 5} more location(s)_\n`;
}
summary += `\n`;
}
summary += `</details>\n\n`;
}
// Download Links
summary += `---\n\n`;
summary += `## 📥 Download Full Report\n\n`;
if (artifactUrl) {
summary += `The complete interactive HTML report is available for download:\n\n`;
summary += `- **[📥 Download HTML Report](${artifactUrl}#artifacts)** - Interactive report with search, filter, and sorting\n`;
summary += `- **Features:** Detailed violations, quality metrics, exportable results\n`;
summary += `- **Retention:** 30 days\n\n`;
}
// Footer
summary += `---\n\n`;
summary += `<sub>Generated by [SunLint](https://github.com/sun-asterisk/engineer-excellence) • `;
if (prNumber) {
summary += `PR #${prNumber} • `;
}
summary += `${new Date().toLocaleString()}</sub>\n`;
return summary;
}
/**
* Calculate statistics from violations
* @param {Array} violations - Violations array
* @returns {Object} Statistics object
*/
function calculateStatistics(violations) {
const totalViolations = violations.length;
const errorCount = violations.filter(v => v.severity === 'error').length;
const warningCount = violations.filter(v => v.severity === 'warning').length;
// Group by file
const fileGroups = {};
for (const v of violations) {
if (!fileGroups[v.file]) {
fileGroups[v.file] = [];
}
fileGroups[v.file].push(v);
}
// Group by rule
const ruleGroups = {};
for (const v of violations) {
if (!ruleGroups[v.rule]) {
ruleGroups[v.rule] = [];
}
ruleGroups[v.rule].push(v);
}
const filesWithIssues = Object.keys(fileGroups).length;
return {
totalViolations,
errorCount,
warningCount,
filesWithIssues,
fileGroups,
ruleGroups
};
}
/**
* Truncate path for display
* @param {string} path - File path
* @param {number} maxLength - Max length
* @returns {string} Truncated path
*/
function truncatePath(path, maxLength) {
if (path.length <= maxLength) {
return path;
}
const parts = path.split('/');
if (parts.length <= 2) {
return '...' + path.slice(-(maxLength - 3));
}
// Keep first and last parts
const first = parts[0];
const last = parts[parts.length - 1];
const remaining = maxLength - first.length - last.length - 5; // 5 for ".../"
if (remaining < 0) {
return '...' + path.slice(-(maxLength - 3));
}
return `${first}/.../${last}`;
}
module.exports = {
generateStepSummary,
calculateStatistics
};