bun-ready 0.2.5 → 0.3.0
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/README.md +114 -1
- package/dist/cli.js +955 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ bun-ready scan .
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
```bash
|
|
26
|
-
bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed]
|
|
26
|
+
bun-ready scan <path> [--format md|json|sarif] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--scope root|packages|all] [--fail-on green|yellow|red] [--ci] [--output-dir <dir>] [--rule <id>=<action>] [--max-warnings <n>] [--baseline <file>] [--update-baseline] [--changed-only] [--since <ref>]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Examples:
|
|
@@ -238,6 +238,119 @@ Add it to the allowlist:
|
|
|
238
238
|
|
|
239
239
|
Some packages have optional native modules that can be disabled or work fine with Bun.
|
|
240
240
|
|
|
241
|
+
## v0.3 New Features
|
|
242
|
+
|
|
243
|
+
### CI Mode
|
|
244
|
+
|
|
245
|
+
Run in CI mode for stable, machine-friendly output:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
bun-ready scan . --ci --output-dir .bun-ready-artifacts
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**CI Mode Benefits:**
|
|
252
|
+
- Reduced "human noise" in stdout
|
|
253
|
+
- Stable section ordering in reports
|
|
254
|
+
- CI summary block with top findings and next actions
|
|
255
|
+
- Automatic artifact generation when `--output-dir` is specified
|
|
256
|
+
|
|
257
|
+
### SARIF Export
|
|
258
|
+
|
|
259
|
+
Generate SARIF 2.1.0 format for GitHub Code Scanning:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
bun-ready scan . --format sarif --out bun-ready.sarif.json
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Policy-as-Code
|
|
266
|
+
|
|
267
|
+
Enforce organization-specific migration policies:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# CLI flags
|
|
271
|
+
bun-ready scan . --rule deps.native_addons=fail --max-warnings 5
|
|
272
|
+
|
|
273
|
+
# Or in config file
|
|
274
|
+
{
|
|
275
|
+
"rules": [
|
|
276
|
+
{ "id": "deps.native_addons", "action": "fail" },
|
|
277
|
+
{ "id": "scripts.lifecycle", "action": "off" }
|
|
278
|
+
],
|
|
279
|
+
"thresholds": {
|
|
280
|
+
"maxWarnings": 10,
|
|
281
|
+
"maxPackagesRed": 2
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Policy Sources** (priority order):
|
|
287
|
+
1. CLI flags (`--rule`, `--max-warnings`)
|
|
288
|
+
2. `bun-ready.config.json` file
|
|
289
|
+
3. Default rules
|
|
290
|
+
|
|
291
|
+
**Policy Actions:**
|
|
292
|
+
- `fail` - Mark finding as failure
|
|
293
|
+
- `warn` - Downgrade to warning
|
|
294
|
+
- `off` - Disable this finding
|
|
295
|
+
- `ignore` - Ignore this finding (don't report)
|
|
296
|
+
|
|
297
|
+
### Baseline / Regression Detection
|
|
298
|
+
|
|
299
|
+
Prevent regressions by comparing against a baseline:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Create baseline (first time)
|
|
303
|
+
bun-ready scan . --baseline bun-ready-baseline.json --update-baseline
|
|
304
|
+
|
|
305
|
+
# In CI - compare against baseline
|
|
306
|
+
bun-ready scan . --baseline bun-ready-baseline.json --ci
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Baseline Features:**
|
|
310
|
+
- Detects new findings
|
|
311
|
+
- Detects resolved findings
|
|
312
|
+
- Detects severity changes (upgrades/downgrades)
|
|
313
|
+
- Regression verdict: fails if new red findings or increased failures
|
|
314
|
+
- Update baseline with `--update-baseline`
|
|
315
|
+
|
|
316
|
+
### Changed-Only Scanning (Monorepos)
|
|
317
|
+
|
|
318
|
+
Scan only changed packages to speed up large monorepo PRs:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
bun-ready scan . --changed-only --since main
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Changed-Only Verdict Types:**
|
|
325
|
+
- **Partial verdict** (no baseline): Warning about partial coverage
|
|
326
|
+
- **Regression verdict** (with baseline): OK - comparing only changed packages
|
|
327
|
+
|
|
328
|
+
### GitHub Action Integration
|
|
329
|
+
|
|
330
|
+
Use bun-ready as a GitHub Action:
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
name: bun-ready Check
|
|
334
|
+
on: [pull_request]
|
|
335
|
+
jobs:
|
|
336
|
+
bun-ready:
|
|
337
|
+
runs-on: ubuntu-latest
|
|
338
|
+
steps:
|
|
339
|
+
- uses: actions/checkout@v4
|
|
340
|
+
- uses: oven-sh/setup-bun@v1
|
|
341
|
+
- uses: ./ # Uses action.yml from repo
|
|
342
|
+
with:
|
|
343
|
+
fail-on: yellow
|
|
344
|
+
baseline: bun-ready-baseline.json
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Action Features:**
|
|
348
|
+
- Automatic artifact upload
|
|
349
|
+
- GitHub job summary generation
|
|
350
|
+
- PR comment support
|
|
351
|
+
- SARIF export for Code Scanning
|
|
352
|
+
- Policy and baseline support
|
|
353
|
+
|
|
241
354
|
## What it checks (MVP)
|
|
242
355
|
- package.json presence & shape
|
|
243
356
|
- lockfiles (npm/yarn/pnpm/bun)
|
package/dist/cli.js
CHANGED
|
@@ -86,9 +86,127 @@ var init_bun_check = __esm(() => {
|
|
|
86
86
|
init_spawn();
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
// src/
|
|
89
|
+
// src/baseline.ts
|
|
90
|
+
var exports_baseline = {};
|
|
91
|
+
__export(exports_baseline, {
|
|
92
|
+
updateBaseline: () => updateBaseline,
|
|
93
|
+
saveBaseline: () => saveBaseline,
|
|
94
|
+
loadBaseline: () => loadBaseline,
|
|
95
|
+
createFindingFingerprint: () => createFindingFingerprint,
|
|
96
|
+
compareFindings: () => compareFindings,
|
|
97
|
+
calculateBaselineMetrics: () => calculateBaselineMetrics
|
|
98
|
+
});
|
|
90
99
|
import { promises as fs4 } from "node:fs";
|
|
91
|
-
import
|
|
100
|
+
import { createHash } from "node:crypto";
|
|
101
|
+
function createFindingFingerprint(finding, packageName) {
|
|
102
|
+
const normalizedDetails = finding.details.map((d) => d.trim().toLowerCase()).sort().join("|");
|
|
103
|
+
const detailsHash = createHash("md5").update(normalizedDetails).digest("hex");
|
|
104
|
+
return {
|
|
105
|
+
id: finding.id,
|
|
106
|
+
packageName: packageName || "root",
|
|
107
|
+
severity: finding.severity,
|
|
108
|
+
detailsHash
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function calculateBaselineMetrics(findings, packages) {
|
|
112
|
+
const greenCount = findings.filter((f) => f.severity === "green").length;
|
|
113
|
+
const yellowCount = findings.filter((f) => f.severity === "yellow").length;
|
|
114
|
+
const redCount = findings.filter((f) => f.severity === "red").length;
|
|
115
|
+
const packagesGreen = packages.filter((p) => p.severity === "green").length;
|
|
116
|
+
const packagesYellow = packages.filter((p) => p.severity === "yellow").length;
|
|
117
|
+
const packagesRed = packages.filter((p) => p.severity === "red").length;
|
|
118
|
+
return {
|
|
119
|
+
totalFindings: findings.length,
|
|
120
|
+
greenCount,
|
|
121
|
+
yellowCount,
|
|
122
|
+
redCount,
|
|
123
|
+
packagesGreen,
|
|
124
|
+
packagesYellow,
|
|
125
|
+
packagesRed
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function compareFindings(baseline, current) {
|
|
129
|
+
const baselineMap = new Map;
|
|
130
|
+
const currentMap = new Map;
|
|
131
|
+
for (const fp of baseline) {
|
|
132
|
+
const key = `${fp.id}:${fp.packageName}:${fp.detailsHash}`;
|
|
133
|
+
baselineMap.set(key, fp);
|
|
134
|
+
}
|
|
135
|
+
for (const fp of current) {
|
|
136
|
+
const key = `${fp.id}:${fp.packageName}:${fp.detailsHash}`;
|
|
137
|
+
currentMap.set(key, fp);
|
|
138
|
+
}
|
|
139
|
+
const newFindings = [];
|
|
140
|
+
for (const [key, fp] of currentMap.entries()) {
|
|
141
|
+
if (!baselineMap.has(key)) {
|
|
142
|
+
newFindings.push(fp);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const resolvedFindings = [];
|
|
146
|
+
for (const [key, fp] of baselineMap.entries()) {
|
|
147
|
+
if (!currentMap.has(key)) {
|
|
148
|
+
resolvedFindings.push(fp);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const severityChanges = [];
|
|
152
|
+
for (const [key, currentFp] of currentMap.entries()) {
|
|
153
|
+
const baselineFp = baselineMap.get(key);
|
|
154
|
+
if (baselineFp && currentFp.severity !== baselineFp.severity) {
|
|
155
|
+
severityChanges.push({
|
|
156
|
+
fingerprint: currentFp,
|
|
157
|
+
oldSeverity: baselineFp.severity,
|
|
158
|
+
newSeverity: currentFp.severity
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const regressionReasons = [];
|
|
163
|
+
let isRegression = false;
|
|
164
|
+
const newRedFindings = newFindings.filter((f) => f.severity === "red");
|
|
165
|
+
if (newRedFindings.length > 0) {
|
|
166
|
+
isRegression = true;
|
|
167
|
+
regressionReasons.push(`New RED findings detected: ${newRedFindings.map((f) => f.id).join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
const upgradedToRed = severityChanges.filter((c) => c.newSeverity === "red");
|
|
170
|
+
if (upgradedToRed.length > 0) {
|
|
171
|
+
isRegression = true;
|
|
172
|
+
regressionReasons.push(`Severity upgraded to RED: ${upgradedToRed.map((c) => c.fingerprint.id).join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
newFindings,
|
|
176
|
+
resolvedFindings,
|
|
177
|
+
severityChanges,
|
|
178
|
+
isRegression,
|
|
179
|
+
regressionReasons
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function saveBaseline(baseline, filePath) {
|
|
183
|
+
const json = JSON.stringify(baseline, null, 2);
|
|
184
|
+
await fs4.writeFile(filePath, json, "utf-8");
|
|
185
|
+
}
|
|
186
|
+
async function loadBaseline(filePath) {
|
|
187
|
+
try {
|
|
188
|
+
const json = await fs4.readFile(filePath, "utf-8");
|
|
189
|
+
const data = JSON.parse(json);
|
|
190
|
+
if (typeof data === "object" && data !== null && typeof data.version === "string" && typeof data.timestamp === "string" && Array.isArray(data.findings)) {
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function updateBaseline(existing, current) {
|
|
199
|
+
return {
|
|
200
|
+
...existing,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
findings: current
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
var init_baseline = () => {};
|
|
206
|
+
|
|
207
|
+
// src/cli.ts
|
|
208
|
+
import { promises as fs5 } from "node:fs";
|
|
209
|
+
import path7 from "node:path";
|
|
92
210
|
|
|
93
211
|
// src/analyze.ts
|
|
94
212
|
init_spawn();
|
|
@@ -1272,6 +1390,119 @@ var formatPackageStats = (pkg) => {
|
|
|
1272
1390
|
}
|
|
1273
1391
|
return lines;
|
|
1274
1392
|
};
|
|
1393
|
+
var formatPolicyApplied = (policy) => {
|
|
1394
|
+
const lines = [];
|
|
1395
|
+
lines.push(`## Policy Applied`);
|
|
1396
|
+
lines.push(``);
|
|
1397
|
+
lines.push(`- **Rules applied**: ${policy.rulesApplied}`);
|
|
1398
|
+
lines.push(`- **Findings modified**: ${policy.findingsModified}`);
|
|
1399
|
+
lines.push(`- **Findings disabled**: ${policy.findingsDisabled}`);
|
|
1400
|
+
lines.push(`- **Severity upgraded**: ${policy.severityUpgraded}`);
|
|
1401
|
+
lines.push(`- **Severity downgraded**: ${policy.severityDowngraded}`);
|
|
1402
|
+
lines.push(``);
|
|
1403
|
+
if (policy.rules.length > 0) {
|
|
1404
|
+
lines.push(`### Policy Rules`);
|
|
1405
|
+
for (const rule of policy.rules) {
|
|
1406
|
+
const actionBadge = getPolicyActionBadge(rule.action);
|
|
1407
|
+
lines.push(`- **${rule.findingId}**: ${actionBadge}`);
|
|
1408
|
+
if (rule.originalSeverity && rule.newSeverity && rule.originalSeverity !== rule.newSeverity) {
|
|
1409
|
+
lines.push(` - ${badge(rule.originalSeverity)} → ${badge(rule.newSeverity)}`);
|
|
1410
|
+
}
|
|
1411
|
+
if (rule.reason) {
|
|
1412
|
+
lines.push(` - Reason: ${rule.reason}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
lines.push(``);
|
|
1416
|
+
} else {
|
|
1417
|
+
lines.push(`No policy rules were applied.`);
|
|
1418
|
+
lines.push(``);
|
|
1419
|
+
}
|
|
1420
|
+
return lines.join(`
|
|
1421
|
+
`);
|
|
1422
|
+
};
|
|
1423
|
+
var getPolicyActionBadge = (action) => {
|
|
1424
|
+
if (action === "fail")
|
|
1425
|
+
return "\uD83D\uDED1 FAIL";
|
|
1426
|
+
if (action === "warn")
|
|
1427
|
+
return "⚠️ WARN";
|
|
1428
|
+
if (action === "off")
|
|
1429
|
+
return "\uD83D\uDD34 OFF";
|
|
1430
|
+
return "\uD83D\uDD35 IGNORE";
|
|
1431
|
+
};
|
|
1432
|
+
var formatBaselineComparison = (comparison) => {
|
|
1433
|
+
const lines = [];
|
|
1434
|
+
lines.push(`## Baseline Comparison`);
|
|
1435
|
+
lines.push(``);
|
|
1436
|
+
if (comparison.isRegression) {
|
|
1437
|
+
lines.push(`\uD83D\uDD34 **REGRESSION DETECTED**`);
|
|
1438
|
+
} else {
|
|
1439
|
+
lines.push(`✅ No regression detected`);
|
|
1440
|
+
}
|
|
1441
|
+
lines.push(``);
|
|
1442
|
+
if (comparison.newFindings.length > 0) {
|
|
1443
|
+
lines.push(`### New Findings (${comparison.newFindings.length})`);
|
|
1444
|
+
for (const f of comparison.newFindings) {
|
|
1445
|
+
const severityBadge = badge(f.severity);
|
|
1446
|
+
lines.push(`- ${severityBadge} **${f.id}** in package \`${f.packageName}\``);
|
|
1447
|
+
}
|
|
1448
|
+
lines.push(``);
|
|
1449
|
+
}
|
|
1450
|
+
if (comparison.resolvedFindings.length > 0) {
|
|
1451
|
+
lines.push(`### Resolved Findings (${comparison.resolvedFindings.length})`);
|
|
1452
|
+
for (const f of comparison.resolvedFindings) {
|
|
1453
|
+
const severityBadge = badge(f.severity);
|
|
1454
|
+
lines.push(`- ${severityBadge} **${f.id}** in package \`${f.packageName}\``);
|
|
1455
|
+
}
|
|
1456
|
+
lines.push(``);
|
|
1457
|
+
}
|
|
1458
|
+
if (comparison.severityChanges.length > 0) {
|
|
1459
|
+
lines.push(`### Severity Changes (${comparison.severityChanges.length})`);
|
|
1460
|
+
for (const c of comparison.severityChanges) {
|
|
1461
|
+
const oldBadge = badge(c.oldSeverity);
|
|
1462
|
+
const newBadge = badge(c.newSeverity);
|
|
1463
|
+
lines.push(`- **${c.fingerprint.id}** in package \`${c.fingerprint.packageName}\`: ${oldBadge} → ${newBadge}`);
|
|
1464
|
+
}
|
|
1465
|
+
lines.push(``);
|
|
1466
|
+
}
|
|
1467
|
+
if (comparison.regressionReasons.length > 0) {
|
|
1468
|
+
lines.push(`### Regression Reasons`);
|
|
1469
|
+
for (const reason of comparison.regressionReasons) {
|
|
1470
|
+
lines.push(`- ${reason}`);
|
|
1471
|
+
}
|
|
1472
|
+
lines.push(``);
|
|
1473
|
+
}
|
|
1474
|
+
if (comparison.newFindings.length === 0 && comparison.resolvedFindings.length === 0 && comparison.severityChanges.length === 0) {
|
|
1475
|
+
lines.push(`No changes detected from baseline.`);
|
|
1476
|
+
lines.push(``);
|
|
1477
|
+
}
|
|
1478
|
+
return lines.join(`
|
|
1479
|
+
`);
|
|
1480
|
+
};
|
|
1481
|
+
var formatChangedOnly = (changedPackages, baselineFile) => {
|
|
1482
|
+
const lines = [];
|
|
1483
|
+
lines.push(`## Scanned Packages (Changed-only)`);
|
|
1484
|
+
lines.push(``);
|
|
1485
|
+
if (changedPackages.length === 0) {
|
|
1486
|
+
lines.push(`No packages were identified as changed.`);
|
|
1487
|
+
} else {
|
|
1488
|
+
lines.push(`The following packages were scanned (only changed packages):`);
|
|
1489
|
+
lines.push(``);
|
|
1490
|
+
for (const pkgPath of changedPackages) {
|
|
1491
|
+
const normalizedPath = pkgPath.replace(/\\/g, "/");
|
|
1492
|
+
lines.push(`- \`${normalizedPath}\``);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
lines.push(``);
|
|
1496
|
+
if (baselineFile) {
|
|
1497
|
+
lines.push(`**Verdict type:** Regression verdict (comparing changed packages against baseline)`);
|
|
1498
|
+
} else {
|
|
1499
|
+
lines.push(`**Verdict type:** Partial verdict (only changed packages scanned, no baseline)`);
|
|
1500
|
+
lines.push(`⚠️ Note: This is a partial scan. For complete verdict, provide a baseline file.`);
|
|
1501
|
+
}
|
|
1502
|
+
lines.push(``);
|
|
1503
|
+
return lines.join(`
|
|
1504
|
+
`);
|
|
1505
|
+
};
|
|
1275
1506
|
function renderMarkdown(r) {
|
|
1276
1507
|
const lines = [];
|
|
1277
1508
|
const bunVersion = process.version;
|
|
@@ -1323,6 +1554,9 @@ function renderMarkdown(r) {
|
|
|
1323
1554
|
}
|
|
1324
1555
|
lines.push(``);
|
|
1325
1556
|
}
|
|
1557
|
+
if (r.policyApplied) {
|
|
1558
|
+
lines.push(formatPolicyApplied(r.policyApplied));
|
|
1559
|
+
}
|
|
1326
1560
|
lines.push(`## Root Package`);
|
|
1327
1561
|
lines.push(`- Path: \`${r.repo.packageJsonPath.replace(/\\/g, "/")}\``);
|
|
1328
1562
|
lines.push(`- Workspaces: ${r.repo.hasWorkspaces ? "yes" : "no"}`);
|
|
@@ -1393,6 +1627,13 @@ function renderMarkdown(r) {
|
|
|
1393
1627
|
}
|
|
1394
1628
|
}
|
|
1395
1629
|
lines.push(``);
|
|
1630
|
+
if (r.baselineComparison) {
|
|
1631
|
+
lines.push(formatBaselineComparison(r.baselineComparison));
|
|
1632
|
+
}
|
|
1633
|
+
if (r.changedPackages) {
|
|
1634
|
+
const baselineFile = r.baselineFile;
|
|
1635
|
+
lines.push(formatChangedOnly(r.changedPackages, baselineFile));
|
|
1636
|
+
}
|
|
1396
1637
|
if (r.packages && r.packages.length > 0) {
|
|
1397
1638
|
const sortedPackages = stableSort(r.packages, (p) => p.name);
|
|
1398
1639
|
for (const pkg of sortedPackages) {
|
|
@@ -1473,6 +1714,9 @@ var renderDetailedReport = (r) => {
|
|
|
1473
1714
|
lines.push(``);
|
|
1474
1715
|
lines.push(`**Overall:** ${badge(r.severity)}`);
|
|
1475
1716
|
lines.push(``);
|
|
1717
|
+
if (r.policyApplied) {
|
|
1718
|
+
lines.push(formatPolicyApplied(r.policyApplied));
|
|
1719
|
+
}
|
|
1476
1720
|
lines.push(`## Detailed Package Usage`);
|
|
1477
1721
|
lines.push(``);
|
|
1478
1722
|
let hasUsageInfo = false;
|
|
@@ -1535,6 +1779,13 @@ var renderDetailedReport = (r) => {
|
|
|
1535
1779
|
}
|
|
1536
1780
|
}
|
|
1537
1781
|
lines.push(``);
|
|
1782
|
+
if (r.baselineComparison) {
|
|
1783
|
+
lines.push(formatBaselineComparison(r.baselineComparison));
|
|
1784
|
+
}
|
|
1785
|
+
if (r.changedPackages) {
|
|
1786
|
+
const baselineFile = r.baselineFile;
|
|
1787
|
+
lines.push(formatChangedOnly(r.changedPackages, baselineFile));
|
|
1788
|
+
}
|
|
1538
1789
|
return lines.join(`
|
|
1539
1790
|
`);
|
|
1540
1791
|
};
|
|
@@ -1544,16 +1795,512 @@ function renderJson(r) {
|
|
|
1544
1795
|
return JSON.stringify(r, null, 2);
|
|
1545
1796
|
}
|
|
1546
1797
|
|
|
1798
|
+
// src/sarif.ts
|
|
1799
|
+
import path6 from "node:path";
|
|
1800
|
+
function severityToSarifLevel(severity) {
|
|
1801
|
+
switch (severity) {
|
|
1802
|
+
case "green":
|
|
1803
|
+
return "note";
|
|
1804
|
+
case "yellow":
|
|
1805
|
+
return "warning";
|
|
1806
|
+
case "red":
|
|
1807
|
+
return "error";
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function createSarifRule(finding) {
|
|
1811
|
+
const descriptionText = finding.details.length > 0 ? finding.details[0] : finding.title;
|
|
1812
|
+
const fullDesc = { text: descriptionText };
|
|
1813
|
+
const helpParts = [];
|
|
1814
|
+
if (finding.details.length > 0) {
|
|
1815
|
+
helpParts.push("Details:");
|
|
1816
|
+
finding.details.forEach((d) => helpParts.push(` - ${d}`));
|
|
1817
|
+
}
|
|
1818
|
+
if (finding.hints.length > 0) {
|
|
1819
|
+
helpParts.push("Hints:");
|
|
1820
|
+
finding.hints.forEach((h) => helpParts.push(` - ${h}`));
|
|
1821
|
+
}
|
|
1822
|
+
const helpText = helpParts.length > 0 ? helpParts.join(`
|
|
1823
|
+
`) : "No hints available";
|
|
1824
|
+
return {
|
|
1825
|
+
id: finding.id,
|
|
1826
|
+
shortDescription: {
|
|
1827
|
+
text: finding.title
|
|
1828
|
+
},
|
|
1829
|
+
fullDescription: fullDesc,
|
|
1830
|
+
help: {
|
|
1831
|
+
text: helpText
|
|
1832
|
+
},
|
|
1833
|
+
defaultConfiguration: {
|
|
1834
|
+
level: severityToSarifLevel(finding.severity)
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function determineFindingLocation(finding, repoPath, packageName) {
|
|
1839
|
+
if (!packageName) {
|
|
1840
|
+
return {
|
|
1841
|
+
physicalLocation: {
|
|
1842
|
+
artifactLocation: {
|
|
1843
|
+
uri: path6.basename(repoPath)
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
const relativePath = packageName === "root" ? "package.json" : packageName;
|
|
1849
|
+
return {
|
|
1850
|
+
physicalLocation: {
|
|
1851
|
+
artifactLocation: {
|
|
1852
|
+
uri: relativePath
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
function createSarifResult(finding, repoPath, packageName) {
|
|
1858
|
+
const messageParts = [finding.title];
|
|
1859
|
+
if (finding.details.length > 0) {
|
|
1860
|
+
messageParts.push("");
|
|
1861
|
+
messageParts.push("Details:");
|
|
1862
|
+
messageParts.push(...finding.details.map((d) => `- ${d}`));
|
|
1863
|
+
}
|
|
1864
|
+
const messageText = messageParts.join(`
|
|
1865
|
+
`);
|
|
1866
|
+
return {
|
|
1867
|
+
ruleId: finding.id,
|
|
1868
|
+
level: severityToSarifLevel(finding.severity),
|
|
1869
|
+
message: {
|
|
1870
|
+
text: messageText
|
|
1871
|
+
},
|
|
1872
|
+
locations: [determineFindingLocation(finding, repoPath, packageName)]
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function renderSarif(result) {
|
|
1876
|
+
const allFindings = [...result.findings];
|
|
1877
|
+
if (result.packages) {
|
|
1878
|
+
for (const pkg of result.packages) {
|
|
1879
|
+
for (const finding of pkg.findings) {
|
|
1880
|
+
if (!allFindings.some((f) => f.id === finding.id)) {
|
|
1881
|
+
allFindings.push(finding);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const rulesMap = new Map;
|
|
1887
|
+
for (const finding of allFindings) {
|
|
1888
|
+
if (!rulesMap.has(finding.id)) {
|
|
1889
|
+
rulesMap.set(finding.id, createSarifRule(finding));
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const rules = Array.from(rulesMap.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
1893
|
+
const results = [];
|
|
1894
|
+
for (const finding of result.findings) {
|
|
1895
|
+
results.push(createSarifResult(finding, result.repo.packageJsonPath, "root"));
|
|
1896
|
+
}
|
|
1897
|
+
for (const pkg of result.packages || []) {
|
|
1898
|
+
const packageName = pkg.name;
|
|
1899
|
+
for (const finding of pkg.findings) {
|
|
1900
|
+
results.push(createSarifResult(finding, result.repo.packageJsonPath, packageName));
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const sarifLog = {
|
|
1904
|
+
version: "2.1.0",
|
|
1905
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
1906
|
+
runs: [
|
|
1907
|
+
{
|
|
1908
|
+
tool: {
|
|
1909
|
+
driver: {
|
|
1910
|
+
name: "bun-ready",
|
|
1911
|
+
version: result.version || "0.3.0",
|
|
1912
|
+
semanticVersion: "2.1.0",
|
|
1913
|
+
rules
|
|
1914
|
+
}
|
|
1915
|
+
},
|
|
1916
|
+
results
|
|
1917
|
+
}
|
|
1918
|
+
]
|
|
1919
|
+
};
|
|
1920
|
+
return sarifLog;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/ci_summary.ts
|
|
1924
|
+
function getTopFindings2(findings, count = 3) {
|
|
1925
|
+
const sorted = [...findings].sort((a, b) => {
|
|
1926
|
+
const severityOrder = { red: 0, yellow: 1, green: 2 };
|
|
1927
|
+
if (severityOrder[a.severity] !== severityOrder[b.severity]) {
|
|
1928
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
1929
|
+
}
|
|
1930
|
+
return a.id.localeCompare(b.id);
|
|
1931
|
+
});
|
|
1932
|
+
const badge2 = (s) => {
|
|
1933
|
+
switch (s) {
|
|
1934
|
+
case "green":
|
|
1935
|
+
return "\uD83D\uDFE2";
|
|
1936
|
+
case "yellow":
|
|
1937
|
+
return "\uD83D\uDFE1";
|
|
1938
|
+
case "red":
|
|
1939
|
+
return "\uD83D\uDD34";
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
return sorted.slice(0, count).map((f) => `${badge2(f.severity)} ${f.title} (${f.id})`);
|
|
1943
|
+
}
|
|
1944
|
+
function generateNextActions(findings) {
|
|
1945
|
+
const actions = [];
|
|
1946
|
+
const actionSet = new Set;
|
|
1947
|
+
for (const finding of findings) {
|
|
1948
|
+
for (const hint of finding.hints) {
|
|
1949
|
+
const action = hint.trim();
|
|
1950
|
+
if (!actionSet.has(action)) {
|
|
1951
|
+
actions.push(action);
|
|
1952
|
+
actionSet.add(action);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return actions.slice(0, 5);
|
|
1957
|
+
}
|
|
1958
|
+
function calculateExitCode(severity, failOn) {
|
|
1959
|
+
if (!failOn) {
|
|
1960
|
+
if (severity === "green")
|
|
1961
|
+
return 0;
|
|
1962
|
+
if (severity === "yellow")
|
|
1963
|
+
return 2;
|
|
1964
|
+
return 3;
|
|
1965
|
+
}
|
|
1966
|
+
if (failOn === "green") {
|
|
1967
|
+
if (severity === "green")
|
|
1968
|
+
return 0;
|
|
1969
|
+
return 3;
|
|
1970
|
+
}
|
|
1971
|
+
if (failOn === "yellow") {
|
|
1972
|
+
if (severity === "red")
|
|
1973
|
+
return 3;
|
|
1974
|
+
return 0;
|
|
1975
|
+
}
|
|
1976
|
+
if (severity === "green")
|
|
1977
|
+
return 0;
|
|
1978
|
+
if (severity === "yellow")
|
|
1979
|
+
return 2;
|
|
1980
|
+
return 3;
|
|
1981
|
+
}
|
|
1982
|
+
function generateCISummary(result, failOn) {
|
|
1983
|
+
const topFindings = getTopFindings2(result.findings, 3);
|
|
1984
|
+
const nextActions = generateNextActions(result.findings);
|
|
1985
|
+
const exitCode = calculateExitCode(result.severity, failOn);
|
|
1986
|
+
return {
|
|
1987
|
+
verdict: result.severity,
|
|
1988
|
+
topFindings,
|
|
1989
|
+
nextActions,
|
|
1990
|
+
exitCode
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function formatCISummaryText(summary) {
|
|
1994
|
+
const badge2 = (s) => {
|
|
1995
|
+
switch (s) {
|
|
1996
|
+
case "green":
|
|
1997
|
+
return "\uD83D\uDFE2 GREEN";
|
|
1998
|
+
case "yellow":
|
|
1999
|
+
return "\uD83D\uDFE1 YELLOW";
|
|
2000
|
+
case "red":
|
|
2001
|
+
return "\uD83D\uDD34 RED";
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
const lines = [];
|
|
2005
|
+
lines.push("=== bun-ready CI Summary ===");
|
|
2006
|
+
lines.push("");
|
|
2007
|
+
lines.push(`Verdict: ${badge2(summary.verdict)}`);
|
|
2008
|
+
lines.push("");
|
|
2009
|
+
if (summary.topFindings.length > 0) {
|
|
2010
|
+
lines.push("Top Issues:");
|
|
2011
|
+
for (const finding of summary.topFindings) {
|
|
2012
|
+
lines.push(` - ${finding}`);
|
|
2013
|
+
}
|
|
2014
|
+
lines.push("");
|
|
2015
|
+
}
|
|
2016
|
+
if (summary.nextActions.length > 0) {
|
|
2017
|
+
lines.push("Next Actions:");
|
|
2018
|
+
for (let i = 0;i < summary.nextActions.length; i++) {
|
|
2019
|
+
lines.push(` ${i + 1}. ${summary.nextActions[i]}`);
|
|
2020
|
+
}
|
|
2021
|
+
lines.push("");
|
|
2022
|
+
}
|
|
2023
|
+
lines.push(`Exit Code: ${summary.exitCode}`);
|
|
2024
|
+
return lines.join(`
|
|
2025
|
+
`);
|
|
2026
|
+
}
|
|
2027
|
+
function formatGitHubJobSummary(summary) {
|
|
2028
|
+
const badge2 = (s) => {
|
|
2029
|
+
switch (s) {
|
|
2030
|
+
case "green":
|
|
2031
|
+
return "\uD83D\uDFE2 **GREEN**";
|
|
2032
|
+
case "yellow":
|
|
2033
|
+
return "\uD83D\uDFE1 **YELLOW**";
|
|
2034
|
+
case "red":
|
|
2035
|
+
return "\uD83D\uDD34 **RED**";
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
const lines = [];
|
|
2039
|
+
lines.push("## bun-ready CI Summary");
|
|
2040
|
+
lines.push("");
|
|
2041
|
+
lines.push(`### Verdict: ${badge2(summary.verdict)}`);
|
|
2042
|
+
lines.push("");
|
|
2043
|
+
if (summary.topFindings.length > 0) {
|
|
2044
|
+
lines.push("### Top Issues");
|
|
2045
|
+
lines.push("");
|
|
2046
|
+
for (const finding of summary.topFindings) {
|
|
2047
|
+
lines.push(`- ${finding}`);
|
|
2048
|
+
}
|
|
2049
|
+
lines.push("");
|
|
2050
|
+
}
|
|
2051
|
+
if (summary.nextActions.length > 0) {
|
|
2052
|
+
lines.push("### Next Actions");
|
|
2053
|
+
lines.push("");
|
|
2054
|
+
for (let i = 0;i < summary.nextActions.length; i++) {
|
|
2055
|
+
lines.push(`${i + 1}. ${summary.nextActions[i]}`);
|
|
2056
|
+
}
|
|
2057
|
+
lines.push("");
|
|
2058
|
+
}
|
|
2059
|
+
lines.push(`**Exit Code:** \`${summary.exitCode}\``);
|
|
2060
|
+
return lines.join(`
|
|
2061
|
+
`);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/policy.ts
|
|
2065
|
+
function applySeverityChange(originalSeverity, change) {
|
|
2066
|
+
if (change === "same")
|
|
2067
|
+
return originalSeverity;
|
|
2068
|
+
if (change === "upgrade") {
|
|
2069
|
+
if (originalSeverity === "green")
|
|
2070
|
+
return "yellow";
|
|
2071
|
+
if (originalSeverity === "yellow")
|
|
2072
|
+
return "red";
|
|
2073
|
+
return "red";
|
|
2074
|
+
}
|
|
2075
|
+
if (originalSeverity === "red")
|
|
2076
|
+
return "yellow";
|
|
2077
|
+
if (originalSeverity === "yellow")
|
|
2078
|
+
return "green";
|
|
2079
|
+
return "green";
|
|
2080
|
+
}
|
|
2081
|
+
function parseRuleArgs(ruleArgs) {
|
|
2082
|
+
const rules = [];
|
|
2083
|
+
for (const arg of ruleArgs) {
|
|
2084
|
+
const parts = arg.split(/[=:]/, 2);
|
|
2085
|
+
if (parts.length !== 2)
|
|
2086
|
+
continue;
|
|
2087
|
+
const [id, actionOrChange] = parts.map((p) => p.trim());
|
|
2088
|
+
if (!id || !actionOrChange) {
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
if (["fail", "warn", "off", "ignore"].includes(actionOrChange)) {
|
|
2092
|
+
const actionRule = {
|
|
2093
|
+
id,
|
|
2094
|
+
action: actionOrChange
|
|
2095
|
+
};
|
|
2096
|
+
rules.push(actionRule);
|
|
2097
|
+
} else if (["upgrade", "downgrade", "same"].includes(actionOrChange)) {
|
|
2098
|
+
const severityRule = {
|
|
2099
|
+
id,
|
|
2100
|
+
severityChange: actionOrChange
|
|
2101
|
+
};
|
|
2102
|
+
rules.push(severityRule);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return rules;
|
|
2106
|
+
}
|
|
2107
|
+
function mergePolicyConfigs(cliPolicy, configPolicy) {
|
|
2108
|
+
if (!cliPolicy && !configPolicy) {
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
const result = {};
|
|
2112
|
+
if (cliPolicy?.rules && cliPolicy.rules.length > 0) {
|
|
2113
|
+
result.rules = cliPolicy.rules;
|
|
2114
|
+
} else if (configPolicy?.rules && configPolicy.rules.length > 0) {
|
|
2115
|
+
result.rules = configPolicy.rules;
|
|
2116
|
+
}
|
|
2117
|
+
if (cliPolicy?.thresholds && Object.keys(cliPolicy.thresholds).length > 0) {
|
|
2118
|
+
result.thresholds = cliPolicy.thresholds;
|
|
2119
|
+
} else if (configPolicy?.thresholds && Object.keys(configPolicy.thresholds).length > 0) {
|
|
2120
|
+
result.thresholds = configPolicy.thresholds;
|
|
2121
|
+
}
|
|
2122
|
+
if (cliPolicy?.failOn) {
|
|
2123
|
+
result.failOn = cliPolicy.failOn;
|
|
2124
|
+
} else if (configPolicy?.failOn) {
|
|
2125
|
+
result.failOn = configPolicy.failOn;
|
|
2126
|
+
}
|
|
2127
|
+
if (Object.keys(result).length === 0) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
return result;
|
|
2131
|
+
}
|
|
2132
|
+
function findMatchingRule(findingId, rules) {
|
|
2133
|
+
for (const rule of rules) {
|
|
2134
|
+
if (rule.id === findingId) {
|
|
2135
|
+
return rule;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
for (const rule of rules) {
|
|
2139
|
+
if (rule.id === "*") {
|
|
2140
|
+
return rule;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
function applyPolicy(findings, policy, metrics) {
|
|
2146
|
+
const rules = policy.rules || [];
|
|
2147
|
+
const thresholds = policy.thresholds;
|
|
2148
|
+
const modifiedFindings = [];
|
|
2149
|
+
const appliedRules = [];
|
|
2150
|
+
let findingsModified = 0;
|
|
2151
|
+
let findingsDisabled = 0;
|
|
2152
|
+
let severityUpgraded = 0;
|
|
2153
|
+
let severityDowngraded = 0;
|
|
2154
|
+
for (const finding of findings) {
|
|
2155
|
+
const rule = findMatchingRule(finding.id, rules);
|
|
2156
|
+
if (!rule) {
|
|
2157
|
+
modifiedFindings.push(finding);
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
const applied = {
|
|
2161
|
+
findingId: finding.id,
|
|
2162
|
+
action: rule.action || "ignore",
|
|
2163
|
+
originalSeverity: finding.severity
|
|
2164
|
+
};
|
|
2165
|
+
if (rule.action === "off" || rule.action === "ignore") {
|
|
2166
|
+
findingsDisabled++;
|
|
2167
|
+
appliedRules.push(applied);
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
let newSeverity = finding.severity;
|
|
2171
|
+
let wasModified = false;
|
|
2172
|
+
if (rule.severityChange) {
|
|
2173
|
+
newSeverity = finding.severity;
|
|
2174
|
+
if (rule.action === "fail") {
|
|
2175
|
+
newSeverity = "red";
|
|
2176
|
+
} else if (rule.action === "warn") {
|
|
2177
|
+
newSeverity = "yellow";
|
|
2178
|
+
}
|
|
2179
|
+
newSeverity = applySeverityChange(newSeverity, rule.severityChange);
|
|
2180
|
+
if (rule.severityChange === "upgrade") {
|
|
2181
|
+
severityUpgraded++;
|
|
2182
|
+
} else if (rule.severityChange === "downgrade") {
|
|
2183
|
+
severityDowngraded++;
|
|
2184
|
+
}
|
|
2185
|
+
wasModified = true;
|
|
2186
|
+
} else if (rule.action === "fail" || rule.action === "warn") {
|
|
2187
|
+
if (rule.action === "fail") {
|
|
2188
|
+
newSeverity = "red";
|
|
2189
|
+
} else if (rule.action === "warn") {
|
|
2190
|
+
newSeverity = "yellow";
|
|
2191
|
+
}
|
|
2192
|
+
wasModified = true;
|
|
2193
|
+
if (newSeverity !== finding.severity) {
|
|
2194
|
+
const severityOrder = { green: 0, yellow: 1, red: 2 };
|
|
2195
|
+
if (severityOrder[newSeverity] > severityOrder[finding.severity]) {
|
|
2196
|
+
severityUpgraded++;
|
|
2197
|
+
} else {
|
|
2198
|
+
severityDowngraded++;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
if (wasModified) {
|
|
2203
|
+
findingsModified++;
|
|
2204
|
+
}
|
|
2205
|
+
applied.newSeverity = newSeverity;
|
|
2206
|
+
applied.reason = rule.reason;
|
|
2207
|
+
modifiedFindings.push({
|
|
2208
|
+
...finding,
|
|
2209
|
+
severity: newSeverity
|
|
2210
|
+
});
|
|
2211
|
+
appliedRules.push(applied);
|
|
2212
|
+
}
|
|
2213
|
+
let rulesApplied = appliedRules.length;
|
|
2214
|
+
if (thresholds && metrics) {
|
|
2215
|
+
if (thresholds.maxPackagesRed !== undefined && metrics.packagesRed > thresholds.maxPackagesRed) {
|
|
2216
|
+
rulesApplied++;
|
|
2217
|
+
}
|
|
2218
|
+
if (thresholds.maxPackagesYellow !== undefined && metrics.packagesYellow > thresholds.maxPackagesYellow) {
|
|
2219
|
+
rulesApplied++;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
const summary = {
|
|
2223
|
+
rulesApplied,
|
|
2224
|
+
findingsModified,
|
|
2225
|
+
findingsDisabled,
|
|
2226
|
+
severityUpgraded,
|
|
2227
|
+
severityDowngraded,
|
|
2228
|
+
rules: appliedRules
|
|
2229
|
+
};
|
|
2230
|
+
return { modifiedFindings, summary };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/cli.ts
|
|
2234
|
+
init_baseline();
|
|
2235
|
+
|
|
2236
|
+
// src/changed_only.ts
|
|
2237
|
+
init_spawn();
|
|
2238
|
+
async function getGitDiffPaths(repoPath, sinceRef) {
|
|
2239
|
+
try {
|
|
2240
|
+
const res = await exec("git", ["diff", "--name-only", sinceRef], repoPath);
|
|
2241
|
+
if (!res.stdout) {
|
|
2242
|
+
return [];
|
|
2243
|
+
}
|
|
2244
|
+
const paths = res.stdout.split(`
|
|
2245
|
+
`).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
2246
|
+
return paths;
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
return [];
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
function isWorkspacePackage(path7, workspacePackages) {
|
|
2252
|
+
if (!workspacePackages || workspacePackages.length === 0) {
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
for (const wp of workspacePackages) {
|
|
2256
|
+
const normalizedPath = path7.replace(/\\/g, "/");
|
|
2257
|
+
const normalizedWpPath = wp.path.replace(/\\/g, "/");
|
|
2258
|
+
if (normalizedPath.startsWith(normalizedWpPath)) {
|
|
2259
|
+
return true;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return false;
|
|
2263
|
+
}
|
|
2264
|
+
function mapPathsToPackages(paths, packages) {
|
|
2265
|
+
const packageMap = new Map;
|
|
2266
|
+
for (const pkg of packages) {
|
|
2267
|
+
const normalizedPath = pkg.path.replace(/\\/g, "/");
|
|
2268
|
+
packageMap.set(normalizedPath, pkg.name);
|
|
2269
|
+
}
|
|
2270
|
+
const changedPackages = new Set;
|
|
2271
|
+
for (const path7 of paths) {
|
|
2272
|
+
for (const [pkgPath, pkgName] of packageMap.entries()) {
|
|
2273
|
+
const relativePath = path7.replace(/\\/g, "/");
|
|
2274
|
+
if (relativePath.startsWith(pkgPath)) {
|
|
2275
|
+
changedPackages.add(pkgPath);
|
|
2276
|
+
break;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return Array.from(changedPackages).sort();
|
|
2281
|
+
}
|
|
2282
|
+
async function detectChangedPackages(repoPath, sinceRef, workspacePackages) {
|
|
2283
|
+
const paths = await getGitDiffPaths(repoPath, sinceRef);
|
|
2284
|
+
if (paths.length === 0) {
|
|
2285
|
+
return [];
|
|
2286
|
+
}
|
|
2287
|
+
let filteredPaths = paths;
|
|
2288
|
+
if (workspacePackages && workspacePackages.length > 0) {
|
|
2289
|
+
filteredPaths = paths.filter((p) => isWorkspacePackage(p, workspacePackages));
|
|
2290
|
+
}
|
|
2291
|
+
return filteredPaths;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
1547
2294
|
// src/cli.ts
|
|
1548
2295
|
var usage = () => {
|
|
1549
2296
|
return [
|
|
1550
2297
|
"bun-ready",
|
|
1551
2298
|
"",
|
|
1552
2299
|
"Usage:",
|
|
1553
|
-
" bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--scope root|packages|all] [--fail-on green|yellow|red]",
|
|
2300
|
+
" bun-ready scan <path> [--format md|json|sarif] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--scope root|packages|all] [--fail-on green|yellow|red] [--ci] [--output-dir <dir>] [--rule <id>=<action>] [--max-warnings <n>] [--baseline <file>] [--update-baseline] [--changed-only] [--since <ref>]",
|
|
1554
2301
|
"",
|
|
1555
2302
|
"Options:",
|
|
1556
|
-
" --format md|json Output format (default: md)",
|
|
2303
|
+
" --format md|json|sarif Output format (default: md)",
|
|
1557
2304
|
" --out <file> Output file path (default: bun-ready.md or bun-ready.json)",
|
|
1558
2305
|
" --no-install Skip bun install --dry-run",
|
|
1559
2306
|
" --no-test Skip bun test",
|
|
@@ -1561,6 +2308,14 @@ var usage = () => {
|
|
|
1561
2308
|
" --detailed Show detailed package usage report with file paths",
|
|
1562
2309
|
" --scope root|packages|all Scan scope for monorepos (default: all)",
|
|
1563
2310
|
" --fail-on green|yellow|red Fail policy (default: red)",
|
|
2311
|
+
" --ci Run in CI mode (stable output, minimal noise)",
|
|
2312
|
+
" --output-dir <dir> Output directory for all artifacts (in CI mode)",
|
|
2313
|
+
" --rule <id>=<action> Apply policy rule (e.g., --rule deps.native_addons=fail)",
|
|
2314
|
+
" --max-warnings <n> Maximum warnings allowed (threshold)",
|
|
2315
|
+
" --baseline <file> Baseline file for regression detection",
|
|
2316
|
+
" --update-baseline Update baseline file after scan",
|
|
2317
|
+
" --changed-only Scan only changed packages (monorepos)",
|
|
2318
|
+
" --since <ref> Git ref for changed packages (e.g., main, HEAD~1)",
|
|
1564
2319
|
"",
|
|
1565
2320
|
"Exit codes:",
|
|
1566
2321
|
" 0 green",
|
|
@@ -1597,11 +2352,17 @@ var parseArgs = (argv) => {
|
|
|
1597
2352
|
let detailed = false;
|
|
1598
2353
|
let scope = "all";
|
|
1599
2354
|
let failOn;
|
|
2355
|
+
let ci;
|
|
2356
|
+
let outputDir;
|
|
2357
|
+
let ruleArgs = [];
|
|
2358
|
+
let maxWarnings;
|
|
2359
|
+
let baseline;
|
|
2360
|
+
let changedOnly;
|
|
1600
2361
|
for (let i = 2;i < args.length; i++) {
|
|
1601
2362
|
const a = args[i] ?? "";
|
|
1602
2363
|
if (a === "--format") {
|
|
1603
2364
|
const v = args[i + 1] ?? "";
|
|
1604
|
-
if (v === "md" || v === "json")
|
|
2365
|
+
if (v === "md" || v === "json" || v === "sarif")
|
|
1605
2366
|
format = v;
|
|
1606
2367
|
i++;
|
|
1607
2368
|
continue;
|
|
@@ -1641,6 +2402,59 @@ var parseArgs = (argv) => {
|
|
|
1641
2402
|
i++;
|
|
1642
2403
|
continue;
|
|
1643
2404
|
}
|
|
2405
|
+
if (a === "--ci") {
|
|
2406
|
+
ci = { mode: true };
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (a === "--output-dir") {
|
|
2410
|
+
outputDir = args[i + 1];
|
|
2411
|
+
i++;
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
if (a === "--rule") {
|
|
2415
|
+
ruleArgs.push(args[i + 1] ?? "");
|
|
2416
|
+
i++;
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (a === "--max-warnings") {
|
|
2420
|
+
const v = args[i + 1] ?? "";
|
|
2421
|
+
const num = parseInt(v, 10);
|
|
2422
|
+
if (!isNaN(num) && num >= 0) {
|
|
2423
|
+
maxWarnings = num;
|
|
2424
|
+
}
|
|
2425
|
+
i++;
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
if (a === "--baseline") {
|
|
2429
|
+
baseline = { file: args[i + 1] ?? "" };
|
|
2430
|
+
i++;
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
if (a === "--update-baseline") {
|
|
2434
|
+
if (!baseline) {
|
|
2435
|
+
baseline = { file: "", update: true };
|
|
2436
|
+
} else {
|
|
2437
|
+
baseline.update = true;
|
|
2438
|
+
}
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (a === "--changed-only") {
|
|
2442
|
+
if (!changedOnly) {
|
|
2443
|
+
changedOnly = { enabled: true };
|
|
2444
|
+
} else {
|
|
2445
|
+
changedOnly.enabled = true;
|
|
2446
|
+
}
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
if (a === "--since") {
|
|
2450
|
+
if (!changedOnly) {
|
|
2451
|
+
changedOnly = { enabled: true, sinceRef: args[i + 1] ?? "" };
|
|
2452
|
+
} else {
|
|
2453
|
+
changedOnly.sinceRef = args[i + 1] ?? "";
|
|
2454
|
+
}
|
|
2455
|
+
i++;
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
1644
2458
|
}
|
|
1645
2459
|
const baseOpts = {
|
|
1646
2460
|
repoPath,
|
|
@@ -1655,35 +2469,36 @@ var parseArgs = (argv) => {
|
|
|
1655
2469
|
if (failOn !== undefined) {
|
|
1656
2470
|
baseOpts.failOn = failOn;
|
|
1657
2471
|
}
|
|
2472
|
+
if (ci !== undefined) {
|
|
2473
|
+
baseOpts.ci = ci;
|
|
2474
|
+
}
|
|
2475
|
+
if (outputDir !== undefined) {
|
|
2476
|
+
if (!baseOpts.ci) {
|
|
2477
|
+
baseOpts.ci = { mode: true };
|
|
2478
|
+
}
|
|
2479
|
+
baseOpts.ci.outputDir = outputDir;
|
|
2480
|
+
}
|
|
2481
|
+
if (ruleArgs.length > 0 || maxWarnings !== undefined) {
|
|
2482
|
+
const policy = {};
|
|
2483
|
+
if (ruleArgs.length > 0) {
|
|
2484
|
+
policy.rules = parseRuleArgs(ruleArgs);
|
|
2485
|
+
}
|
|
2486
|
+
if (maxWarnings !== undefined) {
|
|
2487
|
+
policy.thresholds = { maxWarnings };
|
|
2488
|
+
}
|
|
2489
|
+
baseOpts.policy = policy;
|
|
2490
|
+
}
|
|
2491
|
+
if (baseline !== undefined && (baseline.file || baseline.update)) {
|
|
2492
|
+
baseOpts.baseline = baseline;
|
|
2493
|
+
}
|
|
2494
|
+
if (changedOnly !== undefined && changedOnly.enabled) {
|
|
2495
|
+
baseOpts.changedOnly = changedOnly;
|
|
2496
|
+
}
|
|
1658
2497
|
return {
|
|
1659
2498
|
cmd,
|
|
1660
2499
|
opts: baseOpts
|
|
1661
2500
|
};
|
|
1662
2501
|
};
|
|
1663
|
-
var exitCode = (sev, failOn) => {
|
|
1664
|
-
if (!failOn) {
|
|
1665
|
-
if (sev === "green")
|
|
1666
|
-
return 0;
|
|
1667
|
-
if (sev === "yellow")
|
|
1668
|
-
return 2;
|
|
1669
|
-
return 3;
|
|
1670
|
-
}
|
|
1671
|
-
if (failOn === "green") {
|
|
1672
|
-
if (sev === "green")
|
|
1673
|
-
return 0;
|
|
1674
|
-
return 3;
|
|
1675
|
-
}
|
|
1676
|
-
if (failOn === "yellow") {
|
|
1677
|
-
if (sev === "red")
|
|
1678
|
-
return 3;
|
|
1679
|
-
return 0;
|
|
1680
|
-
}
|
|
1681
|
-
if (sev === "green")
|
|
1682
|
-
return 0;
|
|
1683
|
-
if (sev === "yellow")
|
|
1684
|
-
return 2;
|
|
1685
|
-
return 3;
|
|
1686
|
-
};
|
|
1687
2502
|
var main = async () => {
|
|
1688
2503
|
const { cmd, opts } = parseArgs(process.argv);
|
|
1689
2504
|
if (cmd !== "scan") {
|
|
@@ -1699,6 +2514,7 @@ var main = async () => {
|
|
|
1699
2514
|
configOpts.detailed = opts.detailed;
|
|
1700
2515
|
}
|
|
1701
2516
|
const config = await mergeConfigWithOpts(null, configOpts);
|
|
2517
|
+
const mergedPolicy = mergePolicyConfigs(opts.policy, config?.rules ? { rules: config.rules } : undefined);
|
|
1702
2518
|
const scanOpts = {
|
|
1703
2519
|
repoPath: opts.repoPath,
|
|
1704
2520
|
format: opts.format,
|
|
@@ -1711,14 +2527,78 @@ var main = async () => {
|
|
|
1711
2527
|
if (opts.failOn !== undefined) {
|
|
1712
2528
|
scanOpts.failOn = opts.failOn;
|
|
1713
2529
|
}
|
|
2530
|
+
if (opts.ci !== undefined) {
|
|
2531
|
+
scanOpts.ci = opts.ci;
|
|
2532
|
+
}
|
|
2533
|
+
if (mergedPolicy !== undefined) {
|
|
2534
|
+
scanOpts.policy = mergedPolicy;
|
|
2535
|
+
}
|
|
1714
2536
|
const res = await analyzeRepoOverall(scanOpts);
|
|
1715
|
-
|
|
2537
|
+
let changedPackages;
|
|
2538
|
+
if (opts.changedOnly?.enabled && opts.changedOnly?.sinceRef) {
|
|
2539
|
+
try {
|
|
2540
|
+
const detectedPaths = await detectChangedPackages(opts.repoPath, opts.changedOnly.sinceRef, res.packages?.map((p) => ({ path: p.path, packageJsonPath: p.path, name: p.name })));
|
|
2541
|
+
if (res.packages) {
|
|
2542
|
+
changedPackages = mapPathsToPackages(detectedPaths, res.packages);
|
|
2543
|
+
}
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
process.stderr.write(`Failed to detect changed packages: ${error instanceof Error ? error.message : String(error)}
|
|
2546
|
+
`);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
let baselineData = null;
|
|
2550
|
+
if (opts.baseline?.file) {
|
|
2551
|
+
try {
|
|
2552
|
+
baselineData = await loadBaseline(opts.baseline.file);
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
process.stderr.write(`Failed to load baseline: ${error instanceof Error ? error.message : String(error)}
|
|
2555
|
+
`);
|
|
2556
|
+
process.exit(1);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
let finalResult = res;
|
|
2560
|
+
if (mergedPolicy) {
|
|
2561
|
+
const policyResult = applyPolicy(res.findings, mergedPolicy);
|
|
2562
|
+
finalResult = {
|
|
2563
|
+
...res,
|
|
2564
|
+
findings: policyResult.modifiedFindings,
|
|
2565
|
+
policyApplied: policyResult.summary
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const { createFindingFingerprint: createFindingFingerprint2, calculateBaselineMetrics: calculateBaselineMetrics2 } = await Promise.resolve().then(() => (init_baseline(), exports_baseline));
|
|
2569
|
+
const packages = finalResult.packages || [];
|
|
2570
|
+
const allFindings = [...finalResult.findings];
|
|
2571
|
+
for (const pkg of packages) {
|
|
2572
|
+
for (const finding of pkg.findings) {
|
|
2573
|
+
allFindings.push(finding);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
const currentFingerprints = allFindings.map((f) => createFindingFingerprint2(f));
|
|
2577
|
+
if (baselineData) {
|
|
2578
|
+
const comparison = compareFindings(baselineData.findings, currentFingerprints);
|
|
2579
|
+
finalResult = {
|
|
2580
|
+
...finalResult,
|
|
2581
|
+
baselineComparison: comparison
|
|
2582
|
+
};
|
|
2583
|
+
if (opts.baseline?.update) {
|
|
2584
|
+
const updatedBaseline = {
|
|
2585
|
+
...baselineData,
|
|
2586
|
+
timestamp: new Date().toISOString(),
|
|
2587
|
+
findings: currentFingerprints
|
|
2588
|
+
};
|
|
2589
|
+
await saveBaseline(updatedBaseline, opts.baseline.file || "");
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (changedPackages) {
|
|
2593
|
+
finalResult.changedPackages = changedPackages;
|
|
2594
|
+
}
|
|
2595
|
+
if (finalResult.install?.skipReason || finalResult.test?.skipReason) {
|
|
1716
2596
|
const skipWarnings = [];
|
|
1717
|
-
if (
|
|
1718
|
-
skipWarnings.push(`Install check skipped: ${
|
|
2597
|
+
if (finalResult.install?.skipReason) {
|
|
2598
|
+
skipWarnings.push(`Install check skipped: ${finalResult.install.skipReason}`);
|
|
1719
2599
|
}
|
|
1720
|
-
if (
|
|
1721
|
-
skipWarnings.push(`Test run skipped: ${
|
|
2600
|
+
if (finalResult.test?.skipReason) {
|
|
2601
|
+
skipWarnings.push(`Test run skipped: ${finalResult.test.skipReason}`);
|
|
1722
2602
|
}
|
|
1723
2603
|
if (skipWarnings.length > 0) {
|
|
1724
2604
|
process.stderr.write(`WARNING:
|
|
@@ -1727,13 +2607,48 @@ ${skipWarnings.map((w) => ` - ${w}`).join(`
|
|
|
1727
2607
|
`);
|
|
1728
2608
|
}
|
|
1729
2609
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2610
|
+
let out = "";
|
|
2611
|
+
if (opts.format === "sarif") {
|
|
2612
|
+
out = JSON.stringify(renderSarif(finalResult), null, 2);
|
|
2613
|
+
} else if (opts.format === "json") {
|
|
2614
|
+
out = renderJson(finalResult);
|
|
2615
|
+
} else {
|
|
2616
|
+
out = opts.detailed ? renderDetailedReport(finalResult) : renderMarkdown(finalResult);
|
|
2617
|
+
}
|
|
2618
|
+
let target = opts.outFile;
|
|
2619
|
+
let outputDir = opts.ci?.outputDir;
|
|
2620
|
+
if (!target) {
|
|
2621
|
+
if (opts.format === "sarif") {
|
|
2622
|
+
target = "bun-ready.sarif.json";
|
|
2623
|
+
} else if (opts.format === "json") {
|
|
2624
|
+
target = "bun-ready.json";
|
|
2625
|
+
} else if (opts.detailed) {
|
|
2626
|
+
target = "bun-ready-detailed.md";
|
|
2627
|
+
} else {
|
|
2628
|
+
target = "bun-ready.md";
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
const resolved = outputDir ? path7.resolve(process.cwd(), outputDir, target) : path7.resolve(process.cwd(), target);
|
|
2632
|
+
if (outputDir) {
|
|
2633
|
+
await fs5.mkdir(path7.dirname(resolved), { recursive: true });
|
|
2634
|
+
}
|
|
2635
|
+
await fs5.writeFile(resolved, out, "utf8");
|
|
2636
|
+
if (opts.ci?.mode) {
|
|
2637
|
+
const ciSummary = generateCISummary(finalResult, config?.failOn || opts.failOn);
|
|
2638
|
+
const summaryText = formatCISummaryText(ciSummary);
|
|
2639
|
+
process.stdout.write(`
|
|
2640
|
+
${summaryText}
|
|
1735
2641
|
`);
|
|
1736
|
-
|
|
2642
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
2643
|
+
const githubSummary = formatGitHubJobSummary(ciSummary);
|
|
2644
|
+
await fs5.writeFile(process.env.GITHUB_STEP_SUMMARY, githubSummary, "utf8");
|
|
2645
|
+
}
|
|
2646
|
+
} else {
|
|
2647
|
+
process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
|
|
2648
|
+
`);
|
|
2649
|
+
}
|
|
2650
|
+
const exitCodeValue = calculateExitCode(finalResult.severity, config?.failOn || opts.failOn);
|
|
2651
|
+
process.exit(exitCodeValue);
|
|
1737
2652
|
};
|
|
1738
2653
|
main().catch((e) => {
|
|
1739
2654
|
const msg = e instanceof Error ? e.message : String(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bun-ready",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI that estimates how painful migrating a Node.js repo to Bun might be. Generates a green/yellow/red Markdown report with reasons.",
|
|
5
5
|
"author": "Pas7 Studio",
|
|
6
6
|
"license": "Apache-2.0",
|