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 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. One-time setup creates `.env.testdossier` with owner-only permissions and
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
- 2. Paste the CI-ingestion token shown once by TestDossier into
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
- 3. Run your tests so they create a supported report. For example, with
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
- 4. Upload the completed report:
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 --yes testdossier@0.2.1 upload playwright-results.json --dry-run
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 --yes testdossier@0.2.1 upload playwright-results.json` |
90
- | Cypress JSON | `npx --yes testdossier@0.2.1 upload cypress-results.json` |
91
- | Cucumber JSON | `npx --yes testdossier@0.2.1 upload cucumber-results.json` |
92
- | JUnit XML | `npx --yes testdossier@0.2.1 upload junit-results.xml` |
93
- | Generic JSON | `npx --yes testdossier@0.2.1 upload testdossier-ci.json` |
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 then `npx --yes testdossier@0.2.1` with
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.
@@ -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>
@@ -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.1";
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
- return { runId, build, branch, provider, ciUrl };
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 report uploader ${VERSION}
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.1",
4
- "description": "Upload completed test reports to TestDossier from a local machine or CI.",
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"