@sun-asterisk/sunlint 1.3.19 → 1.3.21
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/cli-program.js +8 -1
- package/core/file-targeting-service.js +66 -15
- package/core/git-utils.js +121 -11
- package/core/github-annotate-service.js +1017 -67
- package/core/output-service.js +292 -29
- package/docs/GITHUB_ACTIONS_INTEGRATION.md +421 -0
- package/package.json +2 -2
package/core/output-service.js
CHANGED
|
@@ -28,54 +28,67 @@ class OutputService {
|
|
|
28
28
|
try {
|
|
29
29
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
30
30
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
31
|
-
return packageJson.version || '1.3.
|
|
31
|
+
return packageJson.version || '1.3.21';
|
|
32
32
|
} catch (error) {
|
|
33
|
-
return '1.3.
|
|
33
|
+
return '1.3.21'; // Fallback version
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async outputResults(results, options, metadata = {}) {
|
|
39
|
-
//
|
|
40
|
-
const
|
|
39
|
+
// Handle GitHub annotation setup
|
|
40
|
+
const githubAnnotateConfig = this._prepareGitHubAnnotation(options);
|
|
41
|
+
|
|
42
|
+
// Generate report based on format (override format to json if github-annotate is enabled)
|
|
43
|
+
const effectiveFormat = githubAnnotateConfig.shouldAnnotate ? 'json' : options.format;
|
|
44
|
+
const report = this.generateReport(results, metadata, { ...options, format: effectiveFormat });
|
|
41
45
|
|
|
42
46
|
// Console output
|
|
43
47
|
if (!options.quiet) {
|
|
44
48
|
console.log(report.formatted);
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
// Determine output file (temp or user-specified)
|
|
52
|
+
let outputFile = options.output;
|
|
53
|
+
let shouldCleanupTempFile = false;
|
|
54
|
+
|
|
55
|
+
if (githubAnnotateConfig.shouldAnnotate && !outputFile) {
|
|
56
|
+
// Create temp file for GitHub annotation
|
|
57
|
+
outputFile = githubAnnotateConfig.tempFile;
|
|
58
|
+
shouldCleanupTempFile = true;
|
|
59
|
+
if (options.verbose) {
|
|
60
|
+
console.log(chalk.gray(`ℹ️ Created temporary report file for GitHub annotation: ${outputFile}`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
// File output
|
|
48
|
-
if (
|
|
49
|
-
const outputData =
|
|
65
|
+
if (outputFile) {
|
|
66
|
+
const outputData = effectiveFormat === 'json' ? report.raw : report.formatted;
|
|
50
67
|
const content = typeof outputData === 'string' ? outputData : JSON.stringify(outputData, null, 2);
|
|
51
|
-
fs.writeFileSync(options.output, content);
|
|
52
|
-
console.log(chalk.green(`📄 Report saved to: ${options.output}`));
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Lấy các biến môi trường cần thiết cho annotate
|
|
59
|
-
const repo = process.env.GITHUB_REPOSITORY || options.githubRepo;
|
|
60
|
-
const prNumber = process.env.GITHUB_PR_NUMBER ? parseInt(process.env.GITHUB_PR_NUMBER) : options.githubPrNumber;
|
|
61
|
-
const githubToken = process.env.GITHUB_TOKEN || options.githubToken;
|
|
62
|
-
if (repo && prNumber && githubToken) {
|
|
63
|
-
await annotate({
|
|
64
|
-
jsonFile: options.output,
|
|
65
|
-
githubToken,
|
|
66
|
-
repo,
|
|
67
|
-
prNumber
|
|
68
|
-
});
|
|
69
|
-
console.log(chalk.green('✅ Annotated GitHub PR with SunLint results.'));
|
|
70
|
-
} else {
|
|
71
|
-
console.log(chalk.yellow('⚠️ Missing GITHUB_REPOSITORY, GITHUB_PR_NUMBER, or GITHUB_TOKEN for GitHub annotation.'));
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
console.log(chalk.red('❌ Failed to annotate GitHub PR:'), err.message);
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(outputFile, content);
|
|
71
|
+
if (!shouldCleanupTempFile) {
|
|
72
|
+
console.log(chalk.green(`📄 Report saved to: ${outputFile}`));
|
|
75
73
|
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(chalk.red(`❌ Failed to write report file: ${error.message}`));
|
|
76
|
+
if (shouldCleanupTempFile) {
|
|
77
|
+
this._cleanupTempFile(outputFile);
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
// GitHub annotation
|
|
84
|
+
if (githubAnnotateConfig.shouldAnnotate) {
|
|
85
|
+
await this._handleGitHubAnnotation(
|
|
86
|
+
githubAnnotateConfig,
|
|
87
|
+
outputFile,
|
|
88
|
+
shouldCleanupTempFile
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
// Summary report output (new feature for CI/CD)
|
|
80
93
|
if (options.outputSummary) {
|
|
81
94
|
const summaryReport = this.generateAndSaveSummaryReport(
|
|
@@ -499,6 +512,256 @@ class OutputService {
|
|
|
499
512
|
};
|
|
500
513
|
}
|
|
501
514
|
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Prepare GitHub annotation configuration
|
|
518
|
+
* Check environment and prerequisites
|
|
519
|
+
* @param {Object} options - CLI options
|
|
520
|
+
* @returns {Object} Configuration object
|
|
521
|
+
* @private
|
|
522
|
+
*/
|
|
523
|
+
_prepareGitHubAnnotation(options) {
|
|
524
|
+
// Check if github-annotate flag is enabled
|
|
525
|
+
if (!options.githubAnnotate) {
|
|
526
|
+
return { shouldAnnotate: false };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Parse mode: true/'all' -> all, 'annotate' -> annotate, 'summary' -> summary
|
|
530
|
+
let mode = 'all'; // default
|
|
531
|
+
if (typeof options.githubAnnotate === 'string') {
|
|
532
|
+
const validModes = ['annotate', 'summary', 'all'];
|
|
533
|
+
if (validModes.includes(options.githubAnnotate.toLowerCase())) {
|
|
534
|
+
mode = options.githubAnnotate.toLowerCase();
|
|
535
|
+
} else {
|
|
536
|
+
console.log(chalk.yellow(`⚠️ Invalid --github-annotate mode: ${options.githubAnnotate}. Using default: all`));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Check if we're in a GitHub Actions environment
|
|
541
|
+
const isGitHubActions = process.env.GITHUB_ACTIONS === 'true';
|
|
542
|
+
if (!isGitHubActions) {
|
|
543
|
+
if (options.verbose) {
|
|
544
|
+
console.log(chalk.yellow('⚠️ --github-annotate only works in GitHub Actions environment'));
|
|
545
|
+
}
|
|
546
|
+
return { shouldAnnotate: false };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get GitHub environment variables
|
|
550
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
551
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
552
|
+
const githubToken = process.env.GITHUB_TOKEN || options.githubToken;
|
|
553
|
+
|
|
554
|
+
// Check if it's a PR event
|
|
555
|
+
const isPullRequestEvent = eventName === 'pull_request' || eventName === 'pull_request_target';
|
|
556
|
+
|
|
557
|
+
if (!isPullRequestEvent) {
|
|
558
|
+
if (options.verbose) {
|
|
559
|
+
console.log(chalk.yellow(`⚠️ GitHub annotation only works on pull_request events (current: ${eventName})`));
|
|
560
|
+
}
|
|
561
|
+
return { shouldAnnotate: false };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Get PR number from GitHub context
|
|
565
|
+
let prNumber = null;
|
|
566
|
+
try {
|
|
567
|
+
// Try to get PR number from GITHUB_EVENT_PATH
|
|
568
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
569
|
+
if (eventPath && fs.existsSync(eventPath)) {
|
|
570
|
+
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
571
|
+
prNumber = event.pull_request?.number;
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (options.verbose) {
|
|
575
|
+
console.log(chalk.yellow(`⚠️ Failed to read GitHub event data: ${error.message}`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Fallback: try from environment variable
|
|
580
|
+
if (!prNumber && process.env.GITHUB_PR_NUMBER) {
|
|
581
|
+
prNumber = parseInt(process.env.GITHUB_PR_NUMBER, 10);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Validate required data
|
|
585
|
+
if (!repo) {
|
|
586
|
+
console.log(chalk.yellow('⚠️ Missing GITHUB_REPOSITORY environment variable'));
|
|
587
|
+
return { shouldAnnotate: false };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (!prNumber) {
|
|
591
|
+
console.log(chalk.yellow('⚠️ Could not determine PR number from GitHub context'));
|
|
592
|
+
return { shouldAnnotate: false };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!githubToken) {
|
|
596
|
+
console.log(chalk.yellow('⚠️ Missing GITHUB_TOKEN for authentication'));
|
|
597
|
+
return { shouldAnnotate: false };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Generate temp file path if needed
|
|
601
|
+
const tempFile = path.join(
|
|
602
|
+
process.env.RUNNER_TEMP || '/tmp',
|
|
603
|
+
`sunlint-report-${Date.now()}-${Math.random().toString(36).substring(2, 11)}.json`
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
shouldAnnotate: true,
|
|
608
|
+
mode,
|
|
609
|
+
repo,
|
|
610
|
+
prNumber,
|
|
611
|
+
githubToken,
|
|
612
|
+
tempFile,
|
|
613
|
+
eventName
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Handle GitHub annotation process
|
|
619
|
+
* @param {Object} config - GitHub annotation configuration
|
|
620
|
+
* @param {string} outputFile - Path to report file
|
|
621
|
+
* @param {boolean} shouldCleanup - Whether to cleanup temp file
|
|
622
|
+
* @private
|
|
623
|
+
*/
|
|
624
|
+
async _handleGitHubAnnotation(config, outputFile, shouldCleanup) {
|
|
625
|
+
const mode = config.mode || 'all';
|
|
626
|
+
const results = {};
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
console.log(chalk.blue(`🔄 GitHub PR annotation mode: ${mode}`));
|
|
630
|
+
|
|
631
|
+
if (!config.repo || !config.prNumber || !config.githubToken) {
|
|
632
|
+
throw new Error('Missing required GitHub configuration');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!outputFile || !fs.existsSync(outputFile)) {
|
|
636
|
+
throw new Error(`Report file not found: ${outputFile}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Import services
|
|
640
|
+
const { annotate, postSummaryComment } = require('./github-annotate-service');
|
|
641
|
+
|
|
642
|
+
// Execute based on mode
|
|
643
|
+
const shouldAnnotate = mode === 'annotate' || mode === 'all';
|
|
644
|
+
const shouldSummary = mode === 'summary' || mode === 'all';
|
|
645
|
+
|
|
646
|
+
// 1. Inline comments (annotate mode)
|
|
647
|
+
if (shouldAnnotate) {
|
|
648
|
+
try {
|
|
649
|
+
console.log(chalk.blue('📝 Creating inline comments...'));
|
|
650
|
+
const annotateResult = await annotate({
|
|
651
|
+
jsonFile: outputFile,
|
|
652
|
+
githubToken: config.githubToken,
|
|
653
|
+
repo: config.repo,
|
|
654
|
+
prNumber: config.prNumber,
|
|
655
|
+
skipDuplicates: true
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
results.annotate = annotateResult;
|
|
659
|
+
|
|
660
|
+
if (annotateResult.success) {
|
|
661
|
+
console.log(chalk.green(`✅ Inline comments: ${annotateResult.stats.commentsCreated} created`));
|
|
662
|
+
if (annotateResult.stats.duplicatesSkipped > 0) {
|
|
663
|
+
console.log(chalk.gray(` • Duplicates skipped: ${annotateResult.stats.duplicatesSkipped}`));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.log(chalk.red(`❌ Failed to create inline comments: ${error.message}`));
|
|
668
|
+
results.annotate = { success: false, error: error.message };
|
|
669
|
+
|
|
670
|
+
// Don't throw if we still need to create summary
|
|
671
|
+
if (!shouldSummary) {
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 2. Summary comment (summary mode)
|
|
678
|
+
if (shouldSummary) {
|
|
679
|
+
try {
|
|
680
|
+
console.log(chalk.blue('💬 Creating summary comment...'));
|
|
681
|
+
const summaryResult = await postSummaryComment({
|
|
682
|
+
jsonFile: outputFile,
|
|
683
|
+
githubToken: config.githubToken,
|
|
684
|
+
repo: config.repo,
|
|
685
|
+
prNumber: config.prNumber
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
results.summary = summaryResult;
|
|
689
|
+
|
|
690
|
+
if (summaryResult.success) {
|
|
691
|
+
console.log(chalk.green(`✅ Summary comment: ${summaryResult.action}`));
|
|
692
|
+
if (summaryResult.stats) {
|
|
693
|
+
console.log(chalk.gray(` • Total violations: ${summaryResult.stats.totalViolations}`));
|
|
694
|
+
console.log(chalk.gray(` • Errors: ${summaryResult.stats.errorCount}, Warnings: ${summaryResult.stats.warningCount}`));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.log(chalk.red(`❌ Failed to create summary comment: ${error.message}`));
|
|
699
|
+
results.summary = { success: false, error: error.message };
|
|
700
|
+
|
|
701
|
+
// Throw if both failed or if this is the only mode
|
|
702
|
+
if (!results.annotate || !results.annotate.success) {
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Final summary
|
|
709
|
+
const successCount = [results.annotate?.success, results.summary?.success].filter(Boolean).length;
|
|
710
|
+
const totalCount = [shouldAnnotate, shouldSummary].filter(Boolean).length;
|
|
711
|
+
|
|
712
|
+
if (successCount === totalCount) {
|
|
713
|
+
console.log(chalk.green(`\n✅ Successfully annotated PR #${config.prNumber} (${successCount}/${totalCount} tasks completed)`));
|
|
714
|
+
} else if (successCount > 0) {
|
|
715
|
+
console.log(chalk.yellow(`\n⚠️ Partially completed (${successCount}/${totalCount} tasks successful)`));
|
|
716
|
+
} else {
|
|
717
|
+
console.log(chalk.red(`\n❌ Annotation failed`));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
} catch (error) {
|
|
721
|
+
console.log(chalk.red(`\n❌ Failed to annotate GitHub PR: ${error.message}`));
|
|
722
|
+
|
|
723
|
+
// Log detailed error in verbose mode
|
|
724
|
+
if (process.env.DEBUG === 'true' && error.stack) {
|
|
725
|
+
console.error(chalk.gray('Error stack:'), error.stack);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Show suggestions based on error type
|
|
729
|
+
if (error.name === 'ValidationError') {
|
|
730
|
+
console.log(chalk.yellow('💡 Hint: Check your GitHub environment variables'));
|
|
731
|
+
} else if (error.name === 'GitHubAPIError') {
|
|
732
|
+
console.log(chalk.yellow('💡 Hint: Check GitHub token permissions (needs pull-requests:write)'));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
} finally {
|
|
736
|
+
// Cleanup temp file if needed
|
|
737
|
+
if (shouldCleanup) {
|
|
738
|
+
this._cleanupTempFile(outputFile);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return results;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Cleanup temporary file
|
|
747
|
+
* @param {string} filePath - Path to temp file
|
|
748
|
+
* @private
|
|
749
|
+
*/
|
|
750
|
+
_cleanupTempFile(filePath) {
|
|
751
|
+
try {
|
|
752
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
753
|
+
fs.unlinkSync(filePath);
|
|
754
|
+
if (process.env.DEBUG === 'true') {
|
|
755
|
+
console.log(chalk.gray(`🗑️ Cleaned up temp file: ${filePath}`));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
// Non-critical error, just log in debug mode
|
|
760
|
+
if (process.env.DEBUG === 'true') {
|
|
761
|
+
console.warn(chalk.yellow(`⚠️ Failed to cleanup temp file: ${error.message}`));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
502
765
|
}
|
|
503
766
|
|
|
504
767
|
module.exports = OutputService;
|