@flakiness/sdk 0.147.0 → 0.148.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
@@ -5,65 +5,14 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/index.ts
8
- import chalk from "chalk";
9
8
  import fs6 from "fs";
10
- import open from "open";
11
9
  import path3 from "path";
10
+ import { FlakinessReport } from "@flakiness/flakiness-report";
12
11
 
13
- // src/flakinessProjectConfig.ts
14
- import fs from "fs";
15
- import path from "path";
16
-
17
- // src/git.ts
18
- import assert from "assert";
19
-
20
- // src/pathutils.ts
21
- var pathutils_exports = {};
22
- __export(pathutils_exports, {
23
- gitFilePath: () => gitFilePath,
24
- normalizePath: () => normalizePath
25
- });
26
- import { posix as posixPath, win32 as win32Path } from "path";
27
- var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
28
- var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
29
- function normalizePath(aPath) {
30
- if (IS_WIN32_PATH.test(aPath)) {
31
- aPath = aPath.split(win32Path.sep).join(posixPath.sep);
32
- }
33
- if (IS_ALMOST_POSIX_PATH.test(aPath))
34
- return "/" + aPath[0] + aPath.substring(2);
35
- return aPath;
36
- }
37
- function gitFilePath(gitRoot, absolutePath) {
38
- return posixPath.relative(gitRoot, absolutePath);
39
- }
40
-
41
- // src/utils.ts
12
+ // src/createEnvironment.ts
42
13
  import { spawnSync } from "child_process";
43
- var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
44
- function errorText(error) {
45
- return FLAKINESS_DBG ? error.stack : error.message;
46
- }
47
- async function retryWithBackoff(job, backoff = []) {
48
- for (const timeout of backoff) {
49
- try {
50
- return await job();
51
- } catch (e) {
52
- if (e instanceof AggregateError)
53
- console.error(`[flakiness.io err]`, errorText(e.errors[0]));
54
- else if (e instanceof Error)
55
- console.error(`[flakiness.io err]`, errorText(e));
56
- else
57
- console.error(`[flakiness.io err]`, e);
58
- await new Promise((x) => setTimeout(x, timeout));
59
- }
60
- }
61
- return await job();
62
- }
63
- 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");
64
- function stripAnsi(str) {
65
- return str.replace(ansiRegex, "");
66
- }
14
+ import fs from "fs";
15
+ import os from "os";
67
16
  function shell(command, args, options) {
68
17
  try {
69
18
  const result = spawnSync(command, args, { encoding: "utf-8", ...options });
@@ -76,141 +25,69 @@ function shell(command, args, options) {
76
25
  return void 0;
77
26
  }
78
27
  }
79
-
80
- // src/git.ts
81
- function gitCommitInfo(gitRepo) {
82
- const sha = shell(`git`, ["rev-parse", "HEAD"], {
83
- cwd: gitRepo,
84
- encoding: "utf-8"
85
- });
86
- assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
87
- return sha.trim();
28
+ function readLinuxOSRelease() {
29
+ const osReleaseText = fs.readFileSync("/etc/os-release", "utf-8");
30
+ return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
31
+ line = line.trim();
32
+ let [key, value] = line.split("=");
33
+ if (value.startsWith('"') && value.endsWith('"'))
34
+ value = value.substring(1, value.length - 1);
35
+ return [key, value];
36
+ }));
88
37
  }
89
- function computeGitRoot(somePathInsideGitRepo) {
90
- const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
91
- cwd: somePathInsideGitRepo,
92
- encoding: "utf-8"
93
- });
94
- assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
95
- return normalizePath(root);
38
+ function osLinuxInfo() {
39
+ const arch = shell(`uname`, [`-m`]);
40
+ const osReleaseMap = readLinuxOSRelease();
41
+ const name = osReleaseMap.get("name") ?? shell(`uname`);
42
+ const version = osReleaseMap.get("version_id");
43
+ return { name, arch, version };
96
44
  }
97
-
98
- // src/flakinessProjectConfig.ts
99
- function createConfigPath(dir) {
100
- return path.join(dir, ".flakiness", "config.json");
45
+ function osDarwinInfo() {
46
+ const name = "macos";
47
+ const arch = shell(`uname`, [`-m`]);
48
+ const version = shell(`sw_vers`, [`-productVersion`]);
49
+ return { name, arch, version };
101
50
  }
102
- var gConfigPath;
103
- function ensureConfigPath() {
104
- if (!gConfigPath)
105
- gConfigPath = computeConfigPath();
106
- return gConfigPath;
51
+ function osWinInfo() {
52
+ const name = "win";
53
+ const arch = process.arch;
54
+ const version = os.release();
55
+ return { name, arch, version };
107
56
  }
108
- function computeConfigPath() {
109
- for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
110
- const configPath = createConfigPath(p);
111
- if (fs.existsSync(configPath))
112
- return configPath;
113
- }
114
- try {
115
- const gitRoot = computeGitRoot(process.cwd());
116
- return createConfigPath(gitRoot);
117
- } catch (e) {
118
- return createConfigPath(process.cwd());
119
- }
57
+ function getOSInfo() {
58
+ if (process.platform === "darwin")
59
+ return osDarwinInfo();
60
+ if (process.platform === "win32")
61
+ return osWinInfo();
62
+ return osLinuxInfo();
63
+ }
64
+ function extractEnvConfiguration() {
65
+ const ENV_PREFIX = "FK_ENV_";
66
+ return Object.fromEntries(
67
+ Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
68
+ );
69
+ }
70
+ function createEnvironment(options) {
71
+ const osInfo = getOSInfo();
72
+ return {
73
+ name: options.name,
74
+ systemData: {
75
+ osArch: osInfo.arch,
76
+ osName: osInfo.name,
77
+ osVersion: osInfo.version
78
+ },
79
+ userSuppliedData: {
80
+ ...extractEnvConfiguration(),
81
+ ...options.userSuppliedData ?? {}
82
+ },
83
+ opaqueData: options.opaqueData
84
+ };
120
85
  }
121
- var FlakinessProjectConfig = class _FlakinessProjectConfig {
122
- constructor(_configPath, _config) {
123
- this._configPath = _configPath;
124
- this._config = _config;
125
- }
126
- static async load() {
127
- const configPath = ensureConfigPath();
128
- const data = await fs.promises.readFile(configPath, "utf-8").catch((e) => void 0);
129
- const json = data ? JSON.parse(data) : {};
130
- return new _FlakinessProjectConfig(configPath, json);
131
- }
132
- static createEmpty() {
133
- return new _FlakinessProjectConfig(ensureConfigPath(), {});
134
- }
135
- path() {
136
- return this._configPath;
137
- }
138
- projectPublicId() {
139
- return this._config.projectPublicId;
140
- }
141
- reportViewerEndpoint() {
142
- return this._config.customReportViewerEndpoint ?? "https://report.flakiness.io";
143
- }
144
- setProjectPublicId(projectId) {
145
- this._config.projectPublicId = projectId;
146
- }
147
- async save() {
148
- await fs.promises.mkdir(path.dirname(this._configPath), { recursive: true });
149
- await fs.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
150
- }
151
- };
152
-
153
- // src/localReportServer.ts
154
- import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
155
- import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
156
- import { createTypedHttpExpressMiddleware } from "@flakiness/shared/node/typedHttpExpress.js";
157
- import bodyParser from "body-parser";
158
- import compression from "compression";
159
- import debug2 from "debug";
160
- import express from "express";
161
- import "express-async-errors";
162
- import http from "http";
163
86
 
164
- // src/localReportApi.ts
165
- import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
87
+ // src/createTestStepSnippets.ts
88
+ import { codeFrameColumns } from "@babel/code-frame";
166
89
  import fs2 from "fs";
167
- import path2 from "path";
168
- import { z } from "zod/v4";
169
-
170
- // src/localGit.ts
171
- import { exec } from "child_process";
172
- import debug from "debug";
173
- import { promisify } from "util";
174
- var log = debug("fk:git");
175
- var execAsync = promisify(exec);
176
- async function listLocalCommits(gitRoot, head, count) {
177
- const FIELD_SEPARATOR = "|~|";
178
- const RECORD_SEPARATOR = "\0";
179
- const prettyFormat = [
180
- "%H",
181
- // %H: Full commit hash
182
- "%at",
183
- // %at: Author date as a Unix timestamp (seconds since epoch)
184
- "%an",
185
- // %an: Author name
186
- "%s",
187
- // %s: Subject (the first line of the commit message)
188
- "%P"
189
- // %P: Parent hashes (space-separated)
190
- ].join(FIELD_SEPARATOR);
191
- const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
192
- try {
193
- const { stdout } = await execAsync(command, { cwd: gitRoot });
194
- if (!stdout) {
195
- return [];
196
- }
197
- return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
198
- const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
199
- const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
200
- return {
201
- commitId,
202
- timestamp: parseInt(timestampStr, 10) * 1e3,
203
- author,
204
- message,
205
- parents,
206
- walkIndex: 0
207
- };
208
- });
209
- } catch (error) {
210
- log(`Failed to list commits for repository at ${gitRoot}:`, error);
211
- return [];
212
- }
213
- }
90
+ import { posix as posixPath } from "path";
214
91
 
215
92
  // src/reportUtils.ts
216
93
  import { Multimap } from "@flakiness/shared/common/multimap.js";
@@ -279,7 +156,7 @@ var ReportUtils;
279
156
  location: tests2[0].location,
280
157
  title: tests2[0].title,
281
158
  tags: tags.length ? tags : void 0,
282
- attempts: tests2.map((t2) => t2.attempts).flat().map((attempt) => ({
159
+ attempts: tests2.map((t) => t.attempts).flat().map((attempt) => ({
283
160
  ...attempt,
284
161
  environmentIdx: envIdToIndex.get(gEnvIds.get(report.environments[attempt.environmentIdx]))
285
162
  }))
@@ -332,254 +209,7 @@ var ReportUtils;
332
209
  }
333
210
  })(ReportUtils || (ReportUtils = {}));
334
211
 
335
- // src/localReportApi.ts
336
- var ReportInfo = class {
337
- constructor(_options) {
338
- this._options = _options;
339
- }
340
- report;
341
- attachmentIdToPath = /* @__PURE__ */ new Map();
342
- commits = [];
343
- async refresh() {
344
- const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
345
- if (!report) {
346
- this.report = void 0;
347
- this.commits = [];
348
- this.attachmentIdToPath = /* @__PURE__ */ new Map();
349
- return;
350
- }
351
- if (JSON.stringify(report) === JSON.stringify(this.report))
352
- return;
353
- this.report = report;
354
- this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
355
- const attachmentsDir = this._options.attachmentsFolder;
356
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
357
- if (missingAttachments.length) {
358
- const first = missingAttachments.slice(0, 3);
359
- for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
360
- console.warn(`Missing attachment with id ${missingAttachments[i]}`);
361
- if (missingAttachments.length > 3)
362
- console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
363
- }
364
- this.attachmentIdToPath = attachmentIdToPath;
365
- }
366
- };
367
- var t = TypedHTTP.Router.create();
368
- var localReportRouter = {
369
- ping: t.get({
370
- handler: async () => {
371
- return "pong";
372
- }
373
- }),
374
- lastCommits: t.get({
375
- handler: async ({ ctx }) => {
376
- return ctx.reportInfo.commits;
377
- }
378
- }),
379
- report: {
380
- attachment: t.rawMethod("GET", {
381
- input: z.object({
382
- attachmentId: z.string().min(1).max(100).transform((id) => id)
383
- }),
384
- handler: async ({ ctx, input }) => {
385
- const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
386
- if (!idx)
387
- throw TypedHTTP.HttpError.withCode("NOT_FOUND");
388
- const buffer = await fs2.promises.readFile(idx.path);
389
- return TypedHTTP.ok(buffer, idx.contentType);
390
- }
391
- }),
392
- json: t.get({
393
- handler: async ({ ctx }) => {
394
- await ctx.reportInfo.refresh();
395
- return ctx.reportInfo.report;
396
- }
397
- })
398
- }
399
- };
400
- async function resolveAttachmentPaths(report, attachmentsDir) {
401
- const attachmentFiles = await listFilesRecursively(attachmentsDir);
402
- const filenameToPath = new Map(attachmentFiles.map((file) => [path2.basename(file), file]));
403
- const attachmentIdToPath = /* @__PURE__ */ new Map();
404
- const missingAttachments = /* @__PURE__ */ new Set();
405
- ReportUtils.visitTests(report, (test) => {
406
- for (const attempt of test.attempts) {
407
- for (const attachment of attempt.attachments ?? []) {
408
- const attachmentPath = filenameToPath.get(attachment.id);
409
- if (!attachmentPath) {
410
- missingAttachments.add(attachment.id);
411
- } else {
412
- attachmentIdToPath.set(attachment.id, {
413
- contentType: attachment.contentType,
414
- id: attachment.id,
415
- path: attachmentPath
416
- });
417
- }
418
- }
419
- }
420
- });
421
- return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
422
- }
423
- async function listFilesRecursively(dir, result = []) {
424
- const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
425
- for (const entry of entries) {
426
- const fullPath = path2.join(dir, entry.name);
427
- if (entry.isDirectory())
428
- await listFilesRecursively(fullPath, result);
429
- else
430
- result.push(fullPath);
431
- }
432
- return result;
433
- }
434
-
435
- // src/localReportServer.ts
436
- var logHTTPServer = debug2("fk:http");
437
- var LocalReportServer = class _LocalReportServer {
438
- constructor(_server, _port, _authToken) {
439
- this._server = _server;
440
- this._port = _port;
441
- this._authToken = _authToken;
442
- }
443
- static async create(options) {
444
- const app = express();
445
- app.set("etag", false);
446
- const authToken = randomUUIDBase62();
447
- app.use(compression());
448
- app.use(bodyParser.json({ limit: 256 * 1024 }));
449
- app.use((req, res, next) => {
450
- if (!req.path.startsWith("/" + authToken))
451
- throw TypedHTTP2.HttpError.withCode("UNAUTHORIZED");
452
- res.setHeader("Access-Control-Allow-Headers", "*");
453
- res.setHeader("Access-Control-Allow-Origin", options.endpoint);
454
- res.setHeader("Access-Control-Allow-Methods", "*");
455
- if (req.method === "OPTIONS") {
456
- res.writeHead(204);
457
- res.end();
458
- return;
459
- }
460
- req.on("aborted", () => logHTTPServer(`REQ ABORTED ${req.method} ${req.originalUrl}`));
461
- res.on("close", () => {
462
- if (!res.headersSent) logHTTPServer(`RES CLOSED BEFORE SEND ${req.method} ${req.originalUrl}`);
463
- });
464
- next();
465
- });
466
- const reportInfo = new ReportInfo(options);
467
- app.use("/" + authToken, createTypedHttpExpressMiddleware({
468
- router: localReportRouter,
469
- createRootContext: async ({ req, res, input }) => ({ reportInfo })
470
- }));
471
- app.use((err, req, res, next) => {
472
- if (err instanceof TypedHTTP2.HttpError)
473
- return res.status(err.status).send({ error: err.message });
474
- logHTTPServer(err);
475
- res.status(500).send({ error: "Internal Server Error" });
476
- });
477
- const server = http.createServer(app);
478
- server.on("error", (err) => {
479
- if (err.code === "ECONNRESET") {
480
- logHTTPServer("Client connection reset. Ignoring.");
481
- return;
482
- }
483
- throw err;
484
- });
485
- const port = await new Promise((resolve) => server.listen(options.port, () => {
486
- resolve(server.address().port);
487
- }));
488
- return new _LocalReportServer(server, port, authToken);
489
- }
490
- authToken() {
491
- return this._authToken;
492
- }
493
- port() {
494
- return this._port;
495
- }
496
- async dispose() {
497
- await new Promise((x) => this._server.close(x));
498
- }
499
- };
500
-
501
- // src/index.ts
502
- import { FlakinessReport } from "@flakiness/flakiness-report";
503
-
504
- // src/createEnvironment.ts
505
- import { spawnSync as spawnSync2 } from "child_process";
506
- import fs3 from "fs";
507
- import os from "os";
508
- function shell2(command, args, options) {
509
- try {
510
- const result = spawnSync2(command, args, { encoding: "utf-8", ...options });
511
- if (result.status !== 0) {
512
- return void 0;
513
- }
514
- return result.stdout.trim();
515
- } catch (e) {
516
- console.error(e);
517
- return void 0;
518
- }
519
- }
520
- function readLinuxOSRelease() {
521
- const osReleaseText = fs3.readFileSync("/etc/os-release", "utf-8");
522
- return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
523
- line = line.trim();
524
- let [key, value] = line.split("=");
525
- if (value.startsWith('"') && value.endsWith('"'))
526
- value = value.substring(1, value.length - 1);
527
- return [key, value];
528
- }));
529
- }
530
- function osLinuxInfo() {
531
- const arch = shell2(`uname`, [`-m`]);
532
- const osReleaseMap = readLinuxOSRelease();
533
- const name = osReleaseMap.get("name") ?? shell2(`uname`);
534
- const version = osReleaseMap.get("version_id");
535
- return { name, arch, version };
536
- }
537
- function osDarwinInfo() {
538
- const name = "macos";
539
- const arch = shell2(`uname`, [`-m`]);
540
- const version = shell2(`sw_vers`, [`-productVersion`]);
541
- return { name, arch, version };
542
- }
543
- function osWinInfo() {
544
- const name = "win";
545
- const arch = process.arch;
546
- const version = os.release();
547
- return { name, arch, version };
548
- }
549
- function getOSInfo() {
550
- if (process.platform === "darwin")
551
- return osDarwinInfo();
552
- if (process.platform === "win32")
553
- return osWinInfo();
554
- return osLinuxInfo();
555
- }
556
- function extractEnvConfiguration() {
557
- const ENV_PREFIX = "FK_ENV_";
558
- return Object.fromEntries(
559
- Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
560
- );
561
- }
562
- function createEnvironment(options) {
563
- const osInfo = getOSInfo();
564
- return {
565
- name: options.name,
566
- systemData: {
567
- osArch: osInfo.arch,
568
- osName: osInfo.name,
569
- osVersion: osInfo.version
570
- },
571
- userSuppliedData: {
572
- ...extractEnvConfiguration(),
573
- ...options.userSuppliedData ?? {}
574
- },
575
- opaqueData: options.opaqueData
576
- };
577
- }
578
-
579
212
  // src/createTestStepSnippets.ts
580
- import { codeFrameColumns } from "@babel/code-frame";
581
- import fs4 from "fs";
582
- import { posix as posixPath2 } from "path";
583
213
  function createTestStepSnippetsInplace(report, gitRoot) {
584
214
  const allSteps = /* @__PURE__ */ new Map();
585
215
  ReportUtils.visitTests(report, (test) => {
@@ -599,7 +229,7 @@ function createTestStepSnippetsInplace(report, gitRoot) {
599
229
  for (const [gitFilePath2, steps] of allSteps) {
600
230
  let source;
601
231
  try {
602
- source = fs4.readFileSync(posixPath2.join(gitRoot, gitFilePath2), "utf-8");
232
+ source = fs2.readFileSync(posixPath.join(gitRoot, gitFilePath2), "utf-8");
603
233
  } catch (e) {
604
234
  continue;
605
235
  }
@@ -621,34 +251,180 @@ function createTestStepSnippetsInplace(report, gitRoot) {
621
251
  }
622
252
  }
623
253
 
254
+ // src/flakinessProjectConfig.ts
255
+ import fs3 from "fs";
256
+ import path from "path";
257
+
258
+ // src/git.ts
259
+ import assert from "assert";
260
+
261
+ // src/pathutils.ts
262
+ var pathutils_exports = {};
263
+ __export(pathutils_exports, {
264
+ gitFilePath: () => gitFilePath,
265
+ normalizePath: () => normalizePath
266
+ });
267
+ import { posix as posixPath2, win32 as win32Path } from "path";
268
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
269
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
270
+ function normalizePath(aPath) {
271
+ if (IS_WIN32_PATH.test(aPath)) {
272
+ aPath = aPath.split(win32Path.sep).join(posixPath2.sep);
273
+ }
274
+ if (IS_ALMOST_POSIX_PATH.test(aPath))
275
+ return "/" + aPath[0] + aPath.substring(2);
276
+ return aPath;
277
+ }
278
+ function gitFilePath(gitRoot, absolutePath) {
279
+ return posixPath2.relative(gitRoot, absolutePath);
280
+ }
281
+
282
+ // src/utils.ts
283
+ import { spawnSync as spawnSync2 } from "child_process";
284
+ var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
285
+ function errorText(error) {
286
+ return FLAKINESS_DBG ? error.stack : error.message;
287
+ }
288
+ async function retryWithBackoff(job, backoff = []) {
289
+ for (const timeout of backoff) {
290
+ try {
291
+ return await job();
292
+ } catch (e) {
293
+ if (e instanceof AggregateError)
294
+ console.error(`[flakiness.io err]`, errorText(e.errors[0]));
295
+ else if (e instanceof Error)
296
+ console.error(`[flakiness.io err]`, errorText(e));
297
+ else
298
+ console.error(`[flakiness.io err]`, e);
299
+ await new Promise((x) => setTimeout(x, timeout));
300
+ }
301
+ }
302
+ return await job();
303
+ }
304
+ 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");
305
+ function stripAnsi(str) {
306
+ return str.replace(ansiRegex, "");
307
+ }
308
+ function shell2(command, args, options) {
309
+ try {
310
+ const result = spawnSync2(command, args, { encoding: "utf-8", ...options });
311
+ if (result.status !== 0) {
312
+ return void 0;
313
+ }
314
+ return result.stdout.trim();
315
+ } catch (e) {
316
+ console.error(e);
317
+ return void 0;
318
+ }
319
+ }
320
+
321
+ // src/git.ts
322
+ function gitCommitInfo(gitRepo) {
323
+ const sha = shell2(`git`, ["rev-parse", "HEAD"], {
324
+ cwd: gitRepo,
325
+ encoding: "utf-8"
326
+ });
327
+ assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
328
+ return sha.trim();
329
+ }
330
+ function computeGitRoot(somePathInsideGitRepo) {
331
+ const root = shell2(`git`, ["rev-parse", "--show-toplevel"], {
332
+ cwd: somePathInsideGitRepo,
333
+ encoding: "utf-8"
334
+ });
335
+ assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
336
+ return normalizePath(root);
337
+ }
338
+
339
+ // src/flakinessProjectConfig.ts
340
+ function createConfigPath(dir) {
341
+ return path.join(dir, ".flakiness", "config.json");
342
+ }
343
+ var gConfigPath;
344
+ function ensureConfigPath() {
345
+ if (!gConfigPath)
346
+ gConfigPath = computeConfigPath();
347
+ return gConfigPath;
348
+ }
349
+ function computeConfigPath() {
350
+ for (let p = process.cwd(); p !== path.resolve(p, ".."); p = path.resolve(p, "..")) {
351
+ const configPath = createConfigPath(p);
352
+ if (fs3.existsSync(configPath))
353
+ return configPath;
354
+ }
355
+ try {
356
+ const gitRoot = computeGitRoot(process.cwd());
357
+ return createConfigPath(gitRoot);
358
+ } catch (e) {
359
+ return createConfigPath(process.cwd());
360
+ }
361
+ }
362
+ var FlakinessProjectConfig = class _FlakinessProjectConfig {
363
+ constructor(_configPath, _config) {
364
+ this._configPath = _configPath;
365
+ this._config = _config;
366
+ }
367
+ static async load() {
368
+ const configPath = ensureConfigPath();
369
+ const data = await fs3.promises.readFile(configPath, "utf-8").catch((e) => void 0);
370
+ const json = data ? JSON.parse(data) : {};
371
+ return new _FlakinessProjectConfig(configPath, json);
372
+ }
373
+ static createEmpty() {
374
+ return new _FlakinessProjectConfig(ensureConfigPath(), {});
375
+ }
376
+ path() {
377
+ return this._configPath;
378
+ }
379
+ projectPublicId() {
380
+ return this._config.projectPublicId;
381
+ }
382
+ reportViewerUrl() {
383
+ return this._config.customReportViewerUrl ?? "https://report.flakiness.io";
384
+ }
385
+ setCustomReportViewerUrl(url) {
386
+ if (url)
387
+ this._config.customReportViewerUrl = url;
388
+ else
389
+ delete this._config.customReportViewerUrl;
390
+ }
391
+ setProjectPublicId(projectId) {
392
+ this._config.projectPublicId = projectId;
393
+ }
394
+ async save() {
395
+ await fs3.promises.mkdir(path.dirname(this._configPath), { recursive: true });
396
+ await fs3.promises.writeFile(this._configPath, JSON.stringify(this._config, null, 2));
397
+ }
398
+ };
399
+
624
400
  // src/reportUploader.ts
625
401
  import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
626
402
  import assert2 from "assert";
627
403
  import crypto from "crypto";
628
- import fs5 from "fs";
404
+ import fs4 from "fs";
629
405
  import { URL as URL2 } from "url";
630
406
 
631
407
  // src/httpUtils.ts
632
- import http2 from "http";
408
+ import http from "http";
633
409
  import https from "https";
634
410
  var FLAKINESS_DBG2 = !!process.env.FLAKINESS_DBG;
635
411
  var httpUtils;
636
412
  ((httpUtils2) => {
637
413
  function createRequest({ url, method = "get", headers = {} }) {
638
- let resolve;
414
+ let resolve2;
639
415
  let reject;
640
416
  const responseDataPromise = new Promise((a, b) => {
641
- resolve = a;
417
+ resolve2 = a;
642
418
  reject = b;
643
419
  });
644
- const protocol = url.startsWith("https") ? https : http2;
420
+ const protocol = url.startsWith("https") ? https : http;
645
421
  headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
646
422
  const request = protocol.request(url, { method, headers }, (res) => {
647
423
  const chunks = [];
648
424
  res.on("data", (chunk) => chunks.push(chunk));
649
425
  res.on("end", () => {
650
426
  if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
651
- resolve(Buffer.concat(chunks));
427
+ resolve2(Buffer.concat(chunks));
652
428
  else
653
429
  reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
654
430
  });
@@ -697,14 +473,14 @@ var httpUtils;
697
473
 
698
474
  // src/reportUploader.ts
699
475
  function sha1File(filePath) {
700
- return new Promise((resolve, reject) => {
476
+ return new Promise((resolve2, reject) => {
701
477
  const hash = crypto.createHash("sha1");
702
- const stream = fs5.createReadStream(filePath);
478
+ const stream = fs4.createReadStream(filePath);
703
479
  stream.on("data", (chunk) => {
704
480
  hash.update(chunk);
705
481
  });
706
482
  stream.on("end", () => {
707
- resolve(hash.digest("hex"));
483
+ resolve2(hash.digest("hex"));
708
484
  });
709
485
  stream.on("error", (err) => {
710
486
  reject(err);
@@ -845,16 +621,16 @@ var ReportUpload = class {
845
621
  url: uploadUrl,
846
622
  headers: {
847
623
  "Content-Type": attachment.contentType,
848
- "Content-Length": (await fs5.promises.stat(attachmentPath)).size + ""
624
+ "Content-Length": (await fs4.promises.stat(attachmentPath)).size + ""
849
625
  },
850
626
  method: "put"
851
627
  });
852
- fs5.createReadStream(attachmentPath).pipe(request);
628
+ fs4.createReadStream(attachmentPath).pipe(request);
853
629
  await responseDataPromise;
854
630
  }, HTTP_BACKOFF);
855
631
  return;
856
632
  }
857
- let buffer = attachment.body ? attachment.body : attachment.path ? await fs5.promises.readFile(attachment.path) : void 0;
633
+ let buffer = attachment.body ? attachment.body : attachment.path ? await fs4.promises.readFile(attachment.path) : void 0;
858
634
  assert2(buffer);
859
635
  const encoding = compressable ? "br" : void 0;
860
636
  if (compressable)
@@ -877,6 +653,178 @@ var ReportUpload = class {
877
653
  }
878
654
  };
879
655
 
656
+ // src/showReport.ts
657
+ import { randomUUIDBase62 } from "@flakiness/shared/node/nodeutils.js";
658
+ import chalk from "chalk";
659
+ import open from "open";
660
+
661
+ // src/staticServer.ts
662
+ import debug from "debug";
663
+ import * as fs5 from "fs";
664
+ import * as http2 from "http";
665
+ import * as path2 from "path";
666
+ var log = debug("fk:static_server");
667
+ var StaticServer = class {
668
+ _server;
669
+ _absoluteFolderPath;
670
+ _pathPrefix;
671
+ _cors;
672
+ _mimeTypes = {
673
+ ".html": "text/html",
674
+ ".js": "text/javascript",
675
+ ".css": "text/css",
676
+ ".json": "application/json",
677
+ ".png": "image/png",
678
+ ".jpg": "image/jpeg",
679
+ ".gif": "image/gif",
680
+ ".svg": "image/svg+xml",
681
+ ".ico": "image/x-icon",
682
+ ".txt": "text/plain"
683
+ };
684
+ constructor(pathPrefix, folderPath, cors) {
685
+ this._pathPrefix = "/" + pathPrefix.replace(/^\//, "").replace(/\/$/, "");
686
+ this._absoluteFolderPath = path2.resolve(folderPath);
687
+ this._cors = cors;
688
+ this._server = http2.createServer((req, res) => this._handleRequest(req, res));
689
+ }
690
+ port() {
691
+ const address = this._server.address();
692
+ if (!address)
693
+ return void 0;
694
+ return address.port;
695
+ }
696
+ address() {
697
+ const address = this._server.address();
698
+ if (!address)
699
+ return void 0;
700
+ const displayHost = address.address.includes(":") ? `[${address.address}]` : address.address;
701
+ return `http://${displayHost}:${address.port}${this._pathPrefix}`;
702
+ }
703
+ async _startServer(port, host) {
704
+ let okListener;
705
+ let errListener;
706
+ const result = new Promise((resolve2, reject) => {
707
+ okListener = resolve2;
708
+ errListener = reject;
709
+ }).finally(() => {
710
+ this._server.removeListener("listening", okListener);
711
+ this._server.removeListener("error", errListener);
712
+ });
713
+ this._server.once("listening", okListener);
714
+ this._server.once("error", errListener);
715
+ this._server.listen(port, host);
716
+ await result;
717
+ log('Serving "%s" on "%s"', this._absoluteFolderPath, this.address());
718
+ }
719
+ async start(port, host = "127.0.0.1") {
720
+ if (port === 0) {
721
+ await this._startServer(port, host);
722
+ return this.address();
723
+ }
724
+ for (let i = 0; i < 20; ++i) {
725
+ const err = await this._startServer(port, host).then(() => void 0).catch((e) => e);
726
+ if (!err)
727
+ return this.address();
728
+ if (err.code !== "EADDRINUSE")
729
+ throw err;
730
+ log("Port %d is busy (EADDRINUSE). Trying next port...", port);
731
+ port = port + 1;
732
+ if (port > 65535)
733
+ port = 4e3;
734
+ }
735
+ log("All sequential ports busy. Falling back to random port.");
736
+ await this._startServer(0, host);
737
+ return this.address();
738
+ }
739
+ stop() {
740
+ return new Promise((resolve2, reject) => {
741
+ this._server.close((err) => {
742
+ if (err) {
743
+ log("Error stopping server: %o", err);
744
+ reject(err);
745
+ } else {
746
+ log("Server stopped.");
747
+ resolve2();
748
+ }
749
+ });
750
+ });
751
+ }
752
+ _errorResponse(req, res, code, text) {
753
+ res.writeHead(code, { "Content-Type": "text/plain" });
754
+ res.end(text);
755
+ log(`[${code}] ${req.method} ${req.url}`);
756
+ }
757
+ _handleRequest(req, res) {
758
+ const { url, method } = req;
759
+ if (this._cors) {
760
+ res.setHeader("Access-Control-Allow-Headers", "*");
761
+ res.setHeader("Access-Control-Allow-Origin", this._cors);
762
+ res.setHeader("Access-Control-Allow-Methods", "*");
763
+ if (req.method === "OPTIONS") {
764
+ res.writeHead(204);
765
+ res.end();
766
+ return;
767
+ }
768
+ }
769
+ if (method !== "GET") {
770
+ this._errorResponse(req, res, 405, "Method Not Allowed");
771
+ return;
772
+ }
773
+ req.on("aborted", () => log(`ABORTED ${req.method} ${req.url}`));
774
+ res.on("close", () => {
775
+ if (!res.headersSent) log(`CLOSED BEFORE SEND ${req.method} ${req.url}`);
776
+ });
777
+ if (!url || !url.startsWith(this._pathPrefix)) {
778
+ this._errorResponse(req, res, 404, "Not Found");
779
+ return;
780
+ }
781
+ const relativePath = url.slice(this._pathPrefix.length);
782
+ const safeSuffix = path2.normalize(decodeURIComponent(relativePath)).replace(/^(\.\.[\/\\])+/, "");
783
+ const filePath = path2.join(this._absoluteFolderPath, safeSuffix);
784
+ if (!filePath.startsWith(this._absoluteFolderPath)) {
785
+ this._errorResponse(req, res, 403, "Forbidden");
786
+ return;
787
+ }
788
+ fs5.stat(filePath, (err, stats) => {
789
+ if (err || !stats.isFile()) {
790
+ this._errorResponse(req, res, 404, "File Not Found");
791
+ return;
792
+ }
793
+ const ext = path2.extname(filePath).toLowerCase();
794
+ const contentType = this._mimeTypes[ext] || "application/octet-stream";
795
+ res.writeHead(200, { "Content-Type": contentType });
796
+ log(`[200] ${req.method} ${req.url} -> ${filePath}`);
797
+ const readStream = fs5.createReadStream(filePath);
798
+ readStream.pipe(res);
799
+ readStream.on("error", (err2) => {
800
+ log("Stream error: %o", err2);
801
+ res.end();
802
+ });
803
+ });
804
+ }
805
+ };
806
+
807
+ // src/showReport.ts
808
+ async function showReport(reportFolder) {
809
+ const config = await FlakinessProjectConfig.load();
810
+ const projectPublicId = config.projectPublicId();
811
+ const reportViewerEndpoint = config.reportViewerUrl();
812
+ const token = randomUUIDBase62();
813
+ const server = new StaticServer(token, reportFolder, reportViewerEndpoint);
814
+ await server.start(9373, "127.0.0.1");
815
+ const url = new URL(reportViewerEndpoint);
816
+ url.searchParams.set("port", String(server.port()));
817
+ url.searchParams.set("token", token);
818
+ if (projectPublicId)
819
+ url.searchParams.set("ppid", projectPublicId);
820
+ console.log(chalk.cyan(`
821
+ Serving Flakiness report at ${url.toString()}
822
+ Press Ctrl+C to quit.`));
823
+ await open(url.toString());
824
+ await new Promise(() => {
825
+ });
826
+ }
827
+
880
828
  // src/systemUtilizationSampler.ts
881
829
  import { spawnSync as spawnSync3 } from "child_process";
882
830
  import os2 from "os";
@@ -951,29 +899,6 @@ function inferRunUrl() {
951
899
  return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
952
900
  return void 0;
953
901
  }
954
- async function showReport(reportFolder) {
955
- const reportPath = path3.join(reportFolder, "report.json");
956
- const config = await FlakinessProjectConfig.load();
957
- const projectPublicId = config.projectPublicId();
958
- const reportViewerEndpoint = config.reportViewerEndpoint();
959
- const server = await LocalReportServer.create({
960
- endpoint: reportViewerEndpoint,
961
- port: 9373,
962
- reportPath,
963
- attachmentsFolder: reportFolder
964
- });
965
- const url = new URL(reportViewerEndpoint);
966
- url.searchParams.set("port", String(server.port()));
967
- url.searchParams.set("token", server.authToken());
968
- if (projectPublicId)
969
- url.searchParams.set("ppid", projectPublicId);
970
- console.log(chalk.cyan(`
971
- Serving Flakiness report at ${url.toString()}
972
- Press Ctrl+C to quit.`));
973
- await open(url.toString());
974
- await new Promise(() => {
975
- });
976
- }
977
902
  async function saveReport(report, attachments, outputFolder) {
978
903
  const reportPath = path3.join(outputFolder, "report.json");
979
904
  const attachmentsFolder = path3.join(outputFolder, "attachments");