@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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +24 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/load.d.ts +11 -0
- package/dist/commands/load.d.ts.map +1 -1
- package/dist/commands/load.js +192 -0
- package/dist/commands/load.js.map +1 -1
- package/dist/commands/login.d.ts +14 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +110 -49
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/run.d.ts +7 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +202 -93
- package/dist/commands/run.js.map +1 -1
- package/dist/lib/auth.d.ts +57 -0
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +134 -1
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config.d.ts +15 -5
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +6 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +6 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +6 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/print-plan.d.ts.map +1 -1
- package/dist/lib/print-plan.js +4 -0
- package/dist/lib/print-plan.js.map +1 -1
- package/dist/lib/upload.d.ts +88 -10
- package/dist/lib/upload.d.ts.map +1 -1
- package/dist/lib/upload.js +117 -188
- package/dist/lib/upload.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +47 -8
- package/dist/main.js.map +1 -1
- package/package.json +6 -6
- package/dist/lib/env.d.ts +0 -29
- package/dist/lib/env.d.ts.map +0 -1
- package/dist/lib/env.js +0 -59
- package/dist/lib/env.js.map +0 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* glubean login —
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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(`${
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
console.error(`${colors.
|
|
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
|
-
|
|
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}
|
|
85
|
+
console.error(`${colors.red}Cannot reach ${authUrl}: ${err instanceof Error ? err.message : err}${colors.reset}`);
|
|
52
86
|
process.exit(1);
|
|
53
87
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAuBxD,UAAU,UAAU;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC;IACvB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC;IACrE;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,oBAAoB,EAAE,eAAe,CAAC;IAC/D;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,cAAc,EAAE,eAAe,CAAC;CACrD;AAuCD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBnD;AA+ED;;;;;;;;;;;GAWG;AACH,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAGjF;AA+DD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,EAAE,CAAC,CA+BnB;AAED,UAAU,kBAAkB;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;CACrC;AAED,UAAU,cAAc;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,kBAAkB,CAAC;CAC1B;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAoM/E;AAUD,eAAO,MAAM,SAAS;;;8BAIM,MAAM;CACjC,CAAC;AAEF,iBAAS,WAAW,CAClB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,IAAI,GAAG,KAAY,GACxB,OAAO,CAKT;AAED;;;;GAIG;AACH,iBAAS,kBAAkB,CACzB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAKT;AA6DD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EACzB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAo3Df"}
|
package/dist/commands/run.js
CHANGED
|
@@ -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}
|
|
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
|
|
642
|
+
console.error(`${colors.dim}Use --project or set GLUBEAN_PROJECT_ID.${colors.reset}`);
|
|
640
643
|
process.exit(1);
|
|
641
644
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
//
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
-
|
|
1966
|
-
|
|
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
|
}
|