testdossier 0.2.0 → 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 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. One-time setup creates `.env.testdossier` with owner-only permissions and
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
- 2. Paste the CI-ingestion token shown once by TestDossier into
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
- 3. Run your tests so they create a supported report. For example, with
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
- 4. Upload the completed report:
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 --yes testdossier@0.2.0 upload playwright-results.json --dry-run
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 --yes testdossier@0.2.0 upload playwright-results.json` |
90
- | Cypress JSON | `npx --yes testdossier@0.2.0 upload cypress-results.json` |
91
- | Cucumber JSON | `npx --yes testdossier@0.2.0 upload cucumber-results.json` |
92
- | JUnit XML | `npx --yes testdossier@0.2.0 upload junit-results.xml` |
93
- | Generic JSON | `npx --yes testdossier@0.2.0 upload testdossier-ci.json` |
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 then `npx --yes testdossier@0.2.0` with
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.
@@ -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.0";
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";
@@ -326,8 +326,8 @@ export function normalizeEndpoint(input = DEFAULT_ORIGIN) {
326
326
  const path = url.pathname.replace(/\/+$/, "");
327
327
  if (!path) {
328
328
  url.pathname = "/api/ci/ingest";
329
- } else if (path !== "/api/ci/ingest") {
330
- throw new CliError("TestDossier URL must be an origin or end with /api/ci/ingest.");
329
+ } else if (path !== "/api/ci/ingest" && path !== "/api/v1/ci/ingest") {
330
+ throw new CliError("TestDossier URL must be an origin or end with /api/ci/ingest (or its /api/v1 alias).");
331
331
  }
332
332
  return url.toString();
333
333
  }
@@ -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) {
@@ -766,11 +811,12 @@ Usage:
766
811
 
767
812
  Options:
768
813
  --format <format> auto (default), cypress, playwright, cucumber, junit, generic
769
- --url <url> TestDossier origin or /api/ci/ingest endpoint
814
+ --url <url> TestDossier origin or ingest endpoint (/api/ci/ingest or /api/v1/ci/ingest)
770
815
  --run-id <id> Stable idempotency key for this execution
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
@@ -886,11 +932,19 @@ export async function runCli(
886
932
  stderr,
887
933
  action: "verify",
888
934
  });
889
- const name = result && typeof result.project_name === "string" && result.project_name
935
+ // A 200 alone is not proof: a mis-pointed URL can hit an HTML page,
936
+ // which the response parser degrades to {}. Only the ingest handler's
937
+ // explicit verify marker + project identity count as success.
938
+ if (!result || result.verify !== true || !result.project_id) {
939
+ throw new CliError(
940
+ `verify got a successful response from ${new URL(endpoint).origin} that is not the TestDossier ingest API — check the --url / TD_URL destination.`,
941
+ 1,
942
+ );
943
+ }
944
+ const name = typeof result.project_name === "string" && result.project_name
890
945
  ? ` "${result.project_name}"`
891
946
  : "";
892
- const id = result && result.project_id ? ` (${result.project_id})` : "";
893
- stdout(`Token verified for project${name}${id} at ${new URL(endpoint).origin} — CI ingestion enabled.`);
947
+ stdout(`Token verified for project${name} (${result.project_id}) at ${new URL(endpoint).origin} CI ingestion enabled.`);
894
948
  return 0;
895
949
  }
896
950
 
@@ -950,6 +1004,7 @@ export async function runCli(
950
1004
  "User-Agent": `testdossier-cli/${VERSION}`,
951
1005
  "X-CI-Run-Id": metadata.runId,
952
1006
  "X-CI-Provider": metadata.provider,
1007
+ "X-CI-Pipeline-Id": metadata.pipelineId,
953
1008
  };
954
1009
  if (format !== "junit" && format !== "generic") baseHeaders["X-CI-Format"] = format;
955
1010
  if (metadata.build) baseHeaders["X-Build"] = metadata.build;
@@ -975,6 +1030,9 @@ export async function runCli(
975
1030
  headers: {
976
1031
  ...baseHeaders,
977
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),
978
1036
  },
979
1037
  body: page.body,
980
1038
  env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdossier",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Upload completed test reports to TestDossier from a local machine or CI.",
5
5
  "license": "MIT",
6
6
  "type": "module",