testdossier 0.2.1 → 0.2.3
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 +41 -13
- package/assets/mark.svg +5 -0
- package/lib/testdossier.mjs +54 -3
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/devkalu/test-dossier/main/cli/assets/mark.svg" width="44" height="44" alt="TestDossier">
|
|
2
|
+
|
|
1
3
|
# TestDossier CLI
|
|
2
4
|
|
|
5
|
+
**Publish test evidence from CI without adding another agent, daemon, or hosted runner.**
|
|
6
|
+
|
|
3
7
|
Upload a completed test report from a developer laptop, test machine, or CI
|
|
4
8
|
job. Run these commands in the project containing your automated tests:
|
|
5
9
|
|
|
6
|
-
1.
|
|
10
|
+
1. Install the latest CLI in the project. To pin a release instead, replace
|
|
11
|
+
`@latest` with an exact version such as `@0.2.3`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install --save-dev testdossier@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. One-time setup — creates `.env.testdossier` with owner-only permissions and
|
|
7
18
|
adds it to `.gitignore`:
|
|
8
19
|
|
|
9
20
|
```bash
|
|
10
21
|
npx testdossier init
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
|
|
24
|
+
3. Paste the CI-ingestion token shown once by TestDossier into
|
|
14
25
|
`.env.testdossier`, replacing the placeholder, then prove the wiring in
|
|
15
26
|
seconds instead of after a full CI round-trip:
|
|
16
27
|
|
|
@@ -22,14 +33,14 @@ npx testdossier verify
|
|
|
22
33
|
token is valid, has the CI-ingestion capability, and reaches the right
|
|
23
34
|
project — and prints that project's name.
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
4. Run your tests so they create a supported report. For example, with
|
|
26
37
|
Playwright's JSON reporter configured to write `playwright-results.json`:
|
|
27
38
|
|
|
28
39
|
```bash
|
|
29
40
|
npx playwright test
|
|
30
41
|
```
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
5. Upload the completed report:
|
|
33
44
|
|
|
34
45
|
```bash
|
|
35
46
|
npx testdossier upload playwright-results.json
|
|
@@ -79,18 +90,18 @@ delete the file to change it.
|
|
|
79
90
|
Before sending anything, inspect what the CLI detected:
|
|
80
91
|
|
|
81
92
|
```bash
|
|
82
|
-
npx
|
|
93
|
+
npx testdossier upload playwright-results.json --dry-run
|
|
83
94
|
```
|
|
84
95
|
|
|
85
96
|
## Supported reports
|
|
86
97
|
|
|
87
98
|
| Runner or format | Example |
|
|
88
99
|
|---|---|
|
|
89
|
-
| Playwright JSON | `npx
|
|
90
|
-
| Cypress JSON | `npx
|
|
91
|
-
| Cucumber JSON | `npx
|
|
92
|
-
| JUnit XML | `npx
|
|
93
|
-
| Generic JSON | `npx
|
|
100
|
+
| Playwright JSON | `npx testdossier upload playwright-results.json` |
|
|
101
|
+
| Cypress JSON | `npx testdossier upload cypress-results.json` |
|
|
102
|
+
| Cucumber JSON | `npx testdossier upload cucumber-results.json` |
|
|
103
|
+
| JUnit XML | `npx testdossier upload junit-results.xml` |
|
|
104
|
+
| Generic JSON | `npx testdossier upload testdossier-ci.json` |
|
|
94
105
|
|
|
95
106
|
Detection uses the report's contents, not its filename. If a custom reporter
|
|
96
107
|
creates an ambiguous shape, pass `--format playwright`, `--format cypress`,
|
|
@@ -103,10 +114,14 @@ For repeat use, install and pin the CLI in the test repository so its source
|
|
|
103
114
|
and version are captured by the lockfile:
|
|
104
115
|
|
|
105
116
|
```bash
|
|
106
|
-
npm install --save-dev testdossier
|
|
117
|
+
npm install --save-dev testdossier@latest
|
|
107
118
|
npx testdossier init
|
|
108
119
|
```
|
|
109
120
|
|
|
121
|
+
Use `npm install --save-dev testdossier@0.2.3` when the repository should pin
|
|
122
|
+
that exact release. In either case, subsequent commands remain
|
|
123
|
+
`npx testdossier ...`; the version selector belongs only to installation.
|
|
124
|
+
|
|
110
125
|
`init` is idempotent: it never overwrites an existing `.env.testdossier` and
|
|
111
126
|
never duplicates the `.gitignore` entry. If you prefer manual setup, create
|
|
112
127
|
`.env.testdossier` yourself with the single line
|
|
@@ -134,7 +149,8 @@ Useful metadata options:
|
|
|
134
149
|
npx testdossier upload junit-results.xml \
|
|
135
150
|
--run-id "local-2026-07-23-1" \
|
|
136
151
|
--build "abc123" \
|
|
137
|
-
--branch "main"
|
|
152
|
+
--branch "main" \
|
|
153
|
+
--pipeline-id "github:acme/storefront:e2e"
|
|
138
154
|
```
|
|
139
155
|
|
|
140
156
|
`--run-id` is the idempotency key. Repeating the same upload with the same run
|
|
@@ -145,18 +161,30 @@ page identity includes the report's file name and a path fingerprint). Without
|
|
|
145
161
|
a run ID, the CLI creates a new local one. Transient network, rate-limit,
|
|
146
162
|
conflict, and server errors are retried with the same run ID.
|
|
147
163
|
|
|
164
|
+
`--pipeline-id` identifies the stable repository/workflow/job that produced the
|
|
165
|
+
run. It is inferred on supported CI providers and can also be set with
|
|
166
|
+
`TD_PIPELINE_ID`. Keep it stable across access-token rotation: Insights uses it
|
|
167
|
+
to let the latest completed result replace an earlier result from the same
|
|
168
|
+
pipeline without allowing another repository's result to be overwritten.
|
|
169
|
+
|
|
148
170
|
Run `npx testdossier --help` for every option.
|
|
149
171
|
|
|
150
172
|
## Using the same flow in CI
|
|
151
173
|
|
|
152
174
|
Commit `testdossier.json` and set `TD_CI_TOKEN` in the provider's encrypted
|
|
153
|
-
secret storage; the pipeline step is
|
|
175
|
+
secret storage; after the dependency install, the pipeline step is
|
|
176
|
+
`npx testdossier` with
|
|
154
177
|
no arguments. Run identity, build, branch, and provider metadata are inferred
|
|
155
178
|
automatically on GitHub Actions, GitLab CI, Jenkins, Azure DevOps, Bitbucket
|
|
156
179
|
Pipelines, and CircleCI. The Access Tokens dialog still generates
|
|
157
180
|
provider-specific snippets that pass the report path explicitly — either style
|
|
158
181
|
works; the explicit one fails louder when the report artifact is missing.
|
|
159
182
|
|
|
183
|
+
Use `@latest` only when adding/updating the dependency. The lockfile then keeps
|
|
184
|
+
CI reproducible, and normal invocations use the installed command without a
|
|
185
|
+
version selector. Update or pin the dependency through your normal dependency
|
|
186
|
+
update process.
|
|
187
|
+
|
|
160
188
|
If `.env.testdossier` is ever committed or shared, revoke that token in
|
|
161
189
|
TestDossier immediately and create a replacement. Hosted CI should use the
|
|
162
190
|
provider's encrypted secret storage instead of an env file.
|
package/assets/mark.svg
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="TestDossier">
|
|
2
|
+
<title>TestDossier</title>
|
|
3
|
+
<path fill="#006b61" fill-rule="evenodd" d="M20 6h14c17 0 28 10 28 26S51 58 34 58H20V6Zm14 14v24c9 0 14-4 14-12s-5-12-14-12Z"/>
|
|
4
|
+
<path fill="#006b61" d="M2 6h32v10H2z"/>
|
|
5
|
+
</svg>
|
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.3";
|
|
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) {
|
|
@@ -755,7 +800,8 @@ async function postWithRetry({ endpoint, headers, body, env, fetchImpl, stderr,
|
|
|
755
800
|
}
|
|
756
801
|
|
|
757
802
|
function helpText() {
|
|
758
|
-
return `TestDossier
|
|
803
|
+
return `TD TestDossier
|
|
804
|
+
CI report uploader · ${VERSION}
|
|
759
805
|
|
|
760
806
|
Usage:
|
|
761
807
|
testdossier init One-time setup: create ${ENV_FILE_NAME} and gitignore it
|
|
@@ -771,6 +817,7 @@ Options:
|
|
|
771
817
|
--build <value> Commit, build number, or release identifier
|
|
772
818
|
--branch <value> Branch name
|
|
773
819
|
--provider <value> Provider label (defaults to local or detected CI)
|
|
820
|
+
--pipeline-id <id> Stable repository/workflow/job identity (normally inferred)
|
|
774
821
|
--ci-url <url> Link to the originating CI run
|
|
775
822
|
--env-file <path> Read TD_CI_TOKEN from this explicit dotenv file
|
|
776
823
|
--dry-run Validate and detect only; send nothing
|
|
@@ -958,6 +1005,7 @@ export async function runCli(
|
|
|
958
1005
|
"User-Agent": `testdossier-cli/${VERSION}`,
|
|
959
1006
|
"X-CI-Run-Id": metadata.runId,
|
|
960
1007
|
"X-CI-Provider": metadata.provider,
|
|
1008
|
+
"X-CI-Pipeline-Id": metadata.pipelineId,
|
|
961
1009
|
};
|
|
962
1010
|
if (format !== "junit" && format !== "generic") baseHeaders["X-CI-Format"] = format;
|
|
963
1011
|
if (metadata.build) baseHeaders["X-Build"] = metadata.build;
|
|
@@ -983,6 +1031,9 @@ export async function runCli(
|
|
|
983
1031
|
headers: {
|
|
984
1032
|
...baseHeaders,
|
|
985
1033
|
"X-CI-Page-Id": pages.length === 1 ? pageBase : `${pageBase}-${index + 1}`,
|
|
1034
|
+
"X-CI-Upload-Id": pageBase,
|
|
1035
|
+
"X-CI-Page-Index": String(index + 1),
|
|
1036
|
+
"X-CI-Page-Count": String(pages.length),
|
|
986
1037
|
},
|
|
987
1038
|
body: page.body,
|
|
988
1039
|
env,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdossier",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Upload
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Upload Playwright, Cypress, Cucumber, JUnit, and JSON test evidence to TestDossier.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"lib",
|
|
13
|
+
"assets",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
".env.testdossier.example"
|
|
@@ -18,7 +19,13 @@
|
|
|
18
19
|
"node": ">=18"
|
|
19
20
|
},
|
|
20
21
|
"homepage": "https://testdossier.com",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/devkalu/test-dossier.git",
|
|
25
|
+
"directory": "cli"
|
|
26
|
+
},
|
|
21
27
|
"bugs": {
|
|
28
|
+
"url": "https://github.com/devkalu/test-dossier/issues",
|
|
22
29
|
"email": "support@testdossier.com"
|
|
23
30
|
},
|
|
24
31
|
"keywords": [
|
|
@@ -27,7 +34,11 @@
|
|
|
27
34
|
"playwright",
|
|
28
35
|
"cypress",
|
|
29
36
|
"cucumber",
|
|
30
|
-
"testdossier"
|
|
37
|
+
"testdossier",
|
|
38
|
+
"test-management",
|
|
39
|
+
"test-evidence",
|
|
40
|
+
"continuous-integration",
|
|
41
|
+
"qa"
|
|
31
42
|
],
|
|
32
43
|
"publishConfig": {
|
|
33
44
|
"access": "public"
|