testdossier 0.1.0 → 0.1.2
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/.env.testdossier.example +3 -0
- package/README.md +67 -14
- package/lib/testdossier.mjs +180 -24
- package/package.json +4 -8
package/README.md
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
# TestDossier CLI
|
|
2
2
|
|
|
3
|
-
Upload a completed test report from a developer laptop, test machine, or CI
|
|
3
|
+
Upload a completed test report from a developer laptop, test machine, or CI
|
|
4
|
+
job. Run these steps in the project containing your automated tests:
|
|
5
|
+
|
|
6
|
+
1. Create `.env.testdossier`:
|
|
7
|
+
|
|
8
|
+
```dotenv
|
|
9
|
+
TD_CI_TOKEN=replace_with_your_td_token
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Replace the placeholder with the CI-ingestion token shown once by
|
|
13
|
+
TestDossier, add the file to `.gitignore`, and protect it:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
chmod 600 .env.testdossier
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Run your tests so they create a supported report. For example, with
|
|
20
|
+
Playwright's JSON reporter configured to write `playwright-results.json`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx playwright test
|
|
24
|
+
test -f playwright-results.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Upload the completed report:
|
|
4
28
|
|
|
5
29
|
```bash
|
|
6
|
-
|
|
7
|
-
npx testdossier upload playwright-results.json
|
|
30
|
+
npx testdossier upload playwright-results.json --env-file .env.testdossier
|
|
8
31
|
```
|
|
9
32
|
|
|
33
|
+
`playwright-results.json` is an example filename, not a file supplied by
|
|
34
|
+
TestDossier. Use the actual report path produced by your runner. Repeat steps 3
|
|
35
|
+
and 4 whenever you want to publish a new run.
|
|
36
|
+
If tests run in multiple shards or batches, merge all shard reports before
|
|
37
|
+
uploading; selecting one shard's file uploads only that shard.
|
|
38
|
+
After merging, the CLI automatically sends Playwright and generic JSON reports
|
|
39
|
+
larger than 200 tests as sequential pages under one stable Test Run.
|
|
40
|
+
|
|
10
41
|
The CLI auto-detects Playwright JSON, Cypress JSON, Cucumber JSON, JUnit XML,
|
|
11
42
|
and TestDossier's generic JSON format. It has zero runtime dependencies and
|
|
12
43
|
requires Node.js 18 or newer.
|
|
@@ -17,10 +48,12 @@ The uploader does one thing: read a completed report and make an outbound HTTPS
|
|
|
17
48
|
request to TestDossier. It does not run tests, accept remote commands, install a
|
|
18
49
|
background service, watch files, or keep a process running.
|
|
19
50
|
|
|
20
|
-
The access token is read
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
The access token is read from an explicitly selected env file or from the
|
|
52
|
+
`TD_CI_TOKEN` process environment. There is deliberately no `--token` option,
|
|
53
|
+
so a token cannot accidentally land in shell history or a process listing.
|
|
54
|
+
The CLI reads only `TD_CI_TOKEN` from the selected file; it does not import or
|
|
55
|
+
execute other entries. Redirects are rejected, and non-HTTPS destinations are
|
|
56
|
+
rejected except for localhost development.
|
|
24
57
|
|
|
25
58
|
Before sending anything, inspect what the CLI detected:
|
|
26
59
|
|
|
@@ -45,31 +78,47 @@ creates an ambiguous shape, pass `--format playwright`, `--format cypress`,
|
|
|
45
78
|
## Local setup
|
|
46
79
|
|
|
47
80
|
Create a project access token in TestDossier with the **CI ingestion**
|
|
48
|
-
capability
|
|
81
|
+
capability and copy it once. For repeat use, install and pin the CLI in the test
|
|
82
|
+
repository so its source and version are captured by the lockfile:
|
|
49
83
|
|
|
50
84
|
```bash
|
|
51
|
-
|
|
85
|
+
npm install --save-dev testdossier
|
|
86
|
+
cp node_modules/testdossier/.env.testdossier.example .env.testdossier
|
|
52
87
|
```
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
Alternatively, create `.env.testdossier` yourself with the single placeholder
|
|
90
|
+
line shown at the top of this guide. Replace `replace_with_your_td_token`, then
|
|
91
|
+
ensure the project ignores and protects the real file:
|
|
92
|
+
|
|
93
|
+
```gitignore
|
|
94
|
+
.env.testdossier
|
|
95
|
+
```
|
|
56
96
|
|
|
57
97
|
```bash
|
|
58
|
-
|
|
59
|
-
npx testdossier upload playwright-results.json
|
|
98
|
+
chmod 600 .env.testdossier
|
|
60
99
|
```
|
|
61
100
|
|
|
101
|
+
Upload with:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx testdossier upload playwright-results.json --env-file .env.testdossier
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For a one-off session, setting `TD_CI_TOKEN` in the process environment still
|
|
108
|
+
works. An explicitly passed `--env-file` takes precedence.
|
|
109
|
+
|
|
62
110
|
The default destination is `https://testdossier.com`. A self-hosted or local
|
|
63
111
|
instance can be selected without putting the token on the command line:
|
|
64
112
|
|
|
65
113
|
```bash
|
|
66
|
-
TD_URL="https://dossier.example.com" npx testdossier upload junit-results.xml
|
|
114
|
+
TD_URL="https://dossier.example.com" npx testdossier upload junit-results.xml --env-file .env.testdossier
|
|
67
115
|
```
|
|
68
116
|
|
|
69
117
|
Useful metadata options:
|
|
70
118
|
|
|
71
119
|
```bash
|
|
72
120
|
npx testdossier upload junit-results.xml \
|
|
121
|
+
--env-file .env.testdossier \
|
|
73
122
|
--run-id "local-2026-07-23-1" \
|
|
74
123
|
--build "abc123" \
|
|
75
124
|
--branch "main"
|
|
@@ -81,3 +130,7 @@ one, the CLI creates a new local run ID. Transient network, rate-limit, conflict
|
|
|
81
130
|
and server errors are retried with the same run ID.
|
|
82
131
|
|
|
83
132
|
Run `npx testdossier --help` for every option.
|
|
133
|
+
|
|
134
|
+
If `.env.testdossier` is ever committed or shared, revoke that token in
|
|
135
|
+
TestDossier immediately and create a replacement. Hosted CI should use the
|
|
136
|
+
provider's encrypted secret storage instead of an env file.
|
package/lib/testdossier.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
export const VERSION = "0.1.
|
|
4
|
+
export const VERSION = "0.1.2";
|
|
5
5
|
const DEFAULT_ORIGIN = "https://testdossier.com";
|
|
6
6
|
const MAX_REPORT_BYTES = 25 * 1024 * 1024;
|
|
7
|
+
const MAX_TESTS_PER_PAGE = 200;
|
|
7
8
|
const RETRYABLE_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
8
9
|
const FORMAT_LABELS = {
|
|
9
10
|
cypress: "Cypress JSON",
|
|
@@ -84,6 +85,76 @@ export function detectReportFormat(text) {
|
|
|
84
85
|
return matches[0];
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function playwrightTestNodes(suites, found = []) {
|
|
89
|
+
for (const suite of Array.isArray(suites) ? suites : []) {
|
|
90
|
+
if (!suite || typeof suite !== "object") continue;
|
|
91
|
+
for (const spec of Array.isArray(suite.specs) ? suite.specs : []) {
|
|
92
|
+
if (!spec || typeof spec !== "object") continue;
|
|
93
|
+
for (const test of Array.isArray(spec.tests) ? spec.tests : []) {
|
|
94
|
+
found.push(test);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
playwrightTestNodes(suite.suites, found);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectedPlaywrightSuite(suite, selectedTests) {
|
|
103
|
+
if (!suite || typeof suite !== "object") return null;
|
|
104
|
+
const next = { ...suite };
|
|
105
|
+
next.specs = (Array.isArray(suite.specs) ? suite.specs : [])
|
|
106
|
+
.filter((spec) => spec && typeof spec === "object")
|
|
107
|
+
.map((spec) => ({
|
|
108
|
+
...spec,
|
|
109
|
+
tests: (Array.isArray(spec.tests) ? spec.tests : []).filter((test) => selectedTests.has(test)),
|
|
110
|
+
}))
|
|
111
|
+
.filter((spec) => spec.tests.length > 0);
|
|
112
|
+
next.suites = (Array.isArray(suite.suites) ? suite.suites : [])
|
|
113
|
+
.map((child) => selectedPlaywrightSuite(child, selectedTests))
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
return next.specs.length > 0 || next.suites.length > 0 ? next : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function splitReportPages(format, report, maxTests = MAX_TESTS_PER_PAGE) {
|
|
119
|
+
if (!Number.isInteger(maxTests) || maxTests < 1 || maxTests > MAX_TESTS_PER_PAGE) {
|
|
120
|
+
throw new CliError(`page size must be an integer from 1 to ${MAX_TESTS_PER_PAGE}.`);
|
|
121
|
+
}
|
|
122
|
+
if (format !== "playwright" && format !== "generic") {
|
|
123
|
+
return [{ body: report, tests: null }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const document = parseJson(report.toString("utf8").replace(/^\uFEFF/, ""));
|
|
127
|
+
if (format === "generic") {
|
|
128
|
+
const tests = document.tests;
|
|
129
|
+
if (tests.length <= maxTests) return [{ body: report, tests: tests.length }];
|
|
130
|
+
const pages = [];
|
|
131
|
+
for (let start = 0; start < tests.length; start += maxTests) {
|
|
132
|
+
const slice = tests.slice(start, start + maxTests);
|
|
133
|
+
pages.push({
|
|
134
|
+
body: Buffer.from(JSON.stringify({ ...document, tests: slice })),
|
|
135
|
+
tests: slice.length,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return pages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const tests = playwrightTestNodes(document.suites);
|
|
142
|
+
if (tests.length <= maxTests) return [{ body: report, tests: tests.length }];
|
|
143
|
+
const pages = [];
|
|
144
|
+
for (let start = 0; start < tests.length; start += maxTests) {
|
|
145
|
+
const slice = tests.slice(start, start + maxTests);
|
|
146
|
+
const selected = new Set(slice);
|
|
147
|
+
const suites = document.suites
|
|
148
|
+
.map((suite) => selectedPlaywrightSuite(suite, selected))
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
pages.push({
|
|
151
|
+
body: Buffer.from(JSON.stringify({ ...document, suites })),
|
|
152
|
+
tests: slice.length,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return pages;
|
|
156
|
+
}
|
|
157
|
+
|
|
87
158
|
function isLoopback(hostname) {
|
|
88
159
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
89
160
|
}
|
|
@@ -130,6 +201,7 @@ export function parseUploadArgs(args) {
|
|
|
130
201
|
branch: undefined,
|
|
131
202
|
provider: undefined,
|
|
132
203
|
ciUrl: undefined,
|
|
204
|
+
envFile: undefined,
|
|
133
205
|
dryRun: false,
|
|
134
206
|
reportPath: undefined,
|
|
135
207
|
};
|
|
@@ -141,6 +213,7 @@ export function parseUploadArgs(args) {
|
|
|
141
213
|
["--branch", "branch"],
|
|
142
214
|
["--provider", "provider"],
|
|
143
215
|
["--ci-url", "ciUrl"],
|
|
216
|
+
["--env-file", "envFile"],
|
|
144
217
|
]);
|
|
145
218
|
|
|
146
219
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -229,6 +302,41 @@ function integerEnv(value, fallback, min, max) {
|
|
|
229
302
|
return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
|
|
230
303
|
}
|
|
231
304
|
|
|
305
|
+
export function parseTokenEnvFile(contents) {
|
|
306
|
+
let token;
|
|
307
|
+
const lines = String(contents).replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
308
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
309
|
+
const line = lines[index].trim();
|
|
310
|
+
if (!line || line.startsWith("#")) continue;
|
|
311
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
312
|
+
if (!match || match[1] !== "TD_CI_TOKEN") continue;
|
|
313
|
+
if (token !== undefined) throw new CliError("env file contains TD_CI_TOKEN more than once.");
|
|
314
|
+
|
|
315
|
+
let value = match[2].trim();
|
|
316
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
317
|
+
value = value.slice(1, -1);
|
|
318
|
+
} else {
|
|
319
|
+
value = value.replace(/\s+#.*$/, "").trim();
|
|
320
|
+
}
|
|
321
|
+
if (!value) throw new CliError(`TD_CI_TOKEN is empty in the env file (line ${index + 1}).`);
|
|
322
|
+
token = value;
|
|
323
|
+
}
|
|
324
|
+
if (token === undefined) throw new CliError("env file does not define TD_CI_TOKEN.");
|
|
325
|
+
return token;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function tokenFromEnvFile(path, stderr) {
|
|
329
|
+
const fileStats = await stat(path).catch((error) => {
|
|
330
|
+
throw new CliError(`could not read env file: ${error.message}`);
|
|
331
|
+
});
|
|
332
|
+
if (!fileStats.isFile()) throw new CliError("env-file path must point to a file.");
|
|
333
|
+
if (fileStats.size > 64 * 1024) throw new CliError("env file is unexpectedly large (maximum 64 KiB).");
|
|
334
|
+
if (process.platform !== "win32" && (fileStats.mode & 0o077) !== 0) {
|
|
335
|
+
stderr(`Warning: ${path} is readable by other users; run chmod 600 "${path}".`);
|
|
336
|
+
}
|
|
337
|
+
return parseTokenEnvFile(await readFile(path, "utf8"));
|
|
338
|
+
}
|
|
339
|
+
|
|
232
340
|
function retryDelayMs(attempt, retryAfter, baseDelay) {
|
|
233
341
|
if (retryAfter) {
|
|
234
342
|
const seconds = Number(retryAfter);
|
|
@@ -303,18 +411,23 @@ Options:
|
|
|
303
411
|
--branch <value> Branch name
|
|
304
412
|
--provider <value> Provider label (defaults to local or detected CI)
|
|
305
413
|
--ci-url <url> Link to the originating CI run
|
|
414
|
+
--env-file <path> Read TD_CI_TOKEN from this explicit dotenv file
|
|
306
415
|
--dry-run Validate and detect only; send nothing
|
|
307
416
|
-h, --help Show help
|
|
308
417
|
--version Show version
|
|
309
418
|
|
|
310
419
|
Authentication:
|
|
311
|
-
Set TD_CI_TOKEN
|
|
312
|
-
accepted as command-line arguments, keeping
|
|
313
|
-
process listings.
|
|
420
|
+
Set TD_CI_TOKEN in the environment or pass --env-file .env.testdossier.
|
|
421
|
+
Tokens are intentionally not accepted as command-line arguments, keeping
|
|
422
|
+
them out of shell history and process listings.
|
|
423
|
+
|
|
424
|
+
Large reports:
|
|
425
|
+
Playwright and generic JSON reports above 200 tests are automatically sent
|
|
426
|
+
as sequential pages that remain grouped under one TestDossier run.
|
|
314
427
|
|
|
315
428
|
Examples:
|
|
316
|
-
|
|
317
|
-
|
|
429
|
+
npx testdossier upload playwright-results.json --env-file .env.testdossier
|
|
430
|
+
npx testdossier upload junit-results.xml --env-file .env.testdossier --build abc123
|
|
318
431
|
npx testdossier upload testdossier-ci.json --dry-run`;
|
|
319
432
|
}
|
|
320
433
|
|
|
@@ -331,6 +444,21 @@ function outputSummary(result, stdout, stderr) {
|
|
|
331
444
|
}
|
|
332
445
|
}
|
|
333
446
|
|
|
447
|
+
function aggregatePageResults(results) {
|
|
448
|
+
const aggregate = { counts: {}, warnings: [] };
|
|
449
|
+
for (const result of results) {
|
|
450
|
+
if (result && result.counts && typeof result.counts === "object") {
|
|
451
|
+
for (const [name, value] of Object.entries(result.counts)) {
|
|
452
|
+
if (typeof value === "number") {
|
|
453
|
+
aggregate.counts[name] = (aggregate.counts[name] || 0) + value;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(result && result.warnings)) aggregate.warnings.push(...result.warnings);
|
|
458
|
+
}
|
|
459
|
+
return aggregate;
|
|
460
|
+
}
|
|
461
|
+
|
|
334
462
|
export async function runCli(
|
|
335
463
|
args,
|
|
336
464
|
{
|
|
@@ -372,45 +500,73 @@ export async function runCli(
|
|
|
372
500
|
const reportText = report.toString("utf8");
|
|
373
501
|
const format = options.format === "auto" ? detectReportFormat(reportText) : options.format;
|
|
374
502
|
assertFormatShape(format, reportText);
|
|
503
|
+
const pages = splitReportPages(format, report);
|
|
504
|
+
const testCount = pages.every((page) => typeof page.tests === "number")
|
|
505
|
+
? pages.reduce((sum, page) => sum + page.tests, 0)
|
|
506
|
+
: null;
|
|
375
507
|
|
|
376
508
|
const endpoint = normalizeEndpoint(options.url || env.TD_URL || DEFAULT_ORIGIN);
|
|
377
509
|
const destination = new URL(endpoint).origin;
|
|
378
|
-
stdout(
|
|
510
|
+
stdout(
|
|
511
|
+
`Validated ${FORMAT_LABELS[format]} (${report.byteLength} bytes` +
|
|
512
|
+
(testCount == null
|
|
513
|
+
? ""
|
|
514
|
+
: `, ${testCount} tests${pages.length > 1 ? ` across ${pages.length} pages` : ""}`) +
|
|
515
|
+
").",
|
|
516
|
+
);
|
|
379
517
|
if (options.dryRun) {
|
|
380
518
|
stdout(`Dry run complete; no data was sent to ${destination}.`);
|
|
381
519
|
return 0;
|
|
382
520
|
}
|
|
383
521
|
|
|
384
|
-
const token =
|
|
522
|
+
const token = options.envFile
|
|
523
|
+
? await tokenFromEnvFile(options.envFile, stderr)
|
|
524
|
+
: env.TD_CI_TOKEN;
|
|
385
525
|
if (!token || !token.startsWith("td_")) {
|
|
386
526
|
throw new CliError("TD_CI_TOKEN is required and must be a TestDossier td_ access token.");
|
|
387
527
|
}
|
|
388
528
|
if (typeof fetchImpl !== "function") throw new CliError("Node 18 or newer is required (global fetch is unavailable).");
|
|
389
529
|
|
|
390
530
|
const metadata = inferredMetadata(options, env);
|
|
391
|
-
const
|
|
531
|
+
const baseHeaders = {
|
|
392
532
|
Authorization: `Bearer ${token}`,
|
|
393
533
|
"Content-Type": format === "junit" ? "application/xml" : "application/json",
|
|
394
534
|
"User-Agent": `testdossier-cli/${VERSION}`,
|
|
395
535
|
"X-CI-Run-Id": metadata.runId,
|
|
396
|
-
"X-CI-Page-Id": "results",
|
|
397
536
|
"X-CI-Provider": metadata.provider,
|
|
398
537
|
};
|
|
399
|
-
if (format !== "junit" && format !== "generic")
|
|
400
|
-
if (metadata.build)
|
|
401
|
-
if (metadata.branch)
|
|
402
|
-
if (metadata.ciUrl)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
538
|
+
if (format !== "junit" && format !== "generic") baseHeaders["X-CI-Format"] = format;
|
|
539
|
+
if (metadata.build) baseHeaders["X-Build"] = metadata.build;
|
|
540
|
+
if (metadata.branch) baseHeaders["X-Branch"] = metadata.branch;
|
|
541
|
+
if (metadata.ciUrl) baseHeaders["X-CI-URL"] = metadata.ciUrl;
|
|
542
|
+
|
|
543
|
+
if (pages.length > 1) {
|
|
544
|
+
stdout(
|
|
545
|
+
`Uploading ${pages.reduce((sum, page) => sum + (page.tests || 0), 0)} tests in ` +
|
|
546
|
+
`${pages.length} pages (run ${metadata.runId}).`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const pageResults = [];
|
|
550
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
551
|
+
const page = pages[index];
|
|
552
|
+
const result = await postWithRetry({
|
|
553
|
+
endpoint,
|
|
554
|
+
headers: {
|
|
555
|
+
...baseHeaders,
|
|
556
|
+
"X-CI-Page-Id": pages.length === 1 ? "results" : String(index + 1),
|
|
557
|
+
},
|
|
558
|
+
body: page.body,
|
|
559
|
+
env,
|
|
560
|
+
fetchImpl,
|
|
561
|
+
stderr,
|
|
562
|
+
});
|
|
563
|
+
pageResults.push(result);
|
|
564
|
+
if (pages.length > 1) {
|
|
565
|
+
stdout(`Uploaded page ${index + 1}/${pages.length} (${page.tests} tests).`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
412
568
|
stdout(`Uploaded ${FORMAT_LABELS[format]} to ${destination} (run ${metadata.runId}).`);
|
|
413
|
-
outputSummary(
|
|
569
|
+
outputSummary(aggregatePageResults(pageResults), stdout, stderr);
|
|
414
570
|
return 0;
|
|
415
571
|
} catch (error) {
|
|
416
572
|
const message = error && error.message ? error.message : String(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdossier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Upload completed test reports to TestDossier from a local machine or CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,19 +9,15 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
".env.testdossier.example"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=18"
|
|
16
17
|
},
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/devkalu/test-dossier.git",
|
|
20
|
-
"directory": "cli"
|
|
21
|
-
},
|
|
22
18
|
"homepage": "https://testdossier.com",
|
|
23
19
|
"bugs": {
|
|
24
|
-
"
|
|
20
|
+
"email": "support@testdossier.com"
|
|
25
21
|
},
|
|
26
22
|
"keywords": [
|
|
27
23
|
"testing",
|