@plune-ai/cli 0.30.2 → 0.32.0
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 +71 -10
- package/dist/cli.cjs +279 -175
- package/dist/index.cjs +90 -66
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +90 -66
- package/dist/reporter-core.cjs +31 -4
- package/dist/reporter-core.d.cts +13 -0
- package/dist/reporter-core.d.ts +13 -0
- package/dist/reporter-core.js +31 -4
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_retry = __esm({
|
|
|
512
512
|
"use strict";
|
|
513
513
|
init_cjs_shims();
|
|
514
514
|
init_errors2();
|
|
515
|
-
defaultSleep = (ms) => new Promise((
|
|
515
|
+
defaultSleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -2458,7 +2458,7 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
2458
2458
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
2459
2459
|
}
|
|
2460
2460
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2461
|
-
return new Promise((
|
|
2461
|
+
return new Promise((resolve11, reject) => {
|
|
2462
2462
|
const target = new URL(url);
|
|
2463
2463
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2464
2464
|
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
@@ -2469,7 +2469,7 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2469
2469
|
res.on("end", () => {
|
|
2470
2470
|
const status = res.statusCode ?? 0;
|
|
2471
2471
|
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2472
|
-
|
|
2472
|
+
resolve11({
|
|
2473
2473
|
ok: status >= 200 && status <= 299,
|
|
2474
2474
|
status,
|
|
2475
2475
|
headers: {
|
|
@@ -2488,12 +2488,12 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2488
2488
|
});
|
|
2489
2489
|
}
|
|
2490
2490
|
function nodePut(url, headers, source, idleMs) {
|
|
2491
|
-
return new Promise((
|
|
2491
|
+
return new Promise((resolve11, reject) => {
|
|
2492
2492
|
const target = new URL(url);
|
|
2493
2493
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2494
2494
|
const length = "body" in source ? source.body.length : source.size;
|
|
2495
2495
|
let answered;
|
|
2496
|
-
const settle = (err) => answered !== void 0 ?
|
|
2496
|
+
const settle = (err) => answered !== void 0 ? resolve11(answered) : reject(err);
|
|
2497
2497
|
const req = send(
|
|
2498
2498
|
target,
|
|
2499
2499
|
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
@@ -2502,7 +2502,7 @@ function nodePut(url, headers, source, idleMs) {
|
|
|
2502
2502
|
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
2503
2503
|
res.on("error", settle);
|
|
2504
2504
|
res.on("close", () => {
|
|
2505
|
-
|
|
2505
|
+
resolve11(answered);
|
|
2506
2506
|
req.destroy();
|
|
2507
2507
|
});
|
|
2508
2508
|
res.resume();
|
|
@@ -2565,7 +2565,7 @@ function createClient(opts) {
|
|
|
2565
2565
|
}
|
|
2566
2566
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2567
2567
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2568
|
-
async function send(method,
|
|
2568
|
+
async function send(method, path19, payload, retries = DEFAULT_RETRIES) {
|
|
2569
2569
|
let last = {
|
|
2570
2570
|
kind: "unavailable",
|
|
2571
2571
|
status: null,
|
|
@@ -2576,7 +2576,7 @@ function createClient(opts) {
|
|
|
2576
2576
|
await respectPause(mine);
|
|
2577
2577
|
let res;
|
|
2578
2578
|
try {
|
|
2579
|
-
res = await doFetch(`${base}${
|
|
2579
|
+
res = await doFetch(`${base}${path19}`, {
|
|
2580
2580
|
method,
|
|
2581
2581
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2582
2582
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2615,10 +2615,10 @@ function createClient(opts) {
|
|
|
2615
2615
|
return { ok: false, ...last };
|
|
2616
2616
|
}
|
|
2617
2617
|
return {
|
|
2618
|
-
post: (
|
|
2619
|
-
upload: (
|
|
2620
|
-
del: (
|
|
2621
|
-
get: (
|
|
2618
|
+
post: (path19, body, retries) => send("POST", path19, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2619
|
+
upload: (path19, bytes, contentType) => send("POST", path19, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2620
|
+
del: (path19) => send("DELETE", path19),
|
|
2621
|
+
get: (path19) => send("GET", path19),
|
|
2622
2622
|
async put(url, headers, source) {
|
|
2623
2623
|
const putRetries = uploadRetries ?? 3;
|
|
2624
2624
|
let last = { ok: false, status: null, code: "network" };
|
|
@@ -2688,6 +2688,25 @@ function shardOf(env) {
|
|
|
2688
2688
|
error: `PLUNE_SHARD=${JSON.stringify(raw.slice(0, 40))} is not "k/N" with 1 <= k <= N <= ${SHARDS_MAX} (for example 2/4) - nothing was sent.`
|
|
2689
2689
|
};
|
|
2690
2690
|
}
|
|
2691
|
+
function configurationOf(env) {
|
|
2692
|
+
const raw = value(env, "PLUNE_CONFIGURATION");
|
|
2693
|
+
if (raw === void 0) return {};
|
|
2694
|
+
const pairs = [];
|
|
2695
|
+
for (const pair of raw.split(";").map((s) => s.trim()).filter((s) => s !== "")) {
|
|
2696
|
+
const at = pair.indexOf("=");
|
|
2697
|
+
const name = at < 0 ? "" : pair.slice(0, at).trim();
|
|
2698
|
+
const label = at < 0 ? "" : pair.slice(at + 1).trim();
|
|
2699
|
+
const twice = pairs.find(([seen]) => seen.toLowerCase() === name.toLowerCase());
|
|
2700
|
+
const why = at < 0 ? 'has no "="' : name === "" ? "has no group name" : label === "" ? "has no value" : twice !== void 0 ? `names the group ${JSON.stringify(twice[0])} a second time` : void 0;
|
|
2701
|
+
if (why !== void 0) {
|
|
2702
|
+
return {
|
|
2703
|
+
error: `PLUNE_CONFIGURATION: ${JSON.stringify(pair.slice(0, 40))} ${why} - write it as "Group=Value;Group=Value" (for example "Browser=Chrome;Operating system=Windows") - nothing was sent.`
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
pairs.push([name, label]);
|
|
2707
|
+
}
|
|
2708
|
+
return pairs.length === 0 ? {} : { configuration: Object.fromEntries(pairs) };
|
|
2709
|
+
}
|
|
2691
2710
|
function value(env, name) {
|
|
2692
2711
|
const raw = env[name]?.trim();
|
|
2693
2712
|
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
@@ -2732,6 +2751,7 @@ function readEnv(env = process.env) {
|
|
|
2732
2751
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2733
2752
|
const full = flag(env, "PLUNE_FULL_RUN");
|
|
2734
2753
|
const { shard, error: shardError } = shardOf(env);
|
|
2754
|
+
const { configuration, error: configurationError } = configurationOf(env);
|
|
2735
2755
|
return {
|
|
2736
2756
|
config: {
|
|
2737
2757
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -2744,6 +2764,7 @@ function readEnv(env = process.env) {
|
|
|
2744
2764
|
...title !== void 0 ? { title } : {},
|
|
2745
2765
|
...environment !== void 0 ? { environment } : {},
|
|
2746
2766
|
...labels !== void 0 ? { labels } : {},
|
|
2767
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
2747
2768
|
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
2748
2769
|
// variable is not an instruction.
|
|
2749
2770
|
...offerDiscovered ? { offerDiscovered } : {},
|
|
@@ -2757,6 +2778,7 @@ function readEnv(env = process.env) {
|
|
|
2757
2778
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
|
|
2758
2779
|
...shard !== void 0 ? { shard } : {},
|
|
2759
2780
|
...shardError !== void 0 ? { shardError } : {},
|
|
2781
|
+
...configurationError !== void 0 ? { configurationError } : {},
|
|
2760
2782
|
...importTimeout !== void 0 ? { importTimeout } : {},
|
|
2761
2783
|
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
2762
2784
|
warnings
|
|
@@ -2813,6 +2835,7 @@ function appendBatch(file, batch) {
|
|
|
2813
2835
|
externalKey: batch.externalKey,
|
|
2814
2836
|
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
2815
2837
|
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
2838
|
+
...batch.configuration !== void 0 ? { configuration: batch.configuration } : {},
|
|
2816
2839
|
results: batch.results
|
|
2817
2840
|
};
|
|
2818
2841
|
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -2876,12 +2899,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2876
2899
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2877
2900
|
return text2;
|
|
2878
2901
|
}
|
|
2879
|
-
function pathIn(
|
|
2880
|
-
const parts =
|
|
2902
|
+
function pathIn(path19) {
|
|
2903
|
+
const parts = path19.split(/[\\/]+/);
|
|
2881
2904
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2882
2905
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2883
2906
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2884
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2907
|
+
const escaped = /^\/(?!\/)/.test(path19) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2885
2908
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2886
2909
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2887
2910
|
}
|
|
@@ -2927,12 +2950,12 @@ function placeOf(error, testFile) {
|
|
|
2927
2950
|
}
|
|
2928
2951
|
function fileOf(raw) {
|
|
2929
2952
|
if (!raw.startsWith("file://")) return raw;
|
|
2930
|
-
let
|
|
2953
|
+
let path19 = raw.slice("file://".length);
|
|
2931
2954
|
try {
|
|
2932
|
-
|
|
2955
|
+
path19 = decodeURIComponent(path19);
|
|
2933
2956
|
} catch {
|
|
2934
2957
|
}
|
|
2935
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2958
|
+
return /^\/[A-Za-z]:\//.test(path19) ? path19.slice(1) : path19;
|
|
2936
2959
|
}
|
|
2937
2960
|
function fromRoot(place, root) {
|
|
2938
2961
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -2972,14 +2995,14 @@ var init_failure_detail = __esm({
|
|
|
2972
2995
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2973
2996
|
FRAME_MAX = 8192;
|
|
2974
2997
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2975
|
-
normal = (
|
|
2998
|
+
normal = (path19) => import_node_path2.posix.normalize(path19.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2976
2999
|
}
|
|
2977
3000
|
});
|
|
2978
3001
|
|
|
2979
3002
|
// src/reporter-core/files-batch.ts
|
|
2980
|
-
async function sha256File(
|
|
3003
|
+
async function sha256File(path19) {
|
|
2981
3004
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
2982
|
-
for await (const part of (0, import_node_fs4.createReadStream)(
|
|
3005
|
+
for await (const part of (0, import_node_fs4.createReadStream)(path19)) hash.update(part);
|
|
2983
3006
|
return hash.digest("hex");
|
|
2984
3007
|
}
|
|
2985
3008
|
var import_node_crypto2, import_node_fs4, FILES_BATCH_MAX, DIRECT_BATCH_BYTES, BATCH_PIPELINES, sha256Bytes, BUCKET_CODES;
|
|
@@ -3102,8 +3125,10 @@ function warnIfDropped(sent, run, log) {
|
|
|
3102
3125
|
async function startRun(passed, declared2) {
|
|
3103
3126
|
const env = readEnv();
|
|
3104
3127
|
if (env.shardError !== void 0) throw new Error(env.shardError);
|
|
3128
|
+
if (env.configurationError !== void 0 && passed.configuration === void 0) throw new Error(env.configurationError);
|
|
3105
3129
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
3106
3130
|
const cfg = { ...env.config, ...stated };
|
|
3131
|
+
const configuration = cfg.configuration !== void 0 && Object.keys(cfg.configuration).length > 0 ? cfg.configuration : void 0;
|
|
3107
3132
|
const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
|
|
3108
3133
|
`));
|
|
3109
3134
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
@@ -3187,6 +3212,7 @@ async function startRun(passed, declared2) {
|
|
|
3187
3212
|
externalKey,
|
|
3188
3213
|
session: sessionMarker,
|
|
3189
3214
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
3215
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
3190
3216
|
results: results.map(onDisk)
|
|
3191
3217
|
});
|
|
3192
3218
|
stats.deferred += results.length;
|
|
@@ -3211,7 +3237,7 @@ async function startRun(passed, declared2) {
|
|
|
3211
3237
|
if (!offline) {
|
|
3212
3238
|
if (expected !== void 0) await resolveKeys(expected.flat());
|
|
3213
3239
|
const stated2 = expected?.slice(0, EXPECTED_MAX) ?? [];
|
|
3214
|
-
const
|
|
3240
|
+
const configuration2 = stated2.length === 0 ? void 0 : {
|
|
3215
3241
|
expected: stated2.flatMap((keys) => {
|
|
3216
3242
|
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
3217
3243
|
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
@@ -3235,7 +3261,7 @@ async function startRun(passed, declared2) {
|
|
|
3235
3261
|
kind: cfg.kind ?? "automated",
|
|
3236
3262
|
...externalKey !== null ? { externalKey } : {},
|
|
3237
3263
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
3238
|
-
...
|
|
3264
|
+
...configuration2 !== void 0 ? { configuration: configuration2 } : {},
|
|
3239
3265
|
// Named even when nobody named it — the list has to read without opening a row. Only what
|
|
3240
3266
|
// the caller DESCRIBED goes to `warnIfDropped` below: a title this side invented is not one
|
|
3241
3267
|
// a person will miss, and a deployment older than the field must not be nagged on every run.
|
|
@@ -3243,7 +3269,7 @@ async function startRun(passed, declared2) {
|
|
|
3243
3269
|
...described
|
|
3244
3270
|
};
|
|
3245
3271
|
let start = await client.post("/v1/runs", request);
|
|
3246
|
-
if (!start.ok && start.status === 413 &&
|
|
3272
|
+
if (!start.ok && start.status === 413 && configuration2 !== void 0) {
|
|
3247
3273
|
log(
|
|
3248
3274
|
`plune: the platform refused the run's list of expected tests as too large (${start.detail || start.kind}) \u2014 starting without it: the results still land, but this run counts no tests that did not run, and claims no full run.`
|
|
3249
3275
|
);
|
|
@@ -3403,7 +3429,7 @@ async function startRun(passed, declared2) {
|
|
|
3403
3429
|
let free = uploadsAtOnce;
|
|
3404
3430
|
const waiting = [];
|
|
3405
3431
|
async function gated(work) {
|
|
3406
|
-
if (free === 0) await new Promise((
|
|
3432
|
+
if (free === 0) await new Promise((resolve11) => waiting.push(resolve11));
|
|
3407
3433
|
else free -= 1;
|
|
3408
3434
|
try {
|
|
3409
3435
|
return await work();
|
|
@@ -3789,7 +3815,8 @@ async function startRun(passed, declared2) {
|
|
|
3789
3815
|
}
|
|
3790
3816
|
const out = await client.post(
|
|
3791
3817
|
`/v1/runs/${runId}/results`,
|
|
3792
|
-
|
|
3818
|
+
// The job's combination on every batch (plune-ai/plune#1251); a refusal of it is a 422 said below as it came.
|
|
3819
|
+
{ results: part, ...configuration !== void 0 ? { configuration } : {} }
|
|
3793
3820
|
);
|
|
3794
3821
|
const counts = out.ok ? out.body?.counts : void 0;
|
|
3795
3822
|
const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
|
|
@@ -4121,14 +4148,14 @@ async function handleRunExec(options) {
|
|
|
4121
4148
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
4122
4149
|
const run = await handleRunStart(options);
|
|
4123
4150
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
4124
|
-
const code = await new Promise((
|
|
4151
|
+
const code = await new Promise((resolve11) => {
|
|
4125
4152
|
const child = spawnImpl(command, args, {
|
|
4126
4153
|
stdio: "inherit",
|
|
4127
4154
|
shell: process.platform === "win32",
|
|
4128
4155
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
4129
4156
|
});
|
|
4130
|
-
child.on("close", (exitCode) =>
|
|
4131
|
-
child.on("error", () =>
|
|
4157
|
+
child.on("close", (exitCode) => resolve11(exitCode ?? 1));
|
|
4158
|
+
child.on("error", () => resolve11(1));
|
|
4132
4159
|
});
|
|
4133
4160
|
return handleRunFinish(run.id, options, code);
|
|
4134
4161
|
}
|
|
@@ -4162,7 +4189,10 @@ async function handleRunReport(options = {}) {
|
|
|
4162
4189
|
for (const line of group) {
|
|
4163
4190
|
const ready = line.results.filter((r) => !isPending(r));
|
|
4164
4191
|
if (line.runId !== null && ready.length > 0) {
|
|
4165
|
-
const out = await client.post(`/v1/runs/${line.runId}/results`, {
|
|
4192
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, {
|
|
4193
|
+
results: ready,
|
|
4194
|
+
...line.configuration !== void 0 ? { configuration: line.configuration } : {}
|
|
4195
|
+
});
|
|
4166
4196
|
if (out.ok) result.sent += ready.length;
|
|
4167
4197
|
else {
|
|
4168
4198
|
result.failed += ready.length;
|
|
@@ -4182,6 +4212,8 @@ async function handleRunReport(options = {}) {
|
|
|
4182
4212
|
...first.externalKey !== null ? { externalKey: first.externalKey } : {},
|
|
4183
4213
|
// Where the run came from, as the reporter recorded it (plune#927) — never this machine's own.
|
|
4184
4214
|
...first.meta !== void 0 ? { meta: first.meta } : {},
|
|
4215
|
+
// The combination the line recorded, or none (`{}`) — never this machine's `PLUNE_CONFIGURATION` (plune-ai/plune#1251).
|
|
4216
|
+
configuration: first.configuration ?? {},
|
|
4185
4217
|
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
4186
4218
|
log: write
|
|
4187
4219
|
});
|
|
@@ -4323,6 +4355,85 @@ var init_run_result_schema = __esm({
|
|
|
4323
4355
|
}
|
|
4324
4356
|
});
|
|
4325
4357
|
|
|
4358
|
+
// src/cli/commands/job-reporter.ts
|
|
4359
|
+
var JOB_TIMEOUT_MS, JOB_SUBJECT_NAME_MAX, JOB_REASON_MAX, JobReporter;
|
|
4360
|
+
var init_job_reporter = __esm({
|
|
4361
|
+
"src/cli/commands/job-reporter.ts"() {
|
|
4362
|
+
"use strict";
|
|
4363
|
+
init_cjs_shims();
|
|
4364
|
+
JOB_TIMEOUT_MS = 5e3;
|
|
4365
|
+
JOB_SUBJECT_NAME_MAX = 200;
|
|
4366
|
+
JOB_REASON_MAX = 2e3;
|
|
4367
|
+
JobReporter = class {
|
|
4368
|
+
constructor(doFetch, base, token, kind) {
|
|
4369
|
+
this.doFetch = doFetch;
|
|
4370
|
+
this.base = base;
|
|
4371
|
+
this.token = token;
|
|
4372
|
+
this.kind = kind;
|
|
4373
|
+
}
|
|
4374
|
+
doFetch;
|
|
4375
|
+
base;
|
|
4376
|
+
token;
|
|
4377
|
+
kind;
|
|
4378
|
+
id;
|
|
4379
|
+
/** The upload's rows, the job's `total`; `null` while there are none to count. */
|
|
4380
|
+
size = null;
|
|
4381
|
+
/** The first report the platform did not take, for the CLI to say once. */
|
|
4382
|
+
problem;
|
|
4383
|
+
async send(method, route, body) {
|
|
4384
|
+
try {
|
|
4385
|
+
const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
|
|
4386
|
+
method,
|
|
4387
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
4388
|
+
body: JSON.stringify(body),
|
|
4389
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
4390
|
+
});
|
|
4391
|
+
const json = await res.json().catch(() => void 0);
|
|
4392
|
+
if (res.ok) return json ?? {};
|
|
4393
|
+
if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
|
|
4394
|
+
this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
|
|
4395
|
+
} catch (err) {
|
|
4396
|
+
this.problem ??= err instanceof Error ? err.message : String(err);
|
|
4397
|
+
}
|
|
4398
|
+
return void 0;
|
|
4399
|
+
}
|
|
4400
|
+
async open(subjectName, rows) {
|
|
4401
|
+
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
4402
|
+
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
4403
|
+
const body = await this.send("POST", "", {
|
|
4404
|
+
kind: this.kind,
|
|
4405
|
+
...name !== "" ? { subjectName: name } : {},
|
|
4406
|
+
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
4407
|
+
});
|
|
4408
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* The upload is in: every row is on the platform. `subjectId` is what the job works on, learnt from the upload's
|
|
4412
|
+
* answer (the run an ingest landed in): the platform takes it while the job names none (#1279).
|
|
4413
|
+
*/
|
|
4414
|
+
async uploaded(at) {
|
|
4415
|
+
if (this.id === void 0) return;
|
|
4416
|
+
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
4417
|
+
done: this.size,
|
|
4418
|
+
total: this.size,
|
|
4419
|
+
...at.cursor !== void 0 ? { cursor: at.cursor } : {},
|
|
4420
|
+
...at.subjectId !== void 0 ? { subjectId: at.subjectId } : {}
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
async finish(outcome) {
|
|
4424
|
+
if (this.id === void 0) return;
|
|
4425
|
+
const id = this.id;
|
|
4426
|
+
this.id = void 0;
|
|
4427
|
+
await this.send(
|
|
4428
|
+
"PATCH",
|
|
4429
|
+
`/${encodeURIComponent(id)}/state`,
|
|
4430
|
+
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
};
|
|
4434
|
+
}
|
|
4435
|
+
});
|
|
4436
|
+
|
|
4326
4437
|
// src/cli/commands/sync.ts
|
|
4327
4438
|
var sync_exports = {};
|
|
4328
4439
|
__export(sync_exports, {
|
|
@@ -4375,7 +4486,7 @@ async function handleSync(deps = {}) {
|
|
|
4375
4486
|
const payload = JSON.stringify(run);
|
|
4376
4487
|
const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
|
|
4377
4488
|
const bearer = token;
|
|
4378
|
-
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
4489
|
+
const job = new JobReporter(doFetch, apiUrl, bearer, "sync");
|
|
4379
4490
|
await job.open(path9.basename(deps.file !== void 0 ? path9.resolve(cwd, deps.file) : cwd), run.summary.total);
|
|
4380
4491
|
try {
|
|
4381
4492
|
const result = await upload();
|
|
@@ -4409,7 +4520,7 @@ async function handleSync(deps = {}) {
|
|
|
4409
4520
|
const body = await res.json().catch(() => ({}));
|
|
4410
4521
|
const id = typeof body.id === "string" ? body.id : "";
|
|
4411
4522
|
const result = { id, url: `${endpoint}/${id}` };
|
|
4412
|
-
await job.uploaded(id || void 0);
|
|
4523
|
+
await job.uploaded({ cursor: id || void 0 });
|
|
4413
4524
|
if (deps.failOnFailures === true) {
|
|
4414
4525
|
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
4415
4526
|
}
|
|
@@ -4436,7 +4547,7 @@ function reportSyncFailure(err, write) {
|
|
|
4436
4547
|
}
|
|
4437
4548
|
return null;
|
|
4438
4549
|
}
|
|
4439
|
-
var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError
|
|
4550
|
+
var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4440
4551
|
var init_sync = __esm({
|
|
4441
4552
|
"src/cli/commands/sync.ts"() {
|
|
4442
4553
|
"use strict";
|
|
@@ -4448,6 +4559,7 @@ var init_sync = __esm({
|
|
|
4448
4559
|
init_api_url();
|
|
4449
4560
|
init_credentials();
|
|
4450
4561
|
init_gate();
|
|
4562
|
+
init_job_reporter();
|
|
4451
4563
|
NotLoggedInError = class extends Error {
|
|
4452
4564
|
constructor() {
|
|
4453
4565
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -4484,71 +4596,6 @@ var init_sync = __esm({
|
|
|
4484
4596
|
}
|
|
4485
4597
|
status;
|
|
4486
4598
|
};
|
|
4487
|
-
JOB_KIND = "sync";
|
|
4488
|
-
JOB_TIMEOUT_MS = 5e3;
|
|
4489
|
-
JOB_SUBJECT_NAME_MAX = 200;
|
|
4490
|
-
JOB_REASON_MAX = 2e3;
|
|
4491
|
-
JobReporter = class {
|
|
4492
|
-
constructor(doFetch, base, token) {
|
|
4493
|
-
this.doFetch = doFetch;
|
|
4494
|
-
this.base = base;
|
|
4495
|
-
this.token = token;
|
|
4496
|
-
}
|
|
4497
|
-
doFetch;
|
|
4498
|
-
base;
|
|
4499
|
-
token;
|
|
4500
|
-
id;
|
|
4501
|
-
/** The run's rows, the job's `total`; `null` while there are none to count. */
|
|
4502
|
-
size = null;
|
|
4503
|
-
/** The first report the platform did not take, for the CLI to say once. */
|
|
4504
|
-
problem;
|
|
4505
|
-
async send(method, route, body) {
|
|
4506
|
-
try {
|
|
4507
|
-
const res = await this.doFetch(`${this.base}/v1/jobs${route}`, {
|
|
4508
|
-
method,
|
|
4509
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
4510
|
-
body: JSON.stringify(body),
|
|
4511
|
-
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
4512
|
-
});
|
|
4513
|
-
const json = await res.json().catch(() => void 0);
|
|
4514
|
-
if (res.ok) return json ?? {};
|
|
4515
|
-
if (route === "" && (res.status === 404 || res.status === 405)) return void 0;
|
|
4516
|
-
this.problem ??= `HTTP ${res.status}${typeof json?.error === "string" ? ` \u2014 ${json.error}` : ""}`;
|
|
4517
|
-
} catch (err) {
|
|
4518
|
-
this.problem ??= err instanceof Error ? err.message : String(err);
|
|
4519
|
-
}
|
|
4520
|
-
return void 0;
|
|
4521
|
-
}
|
|
4522
|
-
async open(subjectName, rows) {
|
|
4523
|
-
this.size = Number.isInteger(rows) && rows > 0 ? rows : null;
|
|
4524
|
-
const name = subjectName.trim().slice(0, JOB_SUBJECT_NAME_MAX);
|
|
4525
|
-
const body = await this.send("POST", "", {
|
|
4526
|
-
kind: JOB_KIND,
|
|
4527
|
-
...name !== "" ? { subjectName: name } : {},
|
|
4528
|
-
...this.size !== null ? { progress: { done: 0, total: this.size } } : {}
|
|
4529
|
-
});
|
|
4530
|
-
if (typeof body?.id === "string") this.id = body.id;
|
|
4531
|
-
}
|
|
4532
|
-
/** The upload is in: every row of the run is on the platform. */
|
|
4533
|
-
async uploaded(cursor) {
|
|
4534
|
-
if (this.id === void 0) return;
|
|
4535
|
-
await this.send("PATCH", `/${encodeURIComponent(this.id)}/progress`, {
|
|
4536
|
-
done: this.size,
|
|
4537
|
-
total: this.size,
|
|
4538
|
-
...cursor !== void 0 ? { cursor } : {}
|
|
4539
|
-
});
|
|
4540
|
-
}
|
|
4541
|
-
async finish(outcome) {
|
|
4542
|
-
if (this.id === void 0) return;
|
|
4543
|
-
const id = this.id;
|
|
4544
|
-
this.id = void 0;
|
|
4545
|
-
await this.send(
|
|
4546
|
-
"PATCH",
|
|
4547
|
-
`/${encodeURIComponent(id)}/state`,
|
|
4548
|
-
outcome.ok ? { state: "completed" } : { state: "failed", reason: outcome.reason.slice(0, JOB_REASON_MAX) }
|
|
4549
|
-
);
|
|
4550
|
-
}
|
|
4551
|
-
};
|
|
4552
4599
|
}
|
|
4553
4600
|
});
|
|
4554
4601
|
|
|
@@ -4585,10 +4632,10 @@ function platformGet(deps) {
|
|
|
4585
4632
|
const token = (deps.loadToken ?? loadToken)();
|
|
4586
4633
|
if (token === null) throw new NotLoggedInError();
|
|
4587
4634
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4588
|
-
return async (
|
|
4635
|
+
return async (path19) => {
|
|
4589
4636
|
let res;
|
|
4590
4637
|
try {
|
|
4591
|
-
res = await doFetch(`${apiUrl}${
|
|
4638
|
+
res = await doFetch(`${apiUrl}${path19}`, { headers: { authorization: `Bearer ${token}` } });
|
|
4592
4639
|
} catch {
|
|
4593
4640
|
throw new SyncNetworkError(apiUrl);
|
|
4594
4641
|
}
|
|
@@ -4929,9 +4976,9 @@ var init_attachments = __esm({
|
|
|
4929
4976
|
});
|
|
4930
4977
|
|
|
4931
4978
|
// src/importers/allure.ts
|
|
4932
|
-
function isFolder(
|
|
4979
|
+
function isFolder(path19) {
|
|
4933
4980
|
try {
|
|
4934
|
-
return fs11.statSync(
|
|
4981
|
+
return fs11.statSync(path19).isDirectory();
|
|
4935
4982
|
} catch {
|
|
4936
4983
|
return false;
|
|
4937
4984
|
}
|
|
@@ -5298,7 +5345,7 @@ var init_xml = __esm({
|
|
|
5298
5345
|
init_failure_detail();
|
|
5299
5346
|
XmlParseError = class extends Error {
|
|
5300
5347
|
constructor(file, line, detail) {
|
|
5301
|
-
super(`${file}:${line} \u2014 ${detail}`);
|
|
5348
|
+
super(`${printable(file)}:${line} \u2014 ${detail}`);
|
|
5302
5349
|
this.file = file;
|
|
5303
5350
|
this.line = line;
|
|
5304
5351
|
this.name = "XmlParseError";
|
|
@@ -5439,10 +5486,10 @@ function attemptOf(result, file, ctx) {
|
|
|
5439
5486
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
5440
5487
|
})),
|
|
5441
5488
|
steps: steps(result.steps),
|
|
5442
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
5489
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path19, body }) => ({
|
|
5443
5490
|
name: name ?? "",
|
|
5444
5491
|
...contentType !== void 0 ? { contentType } : {},
|
|
5445
|
-
...
|
|
5492
|
+
...path19 !== void 0 ? { path: path19 } : {},
|
|
5446
5493
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
5447
5494
|
// bodies is megabytes of base64 that nothing here will read.
|
|
5448
5495
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -5581,12 +5628,40 @@ function detectFormat(source, file) {
|
|
|
5581
5628
|
);
|
|
5582
5629
|
}
|
|
5583
5630
|
function readFolder(dir, format) {
|
|
5584
|
-
if (format
|
|
5585
|
-
throw new UnknownFormatError(dir,
|
|
5631
|
+
if (format === "playwright-json") {
|
|
5632
|
+
throw new UnknownFormatError(dir, "this is a folder, and only allure-results or JUnit XML files are read from one, not playwright-json.");
|
|
5586
5633
|
}
|
|
5587
5634
|
const missing = [];
|
|
5588
5635
|
const notes = [];
|
|
5589
|
-
|
|
5636
|
+
if (format === "allure" || format === void 0 && namesIn(dir, RESULT).length > 0) {
|
|
5637
|
+
return { format: "allure", results: readAllure(dir, missing, notes), missing, notes };
|
|
5638
|
+
}
|
|
5639
|
+
const results = [];
|
|
5640
|
+
const skipped = [];
|
|
5641
|
+
let reports = 0;
|
|
5642
|
+
for (const name of namesIn(dir, /\.xml$/)) {
|
|
5643
|
+
const file = path13.join(dir, name);
|
|
5644
|
+
let source;
|
|
5645
|
+
try {
|
|
5646
|
+
source = fs12.readFileSync(file, "utf8");
|
|
5647
|
+
} catch {
|
|
5648
|
+
throw new RunCommandError(`Cannot read ${printable(file)}.`);
|
|
5649
|
+
}
|
|
5650
|
+
if (!looksLikeJUnit(source)) {
|
|
5651
|
+
skipped.push(name);
|
|
5652
|
+
notes.push(`plune: ${printable(file)} is not a JUnit report \u2014 skipped.`);
|
|
5653
|
+
continue;
|
|
5654
|
+
}
|
|
5655
|
+
reports += 1;
|
|
5656
|
+
for (const r of readJUnit(source, file, missing, notes)) results.push(r);
|
|
5657
|
+
}
|
|
5658
|
+
if (reports === 0) {
|
|
5659
|
+
const names = skipped.slice(0, 3).map(printable).join(", ") + (skipped.length > 3 ? ` and ${skipped.length - 3} more` : "");
|
|
5660
|
+
throw new AllureReportError(
|
|
5661
|
+
`${printable(dir)} \u2014 no ${format === void 0 ? "*-result.json files and no " : ""}JUnit XML files in it${skipped.length > 0 ? ` (skipped, not JUnit: ${names})` : ""}, so there is no report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), the one Maven or Gradle wrote (target/surefire-reports, build/test-results/test), or at a JUnit or Playwright JSON file.`
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
return { format: "junit", results, missing, notes };
|
|
5590
5665
|
}
|
|
5591
5666
|
function readReport(source, file, format) {
|
|
5592
5667
|
const chosen2 = format ?? detectFormat(source, file);
|
|
@@ -5596,11 +5671,15 @@ function readReport(source, file, format) {
|
|
|
5596
5671
|
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
5597
5672
|
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
5598
5673
|
}
|
|
5599
|
-
var IMPORT_FORMATS, UnknownFormatError;
|
|
5674
|
+
var fs12, path13, IMPORT_FORMATS, UnknownFormatError;
|
|
5600
5675
|
var init_importers = __esm({
|
|
5601
5676
|
"src/importers/index.ts"() {
|
|
5602
5677
|
"use strict";
|
|
5603
5678
|
init_cjs_shims();
|
|
5679
|
+
fs12 = __toESM(require("fs"), 1);
|
|
5680
|
+
path13 = __toESM(require("path"), 1);
|
|
5681
|
+
init_run_lifecycle();
|
|
5682
|
+
init_failure_detail();
|
|
5604
5683
|
init_allure();
|
|
5605
5684
|
init_junit();
|
|
5606
5685
|
init_playwright_json();
|
|
@@ -5629,7 +5708,7 @@ async function timeoutMessage(limit, runId, ours, options) {
|
|
|
5629
5708
|
if (!ours) return `${head}; run ${runId} is shared with other jobs and was left open - close it with "plune run finish ${runId}" once they are done, then import this report again.`;
|
|
5630
5709
|
const wait = options.terminateWaitMs ?? 15e3;
|
|
5631
5710
|
let timer;
|
|
5632
|
-
const gaveUp = new Promise((
|
|
5711
|
+
const gaveUp = new Promise((resolve11) => timer = setTimeout(() => resolve11("gaveup"), wait));
|
|
5633
5712
|
const quiet = () => void 0;
|
|
5634
5713
|
const closed = handleRunFinish(runId, { ...options, write: quiet, terminate: true, reason: `import timed out after ${limit} s` }).then(
|
|
5635
5714
|
() => true,
|
|
@@ -5647,9 +5726,9 @@ async function handleRunImport(options) {
|
|
|
5647
5726
|
} else {
|
|
5648
5727
|
let source;
|
|
5649
5728
|
try {
|
|
5650
|
-
source =
|
|
5729
|
+
source = fs13.readFileSync(options.file, "utf8");
|
|
5651
5730
|
} catch {
|
|
5652
|
-
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
5731
|
+
throw new RunCommandError(`Cannot read ${printable(options.file)}.`);
|
|
5653
5732
|
}
|
|
5654
5733
|
read = readReport(source, options.file, options.format);
|
|
5655
5734
|
}
|
|
@@ -5698,7 +5777,7 @@ async function handleRunImport(options) {
|
|
|
5698
5777
|
else {
|
|
5699
5778
|
sent.catch(() => void 0);
|
|
5700
5779
|
let timer;
|
|
5701
|
-
const late = new Promise((
|
|
5780
|
+
const late = new Promise((resolve11) => timer = setTimeout(() => resolve11("late"), limit * 1e3));
|
|
5702
5781
|
const first = await Promise.race([sent, late]).finally(() => clearTimeout(timer));
|
|
5703
5782
|
if (first === "late") {
|
|
5704
5783
|
throw new ImportTimeoutError(await timeoutMessage(limit, session?.runId ?? null, !env.keepOpen, { ...options, apiUrl, token }));
|
|
@@ -5800,12 +5879,12 @@ function describe(result, apiUrl) {
|
|
|
5800
5879
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
5801
5880
|
return lines;
|
|
5802
5881
|
}
|
|
5803
|
-
var
|
|
5882
|
+
var fs13, ImportTimeoutError, NEXT;
|
|
5804
5883
|
var init_run_import = __esm({
|
|
5805
5884
|
"src/cli/commands/run-import.ts"() {
|
|
5806
5885
|
"use strict";
|
|
5807
5886
|
init_cjs_shims();
|
|
5808
|
-
|
|
5887
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5809
5888
|
init_session();
|
|
5810
5889
|
init_failure_detail();
|
|
5811
5890
|
init_importers();
|
|
@@ -5837,10 +5916,10 @@ function isRunResult(v) {
|
|
|
5837
5916
|
}
|
|
5838
5917
|
function handleReport(opts = {}) {
|
|
5839
5918
|
const cwd = opts.cwd ?? process.cwd();
|
|
5840
|
-
const file =
|
|
5919
|
+
const file = path14.join(cwd, ".plune", "last-run.json");
|
|
5841
5920
|
let raw;
|
|
5842
5921
|
try {
|
|
5843
|
-
raw =
|
|
5922
|
+
raw = fs14.readFileSync(file, "utf8");
|
|
5844
5923
|
} catch {
|
|
5845
5924
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
5846
5925
|
}
|
|
@@ -5855,13 +5934,13 @@ function handleReport(opts = {}) {
|
|
|
5855
5934
|
}
|
|
5856
5935
|
return parsed;
|
|
5857
5936
|
}
|
|
5858
|
-
var
|
|
5937
|
+
var fs14, path14, ReportNotFoundError;
|
|
5859
5938
|
var init_report = __esm({
|
|
5860
5939
|
"src/cli/commands/report.ts"() {
|
|
5861
5940
|
"use strict";
|
|
5862
5941
|
init_cjs_shims();
|
|
5863
|
-
|
|
5864
|
-
|
|
5942
|
+
fs14 = __toESM(require("fs"), 1);
|
|
5943
|
+
path14 = __toESM(require("path"), 1);
|
|
5865
5944
|
ReportNotFoundError = class extends Error {
|
|
5866
5945
|
code = "NO_SAVED_RUN";
|
|
5867
5946
|
constructor(message) {
|
|
@@ -5931,7 +6010,7 @@ function isRunResult2(v) {
|
|
|
5931
6010
|
function loadRunResult(file, label) {
|
|
5932
6011
|
let raw;
|
|
5933
6012
|
try {
|
|
5934
|
-
raw =
|
|
6013
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
5935
6014
|
} catch {
|
|
5936
6015
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
5937
6016
|
}
|
|
@@ -5951,12 +6030,12 @@ function handleDiff(opts) {
|
|
|
5951
6030
|
const current = loadRunResult(opts.currentPath, "current");
|
|
5952
6031
|
return diffRuns(baseline, current);
|
|
5953
6032
|
}
|
|
5954
|
-
var
|
|
6033
|
+
var fs15, DiffInputError;
|
|
5955
6034
|
var init_diff2 = __esm({
|
|
5956
6035
|
"src/cli/commands/diff.ts"() {
|
|
5957
6036
|
"use strict";
|
|
5958
6037
|
init_cjs_shims();
|
|
5959
|
-
|
|
6038
|
+
fs15 = __toESM(require("fs"), 1);
|
|
5960
6039
|
init_diff();
|
|
5961
6040
|
DiffInputError = class extends Error {
|
|
5962
6041
|
code = "DIFF_INPUT";
|
|
@@ -6081,8 +6160,8 @@ async function runInitWizard(cwd) {
|
|
|
6081
6160
|
process.exit(1);
|
|
6082
6161
|
return;
|
|
6083
6162
|
}
|
|
6084
|
-
const outputPath =
|
|
6085
|
-
if (
|
|
6163
|
+
const outputPath = path15.join(cwd, "plune.yaml");
|
|
6164
|
+
if (fs16.existsSync(outputPath)) {
|
|
6086
6165
|
const overwrite = await clack.confirm({
|
|
6087
6166
|
message: "plune.yaml already exists. Overwrite?"
|
|
6088
6167
|
});
|
|
@@ -6121,18 +6200,18 @@ async function runInitWizard(cwd) {
|
|
|
6121
6200
|
`;
|
|
6122
6201
|
const spin = clack.spinner();
|
|
6123
6202
|
spin.start("Writing plune.yaml\u2026");
|
|
6124
|
-
|
|
6203
|
+
fs16.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
6125
6204
|
spin.stop("plune.yaml created!");
|
|
6126
6205
|
clack.outro("plune.yaml is ready.");
|
|
6127
6206
|
}
|
|
6128
|
-
var clack,
|
|
6207
|
+
var clack, fs16, path15, import_yaml2;
|
|
6129
6208
|
var init_wizard = __esm({
|
|
6130
6209
|
"src/config/init/wizard.ts"() {
|
|
6131
6210
|
"use strict";
|
|
6132
6211
|
init_cjs_shims();
|
|
6133
6212
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
6134
|
-
|
|
6135
|
-
|
|
6213
|
+
fs16 = __toESM(require("fs"), 1);
|
|
6214
|
+
path15 = __toESM(require("path"), 1);
|
|
6136
6215
|
import_yaml2 = require("yaml");
|
|
6137
6216
|
init_errors();
|
|
6138
6217
|
}
|
|
@@ -6140,21 +6219,21 @@ var init_wizard = __esm({
|
|
|
6140
6219
|
|
|
6141
6220
|
// src/cli/templates/index.ts
|
|
6142
6221
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
6143
|
-
const target =
|
|
6144
|
-
if (!force &&
|
|
6222
|
+
const target = path16.join(cwd, rel);
|
|
6223
|
+
if (!force && fs17.existsSync(target)) {
|
|
6145
6224
|
return "skipped";
|
|
6146
6225
|
}
|
|
6147
|
-
|
|
6148
|
-
|
|
6226
|
+
fs17.mkdirSync(path16.dirname(target), { recursive: true });
|
|
6227
|
+
fs17.writeFileSync(target, content, "utf8");
|
|
6149
6228
|
return "written";
|
|
6150
6229
|
}
|
|
6151
|
-
var
|
|
6230
|
+
var fs17, path16, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
6152
6231
|
var init_templates = __esm({
|
|
6153
6232
|
"src/cli/templates/index.ts"() {
|
|
6154
6233
|
"use strict";
|
|
6155
6234
|
init_cjs_shims();
|
|
6156
|
-
|
|
6157
|
-
|
|
6235
|
+
fs17 = __toESM(require("fs"), 1);
|
|
6236
|
+
path16 = __toESM(require("path"), 1);
|
|
6158
6237
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
6159
6238
|
# .env is gitignored; .env.example is safe to commit.
|
|
6160
6239
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -6334,14 +6413,14 @@ var init_logout = __esm({
|
|
|
6334
6413
|
|
|
6335
6414
|
// src/cli/commands/cairn-artifact.ts
|
|
6336
6415
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
6337
|
-
const entries = await
|
|
6416
|
+
const entries = await fs18.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
6338
6417
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
6339
6418
|
let newest;
|
|
6340
6419
|
for (const entry of entries) {
|
|
6341
6420
|
if (!entry.isDirectory()) continue;
|
|
6342
|
-
const dir =
|
|
6421
|
+
const dir = path17.join(runsDir, entry.name);
|
|
6343
6422
|
try {
|
|
6344
|
-
const report = await
|
|
6423
|
+
const report = await fs18.lstat(path17.join(dir, "report.json"));
|
|
6345
6424
|
if (!report.isFile()) continue;
|
|
6346
6425
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
6347
6426
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -6354,13 +6433,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
6354
6433
|
}
|
|
6355
6434
|
async function readJson(file) {
|
|
6356
6435
|
try {
|
|
6357
|
-
return JSON.parse(await
|
|
6436
|
+
return JSON.parse(await fs18.readFile(file, "utf8"));
|
|
6358
6437
|
} catch {
|
|
6359
6438
|
return void 0;
|
|
6360
6439
|
}
|
|
6361
6440
|
}
|
|
6362
6441
|
async function defaultReadEvidence(dir) {
|
|
6363
|
-
const raw = await readJson(
|
|
6442
|
+
const raw = await readJson(path17.join(dir, "api-evidence.json"));
|
|
6364
6443
|
if (!Array.isArray(raw)) return void 0;
|
|
6365
6444
|
return raw.map((r) => {
|
|
6366
6445
|
const rec = asRecord(r);
|
|
@@ -6403,7 +6482,7 @@ function casesFromApi(report, verdicts) {
|
|
|
6403
6482
|
});
|
|
6404
6483
|
}
|
|
6405
6484
|
async function readCairnRun(dir, deps = {}) {
|
|
6406
|
-
const raw = await readJson(
|
|
6485
|
+
const raw = await readJson(path17.join(dir, "report.json"));
|
|
6407
6486
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
6408
6487
|
const report = asRecord(raw);
|
|
6409
6488
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -6433,20 +6512,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
6433
6512
|
return {
|
|
6434
6513
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
6435
6514
|
run: {
|
|
6436
|
-
runId: str(report["runId"],
|
|
6515
|
+
runId: str(report["runId"], path17.basename(dir)),
|
|
6437
6516
|
mode,
|
|
6438
6517
|
target: str(report["url"], "unknown")
|
|
6439
6518
|
},
|
|
6440
6519
|
cases
|
|
6441
6520
|
};
|
|
6442
6521
|
}
|
|
6443
|
-
var
|
|
6522
|
+
var fs18, path17, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
6444
6523
|
var init_cairn_artifact = __esm({
|
|
6445
6524
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
6446
6525
|
"use strict";
|
|
6447
6526
|
init_cjs_shims();
|
|
6448
|
-
|
|
6449
|
-
|
|
6527
|
+
fs18 = __toESM(require("fs/promises"), 1);
|
|
6528
|
+
path17 = __toESM(require("path"), 1);
|
|
6450
6529
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
6451
6530
|
INGEST_SCHEMA_VERSION = 1;
|
|
6452
6531
|
NoCairnRunsError = class extends Error {
|
|
@@ -6517,28 +6596,42 @@ async function handleIngest(deps) {
|
|
|
6517
6596
|
const payload = await readCairnRun(dir);
|
|
6518
6597
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6519
6598
|
const endpoint = `${apiUrl}/v1/ingest/cairn`;
|
|
6520
|
-
|
|
6599
|
+
const job = new JobReporter(doFetch, apiUrl, token, "ingest");
|
|
6600
|
+
await job.open(path18.basename(path18.resolve(dir)), payload.cases.length);
|
|
6521
6601
|
try {
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
} catch {
|
|
6528
|
-
|
|
6602
|
+
const result = await upload();
|
|
6603
|
+
await job.uploaded({ subjectId: result.runId || void 0 });
|
|
6604
|
+
await job.finish({ ok: true });
|
|
6605
|
+
if (job.problem !== void 0) result.jobProblem = job.problem;
|
|
6606
|
+
return result;
|
|
6607
|
+
} catch (err) {
|
|
6608
|
+
await job.finish({ ok: false, reason: err instanceof Error && err.message.trim() || "Ingest failed." });
|
|
6609
|
+
throw err;
|
|
6529
6610
|
}
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6611
|
+
async function upload() {
|
|
6612
|
+
let res;
|
|
6613
|
+
try {
|
|
6614
|
+
res = await doFetch(endpoint, {
|
|
6615
|
+
method: "POST",
|
|
6616
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
6617
|
+
body: JSON.stringify(payload)
|
|
6618
|
+
});
|
|
6619
|
+
} catch {
|
|
6620
|
+
throw new IngestNetworkError(apiUrl);
|
|
6621
|
+
}
|
|
6622
|
+
if (!res.ok) {
|
|
6623
|
+
const detail = await safeErrorDetail4(res);
|
|
6624
|
+
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
6625
|
+
throw new IngestHttpError(res.status, detail);
|
|
6626
|
+
}
|
|
6627
|
+
const body = await res.json().catch(() => ({}));
|
|
6628
|
+
return {
|
|
6629
|
+
runId: typeof body.runId === "string" ? body.runId : "",
|
|
6630
|
+
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
6631
|
+
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
6632
|
+
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
6633
|
+
};
|
|
6534
6634
|
}
|
|
6535
|
-
const body = await res.json().catch(() => ({}));
|
|
6536
|
-
return {
|
|
6537
|
-
runId: typeof body.runId === "string" ? body.runId : "",
|
|
6538
|
-
linked: typeof body.linked === "number" ? body.linked : 0,
|
|
6539
|
-
proposed: typeof body.proposed === "number" ? body.proposed : 0,
|
|
6540
|
-
skipped: typeof body.skipped === "number" ? body.skipped : 0
|
|
6541
|
-
};
|
|
6542
6635
|
}
|
|
6543
6636
|
function reportIngestFailure(err, write) {
|
|
6544
6637
|
if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
|
|
@@ -6563,14 +6656,16 @@ function formatIngestResult(r) {
|
|
|
6563
6656
|
return `${lines.join("\n")}
|
|
6564
6657
|
`;
|
|
6565
6658
|
}
|
|
6566
|
-
var IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6659
|
+
var path18, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
|
|
6567
6660
|
var init_ingest = __esm({
|
|
6568
6661
|
"src/cli/commands/ingest.ts"() {
|
|
6569
6662
|
"use strict";
|
|
6570
6663
|
init_cjs_shims();
|
|
6664
|
+
path18 = __toESM(require("path"), 1);
|
|
6571
6665
|
init_api_url();
|
|
6572
6666
|
init_credentials();
|
|
6573
6667
|
init_cairn_artifact();
|
|
6668
|
+
init_job_reporter();
|
|
6574
6669
|
IngestNotLoggedInError = class extends Error {
|
|
6575
6670
|
constructor() {
|
|
6576
6671
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -7514,7 +7609,13 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7514
7609
|
await failRunCommand(err, verboseOf(command));
|
|
7515
7610
|
}
|
|
7516
7611
|
});
|
|
7517
|
-
runCommand.command("import").argument(
|
|
7612
|
+
runCommand.command("import").argument(
|
|
7613
|
+
"<file>",
|
|
7614
|
+
"A JUnit XML or Playwright JSON report, a folder of JUnit XML files (target/surefire-reports, build/test-results/test), or an Allure allure-results folder"
|
|
7615
|
+
).description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option(
|
|
7616
|
+
"--format <fmt>",
|
|
7617
|
+
"junit | playwright-json | allure; detected when omitted (a folder with a *-result.json is allure, any other folder is its *.xml files as junit)"
|
|
7618
|
+
).option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--name <text>", "What to call the run in the list (default: the folder name, the time and where it ran; PLUNE_RUN_TITLE sets it too)").option("--create", "Offer tests nothing matched to the review queue", false).option(
|
|
7518
7619
|
"--fail-on-missing-attachments",
|
|
7519
7620
|
"Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
|
|
7520
7621
|
false
|
|
@@ -7644,10 +7745,10 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7644
7745
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
7645
7746
|
try {
|
|
7646
7747
|
const token = options.token ?? await readTokenFromStdin();
|
|
7647
|
-
const { path:
|
|
7748
|
+
const { path: path19, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
7648
7749
|
process.stdout.write(
|
|
7649
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
7650
|
-
` : `Token saved to ${
|
|
7750
|
+
verified ? `Logged in. Token checked against the API and saved to ${path19}
|
|
7751
|
+
` : `Token saved to ${path19} WITHOUT being checked (--skip-verify).
|
|
7651
7752
|
`
|
|
7652
7753
|
);
|
|
7653
7754
|
} catch (err) {
|
|
@@ -7664,9 +7765,9 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7664
7765
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7665
7766
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
7666
7767
|
try {
|
|
7667
|
-
const { removed, path:
|
|
7768
|
+
const { removed, path: path19 } = handleLogout2();
|
|
7668
7769
|
process.stdout.write(
|
|
7669
|
-
removed ? `Logged out. Removed ${
|
|
7770
|
+
removed ? `Logged out. Removed ${path19}
|
|
7670
7771
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
7671
7772
|
);
|
|
7672
7773
|
} catch (err) {
|
|
@@ -7710,9 +7811,12 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7710
7811
|
const verbose = globals.verbose === true;
|
|
7711
7812
|
const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
7712
7813
|
try {
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
)
|
|
7814
|
+
const result = await handleIngest2({ ...dir !== void 0 ? { dir } : {} });
|
|
7815
|
+
process.stdout.write(formatIngestResult2(result));
|
|
7816
|
+
if (result.jobProblem !== void 0) {
|
|
7817
|
+
process.stderr.write(`plune: could not report this ingest to the job queue (${result.jobProblem}) \u2014 the run is recorded.
|
|
7818
|
+
`);
|
|
7819
|
+
}
|
|
7716
7820
|
} catch (err) {
|
|
7717
7821
|
const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
|
|
7718
7822
|
if (code !== null) {
|