testdossier 0.2.1 → 0.2.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/README.md +37 -13
- package/lib/testdossier.mjs +52 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
Upload a completed test report from a developer laptop, test machine, or CI
|
|
4
4
|
job. Run these commands in the project containing your automated tests:
|
|
5
5
|
|
|
6
|
-
1.
|
|
6
|
+
1. Install the latest CLI in the project. To pin a release instead, replace
|
|
7
|
+
`@latest` with an exact version such as `@0.2.2`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev testdossier@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. One-time setup — creates `.env.testdossier` with owner-only permissions and
|
|
7
14
|
adds it to `.gitignore`:
|
|
8
15
|
|
|
9
16
|
```bash
|
|
10
17
|
npx testdossier init
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
3. Paste the CI-ingestion token shown once by TestDossier into
|
|
14
21
|
`.env.testdossier`, replacing the placeholder, then prove the wiring in
|
|
15
22
|
seconds instead of after a full CI round-trip:
|
|
16
23
|
|
|
@@ -22,14 +29,14 @@ npx testdossier verify
|
|
|
22
29
|
token is valid, has the CI-ingestion capability, and reaches the right
|
|
23
30
|
project — and prints that project's name.
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
4. Run your tests so they create a supported report. For example, with
|
|
26
33
|
Playwright's JSON reporter configured to write `playwright-results.json`:
|
|
27
34
|
|
|
28
35
|
```bash
|
|
29
36
|
npx playwright test
|
|
30
37
|
```
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
5. Upload the completed report:
|
|
33
40
|
|
|
34
41
|
```bash
|
|
35
42
|
npx testdossier upload playwright-results.json
|
|
@@ -79,18 +86,18 @@ delete the file to change it.
|
|
|
79
86
|
Before sending anything, inspect what the CLI detected:
|
|
80
87
|
|
|
81
88
|
```bash
|
|
82
|
-
npx
|
|
89
|
+
npx testdossier upload playwright-results.json --dry-run
|
|
83
90
|
```
|
|
84
91
|
|
|
85
92
|
## Supported reports
|
|
86
93
|
|
|
87
94
|
| Runner or format | Example |
|
|
88
95
|
|---|---|
|
|
89
|
-
| Playwright JSON | `npx
|
|
90
|
-
| Cypress JSON | `npx
|
|
91
|
-
| Cucumber JSON | `npx
|
|
92
|
-
| JUnit XML | `npx
|
|
93
|
-
| Generic JSON | `npx
|
|
96
|
+
| Playwright JSON | `npx testdossier upload playwright-results.json` |
|
|
97
|
+
| Cypress JSON | `npx testdossier upload cypress-results.json` |
|
|
98
|
+
| Cucumber JSON | `npx testdossier upload cucumber-results.json` |
|
|
99
|
+
| JUnit XML | `npx testdossier upload junit-results.xml` |
|
|
100
|
+
| Generic JSON | `npx testdossier upload testdossier-ci.json` |
|
|
94
101
|
|
|
95
102
|
Detection uses the report's contents, not its filename. If a custom reporter
|
|
96
103
|
creates an ambiguous shape, pass `--format playwright`, `--format cypress`,
|
|
@@ -103,10 +110,14 @@ For repeat use, install and pin the CLI in the test repository so its source
|
|
|
103
110
|
and version are captured by the lockfile:
|
|
104
111
|
|
|
105
112
|
```bash
|
|
106
|
-
npm install --save-dev testdossier
|
|
113
|
+
npm install --save-dev testdossier@latest
|
|
107
114
|
npx testdossier init
|
|
108
115
|
```
|
|
109
116
|
|
|
117
|
+
Use `npm install --save-dev testdossier@0.2.2` when the repository should pin
|
|
118
|
+
that exact release. In either case, subsequent commands remain
|
|
119
|
+
`npx testdossier ...`; the version selector belongs only to installation.
|
|
120
|
+
|
|
110
121
|
`init` is idempotent: it never overwrites an existing `.env.testdossier` and
|
|
111
122
|
never duplicates the `.gitignore` entry. If you prefer manual setup, create
|
|
112
123
|
`.env.testdossier` yourself with the single line
|
|
@@ -134,7 +145,8 @@ Useful metadata options:
|
|
|
134
145
|
npx testdossier upload junit-results.xml \
|
|
135
146
|
--run-id "local-2026-07-23-1" \
|
|
136
147
|
--build "abc123" \
|
|
137
|
-
--branch "main"
|
|
148
|
+
--branch "main" \
|
|
149
|
+
--pipeline-id "github:acme/storefront:e2e"
|
|
138
150
|
```
|
|
139
151
|
|
|
140
152
|
`--run-id` is the idempotency key. Repeating the same upload with the same run
|
|
@@ -145,18 +157,30 @@ page identity includes the report's file name and a path fingerprint). Without
|
|
|
145
157
|
a run ID, the CLI creates a new local one. Transient network, rate-limit,
|
|
146
158
|
conflict, and server errors are retried with the same run ID.
|
|
147
159
|
|
|
160
|
+
`--pipeline-id` identifies the stable repository/workflow/job that produced the
|
|
161
|
+
run. It is inferred on supported CI providers and can also be set with
|
|
162
|
+
`TD_PIPELINE_ID`. Keep it stable across access-token rotation: Insights uses it
|
|
163
|
+
to let the latest completed result replace an earlier result from the same
|
|
164
|
+
pipeline without allowing another repository's result to be overwritten.
|
|
165
|
+
|
|
148
166
|
Run `npx testdossier --help` for every option.
|
|
149
167
|
|
|
150
168
|
## Using the same flow in CI
|
|
151
169
|
|
|
152
170
|
Commit `testdossier.json` and set `TD_CI_TOKEN` in the provider's encrypted
|
|
153
|
-
secret storage; the pipeline step is
|
|
171
|
+
secret storage; after the dependency install, the pipeline step is
|
|
172
|
+
`npx testdossier` with
|
|
154
173
|
no arguments. Run identity, build, branch, and provider metadata are inferred
|
|
155
174
|
automatically on GitHub Actions, GitLab CI, Jenkins, Azure DevOps, Bitbucket
|
|
156
175
|
Pipelines, and CircleCI. The Access Tokens dialog still generates
|
|
157
176
|
provider-specific snippets that pass the report path explicitly — either style
|
|
158
177
|
works; the explicit one fails louder when the report artifact is missing.
|
|
159
178
|
|
|
179
|
+
Use `@latest` only when adding/updating the dependency. The lockfile then keeps
|
|
180
|
+
CI reproducible, and normal invocations use the installed command without a
|
|
181
|
+
version selector. Update or pin the dependency through your normal dependency
|
|
182
|
+
update process.
|
|
183
|
+
|
|
160
184
|
If `.env.testdossier` is ever committed or shared, revoke that token in
|
|
161
185
|
TestDossier immediately and create a replacement. Hosted CI should use the
|
|
162
186
|
provider's encrypted secret storage instead of an env file.
|
package/lib/testdossier.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
|
|
5
|
-
export const VERSION = "0.2.
|
|
5
|
+
export const VERSION = "0.2.2";
|
|
6
6
|
const DEFAULT_ORIGIN = "https://testdossier.com";
|
|
7
7
|
const ENV_FILE_NAME = ".env.testdossier";
|
|
8
8
|
const CONFIG_FILE_NAME = "testdossier.json";
|
|
@@ -347,6 +347,7 @@ export function parseUploadArgs(args) {
|
|
|
347
347
|
build: undefined,
|
|
348
348
|
branch: undefined,
|
|
349
349
|
provider: undefined,
|
|
350
|
+
pipelineId: undefined,
|
|
350
351
|
ciUrl: undefined,
|
|
351
352
|
envFile: undefined,
|
|
352
353
|
dryRun: false,
|
|
@@ -359,6 +360,7 @@ export function parseUploadArgs(args) {
|
|
|
359
360
|
["--build", "build"],
|
|
360
361
|
["--branch", "branch"],
|
|
361
362
|
["--provider", "provider"],
|
|
363
|
+
["--pipeline-id", "pipelineId"],
|
|
362
364
|
["--ci-url", "ciUrl"],
|
|
363
365
|
["--env-file", "envFile"],
|
|
364
366
|
]);
|
|
@@ -569,6 +571,46 @@ function inferredProvider(env) {
|
|
|
569
571
|
return "local";
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
function boundedPipelineId(value) {
|
|
575
|
+
const normalized = normalizedHeader(value, "pipeline id");
|
|
576
|
+
if (normalized.length <= 200) return normalized;
|
|
577
|
+
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 20);
|
|
578
|
+
return `${normalized.slice(0, 175)}-${digest}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function inferredPipelineId(env, provider) {
|
|
582
|
+
if (provider === "github_actions") {
|
|
583
|
+
const repository = env.GITHUB_REPOSITORY || "unknown-repository";
|
|
584
|
+
const refPrefix = `${repository}/`;
|
|
585
|
+
const workflowRef = String(env.GITHUB_WORKFLOW_REF || "").split("@")[0];
|
|
586
|
+
const workflow = workflowRef.startsWith(refPrefix)
|
|
587
|
+
? workflowRef.slice(refPrefix.length)
|
|
588
|
+
: workflowRef || env.GITHUB_WORKFLOW || "workflow";
|
|
589
|
+
return [provider, repository, workflow, env.GITHUB_JOB || "job"].join(":");
|
|
590
|
+
}
|
|
591
|
+
if (provider === "gitlab_ci") {
|
|
592
|
+
return [provider, env.CI_PROJECT_PATH || "unknown-project", env.CI_JOB_NAME || "job"].join(":");
|
|
593
|
+
}
|
|
594
|
+
if (provider === "azure_devops") {
|
|
595
|
+
return [
|
|
596
|
+
provider,
|
|
597
|
+
env.SYSTEM_TEAMPROJECT || "unknown-project",
|
|
598
|
+
env.BUILD_REPOSITORY_NAME || env.BUILD_REPOSITORY_ID || "repository",
|
|
599
|
+
env.SYSTEM_DEFINITIONID || env.BUILD_DEFINITIONNAME || "pipeline",
|
|
600
|
+
env.SYSTEM_JOBDISPLAYNAME || env.SYSTEM_JOBID || "job",
|
|
601
|
+
].join(":");
|
|
602
|
+
}
|
|
603
|
+
if (provider === "bitbucket_pipelines") {
|
|
604
|
+
return [provider, env.BITBUCKET_REPO_FULL_NAME || "unknown-repository"].join(":");
|
|
605
|
+
}
|
|
606
|
+
if (provider === "circleci") {
|
|
607
|
+
const repository = [env.CIRCLE_PROJECT_USERNAME, env.CIRCLE_PROJECT_REPONAME].filter(Boolean).join("/") || "unknown-repository";
|
|
608
|
+
return [provider, repository, env.CIRCLE_JOB || "job"].join(":");
|
|
609
|
+
}
|
|
610
|
+
if (provider === "jenkins") return [provider, env.JOB_NAME || "job"].join(":");
|
|
611
|
+
return `${provider || "local"}:local`;
|
|
612
|
+
}
|
|
613
|
+
|
|
572
614
|
function inferredMetadata(options, env) {
|
|
573
615
|
// The job identifier is part of every inferred key: two jobs of one
|
|
574
616
|
// workflow/pipeline run would otherwise share a run id, and the server
|
|
@@ -618,7 +660,10 @@ function inferredMetadata(options, env) {
|
|
|
618
660
|
"CI URL",
|
|
619
661
|
500,
|
|
620
662
|
);
|
|
621
|
-
|
|
663
|
+
const pipelineId = boundedPipelineId(
|
|
664
|
+
options.pipelineId || env.TD_PIPELINE_ID || inferredPipelineId(env, provider),
|
|
665
|
+
);
|
|
666
|
+
return { runId, build, branch, provider, ciUrl, pipelineId };
|
|
622
667
|
}
|
|
623
668
|
|
|
624
669
|
function integerEnv(value, fallback, min, max) {
|
|
@@ -771,6 +816,7 @@ Options:
|
|
|
771
816
|
--build <value> Commit, build number, or release identifier
|
|
772
817
|
--branch <value> Branch name
|
|
773
818
|
--provider <value> Provider label (defaults to local or detected CI)
|
|
819
|
+
--pipeline-id <id> Stable repository/workflow/job identity (normally inferred)
|
|
774
820
|
--ci-url <url> Link to the originating CI run
|
|
775
821
|
--env-file <path> Read TD_CI_TOKEN from this explicit dotenv file
|
|
776
822
|
--dry-run Validate and detect only; send nothing
|
|
@@ -958,6 +1004,7 @@ export async function runCli(
|
|
|
958
1004
|
"User-Agent": `testdossier-cli/${VERSION}`,
|
|
959
1005
|
"X-CI-Run-Id": metadata.runId,
|
|
960
1006
|
"X-CI-Provider": metadata.provider,
|
|
1007
|
+
"X-CI-Pipeline-Id": metadata.pipelineId,
|
|
961
1008
|
};
|
|
962
1009
|
if (format !== "junit" && format !== "generic") baseHeaders["X-CI-Format"] = format;
|
|
963
1010
|
if (metadata.build) baseHeaders["X-Build"] = metadata.build;
|
|
@@ -983,6 +1030,9 @@ export async function runCli(
|
|
|
983
1030
|
headers: {
|
|
984
1031
|
...baseHeaders,
|
|
985
1032
|
"X-CI-Page-Id": pages.length === 1 ? pageBase : `${pageBase}-${index + 1}`,
|
|
1033
|
+
"X-CI-Upload-Id": pageBase,
|
|
1034
|
+
"X-CI-Page-Index": String(index + 1),
|
|
1035
|
+
"X-CI-Page-Count": String(pages.length),
|
|
986
1036
|
},
|
|
987
1037
|
body: page.body,
|
|
988
1038
|
env,
|