@flakiness/junit-xml 1.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Degu Labs, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @flakiness/junit-xml
2
+
3
+ Convert JUnit XML test reports into a Flakiness report and upload it to [flakiness.io](https://flakiness.io). Parses Surefire and TestNG output, nested `<testsuites>`, retries, `<system-out>` / `<system-err>`, and file attachments.
4
+
5
+ The recommended way to run it is with `npx` (no install step):
6
+
7
+ ```bash
8
+ npx @flakiness/junit-xml ./build/reports/junit --flakiness-project myorg/myproject
9
+ ```
10
+
11
+ This combines every XML file under the given path into a single Flakiness report and uploads it to flakiness.io.
12
+
13
+ If your environment has no Node.js, a [standalone binary](#standalone-binary-no-nodejs) is also available.
14
+
15
+ ## Contents
16
+
17
+ - [Usage](#usage)
18
+ - [Example: ingesting `bun test` results](#example-ingesting-bun-test-results)
19
+ - [Example: ingesting Rust `cargo-nextest` results](#example-ingesting-rust-cargo-nextest-results)
20
+ - [Standalone binary (no Node.js)](#standalone-binary-no-nodejs)
21
+ - [Uploading](#uploading)
22
+ - [License](#license)
23
+
24
+ ## Usage
25
+
26
+ ```
27
+ flakiness-junit-xml <junit-path> [options]
28
+
29
+ <junit-path> JUnit XML file, or a directory of XML files (scanned recursively)
30
+ --env-name <name> Environment name (defaults to --category, or `junit`)
31
+ --commit-id <id> Git commit ID (auto-detected from cwd if omitted)
32
+ --title <title> Report title (env: FLAKINESS_TITLE)
33
+ --output-dir <dir> Output directory (default: flakiness-report)
34
+ -c, --category <category> Category, e.g. `bun`, `rust` (default: `junit`)
35
+ --flakiness-project <project> Flakiness project, `org/project` (env: FLAKINESS_PROJECT)
36
+ --token <token> Flakiness.io access token (env: FLAKINESS_ACCESS_TOKEN)
37
+ --endpoint <url> Flakiness.io API endpoint override
38
+ --disable-upload Convert only; don't upload (env: FLAKINESS_DISABLE_UPLOAD)
39
+ ```
40
+
41
+ Requires Node.js `^20.17.0 || >=22.9.0`.
42
+
43
+ `flakiness-junit-xml` ingests JUnit XML from **any** test runner. Some runners don't emit it by default — the examples below show how to get XML out of the common ones.
44
+
45
+ ## Example: ingesting `bun test` results
46
+
47
+ `bun test` emits JUnit XML with `--reporter=junit`:
48
+
49
+ ```bash
50
+ bun test --reporter=junit --reporter-outfile=./junit.xml
51
+ npx @flakiness/junit-xml ./junit.xml --category bun --flakiness-project myorg/myproject
52
+ ```
53
+
54
+ ## Example: ingesting Rust `cargo-nextest` results
55
+
56
+ `cargo test` doesn't emit JUnit XML; [`cargo-nextest`](https://nexte.st/) does. Add a CI profile in `.config/nextest.toml`:
57
+
58
+ ```toml
59
+ [profile.ci.junit]
60
+ path = "junit.xml"
61
+ ```
62
+
63
+ Then run the tests and point at the XML nextest writes under `target/nextest/`:
64
+
65
+ ```bash
66
+ cargo nextest run --profile ci
67
+ npx @flakiness/junit-xml ./target/nextest/ci/junit.xml --category rust --flakiness-project myorg/myproject
68
+ ```
69
+
70
+ ## Standalone binary (no Node.js)
71
+
72
+ A secondary distribution: a single self-contained executable that bundles its own runtime, so it works on machines without Node.js. The CLI, flags, and behavior are identical to the `npx` version — only the way you launch it differs.
73
+
74
+ **macOS / Linux:**
75
+
76
+ ```bash
77
+ curl -fsSL https://github.com/flakiness/junit-xml/releases/latest/download/install.sh | sh
78
+ ```
79
+
80
+ **Windows (PowerShell):**
81
+
82
+ ```powershell
83
+ irm https://github.com/flakiness/junit-xml/releases/latest/download/install.ps1 | iex
84
+ ```
85
+
86
+ This installs a `flakiness-junit-xml` command on your `PATH`. Then use it exactly as above:
87
+
88
+ ```bash
89
+ flakiness-junit-xml ./build/reports/junit --flakiness-project myorg/myproject
90
+ ```
91
+
92
+ The installer detects your OS/architecture (x64 and arm64; Linux glibc and Alpine/musl) and always pulls the latest release. To pin a directory, set `INSTALL_DIR` (default `/usr/local/bin`):
93
+
94
+ ```bash
95
+ curl -fsSL https://github.com/flakiness/junit-xml/releases/latest/download/install.sh | INSTALL_DIR="$HOME/.local/bin" sh
96
+ ```
97
+
98
+ Prefer `npx` when Node.js is available — it's the primary, always-current path. Reach for the standalone binary only when Node.js isn't an option.
99
+
100
+ ## Uploading
101
+
102
+ The report is uploaded to flakiness.io automatically. Authentication, in priority order:
103
+
104
+ 1. **Access token** — `--token` or `FLAKINESS_ACCESS_TOKEN`.
105
+ 2. **GitHub Actions OIDC** — no token needed when `--flakiness-project` (or `FLAKINESS_PROJECT`) is set, the project is bound to the repository, and the workflow grants `id-token: write`.
106
+
107
+ To convert without uploading, pass `--disable-upload` or set `FLAKINESS_DISABLE_UPLOAD=1`. The report is still written to `--output-dir`.
108
+
109
+ ## License
110
+
111
+ MIT
package/lib/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { CIUtils, GitWorktree, ReportUtils, uploadReport, writeReport } from "@flakiness/sdk";
3
+ import { Command, Option } from "commander";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { parseJUnit } from "./parser.js";
7
+ const STDERR_LOGGER = {
8
+ log: (...args) => console.error(...args),
9
+ warn: (...args) => console.error(...args),
10
+ error: (...args) => console.error(...args)
11
+ };
12
+ function envBool(name) {
13
+ return ["1", "true"].includes(process.env[name]?.toLowerCase() ?? "");
14
+ }
15
+ const program = new Command("flakiness-junit-xml").description("Convert JUnit XML report(s) to a Flakiness report and upload it to flakiness.io").argument("<junit-path>", "Path to a JUnit XML file or a directory containing XML files").option("--env-name <name>", "Environment name for the report (defaults to --category, or `junit` if neither is set)").option("--commit-id <id>", "Git commit ID (auto-detected from the current working directory if not provided)").addOption(new Option("--title <title>", "Human-readable report title").env("FLAKINESS_TITLE")).option("--output-dir <dir>", "Output directory for the report", "flakiness-report").option("-c, --category <category>", "Report category identifier (e.g. `bun`, `rust`). Defaults to `junit`.").addOption(new Option("--flakiness-project <project>", "Flakiness project identifier in `org/project` format").env("FLAKINESS_PROJECT")).addOption(new Option("-p, --project <org/project>").hideHelp()).addOption(new Option("--token <token>", "Flakiness.io access token for upload").env("FLAKINESS_ACCESS_TOKEN")).option("--endpoint <url>", "Flakiness.io API endpoint override").addOption(new Option("--disable-upload", "Convert only; do not upload to flakiness.io").env("FLAKINESS_DISABLE_UPLOAD")).action(async (junitPath, options) => {
16
+ await runConvert(junitPath, {
17
+ envName: options.envName ?? options.category ?? "junit",
18
+ outputDir: options.outputDir,
19
+ commitId: options.commitId,
20
+ title: options.title,
21
+ category: options.category,
22
+ flakinessProject: options.flakinessProject ?? options.project,
23
+ token: options.token,
24
+ endpoint: options.endpoint,
25
+ disableUpload: !!options.disableUpload
26
+ });
27
+ });
28
+ program.parseAsync(process.argv).catch((err) => {
29
+ console.error(err);
30
+ process.exit(1);
31
+ });
32
+ async function runConvert(junitPath, options) {
33
+ const fullPath = path.resolve(junitPath);
34
+ if (!await exists(fullPath)) {
35
+ console.error(`Error: path ${fullPath} is not accessible`);
36
+ process.exit(1);
37
+ }
38
+ const stat = await fs.stat(fullPath);
39
+ const xmlContents = [];
40
+ if (stat.isFile()) {
41
+ xmlContents.push(await fs.readFile(fullPath, "utf-8"));
42
+ } else if (stat.isDirectory()) {
43
+ const xmlFiles = await findXmlFiles(fullPath);
44
+ if (xmlFiles.length === 0) {
45
+ console.error(`Error: No XML files found in directory ${fullPath}`);
46
+ process.exit(1);
47
+ }
48
+ console.log(`Found ${xmlFiles.length} XML file(s)`);
49
+ for (const xmlFile of xmlFiles)
50
+ xmlContents.push(await fs.readFile(xmlFile, "utf-8"));
51
+ } else {
52
+ console.error(`Error: ${fullPath} is neither a file nor a directory`);
53
+ process.exit(1);
54
+ }
55
+ let commitId;
56
+ if (options.commitId) {
57
+ commitId = options.commitId;
58
+ } else {
59
+ const result = GitWorktree.initialize(process.cwd());
60
+ if (!result.ok) {
61
+ console.error(`Failed to detect git commit (${result.error}). Please provide --commit-id.`);
62
+ process.exit(1);
63
+ }
64
+ commitId = result.commitId;
65
+ }
66
+ const { report, attachments } = await parseJUnit(xmlContents, {
67
+ commitId,
68
+ defaultEnv: ReportUtils.createEnvironment({ name: options.envName }),
69
+ runStartTimestamp: Date.now(),
70
+ runDuration: 0,
71
+ runUrl: CIUtils.runUrl(),
72
+ category: options.category
73
+ });
74
+ if (options.title)
75
+ report.title = options.title;
76
+ if (options.flakinessProject)
77
+ report.flakinessProject = options.flakinessProject;
78
+ await writeReport(report, attachments, options.outputDir);
79
+ console.log(`\u2713 Saved to ${options.outputDir}`);
80
+ const disableUpload = options.disableUpload || envBool("FLAKINESS_DISABLE_UPLOAD");
81
+ if (!disableUpload) {
82
+ await uploadReport(report, attachments, {
83
+ flakinessAccessToken: options.token,
84
+ flakinessEndpoint: options.endpoint,
85
+ logger: STDERR_LOGGER
86
+ });
87
+ }
88
+ }
89
+ async function exists(p) {
90
+ return fs.access(p, fs.constants.F_OK).then(() => true).catch(() => false);
91
+ }
92
+ async function findXmlFiles(dir, result = []) {
93
+ const entries = await fs.readdir(dir, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(dir, entry.name);
96
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
97
+ result.push(fullPath);
98
+ else if (entry.isDirectory())
99
+ await findXmlFiles(fullPath, result);
100
+ }
101
+ return result;
102
+ }
103
+ //# sourceMappingURL=cli.js.map
package/lib/parser.js ADDED
@@ -0,0 +1,238 @@
1
+ import { FlakinessReport as FK } from "@flakiness/flakiness-report";
2
+ import { ReportUtils } from "@flakiness/sdk";
3
+ import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
4
+ import assert from "assert";
5
+ import fs from "fs";
6
+ import mime from "mime";
7
+ import path from "path";
8
+ import { Temporal } from "temporal-polyfill";
9
+ let gTZAbbreviationToIANATimezone;
10
+ function tzAbbreviationToIANA(tz) {
11
+ if (!gTZAbbreviationToIANATimezone) {
12
+ gTZAbbreviationToIANATimezone = /* @__PURE__ */ new Map();
13
+ const probes = [/* @__PURE__ */ new Date("2026-06-15T12:00:00Z"), /* @__PURE__ */ new Date("2026-01-15T12:00:00Z")];
14
+ for (const tz2 of Intl.supportedValuesOf("timeZone")) {
15
+ for (const date of probes) {
16
+ const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz2, timeZoneName: "short" }).formatToParts(date);
17
+ const abbr = parts.find((p) => p.type === "timeZoneName")?.value;
18
+ if (abbr)
19
+ gTZAbbreviationToIANATimezone.set(abbr, tz2);
20
+ }
21
+ }
22
+ }
23
+ return gTZAbbreviationToIANATimezone.get(tz);
24
+ }
25
+ function parseTimestamp(timestamp) {
26
+ const native = new Date(timestamp).getTime();
27
+ if (!isNaN(native))
28
+ return native;
29
+ const parts = timestamp.split(/\s+/);
30
+ const iana = parts.length === 2 ? tzAbbreviationToIANA(parts[1]) : void 0;
31
+ if (iana) {
32
+ const d = Temporal.PlainDateTime.from(parts[0]);
33
+ return d.toZonedDateTime(iana).epochMilliseconds;
34
+ }
35
+ throw new Error(`failed to parse timestamp: ${timestamp}`);
36
+ }
37
+ function getProperties(element) {
38
+ const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
39
+ if (!propertiesNodes.length)
40
+ return [];
41
+ const result = [];
42
+ for (const propertiesNode of propertiesNodes) {
43
+ const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
44
+ for (const property of properties) {
45
+ const name = property.attributes["name"];
46
+ const innerText = property.children.find((node) => node instanceof XmlText);
47
+ const value = property.attributes["value"] ?? innerText?.text ?? "";
48
+ result.push([name, value]);
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ function extractErrors(testcase) {
54
+ const xmlErrors = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
55
+ if (!xmlErrors.length)
56
+ return void 0;
57
+ return xmlErrors.map((xmlErr) => parseError(xmlErr));
58
+ }
59
+ function parseError(xmlErr, explicitStackTrace) {
60
+ const stackTraceContainer = explicitStackTrace ? xmlErr.children.find((child) => child instanceof XmlElement && child.name === "stackTrace") : xmlErr;
61
+ const xmlStackNodes = stackTraceContainer?.children.filter((child) => child instanceof XmlText);
62
+ let stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
63
+ let message = "";
64
+ let stackPrefix = "";
65
+ for (const token of [xmlErr.attributes["type"], xmlErr.attributes["message"]]) {
66
+ if (!token)
67
+ continue;
68
+ message = (message ? message + " " : "") + token;
69
+ if (!stack?.includes(token))
70
+ stackPrefix = (stackPrefix ? stackPrefix + " " : "") + token;
71
+ }
72
+ if (stack && stackPrefix)
73
+ stack = stackPrefix + "\n" + stack;
74
+ return {
75
+ message,
76
+ stack
77
+ };
78
+ }
79
+ function extractStdIO(testcase) {
80
+ const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "system-out" || element.name === "system-err");
81
+ return xmlStdio.map((node) => {
82
+ return node.children.filter((child) => child instanceof XmlText).map((txtNode) => ({
83
+ stream: node.name === "system-out" ? FK.STREAM_STDOUT : FK.STREAM_STDERR,
84
+ text: txtNode.text,
85
+ dts: 0
86
+ }));
87
+ }).flat();
88
+ }
89
+ async function parseAttachment(value) {
90
+ let absolutePath = path.resolve(process.cwd(), value);
91
+ if (fs.existsSync(absolutePath))
92
+ return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
93
+ return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
94
+ }
95
+ async function traverseJUnitReport(context, node) {
96
+ const element = node;
97
+ if (!(element instanceof XmlElement))
98
+ return;
99
+ let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs } = context;
100
+ if (element.attributes["timestamp"])
101
+ currentTimeMs = parseTimestamp(element.attributes["timestamp"]);
102
+ if (element.name === "testsuite") {
103
+ const file = element.attributes["file"];
104
+ const line = parseInt(element.attributes["line"], 10);
105
+ const name = element.attributes["name"];
106
+ const newSuite = {
107
+ title: name ?? file,
108
+ location: file && !isNaN(line) ? {
109
+ file,
110
+ line,
111
+ column: 1
112
+ } : void 0,
113
+ type: name ? "suite" : file ? "file" : "anonymous suite",
114
+ suites: [],
115
+ tests: []
116
+ };
117
+ if (currentSuite) {
118
+ currentSuite.suites ??= [];
119
+ currentSuite.suites.push(newSuite);
120
+ } else {
121
+ report.suites ??= [];
122
+ report.suites.push(newSuite);
123
+ }
124
+ currentSuite = newSuite;
125
+ } else if (element.name === "testcase") {
126
+ assert(currentSuite);
127
+ const file = element.attributes["file"];
128
+ const name = element.attributes["name"];
129
+ const line = parseInt(element.attributes["line"], 10);
130
+ const duration = parseFloat(element.attributes["time"]) * 1e3;
131
+ const annotations = [];
132
+ const attachments = [];
133
+ for (const [key, value] of getProperties(element)) {
134
+ if (key.toLowerCase().startsWith("attachment")) {
135
+ if (context.ignoreAttachments)
136
+ continue;
137
+ const attachment = await parseAttachment(value);
138
+ context.attachments.set(attachment.id, attachment);
139
+ attachments.push({
140
+ id: attachment.id,
141
+ contentType: attachment.contentType,
142
+ //TODO: better default names for attachments?
143
+ name: attachment.type === "file" ? path.basename(attachment.path) : `attachment`
144
+ });
145
+ } else {
146
+ annotations.push({
147
+ type: key,
148
+ description: value.length ? value : void 0
149
+ });
150
+ }
151
+ }
152
+ const childElements = element.children.filter((child) => child instanceof XmlElement);
153
+ const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
154
+ if (xmlSkippedAnnotation)
155
+ annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
156
+ const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
157
+ const errors = extractErrors(element);
158
+ const test = {
159
+ title: name,
160
+ location: file && !isNaN(line) ? {
161
+ file,
162
+ line,
163
+ column: 1
164
+ } : void 0,
165
+ attempts: [{
166
+ environmentIdx: currentEnvIndex,
167
+ expectedStatus,
168
+ annotations,
169
+ attachments,
170
+ startTimestamp: 0,
171
+ duration,
172
+ status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
173
+ errors,
174
+ stdio: extractStdIO(element)
175
+ }]
176
+ };
177
+ for (const rerun of element.children.filter((child) => child instanceof XmlElement && ["rerunFailure", "rerunError", "flakyError", "flakyFailure"].includes(child.name))) {
178
+ const duration2 = parseFloat(rerun.attributes["time"] || "0") * 1e3;
179
+ const attempt = {
180
+ environmentIdx: currentEnvIndex,
181
+ expectedStatus,
182
+ annotations,
183
+ startTimestamp: 0,
184
+ duration: duration2,
185
+ status: "failed",
186
+ errors: [parseError(rerun, "explicit-stack-trace")],
187
+ stdio: extractStdIO(rerun)
188
+ };
189
+ if (rerun.name.startsWith("flaky"))
190
+ test.attempts.splice(test.attempts.length - 1, 0, attempt);
191
+ else
192
+ test.attempts.push(attempt);
193
+ }
194
+ for (const attempt of test.attempts) {
195
+ attempt.startTimestamp = currentTimeMs;
196
+ currentTimeMs += attempt.duration;
197
+ }
198
+ currentSuite.tests ??= [];
199
+ currentSuite.tests.push(test);
200
+ }
201
+ context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
202
+ for (const child of element.children)
203
+ await traverseJUnitReport(context, child);
204
+ }
205
+ async function parseJUnit(xmls, options) {
206
+ const report = {
207
+ category: options.category ?? "junit",
208
+ commitId: options.commitId,
209
+ duration: options.runDuration,
210
+ startTimestamp: options.runStartTimestamp,
211
+ url: options.runUrl,
212
+ environments: [options.defaultEnv],
213
+ suites: [],
214
+ unattributedErrors: []
215
+ };
216
+ const context = {
217
+ currentEnv: options.defaultEnv,
218
+ currentEnvIndex: 0,
219
+ currentTimeMs: 0,
220
+ report,
221
+ currentSuite: void 0,
222
+ attachments: /* @__PURE__ */ new Map(),
223
+ ignoreAttachments: !!options.ignoreAttachments
224
+ };
225
+ for (const xml of xmls) {
226
+ const doc = parseXml(xml);
227
+ for (const element of doc.children)
228
+ await traverseJUnitReport(context, element);
229
+ }
230
+ return {
231
+ report: ReportUtils.normalizeReport(report),
232
+ attachments: Array.from(context.attachments.values())
233
+ };
234
+ }
235
+ export {
236
+ parseJUnit
237
+ };
238
+ //# sourceMappingURL=parser.js.map
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@flakiness/junit-xml",
3
+ "version": "1.0.0-alpha.0",
4
+ "private": false,
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/flakiness/junit-xml.git"
8
+ },
9
+ "description": "Convert JUnit XML test reports into Flakiness Reports",
10
+ "bin": {
11
+ "flakiness-junit-xml": "lib/cli.js"
12
+ },
13
+ "scripts": {
14
+ "build": "kubik build.mts",
15
+ "watch": "kubik build.mts -w",
16
+ "test": "playwright test"
17
+ },
18
+ "keywords": [
19
+ "junit",
20
+ "junit-xml",
21
+ "surefire",
22
+ "testng",
23
+ "flakiness",
24
+ "testing",
25
+ "reporter"
26
+ ],
27
+ "author": "Degu Labs, Inc",
28
+ "license": "MIT",
29
+ "type": "module",
30
+ "engines": {
31
+ "node": "^20.17.0 || >=22.9.0"
32
+ },
33
+ "dependencies": {
34
+ "@flakiness/flakiness-report": "^0.34.0",
35
+ "@flakiness/sdk": "^3.2.0",
36
+ "@rgrove/parse-xml": "^4.2.0",
37
+ "commander": "^14.0.3",
38
+ "mime": "^4.1.0",
39
+ "temporal-polyfill": "^0.3.2"
40
+ },
41
+ "devDependencies": {
42
+ "@playwright/test": "^1.57.0",
43
+ "@types/node": "^25.0.3",
44
+ "esbuild": "^0.27.2",
45
+ "kubik": "^0.24.0",
46
+ "tsx": "^4.21.0",
47
+ "typescript": "^5.9.3"
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * This is largely based upon a nice writeup from here: https://github.com/testmoapp/junitxml
3
+ */
4
+ import { FlakinessReport as FK } from '@flakiness/flakiness-report';
5
+ import { ReportUtils } from '@flakiness/sdk';
6
+ export declare function parseJUnit(xmls: string[], options: {
7
+ defaultEnv: FK.Environment;
8
+ commitId: FK.CommitId;
9
+ runDuration: FK.DurationMS;
10
+ runStartTimestamp: FK.UnixTimestampMS;
11
+ runUrl?: string;
12
+ ignoreAttachments?: boolean;
13
+ category?: string;
14
+ }): Promise<{
15
+ report: FK.Report;
16
+ attachments: ReportUtils.Attachment[];
17
+ }>;
18
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,eAAe,IAAI,EAAE,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA2P7C,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IACxD,UAAU,EAAE,EAAE,CAAC,WAAW,CAAC;IAC3B,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC;IACtB,WAAW,EAAE,EAAE,CAAC,UAAU,CAAC;IAC3B,iBAAiB,EAAE,EAAE,CAAC,eAAe,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC;IAAC,WAAW,EAAE,WAAW,CAAC,UAAU,EAAE,CAAA;CAAE,CAAC,CA+BxE"}