@prodcycle/prodcycle 0.6.3 → 0.6.5
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/dist/api-client.js +18 -3
- package/dist/utils/fs.js +31 -12
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -47,10 +47,25 @@ const MAX_RETRY_AFTER_SECONDS = envInt('PC_MAX_RETRY_AFTER_SECONDS', 300);
|
|
|
47
47
|
/**
|
|
48
48
|
* Per-request fetch timeout. Without this a stalled connection would tie
|
|
49
49
|
* up the CLI indefinitely, bypassing both the retry cap and the async-poll
|
|
50
|
-
* deadline.
|
|
51
|
-
*
|
|
50
|
+
* deadline.
|
|
51
|
+
*
|
|
52
|
+
* Default is 5 minutes — chosen so the chunked-session `/chunks` upload
|
|
53
|
+
* path has enough headroom under server-side load. The bottleneck on
|
|
54
|
+
* busy servers is the per-chunk transaction (policy eval + per-finding
|
|
55
|
+
* unique-index check on `(scan_id, fingerprint)`), which can take tens
|
|
56
|
+
* of seconds on big chunks. Sync `/validate` scans normally finish in
|
|
57
|
+
* seconds, so a longer default doesn't hurt them — it only matters
|
|
58
|
+
* when a single request stalls. CI runs that want tighter feedback can
|
|
59
|
+
* shrink via `PC_REQUEST_TIMEOUT_MS`.
|
|
60
|
+
*
|
|
61
|
+
* Pre-fix this was 120 s and a megarepo chunked scan (infisical-
|
|
62
|
+
* infisical, ~11.5 k files, 2026-05-13 GA-validation sweep) burned
|
|
63
|
+
* through the full retry budget (4 × 120 s per stuck chunk) before
|
|
64
|
+
* giving up with `Failed to connect to ProdCycle API: The operation
|
|
65
|
+
* was aborted due to timeout`. The body-read retry path from #30 was
|
|
66
|
+
* firing correctly — it just wasn't enough budget.
|
|
52
67
|
*/
|
|
53
|
-
const REQUEST_TIMEOUT_MS = envInt('PC_REQUEST_TIMEOUT_MS',
|
|
68
|
+
const REQUEST_TIMEOUT_MS = envInt('PC_REQUEST_TIMEOUT_MS', 300_000);
|
|
54
69
|
/**
|
|
55
70
|
* Conservative client-side chunk sizing for the chunked-session flow. The
|
|
56
71
|
* /chunks endpoint accepts up to 50 MB / 2000 files per request, but most
|
package/dist/utils/fs.js
CHANGED
|
@@ -38,6 +38,12 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const minimatch_1 = require("minimatch");
|
|
40
40
|
const MAX_FILE_SIZE = 256 * 1024; // 256 KB
|
|
41
|
+
// Reusable strict UTF-8 decoder. `TextDecoder` is stateless on the same
|
|
42
|
+
// instance (encoding + fatal are fixed at construction), so we allocate
|
|
43
|
+
// once at module load instead of once per file in the walk loop —
|
|
44
|
+
// repos with thousands of files would otherwise produce thousands of
|
|
45
|
+
// short-lived decoder objects per scan.
|
|
46
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
41
47
|
/**
|
|
42
48
|
* Total file ceiling per scan. Hit on the OSS-CLI benchmark scanning
|
|
43
49
|
* `hapifhir/hapi-fhir` (~13k files) — the CLI silently dropped ~3k files
|
|
@@ -397,18 +403,31 @@ function walk(dir, repoRoot, gitignores, prodcycleIgnores, includePatterns, user
|
|
|
397
403
|
}
|
|
398
404
|
if (isBinary(buffer))
|
|
399
405
|
continue;
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
406
|
+
// Strict UTF-8 decode: throw (and skip the file) on invalid byte
|
|
407
|
+
// sequences. Pre-fix this was `buffer.toString('utf8')`, which
|
|
408
|
+
// silently replaces invalid bytes with U+FFFD and includes the file
|
|
409
|
+
// anyway. Python's `open(encoding='utf-8')` raises UnicodeDecodeError
|
|
410
|
+
// on the same input and skips the file via its except clause, so
|
|
411
|
+
// Node ended up sending files Python wouldn't. Those files were
|
|
412
|
+
// overwhelmingly garbage (U+FFFD soup with no real content), but
|
|
413
|
+
// the inflated payload pushed many scans over the sync /validate
|
|
414
|
+
// limit and into the chunked-session fallback — Node ran 5–75x
|
|
415
|
+
// slower than Python on the same repos (dexidp-dex: npm=525s vs
|
|
416
|
+
// py=7s, frappe-erpnext: 1448s vs 42s, both 0 finding differences)
|
|
417
|
+
// during the 2026-05-12 GA-validation sweep. Catch the decode
|
|
418
|
+
// error and treat exactly like Python.
|
|
419
|
+
let content;
|
|
420
|
+
try {
|
|
421
|
+
content = UTF8_DECODER.decode(buffer);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Post-decode size filter mirrors the service's 256 KB per-file
|
|
427
|
+
// enforcement. Without invalid-UTF-8 inflation (now skipped above),
|
|
428
|
+
// post-decode byte length usually matches `stats.size`, but a BOM
|
|
429
|
+
// or rare normalization edge case can still differ — keep the
|
|
430
|
+
// re-measure as defense-in-depth.
|
|
412
431
|
if (Buffer.byteLength(content, 'utf8') > MAX_FILE_SIZE)
|
|
413
432
|
continue;
|
|
414
433
|
files[relPath] = content;
|
package/package.json
CHANGED