@sun-asterisk/sunlint 1.3.49 → 1.3.51

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
- : gitInfo.repository_url;
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
- : (gitInfo.repository_name || null);
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 || gitInfo.branch;
166
- const commit_hash = process.env.GITHUB_SHA || gitInfo.commit_hash;
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
- // Get commit details from GitHub context or git
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
- || gitInfo.commit_message;
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
- || gitInfo.author_email;
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
- || gitInfo.author_name;
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 GitHub event or git
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)
@@ -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
- // Save cursor position BEFORE first render
114
- process.stdout.write(ansi.saveCursor);
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
- ansi.restoreCursor + ansi.clearToEnd +
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(ansi.restoreCursor + ansi.clearToEnd + '\n');
185
+ process.stdout.write(`\x1b[${totalLines}A\r` + ansi.clearToEnd + '\n');
190
186
  reject(new Error('Cancelled by user'));
191
187
  }
192
188
  }
@@ -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 to get GitHub Actions OIDC token first (requires API call)
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
- // Check if running in GitHub Actions without OIDC token
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 = process.env.GITHUB_ACTIONS ?
290
- `${process.env.GITHUB_REPOSITORY || 'unknown'}-${process.env.GITHUB_RUN_ID || 'local'}-${process.env.GITHUB_RUN_ATTEMPT || '1'}` :
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
  */
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.49",
3
+ "version": "1.3.51",
4
4
  "description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@babel/parser": "^7.25.8",
58
+ "@babel/traverse": "^7.25.8",
58
59
  "@octokit/rest": "^22.0.0",
59
60
  "@typescript-eslint/eslint-plugin": "^8.38.0",
60
61
  "@typescript-eslint/parser": "^8.38.0",