@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.
@@ -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.18';
31
+ return packageJson.version || '1.3.21';
32
32
  } catch (error) {
33
- return '1.3.18'; // Fallback version
33
+ return '1.3.21'; // Fallback version
34
34
  }
35
35
  }
36
36
 
37
37
 
38
38
  async outputResults(results, options, metadata = {}) {
39
- // Generate report based on format
40
- const report = this.generateReport(results, metadata, options);
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 (options.output) {
49
- const outputData = options.format === 'json' ? report.raw : report.formatted;
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
- // Annotate GitHub nếu đủ điều kiện
55
- if (options.githubAnnotate && options.format === 'json') {
56
- try {
57
- const annotate = require('./github-annotate-service').annotate;
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;