@flakiness/sdk 0.130.0 → 0.132.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 +85 -62
- package/lib/cli/cmd-convert.js +32 -21
- package/lib/cli/cmd-link.js +1 -70
- package/lib/cli/cmd-login.js +7 -4
- package/lib/cli/cmd-logout.js +1 -1
- package/lib/cli/cmd-show-report.js +6 -3
- package/lib/cli/cmd-upload.js +21 -19
- package/lib/localGit.js +6 -3
- package/lib/localReportApi.js +6 -3
- package/lib/localReportServer.js +6 -3
- package/lib/playwright-test.js +32 -24
- package/lib/utils.js +24 -0
- package/package.json +4 -4
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cli.js
CHANGED
|
@@ -742,7 +742,7 @@ import path11 from "path";
|
|
|
742
742
|
// ../package.json
|
|
743
743
|
var package_default = {
|
|
744
744
|
name: "flakiness",
|
|
745
|
-
version: "0.
|
|
745
|
+
version: "0.132.0",
|
|
746
746
|
private: true,
|
|
747
747
|
scripts: {
|
|
748
748
|
minor: "./version.mjs minor",
|
|
@@ -911,6 +911,29 @@ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:
|
|
|
911
911
|
function stripAnsi(str) {
|
|
912
912
|
return str.replace(ansiRegex, "");
|
|
913
913
|
}
|
|
914
|
+
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
915
|
+
const reportPath = path.join(outputFolder, "report.json");
|
|
916
|
+
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
917
|
+
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
918
|
+
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
919
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
920
|
+
if (attachments.length)
|
|
921
|
+
await fs.promises.mkdir(attachmentsFolder);
|
|
922
|
+
const movedAttachments = [];
|
|
923
|
+
for (const attachment of attachments) {
|
|
924
|
+
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
925
|
+
if (attachment.path)
|
|
926
|
+
await fs.promises.cp(attachment.path, attachmentPath);
|
|
927
|
+
else if (attachment.body)
|
|
928
|
+
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
929
|
+
movedAttachments.push({
|
|
930
|
+
contentType: attachment.contentType,
|
|
931
|
+
id: attachment.id,
|
|
932
|
+
path: attachmentPath
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return movedAttachments;
|
|
936
|
+
}
|
|
914
937
|
function shell(command, args, options) {
|
|
915
938
|
try {
|
|
916
939
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -1436,21 +1459,8 @@ async function cmdConvert(junitPath, options) {
|
|
|
1436
1459
|
runStartTimestamp: Date.now(),
|
|
1437
1460
|
runDuration: 0
|
|
1438
1461
|
});
|
|
1439
|
-
await
|
|
1440
|
-
console.log(
|
|
1441
|
-
if (attachments.length > 0) {
|
|
1442
|
-
await fs5.mkdir("fkattachments", { recursive: true });
|
|
1443
|
-
for (const attachment of attachments) {
|
|
1444
|
-
if (attachment.path) {
|
|
1445
|
-
const destPath = path5.join("fkattachments", attachment.id);
|
|
1446
|
-
await fs5.copyFile(attachment.path, destPath);
|
|
1447
|
-
} else if (attachment.body) {
|
|
1448
|
-
const destPath = path5.join("fkattachments", attachment.id);
|
|
1449
|
-
await fs5.writeFile(destPath, attachment.body);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
1453
|
-
}
|
|
1462
|
+
await saveReportAndAttachments(report, attachments, options.outputDir);
|
|
1463
|
+
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1454
1464
|
}
|
|
1455
1465
|
async function findXmlFiles(dir, result = []) {
|
|
1456
1466
|
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
@@ -1506,12 +1516,7 @@ async function cmdDownload(session2, project, runId) {
|
|
|
1506
1516
|
}
|
|
1507
1517
|
|
|
1508
1518
|
// src/cli/cmd-link.ts
|
|
1509
|
-
async function cmdLink(slug) {
|
|
1510
|
-
const session2 = await FlakinessSession.load();
|
|
1511
|
-
if (!session2) {
|
|
1512
|
-
console.log(`Please login first`);
|
|
1513
|
-
process.exit(1);
|
|
1514
|
-
}
|
|
1519
|
+
async function cmdLink(session2, slug) {
|
|
1515
1520
|
const [orgSlug, projectSlug] = slug.split("/");
|
|
1516
1521
|
const project = await session2.api.project.findProject.GET({
|
|
1517
1522
|
orgSlug,
|
|
@@ -1544,12 +1549,13 @@ async function cmdLogout() {
|
|
|
1544
1549
|
return;
|
|
1545
1550
|
const currentSession = await session2.api.user.currentSession.GET().catch((e) => void 0);
|
|
1546
1551
|
if (currentSession)
|
|
1547
|
-
await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
1552
|
+
await session2.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
|
|
1548
1553
|
await FlakinessSession.remove();
|
|
1549
1554
|
}
|
|
1550
1555
|
|
|
1551
1556
|
// src/cli/cmd-login.ts
|
|
1552
|
-
|
|
1557
|
+
var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
|
|
1558
|
+
async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
1553
1559
|
await cmdLogout();
|
|
1554
1560
|
const api = createServerAPI(endpoint);
|
|
1555
1561
|
const data = await api.deviceauth.createRequest.POST({
|
|
@@ -1563,8 +1569,8 @@ async function cmdLogin(endpoint) {
|
|
|
1563
1569
|
await new Promise((x) => setTimeout(x, 2e3));
|
|
1564
1570
|
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
|
|
1565
1571
|
if (!result) {
|
|
1566
|
-
console.
|
|
1567
|
-
|
|
1572
|
+
console.error(`Authorization request was rejected.`);
|
|
1573
|
+
process.exit(1);
|
|
1568
1574
|
}
|
|
1569
1575
|
token = result.token;
|
|
1570
1576
|
if (token)
|
|
@@ -1586,6 +1592,7 @@ async function cmdLogin(endpoint) {
|
|
|
1586
1592
|
const message = e instanceof Error ? e.message : String(e);
|
|
1587
1593
|
console.error(`x Failed to login:`, message);
|
|
1588
1594
|
}
|
|
1595
|
+
return session2;
|
|
1589
1596
|
}
|
|
1590
1597
|
|
|
1591
1598
|
// src/cli/cmd-show-report.ts
|
|
@@ -1624,8 +1631,10 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
1624
1631
|
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
1625
1632
|
"%an",
|
|
1626
1633
|
// %an: Author name
|
|
1627
|
-
"%s"
|
|
1634
|
+
"%s",
|
|
1628
1635
|
// %s: Subject (the first line of the commit message)
|
|
1636
|
+
"%P"
|
|
1637
|
+
// %P: Parent hashes (space-separated)
|
|
1629
1638
|
].join(FIELD_SEPARATOR);
|
|
1630
1639
|
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
1631
1640
|
try {
|
|
@@ -1634,13 +1643,14 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
1634
1643
|
return [];
|
|
1635
1644
|
}
|
|
1636
1645
|
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
1637
|
-
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
1646
|
+
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
1647
|
+
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
1638
1648
|
return {
|
|
1639
1649
|
commitId,
|
|
1640
1650
|
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
1641
|
-
// Convert timestamp from seconds to milliseconds
|
|
1642
1651
|
author,
|
|
1643
1652
|
message,
|
|
1653
|
+
parents,
|
|
1644
1654
|
walkIndex: 0
|
|
1645
1655
|
};
|
|
1646
1656
|
});
|
|
@@ -2180,29 +2190,31 @@ import path10 from "path";
|
|
|
2180
2190
|
var warn = (txt) => console.warn(chalk2.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
2181
2191
|
var err = (txt) => console.error(chalk2.red(`[flakiness.io] Error: ${txt}`));
|
|
2182
2192
|
var log = (txt) => console.log(`[flakiness.io] ${txt}`);
|
|
2183
|
-
async function cmdUpload(
|
|
2184
|
-
const fullPath = path10.resolve(relativePath);
|
|
2185
|
-
if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
|
|
2186
|
-
err(`Path ${fullPath} is not accessible!`);
|
|
2187
|
-
process.exit(1);
|
|
2188
|
-
}
|
|
2189
|
-
const text = await fs10.readFile(fullPath, "utf-8");
|
|
2190
|
-
const report = JSON.parse(text);
|
|
2191
|
-
const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
|
|
2192
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
2193
|
-
if (missingAttachments.length) {
|
|
2194
|
-
warn(`Missing ${missingAttachments.length} attachments`);
|
|
2195
|
-
}
|
|
2193
|
+
async function cmdUpload(relativePaths, options) {
|
|
2196
2194
|
const uploader = new ReportUploader({
|
|
2197
2195
|
flakinessAccessToken: options.accessToken,
|
|
2198
2196
|
flakinessEndpoint: options.endpoint
|
|
2199
2197
|
});
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2198
|
+
for (const relativePath of relativePaths) {
|
|
2199
|
+
const fullPath = path10.resolve(relativePath);
|
|
2200
|
+
if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
|
|
2201
|
+
err(`Path ${fullPath} is not accessible!`);
|
|
2202
|
+
process.exit(1);
|
|
2203
|
+
}
|
|
2204
|
+
const text = await fs10.readFile(fullPath, "utf-8");
|
|
2205
|
+
const report = JSON.parse(text);
|
|
2206
|
+
const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
|
|
2207
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
2208
|
+
if (missingAttachments.length) {
|
|
2209
|
+
warn(`Missing ${missingAttachments.length} attachments`);
|
|
2210
|
+
}
|
|
2211
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
2212
|
+
const uploadResult = await upload.upload();
|
|
2213
|
+
if (!uploadResult.success) {
|
|
2214
|
+
err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
2215
|
+
} else {
|
|
2216
|
+
log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
2217
|
+
}
|
|
2206
2218
|
}
|
|
2207
2219
|
}
|
|
2208
2220
|
|
|
@@ -2221,7 +2233,7 @@ async function cmdWhoami() {
|
|
|
2221
2233
|
// src/cli/cli.ts
|
|
2222
2234
|
var session = await FlakinessSession.load();
|
|
2223
2235
|
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
2224
|
-
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ??
|
|
2236
|
+
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
|
|
2225
2237
|
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
2226
2238
|
async function runCommand(callback) {
|
|
2227
2239
|
try {
|
|
@@ -2259,11 +2271,8 @@ async function ensureAccessToken(options) {
|
|
|
2259
2271
|
program.command("upload-playwright-json", { hidden: true }).description("Upload Playwright Test JSON report to the flakiness.io service").argument("<relative-path-to-json>", "Path to the Playwright JSON report file").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePath, options) => runCommand(async () => {
|
|
2260
2272
|
await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
|
|
2261
2273
|
}));
|
|
2262
|
-
|
|
2263
|
-
program.command("login").description("Login to the flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
|
|
2274
|
+
program.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
|
|
2264
2275
|
await cmdLogin(options.endpoint);
|
|
2265
|
-
if (options.link)
|
|
2266
|
-
await cmdLink(options.link);
|
|
2267
2276
|
}));
|
|
2268
2277
|
program.command("logout").description("Logout from current session").action(async () => runCommand(async () => {
|
|
2269
2278
|
await cmdLogout();
|
|
@@ -2271,11 +2280,25 @@ program.command("logout").description("Logout from current session").action(asyn
|
|
|
2271
2280
|
program.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
|
|
2272
2281
|
await cmdWhoami();
|
|
2273
2282
|
}));
|
|
2274
|
-
program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("org/project",
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2283
|
+
program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("flakiness.io/org/project", "A URL of the Flakiness.io project").action(async (slugOrUrl, options) => runCommand(async () => {
|
|
2284
|
+
let slug = slugOrUrl;
|
|
2285
|
+
let endpoint = options.endpoint;
|
|
2286
|
+
if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
|
|
2287
|
+
const url = URL.parse(slugOrUrl);
|
|
2288
|
+
if (!url) {
|
|
2289
|
+
console.error(`Invalid URL: ${slugOrUrl}`);
|
|
2290
|
+
process.exit(1);
|
|
2291
|
+
}
|
|
2292
|
+
slug = url.pathname.substring(1);
|
|
2293
|
+
endpoint = url.origin;
|
|
2294
|
+
} else if (slugOrUrl.startsWith("flakiness.io/")) {
|
|
2295
|
+
endpoint = "https://flakiness.io";
|
|
2296
|
+
slug = slugOrUrl.substring("flakiness.io/".length);
|
|
2297
|
+
}
|
|
2298
|
+
let session2 = await FlakinessSession.load();
|
|
2299
|
+
if (!session2 || session2.endpoint() !== endpoint || await session2.api.user.whoami.GET().catch((e) => void 0) === void 0)
|
|
2300
|
+
session2 = await cmdLogin(endpoint);
|
|
2301
|
+
await cmdLink(session2, slug);
|
|
2279
2302
|
}));
|
|
2280
2303
|
program.command("unlink").description("Unlink repository from the flakiness project").action(async () => runCommand(async () => {
|
|
2281
2304
|
await cmdUnlink();
|
|
@@ -2328,16 +2351,16 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
2328
2351
|
}
|
|
2329
2352
|
await Promise.all(downloaders);
|
|
2330
2353
|
}));
|
|
2331
|
-
program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-
|
|
2354
|
+
program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-paths...>", "Paths to the Flakiness report files").addOption(optAccessToken).addOption(optEndpoint).addOption(optAttachmentsDir).action(async (relativePaths, options) => {
|
|
2332
2355
|
await runCommand(async () => {
|
|
2333
|
-
await cmdUpload(
|
|
2356
|
+
await cmdUpload(relativePaths, await ensureAccessToken(options));
|
|
2334
2357
|
});
|
|
2335
2358
|
});
|
|
2336
|
-
program.command("show
|
|
2359
|
+
program.command("show").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`. (default: flakiness-report)").action(async (arg) => runCommand(async () => {
|
|
2337
2360
|
const dir = path11.resolve(arg ?? "flakiness-report");
|
|
2338
2361
|
await cmdShowReport(dir);
|
|
2339
2362
|
}));
|
|
2340
|
-
program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "
|
|
2363
|
+
program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "junit").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").option("--output-dir <dir>", "Output directory for the report", "flakiness-report").action(async (junitPath, options) => {
|
|
2341
2364
|
await runCommand(async () => {
|
|
2342
2365
|
await cmdConvert(junitPath, options);
|
|
2343
2366
|
});
|
package/lib/cli/cmd-convert.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/cmd-convert.ts
|
|
4
4
|
import fs3 from "fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
|
|
7
7
|
// src/junit.ts
|
|
8
8
|
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
9
9
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
10
10
|
import assert2 from "assert";
|
|
11
11
|
import fs2 from "fs";
|
|
12
|
-
import
|
|
12
|
+
import path2 from "path";
|
|
13
13
|
|
|
14
14
|
// src/utils.ts
|
|
15
15
|
import { ReportUtils } from "@flakiness/report";
|
|
@@ -19,6 +19,7 @@ import crypto from "crypto";
|
|
|
19
19
|
import fs from "fs";
|
|
20
20
|
import http from "http";
|
|
21
21
|
import https from "https";
|
|
22
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
22
23
|
function sha1File(filePath) {
|
|
23
24
|
return new Promise((resolve, reject) => {
|
|
24
25
|
const hash = crypto.createHash("sha1");
|
|
@@ -122,6 +123,29 @@ var httpUtils;
|
|
|
122
123
|
httpUtils2.postJSON = postJSON;
|
|
123
124
|
})(httpUtils || (httpUtils = {}));
|
|
124
125
|
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");
|
|
126
|
+
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
127
|
+
const reportPath = path.join(outputFolder, "report.json");
|
|
128
|
+
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
129
|
+
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
130
|
+
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
131
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
132
|
+
if (attachments.length)
|
|
133
|
+
await fs.promises.mkdir(attachmentsFolder);
|
|
134
|
+
const movedAttachments = [];
|
|
135
|
+
for (const attachment of attachments) {
|
|
136
|
+
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
137
|
+
if (attachment.path)
|
|
138
|
+
await fs.promises.cp(attachment.path, attachmentPath);
|
|
139
|
+
else if (attachment.body)
|
|
140
|
+
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
141
|
+
movedAttachments.push({
|
|
142
|
+
contentType: attachment.contentType,
|
|
143
|
+
id: attachment.id,
|
|
144
|
+
path: attachmentPath
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return movedAttachments;
|
|
148
|
+
}
|
|
125
149
|
function shell(command, args, options) {
|
|
126
150
|
try {
|
|
127
151
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -187,7 +211,7 @@ function extractStdout(testcase, stdio) {
|
|
|
187
211
|
}));
|
|
188
212
|
}
|
|
189
213
|
async function parseAttachment(value) {
|
|
190
|
-
let absolutePath =
|
|
214
|
+
let absolutePath = path2.resolve(process.cwd(), value);
|
|
191
215
|
if (fs2.existsSync(absolutePath)) {
|
|
192
216
|
const id = await sha1File(absolutePath);
|
|
193
217
|
return {
|
|
@@ -260,7 +284,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
260
284
|
id: attachment.id,
|
|
261
285
|
contentType: attachment.contentType,
|
|
262
286
|
//TODO: better default names for attachments?
|
|
263
|
-
name: attachment.path ?
|
|
287
|
+
name: attachment.path ? path2.basename(attachment.path) : `attachment`
|
|
264
288
|
});
|
|
265
289
|
} else {
|
|
266
290
|
annotations.push({
|
|
@@ -335,7 +359,7 @@ async function parseJUnit(xmls, options) {
|
|
|
335
359
|
|
|
336
360
|
// src/cli/cmd-convert.ts
|
|
337
361
|
async function cmdConvert(junitPath, options) {
|
|
338
|
-
const fullPath =
|
|
362
|
+
const fullPath = path3.resolve(junitPath);
|
|
339
363
|
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
340
364
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
341
365
|
process.exit(1);
|
|
@@ -377,26 +401,13 @@ async function cmdConvert(junitPath, options) {
|
|
|
377
401
|
runStartTimestamp: Date.now(),
|
|
378
402
|
runDuration: 0
|
|
379
403
|
});
|
|
380
|
-
await
|
|
381
|
-
console.log(
|
|
382
|
-
if (attachments.length > 0) {
|
|
383
|
-
await fs3.mkdir("fkattachments", { recursive: true });
|
|
384
|
-
for (const attachment of attachments) {
|
|
385
|
-
if (attachment.path) {
|
|
386
|
-
const destPath = path2.join("fkattachments", attachment.id);
|
|
387
|
-
await fs3.copyFile(attachment.path, destPath);
|
|
388
|
-
} else if (attachment.body) {
|
|
389
|
-
const destPath = path2.join("fkattachments", attachment.id);
|
|
390
|
-
await fs3.writeFile(destPath, attachment.body);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
394
|
-
}
|
|
404
|
+
await saveReportAndAttachments(report, attachments, options.outputDir);
|
|
405
|
+
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
395
406
|
}
|
|
396
407
|
async function findXmlFiles(dir, result = []) {
|
|
397
408
|
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
398
409
|
for (const entry of entries) {
|
|
399
|
-
const fullPath =
|
|
410
|
+
const fullPath = path3.join(dir, entry.name);
|
|
400
411
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
401
412
|
result.push(fullPath);
|
|
402
413
|
else if (entry.isDirectory())
|
package/lib/cli/cmd-link.js
CHANGED
|
@@ -185,77 +185,8 @@ var FlakinessConfig = class _FlakinessConfig {
|
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
-
// src/flakinessSession.ts
|
|
189
|
-
import fs2 from "fs/promises";
|
|
190
|
-
import os from "os";
|
|
191
|
-
import path3 from "path";
|
|
192
|
-
|
|
193
|
-
// src/serverapi.ts
|
|
194
|
-
import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
195
|
-
function createServerAPI(endpoint, options) {
|
|
196
|
-
endpoint += "/api/";
|
|
197
|
-
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
198
|
-
...init,
|
|
199
|
-
headers: {
|
|
200
|
-
...init.headers,
|
|
201
|
-
"Authorization": `Bearer ${options.auth}`
|
|
202
|
-
}
|
|
203
|
-
}) : fetch;
|
|
204
|
-
if (options?.retries)
|
|
205
|
-
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
206
|
-
return TypedHTTP.createClient(endpoint, fetcher);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/flakinessSession.ts
|
|
210
|
-
var CONFIG_DIR = (() => {
|
|
211
|
-
const configDir = process.platform === "darwin" ? path3.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path3.join(os.homedir(), "AppData", "Roaming", "flakiness") : path3.join(os.homedir(), ".config", "flakiness");
|
|
212
|
-
return configDir;
|
|
213
|
-
})();
|
|
214
|
-
var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
|
|
215
|
-
var FlakinessSession = class _FlakinessSession {
|
|
216
|
-
constructor(_config) {
|
|
217
|
-
this._config = _config;
|
|
218
|
-
this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
|
|
219
|
-
}
|
|
220
|
-
static async loadOrDie() {
|
|
221
|
-
const session = await _FlakinessSession.load();
|
|
222
|
-
if (!session)
|
|
223
|
-
throw new Error(`Please login first with 'npx flakiness login'`);
|
|
224
|
-
return session;
|
|
225
|
-
}
|
|
226
|
-
static async load() {
|
|
227
|
-
const data = await fs2.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
228
|
-
if (!data)
|
|
229
|
-
return void 0;
|
|
230
|
-
const json = JSON.parse(data);
|
|
231
|
-
return new _FlakinessSession(json);
|
|
232
|
-
}
|
|
233
|
-
static async remove() {
|
|
234
|
-
await fs2.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
235
|
-
}
|
|
236
|
-
api;
|
|
237
|
-
endpoint() {
|
|
238
|
-
return this._config.endpoint;
|
|
239
|
-
}
|
|
240
|
-
path() {
|
|
241
|
-
return CONFIG_PATH;
|
|
242
|
-
}
|
|
243
|
-
sessionToken() {
|
|
244
|
-
return this._config.token;
|
|
245
|
-
}
|
|
246
|
-
async save() {
|
|
247
|
-
await fs2.mkdir(CONFIG_DIR, { recursive: true });
|
|
248
|
-
await fs2.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
188
|
// src/cli/cmd-link.ts
|
|
253
|
-
async function cmdLink(slug) {
|
|
254
|
-
const session = await FlakinessSession.load();
|
|
255
|
-
if (!session) {
|
|
256
|
-
console.log(`Please login first`);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
189
|
+
async function cmdLink(session, slug) {
|
|
259
190
|
const [orgSlug, projectSlug] = slug.split("/");
|
|
260
191
|
const project = await session.api.project.findProject.GET({
|
|
261
192
|
orgSlug,
|
package/lib/cli/cmd-login.js
CHANGED
|
@@ -171,12 +171,13 @@ async function cmdLogout() {
|
|
|
171
171
|
return;
|
|
172
172
|
const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
|
|
173
173
|
if (currentSession)
|
|
174
|
-
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
174
|
+
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
|
|
175
175
|
await FlakinessSession.remove();
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/cli/cmd-login.ts
|
|
179
|
-
|
|
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.
|
|
194
|
-
|
|
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
|
package/lib/cli/cmd-logout.js
CHANGED
|
@@ -161,7 +161,7 @@ async function cmdLogout() {
|
|
|
161
161
|
return;
|
|
162
162
|
const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
|
|
163
163
|
if (currentSession)
|
|
164
|
-
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId });
|
|
164
|
+
await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
|
|
165
165
|
await FlakinessSession.remove();
|
|
166
166
|
}
|
|
167
167
|
export {
|
|
@@ -320,8 +320,10 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
320
320
|
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
321
321
|
"%an",
|
|
322
322
|
// %an: Author name
|
|
323
|
-
"%s"
|
|
323
|
+
"%s",
|
|
324
324
|
// %s: Subject (the first line of the commit message)
|
|
325
|
+
"%P"
|
|
326
|
+
// %P: Parent hashes (space-separated)
|
|
325
327
|
].join(FIELD_SEPARATOR);
|
|
326
328
|
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
327
329
|
try {
|
|
@@ -330,13 +332,14 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
330
332
|
return [];
|
|
331
333
|
}
|
|
332
334
|
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
333
|
-
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
335
|
+
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
336
|
+
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
334
337
|
return {
|
|
335
338
|
commitId,
|
|
336
339
|
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
337
|
-
// Convert timestamp from seconds to milliseconds
|
|
338
340
|
author,
|
|
339
341
|
message,
|
|
342
|
+
parents,
|
|
340
343
|
walkIndex: 0
|
|
341
344
|
};
|
|
342
345
|
});
|
package/lib/cli/cmd-upload.js
CHANGED
|
@@ -288,29 +288,31 @@ var ReportUpload = class {
|
|
|
288
288
|
var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
289
289
|
var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
|
|
290
290
|
var log = (txt) => console.log(`[flakiness.io] ${txt}`);
|
|
291
|
-
async function cmdUpload(
|
|
292
|
-
const fullPath = path2.resolve(relativePath);
|
|
293
|
-
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
294
|
-
err(`Path ${fullPath} is not accessible!`);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
const text = await fs3.readFile(fullPath, "utf-8");
|
|
298
|
-
const report = JSON.parse(text);
|
|
299
|
-
const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
|
|
300
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
301
|
-
if (missingAttachments.length) {
|
|
302
|
-
warn(`Missing ${missingAttachments.length} attachments`);
|
|
303
|
-
}
|
|
291
|
+
async function cmdUpload(relativePaths, options) {
|
|
304
292
|
const uploader = new ReportUploader({
|
|
305
293
|
flakinessAccessToken: options.accessToken,
|
|
306
294
|
flakinessEndpoint: options.endpoint
|
|
307
295
|
});
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
296
|
+
for (const relativePath of relativePaths) {
|
|
297
|
+
const fullPath = path2.resolve(relativePath);
|
|
298
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
299
|
+
err(`Path ${fullPath} is not accessible!`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
const text = await fs3.readFile(fullPath, "utf-8");
|
|
303
|
+
const report = JSON.parse(text);
|
|
304
|
+
const attachmentsDir = options.attachmentsDir ?? path2.dirname(fullPath);
|
|
305
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
306
|
+
if (missingAttachments.length) {
|
|
307
|
+
warn(`Missing ${missingAttachments.length} attachments`);
|
|
308
|
+
}
|
|
309
|
+
const upload = uploader.createUpload(report, Array.from(attachmentIdToPath.values()));
|
|
310
|
+
const uploadResult = await upload.upload();
|
|
311
|
+
if (!uploadResult.success) {
|
|
312
|
+
err(`Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
313
|
+
} else {
|
|
314
|
+
log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
315
|
+
}
|
|
314
316
|
}
|
|
315
317
|
}
|
|
316
318
|
export {
|
package/lib/localGit.js
CHANGED
|
@@ -12,8 +12,10 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
12
12
|
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
13
13
|
"%an",
|
|
14
14
|
// %an: Author name
|
|
15
|
-
"%s"
|
|
15
|
+
"%s",
|
|
16
16
|
// %s: Subject (the first line of the commit message)
|
|
17
|
+
"%P"
|
|
18
|
+
// %P: Parent hashes (space-separated)
|
|
17
19
|
].join(FIELD_SEPARATOR);
|
|
18
20
|
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
19
21
|
try {
|
|
@@ -22,13 +24,14 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
22
24
|
return [];
|
|
23
25
|
}
|
|
24
26
|
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
25
|
-
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
27
|
+
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
28
|
+
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
26
29
|
return {
|
|
27
30
|
commitId,
|
|
28
31
|
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
29
|
-
// Convert timestamp from seconds to milliseconds
|
|
30
32
|
author,
|
|
31
33
|
message,
|
|
34
|
+
parents,
|
|
32
35
|
walkIndex: 0
|
|
33
36
|
};
|
|
34
37
|
});
|
package/lib/localReportApi.js
CHANGED
|
@@ -18,8 +18,10 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
18
18
|
// %at: Author date as a Unix timestamp (seconds since epoch)
|
|
19
19
|
"%an",
|
|
20
20
|
// %an: Author name
|
|
21
|
-
"%s"
|
|
21
|
+
"%s",
|
|
22
22
|
// %s: Subject (the first line of the commit message)
|
|
23
|
+
"%P"
|
|
24
|
+
// %P: Parent hashes (space-separated)
|
|
23
25
|
].join(FIELD_SEPARATOR);
|
|
24
26
|
const command = `git log ${head} -n ${count} --pretty=format:"${prettyFormat}" -z`;
|
|
25
27
|
try {
|
|
@@ -28,13 +30,14 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
28
30
|
return [];
|
|
29
31
|
}
|
|
30
32
|
return stdout.trim().split(RECORD_SEPARATOR).filter((record) => record).map((record) => {
|
|
31
|
-
const [commitId, timestampStr, author, message] = record.split(FIELD_SEPARATOR);
|
|
33
|
+
const [commitId, timestampStr, author, message, parentsStr] = record.split(FIELD_SEPARATOR);
|
|
34
|
+
const parents = parentsStr ? parentsStr.split(" ").filter((p) => p) : [];
|
|
32
35
|
return {
|
|
33
36
|
commitId,
|
|
34
37
|
timestamp: parseInt(timestampStr, 10) * 1e3,
|
|
35
|
-
// Convert timestamp from seconds to milliseconds
|
|
36
38
|
author,
|
|
37
39
|
message,
|
|
40
|
+
parents,
|
|
38
41
|
walkIndex: 0
|
|
39
42
|
};
|
|
40
43
|
});
|