testdossier 0.1.0 → 0.1.1

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.
@@ -0,0 +1,3 @@
1
+ # Copy this file to .env.testdossier, replace the placeholder, and keep the
2
+ # resulting file out of git.
3
+ TD_CI_TOKEN=replace_with_your_td_token
package/README.md CHANGED
@@ -2,9 +2,18 @@
2
2
 
3
3
  Upload a completed test report from a developer laptop, test machine, or CI job:
4
4
 
5
+ Create `.env.testdossier` in the project containing the test report:
6
+
7
+ ```dotenv
8
+ TD_CI_TOKEN=replace_with_your_td_token
9
+ ```
10
+
11
+ Replace the placeholder with the CI-ingestion token shown once by TestDossier,
12
+ then:
13
+
5
14
  ```bash
6
- export TD_CI_TOKEN="td_..."
7
- npx testdossier upload playwright-results.json
15
+ chmod 600 .env.testdossier
16
+ npx testdossier upload playwright-results.json --env-file .env.testdossier
8
17
  ```
9
18
 
10
19
  The CLI auto-detects Playwright JSON, Cypress JSON, Cucumber JSON, JUnit XML,
@@ -17,10 +26,12 @@ The uploader does one thing: read a completed report and make an outbound HTTPS
17
26
  request to TestDossier. It does not run tests, accept remote commands, install a
18
27
  background service, watch files, or keep a process running.
19
28
 
20
- The access token is read only from `TD_CI_TOKEN`. There is deliberately no
21
- `--token` option, so a token cannot accidentally land in shell history or a
22
- process listing. Redirects are rejected, and non-HTTPS destinations are rejected
23
- except for localhost development.
29
+ The access token is read from an explicitly selected env file or from the
30
+ `TD_CI_TOKEN` process environment. There is deliberately no `--token` option,
31
+ so a token cannot accidentally land in shell history or a process listing.
32
+ The CLI reads only `TD_CI_TOKEN` from the selected file; it does not import or
33
+ execute other entries. Redirects are rejected, and non-HTTPS destinations are
34
+ rejected except for localhost development.
24
35
 
25
36
  Before sending anything, inspect what the CLI detected:
26
37
 
@@ -45,31 +56,47 @@ creates an ambiguous shape, pass `--format playwright`, `--format cypress`,
45
56
  ## Local setup
46
57
 
47
58
  Create a project access token in TestDossier with the **CI ingestion**
48
- capability, copy it once, and place it in an environment variable:
59
+ capability and copy it once. For repeat use, install and pin the CLI in the test
60
+ repository so its source and version are captured by the lockfile:
61
+
62
+ ```bash
63
+ npm install --save-dev testdossier
64
+ cp node_modules/testdossier/.env.testdossier.example .env.testdossier
65
+ ```
66
+
67
+ Alternatively, create `.env.testdossier` yourself with the single placeholder
68
+ line shown at the top of this guide. Replace `replace_with_your_td_token`, then
69
+ ensure the project ignores and protects the real file:
70
+
71
+ ```gitignore
72
+ .env.testdossier
73
+ ```
49
74
 
50
75
  ```bash
51
- export TD_CI_TOKEN="td_..."
76
+ chmod 600 .env.testdossier
52
77
  ```
53
78
 
54
- For repeat use, install and pin the CLI in the test repository so its source and
55
- version are captured by the lockfile:
79
+ Upload with:
56
80
 
57
81
  ```bash
58
- npm install --save-dev testdossier
59
- npx testdossier upload playwright-results.json
82
+ npx testdossier upload playwright-results.json --env-file .env.testdossier
60
83
  ```
61
84
 
85
+ For a one-off session, setting `TD_CI_TOKEN` in the process environment still
86
+ works. An explicitly passed `--env-file` takes precedence.
87
+
62
88
  The default destination is `https://testdossier.com`. A self-hosted or local
63
89
  instance can be selected without putting the token on the command line:
64
90
 
65
91
  ```bash
66
- TD_URL="https://dossier.example.com" npx testdossier upload junit-results.xml
92
+ TD_URL="https://dossier.example.com" npx testdossier upload junit-results.xml --env-file .env.testdossier
67
93
  ```
68
94
 
69
95
  Useful metadata options:
70
96
 
71
97
  ```bash
72
98
  npx testdossier upload junit-results.xml \
99
+ --env-file .env.testdossier \
73
100
  --run-id "local-2026-07-23-1" \
74
101
  --build "abc123" \
75
102
  --branch "main"
@@ -81,3 +108,7 @@ one, the CLI creates a new local run ID. Transient network, rate-limit, conflict
81
108
  and server errors are retried with the same run ID.
82
109
 
83
110
  Run `npx testdossier --help` for every option.
111
+
112
+ If `.env.testdossier` is ever committed or shared, revoke that token in
113
+ TestDossier immediately and create a replacement. Hosted CI should use the
114
+ provider's encrypted secret storage instead of an env file.
@@ -1,7 +1,7 @@
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.0";
4
+ export const VERSION = "0.1.1";
5
5
  const DEFAULT_ORIGIN = "https://testdossier.com";
6
6
  const MAX_REPORT_BYTES = 25 * 1024 * 1024;
7
7
  const RETRYABLE_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
@@ -130,6 +130,7 @@ export function parseUploadArgs(args) {
130
130
  branch: undefined,
131
131
  provider: undefined,
132
132
  ciUrl: undefined,
133
+ envFile: undefined,
133
134
  dryRun: false,
134
135
  reportPath: undefined,
135
136
  };
@@ -141,6 +142,7 @@ export function parseUploadArgs(args) {
141
142
  ["--branch", "branch"],
142
143
  ["--provider", "provider"],
143
144
  ["--ci-url", "ciUrl"],
145
+ ["--env-file", "envFile"],
144
146
  ]);
145
147
 
146
148
  for (let i = 0; i < args.length; i += 1) {
@@ -229,6 +231,41 @@ function integerEnv(value, fallback, min, max) {
229
231
  return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
230
232
  }
231
233
 
234
+ export function parseTokenEnvFile(contents) {
235
+ let token;
236
+ const lines = String(contents).replace(/^\uFEFF/, "").split(/\r?\n/);
237
+ for (let index = 0; index < lines.length; index += 1) {
238
+ const line = lines[index].trim();
239
+ if (!line || line.startsWith("#")) continue;
240
+ const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
241
+ if (!match || match[1] !== "TD_CI_TOKEN") continue;
242
+ if (token !== undefined) throw new CliError("env file contains TD_CI_TOKEN more than once.");
243
+
244
+ let value = match[2].trim();
245
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
246
+ value = value.slice(1, -1);
247
+ } else {
248
+ value = value.replace(/\s+#.*$/, "").trim();
249
+ }
250
+ if (!value) throw new CliError(`TD_CI_TOKEN is empty in the env file (line ${index + 1}).`);
251
+ token = value;
252
+ }
253
+ if (token === undefined) throw new CliError("env file does not define TD_CI_TOKEN.");
254
+ return token;
255
+ }
256
+
257
+ async function tokenFromEnvFile(path, stderr) {
258
+ const fileStats = await stat(path).catch((error) => {
259
+ throw new CliError(`could not read env file: ${error.message}`);
260
+ });
261
+ if (!fileStats.isFile()) throw new CliError("env-file path must point to a file.");
262
+ if (fileStats.size > 64 * 1024) throw new CliError("env file is unexpectedly large (maximum 64 KiB).");
263
+ if (process.platform !== "win32" && (fileStats.mode & 0o077) !== 0) {
264
+ stderr(`Warning: ${path} is readable by other users; run chmod 600 "${path}".`);
265
+ }
266
+ return parseTokenEnvFile(await readFile(path, "utf8"));
267
+ }
268
+
232
269
  function retryDelayMs(attempt, retryAfter, baseDelay) {
233
270
  if (retryAfter) {
234
271
  const seconds = Number(retryAfter);
@@ -303,18 +340,19 @@ Options:
303
340
  --branch <value> Branch name
304
341
  --provider <value> Provider label (defaults to local or detected CI)
305
342
  --ci-url <url> Link to the originating CI run
343
+ --env-file <path> Read TD_CI_TOKEN from this explicit dotenv file
306
344
  --dry-run Validate and detect only; send nothing
307
345
  -h, --help Show help
308
346
  --version Show version
309
347
 
310
348
  Authentication:
311
- Set TD_CI_TOKEN=td_... in the environment. Tokens are intentionally not
312
- accepted as command-line arguments, keeping them out of shell history and
313
- process listings.
349
+ Set TD_CI_TOKEN in the environment or pass --env-file .env.testdossier.
350
+ Tokens are intentionally not accepted as command-line arguments, keeping
351
+ them out of shell history and process listings.
314
352
 
315
353
  Examples:
316
- TD_CI_TOKEN=td_... npx testdossier upload playwright-results.json
317
- TD_CI_TOKEN=td_... npx testdossier upload junit-results.xml --build abc123
354
+ npx testdossier upload playwright-results.json --env-file .env.testdossier
355
+ npx testdossier upload junit-results.xml --env-file .env.testdossier --build abc123
318
356
  npx testdossier upload testdossier-ci.json --dry-run`;
319
357
  }
320
358
 
@@ -381,7 +419,9 @@ export async function runCli(
381
419
  return 0;
382
420
  }
383
421
 
384
- const token = env.TD_CI_TOKEN;
422
+ const token = options.envFile
423
+ ? await tokenFromEnvFile(options.envFile, stderr)
424
+ : env.TD_CI_TOKEN;
385
425
  if (!token || !token.startsWith("td_")) {
386
426
  throw new CliError("TD_CI_TOKEN is required and must be a TestDossier td_ access token.");
387
427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdossier",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Upload completed test reports to TestDossier from a local machine or CI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
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"