@sun-asterisk/sunlint 1.3.50 → 1.3.52
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.
|
@@ -154,42 +154,67 @@ class SummaryReportService {
|
|
|
154
154
|
const gitInfo = this.getGitInfo(options.cwd);
|
|
155
155
|
|
|
156
156
|
// Override with environment variables if available (from CI/CD)
|
|
157
|
+
// Supports GitHub Actions, GitLab CI, and Bitbucket Pipelines
|
|
157
158
|
const repository_url = process.env.GITHUB_REPOSITORY
|
|
158
159
|
? `https://github.com/${process.env.GITHUB_REPOSITORY}`
|
|
159
|
-
:
|
|
160
|
+
: process.env.CI_PROJECT_URL // GitLab CI: full URL available directly
|
|
161
|
+
|| (process.env.BITBUCKET_WORKSPACE && process.env.BITBUCKET_REPO_SLUG
|
|
162
|
+
? `https://bitbucket.org/${process.env.BITBUCKET_WORKSPACE}/${process.env.BITBUCKET_REPO_SLUG}`
|
|
163
|
+
: null)
|
|
164
|
+
|| gitInfo.repository_url;
|
|
160
165
|
|
|
161
166
|
const repository_name = process.env.GITHUB_REPOSITORY
|
|
162
167
|
? process.env.GITHUB_REPOSITORY.split('/')[1]
|
|
163
|
-
:
|
|
168
|
+
: process.env.CI_PROJECT_NAME // GitLab CI
|
|
169
|
+
|| process.env.BITBUCKET_REPO_SLUG // Bitbucket Pipelines
|
|
170
|
+
|| (gitInfo.repository_name || null);
|
|
164
171
|
|
|
165
|
-
const branch = process.env.GITHUB_REF_NAME
|
|
166
|
-
|
|
172
|
+
const branch = process.env.GITHUB_REF_NAME
|
|
173
|
+
|| process.env.CI_COMMIT_REF_NAME // GitLab CI
|
|
174
|
+
|| process.env.BITBUCKET_BRANCH // Bitbucket Pipelines
|
|
175
|
+
|| gitInfo.branch;
|
|
167
176
|
|
|
168
|
-
|
|
177
|
+
const commit_hash = process.env.GITHUB_SHA
|
|
178
|
+
|| process.env.CI_COMMIT_SHA // GitLab CI
|
|
179
|
+
|| process.env.BITBUCKET_COMMIT // Bitbucket Pipelines
|
|
180
|
+
|| gitInfo.commit_hash;
|
|
181
|
+
|
|
182
|
+
// Get commit details from CI context or git
|
|
169
183
|
const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
|
|
170
184
|
|| (process.env.GITHUB_EVENT_PATH
|
|
171
185
|
? this._getGitHubEventData('head_commit.message')
|
|
172
186
|
: null)
|
|
173
|
-
||
|
|
187
|
+
|| process.env.CI_COMMIT_MESSAGE // GitLab CI
|
|
188
|
+
|| gitInfo.commit_message; // Bitbucket: no env var, fallback to git
|
|
174
189
|
|
|
175
190
|
const author_email = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL
|
|
176
191
|
|| (process.env.GITHUB_EVENT_PATH
|
|
177
192
|
? this._getGitHubEventData('head_commit.author.email')
|
|
178
193
|
: null)
|
|
179
|
-
||
|
|
194
|
+
|| process.env.GITLAB_USER_EMAIL // GitLab CI
|
|
195
|
+
|| gitInfo.author_email; // Bitbucket: no env var, fallback to git
|
|
180
196
|
|
|
181
197
|
const author_name = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME
|
|
182
198
|
|| (process.env.GITHUB_EVENT_PATH
|
|
183
199
|
? this._getGitHubEventData('head_commit.author.name')
|
|
184
200
|
: null)
|
|
185
|
-
||
|
|
201
|
+
|| process.env.GITLAB_USER_NAME // GitLab CI
|
|
202
|
+
|| gitInfo.author_name; // Bitbucket: no env var, fallback to git
|
|
186
203
|
|
|
187
|
-
// Get PR number from
|
|
204
|
+
// Get PR/MR number from CI context or git
|
|
188
205
|
let pr_number = null;
|
|
189
206
|
if (process.env.GITHUB_EVENT_PATH) {
|
|
190
207
|
pr_number = this._getGitHubEventData('pull_request.number')
|
|
191
208
|
|| this._getGitHubEventData('number');
|
|
192
209
|
}
|
|
210
|
+
// GitLab CI: Merge Request IID
|
|
211
|
+
if (!pr_number && process.env.CI_MERGE_REQUEST_IID) {
|
|
212
|
+
pr_number = parseInt(process.env.CI_MERGE_REQUEST_IID);
|
|
213
|
+
}
|
|
214
|
+
// Bitbucket Pipelines: Pull Request ID
|
|
215
|
+
if (!pr_number && process.env.BITBUCKET_PR_ID) {
|
|
216
|
+
pr_number = parseInt(process.env.BITBUCKET_PR_ID);
|
|
217
|
+
}
|
|
193
218
|
pr_number = pr_number || gitInfo.pr_number;
|
|
194
219
|
|
|
195
220
|
// Get project path (for mono-repo support)
|
package/core/tui-select.js
CHANGED
|
@@ -8,12 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const readline = require('readline');
|
|
12
|
-
|
|
13
11
|
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
14
12
|
const ansi = {
|
|
15
|
-
saveCursor: '\x1b7',
|
|
16
|
-
restoreCursor: '\x1b8',
|
|
17
13
|
clearToEnd: '\x1b[J',
|
|
18
14
|
clearLine: '\x1b[2K\r',
|
|
19
15
|
hideCursor: '\x1b[?25l',
|
|
@@ -108,13 +104,13 @@ function tuiSelect({ question, options, labels = {}, badges = {}, defaultIndex =
|
|
|
108
104
|
return badges[value] ? c('green', ` ${badges[value]}`) : '';
|
|
109
105
|
}
|
|
110
106
|
|
|
107
|
+
// question(1) + hint(1) + blank(1) + options(N)
|
|
108
|
+
const totalLines = 3 + options.length;
|
|
109
|
+
|
|
111
110
|
function render(firstRender = false) {
|
|
112
|
-
if (firstRender) {
|
|
113
|
-
//
|
|
114
|
-
process.stdout.write(ansi.
|
|
115
|
-
} else {
|
|
116
|
-
// Restore to saved position, then clear everything below
|
|
117
|
-
process.stdout.write(ansi.restoreCursor + ansi.clearToEnd);
|
|
111
|
+
if (!firstRender) {
|
|
112
|
+
// Move cursor up to where the prompt started, then clear to end
|
|
113
|
+
process.stdout.write(`\x1b[${totalLines}A\r` + ansi.clearToEnd);
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
// Question line
|
|
@@ -177,7 +173,7 @@ function tuiSelect({ question, options, labels = {}, badges = {}, defaultIndex =
|
|
|
177
173
|
const label = renderLabel(value);
|
|
178
174
|
const badge = renderBadge(value);
|
|
179
175
|
process.stdout.write(
|
|
180
|
-
|
|
176
|
+
`\x1b[${totalLines}A\r` + ansi.clearToEnd +
|
|
181
177
|
` ${c('green', '✔')} ${bold(question)} ${c('cyan', label)}${badge}\n\n`
|
|
182
178
|
);
|
|
183
179
|
|
|
@@ -186,7 +182,7 @@ function tuiSelect({ question, options, labels = {}, badges = {}, defaultIndex =
|
|
|
186
182
|
// Esc / Ctrl+C
|
|
187
183
|
cleanup();
|
|
188
184
|
process.stdin.removeListener('data', onKeypress);
|
|
189
|
-
process.stdout.write(
|
|
185
|
+
process.stdout.write(`\x1b[${totalLines}A\r` + ansi.clearToEnd + '\n');
|
|
190
186
|
reject(new Error('Cancelled by user'));
|
|
191
187
|
}
|
|
192
188
|
}
|
package/core/upload-service.js
CHANGED
|
@@ -221,9 +221,10 @@ class UploadService {
|
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
223
|
* Get OIDC token from environment variables (for CI authentication)
|
|
224
|
+
* Supports both GitHub Actions and GitLab CI
|
|
224
225
|
*/
|
|
225
226
|
getOidcToken() {
|
|
226
|
-
// Try
|
|
227
|
+
// Try GitHub Actions OIDC token (requires API call)
|
|
227
228
|
if (process.env.GITHUB_ACTIONS && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
|
|
228
229
|
try {
|
|
229
230
|
const githubToken = this.requestGitHubOidcToken();
|
|
@@ -235,9 +236,32 @@ class UploadService {
|
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
//
|
|
239
|
+
// Try GitLab CI OIDC token (available directly as env var)
|
|
240
|
+
if (process.env.GITLAB_CI) {
|
|
241
|
+
try {
|
|
242
|
+
const gitlabToken = this.requestGitLabOidcToken();
|
|
243
|
+
if (gitlabToken) {
|
|
244
|
+
return gitlabToken;
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.log(chalk.yellow(`⚠️ Failed to get GitLab OIDC token: ${error.message}`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Try API_SECRET_KEY (for Bitbucket Pipelines or any CI without OIDC)
|
|
252
|
+
const apiKey = process.env.SUNLINT_API_KEY || process.env.API_SECRET_KEY;
|
|
253
|
+
if (apiKey) {
|
|
254
|
+
console.log(chalk.green('🔑 Using API key authentication'));
|
|
255
|
+
return apiKey;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Warn if running in CI without any authentication
|
|
239
259
|
if (process.env.GITHUB_ACTIONS) {
|
|
240
260
|
console.log(chalk.yellow('⚠️ Running in GitHub Actions but no OIDC token available. Upload may require authentication.'));
|
|
261
|
+
} else if (process.env.GITLAB_CI) {
|
|
262
|
+
console.log(chalk.yellow('⚠️ Running in GitLab CI but no OIDC token available. Add id_tokens to your .gitlab-ci.yml.'));
|
|
263
|
+
} else if (process.env.BITBUCKET_PIPELINE_UUID) {
|
|
264
|
+
console.log(chalk.yellow('⚠️ Running in Bitbucket Pipelines but no API key found. Set SUNLINT_API_KEY in repository variables.'));
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
return null;
|
|
@@ -249,34 +273,53 @@ class UploadService {
|
|
|
249
273
|
requestGitHubOidcToken() {
|
|
250
274
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
251
275
|
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
252
|
-
|
|
276
|
+
|
|
253
277
|
if (!requestToken || !requestUrl) {
|
|
254
278
|
throw new Error('Missing GitHub OIDC request credentials');
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
try {
|
|
258
282
|
// Use curl to request OIDC token from GitHub with specific audience
|
|
259
|
-
const curlCommand = `curl -H "Authorization: bearer ${requestToken}" "${requestUrl}&audience=coding-standards-report-api"`;
|
|
260
|
-
|
|
261
|
-
const response = execSync(curlCommand, {
|
|
283
|
+
const curlCommand = `curl -s -H "Authorization: bearer ${requestToken}" "${requestUrl}&audience=coding-standards-report-api"`;
|
|
284
|
+
|
|
285
|
+
const response = execSync(curlCommand, {
|
|
262
286
|
encoding: 'utf8',
|
|
263
287
|
timeout: 10000, // 10 second timeout
|
|
264
288
|
stdio: 'pipe'
|
|
265
289
|
});
|
|
266
290
|
|
|
267
291
|
const responseData = JSON.parse(response);
|
|
268
|
-
|
|
292
|
+
|
|
269
293
|
if (responseData.value) {
|
|
270
294
|
return responseData.value;
|
|
271
295
|
} else {
|
|
272
296
|
throw new Error('No token in response');
|
|
273
297
|
}
|
|
274
|
-
|
|
298
|
+
|
|
275
299
|
} catch (error) {
|
|
276
300
|
throw new Error(`GitHub OIDC request failed: ${error.message}`);
|
|
277
301
|
}
|
|
278
302
|
}
|
|
279
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Get OIDC token from GitLab CI
|
|
306
|
+
* GitLab provides the JWT directly as an environment variable (no API call needed).
|
|
307
|
+
* Requires id_tokens configuration in .gitlab-ci.yml:
|
|
308
|
+
* id_tokens:
|
|
309
|
+
* SUNLINT_ID_TOKEN:
|
|
310
|
+
* aud: coding-standards-report-api
|
|
311
|
+
*/
|
|
312
|
+
requestGitLabOidcToken() {
|
|
313
|
+
// GitLab CI id_tokens: custom-named token with specific audience
|
|
314
|
+
const token = process.env.SUNLINT_ID_TOKEN || process.env.CI_JOB_JWT_V2 || process.env.CI_JOB_JWT;
|
|
315
|
+
|
|
316
|
+
if (!token) {
|
|
317
|
+
throw new Error('No GitLab OIDC token found. Configure id_tokens in .gitlab-ci.yml');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
|
|
280
323
|
/**
|
|
281
324
|
* Generate idempotency key for safe retries
|
|
282
325
|
*/
|
|
@@ -284,20 +327,35 @@ class UploadService {
|
|
|
284
327
|
// Create deterministic key based on file content and timestamp
|
|
285
328
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
286
329
|
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
287
|
-
|
|
330
|
+
|
|
288
331
|
// Include CI context for uniqueness across environments and attempts
|
|
289
|
-
const ciContext =
|
|
290
|
-
|
|
291
|
-
'local';
|
|
292
|
-
|
|
332
|
+
const ciContext = this.buildCiContext();
|
|
333
|
+
|
|
293
334
|
// Create hash from content + date + CI context + run attempt
|
|
294
335
|
const hashInput = `${fileContent}-${timestamp}-${ciContext}`;
|
|
295
336
|
const hash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
296
|
-
|
|
337
|
+
|
|
297
338
|
// Return first 32 characters for reasonable key length
|
|
298
339
|
return `sunlint-${hash.substring(0, 24)}`;
|
|
299
340
|
}
|
|
300
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Build CI context string for idempotency key generation
|
|
344
|
+
* Supports GitHub Actions and GitLab CI
|
|
345
|
+
*/
|
|
346
|
+
buildCiContext() {
|
|
347
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
348
|
+
return `${process.env.GITHUB_REPOSITORY || 'unknown'}-${process.env.GITHUB_RUN_ID || 'local'}-${process.env.GITHUB_RUN_ATTEMPT || '1'}`;
|
|
349
|
+
}
|
|
350
|
+
if (process.env.GITLAB_CI) {
|
|
351
|
+
return `${process.env.CI_PROJECT_PATH || 'unknown'}-${process.env.CI_PIPELINE_ID || 'local'}-${process.env.CI_JOB_ID || '1'}`;
|
|
352
|
+
}
|
|
353
|
+
if (process.env.BITBUCKET_PIPELINE_UUID) {
|
|
354
|
+
return `${process.env.BITBUCKET_REPO_SLUG || 'unknown'}-${process.env.BITBUCKET_BUILD_NUMBER || 'local'}-${process.env.BITBUCKET_STEP_UUID || '1'}`;
|
|
355
|
+
}
|
|
356
|
+
return 'local';
|
|
357
|
+
}
|
|
358
|
+
|
|
301
359
|
/**
|
|
302
360
|
* Validate API endpoint accessibility
|
|
303
361
|
*/
|
|
Binary file
|
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
# CI Report Upload Flow: GitHub Actions / GitLab CI / Bitbucket Pipelines
|
|
2
|
+
|
|
3
|
+
Tài liệu chi tiết luồng hoạt động khi chạy SunLint trên CI/CD,
|
|
4
|
+
từ lúc scan code đến khi report được lưu vào database.
|
|
5
|
+
Hỗ trợ 3 nền tảng: GitHub Actions, GitLab CI, Bitbucket Pipelines.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Workflow YAML mẫu
|
|
10
|
+
|
|
11
|
+
### GitHub Actions
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
name: Sunlint Code Quality Check
|
|
15
|
+
on: [pull_request]
|
|
16
|
+
jobs:
|
|
17
|
+
sunlint:
|
|
18
|
+
runs-on: "macos-latest"
|
|
19
|
+
permissions:
|
|
20
|
+
id-token: write # BẮT BUỘC - cho phép request OIDC token
|
|
21
|
+
contents: read
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v3
|
|
24
|
+
- uses: actions/setup-node@v3
|
|
25
|
+
with:
|
|
26
|
+
node-version: "21"
|
|
27
|
+
- run: npm install -g @sun-asterisk/sunlint
|
|
28
|
+
- run: sunlint --specific --languages=dart --input=lib --output-summary=report.json --upload-report --quiet
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### GitLab CI
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
sunlint:
|
|
35
|
+
image: node:21
|
|
36
|
+
stage: test
|
|
37
|
+
# Cấu hình OIDC token cho SunLint
|
|
38
|
+
id_tokens:
|
|
39
|
+
SUNLINT_ID_TOKEN:
|
|
40
|
+
aud: coding-standards-report-api
|
|
41
|
+
script:
|
|
42
|
+
- npm install -g @sun-asterisk/sunlint
|
|
43
|
+
- sunlint --specific --languages=dart --input=lib --output-summary=report.json --upload-report --quiet
|
|
44
|
+
rules:
|
|
45
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Bitbucket Pipelines
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
pipelines:
|
|
52
|
+
pull-requests:
|
|
53
|
+
'**':
|
|
54
|
+
- step:
|
|
55
|
+
name: SunLint Code Quality Check
|
|
56
|
+
image: node:21
|
|
57
|
+
script:
|
|
58
|
+
- npm install -g @sun-asterisk/sunlint
|
|
59
|
+
- sunlint --specific --languages=dart --input=lib --output-summary=report.json --upload-report --quiet
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **Lưu ý Bitbucket**: Dùng `SUNLINT_API_KEY` (API key) thay vì OIDC.
|
|
63
|
+
> Cấu hình trong Repository Settings → Repository variables → `SUNLINT_API_KEY` (đánh dấu Secured).
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Tổng quan 8 phases
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
PHASE 1: Khởi tạo CLI
|
|
71
|
+
PHASE 2: Chọn rules & files
|
|
72
|
+
PHASE 3: Phân tích code
|
|
73
|
+
PHASE 4: Tạo report.json
|
|
74
|
+
PHASE 5: Upload report (xác thực)
|
|
75
|
+
PHASE 6: Server nhận & xác thực
|
|
76
|
+
PHASE 7: Lưu vào Database
|
|
77
|
+
PHASE 8: CI kết thúc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Data flow tổng quát
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐
|
|
84
|
+
│ CI │ │ SunLint │ │ OIDC │ │ Dashboard│ │PostgreSQL│
|
|
85
|
+
│ Runner │───>│ CLI │───>│ Provider │ │ API │ │ DB │
|
|
86
|
+
│ │ │ │ │ /API Key │ │ │ │ │
|
|
87
|
+
└──────────┘ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └────┬─────┘
|
|
88
|
+
│ │ │ │
|
|
89
|
+
1. Scan code │ │ │
|
|
90
|
+
2. Tạo report.json │ │ │
|
|
91
|
+
3. Lấy auth token ─────>│ │ │
|
|
92
|
+
4. Nhận JWT/API key <───┘ │ │
|
|
93
|
+
5. POST report.json + token ───────────>│ │
|
|
94
|
+
6. Verify token ─────────────────────── │ │
|
|
95
|
+
7. Token hợp lệ │ │
|
|
96
|
+
8. Parse report.json │ │
|
|
97
|
+
9. INSERT repository ───────────────────┼──────────────>│
|
|
98
|
+
10. INSERT report ───────────────────────┼──────────────>│
|
|
99
|
+
11. INSERT violations ───────────────────┼──────────────>│
|
|
100
|
+
12. Invalidate cache │ │
|
|
101
|
+
13. HTTP 201 <───────────────────────────┘ │
|
|
102
|
+
14. CI exit(0) │
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## PHASE 1: Khởi tạo CLI
|
|
108
|
+
|
|
109
|
+
**File**: `core/cli-action-handler.js` - `execute()` (line 42-141)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
cli.js → CliActionHandler.execute()
|
|
113
|
+
├── displayModernBanner() → "SunLint v1.x.x"
|
|
114
|
+
├── handleShortcuts() → check --version, --list-rules
|
|
115
|
+
└── loadConfiguration() → ConfigManager đọc .sunlint.json (nếu có)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Validate input (line 316-390)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
validateInput(config)
|
|
122
|
+
├── Check --engine option hợp lệ
|
|
123
|
+
├── Check --upload-report CẦN --output-summary
|
|
124
|
+
│ └── options.uploadReport chưa có URL?
|
|
125
|
+
│ → Gán default: "https://coding-standards-report.sun-asterisk.vn/api/reports"
|
|
126
|
+
└── Check --input=lib tồn tại
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## PHASE 2: Chọn rules & files
|
|
132
|
+
|
|
133
|
+
### Select rules (line 63)
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
ruleSelectionService.selectRules(config, options)
|
|
137
|
+
└── --specific --languages=dart
|
|
138
|
+
→ Chọn tất cả rules hỗ trợ Dart:
|
|
139
|
+
C001, C002, C003, C005, C006, C008, C010...
|
|
140
|
+
D001, D002, D003, D004, D005...
|
|
141
|
+
S001, S002, S003...
|
|
142
|
+
(khoảng 80-100 rules cho Dart)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### File targeting (line 71)
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
fileTargetingService.getTargetFiles(["lib"], config, options)
|
|
149
|
+
└── Scan thư mục lib/
|
|
150
|
+
→ Tìm tất cả file *.dart
|
|
151
|
+
→ Trả về danh sách: ["lib/main.dart", "lib/app.dart", ...]
|
|
152
|
+
→ KHÔNG filter bởi git diff → QUÉT TOÀN BỘ CODE
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **Lưu ý**: Với lệnh `--input=lib` (KHÔNG có `--changed-files`),
|
|
156
|
+
> sunlint quét **TOÀN BỘ** code trong thư mục `lib/`, không chỉ phần diff.
|
|
157
|
+
> Muốn chỉ quét diff, thêm `--changed-files`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## PHASE 3: Phân tích code
|
|
162
|
+
|
|
163
|
+
### Khởi tạo engines (line 146-188)
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
runModernAnalysis(rulesToRun, files, config)
|
|
167
|
+
└── orchestrator.initialize({
|
|
168
|
+
enabledEngines: ["heuristic"], ← Dart chỉ dùng heuristic
|
|
169
|
+
heuristicConfig: {
|
|
170
|
+
targetFiles: ["lib/*.dart"],
|
|
171
|
+
projectPath: "/path/to/project",
|
|
172
|
+
maxSemanticFiles: 1000
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Chạy phân tích
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
orchestrator.analyze(files, rulesToRun, options)
|
|
181
|
+
│
|
|
182
|
+
├── Với mỗi file .dart:
|
|
183
|
+
│ ├── HeuristicEngine: pattern matching (regex)
|
|
184
|
+
│ └── DartAnalyzer: sunlint-dart-macos binary (nếu spawn OK)
|
|
185
|
+
│ ├── Binary chạy được? → Deep AST analysis (nhiều violations)
|
|
186
|
+
│ └── Binary FAIL? → Fallback regex (ít violations)
|
|
187
|
+
│
|
|
188
|
+
└── Kết quả: { results: [...], summary: { total, errors, warnings } }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## PHASE 4: Tạo report.json
|
|
194
|
+
|
|
195
|
+
### Output results
|
|
196
|
+
|
|
197
|
+
**File**: `core/output-service.js` - `outputResults()` (line 38-129)
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
outputService.outputResults(results, options, metadata)
|
|
201
|
+
│
|
|
202
|
+
├── generateReport(results) → Formatted text + violations array
|
|
203
|
+
│
|
|
204
|
+
├── console.log(report.formatted) ← ĐÂY LÀ CHỖ BỊ KILL nếu quá nhiều output
|
|
205
|
+
│ --quiet sẽ skip dòng này
|
|
206
|
+
│
|
|
207
|
+
└── options.outputSummary = "report.json" → Vào bước tiếp theo
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Tạo summary report
|
|
211
|
+
|
|
212
|
+
**File**: `core/summary-report-service.js` - `generateSummaryReport()` (line 152-335)
|
|
213
|
+
|
|
214
|
+
#### Thu thập Git info (line 37-143)
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
getGitInfo()
|
|
218
|
+
├── git config --get remote.origin.url → "https://github.com/org/repo"
|
|
219
|
+
├── git rev-parse --abbrev-ref HEAD → "feature/xyz"
|
|
220
|
+
├── git rev-parse HEAD → "abc123..."
|
|
221
|
+
├── git log -1 --pretty=%B → "feat: add login page"
|
|
222
|
+
├── git log -1 --pretty=%ae → "dev@sun-asterisk.com"
|
|
223
|
+
├── git log -1 --pretty=%an → "Nguyen Van A"
|
|
224
|
+
└── Trích PR number từ commit message hoặc branch name
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Trên CI, override bằng env vars (theo thứ tự ưu tiên: GitHub → GitLab → Bitbucket → git):
|
|
228
|
+
|
|
229
|
+
| Trường | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
230
|
+
|--------|----------------|-----------|---------------------|
|
|
231
|
+
| repository_url | `GITHUB_REPOSITORY` (ghép) | `CI_PROJECT_URL` | `BITBUCKET_WORKSPACE` + `BITBUCKET_REPO_SLUG` (ghép) |
|
|
232
|
+
| repository_name | `GITHUB_REPOSITORY` (split) | `CI_PROJECT_NAME` | `BITBUCKET_REPO_SLUG` |
|
|
233
|
+
| branch | `GITHUB_REF_NAME` | `CI_COMMIT_REF_NAME` | `BITBUCKET_BRANCH` |
|
|
234
|
+
| commit_hash | `GITHUB_SHA` | `CI_COMMIT_SHA` | `BITBUCKET_COMMIT` |
|
|
235
|
+
| commit_message | `GITHUB_EVENT_PATH` (JSON) | `CI_COMMIT_MESSAGE` | git fallback |
|
|
236
|
+
| author_email | `GITHUB_EVENT_PATH` (JSON) | `GITLAB_USER_EMAIL` | git fallback |
|
|
237
|
+
| author_name | `GITHUB_EVENT_PATH` (JSON) | `GITLAB_USER_NAME` | git fallback |
|
|
238
|
+
| PR/MR number | `GITHUB_EVENT_PATH` (JSON) | `CI_MERGE_REQUEST_IID` | `BITBUCKET_PR_ID` |
|
|
239
|
+
|
|
240
|
+
> **Bitbucket**: Không cung cấp commit_message, author_email, author_name qua env vars.
|
|
241
|
+
> SunLint tự lấy từ `git log` trên runner (luôn có vì đã checkout code).
|
|
242
|
+
|
|
243
|
+
#### Build report JSON (line 152-335)
|
|
244
|
+
|
|
245
|
+
Gom violations theo rule:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
violations[] → { C006: 150, C010: 89, D008: 200, ... }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Cấu trúc file `report.json`:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"repository_url": "https://github.com/org/my-flutter-app",
|
|
256
|
+
"repository_name": "my-flutter-app",
|
|
257
|
+
"project_path": null,
|
|
258
|
+
"branch": "feature/login",
|
|
259
|
+
"commit_hash": "abc123def456...",
|
|
260
|
+
"commit_message": "feat: add login page (#42)",
|
|
261
|
+
"author_email": "dev@sun-asterisk.com",
|
|
262
|
+
"author_name": "Nguyen Van A",
|
|
263
|
+
"pr_number": 42,
|
|
264
|
+
"score": 65,
|
|
265
|
+
"total_violations": 4293,
|
|
266
|
+
"error_count": 12,
|
|
267
|
+
"warning_count": 4281,
|
|
268
|
+
"info_count": 0,
|
|
269
|
+
"lines_of_code": 50000,
|
|
270
|
+
"files_analyzed": 320,
|
|
271
|
+
"sunlint_version": "1.3.25",
|
|
272
|
+
"analysis_duration_ms": 15000,
|
|
273
|
+
"scoring_mode": "project",
|
|
274
|
+
"violations": [
|
|
275
|
+
{ "rule_code": "D008", "count": 200, "severity": "warning" },
|
|
276
|
+
{ "rule_code": "C006", "count": 150, "severity": "warning" },
|
|
277
|
+
{ "rule_code": "C010", "count": 89, "severity": "warning" }
|
|
278
|
+
],
|
|
279
|
+
"metadata": {
|
|
280
|
+
"generated_at": "2026-03-12T10:30:00.000Z",
|
|
281
|
+
"tool": "SunLint",
|
|
282
|
+
"version": "1.3.25"
|
|
283
|
+
},
|
|
284
|
+
"quality": {
|
|
285
|
+
"score": 65,
|
|
286
|
+
"grade": "C",
|
|
287
|
+
"metrics": { "errors": 12, "warnings": 4281, "linesOfCode": 50000 }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
fs.writeFileSync("report.json", JSON.stringify(summaryReport))
|
|
294
|
+
→ "Summary report saved to: report.json"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## PHASE 5: Upload report
|
|
300
|
+
|
|
301
|
+
### Kích hoạt upload
|
|
302
|
+
|
|
303
|
+
**File**: `core/output-service.js` (line 110-112)
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
options.uploadReport = "https://coding-standards-report.sun-asterisk.vn/api/reports"
|
|
307
|
+
→ handleUploadReport("report.json", apiUrl, options)
|
|
308
|
+
→ uploadService.uploadReportToApi("report.json", apiUrl)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Lấy auth token
|
|
312
|
+
|
|
313
|
+
**File**: `core/upload-service.js` - `getOidcToken()` (line 226-280)
|
|
314
|
+
|
|
315
|
+
Thứ tự thử xác thực:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
getOidcToken()
|
|
319
|
+
│
|
|
320
|
+
├── 1. GitHub Actions OIDC?
|
|
321
|
+
│ Check: GITHUB_ACTIONS + ACTIONS_ID_TOKEN_REQUEST_TOKEN + ACTIONS_ID_TOKEN_REQUEST_URL
|
|
322
|
+
│ → requestGitHubOidcToken() (gọi API GitHub để lấy JWT)
|
|
323
|
+
│
|
|
324
|
+
├── 2. GitLab CI OIDC?
|
|
325
|
+
│ Check: GITLAB_CI
|
|
326
|
+
│ → requestGitLabOidcToken() (đọc env var SUNLINT_ID_TOKEN)
|
|
327
|
+
│
|
|
328
|
+
├── 3. API Key? (Bitbucket hoặc bất kỳ CI nào)
|
|
329
|
+
│ Check: SUNLINT_API_KEY || API_SECRET_KEY
|
|
330
|
+
│ → return apiKey
|
|
331
|
+
│
|
|
332
|
+
└── 4. Không có gì → return null + warning message
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### GitHub Actions: Lấy OIDC token qua API
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
requestGitHubOidcToken()
|
|
339
|
+
│
|
|
340
|
+
│ curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN"
|
|
341
|
+
│ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=coding-standards-report-api"
|
|
342
|
+
│
|
|
343
|
+
│ HTTPS request tới GitHub OIDC Provider
|
|
344
|
+
│
|
|
345
|
+
└── Response: { "value": "eyJhbGciOiJSUzI1NiIs..." }
|
|
346
|
+
│
|
|
347
|
+
└── JWT Token chứa:
|
|
348
|
+
{
|
|
349
|
+
"iss": "https://token.actions.githubusercontent.com",
|
|
350
|
+
"aud": "coding-standards-report-api",
|
|
351
|
+
"sub": "repo:org/my-flutter-app:ref:refs/heads/feature/login",
|
|
352
|
+
"repository": "org/my-flutter-app",
|
|
353
|
+
"repository_owner": "sun-asterisk",
|
|
354
|
+
"actor": "nguyen-van-a",
|
|
355
|
+
"exp": 1741689600 ← hết hạn sau ~5 phút
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### GitLab CI: Token có sẵn trong env var
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
requestGitLabOidcToken()
|
|
363
|
+
│
|
|
364
|
+
└── return process.env.SUNLINT_ID_TOKEN
|
|
365
|
+
│
|
|
366
|
+
└── JWT Token chứa:
|
|
367
|
+
{
|
|
368
|
+
"iss": "https://gitlab.com",
|
|
369
|
+
"aud": "coding-standards-report-api",
|
|
370
|
+
"sub": "project_path:group/repo:ref_type:branch:ref:main",
|
|
371
|
+
"project_path": "group/repo",
|
|
372
|
+
"namespace_path": "group",
|
|
373
|
+
"user_login": "nguyen-van-a",
|
|
374
|
+
"pipeline_id": "12345"
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### Bitbucket Pipelines: API key từ env var
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
Không có OIDC → Fallback sang API key:
|
|
382
|
+
process.env.SUNLINT_API_KEY → "k7Hs9mPqR2xYwN3v..."
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Gửi report
|
|
386
|
+
|
|
387
|
+
**File**: `core/upload-service.js` - `buildCurlCommand()` (line 180-208)
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
curl -X POST \
|
|
391
|
+
--connect-timeout 30 \
|
|
392
|
+
--max-time 30 \
|
|
393
|
+
-H "Content-Type: application/json" \
|
|
394
|
+
-H "User-Agent: SunLint-Report-Uploader/1.0" \
|
|
395
|
+
-H "Idempotency-Key: sunlint-a1b2c3d4e5f6..." \
|
|
396
|
+
-H "Authorization: Bearer <OIDC_TOKEN hoặc API_KEY>" \
|
|
397
|
+
--data-binary @"report.json" \
|
|
398
|
+
-i -s \
|
|
399
|
+
"https://coding-standards-report.sun-asterisk.vn/api/reports"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
execSync(curlCommand)
|
|
404
|
+
→ Parse HTTP response headers + body
|
|
405
|
+
→ Trích xuất status code (201 = thành công)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## PHASE 6: Server nhận & xác thực
|
|
411
|
+
|
|
412
|
+
### API Route
|
|
413
|
+
|
|
414
|
+
**File**: `coding-standards-reports/app/api/reports/route.ts` - `POST` (line 45-406)
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
POST /api/reports
|
|
418
|
+
│
|
|
419
|
+
└── verifyAuth(authHeader)
|
|
420
|
+
│
|
|
421
|
+
├── 1. Thử API_SECRET_KEY match?
|
|
422
|
+
│ token === process.env.API_SECRET_KEY → OK (actor: "api-key")
|
|
423
|
+
│ (Bitbucket Pipelines dùng cách này)
|
|
424
|
+
│
|
|
425
|
+
├── 2. Thử GitHub OIDC?
|
|
426
|
+
│ verifyGitHubOIDCToken(token) → OK (actor: github username)
|
|
427
|
+
│
|
|
428
|
+
├── 3. Thử GitLab OIDC?
|
|
429
|
+
│ verifyGitLabOIDCToken(token) → OK (actor: gitlab username)
|
|
430
|
+
│
|
|
431
|
+
└── 4. Tất cả fail → 401 Unauthorized
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Xác minh JWT (GitHub OIDC)
|
|
435
|
+
|
|
436
|
+
**File**: `coding-standards-reports/lib/auth/github-oidc.ts` (line 79-111)
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
verifyGitHubOIDCToken(token, "coding-standards-report-api")
|
|
440
|
+
│
|
|
441
|
+
├── Fetch JWKS:
|
|
442
|
+
│ GET https://token.actions.githubusercontent.com/.well-known/jwks
|
|
443
|
+
│ → Lấy public keys của GitHub (RSA)
|
|
444
|
+
│
|
|
445
|
+
├── jwtVerify(token, JWKS, {
|
|
446
|
+
│ issuer: "https://token.actions.githubusercontent.com",
|
|
447
|
+
│ audience: "coding-standards-report-api",
|
|
448
|
+
│ requiredClaims: ["iss","sub","aud","repository","repository_owner"]
|
|
449
|
+
│ })
|
|
450
|
+
│
|
|
451
|
+
├── Verify chữ ký RSA OK
|
|
452
|
+
├── Check issuer khớp OK
|
|
453
|
+
├── Check audience = "coding-standards-report-api" OK
|
|
454
|
+
├── Check token chưa hết hạn (exp) OK
|
|
455
|
+
├── Check có đủ required claims OK
|
|
456
|
+
│
|
|
457
|
+
└── Return payload:
|
|
458
|
+
{
|
|
459
|
+
repository: "org/my-flutter-app",
|
|
460
|
+
repository_owner: "sun-asterisk",
|
|
461
|
+
actor: "nguyen-van-a"
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Xác minh JWT (GitLab OIDC)
|
|
466
|
+
|
|
467
|
+
**File**: `coding-standards-reports/lib/auth/gitlab-oidc.ts` (line 122-153)
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
verifyGitLabOIDCToken(token, "coding-standards-report-api")
|
|
471
|
+
│
|
|
472
|
+
├── Fetch JWKS:
|
|
473
|
+
│ GET https://gitlab.com/oauth/discovery/keys
|
|
474
|
+
│ → Lấy public keys của GitLab (RSA)
|
|
475
|
+
│
|
|
476
|
+
├── jwtVerify(token, JWKS, {
|
|
477
|
+
│ issuer: "https://gitlab.com",
|
|
478
|
+
│ audience: "coding-standards-report-api",
|
|
479
|
+
│ requiredClaims: ["iss","sub","aud","project_path","namespace_path"]
|
|
480
|
+
│ })
|
|
481
|
+
│
|
|
482
|
+
└── Return payload:
|
|
483
|
+
{
|
|
484
|
+
project_path: "group/my-flutter-app",
|
|
485
|
+
namespace_path: "group",
|
|
486
|
+
user_login: "nguyen-van-a"
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Xác minh API key (Bitbucket)
|
|
491
|
+
|
|
492
|
+
```
|
|
493
|
+
token === process.env.API_SECRET_KEY?
|
|
494
|
+
→ Có: authenticated = true, actor = "api-key"
|
|
495
|
+
→ Không: tiếp tục thử OIDC providers
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## So sánh 3 phương thức xác thực
|
|
501
|
+
|
|
502
|
+
| | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
503
|
+
|---|---|---|---|
|
|
504
|
+
| **Phương thức** | OIDC (JWT) | OIDC (JWT) | API Key |
|
|
505
|
+
| **Cấu hình CI** | `permissions: id-token: write` | `id_tokens: SUNLINT_ID_TOKEN` | `SUNLINT_API_KEY` (Secured variable) |
|
|
506
|
+
| **Cách lấy token** | Gọi API GitHub runtime | Có sẵn trong env var | Có sẵn trong env var |
|
|
507
|
+
| **Server verify** | Fetch JWKS → verify chữ ký RSA | Fetch JWKS → verify chữ ký RSA | So sánh string với `API_SECRET_KEY` |
|
|
508
|
+
| **Shared secret** | Không | Không | Có (API key) |
|
|
509
|
+
| **Token hết hạn** | ~5 phút (tự động) | ~5 phút (tự động) | Không bao giờ (rotate thủ công) |
|
|
510
|
+
| **Audit trail** | Biết repo + actor + workflow | Biết project + user + pipeline | Chỉ biết "api-key" |
|
|
511
|
+
| **Server config** | Không cần | `GITLAB_OIDC_ISSUER` (nếu self-hosted) | `API_SECRET_KEY` |
|
|
512
|
+
|
|
513
|
+
### Bảo mật
|
|
514
|
+
|
|
515
|
+
| | OIDC (GitHub/GitLab) | API Key (Bitbucket) |
|
|
516
|
+
|---|---|---|
|
|
517
|
+
| Rủi ro lộ secret | Không có secret để lộ | Key lộ → ai cũng upload được |
|
|
518
|
+
| Phạm vi | Gắn với repo/workflow cụ thể | Ai có key đều dùng được |
|
|
519
|
+
| Rotation | Tự động mỗi lần chạy CI | Phải đổi thủ công cả 2 phía |
|
|
520
|
+
| Giảm thiểu rủi ro | -- | Đánh dấu Secured + rotate định kỳ |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## PHASE 7: Lưu vào Database
|
|
525
|
+
|
|
526
|
+
### Parse body & nhận diện format
|
|
527
|
+
|
|
528
|
+
**File**: `coding-standards-reports/app/api/reports/route.ts` (line 56-129)
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
Kiểm tra format:
|
|
532
|
+
body.repository_url OK
|
|
533
|
+
body.commit_hash OK
|
|
534
|
+
body.score OK
|
|
535
|
+
body.violations[] OK
|
|
536
|
+
body.metadata.tool === "SunLint" OK
|
|
537
|
+
→ Nhận diện: "Native SunLint CLI format"
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
3 formats được hỗ trợ:
|
|
541
|
+
1. **Native SunLint CLI** (`--output-summary`) - format hiện tại
|
|
542
|
+
2. **Enriched format** (legacy, nested git object)
|
|
543
|
+
3. **Legacy manual format** (từ old workflow)
|
|
544
|
+
|
|
545
|
+
### Tìm/tạo repository (line 150-238)
|
|
546
|
+
|
|
547
|
+
```sql
|
|
548
|
+
-- Tìm repository đã tồn tại chưa?
|
|
549
|
+
SELECT * FROM repositories
|
|
550
|
+
WHERE repository_url = 'https://github.com/org/my-flutter-app'
|
|
551
|
+
AND project_path IS NULL
|
|
552
|
+
LIMIT 1;
|
|
553
|
+
|
|
554
|
+
-- Nếu chưa có → INSERT
|
|
555
|
+
INSERT INTO repositories (repository_url, repository_name, project_path)
|
|
556
|
+
VALUES ('https://github.com/org/my-flutter-app', 'my-flutter-app', NULL)
|
|
557
|
+
RETURNING *;
|
|
558
|
+
|
|
559
|
+
-- Tìm rubato_id từ project_access (Google Sheets sync)
|
|
560
|
+
SELECT rubato_id FROM project_access
|
|
561
|
+
WHERE repository LIKE '%https://github.com/org/my-flutter-app%'
|
|
562
|
+
LIMIT 1;
|
|
563
|
+
|
|
564
|
+
-- Nếu tìm được → Tạo mapping
|
|
565
|
+
INSERT INTO project_repository_mapping (...)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Thêm report (line 241-270)
|
|
569
|
+
|
|
570
|
+
```sql
|
|
571
|
+
INSERT INTO reports (
|
|
572
|
+
repository_id, repository_url, repository_name,
|
|
573
|
+
branch, commit_hash, commit_message,
|
|
574
|
+
author_email, author_name, pr_number,
|
|
575
|
+
score, total_violations, error_count, warning_count,
|
|
576
|
+
info_count, lines_of_code, files_analyzed,
|
|
577
|
+
sunlint_version, analysis_duration_ms
|
|
578
|
+
) VALUES (
|
|
579
|
+
42, 'https://github.com/org/my-flutter-app', 'my-flutter-app',
|
|
580
|
+
'feature/login', 'abc123...', 'feat: add login page (#42)',
|
|
581
|
+
'dev@sun-asterisk.com', 'Nguyen Van A', 42,
|
|
582
|
+
65, 4293, 12, 4281,
|
|
583
|
+
0, 50000, 320,
|
|
584
|
+
'1.3.25', 15000
|
|
585
|
+
) RETURNING *;
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Thêm violations (line 276-287)
|
|
589
|
+
|
|
590
|
+
```sql
|
|
591
|
+
-- Với mỗi violation summary:
|
|
592
|
+
INSERT INTO violations (report_id, rule_code, violation_count)
|
|
593
|
+
VALUES (report_id, 'D008', 200)
|
|
594
|
+
ON CONFLICT (report_id, rule_code) DO UPDATE
|
|
595
|
+
SET violation_count = EXCLUDED.violation_count;
|
|
596
|
+
|
|
597
|
+
INSERT INTO violations (report_id, rule_code, violation_count)
|
|
598
|
+
VALUES (report_id, 'C006', 150) ...
|
|
599
|
+
|
|
600
|
+
-- ... lặp cho tất cả rules có violations
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Thêm dữ liệu architecture (line 290-381, nếu có)
|
|
604
|
+
|
|
605
|
+
```sql
|
|
606
|
+
INSERT INTO report_architecture (
|
|
607
|
+
report_id, primary_pattern, primary_confidence, health_score,
|
|
608
|
+
violation_count, rules_checked, rules_passed, rules_failed,
|
|
609
|
+
is_hybrid, combination, secondary_patterns, analysis_time_ms
|
|
610
|
+
) VALUES (...) RETURNING *;
|
|
611
|
+
|
|
612
|
+
-- Thêm architecture rules + violations
|
|
613
|
+
INSERT INTO report_architecture_rules (...) VALUES ...;
|
|
614
|
+
INSERT INTO report_architecture_violations (...) VALUES ...;
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Xóa cache & phản hồi (line 383-398)
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
revalidateTag("department-health")
|
|
621
|
+
revalidateTag("dept-adoption")
|
|
622
|
+
revalidateTag("dept-scores")
|
|
623
|
+
revalidateTag("score-trend")
|
|
624
|
+
revalidateTag("violations-severity")
|
|
625
|
+
revalidateTag("projects-filter")
|
|
626
|
+
|
|
627
|
+
→ Response: { success: true, report_id: 1234, message: "Report submitted successfully" }
|
|
628
|
+
→ HTTP 201 Created
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## PHASE 8: CI kết thúc
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
uploadService nhận HTTP 201
|
|
637
|
+
→ "Report uploaded successfully!"
|
|
638
|
+
→ "HTTP Status: 201"
|
|
639
|
+
|
|
640
|
+
handleExit(results)
|
|
641
|
+
→ Có errors? → process.exit(1) → CI job FAIL
|
|
642
|
+
→ Chỉ warnings? → process.exit(0) → CI job PASS
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Env vars mapping đầy đủ
|
|
648
|
+
|
|
649
|
+
### Detect CI platform
|
|
650
|
+
|
|
651
|
+
| Mục đích | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
652
|
+
|----------|----------------|-----------|---------------------|
|
|
653
|
+
| Detect CI | `GITHUB_ACTIONS` | `GITLAB_CI` | `BITBUCKET_PIPELINE_UUID` |
|
|
654
|
+
|
|
655
|
+
### Report metadata
|
|
656
|
+
|
|
657
|
+
| Mục đích | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
658
|
+
|----------|----------------|-----------|---------------------|
|
|
659
|
+
| Repository URL | `GITHUB_REPOSITORY` (ghép) | `CI_PROJECT_URL` | `BITBUCKET_WORKSPACE` + `BITBUCKET_REPO_SLUG` (ghép) |
|
|
660
|
+
| Repository name | `GITHUB_REPOSITORY` (split `/`) | `CI_PROJECT_NAME` | `BITBUCKET_REPO_SLUG` |
|
|
661
|
+
| Branch | `GITHUB_REF_NAME` | `CI_COMMIT_REF_NAME` | `BITBUCKET_BRANCH` |
|
|
662
|
+
| Commit SHA | `GITHUB_SHA` | `CI_COMMIT_SHA` | `BITBUCKET_COMMIT` |
|
|
663
|
+
| Commit message | `GITHUB_EVENT_PATH` (JSON file) | `CI_COMMIT_MESSAGE` | git fallback |
|
|
664
|
+
| Author email | `GITHUB_EVENT_PATH` (JSON file) | `GITLAB_USER_EMAIL` | git fallback |
|
|
665
|
+
| Author name | `GITHUB_EVENT_PATH` (JSON file) | `GITLAB_USER_NAME` | git fallback |
|
|
666
|
+
| PR/MR number | `GITHUB_EVENT_PATH` (JSON file) | `CI_MERGE_REQUEST_IID` | `BITBUCKET_PR_ID` |
|
|
667
|
+
|
|
668
|
+
### Authentication
|
|
669
|
+
|
|
670
|
+
| Mục đích | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
671
|
+
|----------|----------------|-----------|---------------------|
|
|
672
|
+
| Auth method | OIDC | OIDC | API Key |
|
|
673
|
+
| Token source | `ACTIONS_ID_TOKEN_REQUEST_*` (API) | `SUNLINT_ID_TOKEN` (env var) | `SUNLINT_API_KEY` (env var) |
|
|
674
|
+
| Audience | `coding-standards-report-api` | `coding-standards-report-api` | N/A |
|
|
675
|
+
|
|
676
|
+
### Idempotency key context
|
|
677
|
+
|
|
678
|
+
| Mục đích | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|
|
679
|
+
|----------|----------------|-----------|---------------------|
|
|
680
|
+
| Run ID | `GITHUB_RUN_ID` | `CI_PIPELINE_ID` | `BITBUCKET_BUILD_NUMBER` |
|
|
681
|
+
| Run attempt | `GITHUB_RUN_ATTEMPT` | `CI_JOB_ID` | `BITBUCKET_STEP_UUID` |
|
|
682
|
+
| Repo identifier | `GITHUB_REPOSITORY` | `CI_PROJECT_PATH` | `BITBUCKET_REPO_SLUG` |
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## Source files tham chiếu
|
|
687
|
+
|
|
688
|
+
| Phase | File | Function |
|
|
689
|
+
|-------|------|----------|
|
|
690
|
+
| 1 | `core/cli-action-handler.js` | `execute()`, `validateInput()` |
|
|
691
|
+
| 2 | `core/rule-selection-service.js` | `selectRules()` |
|
|
692
|
+
| 2 | `core/file-targeting-service.js` | `getTargetFiles()` |
|
|
693
|
+
| 3 | `core/analysis-orchestrator.js` | `analyze()` |
|
|
694
|
+
| 3 | `core/adapters/dart-analyzer.js` | `resolveBinary()`, `analyze()` |
|
|
695
|
+
| 4 | `core/output-service.js` | `outputResults()`, `generateAndSaveSummaryReport()` |
|
|
696
|
+
| 4 | `core/summary-report-service.js` | `generateSummaryReport()`, `getGitInfo()` |
|
|
697
|
+
| 5 | `core/upload-service.js` | `uploadReportToApi()`, `getOidcToken()`, `requestGitHubOidcToken()`, `requestGitLabOidcToken()` |
|
|
698
|
+
| 6 | `coding-standards-reports/app/api/reports/route.ts` | `POST()`, `verifyAuth()` |
|
|
699
|
+
| 6 | `coding-standards-reports/lib/auth/github-oidc.ts` | `verifyGitHubOIDCToken()` |
|
|
700
|
+
| 6 | `coding-standards-reports/lib/auth/gitlab-oidc.ts` | `verifyGitLabOIDCToken()` |
|
|
701
|
+
| 7 | `coding-standards-reports/app/api/reports/route.ts` | SQL queries (INSERT) |
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Hướng dẫn cấu hình nhanh
|
|
706
|
+
|
|
707
|
+
### GitHub Actions
|
|
708
|
+
|
|
709
|
+
1. Thêm `permissions: id-token: write` vào job
|
|
710
|
+
2. Thêm `--upload-report --quiet` vào lệnh sunlint
|
|
711
|
+
3. Không cần cấu hình gì thêm trên server
|
|
712
|
+
|
|
713
|
+
### GitLab CI
|
|
714
|
+
|
|
715
|
+
1. Thêm `id_tokens` vào job trong `.gitlab-ci.yml`:
|
|
716
|
+
```yaml
|
|
717
|
+
id_tokens:
|
|
718
|
+
SUNLINT_ID_TOKEN:
|
|
719
|
+
aud: coding-standards-report-api
|
|
720
|
+
```
|
|
721
|
+
2. Thêm `--upload-report --quiet` vào lệnh sunlint
|
|
722
|
+
3. Nếu GitLab self-hosted: set `GITLAB_OIDC_ISSUER` trên server Dashboard
|
|
723
|
+
|
|
724
|
+
### Bitbucket Pipelines
|
|
725
|
+
|
|
726
|
+
1. Tạo API key trên server: `openssl rand -base64 32`
|
|
727
|
+
2. Đặt giá trị vào server Dashboard `.env.local` → `API_SECRET_KEY=<giá_trị>`
|
|
728
|
+
3. Đặt **cùng giá trị** vào Bitbucket → Repository Settings → Repository variables:
|
|
729
|
+
- Name: `SUNLINT_API_KEY`
|
|
730
|
+
- Value: `<giá_trị>`
|
|
731
|
+
- Secured: ✅
|
|
732
|
+
4. Thêm `--upload-report --quiet` vào lệnh sunlint
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Xử lý sự cố
|
|
737
|
+
|
|
738
|
+
### Process bị kill khi output quá nhiều
|
|
739
|
+
- **Nguyên nhân**: `console.log(report.formatted)` in tất cả violations ra stdout
|
|
740
|
+
- **Cách xử lý**: Thêm `--quiet` để skip console output, report.json vẫn được tạo đầy đủ
|
|
741
|
+
|
|
742
|
+
### Binary Dart không chạy trên CI
|
|
743
|
+
- **Nguyên nhân**: `sunlint-dart-macos` là ARM64, không chạy trên Intel runner
|
|
744
|
+
- **Cách xử lý**: Dùng `macos-14` hoặc `macos-latest` (Apple Silicon)
|
|
745
|
+
|
|
746
|
+
### Upload thất bại 401 Unauthorized (GitHub)
|
|
747
|
+
- **Nguyên nhân**: Thiếu `permissions: id-token: write` trong workflow
|
|
748
|
+
- **Cách xử lý**: Thêm permission vào job
|
|
749
|
+
|
|
750
|
+
### Upload thất bại 401 Unauthorized (GitLab)
|
|
751
|
+
- **Nguyên nhân**: Thiếu `id_tokens` trong `.gitlab-ci.yml`
|
|
752
|
+
- **Cách xử lý**: Thêm cấu hình `id_tokens` với audience `coding-standards-report-api`
|
|
753
|
+
|
|
754
|
+
### Upload thất bại 401 Unauthorized (Bitbucket)
|
|
755
|
+
- **Nguyên nhân**: `SUNLINT_API_KEY` không khớp với `API_SECRET_KEY` trên server
|
|
756
|
+
- **Cách xử lý**: Kiểm tra giá trị 2 bên phải giống nhau. Tạo lại bằng `openssl rand -base64 32`
|
|
757
|
+
|
|
758
|
+
### Upload thất bại - không có token (GitHub)
|
|
759
|
+
- **Nguyên nhân**: `ACTIONS_ID_TOKEN_REQUEST_TOKEN` không được set
|
|
760
|
+
- **Cách xử lý**: Đảm bảo `id-token: write` và workflow chạy trên GitHub Actions
|
|
761
|
+
|
|
762
|
+
### Upload thất bại - không có token (GitLab)
|
|
763
|
+
- **Nguyên nhân**: `SUNLINT_ID_TOKEN` không được set
|
|
764
|
+
- **Cách xử lý**: Thêm `id_tokens` vào job:
|
|
765
|
+
```yaml
|
|
766
|
+
id_tokens:
|
|
767
|
+
SUNLINT_ID_TOKEN:
|
|
768
|
+
aud: coding-standards-report-api
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Upload thất bại - không có token (Bitbucket)
|
|
772
|
+
- **Nguyên nhân**: `SUNLINT_API_KEY` chưa được cấu hình
|
|
773
|
+
- **Cách xử lý**: Thêm `SUNLINT_API_KEY` vào Repository Settings → Repository variables (đánh dấu Secured)
|
|
774
|
+
|
|
775
|
+
### Self-hosted GitLab: JWKS fetch thất bại
|
|
776
|
+
- **Nguyên nhân**: Server Dashboard không biết URL GitLab instance
|
|
777
|
+
- **Cách xử lý**: Set `GITLAB_OIDC_ISSUER=https://gitlab.your-company.com` trên server
|
package/package.json
CHANGED