@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/cli-program.js
CHANGED
|
@@ -37,7 +37,7 @@ function createCliProgram() {
|
|
|
37
37
|
.option('--output-summary <file>', 'Output summary report file path (JSON format for CI/CD)')
|
|
38
38
|
.option('--upload-report [url]', 'Upload summary report to API endpoint after analysis (default: Sun* Coding Standards API)')
|
|
39
39
|
.option('--config <file>', 'Configuration file path (default: auto-discover)')
|
|
40
|
-
.option('--github-annotate', 'Annotate GitHub PR
|
|
40
|
+
.option('--github-annotate [mode]', 'Annotate GitHub PR: annotate (inline), summary (comment), all (both) - default: all');
|
|
41
41
|
|
|
42
42
|
// File targeting options
|
|
43
43
|
program
|
|
@@ -135,6 +135,13 @@ CI/CD Integration:
|
|
|
135
135
|
$ sunlint --all --output-summary=report.json --upload-report
|
|
136
136
|
$ sunlint --all --output-summary=report.json --upload-report=https://custom-api.com/reports
|
|
137
137
|
|
|
138
|
+
GitHub Actions Integration:
|
|
139
|
+
$ sunlint --all --input=src --github-annotate # Both inline + summary (default)
|
|
140
|
+
$ sunlint --all --input=src --github-annotate=annotate # Inline comments only
|
|
141
|
+
$ sunlint --all --input=src --github-annotate=summary # Summary comment only
|
|
142
|
+
$ sunlint --all --input=src --github-annotate=all # Both inline + summary
|
|
143
|
+
$ sunlint --all --changed-files --github-annotate # With changed files
|
|
144
|
+
|
|
138
145
|
ESLint Integration:
|
|
139
146
|
$ sunlint --typescript --eslint-integration --input=src
|
|
140
147
|
$ sunlint --all --eslint-integration --eslint-merge-rules --input=src
|
|
@@ -12,6 +12,7 @@ const { minimatch } = require('minimatch');
|
|
|
12
12
|
class FileTargetingService {
|
|
13
13
|
constructor() {
|
|
14
14
|
this.supportedLanguages = ['typescript', 'javascript', 'dart', 'kotlin', 'java', 'swift'];
|
|
15
|
+
this.GitUtils = require('./git-utils');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -22,35 +23,43 @@ class FileTargetingService {
|
|
|
22
23
|
try {
|
|
23
24
|
const startTime = Date.now();
|
|
24
25
|
const metadata = config._metadata;
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
if (cliOptions.verbose) {
|
|
27
28
|
console.log(chalk.cyan(`đ File Targeting: ${this.getTargetingMode(metadata)}`));
|
|
28
29
|
if (metadata?.shouldBypassProjectDiscovery) {
|
|
29
30
|
console.log(chalk.blue(`đ¯ Optimized targeting for ${metadata.analysisScope}`));
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
let allFiles = [];
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
allFiles = await this.
|
|
35
|
+
|
|
36
|
+
// Handle --changed-files option (git diff mode)
|
|
37
|
+
if (cliOptions.changedFiles) {
|
|
38
|
+
if (cliOptions.verbose) {
|
|
39
|
+
console.log(chalk.cyan('đ Using --changed-files mode (git diff)'));
|
|
40
|
+
}
|
|
41
|
+
allFiles = await this.getGitChangedFiles(cliOptions);
|
|
41
42
|
} else {
|
|
42
|
-
|
|
43
|
+
// Smart project-level optimization
|
|
44
|
+
const optimizedPaths = this.optimizeProjectPaths(inputPaths, cliOptions);
|
|
45
|
+
|
|
46
|
+
// Use enhanced targeting based on metadata
|
|
47
|
+
if (metadata?.shouldBypassProjectDiscovery) {
|
|
48
|
+
allFiles = await this.collectTargetedFiles(optimizedPaths, config, cliOptions);
|
|
49
|
+
} else {
|
|
50
|
+
allFiles = await this.collectProjectFiles(optimizedPaths, config, cliOptions);
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
// Apply filtering logic
|
|
46
55
|
const targetFiles = this.applyFiltering(allFiles, config, cliOptions);
|
|
47
|
-
|
|
56
|
+
|
|
48
57
|
const duration = Date.now() - startTime;
|
|
49
|
-
|
|
58
|
+
|
|
50
59
|
if (cliOptions.verbose) {
|
|
51
60
|
console.log(chalk.green(`â
File targeting completed in ${duration}ms (${targetFiles.length} files)`));
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
|
|
54
63
|
return {
|
|
55
64
|
files: targetFiles,
|
|
56
65
|
stats: this.generateStats(targetFiles, config),
|
|
@@ -62,6 +71,41 @@ class FileTargetingService {
|
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Get files changed in git (for --changed-files option)
|
|
76
|
+
*/
|
|
77
|
+
async getGitChangedFiles(cliOptions) {
|
|
78
|
+
try {
|
|
79
|
+
const baseRef = cliOptions.diffBase || null; // null = auto-detect
|
|
80
|
+
const changedFiles = this.GitUtils.getChangedFiles(baseRef);
|
|
81
|
+
|
|
82
|
+
if (cliOptions.verbose) {
|
|
83
|
+
const detectedBase = baseRef || this.GitUtils.getSmartBaseRef();
|
|
84
|
+
console.log(chalk.blue(`âšī¸ Using base ref: ${detectedBase}`));
|
|
85
|
+
console.log(chalk.blue(`âšī¸ Found ${changedFiles.length} changed file(s)`));
|
|
86
|
+
|
|
87
|
+
if (changedFiles.length > 0 && changedFiles.length <= 10) {
|
|
88
|
+
console.log(chalk.gray(' Changed files:'));
|
|
89
|
+
changedFiles.forEach(f => {
|
|
90
|
+
console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
|
|
91
|
+
});
|
|
92
|
+
} else if (changedFiles.length > 10) {
|
|
93
|
+
console.log(chalk.gray(` First 10 changed files:`));
|
|
94
|
+
changedFiles.slice(0, 10).forEach(f => {
|
|
95
|
+
console.log(chalk.gray(` - ${path.relative(process.cwd(), f)}`));
|
|
96
|
+
});
|
|
97
|
+
console.log(chalk.gray(` ... and ${changedFiles.length - 10} more`));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return changedFiles;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(chalk.yellow(`â ī¸ Failed to get changed files: ${error.message}`));
|
|
104
|
+
console.error(chalk.yellow('âšī¸ Falling back to all files'));
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
/**
|
|
66
110
|
* Get targeting mode description
|
|
67
111
|
*/
|
|
@@ -293,15 +337,22 @@ class FileTargetingService {
|
|
|
293
337
|
if (debug) console.log(`đ [DEBUG] applyFiltering: start with ${filteredFiles.length} files`);
|
|
294
338
|
if (debug) console.log(`đ [DEBUG] config.include:`, config.include);
|
|
295
339
|
if (debug) console.log(`đ [DEBUG] cliOptions.include:`, cliOptions.include);
|
|
340
|
+
if (debug) console.log(`đ [DEBUG] cliOptions.changedFiles:`, cliOptions.changedFiles);
|
|
341
|
+
|
|
342
|
+
// IMPORTANT: When using --changed-files, skip include patterns
|
|
343
|
+
// Git already filtered the files, we only need to apply excludes
|
|
344
|
+
const skipIncludePatterns = cliOptions.changedFiles;
|
|
296
345
|
|
|
297
346
|
// 1. Apply config include patterns first (medium priority)
|
|
298
|
-
if (config.include && config.include.length > 0) {
|
|
347
|
+
if (!skipIncludePatterns && config.include && config.include.length > 0) {
|
|
299
348
|
filteredFiles = this.applyIncludePatterns(filteredFiles, config.include, debug);
|
|
300
349
|
if (debug) console.log(`đ [DEBUG] After config include: ${filteredFiles.length} files`);
|
|
350
|
+
} else if (skipIncludePatterns && debug) {
|
|
351
|
+
console.log(`đ [DEBUG] Skipping config include patterns (--changed-files mode)`);
|
|
301
352
|
}
|
|
302
353
|
|
|
303
354
|
// 2. Apply CLI include overrides (highest priority - completely overrides config)
|
|
304
|
-
if (cliOptions.include) {
|
|
355
|
+
if (!skipIncludePatterns && cliOptions.include) {
|
|
305
356
|
// CLI include completely replaces config include - start fresh from all files
|
|
306
357
|
filteredFiles = this.applyIncludePatterns([...files], cliOptions.include, debug);
|
|
307
358
|
}
|
package/core/git-utils.js
CHANGED
|
@@ -21,13 +21,98 @@ class GitUtils {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Detect if running in PR context (GitHub Actions, GitLab CI, etc.)
|
|
26
|
+
* @returns {Object|null} PR context info or null
|
|
27
|
+
*/
|
|
28
|
+
static detectPRContext() {
|
|
29
|
+
// GitHub Actions
|
|
30
|
+
if (process.env.GITHUB_EVENT_NAME === 'pull_request' ||
|
|
31
|
+
process.env.GITHUB_EVENT_NAME === 'pull_request_target') {
|
|
32
|
+
return {
|
|
33
|
+
provider: 'github',
|
|
34
|
+
baseBranch: process.env.GITHUB_BASE_REF,
|
|
35
|
+
headBranch: process.env.GITHUB_HEAD_REF,
|
|
36
|
+
prNumber: process.env.GITHUB_REF ? process.env.GITHUB_REF.match(/refs\/pull\/(\d+)\/merge/)?.[1] : null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GitLab CI
|
|
41
|
+
if (process.env.CI_MERGE_REQUEST_ID) {
|
|
42
|
+
return {
|
|
43
|
+
provider: 'gitlab',
|
|
44
|
+
baseBranch: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
45
|
+
headBranch: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
46
|
+
prNumber: process.env.CI_MERGE_REQUEST_IID
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get smart base reference for diff
|
|
55
|
+
* Auto-detects PR context or falls back to common base branches
|
|
56
|
+
* @param {string} cwd - Working directory
|
|
57
|
+
* @returns {string} Base reference for git diff
|
|
58
|
+
*/
|
|
59
|
+
static getSmartBaseRef(cwd = process.cwd()) {
|
|
60
|
+
if (!this.isGitRepository(cwd)) {
|
|
61
|
+
throw new Error('Not a git repository');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check PR context first
|
|
65
|
+
const prContext = this.detectPRContext();
|
|
66
|
+
if (prContext && prContext.baseBranch) {
|
|
67
|
+
const candidates = [
|
|
68
|
+
`origin/${prContext.baseBranch}`,
|
|
69
|
+
`upstream/${prContext.baseBranch}`,
|
|
70
|
+
prContext.baseBranch
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
try {
|
|
75
|
+
execSync(`git rev-parse --verify ${candidate}`, { cwd, stdio: 'ignore' });
|
|
76
|
+
return candidate;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Continue to next candidate
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback to common base branches
|
|
84
|
+
const fallbackBranches = [
|
|
85
|
+
'origin/main',
|
|
86
|
+
'origin/master',
|
|
87
|
+
'origin/develop',
|
|
88
|
+
'upstream/main',
|
|
89
|
+
'upstream/master',
|
|
90
|
+
'main',
|
|
91
|
+
'master',
|
|
92
|
+
'develop'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const branch of fallbackBranches) {
|
|
96
|
+
try {
|
|
97
|
+
execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
|
|
98
|
+
return branch;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Continue to next candidate
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Last resort: use HEAD (uncommitted changes only)
|
|
105
|
+
return 'HEAD';
|
|
106
|
+
}
|
|
107
|
+
|
|
24
108
|
/**
|
|
25
109
|
* Get list of changed files compared to base reference
|
|
26
|
-
* @param {string} baseRef - Base git reference (e.g., 'origin/main')
|
|
110
|
+
* @param {string|null} baseRef - Base git reference (e.g., 'origin/main'). If null, auto-detect.
|
|
27
111
|
* @param {string} cwd - Working directory
|
|
112
|
+
* @param {boolean} includeUncommitted - Include uncommitted changes
|
|
28
113
|
* @returns {string[]} Array of changed file paths
|
|
29
114
|
*/
|
|
30
|
-
static getChangedFiles(baseRef =
|
|
115
|
+
static getChangedFiles(baseRef = null, cwd = process.cwd(), includeUncommitted = true) {
|
|
31
116
|
if (!this.isGitRepository(cwd)) {
|
|
32
117
|
throw new Error('Not a git repository');
|
|
33
118
|
}
|
|
@@ -35,15 +120,40 @@ class GitUtils {
|
|
|
35
120
|
try {
|
|
36
121
|
// Get git root directory
|
|
37
122
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
123
|
+
|
|
124
|
+
// Auto-detect base ref if not provided
|
|
125
|
+
const actualBaseRef = baseRef || this.getSmartBaseRef(cwd);
|
|
126
|
+
|
|
127
|
+
const allFiles = new Set();
|
|
128
|
+
|
|
129
|
+
// Get committed changes
|
|
130
|
+
if (actualBaseRef !== 'HEAD') {
|
|
131
|
+
// Use two-dot diff for branch comparison (what's new in this branch)
|
|
132
|
+
const command = `git diff --name-only ${actualBaseRef}..HEAD`;
|
|
133
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
134
|
+
|
|
135
|
+
output
|
|
136
|
+
.split('\n')
|
|
137
|
+
.filter(file => file.trim() !== '')
|
|
138
|
+
.forEach(file => allFiles.add(path.resolve(gitRoot, file)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get uncommitted changes if requested
|
|
142
|
+
if (includeUncommitted) {
|
|
143
|
+
const uncommittedCommand = 'git diff --name-only HEAD';
|
|
144
|
+
try {
|
|
145
|
+
const uncommittedOutput = execSync(uncommittedCommand, { cwd: gitRoot, encoding: 'utf8' });
|
|
146
|
+
uncommittedOutput
|
|
147
|
+
.split('\n')
|
|
148
|
+
.filter(file => file.trim() !== '')
|
|
149
|
+
.forEach(file => allFiles.add(path.resolve(gitRoot, file)));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Ignore errors for uncommitted changes (might be empty)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Filter to only existing files
|
|
156
|
+
return Array.from(allFiles).filter(file => fs.existsSync(file));
|
|
47
157
|
} catch (error) {
|
|
48
158
|
throw new Error(`Failed to get changed files: ${error.message}`);
|
|
49
159
|
}
|