@sun-asterisk/sunlint 1.3.34 → 1.3.35
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/core/architecture-integration.js +16 -7
- package/core/auto-performance-manager.js +1 -1
- package/core/cli-action-handler.js +92 -2
- package/core/cli-program.js +96 -138
- package/core/file-targeting-service.js +62 -4
- package/core/git-utils.js +19 -12
- package/core/github-annotate-service.js +326 -11
- package/core/html-report-generator.js +326 -731
- package/core/impact-integration.js +433 -0
- package/core/output-service.js +293 -21
- package/core/scoring-service.js +3 -2
- package/engines/arch-detect/core/analyzer.js +413 -0
- package/engines/arch-detect/core/index.js +22 -0
- package/engines/arch-detect/engine/hybrid-detector.js +176 -0
- package/engines/arch-detect/engine/index.js +24 -0
- package/engines/arch-detect/engine/rule-executor.js +228 -0
- package/engines/arch-detect/engine/score-calculator.js +214 -0
- package/engines/arch-detect/engine/violation-detector.js +616 -0
- package/engines/arch-detect/index.js +50 -0
- package/engines/arch-detect/rules/base-rule.js +187 -0
- package/engines/arch-detect/rules/index.js +35 -0
- package/engines/arch-detect/rules/layered/index.js +28 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
- package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
- package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
- package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
- package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
- package/engines/arch-detect/rules/modular/index.js +27 -0
- package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
- package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
- package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
- package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
- package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
- package/engines/arch-detect/rules/presentation/index.js +27 -0
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
- package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
- package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
- package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
- package/engines/arch-detect/rules/project-scanner/index.js +31 -0
- package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
- package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
- package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
- package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
- package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
- package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
- package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
- package/engines/arch-detect/rules/rule-registry.js +111 -0
- package/engines/arch-detect/types/context.types.js +60 -0
- package/engines/arch-detect/types/enums.js +161 -0
- package/engines/arch-detect/types/index.js +25 -0
- package/engines/arch-detect/types/result.types.js +7 -0
- package/engines/arch-detect/types/rule.types.js +7 -0
- package/engines/arch-detect/utils/file-scanner.js +411 -0
- package/engines/arch-detect/utils/index.js +23 -0
- package/engines/arch-detect/utils/pattern-matcher.js +328 -0
- package/engines/impact/cli.js +106 -0
- package/engines/impact/config/default-config.js +54 -0
- package/engines/impact/core/change-detector.js +258 -0
- package/engines/impact/core/detectors/database-detector.js +1317 -0
- package/engines/impact/core/detectors/endpoint-detector.js +55 -0
- package/engines/impact/core/impact-analyzer.js +124 -0
- package/engines/impact/core/report-generator.js +462 -0
- package/engines/impact/core/utils/ast-parser.js +241 -0
- package/engines/impact/core/utils/dependency-graph.js +159 -0
- package/engines/impact/core/utils/file-utils.js +116 -0
- package/engines/impact/core/utils/git-utils.js +203 -0
- package/engines/impact/core/utils/logger.js +13 -0
- package/engines/impact/core/utils/method-call-graph.js +1192 -0
- package/engines/impact/index.js +135 -0
- package/engines/impact/package.json +29 -0
- package/package.json +18 -43
- package/scripts/build-release.sh +0 -0
- package/scripts/copy-impact-analyzer.js +135 -0
- package/scripts/install.sh +0 -0
- package/scripts/manual-release.sh +0 -0
- package/scripts/pre-release-test.sh +0 -0
- package/scripts/prepare-release.sh +0 -0
- package/scripts/quick-performance-test.js +0 -0
- package/scripts/setup-github-registry.sh +0 -0
- package/scripts/trigger-release.sh +0 -0
- package/scripts/verify-install.sh +0 -0
- package/templates/combined-report.html +1418 -0
|
@@ -71,9 +71,10 @@ function sleep(ms) {
|
|
|
71
71
|
* @returns {Promise<string|null>} AI-generated summary or null
|
|
72
72
|
*/
|
|
73
73
|
async function generateAISummary(violations, stats) {
|
|
74
|
-
|
|
74
|
+
// Prefer GH_MODELS_TOKEN (PAT with Models access) over GITHUB_TOKEN
|
|
75
|
+
const token = process.env.GH_MODELS_TOKEN || process.env.GITHUB_TOKEN;
|
|
75
76
|
if (!token) {
|
|
76
|
-
logger.debug('No
|
|
77
|
+
logger.debug('No GitHub token available, skipping AI summary');
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -96,7 +97,7 @@ async function generateAISummary(violations, stats) {
|
|
|
96
97
|
ruleGroups[v.rule] = (ruleGroups[v.rule] || 0) + 1;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
const prompt = `You are a code review assistant. Analyze these code quality violations and provide a brief
|
|
100
|
+
const prompt = `You are a code review assistant. Analyze these code quality violations and provide a verdict and brief summary.
|
|
100
101
|
|
|
101
102
|
Violations by rule:
|
|
102
103
|
${Object.entries(ruleGroups).map(([rule, count]) => `- ${rule}: ${count} issues`).join('\n')}
|
|
@@ -106,10 +107,19 @@ ${topViolations.slice(0, 5).map(v => `- [${v.rule}] ${v.file}: ${v.message}`).jo
|
|
|
106
107
|
|
|
107
108
|
Stats: ${stats.errorCount} errors, ${stats.warningCount} warnings in ${stats.filesWithIssues} files.
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
Your response MUST start with one of these verdicts on its own line:
|
|
111
|
+
- "🚫 REQUIRES FIXES" - if there are errors or critical issues that must be fixed before merging
|
|
112
|
+
- "⚠️ NEEDS ATTENTION" - if there are warnings that should be reviewed but not blocking
|
|
113
|
+
- "✅ READY TO MERGE" - if issues are minor or acceptable
|
|
114
|
+
|
|
115
|
+
Then provide 2-3 sentences about:
|
|
116
|
+
1. Main issues found
|
|
117
|
+
2. Priority areas to address
|
|
118
|
+
|
|
119
|
+
Keep it under 120 words total.`;
|
|
120
|
+
|
|
121
|
+
const tokenSource = process.env.GH_MODELS_TOKEN ? 'GH_MODELS_TOKEN' : 'GITHUB_TOKEN';
|
|
122
|
+
logger.info(`Generating AI summary via GitHub Models API (using ${tokenSource})...`);
|
|
113
123
|
|
|
114
124
|
const response = await fetch('https://models.inference.ai.azure.com/chat/completions', {
|
|
115
125
|
method: 'POST',
|
|
@@ -126,7 +136,8 @@ Keep it under 100 words, no markdown headers.`;
|
|
|
126
136
|
});
|
|
127
137
|
|
|
128
138
|
if (!response.ok) {
|
|
129
|
-
|
|
139
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
140
|
+
logger.warn(`GitHub Models API error: ${response.status} - ${errorText}`);
|
|
130
141
|
return null;
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -138,13 +149,106 @@ Keep it under 100 words, no markdown headers.`;
|
|
|
138
149
|
return aiSummary;
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
logger.warn('AI summary response empty');
|
|
141
153
|
return null;
|
|
142
154
|
} catch (error) {
|
|
143
|
-
logger.
|
|
155
|
+
logger.warn(`AI summary generation failed: ${error.message}`);
|
|
144
156
|
return null;
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Format AI summary for better readability
|
|
162
|
+
* @param {string} summary - Raw AI summary text
|
|
163
|
+
* @returns {string} Formatted summary with highlights
|
|
164
|
+
*/
|
|
165
|
+
function formatAISummary(summary) {
|
|
166
|
+
if (!summary) return '';
|
|
167
|
+
|
|
168
|
+
let formatted = summary;
|
|
169
|
+
let verdictLine = '';
|
|
170
|
+
|
|
171
|
+
// Extract verdict from first line
|
|
172
|
+
const lines = formatted.split('\n');
|
|
173
|
+
const firstLine = lines[0].trim();
|
|
174
|
+
|
|
175
|
+
if (firstLine.includes('REQUIRES FIXES') || firstLine.includes('🚫')) {
|
|
176
|
+
verdictLine = '> 🚫 **REQUIRES FIXES** - Critical issues must be resolved before merging\n\n';
|
|
177
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
178
|
+
} else if (firstLine.includes('NEEDS ATTENTION') || firstLine.includes('⚠️')) {
|
|
179
|
+
verdictLine = '> ⚠️ **NEEDS ATTENTION** - Review recommended but not blocking\n\n';
|
|
180
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
181
|
+
} else if (firstLine.includes('READY TO MERGE') || firstLine.includes('✅')) {
|
|
182
|
+
verdictLine = '> ✅ **READY TO MERGE** - Code quality meets standards\n\n';
|
|
183
|
+
formatted = lines.slice(1).join('\n').trim();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Highlight rule IDs (C001, C019, etc.)
|
|
187
|
+
formatted = formatted.replace(/\b(C\d{3})\b/g, '`$1`');
|
|
188
|
+
|
|
189
|
+
// Highlight severity words
|
|
190
|
+
formatted = formatted.replace(/\b(critical|high|error|errors)\b/gi, '**$1**');
|
|
191
|
+
formatted = formatted.replace(/\b(warning|warnings|medium)\b/gi, '*$1*');
|
|
192
|
+
|
|
193
|
+
// Split into sentences and format as list if multiple sentences
|
|
194
|
+
const sentences = formatted.split(/(?<=[.!?])\s+/).filter(s => s.trim());
|
|
195
|
+
|
|
196
|
+
if (sentences.length > 1) {
|
|
197
|
+
// Format as bullet points
|
|
198
|
+
const mainIssues = [];
|
|
199
|
+
const recommendations = [];
|
|
200
|
+
|
|
201
|
+
for (const sentence of sentences) {
|
|
202
|
+
const lower = sentence.toLowerCase();
|
|
203
|
+
if (lower.includes('prioritize') || lower.includes('focus') || lower.includes('recommend') || lower.includes('should')) {
|
|
204
|
+
recommendations.push(sentence.trim());
|
|
205
|
+
} else {
|
|
206
|
+
mainIssues.push(sentence.trim());
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let result = verdictLine;
|
|
211
|
+
|
|
212
|
+
if (mainIssues.length > 0) {
|
|
213
|
+
result += '**🔍 Issues Found:**\n';
|
|
214
|
+
for (const issue of mainIssues) {
|
|
215
|
+
result += `- ${issue}\n`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (recommendations.length > 0) {
|
|
220
|
+
result += '\n**💡 Recommendations:**\n';
|
|
221
|
+
for (const rec of recommendations) {
|
|
222
|
+
result += `- ${rec}\n`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result || verdictLine + formatted;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return verdictLine + (formatted ? `> ${formatted}` : '');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Generate a visual score bar using Unicode blocks
|
|
234
|
+
* @param {number} score - Score from 0-100
|
|
235
|
+
* @returns {string} Visual progress bar
|
|
236
|
+
*/
|
|
237
|
+
function generateScoreBar(score) {
|
|
238
|
+
const total = 20;
|
|
239
|
+
const filled = Math.round((score / 100) * total);
|
|
240
|
+
const empty = total - filled;
|
|
241
|
+
|
|
242
|
+
// Use different colors based on score
|
|
243
|
+
let color;
|
|
244
|
+
if (score >= 80) color = '🟩';
|
|
245
|
+
else if (score >= 60) color = '🟨';
|
|
246
|
+
else if (score >= 40) color = '🟧';
|
|
247
|
+
else color = '🟥';
|
|
248
|
+
|
|
249
|
+
return color.repeat(filled) + '⬜'.repeat(empty);
|
|
250
|
+
}
|
|
251
|
+
|
|
148
252
|
/**
|
|
149
253
|
* Calculate quality score from violations
|
|
150
254
|
* @param {number} errorCount - Number of errors
|
|
@@ -1030,8 +1134,8 @@ async function postSummaryComment({
|
|
|
1030
1134
|
|
|
1031
1135
|
// AI Summary (if available)
|
|
1032
1136
|
if (aiSummary) {
|
|
1033
|
-
summary += `####
|
|
1034
|
-
summary += `${aiSummary}\n\n`;
|
|
1137
|
+
summary += `#### 🧠 AI Analysis\n\n`;
|
|
1138
|
+
summary += `${formatAISummary(aiSummary)}\n\n`;
|
|
1035
1139
|
}
|
|
1036
1140
|
|
|
1037
1141
|
// Compact summary table
|
|
@@ -1142,9 +1246,220 @@ async function postSummaryComment({
|
|
|
1142
1246
|
}
|
|
1143
1247
|
}
|
|
1144
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* Post combined summary comment on GitHub PR (Code Quality + Architecture + Impact)
|
|
1251
|
+
* @param {Object} options
|
|
1252
|
+
* @param {string} [options.githubToken] - GitHub token, falls back to GITHUB_TOKEN env
|
|
1253
|
+
* @param {string} options.repo - GitHub repo in format owner/repo
|
|
1254
|
+
* @param {number} options.prNumber - Pull request number
|
|
1255
|
+
* @param {Object} [options.codeQuality] - Code quality results
|
|
1256
|
+
* @param {Object} [options.architecture] - Architecture detection results
|
|
1257
|
+
* @param {Object} [options.impact] - Impact analysis results
|
|
1258
|
+
* @returns {Promise<Object>} Result object
|
|
1259
|
+
*/
|
|
1260
|
+
async function postCombinedSummaryComment({
|
|
1261
|
+
githubToken,
|
|
1262
|
+
repo,
|
|
1263
|
+
prNumber,
|
|
1264
|
+
codeQuality = null,
|
|
1265
|
+
architecture = null,
|
|
1266
|
+
impact = null
|
|
1267
|
+
}) {
|
|
1268
|
+
const startTime = Date.now();
|
|
1269
|
+
|
|
1270
|
+
try {
|
|
1271
|
+
logger.info('Starting combined summary comment', { repo, prNumber });
|
|
1272
|
+
|
|
1273
|
+
// Validate basic params
|
|
1274
|
+
const token = githubToken || process.env.GITHUB_TOKEN;
|
|
1275
|
+
if (!token) {
|
|
1276
|
+
throw new ValidationError('githubToken is required or GITHUB_TOKEN env var must be set');
|
|
1277
|
+
}
|
|
1278
|
+
if (!repo || !repo.includes('/')) {
|
|
1279
|
+
throw new ValidationError('repo must be in format "owner/repo"');
|
|
1280
|
+
}
|
|
1281
|
+
if (!prNumber || prNumber <= 0) {
|
|
1282
|
+
throw new ValidationError('prNumber must be a positive integer');
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const [owner, repoName] = repo.split('/');
|
|
1286
|
+
|
|
1287
|
+
if (!Octokit) {
|
|
1288
|
+
Octokit = (await import('@octokit/rest')).Octokit;
|
|
1289
|
+
}
|
|
1290
|
+
const octokit = new Octokit({ auth: token });
|
|
1291
|
+
|
|
1292
|
+
// Build combined summary - Clean minimal style with logo
|
|
1293
|
+
let summary = `## <a href="https://coding-standards.sun-asterisk.vn/"><img src="https://coding-standards.sun-asterisk.vn/logo-light.svg" alt="SunLint" height="28"></a> Code Quality Analysis\n\n`;
|
|
1294
|
+
|
|
1295
|
+
// === Quality Score Section ===
|
|
1296
|
+
if (codeQuality) {
|
|
1297
|
+
const { errorCount = 0, warningCount = 0, filesWithIssues = 0, totalViolations = 0, score = {} } = codeQuality;
|
|
1298
|
+
const qualityScore = typeof score.value === 'number' && !isNaN(score.value) ? score.value : 0;
|
|
1299
|
+
const grade = score.grade || 'F';
|
|
1300
|
+
|
|
1301
|
+
// Grade emoji and description based on score
|
|
1302
|
+
let gradeEmoji, gradeDesc;
|
|
1303
|
+
if (qualityScore >= 90) {
|
|
1304
|
+
gradeEmoji = '🏆'; gradeDesc = 'Excellent';
|
|
1305
|
+
} else if (qualityScore >= 80) {
|
|
1306
|
+
gradeEmoji = '✨'; gradeDesc = 'Good';
|
|
1307
|
+
} else if (qualityScore >= 70) {
|
|
1308
|
+
gradeEmoji = '👍'; gradeDesc = 'Fair';
|
|
1309
|
+
} else if (qualityScore >= 60) {
|
|
1310
|
+
gradeEmoji = '⚡'; gradeDesc = 'Needs Work';
|
|
1311
|
+
} else {
|
|
1312
|
+
gradeEmoji = '🔧'; gradeDesc = 'Needs Improvement';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Clean score display
|
|
1316
|
+
summary += `### Quality Score: **${qualityScore}/100** · Grade: ${gradeEmoji} ${grade} (${gradeDesc})\n\n`;
|
|
1317
|
+
|
|
1318
|
+
// Issue summary
|
|
1319
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
1320
|
+
summary += `✅ **No issues found!** Great job!\n\n`;
|
|
1321
|
+
} else {
|
|
1322
|
+
summary += `| | Count |\n`;
|
|
1323
|
+
summary += `|:--|--:|\n`;
|
|
1324
|
+
if (errorCount > 0) {
|
|
1325
|
+
summary += `| ❌ Errors | **${errorCount}** |\n`;
|
|
1326
|
+
}
|
|
1327
|
+
if (warningCount > 0) {
|
|
1328
|
+
summary += `| ⚠️ Warnings | ${warningCount} |\n`;
|
|
1329
|
+
}
|
|
1330
|
+
summary += `| 📁 Files with issues | ${filesWithIssues} |\n\n`;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// AI Summary (if available)
|
|
1334
|
+
if (codeQuality.aiSummary) {
|
|
1335
|
+
summary += `#### 🧠 AI Analysis\n\n`;
|
|
1336
|
+
summary += `${formatAISummary(codeQuality.aiSummary)}\n\n`;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// === Additional Metrics (Direct display) ===
|
|
1341
|
+
if (architecture || impact) {
|
|
1342
|
+
// Architecture section
|
|
1343
|
+
if (architecture) {
|
|
1344
|
+
const { pattern, confidence, healthScore, violations = [] } = architecture;
|
|
1345
|
+
const patternDisplay = pattern ? pattern.toUpperCase() : 'UNKNOWN';
|
|
1346
|
+
const confidenceDisplay = confidence !== undefined ? confidence : 0;
|
|
1347
|
+
const healthDisplay = healthScore !== undefined ? healthScore : 0;
|
|
1348
|
+
const healthIcon = healthDisplay >= 80 ? '🟢' : healthDisplay >= 60 ? '🟡' : '🔴';
|
|
1349
|
+
|
|
1350
|
+
summary += `#### 🏛️ Architecture\n\n`;
|
|
1351
|
+
summary += `| Pattern | Confidence | Health |\n`;
|
|
1352
|
+
summary += `|:--|:--:|:--:|\n`;
|
|
1353
|
+
summary += `| **${patternDisplay}** | ${confidenceDisplay}% | ${healthIcon} ${healthDisplay}/100 |\n\n`;
|
|
1354
|
+
|
|
1355
|
+
if (violations.length > 0) {
|
|
1356
|
+
summary += `⚠️ ${violations.length} architecture violations detected\n\n`;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Impact section
|
|
1361
|
+
if (impact) {
|
|
1362
|
+
const { score: impactScore = 0, severity = 'LOW', endpoints = [], tables = [] } = impact;
|
|
1363
|
+
const severityIcon = severity === 'HIGH' ? '🔴' : severity === 'MEDIUM' ? '🟡' : '🟢';
|
|
1364
|
+
|
|
1365
|
+
summary += `#### 🎯 Impact Analysis\n\n`;
|
|
1366
|
+
summary += `| Severity | Score | Affected |\n`;
|
|
1367
|
+
summary += `|:--|:--:|:--|\n`;
|
|
1368
|
+
|
|
1369
|
+
let affectedItems = [];
|
|
1370
|
+
if (endpoints.length > 0) affectedItems.push(`${endpoints.length} APIs`);
|
|
1371
|
+
if (tables.length > 0) affectedItems.push(`${tables.length} tables`);
|
|
1372
|
+
const affectedText = affectedItems.length > 0 ? affectedItems.join(', ') : 'None';
|
|
1373
|
+
|
|
1374
|
+
summary += `| ${severityIcon} **${severity}** | ${impactScore}/100 | ${affectedText} |\n\n`;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// === Footer ===
|
|
1379
|
+
summary += `---\n`;
|
|
1380
|
+
summary += `<sub>`;
|
|
1381
|
+
summary += `Powered by [SunLint](https://coding-standards.sun-asterisk.vn)`;
|
|
1382
|
+
|
|
1383
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
1384
|
+
const runUrl = `https://github.com/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
1385
|
+
summary += ` · [View Details](${runUrl})`;
|
|
1386
|
+
}
|
|
1387
|
+
summary += `</sub>\n`;
|
|
1388
|
+
|
|
1389
|
+
// Find existing comment
|
|
1390
|
+
let existingComment = null;
|
|
1391
|
+
try {
|
|
1392
|
+
const { data: comments } = await octokit.issues.listComments({
|
|
1393
|
+
owner,
|
|
1394
|
+
repo: repoName,
|
|
1395
|
+
issue_number: prNumber,
|
|
1396
|
+
per_page: 100
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
existingComment = comments.find(comment =>
|
|
1400
|
+
comment.body.includes('Code Quality Analysis') ||
|
|
1401
|
+
comment.body.includes('☀️ SunLint') ||
|
|
1402
|
+
comment.body.includes('SunLint Analysis Report') ||
|
|
1403
|
+
comment.body.includes('SunLint Report')
|
|
1404
|
+
);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
logger.warn('Failed to fetch existing comments', { error: error.message });
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Post or update comment
|
|
1410
|
+
let commentResult;
|
|
1411
|
+
try {
|
|
1412
|
+
if (existingComment) {
|
|
1413
|
+
commentResult = await withRetry(async () => {
|
|
1414
|
+
return await octokit.issues.updateComment({
|
|
1415
|
+
owner,
|
|
1416
|
+
repo: repoName,
|
|
1417
|
+
comment_id: existingComment.id,
|
|
1418
|
+
body: summary
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
logger.info('Combined summary updated');
|
|
1422
|
+
} else {
|
|
1423
|
+
commentResult = await withRetry(async () => {
|
|
1424
|
+
return await octokit.issues.createComment({
|
|
1425
|
+
owner,
|
|
1426
|
+
repo: repoName,
|
|
1427
|
+
issue_number: prNumber,
|
|
1428
|
+
body: summary
|
|
1429
|
+
});
|
|
1430
|
+
});
|
|
1431
|
+
logger.info('Combined summary created');
|
|
1432
|
+
}
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
throw new GitHubAPIError(
|
|
1435
|
+
`Failed to post combined summary: ${error.message}`,
|
|
1436
|
+
error.status,
|
|
1437
|
+
error
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
success: true,
|
|
1443
|
+
action: existingComment ? 'updated' : 'created',
|
|
1444
|
+
commentId: commentResult.data.id,
|
|
1445
|
+
commentUrl: commentResult.data.html_url,
|
|
1446
|
+
duration: Date.now() - startTime
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
logger.error('Combined summary failed', error);
|
|
1451
|
+
if (error instanceof ValidationError || error instanceof GitHubAPIError) {
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
throw new Error(`Combined summary failed: ${error.message}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1145
1458
|
module.exports = {
|
|
1146
1459
|
annotate,
|
|
1147
1460
|
postSummaryComment,
|
|
1461
|
+
postCombinedSummaryComment,
|
|
1462
|
+
generateAISummary,
|
|
1148
1463
|
ValidationError,
|
|
1149
1464
|
GitHubAPIError
|
|
1150
1465
|
};
|