@flakiness/sdk 0.129.4 → 0.131.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.
@@ -249,13 +249,88 @@ var FlakinessSession = class _FlakinessSession {
249
249
  }
250
250
  };
251
251
 
252
- // src/cli/cmd-link.ts
253
- async function cmdLink(slug) {
252
+ // ../server/lib/common/knownClientIds.js
253
+ var KNOWN_CLIENT_IDS = {
254
+ OFFICIAL_WEB: "flakiness-io-official-cli",
255
+ OFFICIAL_CLI: "flakiness-io-official-website"
256
+ };
257
+
258
+ // src/cli/cmd-login.ts
259
+ import open from "open";
260
+ import os2 from "os";
261
+
262
+ // src/cli/cmd-logout.ts
263
+ async function cmdLogout() {
254
264
  const session = await FlakinessSession.load();
255
- if (!session) {
256
- console.log(`Please login first`);
265
+ if (!session)
266
+ return;
267
+ const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
268
+ if (currentSession)
269
+ await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
270
+ await FlakinessSession.remove();
271
+ }
272
+
273
+ // src/cli/cmd-login.ts
274
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
275
+ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
276
+ await cmdLogout();
277
+ const api = createServerAPI(endpoint);
278
+ const data = await api.deviceauth.createRequest.POST({
279
+ clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
280
+ name: os2.hostname()
281
+ });
282
+ await open(new URL(data.verificationUrl, endpoint).href);
283
+ console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
284
+ let token;
285
+ while (Date.now() < data.deadline) {
286
+ await new Promise((x) => setTimeout(x, 2e3));
287
+ const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
288
+ if (!result) {
289
+ console.error(`Authorization request was rejected.`);
290
+ process.exit(1);
291
+ }
292
+ token = result.token;
293
+ if (token)
294
+ break;
295
+ }
296
+ if (!token) {
297
+ console.log(`Failed to login.`);
257
298
  process.exit(1);
258
299
  }
300
+ const session = new FlakinessSession({
301
+ endpoint,
302
+ token
303
+ });
304
+ try {
305
+ const user = await session.api.user.whoami.GET();
306
+ await session.save();
307
+ console.log(`\u2713 Logged in as ${user.userName} (${user.userLogin})`);
308
+ } catch (e) {
309
+ const message = e instanceof Error ? e.message : String(e);
310
+ console.error(`x Failed to login:`, message);
311
+ }
312
+ return session;
313
+ }
314
+
315
+ // src/cli/cmd-link.ts
316
+ async function cmdLink(slugOrUrl) {
317
+ let slug = slugOrUrl;
318
+ let endpoint = DEFAULT_FLAKINESS_ENDPOINT;
319
+ if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
320
+ const url = URL.parse(slugOrUrl);
321
+ if (!url) {
322
+ console.error(`Invalid URL: ${slugOrUrl}`);
323
+ process.exit(1);
324
+ }
325
+ slug = url.pathname.substring(1);
326
+ endpoint = url.origin;
327
+ } else if (slugOrUrl.startsWith("flakiness.io/")) {
328
+ endpoint = "https://flakiness.io";
329
+ slug = slugOrUrl.substring("flakiness.io/".length);
330
+ }
331
+ let session = await FlakinessSession.load();
332
+ if (!session || session.endpoint() !== endpoint)
333
+ session = await cmdLogin(endpoint);
259
334
  const [orgSlug, projectSlug] = slug.split("/");
260
335
  const project = await session.api.project.findProject.GET({
261
336
  orgSlug,
@@ -176,7 +176,8 @@ async function cmdLogout() {
176
176
  }
177
177
 
178
178
  // src/cli/cmd-login.ts
179
- async function cmdLogin(endpoint) {
179
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
180
+ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
180
181
  await cmdLogout();
181
182
  const api = createServerAPI(endpoint);
182
183
  const data = await api.deviceauth.createRequest.POST({
@@ -190,8 +191,8 @@ async function cmdLogin(endpoint) {
190
191
  await new Promise((x) => setTimeout(x, 2e3));
191
192
  const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
192
193
  if (!result) {
193
- console.log(`Authorization request was rejected.`);
194
- return;
194
+ console.error(`Authorization request was rejected.`);
195
+ process.exit(1);
195
196
  }
196
197
  token = result.token;
197
198
  if (token)
@@ -213,8 +214,10 @@ async function cmdLogin(endpoint) {
213
214
  const message = e instanceof Error ? e.message : String(e);
214
215
  console.error(`x Failed to login:`, message);
215
216
  }
217
+ return session;
216
218
  }
217
219
  export {
220
+ DEFAULT_FLAKINESS_ENDPOINT,
218
221
  cmdLogin
219
222
  };
220
223
  //# sourceMappingURL=cmd-login.js.map
@@ -1,7 +1,7 @@
1
1
  // src/cli/cmd-show-report.ts
2
2
  import chalk from "chalk";
3
3
  import open from "open";
4
- import path4 from "path";
4
+ import path5 from "path";
5
5
 
6
6
  // src/flakinessConfig.ts
7
7
  import fs2 from "fs";
@@ -298,9 +298,14 @@ import compression from "compression";
298
298
  import debug from "debug";
299
299
  import express from "express";
300
300
  import "express-async-errors";
301
- import fs5 from "fs";
302
301
  import http2 from "http";
303
302
 
303
+ // src/localReportApi.ts
304
+ import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
305
+ import fs4 from "fs";
306
+ import path4 from "path";
307
+ import { z } from "zod/v4";
308
+
304
309
  // src/localGit.ts
305
310
  import { exec } from "child_process";
306
311
  import { promisify } from "util";
@@ -342,9 +347,37 @@ async function listLocalCommits(gitRoot, head, count) {
342
347
  }
343
348
 
344
349
  // src/localReportApi.ts
345
- import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
346
- import fs4 from "fs";
347
- import { z } from "zod/v4";
350
+ var ReportInfo = class {
351
+ constructor(_options) {
352
+ this._options = _options;
353
+ }
354
+ report;
355
+ attachmentIdToPath = /* @__PURE__ */ new Map();
356
+ commits = [];
357
+ async refresh() {
358
+ const report = await fs4.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
359
+ if (!report) {
360
+ this.report = void 0;
361
+ this.commits = [];
362
+ this.attachmentIdToPath = /* @__PURE__ */ new Map();
363
+ return;
364
+ }
365
+ if (JSON.stringify(report) === JSON.stringify(this.report))
366
+ return;
367
+ this.report = report;
368
+ this.commits = await listLocalCommits(path4.dirname(this._options.reportPath), report.commitId, 100);
369
+ const attachmentsDir = this._options.attachmentsFolder;
370
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
371
+ if (missingAttachments.length) {
372
+ const first = missingAttachments.slice(0, 3);
373
+ for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
374
+ console.warn(`Missing attachment with id ${missingAttachments[i]}`);
375
+ if (missingAttachments.length > 3)
376
+ console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
377
+ }
378
+ this.attachmentIdToPath = attachmentIdToPath;
379
+ }
380
+ };
348
381
  var t = TypedHTTP2.Router.create();
349
382
  var localReportRouter = {
350
383
  ping: t.get({
@@ -354,7 +387,7 @@ var localReportRouter = {
354
387
  }),
355
388
  lastCommits: t.get({
356
389
  handler: async ({ ctx }) => {
357
- return ctx.commits;
390
+ return ctx.reportInfo.commits;
358
391
  }
359
392
  }),
360
393
  report: {
@@ -363,7 +396,7 @@ var localReportRouter = {
363
396
  attachmentId: z.string().min(1).max(100).transform((id) => id)
364
397
  }),
365
398
  handler: async ({ ctx, input }) => {
366
- const idx = ctx.attachmentIdToPath.get(input.attachmentId);
399
+ const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
367
400
  if (!idx)
368
401
  throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
369
402
  const buffer = await fs4.promises.readFile(idx.path);
@@ -372,7 +405,8 @@ var localReportRouter = {
372
405
  }),
373
406
  json: t.get({
374
407
  handler: async ({ ctx }) => {
375
- return ctx.report;
408
+ await ctx.reportInfo.refresh();
409
+ return ctx.reportInfo.report;
376
410
  }
377
411
  })
378
412
  }
@@ -387,17 +421,6 @@ var LocalReportServer = class _LocalReportServer {
387
421
  this._authToken = _authToken;
388
422
  }
389
423
  static async create(options) {
390
- const report = JSON.parse(await fs5.promises.readFile(options.reportPath, "utf-8"));
391
- const attachmentsDir = options.attachmentsFolder;
392
- const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
393
- if (missingAttachments.length) {
394
- const first = missingAttachments.slice(0, 3);
395
- for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
396
- console.warn(`Missing attachment with id ${missingAttachments[i]}`);
397
- if (missingAttachments.length > 3)
398
- console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
399
- }
400
- const commits = await listLocalCommits(process.cwd(), report.commitId, 100);
401
424
  const app = express();
402
425
  app.set("etag", false);
403
426
  const authToken = randomUUIDBase62();
@@ -420,9 +443,10 @@ var LocalReportServer = class _LocalReportServer {
420
443
  });
421
444
  next();
422
445
  });
446
+ const reportInfo = new ReportInfo(options);
423
447
  app.use("/" + authToken, createTypedHttpExpressMiddleware({
424
448
  router: localReportRouter,
425
- createRootContext: async ({ req, res, input }) => ({ report, commits, attachmentIdToPath })
449
+ createRootContext: async ({ req, res, input }) => ({ reportInfo })
426
450
  }));
427
451
  app.use((err, req, res, next) => {
428
452
  if (err instanceof TypedHTTP3.HttpError)
@@ -456,7 +480,7 @@ var LocalReportServer = class _LocalReportServer {
456
480
 
457
481
  // src/cli/cmd-show-report.ts
458
482
  async function cmdShowReport(reportFolder) {
459
- const reportPath = path4.join(reportFolder, "report.json");
483
+ const reportPath = path5.join(reportFolder, "report.json");
460
484
  const session = await FlakinessSession.load();
461
485
  const config = await FlakinessConfig.load();
462
486
  const projectPublicId = config.projectPublicId();
package/lib/junit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/junit.ts
2
- import { ReportUtils as ReportUtils2, FlakinessReport as FK } from "@flakiness/report";
2
+ import { ReportUtils as ReportUtils2 } from "@flakiness/report";
3
3
  import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
4
4
  import assert from "assert";
5
5
  import fs2 from "fs";
@@ -191,7 +191,7 @@ async function traverseJUnitReport(context, node) {
191
191
  file,
192
192
  line,
193
193
  column: 1
194
- } : FK.NO_LOCATION,
194
+ } : void 0,
195
195
  type: name ? "suite" : file ? "file" : "anonymous suite",
196
196
  suites: [],
197
197
  tests: []
@@ -253,7 +253,7 @@ async function traverseJUnitReport(context, node) {
253
253
  file,
254
254
  line,
255
255
  column: 1
256
- } : FK.NO_LOCATION,
256
+ } : void 0,
257
257
  attempts: [{
258
258
  environmentIdx: currentEnvIndex,
259
259
  expectedStatus,
@@ -1,7 +1,207 @@
1
1
  // src/localReportApi.ts
2
2
  import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
3
- import fs from "fs";
3
+ import fs2 from "fs";
4
+ import path2 from "path";
4
5
  import { z } from "zod/v4";
6
+
7
+ // src/localGit.ts
8
+ import { exec } from "child_process";
9
+ import { promisify } from "util";
10
+ var execAsync = promisify(exec);
11
+ async function listLocalCommits(gitRoot, head, count) {
12
+ const FIELD_SEPARATOR = "|~|";
13
+ const RECORD_SEPARATOR = "\0";
14
+ const prettyFormat = [
15
+ "%H",
16
+ // %H: Full commit hash
17
+ "%at",
18
+ // %at: Author date as a Unix timestamp (seconds since epoch)
19
+ "%an",
20
+ // %an: Author name
21
+ "%s"
22
+ // %s: Subject (the first line of the commit message)
23
+ ].join(FIELD_SEPARATOR);
24
+ const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
25
+ try {
26
+ const { stdout } = await execAsync(command, { cwd: gitRoot });
27
+ if (!stdout) {
28
+ return [];
29
+ }
30
+ return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
31
+ const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
32
+ return {
33
+ commitId,
34
+ timestamp: parseInt(timestampStr, 10) * 1e3,
35
+ // Convert timestamp from seconds to milliseconds
36
+ author,
37
+ message,
38
+ walkIndex: 0
39
+ };
40
+ });
41
+ } catch (error) {
42
+ console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ // src/utils.ts
48
+ import { ReportUtils } from "@flakiness/report";
49
+ import fs from "fs";
50
+ import http from "http";
51
+ import https from "https";
52
+ import path, { posix as posixPath, win32 as win32Path } from "path";
53
+ var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
54
+ function errorText(error) {
55
+ return FLAKINESS_DBG ? error.stack : error.message;
56
+ }
57
+ async function retryWithBackoff(job, backoff = []) {
58
+ for (const timeout of backoff) {
59
+ try {
60
+ return await job();
61
+ } catch (e) {
62
+ if (e instanceof AggregateError)
63
+ console.error(`[flakiness.io err]`, errorText(e.errors[0]));
64
+ else if (e instanceof Error)
65
+ console.error(`[flakiness.io err]`, errorText(e));
66
+ else
67
+ console.error(`[flakiness.io err]`, e);
68
+ await new Promise((x) => setTimeout(x, timeout));
69
+ }
70
+ }
71
+ return await job();
72
+ }
73
+ var httpUtils;
74
+ ((httpUtils2) => {
75
+ function createRequest({ url, method = "get", headers = {} }) {
76
+ let resolve;
77
+ let reject;
78
+ const responseDataPromise = new Promise((a, b) => {
79
+ resolve = a;
80
+ reject = b;
81
+ });
82
+ const protocol = url.startsWith("https") ? https : http;
83
+ headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
84
+ const request = protocol.request(url, { method, headers }, (res) => {
85
+ const chunks = [];
86
+ res.on("data", (chunk) => chunks.push(chunk));
87
+ res.on("end", () => {
88
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
89
+ resolve(Buffer.concat(chunks));
90
+ else
91
+ reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
92
+ });
93
+ res.on("error", (error) => reject(error));
94
+ });
95
+ request.on("error", reject);
96
+ return { request, responseDataPromise };
97
+ }
98
+ httpUtils2.createRequest = createRequest;
99
+ async function getBuffer(url, backoff) {
100
+ return await retryWithBackoff(async () => {
101
+ const { request, responseDataPromise } = createRequest({ url });
102
+ request.end();
103
+ return await responseDataPromise;
104
+ }, backoff);
105
+ }
106
+ httpUtils2.getBuffer = getBuffer;
107
+ async function getText(url, backoff) {
108
+ const buffer = await getBuffer(url, backoff);
109
+ return buffer.toString("utf-8");
110
+ }
111
+ httpUtils2.getText = getText;
112
+ async function getJSON(url) {
113
+ return JSON.parse(await getText(url));
114
+ }
115
+ httpUtils2.getJSON = getJSON;
116
+ async function postText(url, text, backoff) {
117
+ const headers = {
118
+ "Content-Type": "application/json",
119
+ "Content-Length": Buffer.byteLength(text) + ""
120
+ };
121
+ return await retryWithBackoff(async () => {
122
+ const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
123
+ request.write(text);
124
+ request.end();
125
+ return await responseDataPromise;
126
+ }, backoff);
127
+ }
128
+ httpUtils2.postText = postText;
129
+ async function postJSON(url, json, backoff) {
130
+ const buffer = await postText(url, JSON.stringify(json), backoff);
131
+ return JSON.parse(buffer.toString("utf-8"));
132
+ }
133
+ httpUtils2.postJSON = postJSON;
134
+ })(httpUtils || (httpUtils = {}));
135
+ 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");
136
+ async function resolveAttachmentPaths(report, attachmentsDir) {
137
+ const attachmentFiles = await listFilesRecursively(attachmentsDir);
138
+ const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
139
+ const attachmentIdToPath = /* @__PURE__ */ new Map();
140
+ const missingAttachments = /* @__PURE__ */ new Set();
141
+ ReportUtils.visitTests(report, (test) => {
142
+ for (const attempt of test.attempts) {
143
+ for (const attachment of attempt.attachments ?? []) {
144
+ const attachmentPath = filenameToPath.get(attachment.id);
145
+ if (!attachmentPath) {
146
+ missingAttachments.add(attachment.id);
147
+ } else {
148
+ attachmentIdToPath.set(attachment.id, {
149
+ contentType: attachment.contentType,
150
+ id: attachment.id,
151
+ path: attachmentPath
152
+ });
153
+ }
154
+ }
155
+ }
156
+ });
157
+ return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
158
+ }
159
+ async function listFilesRecursively(dir, result = []) {
160
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ const fullPath = path.join(dir, entry.name);
163
+ if (entry.isDirectory())
164
+ await listFilesRecursively(fullPath, result);
165
+ else
166
+ result.push(fullPath);
167
+ }
168
+ return result;
169
+ }
170
+ var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
171
+ var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
172
+
173
+ // src/localReportApi.ts
174
+ var ReportInfo = class {
175
+ constructor(_options) {
176
+ this._options = _options;
177
+ }
178
+ report;
179
+ attachmentIdToPath = /* @__PURE__ */ new Map();
180
+ commits = [];
181
+ async refresh() {
182
+ const report = await fs2.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
183
+ if (!report) {
184
+ this.report = void 0;
185
+ this.commits = [];
186
+ this.attachmentIdToPath = /* @__PURE__ */ new Map();
187
+ return;
188
+ }
189
+ if (JSON.stringify(report) === JSON.stringify(this.report))
190
+ return;
191
+ this.report = report;
192
+ this.commits = await listLocalCommits(path2.dirname(this._options.reportPath), report.commitId, 100);
193
+ const attachmentsDir = this._options.attachmentsFolder;
194
+ const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
195
+ if (missingAttachments.length) {
196
+ const first = missingAttachments.slice(0, 3);
197
+ for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
198
+ console.warn(`Missing attachment with id ${missingAttachments[i]}`);
199
+ if (missingAttachments.length > 3)
200
+ console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
201
+ }
202
+ this.attachmentIdToPath = attachmentIdToPath;
203
+ }
204
+ };
5
205
  var t = TypedHTTP.Router.create();
6
206
  var localReportRouter = {
7
207
  ping: t.get({
@@ -11,7 +211,7 @@ var localReportRouter = {
11
211
  }),
12
212
  lastCommits: t.get({
13
213
  handler: async ({ ctx }) => {
14
- return ctx.commits;
214
+ return ctx.reportInfo.commits;
15
215
  }
16
216
  }),
17
217
  report: {
@@ -20,21 +220,23 @@ var localReportRouter = {
20
220
  attachmentId: z.string().min(1).max(100).transform((id) => id)
21
221
  }),
22
222
  handler: async ({ ctx, input }) => {
23
- const idx = ctx.attachmentIdToPath.get(input.attachmentId);
223
+ const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
24
224
  if (!idx)
25
225
  throw TypedHTTP.HttpError.withCode("NOT_FOUND");
26
- const buffer = await fs.promises.readFile(idx.path);
226
+ const buffer = await fs2.promises.readFile(idx.path);
27
227
  return TypedHTTP.ok(buffer, idx.contentType);
28
228
  }
29
229
  }),
30
230
  json: t.get({
31
231
  handler: async ({ ctx }) => {
32
- return ctx.report;
232
+ await ctx.reportInfo.refresh();
233
+ return ctx.reportInfo.report;
33
234
  }
34
235
  })
35
236
  }
36
237
  };
37
238
  export {
239
+ ReportInfo,
38
240
  localReportRouter
39
241
  };
40
242
  //# sourceMappingURL=localReportApi.js.map