@flakiness/sdk 0.150.1 → 0.151.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/lib/index.js CHANGED
@@ -1,18 +1,1196 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/index.ts
1
8
  import { FlakinessReport } from "@flakiness/flakiness-report";
2
- import { CIUtils } from "./ciUtils.js";
3
- import { GitWorktree } from "./gitWorktree.js";
4
- import * as ReportUtils from "./reportUtils.js";
5
- import { SystemUtilizationSampler } from "./systemUtilizationSampler.js";
6
- import { showReport } from "./showReport.js";
7
- import { uploadReport } from "./uploadReport.js";
8
- import { writeReport } from "./writeReport.js";
9
- import { FlakinessProjectConfig } from "./flakinessProjectConfig.js";
9
+
10
+ // src/ciUtils.ts
11
+ var CIUtils;
12
+ ((CIUtils2) => {
13
+ function runUrl() {
14
+ return githubActions() ?? azure() ?? process.env.CI_JOB_URL ?? process.env.BUILD_URL;
15
+ }
16
+ CIUtils2.runUrl = runUrl;
17
+ })(CIUtils || (CIUtils = {}));
18
+ function githubActions() {
19
+ const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
20
+ const repo = process.env.GITHUB_REPOSITORY;
21
+ const runId = process.env.GITHUB_RUN_ID;
22
+ if (!repo || !runId) return void 0;
23
+ try {
24
+ const url = new URL(`${serverUrl}/${repo}/actions/runs/${runId}`);
25
+ const attempt = process.env.GITHUB_RUN_ATTEMPT;
26
+ if (attempt) url.searchParams.set("attempt", attempt);
27
+ url.searchParams.set("check_suite_focus", "true");
28
+ return url.toString();
29
+ } catch (error) {
30
+ return void 0;
31
+ }
32
+ }
33
+ function azure() {
34
+ const collectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
35
+ const project = process.env.SYSTEM_TEAMPROJECT;
36
+ const buildId = process.env.BUILD_BUILDID;
37
+ if (!collectionUri || !project || !buildId)
38
+ return void 0;
39
+ try {
40
+ const baseUrl = collectionUri.endsWith("/") ? collectionUri : `${collectionUri}/`;
41
+ const url = new URL(`${baseUrl}${project}/_build/results`);
42
+ url.searchParams.set("buildId", buildId);
43
+ return url.toString();
44
+ } catch (error) {
45
+ return void 0;
46
+ }
47
+ }
48
+
49
+ // src/gitWorktree.ts
50
+ import assert from "assert";
51
+ import { exec } from "child_process";
52
+ import debug from "debug";
53
+ import { posix as posixPath, win32 as win32Path } from "path";
54
+ import { promisify } from "util";
55
+
56
+ // src/_internalUtils.ts
57
+ import { spawnSync } from "child_process";
58
+ import crypto from "crypto";
59
+ import fs from "fs";
60
+ import util from "util";
61
+ import zlib from "zlib";
62
+ var asyncBrotliCompress = util.promisify(zlib.brotliCompress);
63
+ async function compressTextAsync(text) {
64
+ return asyncBrotliCompress(text, {
65
+ chunkSize: 32 * 1024,
66
+ params: {
67
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 6,
68
+ [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
69
+ }
70
+ });
71
+ }
72
+ var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
73
+ function errorText(error) {
74
+ return FLAKINESS_DBG ? error.stack : error.message;
75
+ }
76
+ async function retryWithBackoff(job, backoff = []) {
77
+ for (const timeout of backoff) {
78
+ try {
79
+ return await job();
80
+ } catch (e) {
81
+ if (e instanceof AggregateError)
82
+ console.error(`[flakiness.io err]`, errorText(e.errors[0]));
83
+ else if (e instanceof Error)
84
+ console.error(`[flakiness.io err]`, errorText(e));
85
+ else
86
+ console.error(`[flakiness.io err]`, e);
87
+ await new Promise((x) => setTimeout(x, timeout));
88
+ }
89
+ }
90
+ return await job();
91
+ }
92
+ function shell(command, args, options) {
93
+ try {
94
+ const result = spawnSync(command, args, { encoding: "utf-8", ...options });
95
+ if (result.status !== 0) {
96
+ return void 0;
97
+ }
98
+ return result.stdout.trim();
99
+ } catch (e) {
100
+ console.error(e);
101
+ return void 0;
102
+ }
103
+ }
104
+ function sha1Text(data) {
105
+ const hash = crypto.createHash("sha1");
106
+ hash.update(data);
107
+ return hash.digest("hex");
108
+ }
109
+ function sha1File(filePath) {
110
+ return new Promise((resolve2, reject) => {
111
+ const hash = crypto.createHash("sha1");
112
+ const stream = fs.createReadStream(filePath);
113
+ stream.on("data", (chunk) => {
114
+ hash.update(chunk);
115
+ });
116
+ stream.on("end", () => {
117
+ resolve2(hash.digest("hex"));
118
+ });
119
+ stream.on("error", (err) => {
120
+ reject(err);
121
+ });
122
+ });
123
+ }
124
+ function randomUUIDBase62() {
125
+ const BASE62_CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
126
+ let num = BigInt("0x" + crypto.randomUUID().replace(/-/g, ""));
127
+ if (num === 0n)
128
+ return BASE62_CHARSET[0];
129
+ const chars = [];
130
+ while (num > 0n) {
131
+ const remainder = Number(num % 62n);
132
+ num /= 62n;
133
+ chars.push(BASE62_CHARSET[remainder]);
134
+ }
135
+ return chars.reverse().join("");
136
+ }
137
+
138
+ // src/gitWorktree.ts
139
+ var log = debug("fk:git");
140
+ var execAsync = promisify(exec);
141
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
142
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
143
+ function toPosixAbsolutePath(absolutePath) {
144
+ if (IS_WIN32_PATH.test(absolutePath)) {
145
+ absolutePath = absolutePath.split(win32Path.sep).join(posixPath.sep);
146
+ }
147
+ if (IS_ALMOST_POSIX_PATH.test(absolutePath))
148
+ return "/" + absolutePath[0] + absolutePath.substring(2);
149
+ return absolutePath;
150
+ }
151
+ function toNativeAbsolutePath(posix) {
152
+ if (process.platform !== "win32")
153
+ return posix;
154
+ assert(posix.startsWith("/"), "The path must be absolute");
155
+ const m = posix.match(/^\/([a-zA-Z])(\/.*)?$/);
156
+ assert(m, `Invalid POSIX path: ${posix}`);
157
+ const drive = m[1];
158
+ const rest = (m[2] ?? "").split(posixPath.sep).join(win32Path.sep);
159
+ return drive.toUpperCase() + ":" + rest;
160
+ }
161
+ var GitWorktree = class _GitWorktree {
162
+ constructor(_gitRoot) {
163
+ this._gitRoot = _gitRoot;
164
+ this._posixGitRoot = toPosixAbsolutePath(this._gitRoot);
165
+ }
166
+ /**
167
+ * Creates a GitWorktree instance from any path inside a git repository.
168
+ *
169
+ * @param {string} somePathInsideGitRepo - Any path (file or directory) within a git repository.
170
+ * Can be absolute or relative. The function will locate the git root directory.
171
+ *
172
+ * @returns {GitWorktree} A new GitWorktree instance bound to the discovered git root.
173
+ *
174
+ * @throws {Error} Throws if the path is not inside a git repository or if git commands fail.
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * const worktree = GitWorktree.create('./src/my-test.ts');
179
+ * const gitRoot = worktree.rootPath();
180
+ * ```
181
+ */
182
+ static create(somePathInsideGitRepo) {
183
+ const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
184
+ cwd: somePathInsideGitRepo,
185
+ encoding: "utf-8"
186
+ });
187
+ assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
188
+ return new _GitWorktree(root);
189
+ }
190
+ _posixGitRoot;
191
+ /**
192
+ * Returns the native absolute path of the git repository root directory.
193
+ *
194
+ * @returns {string} Native absolute path to the git root. Format matches the current platform
195
+ * (Windows or POSIX).
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const root = worktree.rootPath();
200
+ * // On Windows: 'D:\project'
201
+ * // On Unix: '/project'
202
+ * ```
203
+ */
204
+ rootPath() {
205
+ return this._gitRoot;
206
+ }
207
+ /**
208
+ * Returns the commit ID (SHA-1 hash) of the current HEAD commit.
209
+ *
210
+ * @returns {FlakinessReport.CommitId} Full 40-character commit hash of the HEAD commit.
211
+ *
212
+ * @throws {Error} Throws if git command fails or repository is in an invalid state.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * const commitId = worktree.headCommitId();
217
+ * // Returns: 'a1b2c3d4e5f6...' (40-character SHA-1)
218
+ * ```
219
+ */
220
+ headCommitId() {
221
+ const sha = shell(`git`, ["rev-parse", "HEAD"], {
222
+ cwd: this._gitRoot,
223
+ encoding: "utf-8"
224
+ });
225
+ assert(sha, `FAILED: git rev-parse HEAD @ ${this._gitRoot}`);
226
+ return sha.trim();
227
+ }
228
+ /**
229
+ * Converts a native absolute path to a git-relative POSIX path.
230
+ *
231
+ * Takes any absolute path (Windows or POSIX format) and converts it to a POSIX path
232
+ * relative to the git repository root. This is essential for Flakiness reports where
233
+ * all file paths must be git-relative and use POSIX separators.
234
+ *
235
+ * @param {string} absolutePath - Native absolute path to convert. Can be in Windows format
236
+ * (e.g., `D:\project\src\test.ts`) or POSIX format (e.g., `/project/src/test.ts`).
237
+ *
238
+ * @returns {FlakinessReport.GitFilePath} POSIX path relative to git root (e.g., `src/test.ts`).
239
+ * Returns an empty string if the path is the git root itself.
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const gitPath = worktree.gitPath('/Users/project/src/test.ts');
244
+ * // Returns: 'src/test.ts'
245
+ * ```
246
+ */
247
+ gitPath(absolutePath) {
248
+ return posixPath.relative(this._posixGitRoot, toPosixAbsolutePath(absolutePath));
249
+ }
250
+ /**
251
+ * Converts a git-relative POSIX path to a native absolute path.
252
+ *
253
+ * Takes a POSIX path relative to the git root and converts it to the native absolute path
254
+ * format for the current platform (Windows or POSIX). This is the inverse of `gitPath()`.
255
+ *
256
+ * @param {FlakinessReport.GitFilePath} relativePath - POSIX path relative to git root
257
+ * (e.g., `src/test.ts`).
258
+ *
259
+ * @returns {string} Native absolute path. On Windows, returns Windows format (e.g., `D:\project\src\test.ts`).
260
+ * On POSIX systems, returns POSIX format (e.g., `/project/src/test.ts`).
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const absolutePath = worktree.absolutePath('src/test.ts');
265
+ * // On Windows: 'D:\project\src\test.ts'
266
+ * // On Unix: '/project/src/test.ts'
267
+ * ```
268
+ */
269
+ absolutePath(relativePath) {
270
+ return toNativeAbsolutePath(posixPath.join(this._posixGitRoot, relativePath));
271
+ }
272
+ /**
273
+ * Lists recent commits from the repository.
274
+ *
275
+ * Retrieves commit information including commit ID, timestamp, author, message, and parent commits.
276
+ * Note: CI environments often have shallow checkouts with limited history, which may affect
277
+ * the number of commits returned.
278
+ *
279
+ * @param {number} count - Maximum number of commits to retrieve, starting from HEAD.
280
+ *
281
+ * @returns {Promise<GitCommit[]>} Promise that resolves to an array of commit objects, ordered
282
+ * from most recent to oldest. Each commit includes:
283
+ * - `commitId` - Full commit hash
284
+ * - `timestamp` - Commit timestamp in milliseconds since Unix epoch
285
+ * - `message` - Commit message (subject line)
286
+ * - `author` - Author name
287
+ * - `parents` - Array of parent commit IDs
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const commits = await worktree.listCommits(10);
292
+ * console.log(`Latest commit: ${commits[0].message}`);
293
+ * ```
294
+ */
295
+ async listCommits(count) {
296
+ return await listCommits(this._gitRoot, "HEAD", count);
297
+ }
298
+ };
299
+ async function listCommits(gitRoot, head, count) {
300
+ const FIELD_SEPARATOR = "|~|";
301
+ const RECORD_SEPARATOR = "\0";
302
+ const prettyFormat = [
303
+ "%H",
304
+ // Full commit hash
305
+ "%ct",
306
+ // Commit timestamp (Unix seconds)
307
+ "%an",
308
+ // Author name
309
+ "%s",
310
+ // Subject line
311
+ "%P"
312
+ // Parent hashes (space-separated)
313
+ ].join(FIELD_SEPARATOR);
314
+ const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
315
+ try {
316
+ const { stdout } = await execAsync(command, { cwd: gitRoot });
317
+ if (!stdout) {
318
+ return [];
319
+ }
320
+ return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
321
+ const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
322
+ const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
323
+ return {
324
+ commitId,
325
+ timestamp: parseInt(timestampStr, 10) * 1e3,
326
+ author,
327
+ message,
328
+ parents,
329
+ walkIndex: 0
330
+ };
331
+ });
332
+ } catch (error) {
333
+ log(`Failed to list commits for repository at ${gitRoot}:`, error);
334
+ return [];
335
+ }
336
+ }
337
+
338
+ // src/reportUtils.ts
339
+ var reportUtils_exports = {};
340
+ __export(reportUtils_exports, {
341
+ createDataAttachment: () => createDataAttachment,
342
+ createEnvironment: () => createEnvironment,
343
+ createFileAttachment: () => createFileAttachment,
344
+ createTestStepSnippetsInplace: () => createTestStepSnippetsInplace,
345
+ normalizeReport: () => normalizeReport,
346
+ stripAnsi: () => stripAnsi,
347
+ visitTests: () => visitTests
348
+ });
349
+
350
+ // src/createEnvironment.ts
351
+ import fs2 from "fs";
352
+ import os from "os";
353
+ function readLinuxOSRelease() {
354
+ const osReleaseText = fs2.readFileSync("/etc/os-release", "utf-8");
355
+ return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
356
+ line = line.trim();
357
+ let [key, value] = line.split("=");
358
+ if (value.startsWith('"') && value.endsWith('"'))
359
+ value = value.substring(1, value.length - 1);
360
+ return [key, value];
361
+ }));
362
+ }
363
+ function osLinuxInfo() {
364
+ const arch = shell(`uname`, [`-m`]);
365
+ const osReleaseMap = readLinuxOSRelease();
366
+ const name = osReleaseMap.get("name") ?? shell(`uname`);
367
+ const version = osReleaseMap.get("version_id");
368
+ return { name, arch, version };
369
+ }
370
+ function osDarwinInfo() {
371
+ const name = "macos";
372
+ const arch = shell(`uname`, [`-m`]);
373
+ const version = shell(`sw_vers`, [`-productVersion`]);
374
+ return { name, arch, version };
375
+ }
376
+ function osWinInfo() {
377
+ const name = "win";
378
+ const arch = process.arch;
379
+ const version = os.release();
380
+ return { name, arch, version };
381
+ }
382
+ function getOSInfo() {
383
+ if (process.platform === "darwin")
384
+ return osDarwinInfo();
385
+ if (process.platform === "win32")
386
+ return osWinInfo();
387
+ return osLinuxInfo();
388
+ }
389
+ function extractEnvConfiguration() {
390
+ const ENV_PREFIX = "FK_ENV_";
391
+ return Object.fromEntries(
392
+ Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
393
+ );
394
+ }
395
+ function createEnvironment(options) {
396
+ const osInfo = getOSInfo();
397
+ return {
398
+ name: options.name,
399
+ systemData: {
400
+ osArch: osInfo.arch,
401
+ osName: osInfo.name,
402
+ osVersion: osInfo.version
403
+ },
404
+ userSuppliedData: {
405
+ ...extractEnvConfiguration(),
406
+ ...options.userSuppliedData ?? {}
407
+ },
408
+ opaqueData: options.opaqueData
409
+ };
410
+ }
411
+
412
+ // src/createTestStepSnippets.ts
413
+ import { codeFrameColumns } from "@babel/code-frame";
414
+ import fs3 from "fs";
415
+
416
+ // src/visitTests.ts
417
+ function visitTests(report, testVisitor) {
418
+ function visitSuite(suite, parents) {
419
+ parents.push(suite);
420
+ for (const test of suite.tests ?? [])
421
+ testVisitor(test, parents);
422
+ for (const childSuite of suite.suites ?? [])
423
+ visitSuite(childSuite, parents);
424
+ parents.pop();
425
+ }
426
+ for (const test of report.tests ?? [])
427
+ testVisitor(test, []);
428
+ for (const suite of report.suites)
429
+ visitSuite(suite, []);
430
+ }
431
+
432
+ // src/createTestStepSnippets.ts
433
+ function createTestStepSnippetsInplace(worktree, report) {
434
+ const allSteps = /* @__PURE__ */ new Map();
435
+ visitTests(report, (test) => {
436
+ for (const attempt of test.attempts) {
437
+ for (const step of attempt.steps ?? []) {
438
+ if (!step.location)
439
+ continue;
440
+ let fileSteps = allSteps.get(step.location.file);
441
+ if (!fileSteps) {
442
+ fileSteps = /* @__PURE__ */ new Set();
443
+ allSteps.set(step.location.file, fileSteps);
444
+ }
445
+ fileSteps.add(step);
446
+ }
447
+ }
448
+ });
449
+ for (const [gitFilePath, steps] of allSteps) {
450
+ let source;
451
+ try {
452
+ source = fs3.readFileSync(worktree.absolutePath(gitFilePath), "utf-8");
453
+ } catch (e) {
454
+ continue;
455
+ }
456
+ const lines = source.split("\n").length;
457
+ const highlighted = codeFrameColumns(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
458
+ const highlightedLines = highlighted.split("\n");
459
+ const lineWithArrow = highlightedLines[highlightedLines.length - 1];
460
+ for (const step of steps) {
461
+ if (!step.location)
462
+ continue;
463
+ if (step.location.line < 2 || step.location.line >= lines)
464
+ continue;
465
+ const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
466
+ const index = lineWithArrow.indexOf("^");
467
+ const shiftedArrow = lineWithArrow.slice(0, index) + " ".repeat(step.location.column - 1) + lineWithArrow.slice(index);
468
+ snippetLines.splice(2, 0, shiftedArrow);
469
+ step.snippet = snippetLines.join("\n");
470
+ }
471
+ }
472
+ }
473
+
474
+ // src/normalizeReport.ts
475
+ import stableObjectHash from "stable-hash";
476
+ var Multimap = class {
477
+ _map = /* @__PURE__ */ new Map();
478
+ set(key, value) {
479
+ const set = this._map.get(key) ?? /* @__PURE__ */ new Set();
480
+ this._map.set(key, set);
481
+ set.add(value);
482
+ }
483
+ getAll(key) {
484
+ return Array.from(this._map.get(key) ?? []);
485
+ }
486
+ };
487
+ function normalizeReport(report) {
488
+ const gEnvs = /* @__PURE__ */ new Map();
489
+ const gSuites = /* @__PURE__ */ new Map();
490
+ const gTests = new Multimap();
491
+ const gSuiteIds = /* @__PURE__ */ new Map();
492
+ const gTestIds = /* @__PURE__ */ new Map();
493
+ const gEnvIds = /* @__PURE__ */ new Map();
494
+ const gSuiteChildren = new Multimap();
495
+ const gSuiteTests = new Multimap();
496
+ for (const env of report.environments) {
497
+ const envId = computeEnvId(env);
498
+ gEnvs.set(envId, env);
499
+ gEnvIds.set(env, envId);
500
+ }
501
+ const usedEnvIds = /* @__PURE__ */ new Set();
502
+ function visitTests2(tests, suiteId) {
503
+ for (const test of tests ?? []) {
504
+ const testId = computeTestId(test, suiteId);
505
+ gTests.set(testId, test);
506
+ gTestIds.set(test, testId);
507
+ gSuiteTests.set(suiteId, test);
508
+ for (const attempt of test.attempts) {
509
+ const env = report.environments[attempt.environmentIdx];
510
+ const envId = gEnvIds.get(env);
511
+ usedEnvIds.add(envId);
512
+ }
513
+ }
514
+ }
515
+ function visitSuite(suite, parentSuiteId) {
516
+ const suiteId = computeSuiteId(suite, parentSuiteId);
517
+ gSuites.set(suiteId, suite);
518
+ gSuiteIds.set(suite, suiteId);
519
+ for (const childSuite of suite.suites ?? []) {
520
+ visitSuite(childSuite, suiteId);
521
+ gSuiteChildren.set(suiteId, childSuite);
522
+ }
523
+ visitTests2(suite.tests ?? [], suiteId);
524
+ }
525
+ function transformTests(tests) {
526
+ const testIds = new Set(tests.map((test) => gTestIds.get(test)));
527
+ return [...testIds].map((testId) => {
528
+ const tests2 = gTests.getAll(testId);
529
+ const tags = tests2.map((test) => test.tags ?? []).flat();
530
+ return {
531
+ location: tests2[0].location,
532
+ title: tests2[0].title,
533
+ tags: tags.length ? tags : void 0,
534
+ attempts: tests2.map((t) => t.attempts).flat().map((attempt) => ({
535
+ ...attempt,
536
+ environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
537
+ }))
538
+ };
539
+ });
540
+ }
541
+ function transformSuites(suites) {
542
+ const suiteIds = new Set(suites.map((suite) => gSuiteIds.get(suite)));
543
+ return [...suiteIds].map((suiteId) => {
544
+ const suite = gSuites.get(suiteId);
545
+ return {
546
+ location: suite.location,
547
+ title: suite.title,
548
+ type: suite.type,
549
+ suites: transformSuites(gSuiteChildren.getAll(suiteId)),
550
+ tests: transformTests(gSuiteTests.getAll(suiteId))
551
+ };
552
+ });
553
+ }
554
+ visitTests2(report.tests ?? [], "suiteless");
555
+ for (const suite of report.suites)
556
+ visitSuite(suite);
557
+ const newEnvironments = [...usedEnvIds];
558
+ const envIdToIndex = new Map(newEnvironments.map((envId, index) => [envId, index]));
559
+ return {
560
+ ...report,
561
+ environments: newEnvironments.map((envId) => gEnvs.get(envId)),
562
+ suites: transformSuites(report.suites),
563
+ tests: transformTests(report.tests ?? [])
564
+ };
565
+ }
566
+ function computeEnvId(env) {
567
+ return stableObjectHash(env);
568
+ }
569
+ function computeSuiteId(suite, parentSuiteId) {
570
+ return stableObjectHash({
571
+ parentSuiteId: parentSuiteId ?? "",
572
+ type: suite.type,
573
+ file: suite.location?.file ?? "",
574
+ title: suite.title
575
+ });
576
+ }
577
+ function computeTestId(test, suiteId) {
578
+ return stableObjectHash({
579
+ suiteId,
580
+ file: test.location?.file ?? "",
581
+ title: test.title
582
+ });
583
+ }
584
+
585
+ // src/stripAnsi.ts
586
+ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
587
+ function stripAnsi(str) {
588
+ return str.replace(ansiRegex, "");
589
+ }
590
+
591
+ // src/uploadReport.ts
592
+ import assert2 from "assert";
593
+ import fs4 from "fs";
594
+ import { URL as URL2 } from "url";
595
+ async function createFileAttachment(contentType, filePath) {
596
+ return {
597
+ type: "file",
598
+ contentType,
599
+ id: await sha1File(filePath),
600
+ path: filePath
601
+ };
602
+ }
603
+ async function createDataAttachment(contentType, data) {
604
+ return {
605
+ type: "buffer",
606
+ contentType,
607
+ id: sha1Text(data),
608
+ body: data
609
+ };
610
+ }
611
+ async function uploadReport(report, attachments, options) {
612
+ const flakinessAccessToken = options?.flakinessAccessToken ?? process.env["FLAKINESS_ACCESS_TOKEN"];
613
+ const flakinessEndpoint = options?.flakinessEndpoint ?? process.env["FLAKINESS_ENDPOINT"] ?? "https://flakiness.io";
614
+ const logger = options?.logger ?? console;
615
+ if (!flakinessAccessToken) {
616
+ const reason = "No FLAKINESS_ACCESS_TOKEN found";
617
+ if (process.env.CI)
618
+ logger.warn(`[flakiness.io] \u26A0 Skipping upload: ${reason}`);
619
+ return { status: "skipped", reason };
620
+ }
621
+ try {
622
+ const upload = new ReportUpload(report, attachments, { flakinessAccessToken, flakinessEndpoint });
623
+ const uploadResult = await upload.upload();
624
+ if (!uploadResult.success) {
625
+ const errorMessage = uploadResult.message || "Unknown upload error";
626
+ logger.error(`[flakiness.io] \u2715 Failed to upload: ${errorMessage}`);
627
+ if (options?.throwOnFailure)
628
+ throw new Error(`Flakiness upload failed: ${errorMessage}`);
629
+ return { status: "failed", error: errorMessage };
630
+ }
631
+ logger.log(`[flakiness.io] \u2713 Uploaded to ${uploadResult.reportUrl}`);
632
+ return { status: "success", reportUrl: uploadResult.reportUrl };
633
+ } catch (e) {
634
+ const errorMessage = e.message || String(e);
635
+ logger.error(`[flakiness.io] \u2715 Unexpected error during upload: ${errorMessage}`);
636
+ if (options?.throwOnFailure)
637
+ throw e;
638
+ return { status: "failed", error: errorMessage };
639
+ }
640
+ }
641
+ var HTTP_BACKOFF = [100, 500, 1e3, 1e3, 1e3, 1e3];
642
+ var ReportUpload = class {
643
+ _report;
644
+ _attachments;
645
+ _options;
646
+ constructor(report, attachments, options) {
647
+ this._options = options;
648
+ this._report = report;
649
+ this._attachments = attachments;
650
+ }
651
+ async _api(pathname, token, body) {
652
+ const url = new URL2(this._options.flakinessEndpoint);
653
+ url.pathname = pathname;
654
+ return await fetch(url, {
655
+ method: "POST",
656
+ headers: {
657
+ "Authorization": `Bearer ${token}`,
658
+ "Content-Type": "application/json"
659
+ },
660
+ body: body ? JSON.stringify(body) : void 0
661
+ }).then(async (response) => !response.ok ? {
662
+ result: void 0,
663
+ error: response.status + " " + url.href + " " + await response.text()
664
+ } : {
665
+ result: await response.json(),
666
+ error: void 0
667
+ }).catch((error) => ({
668
+ result: void 0,
669
+ error
670
+ }));
671
+ }
672
+ async upload() {
673
+ const response = await this._api("/api/upload/start", this._options.flakinessAccessToken);
674
+ if (response?.error || !response.result)
675
+ return { success: false, message: response.error };
676
+ const webUrl = new URL2(response.result.webUrl, this._options.flakinessEndpoint).toString();
677
+ const attachmentsPresignedUrls = await this._api("/api/upload/attachments", response.result.uploadToken, {
678
+ attachmentIds: this._attachments.map((a) => a.id)
679
+ });
680
+ if (attachmentsPresignedUrls?.error || !attachmentsPresignedUrls.result)
681
+ return { success: false, message: attachmentsPresignedUrls.error };
682
+ const attachments = new Map(attachmentsPresignedUrls.result.map((a) => [a.attachmentId, a.presignedUrl]));
683
+ await Promise.all([
684
+ this._uploadReport(JSON.stringify(this._report), response.result.presignedReportUrl),
685
+ ...this._attachments.map((attachment) => {
686
+ const uploadURL = attachments.get(attachment.id);
687
+ if (!uploadURL)
688
+ throw new Error("Internal error: missing upload URL for attachment!");
689
+ return this._uploadAttachment(attachment, uploadURL);
690
+ })
691
+ ]);
692
+ await this._api("/api/upload/finish", response.result.uploadToken);
693
+ return { success: true, reportUrl: webUrl };
694
+ }
695
+ async _uploadReport(data, uploadUrl) {
696
+ const compressed = await compressTextAsync(data);
697
+ const headers = {
698
+ "Content-Type": "application/json",
699
+ "Content-Length": Buffer.byteLength(compressed) + "",
700
+ "Content-Encoding": "br"
701
+ };
702
+ await retryWithBackoff(async () => {
703
+ const response = await fetch(uploadUrl, {
704
+ method: "PUT",
705
+ headers,
706
+ body: Buffer.from(compressed)
707
+ });
708
+ if (!response.ok) {
709
+ throw new Error(`Request to ${uploadUrl} failed with ${response.status}`);
710
+ }
711
+ await response.arrayBuffer();
712
+ }, HTTP_BACKOFF);
713
+ }
714
+ async _uploadAttachment(attachment, uploadUrl) {
715
+ const mimeType = attachment.contentType.toLocaleLowerCase().trim();
716
+ const compressable = mimeType.startsWith("text/") || mimeType.endsWith("+json") || mimeType.endsWith("+text") || mimeType.endsWith("+xml");
717
+ if (!compressable && attachment.type === "file") {
718
+ await retryWithBackoff(async () => {
719
+ const fileBuffer = await fs4.promises.readFile(attachment.path);
720
+ const response = await fetch(uploadUrl, {
721
+ method: "PUT",
722
+ headers: {
723
+ "Content-Type": attachment.contentType,
724
+ "Content-Length": fileBuffer.length + ""
725
+ },
726
+ body: new Uint8Array(fileBuffer)
727
+ });
728
+ if (!response.ok) {
729
+ throw new Error(`Request to ${uploadUrl} failed with ${response.status}`);
730
+ }
731
+ await response.arrayBuffer();
732
+ }, HTTP_BACKOFF);
733
+ return;
734
+ }
735
+ let buffer = attachment.type === "buffer" ? attachment.body : await fs4.promises.readFile(attachment.path);
736
+ assert2(buffer);
737
+ const encoding = compressable ? "br" : void 0;
738
+ if (compressable)
739
+ buffer = await compressTextAsync(buffer);
740
+ const headers = {
741
+ "Content-Type": attachment.contentType,
742
+ "Content-Length": Buffer.byteLength(buffer) + ""
743
+ };
744
+ if (encoding) {
745
+ headers["Content-Encoding"] = encoding;
746
+ }
747
+ await retryWithBackoff(async () => {
748
+ const response = await fetch(uploadUrl, {
749
+ method: "PUT",
750
+ headers,
751
+ body: new Uint8Array(buffer)
752
+ });
753
+ if (!response.ok) {
754
+ throw new Error(`Request to ${uploadUrl} failed with ${response.status}`);
755
+ }
756
+ await response.arrayBuffer();
757
+ }, HTTP_BACKOFF);
758
+ }
759
+ };
760
+
761
+ // src/systemUtilizationSampler.ts
762
+ import { spawnSync as spawnSync2 } from "child_process";
763
+ import os2 from "os";
764
+ function getAvailableMemMacOS() {
765
+ const lines = spawnSync2("vm_stat", { encoding: "utf8" }).stdout.trim().split("\n");
766
+ const pageSize = parseInt(lines[0].match(/page size of (\d+) bytes/)[1], 10);
767
+ if (isNaN(pageSize)) {
768
+ console.warn("[flakiness.io] Error detecting macos page size");
769
+ return 0;
770
+ }
771
+ let totalFree = 0;
772
+ for (const line of lines) {
773
+ if (/Pages (free|inactive|speculative):/.test(line)) {
774
+ const match = line.match(/\d+/);
775
+ if (match)
776
+ totalFree += parseInt(match[0], 10);
777
+ }
778
+ }
779
+ return totalFree * pageSize;
780
+ }
781
+ function getSystemUtilization() {
782
+ let idleTicks = 0;
783
+ let totalTicks = 0;
784
+ for (const cpu of os2.cpus()) {
785
+ totalTicks += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
786
+ idleTicks += cpu.times.idle;
787
+ }
788
+ return {
789
+ idleTicks,
790
+ totalTicks,
791
+ timestamp: Date.now(),
792
+ freeBytes: os2.platform() === "darwin" ? getAvailableMemMacOS() : os2.freemem()
793
+ };
794
+ }
795
+ function toFKUtilization(sample, previous) {
796
+ const idleTicks = sample.idleTicks - previous.idleTicks;
797
+ const totalTicks = sample.totalTicks - previous.totalTicks;
798
+ const cpuUtilization = Math.floor((1 - idleTicks / totalTicks) * 1e4) / 100;
799
+ const memoryUtilization = Math.floor((1 - sample.freeBytes / os2.totalmem()) * 1e4) / 100;
800
+ return {
801
+ cpuUtilization,
802
+ memoryUtilization,
803
+ dts: sample.timestamp - previous.timestamp
804
+ };
805
+ }
806
+ var SystemUtilizationSampler = class {
807
+ /**
808
+ * The accumulated system utilization data.
809
+ *
810
+ * This object is populated as samples are collected and can be directly included in
811
+ * Flakiness reports. It contains:
812
+ * - `samples` - Array of utilization samples with CPU/memory percentages and durations
813
+ * - `startTimestamp` - Timestamp when sampling began
814
+ * - `totalMemoryBytes` - Total system memory in bytes
815
+ */
816
+ result;
817
+ _lastSample = getSystemUtilization();
818
+ _timer;
819
+ /**
820
+ * Creates a new SystemUtilizationSampler and starts sampling immediately.
821
+ *
822
+ * The first sample is collected after 50ms, and subsequent samples are collected
823
+ * every 1000ms. Call `dispose()` to stop sampling and clean up resources.
824
+ */
825
+ constructor() {
826
+ this.result = {
827
+ samples: [],
828
+ startTimestamp: this._lastSample.timestamp,
829
+ totalMemoryBytes: os2.totalmem()
830
+ };
831
+ this._timer = setTimeout(this._addSample.bind(this), 50);
832
+ }
833
+ _addSample() {
834
+ const sample = getSystemUtilization();
835
+ this.result.samples.push(toFKUtilization(sample, this._lastSample));
836
+ this._lastSample = sample;
837
+ this._timer = setTimeout(this._addSample.bind(this), 1e3);
838
+ }
839
+ /**
840
+ * Stops sampling and cleans up resources.
841
+ *
842
+ * Call this method when you're done collecting utilization data to stop the sampling
843
+ * timer and prevent memory leaks. The `result` object remains accessible after disposal.
844
+ */
845
+ dispose() {
846
+ clearTimeout(this._timer);
847
+ }
848
+ };
849
+
850
+ // src/showReport.ts
851
+ import chalk from "chalk";
852
+ import open from "open";
853
+
854
+ // src/flakinessProjectConfig.ts
855
+ import fs5 from "fs";
856
+ import path from "path";
857
+ function createConfigPath(dir) {
858
+ return path.join(dir, ".flakiness", "config.json");
859
+ }
860
+ var gConfigPath;
861
+ function ensureConfigPath() {
862
+ if (!gConfigPath)
863
+ gConfigPath = computeConfigPath();
864
+ return gConfigPath;
865
+ }
866
+ function computeConfigPath() {
867
+ for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
868
+ const configPath = createConfigPath(p);
869
+ if (fs5.existsSync(configPath))
870
+ return configPath;
871
+ }
872
+ try {
873
+ const worktree = GitWorktree.create(process.cwd());
874
+ return createConfigPath(worktree.rootPath());
875
+ } catch (e) {
876
+ return createConfigPath(process.cwd());
877
+ }
878
+ }
879
+ var FlakinessProjectConfig = class _FlakinessProjectConfig {
880
+ constructor(_configPath, _config) {
881
+ this._configPath = _configPath;
882
+ this._config = _config;
883
+ }
884
+ /**
885
+ * Loads the Flakiness project configuration from disk.
886
+ *
887
+ * Searches for an existing `.flakiness/config.json` file starting from the current working
888
+ * directory and walking up the directory tree. If no config exists, it determines the
889
+ * appropriate location (git root or current directory) for future saves.
890
+ *
891
+ * @returns {Promise<FlakinessProjectConfig>} Promise that resolves to a FlakinessProjectConfig
892
+ * instance. If no config file exists, returns an instance with default/empty values.
893
+ *
894
+ * @example
895
+ * ```typescript
896
+ * const config = await FlakinessProjectConfig.load();
897
+ * const projectId = config.projectPublicId();
898
+ * ```
899
+ */
900
+ static async load() {
901
+ const configPath = ensureConfigPath();
902
+ const data = await fs5.promises.readFile(configPath, "utf-8").catch((e) => void 0);
903
+ const json = data ? JSON.parse(data) : {};
904
+ return new _FlakinessProjectConfig(configPath, json);
905
+ }
906
+ /**
907
+ * Creates a new empty Flakiness project configuration.
908
+ *
909
+ * Creates a configuration instance with no values set. Use this when you want to build
910
+ * a configuration from scratch. Call `save()` to persist it to disk.
911
+ *
912
+ * @returns {FlakinessProjectConfig} A new empty configuration instance.
913
+ *
914
+ * @example
915
+ * ```typescript
916
+ * const config = FlakinessProjectConfig.createEmpty();
917
+ * config.setProjectPublicId('my-project-id');
918
+ * await config.save();
919
+ * ```
920
+ */
921
+ static createEmpty() {
922
+ return new _FlakinessProjectConfig(ensureConfigPath(), {});
923
+ }
924
+ /**
925
+ * Returns the absolute path to the configuration file.
926
+ *
927
+ * @returns {string} Absolute path to `.flakiness/config.json`.
928
+ */
929
+ path() {
930
+ return this._configPath;
931
+ }
932
+ /**
933
+ * Returns the project's public ID, if configured.
934
+ *
935
+ * The project public ID is used to associate reports with a specific Flakiness.io project.
936
+ *
937
+ * @returns {string | undefined} Project public ID, or `undefined` if not set.
938
+ */
939
+ projectPublicId() {
940
+ return this._config.projectPublicId;
941
+ }
942
+ /**
943
+ * Returns the report viewer URL, either custom or default.
944
+ *
945
+ * @returns {string} Custom report viewer URL if configured, otherwise the default
946
+ * `https://report.flakiness.io`.
947
+ */
948
+ reportViewerUrl() {
949
+ return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
950
+ }
951
+ /**
952
+ * Sets or clears the custom report viewer URL.
953
+ *
954
+ * @param {string | undefined} url - Custom report viewer URL to use, or `undefined` to
955
+ * clear and use the default URL.
956
+ */
957
+ setCustomReportViewerUrl(url) {
958
+ if (url)
959
+ this._config.customReportViewerUrl = url;
960
+ else
961
+ delete this._config.customReportViewerUrl;
962
+ }
963
+ /**
964
+ * Sets the project's public ID.
965
+ *
966
+ * @param {string | undefined} projectId - Project public ID to set, or `undefined` to clear.
967
+ */
968
+ setProjectPublicId(projectId) {
969
+ this._config.projectPublicId = projectId;
970
+ }
971
+ /**
972
+ * Saves the configuration to disk.
973
+ *
974
+ * Writes the current configuration values to `.flakiness/config.json`. Creates the
975
+ * `.flakiness` directory if it doesn't exist.
976
+ *
977
+ * @returns {Promise<void>} Promise that resolves when the file has been written.
978
+ *
979
+ * @throws {Error} Throws if unable to create directories or write the file.
980
+ *
981
+ * @example
982
+ * ```typescript
983
+ * const config = await FlakinessProjectConfig.load();
984
+ * config.setProjectPublicId('my-project');
985
+ * await config.save();
986
+ * ```
987
+ */
988
+ async save() {
989
+ await fs5.promises.mkdir(path.dirname(this._configPath), { recursive: true });
990
+ await fs5.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
991
+ }
992
+ };
993
+
994
+ // src/staticServer.ts
995
+ import debug2 from "debug";
996
+ import * as fs6 from "fs";
997
+ import * as http from "http";
998
+ import * as path2 from "path";
999
+ var log2 = debug2("fk:static_server");
1000
+ var StaticServer = class {
1001
+ _server;
1002
+ _absoluteFolderPath;
1003
+ _pathPrefix;
1004
+ _cors;
1005
+ _mimeTypes = {
1006
+ ".html": "text/html",
1007
+ ".js": "text/javascript",
1008
+ ".css": "text/css",
1009
+ ".json": "application/json",
1010
+ ".png": "image/png",
1011
+ ".jpg": "image/jpeg",
1012
+ ".gif": "image/gif",
1013
+ ".svg": "image/svg+xml",
1014
+ ".ico": "image/x-icon",
1015
+ ".txt": "text/plain"
1016
+ };
1017
+ constructor(pathPrefix, folderPath, cors) {
1018
+ this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
1019
+ this._absoluteFolderPath = path2.resolve(folderPath);
1020
+ this._cors = cors;
1021
+ this._server = http.createServer((req, res) => this._handleRequest(req, res));
1022
+ }
1023
+ port() {
1024
+ const address = this._server.address();
1025
+ if (!address)
1026
+ return void 0;
1027
+ return address.port;
1028
+ }
1029
+ address() {
1030
+ const address = this._server.address();
1031
+ if (!address)
1032
+ return void 0;
1033
+ const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
1034
+ return `http://${displayHost}:${address.port}${this._pathPrefix}`;
1035
+ }
1036
+ async _startServer(port, host) {
1037
+ let okListener;
1038
+ let errListener;
1039
+ const result = new Promise((resolve2, reject) => {
1040
+ okListener = resolve2;
1041
+ errListener = reject;
1042
+ }).finally(() => {
1043
+ this._server.removeListener("listening", okListener);
1044
+ this._server.removeListener("error", errListener);
1045
+ });
1046
+ this._server.once("listening", okListener);
1047
+ this._server.once("error", errListener);
1048
+ this._server.listen(port, host);
1049
+ await result;
1050
+ log2('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
1051
+ }
1052
+ async start(port, host = "127.0.0.1") {
1053
+ if (port === 0) {
1054
+ await this._startServer(port, host);
1055
+ return this.address();
1056
+ }
1057
+ for (let i = 0; i < 20; ++i) {
1058
+ const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
1059
+ if (!err)
1060
+ return this.address();
1061
+ if (err.code !== "EADDRINUSE")
1062
+ throw err;
1063
+ log2("Port %d is busy (EADDRINUSE). Trying next port...", port);
1064
+ port = port + 1;
1065
+ if (port > 65535)
1066
+ port = 4e3;
1067
+ }
1068
+ log2("All sequential ports busy. Falling back to random port.");
1069
+ await this._startServer(0, host);
1070
+ return this.address();
1071
+ }
1072
+ stop() {
1073
+ return new Promise((resolve2, reject) => {
1074
+ this._server.close((err) => {
1075
+ if (err) {
1076
+ log2("Error stopping server: %o", err);
1077
+ reject(err);
1078
+ } else {
1079
+ log2("Server stopped.");
1080
+ resolve2();
1081
+ }
1082
+ });
1083
+ });
1084
+ }
1085
+ _errorResponse(req, res, code, text) {
1086
+ res.writeHead(code, { "Content-Type": "text/plain" });
1087
+ res.end(text);
1088
+ log2(`[${code}] ${req.method} ${req.url}`);
1089
+ }
1090
+ _handleRequest(req, res) {
1091
+ const { url, method } = req;
1092
+ if (this._cors) {
1093
+ res.setHeader("Access-Control-Allow-Headers", "*");
1094
+ res.setHeader("Access-Control-Allow-Origin", this._cors);
1095
+ res.setHeader("Access-Control-Allow-Methods", "*");
1096
+ if (req.method === "OPTIONS") {
1097
+ res.writeHead(204);
1098
+ res.end();
1099
+ return;
1100
+ }
1101
+ }
1102
+ if (method !== "GET") {
1103
+ this._errorResponse(req, res, 405, "Method Not Allowed");
1104
+ return;
1105
+ }
1106
+ req.on("aborted", () => log2(`ABORTED ${req.method} ${req.url}`));
1107
+ res.on("close", () => {
1108
+ if (!res.headersSent) log2(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
1109
+ });
1110
+ if (!url || !url.startsWith(this._pathPrefix)) {
1111
+ this._errorResponse(req, res, 404, "Not Found");
1112
+ return;
1113
+ }
1114
+ const relativePath = url.slice(this._pathPrefix.length);
1115
+ const safeSuffix = path2.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
1116
+ const filePath = path2.join(this._absoluteFolderPath, safeSuffix);
1117
+ if (!filePath.startsWith(this._absoluteFolderPath)) {
1118
+ this._errorResponse(req, res, 403, "Forbidden");
1119
+ return;
1120
+ }
1121
+ fs6.stat(filePath, (err, stats) => {
1122
+ if (err || !stats.isFile()) {
1123
+ this._errorResponse(req, res, 404, "File Not Found");
1124
+ return;
1125
+ }
1126
+ const ext = path2.extname(filePath).toLowerCase();
1127
+ const contentType = this._mimeTypes[ext] || "application/octet-stream";
1128
+ res.writeHead(200, { "Content-Type": contentType });
1129
+ log2(`[200] ${req.method} ${req.url} -> ${filePath}`);
1130
+ const readStream = fs6.createReadStream(filePath);
1131
+ readStream.pipe(res);
1132
+ readStream.on("error", (err2) => {
1133
+ log2("Stream error: %o", err2);
1134
+ res.end();
1135
+ });
1136
+ });
1137
+ }
1138
+ };
1139
+
1140
+ // src/showReport.ts
1141
+ async function showReport(reportFolder) {
1142
+ const config = await FlakinessProjectConfig.load();
1143
+ const projectPublicId = config.projectPublicId();
1144
+ const reportViewerEndpoint = config.reportViewerUrl();
1145
+ const token = randomUUIDBase62();
1146
+ const server = new StaticServer(token, reportFolder, reportViewerEndpoint);
1147
+ await server.start(9373, "127.0.0.1");
1148
+ const url = new URL(reportViewerEndpoint);
1149
+ url.searchParams.set("port", String(server.port()));
1150
+ url.searchParams.set("token", token);
1151
+ if (projectPublicId)
1152
+ url.searchParams.set("ppid", projectPublicId);
1153
+ console.log(chalk.cyan(`
1154
+ Serving Flakiness report at ${url.toString()}
1155
+ Press Ctrl+C to quit.`));
1156
+ await open(url.toString());
1157
+ await new Promise(() => {
1158
+ });
1159
+ }
1160
+
1161
+ // src/writeReport.ts
1162
+ import fs7 from "fs";
1163
+ import path3 from "path";
1164
+ async function writeReport(report, attachments, outputFolder) {
1165
+ const reportPath = path3.join(outputFolder, "report.json");
1166
+ const attachmentsFolder = path3.join(outputFolder, "attachments");
1167
+ await fs7.promises.rm(outputFolder, { recursive: true, force: true });
1168
+ await fs7.promises.mkdir(outputFolder, { recursive: true });
1169
+ await fs7.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
1170
+ if (attachments.length)
1171
+ await fs7.promises.mkdir(attachmentsFolder);
1172
+ const movedAttachments = [];
1173
+ for (const attachment of attachments) {
1174
+ const attachmentPath = path3.join(attachmentsFolder, attachment.id);
1175
+ if (attachment.type === "file")
1176
+ await fs7.promises.cp(attachment.path, attachmentPath);
1177
+ else if (attachment.type === "buffer")
1178
+ await fs7.promises.writeFile(attachmentPath, attachment.body);
1179
+ movedAttachments.push({
1180
+ type: "file",
1181
+ contentType: attachment.contentType,
1182
+ id: attachment.id,
1183
+ path: attachmentPath
1184
+ });
1185
+ }
1186
+ return movedAttachments;
1187
+ }
10
1188
  export {
11
1189
  CIUtils,
12
1190
  FlakinessProjectConfig,
13
1191
  FlakinessReport,
14
1192
  GitWorktree,
15
- ReportUtils,
1193
+ reportUtils_exports as ReportUtils,
16
1194
  SystemUtilizationSampler,
17
1195
  showReport,
18
1196
  uploadReport,