UNPKG

@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
/** * 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 };