@glubean/cli 0.8.0 → 0.8.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.
Files changed (42) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +24 -0
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/commands/load.d.ts +11 -0
  5. package/dist/commands/load.d.ts.map +1 -1
  6. package/dist/commands/load.js +192 -0
  7. package/dist/commands/load.js.map +1 -1
  8. package/dist/commands/login.d.ts +14 -1
  9. package/dist/commands/login.d.ts.map +1 -1
  10. package/dist/commands/login.js +110 -49
  11. package/dist/commands/login.js.map +1 -1
  12. package/dist/commands/run.d.ts +7 -0
  13. package/dist/commands/run.d.ts.map +1 -1
  14. package/dist/commands/run.js +202 -93
  15. package/dist/commands/run.js.map +1 -1
  16. package/dist/lib/auth.d.ts +57 -0
  17. package/dist/lib/auth.d.ts.map +1 -1
  18. package/dist/lib/auth.js +134 -1
  19. package/dist/lib/auth.js.map +1 -1
  20. package/dist/lib/config.d.ts +15 -5
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/config.js +6 -1
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/constants.d.ts +6 -1
  25. package/dist/lib/constants.d.ts.map +1 -1
  26. package/dist/lib/constants.js +6 -1
  27. package/dist/lib/constants.js.map +1 -1
  28. package/dist/lib/print-plan.d.ts.map +1 -1
  29. package/dist/lib/print-plan.js +4 -0
  30. package/dist/lib/print-plan.js.map +1 -1
  31. package/dist/lib/upload.d.ts +88 -10
  32. package/dist/lib/upload.d.ts.map +1 -1
  33. package/dist/lib/upload.js +117 -188
  34. package/dist/lib/upload.js.map +1 -1
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +47 -8
  37. package/dist/main.js.map +1 -1
  38. package/package.json +6 -6
  39. package/dist/lib/env.d.ts +0 -29
  40. package/dist/lib/env.d.ts.map +0 -1
  41. package/dist/lib/env.js +0 -59
  42. package/dist/lib/env.js.map +0 -1
@@ -1,8 +1,16 @@
1
1
  /**
2
- * glubean login — Authenticate with Glubean Cloud.
2
+ * glubean login — Sign the CLI in to Glubean Cloud via the device-authorization
3
+ * grant (RFC 8628). The CLI requests a code from the AUTH plane (server-hono),
4
+ * opens the browser to approve it, polls until a `glb_` token is minted, and
5
+ * saves it to ~/.glubean/credentials.json for `--upload`.
6
+ *
7
+ * Login talks to the AUTH url (server-hono); uploads talk to the platform API
8
+ * (`GLUBEAN_API_URL`) — two separate planes. The open platform is token-only and
9
+ * has no login of its own.
3
10
  */
4
- import { input, password } from "@inquirer/prompts";
5
- import { resolveApiUrl, writeCredentials } from "../lib/auth.js";
11
+ import { execFile } from "node:child_process";
12
+ import { resolveApiUrl, resolveAuthUrl, writeCredentials, } from "../lib/auth.js";
13
+ import { DEFAULT_API_URL, DEFAULT_AUTH_URL } from "../lib/constants.js";
6
14
  const colors = {
7
15
  reset: "\x1b[0m",
8
16
  green: "\x1b[32m",
@@ -10,66 +18,119 @@ const colors = {
10
18
  dim: "\x1b[2m",
11
19
  bold: "\x1b[1m",
12
20
  yellow: "\x1b[33m",
21
+ cyan: "\x1b[36m",
13
22
  };
23
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
24
+ /** Best-effort: open a URL in the default browser. Failures are ignored — the
25
+ * CLI always prints the URL so the user can open it manually. */
26
+ function openBrowser(url) {
27
+ try {
28
+ if (process.platform === "darwin") {
29
+ execFile("open", [url]);
30
+ }
31
+ else if (process.platform === "win32") {
32
+ execFile("cmd", ["/c", "start", "", url]);
33
+ }
34
+ else {
35
+ execFile("xdg-open", [url]);
36
+ }
37
+ }
38
+ catch {
39
+ // ignored — the URL is printed regardless
40
+ }
41
+ }
42
+ async function persist(token, options, apiUrl, authUrl, defaultProject) {
43
+ // --project wins; otherwise persist the project the grant bound the token to
44
+ // (the org's default), so `glubean run --upload` works without --project.
45
+ const projectId = options.project ?? defaultProject;
46
+ const savedPath = await writeCredentials({
47
+ token,
48
+ projectId,
49
+ apiUrl: apiUrl !== DEFAULT_API_URL ? apiUrl : undefined,
50
+ authUrl: authUrl !== DEFAULT_AUTH_URL ? authUrl : undefined,
51
+ });
52
+ console.log(`${colors.green}Logged in${colors.reset} ${colors.dim}→ credentials saved to ${savedPath}${colors.reset}`);
53
+ if (projectId) {
54
+ console.log(`${colors.dim}Default project: ${projectId}${colors.reset}`);
55
+ console.log(`\n${colors.dim}Run tests and upload: glubean run --upload${colors.reset}`);
56
+ }
57
+ else {
58
+ console.log(`\n${colors.dim}Run tests and upload: glubean run --upload --project <id> (or set GLUBEAN_PROJECT_ID).${colors.reset}`);
59
+ }
60
+ }
14
61
  export async function loginCommand(options) {
62
+ const authUrl = (await resolveAuthUrl(options)).replace(/\/+$/, "");
15
63
  const apiUrl = await resolveApiUrl(options);
16
- let token = options.token;
17
- if (!token) {
18
- const appUrl = apiUrl.replace("api.", "app.");
19
- console.log(`${colors.bold}Create a personal access token:${colors.reset}`);
20
- console.log(` ${colors.dim}${appUrl}/settings/tokens${colors.reset}`);
21
- console.log(` ${colors.dim}This token grants access to all your projects.${colors.reset}`);
22
- console.log(` ${colors.dim}For per-project tokens, use project settings → API keys.${colors.reset}`);
23
- console.log();
24
- token = await password({
25
- message: "Paste your token (gb_...)",
26
- mask: "*",
27
- });
64
+ // Non-interactive escape hatch: persist a token the user already created in the
65
+ // dashboard (Project → Tokens). The upload preflight validates it later.
66
+ if (options.token) {
67
+ await persist(options.token, options, apiUrl, authUrl);
68
+ return;
28
69
  }
29
- if (!token) {
30
- console.error(`${colors.red}Error: No token provided.${colors.reset}`);
31
- process.exit(1);
32
- }
33
- // Validate token via whoami
34
- console.log(`${colors.dim}Validating...${colors.reset}`);
70
+ // 1) Start the device grant.
71
+ let grant;
35
72
  try {
36
- const resp = await fetch(`${apiUrl}/open/v1/whoami`, {
37
- headers: { Authorization: `Bearer ${token}` },
73
+ const resp = await fetch(`${authUrl}/cli/device/code`, {
74
+ method: "POST",
75
+ headers: { "content-type": "application/json" },
38
76
  });
39
77
  if (!resp.ok) {
40
- const body = await resp.text();
41
- console.error(`${colors.red}Authentication failed (${resp.status}): ${body}${colors.reset}`);
78
+ console.error(`${colors.red}Could not start login (${resp.status}) at ${authUrl}.${colors.reset}`);
79
+ console.error(`${colors.dim}Check --auth-url / GLUBEAN_AUTH_URL points at the Glubean auth server.${colors.reset}`);
42
80
  process.exit(1);
43
81
  }
44
- const whoami = await resp.json();
45
- const identity = whoami.kind === "user"
46
- ? `user ${whoami.userId}`
47
- : `project ${whoami.projectName ?? whoami.projectId}`;
48
- console.log(`${colors.green}Authenticated as ${identity}${colors.reset}`);
82
+ grant = (await resp.json());
49
83
  }
50
84
  catch (err) {
51
- console.error(`${colors.red}Failed to reach ${apiUrl}: ${err instanceof Error ? err.message : err}${colors.reset}`);
85
+ console.error(`${colors.red}Cannot reach ${authUrl}: ${err instanceof Error ? err.message : err}${colors.reset}`);
52
86
  process.exit(1);
53
87
  }
54
- // Resolve project ID: flag interactive prompt
55
- let projectId = options.project;
56
- if (!projectId) {
57
- projectId = await input({
58
- message: "Project ID (optional, from project settings)",
59
- default: "",
60
- });
61
- if (projectId === "")
62
- projectId = undefined;
88
+ // 2) Show the code + open the browser.
89
+ const openUrl = grant.verification_uri_complete ?? grant.verification_uri;
90
+ console.log(`\n${colors.bold}Authorize the Glubean CLI${colors.reset}`);
91
+ console.log(` ${colors.dim}Open:${colors.reset} ${colors.cyan}${grant.verification_uri}${colors.reset}`);
92
+ console.log(` ${colors.dim}Code:${colors.reset} ${colors.bold}${grant.user_code}${colors.reset}\n`);
93
+ if (options.noBrowser) {
94
+ console.log(`${colors.dim}Open the URL above and enter the code.${colors.reset}`);
63
95
  }
64
- const savedPath = await writeCredentials({
65
- token,
66
- projectId,
67
- apiUrl: apiUrl !== "https://api.glubean.com" ? apiUrl : undefined,
68
- });
69
- console.log(`${colors.green}Credentials saved${colors.reset} ${colors.dim}→ ${savedPath}${colors.reset}`);
70
- if (projectId) {
71
- console.log(`${colors.dim}Default project: ${projectId}${colors.reset}`);
96
+ else {
97
+ console.log(`${colors.dim}Opening your browser…${colors.reset}`);
98
+ openBrowser(openUrl);
99
+ }
100
+ // 3) Poll until approved / denied / expired (or the grant times out).
101
+ const intervalMs = Math.max(1, grant.interval) * 1000;
102
+ const deadline = Date.now() + Math.max(1, grant.expires_in) * 1000;
103
+ console.log(`${colors.dim}Waiting for approval…${colors.reset}`);
104
+ while (Date.now() < deadline) {
105
+ await sleep(intervalMs);
106
+ let poll;
107
+ try {
108
+ const resp = await fetch(`${authUrl}/cli/device/token`, {
109
+ method: "POST",
110
+ headers: { "content-type": "application/json" },
111
+ body: JSON.stringify({ device_code: grant.device_code }),
112
+ });
113
+ if (!resp.ok)
114
+ continue; // transient — keep polling
115
+ poll = (await resp.json());
116
+ }
117
+ catch {
118
+ continue; // transient network blip — keep polling
119
+ }
120
+ if (poll.status === "approved" && poll.token) {
121
+ await persist(poll.token, options, apiUrl, authUrl, poll.projectId);
122
+ return;
123
+ }
124
+ if (poll.status === "denied") {
125
+ console.error(`${colors.red}Login denied in the browser.${colors.reset}`);
126
+ process.exit(1);
127
+ }
128
+ if (poll.status === "expired") {
129
+ console.error(`${colors.red}The login code expired. Run 'glubean login' again.${colors.reset}`);
130
+ process.exit(1);
131
+ }
72
132
  }
73
- console.log(`\n${colors.dim}Run tests and upload: glubean run --upload${colors.reset}`);
133
+ console.error(`${colors.red}Timed out waiting for approval. Run 'glubean login' again.${colors.reset}`);
134
+ process.exit(1);
74
135
  }
75
136
  //# sourceMappingURL=login.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
@@ -45,6 +45,13 @@ interface RunOptions {
45
45
  upload?: boolean;
46
46
  uploadReceiptJson?: string;
47
47
  project?: string;
48
+ /**
49
+ * Upload TARGET (the API/system under test the runs belong to). Resolved from
50
+ * the profile's `upload.targetId` (or `GLUBEAN_TARGET_ID`). When unset, the
51
+ * upload routes to the project's default target (resolved server-side at
52
+ * upload time). Runs live under a target — see auth.ts `resolveTargetId`.
53
+ */
54
+ target?: string;
48
55
  token?: string;
49
56
  /** Env var name holding this profile's upload token (from upload.tokenEnv). */
50
57
  tokenEnv?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,12 @@
1
1
  import { bootstrap, evaluateThresholds, MetricCollector, ProjectRunner, buildRunContext, } from "@glubean/runner";
2
2
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { stat, readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
4
5
  import { glob } from "node:fs/promises";
5
6
  import { CONFIG_DEFAULTS, mergeRunOptions, toSharedRunConfig } from "../lib/config.js";
6
7
  import { loadProjectEnv } from "@glubean/runner";
7
8
  import { resolveEnvFileName } from "../lib/active_env.js";
8
9
  import { shouldSkipTest } from "../lib/skip.js";
9
- import { CLI_VERSION } from "../version.js";
10
- import { redactMetadataForUpload } from "../lib/redact-metadata.js";
11
10
  import { extractContractCases, extractFromSource } from "@glubean/scanner/static";
12
11
  import { buildSuffixes, classifyByStem, extractContractFromFile, findTemplateMatch, GLUBEAN_KINDS, loadProjectOverlays, matchesTemplateFilter, } from "@glubean/scanner";
13
12
  import { applyEnvTemplating } from "@glubean/runner";
@@ -610,11 +609,15 @@ export async function runCommand(target, options = {}) {
610
609
  console.log(`${colors.dim}Loaded ${Object.keys(envVars).length} vars from ${envFileName}${colors.reset}`);
611
610
  }
612
611
  // ── Preflight: verify auth before running tests when --upload is set ────
612
+ // The resolved upload target is hoisted so the post-run upload reuses it —
613
+ // resolution happens here (pre-run) so a misconfigured destination fails fast.
614
+ let resolvedUploadTargetId;
613
615
  if (options.upload) {
614
- const { resolveToken, resolveProjectId, resolveApiUrl } = await import("../lib/auth.js");
616
+ const { resolveToken, resolveProjectId, resolveApiUrl, resolveTargetId, resolveDefaultTargetId, checkUploadAuth, checkTargetInProject, } = await import("../lib/auth.js");
615
617
  const authOpts = {
616
618
  token: options.token,
617
619
  project: options.project,
620
+ target: options.target,
618
621
  apiUrl: options.apiUrl,
619
622
  };
620
623
  const sources = {
@@ -630,34 +633,87 @@ export async function runCommand(target, options = {}) {
630
633
  console.error(`${colors.dim}This profile's upload.tokenEnv points at '${options.tokenEnv}', but it's empty/unset. Set it in .env.secrets or the environment.${colors.reset}`);
631
634
  }
632
635
  else {
633
- console.error(`${colors.dim}Run 'glubean login', set GLUBEAN_TOKEN, or add token to .env.secrets or package.json glubean.cloud.${colors.reset}`);
636
+ console.error(`${colors.dim}Create a project token (glb_…) in the dashboard (Project → Tokens), then run 'glubean login' to save it, set GLUBEAN_TOKEN / --token, or add it to .env.secrets.${colors.reset}`);
634
637
  }
635
638
  process.exit(1);
636
639
  }
637
640
  if (!preProject) {
638
641
  console.error(`${colors.red}Error: --upload requires a project ID but none found.${colors.reset}`);
639
- console.error(`${colors.dim}Use --project, set projectId in package.json glubean.cloud, or run 'glubean login'.${colors.reset}`);
642
+ console.error(`${colors.dim}Use --project or set GLUBEAN_PROJECT_ID.${colors.reset}`);
640
643
  process.exit(1);
641
644
  }
642
- try {
643
- const resp = await fetch(`${preApiUrl}/open/v1/whoami`, {
644
- headers: { Authorization: `Bearer ${preToken}` },
645
- });
646
- if (!resp.ok) {
647
- console.error(`${colors.red}Error: authentication failed (${resp.status}).${colors.reset}`);
648
- if (resp.status === 401) {
649
- console.error(`${colors.dim}Token is invalid or expired. Run 'glubean login' to re-authenticate.${colors.reset}`);
645
+ // Validate against the SAME server runs upload to. Don't pre-judge token
646
+ // format locally let the server decide. A least-privilege ingest token
647
+ // (runs:write, no projects:read) gets 403 yet can still POST runs, so that
648
+ // alone proceeds; a known-bad config (401 invalid token, 404 mistyped
649
+ // project / wrong API URL, 5xx, unreachable) is fatal BEFORE running tests.
650
+ const check = await checkUploadAuth(preApiUrl, preProject, preToken);
651
+ if (!check.proceed) {
652
+ if (check.status === 401) {
653
+ console.error(`${colors.red}Error: authentication failed (401).${colors.reset}`);
654
+ console.error(`${colors.dim}The token is invalid/expired or not a platform project token (glb_…). Create one in the dashboard (Project → Tokens) and run 'glubean login' (or set GLUBEAN_TOKEN).${colors.reset}`);
655
+ }
656
+ else if (check.status === 404) {
657
+ console.error(`${colors.red}Error: project ${preProject} not found (404).${colors.reset}`);
658
+ console.error(`${colors.dim}Check that --project / GLUBEAN_PROJECT_ID is a real project id and --api-url / GLUBEAN_API_URL points at the right server.${colors.reset}`);
659
+ }
660
+ else if (check.status === 403) {
661
+ console.error(`${colors.red}Error: access to project ${preProject} is forbidden (403).${colors.reset}`);
662
+ console.error(`${colors.dim}The token's org has no access to this project (or its membership was revoked). Use a token whose org owns the project.${colors.reset}`);
663
+ }
664
+ else if (check.status === 0) {
665
+ console.error(`${colors.red}Error: cannot reach server at ${preApiUrl}.${colors.reset}`);
666
+ }
667
+ else {
668
+ console.error(`${colors.red}Error: upload preflight got an unexpected response (${check.status}).${colors.reset}`);
669
+ console.error(`${colors.dim}Check that --api-url / GLUBEAN_API_URL points at the Glubean platform API.${colors.reset}`);
670
+ }
671
+ process.exit(1);
672
+ }
673
+ if (check.unverified) {
674
+ // 403 — can't read the project with this token's scope, but it can write
675
+ // runs. Proceed; the post-run upload surfaces any genuine error.
676
+ console.log(`${colors.dim}Skipping pre-run project check (insufficient read scope); will upload to ${preApiUrl} after the run.${colors.reset}`);
677
+ }
678
+ else {
679
+ console.log(`${colors.dim}Authenticated · upload to ${preApiUrl} (project ${check.projectName ?? preProject})${colors.reset}`);
680
+ }
681
+ // Resolve the upload TARGET here too (pre-run) so a misconfigured target
682
+ // fails fast instead of after the whole suite. The post-run block reuses it.
683
+ let preTarget = await resolveTargetId(authOpts, sources);
684
+ if (preTarget) {
685
+ // EXPLICIT target — validate it belongs to the project (a typo would
686
+ // otherwise 404 only on the final POST, after the suite ran).
687
+ const tcheck = await checkTargetInProject(preApiUrl, preProject, preTarget, preToken);
688
+ if (!tcheck.proceed) {
689
+ if (tcheck.status === 404) {
690
+ console.error(`${colors.red}Error: target ${preTarget} not found in project ${preProject} (404).${colors.reset}`);
691
+ console.error(`${colors.dim}Check upload.targetId / GLUBEAN_TARGET_ID / --upload-target.${colors.reset}`);
692
+ }
693
+ else if (tcheck.status === 401) {
694
+ console.error(`${colors.red}Error: authentication failed validating the target (401).${colors.reset}`);
695
+ }
696
+ else if (tcheck.status === 0) {
697
+ console.error(`${colors.red}Error: cannot reach server at ${preApiUrl}.${colors.reset}`);
698
+ }
699
+ else {
700
+ console.error(`${colors.red}Error: could not validate target ${preTarget} (${tcheck.status}).${colors.reset}`);
650
701
  }
651
702
  process.exit(1);
652
703
  }
653
- const identity = await resp.json();
654
- console.log(`${colors.dim}Authenticated as ${identity.kind === "project_token" ? `project token (${identity.projectName})` : "user"} · upload to ${preApiUrl}${colors.reset}`);
704
+ // 403 insufficient_scope (unverified) → no targets:read; can't validate, proceed.
655
705
  }
656
- catch (err) {
657
- console.error(`${colors.red}Error: cannot reach server at ${preApiUrl}${colors.reset}`);
658
- console.error(`${colors.dim}${err.message}${colors.reset}`);
659
- process.exit(1);
706
+ else {
707
+ // No explicit target the project's default target (deterministic for a
708
+ // default project, else slug-validated by listing).
709
+ preTarget = await resolveDefaultTargetId(preApiUrl, preProject, preToken);
710
+ if (!preTarget) {
711
+ console.error(`${colors.red}Error: could not resolve an upload target for project ${preProject}.${colors.reset}`);
712
+ console.error(`${colors.dim}Set the target explicitly (upload.targetId in glubean.yaml, GLUBEAN_TARGET_ID, or --upload-target). Auto-resolving a non-default project's target needs a token with the targets:read scope.${colors.reset}`);
713
+ process.exit(1);
714
+ }
660
715
  }
716
+ resolvedUploadTargetId = preTarget;
661
717
  }
662
718
  // ── Bootstrap plugins BEFORE discovery ─────────────────────────────────
663
719
  // CLI's `discoverTests` dynamically imports each .contract.ts / .test.ts
@@ -1894,6 +1950,7 @@ export async function runCommand(target, options = {}) {
1894
1950
  const authOpts = {
1895
1951
  token: options.token,
1896
1952
  project: options.project,
1953
+ target: options.target,
1897
1954
  apiUrl: options.apiUrl,
1898
1955
  };
1899
1956
  const sources = {
@@ -1912,7 +1969,7 @@ export async function runCommand(target, options = {}) {
1912
1969
  process.exit(1);
1913
1970
  }
1914
1971
  else {
1915
- const { compileScopes, redactEvent, BUILTIN_SCOPES } = await import("@glubean/redaction");
1972
+ const { compileScopes, redactEvent, redactValue, BUILTIN_SCOPES } = await import("@glubean/redaction");
1916
1973
  // Prefer the v1 plan's full redaction config when supplied
1917
1974
  // (Phase 4 init scaffolds `defaults.redaction` in glubean.yaml,
1918
1975
  // including any custom globalRules / sensitiveKeys / customPatterns).
@@ -1925,82 +1982,134 @@ export async function runCommand(target, options = {}) {
1925
1982
  globalRules: effectiveRedaction.globalRules,
1926
1983
  replacementFormat: effectiveRedaction.replacementFormat,
1927
1984
  });
1928
- // Generate metadata for test registry
1929
- let metadata;
1930
- try {
1931
- const { scan } = await import("@glubean/scanner");
1932
- const { buildMetadata } = await import("../metadata.js");
1933
- const scanResult = await scan(rootDir);
1934
- const built = await buildMetadata(scanResult, {
1935
- generatedBy: `@glubean/cli@${CLI_VERSION}`,
1936
- projectId,
1937
- // Upload path only: carry the lossless full CONTRACT projection for
1938
- // the Cloud c/f metadata snapshot. Deep-redacted below before upload;
1939
- // never written to the on-disk metadata.json (that path omits it).
1940
- // `workflows` is always present (Design Y) and redacted in the same
1941
- // pass below.
1942
- includeProjection: true,
1943
- });
1944
- metadata = built;
1945
- }
1946
- catch {
1947
- // Non-critical: upload results without metadata
1985
+ // The upload TARGET (the API/system under test runs belong to — ADR 0007)
1986
+ // was resolved + validated in the preflight (pre-run, so a misconfigured
1987
+ // destination fails fast); reuse it. The guard is defensive — the preflight
1988
+ // exits on a null target, so this can't normally fire.
1989
+ const targetId = resolvedUploadTargetId;
1990
+ if (!targetId) {
1991
+ console.error(`${colors.red}Upload failed: no upload target resolved.${colors.reset}`);
1992
+ process.exit(1);
1948
1993
  }
1949
- // Phase 5 5a — attach run-plan provenance to the upload metadata
1950
- // bucket. Cloud server projects this to top-level RunEntity fields
1951
- // (see apps/server/src/tasks/helpers/extract-run-plan.ts). Nested
1952
- // under `metadata` to clear the server DTO's `forbidNonWhitelisted`
1953
- // top-level gate. Only emitted when:
1954
- // 1. The run used a profile (no profile nothing to record).
1955
- // 2. The scan path produced metadata.
1956
- // Skipping runPlan in the degraded-scan path is intentional —
1957
- // synthesizing a runPlan-only shell with `files: {}` would make
1958
- // the server's upsertTests treat all active tests as "removed"
1959
- // (authoritative file map = empty). Better to lose runPlan
1960
- // provenance on degraded scans than to corrupt the test registry.
1961
- if (metadata && options.profile) {
1962
- const runPlan = {
1963
- profile: options.profile,
1994
+ else {
1995
+ // ── Result blob: the full ExecutionResult, run-data ONLY (per D7 the
1996
+ // contract/workflow projection is a separate c/f line). Events are
1997
+ // scope-redacted; the rest of the payload can ALSO carry secrets, so
1998
+ // scrub it too: `context.command` is raw argv (e.g. `--token glb_…`,
1999
+ // `--input-json '{"password":…}'`)dropped outright; `customMetadata`
2000
+ // is user-supplied → deep-redacted. Without this the blob would store
2001
+ // those verbatim in Cloud.
2002
+ const { command: _rawCommand, ...safeContext } = runContext ?? {};
2003
+ const redactNonEvent = (v) => redactValue(v, {
2004
+ globalRules: effectiveRedaction.globalRules,
2005
+ replacementFormat: effectiveRedaction.replacementFormat,
2006
+ maxDepth: 64,
2007
+ });
2008
+ const redactedResult = {
2009
+ ...resultPayload,
2010
+ context: redactNonEvent(safeContext),
2011
+ ...(resultPayload.customMetadata
2012
+ ? { customMetadata: redactNonEvent(resultPayload.customMetadata) }
2013
+ : {}),
2014
+ tests: resultPayload.tests.map((t) => ({
2015
+ ...t,
2016
+ events: t.events.map((e) => redactEvent(e, compiledScopes, effectiveRedaction.replacementFormat)),
2017
+ })),
1964
2018
  };
1965
- if (options.suites && options.suites.length > 0) {
1966
- runPlan.suites = options.suites;
2019
+ // ── Analytics substrate. Server derive-on-ingest (plan D2) isn't built
2020
+ // yet, so the CLI sends per-test rows + metric points explicitly.
2021
+ const testResults = collectedRuns.map((r) => ({
2022
+ testId: r.testId,
2023
+ name: r.testName,
2024
+ // Mirror the CLI's own pass/fail/skip classification: a clean skip
2025
+ // (success:true + a status:"skipped" event) → "skipped" (excluded from
2026
+ // flaky denominators, plan D3). A test that FAILED then emitted skip is
2027
+ // counted as failed (success:false), so gate skip on success first —
2028
+ // otherwise the failure would wrongly drop out of the denominators.
2029
+ status: r.success
2030
+ ? r.events.some((e) => e.type === "status" && e.status === "skipped")
2031
+ ? "skipped"
2032
+ : "passed"
2033
+ : "failed",
2034
+ durationMs: r.durationMs,
2035
+ ...(r.tags && r.tags.length ? { tags: r.tags } : {}),
2036
+ eventCount: r.events.length,
2037
+ }));
2038
+ // Metric tags (method/path) can in rare cases embed a secret in a path
2039
+ // segment — redact them with the same engine the projection line uses.
2040
+ const redactTags = (tags) => tags
2041
+ ? redactValue(tags, {
2042
+ globalRules: effectiveRedaction.globalRules,
2043
+ replacementFormat: effectiveRedaction.replacementFormat,
2044
+ })
2045
+ : undefined;
2046
+ const metrics = [];
2047
+ for (const r of collectedRuns) {
2048
+ for (const e of r.events) {
2049
+ if (e.type !== "metric")
2050
+ continue;
2051
+ // Skip valueless metric events: the server requires a finite numeric
2052
+ // value, and one bad point must not reject the whole run's upload.
2053
+ if (!Number.isFinite(e.value))
2054
+ continue;
2055
+ metrics.push({
2056
+ name: e.name,
2057
+ value: e.value,
2058
+ ...(e.unit ? { unit: e.unit } : {}),
2059
+ ...(e.tags ? { tags: redactTags(e.tags) } : {}),
2060
+ testId: r.testId,
2061
+ });
2062
+ }
2063
+ }
2064
+ const input = {
2065
+ kind: "test",
2066
+ schemaVersion: "glubean.test.v1",
2067
+ // Stable idempotency id for this run — reused across the upload retry so
2068
+ // a lost-response retry replaces this run instead of duplicating it (P1).
2069
+ clientRunId: randomUUID(),
2070
+ // A breached metric threshold fails the run (mirrors the process exit
2071
+ // below) even when every test passed — don't record it as "passed".
2072
+ status: failed > 0 || (thresholdSummary && !thresholdSummary.pass) ? "failed" : "passed",
2073
+ startedAt: runStartTime,
2074
+ completedAt: new Date(Date.parse(runStartTime) + totalDurationMs).toISOString(),
2075
+ durationMs: totalDurationMs,
2076
+ summary: {
2077
+ total: passed + failed + skipped,
2078
+ passed,
2079
+ failed,
2080
+ skipped,
2081
+ durationMs: totalDurationMs,
2082
+ // Run-plan provenance (was metadata.runPlan). The summary jsonb keeps
2083
+ // extras (SUMMARY_SCHEMA catchall), so profile/suite facets survive
2084
+ // for grouping even though the new run row has no dedicated columns.
2085
+ ...(options.profile ? { profile: options.profile } : {}),
2086
+ ...(options.suites && options.suites.length ? { suites: options.suites } : {}),
2087
+ },
2088
+ result: redactedResult,
2089
+ ...(testResults.length ? { testResults } : {}),
2090
+ ...(metrics.length ? { metrics } : {}),
2091
+ };
2092
+ const uploadReceipt = await uploadToCloud(input, {
2093
+ apiUrl,
2094
+ token,
2095
+ projectId,
2096
+ targetId,
2097
+ envFile: effectiveRun.envFile,
2098
+ rootDir,
2099
+ });
2100
+ if (options.uploadReceiptJson) {
2101
+ const receiptPath = resolveOutputPath(options.uploadReceiptJson, process.cwd());
2102
+ await mkdir(dirname(receiptPath), { recursive: true });
2103
+ await writeFile(receiptPath, JSON.stringify(uploadReceipt, null, 2) + "\n", "utf-8");
2104
+ console.log(`${colors.dim}Upload receipt written to: ${receiptPath}${colors.reset}`);
2105
+ }
2106
+ // A requested --upload that didn't create a run is a failure, even on a
2107
+ // green test run — exit non-zero so CI doesn't read false-green. (The
2108
+ // receipt is written above first, so the failure is still recorded.)
2109
+ if (uploadReceipt.resultUpload.status === "failed") {
2110
+ console.error(`${colors.red}Upload failed: the run was not recorded in Cloud (see the error above).${colors.reset}`);
2111
+ process.exit(1);
1967
2112
  }
1968
- metadata = { ...metadata, runPlan };
1969
- }
1970
- // Deep-redact the FULL contract + workflow projection before upload. Test
1971
- // events are redacted below via scope-based `redactEvent`, but the
1972
- // projection is a free-form tree that can carry secrets anywhere
1973
- // (examples, default headers, gRPC metadata, `extensions`/`meta`, literal
1974
- // compare/switch values, assertion messages). `redactMetadataForUpload`
1975
- // redacts ONLY the projection buckets (contractsProjection + workflows) —
1976
- // never `files`/`rootHash` — so the server's test registry/dedup keeps
1977
- // its verbatim sha256 hashes. The projection is uploaded WHOLE (branch/
1978
- // poll included): it is the lossless source for the server snapshot, not
1979
- // a run view (see the buildMetadata R14 note); the branch/poll run-view
1980
- // gate is a separate layer, untouched here.
1981
- if (metadata) {
1982
- metadata = await redactMetadataForUpload(metadata, effectiveRedaction);
1983
- }
1984
- const redactedPayload = {
1985
- ...resultPayload,
1986
- metadata,
1987
- tests: resultPayload.tests.map((t) => ({
1988
- ...t,
1989
- events: t.events.map((e) => redactEvent(e, compiledScopes, effectiveRedaction.replacementFormat)),
1990
- })),
1991
- };
1992
- const uploadReceipt = await uploadToCloud(redactedPayload, {
1993
- apiUrl,
1994
- token,
1995
- projectId,
1996
- envFile: effectiveRun.envFile,
1997
- rootDir,
1998
- });
1999
- if (options.uploadReceiptJson) {
2000
- const receiptPath = resolveOutputPath(options.uploadReceiptJson, process.cwd());
2001
- await mkdir(dirname(receiptPath), { recursive: true });
2002
- await writeFile(receiptPath, JSON.stringify(uploadReceipt, null, 2) + "\n", "utf-8");
2003
- console.log(`${colors.dim}Upload receipt written to: ${receiptPath}${colors.reset}`);
2004
2113
  }
2005
2114
  }
2006
2115
  }