@flakiness/sdk 0.95.0 → 0.97.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/cli/cli.js +1229 -386
- package/lib/cli/cmd-convert.js +9 -12
- package/lib/cli/cmd-download.js +9 -459
- package/lib/cli/cmd-link.js +84 -271
- package/lib/cli/cmd-login.js +26 -232
- package/lib/cli/cmd-logout.js +22 -230
- package/lib/cli/cmd-show-report.js +488 -0
- package/lib/cli/cmd-status.js +84 -271
- package/lib/cli/cmd-unlink.js +68 -35
- package/lib/cli/cmd-upload-playwright-json.js +52 -257
- package/lib/cli/cmd-upload.js +92 -288
- package/lib/cli/cmd-whoami.js +16 -230
- package/lib/{flakinessLink.js → flakinessConfig.js} +65 -36
- package/lib/flakinessSession.js +16 -230
- package/lib/junit.js +8 -9
- package/lib/localGit.js +43 -0
- package/lib/localReportApi.js +40 -0
- package/lib/localReportServer.js +300 -0
- package/lib/playwright-test.js +496 -318
- package/lib/playwrightJSONReport.js +12 -15
- package/lib/reportUploader.js +47 -248
- package/lib/serverapi.js +10 -230
- package/lib/utils.js +46 -18
- package/package.json +16 -5
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cmd-serve.js +0 -7
package/lib/flakinessSession.js
CHANGED
|
@@ -3,247 +3,26 @@ import fs from "fs/promises";
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
((TypedHTTP2) => {
|
|
9
|
-
TypedHTTP2.StatusCodes = {
|
|
10
|
-
Informational: {
|
|
11
|
-
CONTINUE: 100,
|
|
12
|
-
SWITCHING_PROTOCOLS: 101,
|
|
13
|
-
PROCESSING: 102,
|
|
14
|
-
EARLY_HINTS: 103
|
|
15
|
-
},
|
|
16
|
-
Success: {
|
|
17
|
-
OK: 200,
|
|
18
|
-
CREATED: 201,
|
|
19
|
-
ACCEPTED: 202,
|
|
20
|
-
NON_AUTHORITATIVE_INFORMATION: 203,
|
|
21
|
-
NO_CONTENT: 204,
|
|
22
|
-
RESET_CONTENT: 205,
|
|
23
|
-
PARTIAL_CONTENT: 206,
|
|
24
|
-
MULTI_STATUS: 207
|
|
25
|
-
},
|
|
26
|
-
Redirection: {
|
|
27
|
-
MULTIPLE_CHOICES: 300,
|
|
28
|
-
MOVED_PERMANENTLY: 301,
|
|
29
|
-
MOVED_TEMPORARILY: 302,
|
|
30
|
-
SEE_OTHER: 303,
|
|
31
|
-
NOT_MODIFIED: 304,
|
|
32
|
-
USE_PROXY: 305,
|
|
33
|
-
TEMPORARY_REDIRECT: 307,
|
|
34
|
-
PERMANENT_REDIRECT: 308
|
|
35
|
-
},
|
|
36
|
-
ClientErrors: {
|
|
37
|
-
BAD_REQUEST: 400,
|
|
38
|
-
UNAUTHORIZED: 401,
|
|
39
|
-
PAYMENT_REQUIRED: 402,
|
|
40
|
-
FORBIDDEN: 403,
|
|
41
|
-
NOT_FOUND: 404,
|
|
42
|
-
METHOD_NOT_ALLOWED: 405,
|
|
43
|
-
NOT_ACCEPTABLE: 406,
|
|
44
|
-
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
45
|
-
REQUEST_TIMEOUT: 408,
|
|
46
|
-
CONFLICT: 409,
|
|
47
|
-
GONE: 410,
|
|
48
|
-
LENGTH_REQUIRED: 411,
|
|
49
|
-
PRECONDITION_FAILED: 412,
|
|
50
|
-
REQUEST_TOO_LONG: 413,
|
|
51
|
-
REQUEST_URI_TOO_LONG: 414,
|
|
52
|
-
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
53
|
-
REQUESTED_RANGE_NOT_SATISFIABLE: 416,
|
|
54
|
-
EXPECTATION_FAILED: 417,
|
|
55
|
-
IM_A_TEAPOT: 418,
|
|
56
|
-
INSUFFICIENT_SPACE_ON_RESOURCE: 419,
|
|
57
|
-
METHOD_FAILURE: 420,
|
|
58
|
-
MISDIRECTED_REQUEST: 421,
|
|
59
|
-
UNPROCESSABLE_ENTITY: 422,
|
|
60
|
-
LOCKED: 423,
|
|
61
|
-
FAILED_DEPENDENCY: 424,
|
|
62
|
-
UPGRADE_REQUIRED: 426,
|
|
63
|
-
PRECONDITION_REQUIRED: 428,
|
|
64
|
-
TOO_MANY_REQUESTS: 429,
|
|
65
|
-
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
66
|
-
UNAVAILABLE_FOR_LEGAL_REASONS: 451
|
|
67
|
-
},
|
|
68
|
-
ServerErrors: {
|
|
69
|
-
INTERNAL_SERVER_ERROR: 500,
|
|
70
|
-
NOT_IMPLEMENTED: 501,
|
|
71
|
-
BAD_GATEWAY: 502,
|
|
72
|
-
SERVICE_UNAVAILABLE: 503,
|
|
73
|
-
GATEWAY_TIMEOUT: 504,
|
|
74
|
-
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
75
|
-
INSUFFICIENT_STORAGE: 507,
|
|
76
|
-
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const AllErrorCodes = {
|
|
80
|
-
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
81
|
-
...TypedHTTP2.StatusCodes.ServerErrors
|
|
82
|
-
};
|
|
83
|
-
class HttpError extends Error {
|
|
84
|
-
constructor(status, message) {
|
|
85
|
-
super(message);
|
|
86
|
-
this.status = status;
|
|
87
|
-
}
|
|
88
|
-
static withCode(code, message) {
|
|
89
|
-
const statusCode = AllErrorCodes[code];
|
|
90
|
-
const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
|
|
91
|
-
return new HttpError(statusCode, message ?? defaultMessage);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
TypedHTTP2.HttpError = HttpError;
|
|
95
|
-
function isInformationalResponse(response) {
|
|
96
|
-
return response.status >= 100 && response.status < 200;
|
|
97
|
-
}
|
|
98
|
-
TypedHTTP2.isInformationalResponse = isInformationalResponse;
|
|
99
|
-
function isSuccessResponse(response) {
|
|
100
|
-
return response.status >= 200 && response.status < 300;
|
|
101
|
-
}
|
|
102
|
-
TypedHTTP2.isSuccessResponse = isSuccessResponse;
|
|
103
|
-
function isRedirectResponse(response) {
|
|
104
|
-
return response.status >= 300 && response.status < 400;
|
|
105
|
-
}
|
|
106
|
-
TypedHTTP2.isRedirectResponse = isRedirectResponse;
|
|
107
|
-
function isErrorResponse(response) {
|
|
108
|
-
return response.status >= 400 && response.status < 600;
|
|
109
|
-
}
|
|
110
|
-
TypedHTTP2.isErrorResponse = isErrorResponse;
|
|
111
|
-
function info(status) {
|
|
112
|
-
return { status };
|
|
113
|
-
}
|
|
114
|
-
TypedHTTP2.info = info;
|
|
115
|
-
function ok(data, status) {
|
|
116
|
-
return {
|
|
117
|
-
status: status ?? TypedHTTP2.StatusCodes.Success.OK,
|
|
118
|
-
data
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
TypedHTTP2.ok = ok;
|
|
122
|
-
function redirect(url, status = 302) {
|
|
123
|
-
return { status, url };
|
|
124
|
-
}
|
|
125
|
-
TypedHTTP2.redirect = redirect;
|
|
126
|
-
function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
|
|
127
|
-
return { status, message };
|
|
128
|
-
}
|
|
129
|
-
TypedHTTP2.error = error;
|
|
130
|
-
class Router {
|
|
131
|
-
constructor(_resolveContext) {
|
|
132
|
-
this._resolveContext = _resolveContext;
|
|
133
|
-
}
|
|
134
|
-
static create() {
|
|
135
|
-
return new Router(async (e) => e.ctx);
|
|
136
|
-
}
|
|
137
|
-
rawMethod(method, route) {
|
|
138
|
-
return {
|
|
139
|
-
[method]: {
|
|
140
|
-
method,
|
|
141
|
-
input: route.input,
|
|
142
|
-
etag: route.etag,
|
|
143
|
-
resolveContext: this._resolveContext,
|
|
144
|
-
handler: route.handler
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
get(route) {
|
|
149
|
-
return this.rawMethod("GET", {
|
|
150
|
-
...route,
|
|
151
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
post(route) {
|
|
155
|
-
return this.rawMethod("POST", {
|
|
156
|
-
...route,
|
|
157
|
-
handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result))
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
use(resolveContext) {
|
|
161
|
-
return new Router(async (options) => {
|
|
162
|
-
const m = await this._resolveContext(options);
|
|
163
|
-
return resolveContext({ ...options, ctx: m });
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
TypedHTTP2.Router = Router;
|
|
168
|
-
function createClient(base, fetchCallback) {
|
|
169
|
-
function buildUrl(path2, input, options) {
|
|
170
|
-
const method = path2.at(-1);
|
|
171
|
-
const url = new URL(path2.slice(0, path2.length - 1).join("/"), base);
|
|
172
|
-
const signal = options?.signal;
|
|
173
|
-
let body = void 0;
|
|
174
|
-
if (method === "GET" && input)
|
|
175
|
-
url.searchParams.set("input", JSON.stringify(input));
|
|
176
|
-
else if (method !== "GET" && input)
|
|
177
|
-
body = JSON.stringify(input);
|
|
178
|
-
return {
|
|
179
|
-
url,
|
|
180
|
-
method,
|
|
181
|
-
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
182
|
-
body,
|
|
183
|
-
signal
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function createProxy(path2 = []) {
|
|
187
|
-
return new Proxy(() => {
|
|
188
|
-
}, {
|
|
189
|
-
get(target, prop) {
|
|
190
|
-
if (typeof prop === "symbol")
|
|
191
|
-
return void 0;
|
|
192
|
-
if (prop === "prepare")
|
|
193
|
-
return (input, options) => buildUrl(path2, input, options);
|
|
194
|
-
const newPath = [...path2, prop];
|
|
195
|
-
return createProxy(newPath);
|
|
196
|
-
},
|
|
197
|
-
apply(target, thisArg, args) {
|
|
198
|
-
const options = buildUrl(path2, args[0], args[1]);
|
|
199
|
-
return fetchCallback(options.url, {
|
|
200
|
-
method: options.method,
|
|
201
|
-
body: options.body,
|
|
202
|
-
headers: options.headers,
|
|
203
|
-
signal: options.signal
|
|
204
|
-
}).then(async (response) => {
|
|
205
|
-
if (response.status >= 200 && response.status < 300) {
|
|
206
|
-
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
207
|
-
const text = await response.text();
|
|
208
|
-
return text.length ? JSON.parse(text) : void 0;
|
|
209
|
-
}
|
|
210
|
-
return await response.blob();
|
|
211
|
-
}
|
|
212
|
-
if (response.status >= 400 && response.status < 600) {
|
|
213
|
-
const text = await response.text();
|
|
214
|
-
if (text)
|
|
215
|
-
throw new Error(`HTTP request failed with status ${response.status}: ${text}`);
|
|
216
|
-
else
|
|
217
|
-
throw new Error(`HTTP request failed with status ${response.status}`);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
return createProxy();
|
|
224
|
-
}
|
|
225
|
-
TypedHTTP2.createClient = createClient;
|
|
226
|
-
})(TypedHTTP || (TypedHTTP = {}));
|
|
6
|
+
// src/serverapi.ts
|
|
7
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
227
8
|
|
|
228
9
|
// src/utils.ts
|
|
10
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
229
11
|
import http from "http";
|
|
230
12
|
import https from "https";
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
var gunzipSync = zlib.gunzipSync;
|
|
236
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
237
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
13
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
14
|
+
function errorText(error) {
|
|
15
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
16
|
+
}
|
|
238
17
|
async function retryWithBackoff(job, backoff = []) {
|
|
239
18
|
for (const timeout of backoff) {
|
|
240
19
|
try {
|
|
241
20
|
return await job();
|
|
242
21
|
} catch (e) {
|
|
243
22
|
if (e instanceof AggregateError)
|
|
244
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
23
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
245
24
|
else if (e instanceof Error)
|
|
246
|
-
console.error(`[flakiness.io err]`, e
|
|
25
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
247
26
|
else
|
|
248
27
|
console.error(`[flakiness.io err]`, e);
|
|
249
28
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -261,6 +40,7 @@ var httpUtils;
|
|
|
261
40
|
reject = b;
|
|
262
41
|
});
|
|
263
42
|
const protocol = url.startsWith("https") ? https : http;
|
|
43
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
264
44
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
265
45
|
const chunks = [];
|
|
266
46
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -342,6 +122,12 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
342
122
|
this._config = _config;
|
|
343
123
|
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
344
124
|
}
|
|
125
|
+
static async loadOrDie() {
|
|
126
|
+
const session = await _FlakinessSession.load();
|
|
127
|
+
if (!session)
|
|
128
|
+
throw new Error(`Please login first with 'npx flakiness login'`);
|
|
129
|
+
return session;
|
|
130
|
+
}
|
|
345
131
|
static async load() {
|
|
346
132
|
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
347
133
|
if (!data)
|
package/lib/junit.js
CHANGED
|
@@ -6,17 +6,11 @@ import fs2 from "fs";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
|
|
8
8
|
// src/utils.ts
|
|
9
|
+
import { FlakinessReport } from "@flakiness/report";
|
|
9
10
|
import crypto from "crypto";
|
|
10
11
|
import fs from "fs";
|
|
11
12
|
import http from "http";
|
|
12
13
|
import https from "https";
|
|
13
|
-
import util from "util";
|
|
14
|
-
import zlib from "zlib";
|
|
15
|
-
var gzipAsync = util.promisify(zlib.gzip);
|
|
16
|
-
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
17
|
-
var gunzipSync = zlib.gunzipSync;
|
|
18
|
-
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
19
|
-
var brotliCompressSync = zlib.brotliCompressSync;
|
|
20
14
|
function sha1File(filePath) {
|
|
21
15
|
return new Promise((resolve, reject) => {
|
|
22
16
|
const hash = crypto.createHash("sha1");
|
|
@@ -32,6 +26,10 @@ function sha1File(filePath) {
|
|
|
32
26
|
});
|
|
33
27
|
});
|
|
34
28
|
}
|
|
29
|
+
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
30
|
+
function errorText(error) {
|
|
31
|
+
return FLAKINESS_DBG ? error.stack : error.message;
|
|
32
|
+
}
|
|
35
33
|
function sha1Buffer(data) {
|
|
36
34
|
const hash = crypto.createHash("sha1");
|
|
37
35
|
hash.update(data);
|
|
@@ -43,9 +41,9 @@ async function retryWithBackoff(job, backoff = []) {
|
|
|
43
41
|
return await job();
|
|
44
42
|
} catch (e) {
|
|
45
43
|
if (e instanceof AggregateError)
|
|
46
|
-
console.error(`[flakiness.io err]`, e.errors[0]
|
|
44
|
+
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
47
45
|
else if (e instanceof Error)
|
|
48
|
-
console.error(`[flakiness.io err]`, e
|
|
46
|
+
console.error(`[flakiness.io err]`, errorText(e));
|
|
49
47
|
else
|
|
50
48
|
console.error(`[flakiness.io err]`, e);
|
|
51
49
|
await new Promise((x) => setTimeout(x, timeout));
|
|
@@ -63,6 +61,7 @@ var httpUtils;
|
|
|
63
61
|
reject = b;
|
|
64
62
|
});
|
|
65
63
|
const protocol = url.startsWith("https") ? https : http;
|
|
64
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
66
65
|
const request = protocol.request(url, { method, headers }, (res) => {
|
|
67
66
|
const chunks = [];
|
|
68
67
|
res.on("data", (chunk) => chunks.push(chunk));
|
package/lib/localGit.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/localGit.ts
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
var execAsync = promisify(exec);
|
|
5
|
+
async function listLocalCommits(gitRoot, head, count) {
|
|
6
|
+
const FIELD_SEPARATOR = "|~|";
|
|
7
|
+
const RECORD_SEPARATOR = "\0";
|
|
8
|
+
const prettyFormat = [
|
|
9
|
+
"%H",
|
|
10
|
+
// %H: Full commit hash
|
|
11
|
+
"%at",
|
|
12
|
+
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
13
|
+
"%an",
|
|
14
|
+
// %an: Author name
|
|
15
|
+
"%s"
|
|
16
|
+
// %s: Subject (the first line of the commit message)
|
|
17
|
+
].join(FIELD_SEPARATOR);
|
|
18
|
+
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execAsync(command, { cwd: gitRoot });
|
|
21
|
+
if (!stdout) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
25
|
+
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
26
|
+
return {
|
|
27
|
+
commitId,
|
|
28
|
+
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
29
|
+
// Convert timestamp from seconds to milliseconds
|
|
30
|
+
author,
|
|
31
|
+
message,
|
|
32
|
+
walkIndex: 0
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Failed to list commits for repository at ${gitRoot}:`, error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
listLocalCommits
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=localGit.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/localReportApi.ts
|
|
2
|
+
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
var t = TypedHTTP.Router.create();
|
|
6
|
+
var localReportRouter = {
|
|
7
|
+
ping: t.get({
|
|
8
|
+
handler: async () => {
|
|
9
|
+
return "pong";
|
|
10
|
+
}
|
|
11
|
+
}),
|
|
12
|
+
lastCommits: t.get({
|
|
13
|
+
handler: async ({ ctx }) => {
|
|
14
|
+
return ctx.commits;
|
|
15
|
+
}
|
|
16
|
+
}),
|
|
17
|
+
report: {
|
|
18
|
+
attachment: t.rawMethod("GET", {
|
|
19
|
+
input: z.object({
|
|
20
|
+
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
21
|
+
}),
|
|
22
|
+
handler: async ({ ctx, input }) => {
|
|
23
|
+
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
24
|
+
if (!idx)
|
|
25
|
+
throw TypedHTTP.HttpError.withCode("NOT_FOUND");
|
|
26
|
+
const buffer = await fs.promises.readFile(idx.path);
|
|
27
|
+
return TypedHTTP.ok(buffer, idx.contentType);
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
json: t.get({
|
|
31
|
+
handler: async ({ ctx }) => {
|
|
32
|
+
return ctx.report;
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export {
|
|
38
|
+
localReportRouter
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=localReportApi.js.map
|