@plune-ai/cli 0.18.0 → 0.19.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 +22 -4
- package/dist/cli.cjs +84 -18
- package/dist/index.cjs +28 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +28 -1
- package/dist/reporter-core.cjs +8 -3
- package/dist/reporter-core.d.cts +17 -3
- package/dist/reporter-core.d.ts +17 -3
- package/dist/reporter-core.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,11 +119,11 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
|
|
|
119
119
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
120
120
|
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Account → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). In CI, skip this step: every platform command reads `PLUNE_TOKEN` from the environment first, and the saved login only when it is not set. |
|
|
121
121
|
| `plune logout` | Remove the saved token. |
|
|
122
|
-
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON. |
|
|
123
|
-
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way). |
|
|
122
|
+
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. |
|
|
123
|
+
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze when this import finished the run. |
|
|
124
124
|
| `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--json` for one machine-readable line. |
|
|
125
|
-
| `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text
|
|
126
|
-
| `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey
|
|
125
|
+
| `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze. |
|
|
126
|
+
| `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code instead — or with the command's own when no result reached the run. |
|
|
127
127
|
| `plune run delete <id>` | Delete a run and everything it produced — its results and the review-queue entries it raised. Approved test cases and the audit log stay. **Recoverable for 30 days** (an owner of the project can put it back from Settings → Bin), then gone for good. No prompt: it is your data, and this command belongs in scripts. |
|
|
128
128
|
| `plune run report` | Replay `.plune/pending-results.jsonl` — send what the reporter could not. The lines of one reporter run go back as one run, with the commit and branch the reporter recorded. Once every result has gone the file is renamed to `pending-results.<time>.sent.jsonl`; if anything did not go, it stays as it was. Flags: `--file <path>`. |
|
|
129
129
|
| `plune ingest [dir]` | Record a [Cairn](https://github.com/plune-ai/cairn) run in Plune. Omit `[dir]` for the newest run under `./runs`, or name the directory holding `report.json`. Generated cases arrive as **review proposals** — nothing is created until a person approves it. |
|
|
@@ -135,6 +135,24 @@ Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
|
135
135
|
|
|
136
136
|
**Exit codes:** `0` everything passed · `1` an assertion failed · `2` configuration or execution error · `3` `pull` refused to overwrite uncommitted changes · `4` `push` had refusals.
|
|
137
137
|
|
|
138
|
+
### A release gate: `--fail-on-failures`
|
|
139
|
+
|
|
140
|
+
`plune run finish`, `plune run exec`, `plune run import` and `plune sync` take `--fail-on-failures`. The exit
|
|
141
|
+
code is then the platform's verdict on the run, not the runner's: the summary the platform froze at `finish`
|
|
142
|
+
(for `sync`, one `GET /v1/runs/:id/summary` after the upload). The platform counts a muted case's failure under
|
|
143
|
+
`muted`, so a mute set in Plune is honoured without the CLI knowing about it.
|
|
144
|
+
|
|
145
|
+
| The run | Exit |
|
|
146
|
+
| ------- | ---- |
|
|
147
|
+
| a `failed` or `broken` result that is not muted | `1` |
|
|
148
|
+
| an expected case that never reported (`notRun`): a dead shard is not green | `1` |
|
|
149
|
+
| only `passed`, `blocked`, `skipped` and muted failures | `0` |
|
|
150
|
+
| no verdict: the platform could not be reached, the finish was refused, the run was left open or terminated | `1` |
|
|
151
|
+
| `run exec` where no result reached the run and nothing was expected of it | the command's own code |
|
|
152
|
+
|
|
153
|
+
Without the flag every command exits as it always did. `plune run` (local evals) is not affected: it stays the
|
|
154
|
+
raw local signal and works offline.
|
|
155
|
+
|
|
138
156
|
## Already running tests? Bring the results in
|
|
139
157
|
|
|
140
158
|
Everything above generates checks, which is why it needs a provider key. If you already have a
|
package/dist/cli.cjs
CHANGED
|
@@ -3306,21 +3306,26 @@ async function startRun(passed, declared2) {
|
|
|
3306
3306
|
},
|
|
3307
3307
|
flush,
|
|
3308
3308
|
async finish(reason) {
|
|
3309
|
-
if (done) return;
|
|
3309
|
+
if (done) return void 0;
|
|
3310
3310
|
done = true;
|
|
3311
3311
|
await flush();
|
|
3312
3312
|
await offer();
|
|
3313
|
+
let frozen;
|
|
3313
3314
|
if (runId !== null && !offline) {
|
|
3314
3315
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
3315
3316
|
event: "finish",
|
|
3316
3317
|
...reason !== void 0 ? { reason } : {}
|
|
3317
3318
|
});
|
|
3318
3319
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
3319
|
-
else
|
|
3320
|
-
|
|
3320
|
+
else {
|
|
3321
|
+
frozen = out.body?.summary ?? void 0;
|
|
3322
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
3323
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
3324
|
+
}
|
|
3321
3325
|
}
|
|
3322
3326
|
}
|
|
3323
3327
|
summarise();
|
|
3328
|
+
return frozen;
|
|
3324
3329
|
},
|
|
3325
3330
|
async leaveOpen() {
|
|
3326
3331
|
if (done) return;
|
|
@@ -3398,6 +3403,28 @@ var init_session = __esm({
|
|
|
3398
3403
|
}
|
|
3399
3404
|
});
|
|
3400
3405
|
|
|
3406
|
+
// src/cli/gate.ts
|
|
3407
|
+
function gateExit(verdict, runnerCode) {
|
|
3408
|
+
if (verdict?.statusEvent === "terminated") {
|
|
3409
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
3410
|
+
}
|
|
3411
|
+
const counts = verdict?.counts;
|
|
3412
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
3413
|
+
const n = (status) => counts[status] ?? 0;
|
|
3414
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
3415
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
3416
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
3417
|
+
}
|
|
3418
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
3419
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
3420
|
+
}
|
|
3421
|
+
var init_gate = __esm({
|
|
3422
|
+
"src/cli/gate.ts"() {
|
|
3423
|
+
"use strict";
|
|
3424
|
+
init_cjs_shims();
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3401
3428
|
// src/cli/commands/run-lifecycle.ts
|
|
3402
3429
|
var run_lifecycle_exports = {};
|
|
3403
3430
|
__export(run_lifecycle_exports, {
|
|
@@ -3455,7 +3482,7 @@ async function handleRunStart(options = {}) {
|
|
|
3455
3482
|
}
|
|
3456
3483
|
return result;
|
|
3457
3484
|
}
|
|
3458
|
-
async function handleRunFinish(id, options = {}) {
|
|
3485
|
+
async function handleRunFinish(id, options = {}, runnerCode) {
|
|
3459
3486
|
const { client, write } = connect(options);
|
|
3460
3487
|
const event = options.terminate === true ? "terminate" : "finish";
|
|
3461
3488
|
const out = await client.post(`/v1/runs/${id}/events`, {
|
|
@@ -3468,6 +3495,11 @@ async function handleRunFinish(id, options = {}) {
|
|
|
3468
3495
|
);
|
|
3469
3496
|
}
|
|
3470
3497
|
write(`Run ${id} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
3498
|
+
if (options.failOnFailures !== true) return runnerCode ?? 0;
|
|
3499
|
+
const summary = out.body?.summary;
|
|
3500
|
+
const gate = gateExit({ statusEvent: out.body?.run?.statusEvent, counts: summary?.counts, notRun: summary?.notRun }, runnerCode);
|
|
3501
|
+
write(gate.line);
|
|
3502
|
+
return gate.code;
|
|
3471
3503
|
}
|
|
3472
3504
|
async function handleRunDelete(id, options = {}) {
|
|
3473
3505
|
const { client, write } = connect(options);
|
|
@@ -3499,8 +3531,7 @@ async function handleRunExec(options) {
|
|
|
3499
3531
|
child.on("close", (exitCode) => resolve9(exitCode ?? 1));
|
|
3500
3532
|
child.on("error", () => resolve9(1));
|
|
3501
3533
|
});
|
|
3502
|
-
|
|
3503
|
-
return code;
|
|
3534
|
+
return handleRunFinish(run.id, options, code);
|
|
3504
3535
|
}
|
|
3505
3536
|
async function handleRunReport(options = {}) {
|
|
3506
3537
|
const file = options.file ?? DEFAULT_FALLBACK_PATH;
|
|
@@ -3596,6 +3627,7 @@ var init_run_lifecycle = __esm({
|
|
|
3596
3627
|
init_env2();
|
|
3597
3628
|
init_fallback();
|
|
3598
3629
|
init_session();
|
|
3630
|
+
init_gate();
|
|
3599
3631
|
NoTokenError = class extends Error {
|
|
3600
3632
|
constructor() {
|
|
3601
3633
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -4469,8 +4501,9 @@ async function handleRunImport(options) {
|
|
|
4469
4501
|
results.map((r) => r.keys)
|
|
4470
4502
|
);
|
|
4471
4503
|
for (const result2 of results) await session.add(result2);
|
|
4504
|
+
let frozen;
|
|
4472
4505
|
if (env.keepOpen) await session.leaveOpen();
|
|
4473
|
-
else await session.finish();
|
|
4506
|
+
else frozen = await session.finish();
|
|
4474
4507
|
const stats = session.stats;
|
|
4475
4508
|
const result = {
|
|
4476
4509
|
runId: session.runId,
|
|
@@ -4499,6 +4532,14 @@ async function handleRunImport(options) {
|
|
|
4499
4532
|
offering: options.create === true
|
|
4500
4533
|
};
|
|
4501
4534
|
for (const line of describe(result, apiUrl)) write(line);
|
|
4535
|
+
if (options.failOnFailures === true) {
|
|
4536
|
+
if (env.keepOpen) {
|
|
4537
|
+
write("plune: the run is left open for the jobs still reporting, so there is no verdict yet \u2014 gate on the step that finishes it.");
|
|
4538
|
+
}
|
|
4539
|
+
const gate = gateExit(frozen);
|
|
4540
|
+
write(gate.line);
|
|
4541
|
+
result.gate = gate.code;
|
|
4542
|
+
}
|
|
4502
4543
|
return result;
|
|
4503
4544
|
}
|
|
4504
4545
|
function runUrl2(apiUrl, id) {
|
|
@@ -4565,6 +4606,7 @@ var init_run_import = __esm({
|
|
|
4565
4606
|
init_run_lifecycle();
|
|
4566
4607
|
init_api_url();
|
|
4567
4608
|
init_credentials();
|
|
4609
|
+
init_gate();
|
|
4568
4610
|
init_env2();
|
|
4569
4611
|
NEXT = {
|
|
4570
4612
|
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
@@ -5220,7 +5262,18 @@ async function handleSync(deps = {}) {
|
|
|
5220
5262
|
}
|
|
5221
5263
|
const body = await res.json().catch(() => ({}));
|
|
5222
5264
|
const id = typeof body.id === "string" ? body.id : "";
|
|
5223
|
-
|
|
5265
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
5266
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
5267
|
+
return result;
|
|
5268
|
+
}
|
|
5269
|
+
async function readVerdict(doFetch, url, token) {
|
|
5270
|
+
try {
|
|
5271
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
5272
|
+
const body = await res.json().catch(() => void 0);
|
|
5273
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
5274
|
+
} catch {
|
|
5275
|
+
return void 0;
|
|
5276
|
+
}
|
|
5224
5277
|
}
|
|
5225
5278
|
function reportSyncFailure(err, write) {
|
|
5226
5279
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
@@ -5243,6 +5296,7 @@ var init_sync = __esm({
|
|
|
5243
5296
|
init_run_result_schema();
|
|
5244
5297
|
init_api_url();
|
|
5245
5298
|
init_credentials();
|
|
5299
|
+
init_gate();
|
|
5246
5300
|
NotLoggedInError = class extends Error {
|
|
5247
5301
|
constructor() {
|
|
5248
5302
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -5907,6 +5961,7 @@ function createProgram() {
|
|
|
5907
5961
|
process.exit(1);
|
|
5908
5962
|
};
|
|
5909
5963
|
const verboseOf = (command) => command.optsWithGlobals().verbose === true;
|
|
5964
|
+
const FAIL_ON_FAILURES = "Exit with the platform's verdict on the run: 1 on a failed or broken result (muted ones do not count), on an expected case that never ran, or when there is no verdict at all";
|
|
5910
5965
|
runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
|
|
5911
5966
|
try {
|
|
5912
5967
|
const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
@@ -5918,14 +5973,16 @@ function createProgram() {
|
|
|
5918
5973
|
await failRunCommand(err, verboseOf(command));
|
|
5919
5974
|
}
|
|
5920
5975
|
});
|
|
5921
|
-
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").action(
|
|
5976
|
+
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
5922
5977
|
async (id, options, command) => {
|
|
5923
5978
|
try {
|
|
5924
5979
|
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
5925
|
-
await handleRunFinish2(id, {
|
|
5980
|
+
const code = await handleRunFinish2(id, {
|
|
5926
5981
|
terminate: options.terminate,
|
|
5927
|
-
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
5982
|
+
...options.reason !== void 0 ? { reason: options.reason } : {},
|
|
5983
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5928
5984
|
});
|
|
5985
|
+
if (options.failOnFailures) process.exitCode = code;
|
|
5929
5986
|
} catch (err) {
|
|
5930
5987
|
await failRunCommand(err, verboseOf(command));
|
|
5931
5988
|
}
|
|
@@ -5933,12 +5990,13 @@ function createProgram() {
|
|
|
5933
5990
|
);
|
|
5934
5991
|
runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description(
|
|
5935
5992
|
"Open a run, run a command inside it, close the run \u2014 whatever the command returns"
|
|
5936
|
-
).option("--key <externalKey>", "The key several processes share; generated when absent").action(async (argv, options, command) => {
|
|
5993
|
+
).option("--key <externalKey>", "The key several processes share; generated when absent").option("--fail-on-failures", `${FAIL_ON_FAILURES}; the command's own exit code when no result reached the run`, false).action(async (argv, options, command) => {
|
|
5937
5994
|
try {
|
|
5938
5995
|
const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
5939
5996
|
process.exitCode = await handleRunExec2({
|
|
5940
5997
|
argv,
|
|
5941
|
-
...options.key !== void 0 ? { key: options.key } : {}
|
|
5998
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
5999
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5942
6000
|
});
|
|
5943
6001
|
} catch (err) {
|
|
5944
6002
|
await failRunCommand(err, verboseOf(command));
|
|
@@ -5967,7 +6025,7 @@ function createProgram() {
|
|
|
5967
6025
|
"--fail-on-missing-attachments",
|
|
5968
6026
|
"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",
|
|
5969
6027
|
false
|
|
5970
|
-
).action(
|
|
6028
|
+
).option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
5971
6029
|
async (file, options, command) => {
|
|
5972
6030
|
const [
|
|
5973
6031
|
{ handleRunImport: handleRunImport2 },
|
|
@@ -5991,9 +6049,11 @@ function createProgram() {
|
|
|
5991
6049
|
file,
|
|
5992
6050
|
...format !== void 0 ? { format } : {},
|
|
5993
6051
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
5994
|
-
...options.create ? { create: true } : {}
|
|
6052
|
+
...options.create ? { create: true } : {},
|
|
6053
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5995
6054
|
});
|
|
5996
6055
|
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|
|
6056
|
+
if (imported.gate === 1) process.exitCode = 1;
|
|
5997
6057
|
} catch (err) {
|
|
5998
6058
|
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2 || err instanceof AllureReportError2) {
|
|
5999
6059
|
process.stderr.write(err.message + "\n");
|
|
@@ -6103,18 +6163,24 @@ function createProgram() {
|
|
|
6103
6163
|
failUnexpected(err, verbose);
|
|
6104
6164
|
}
|
|
6105
6165
|
});
|
|
6106
|
-
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").action(async (options, command) => {
|
|
6166
|
+
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").option("--fail-on-failures", `${FAIL_ON_FAILURES}; read with one GET of the run's summary after the upload`, false).action(async (options, command) => {
|
|
6107
6167
|
const globals = command.optsWithGlobals();
|
|
6108
6168
|
const verbose = globals.verbose === true;
|
|
6109
6169
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
6110
6170
|
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
6111
6171
|
try {
|
|
6112
|
-
const { id, url } = await handleSync2({
|
|
6172
|
+
const { id, url, gate } = await handleSync2({
|
|
6113
6173
|
cwd,
|
|
6114
|
-
...options.file !== void 0 ? { file: options.file } : {}
|
|
6174
|
+
...options.file !== void 0 ? { file: options.file } : {},
|
|
6175
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
6115
6176
|
});
|
|
6116
6177
|
process.stdout.write(`Synced run ${id} \u2192 ${url}
|
|
6117
6178
|
`);
|
|
6179
|
+
if (gate !== void 0) {
|
|
6180
|
+
process.stdout.write(`${gate.line}
|
|
6181
|
+
`);
|
|
6182
|
+
process.exitCode = gate.code;
|
|
6183
|
+
}
|
|
6118
6184
|
} catch (err) {
|
|
6119
6185
|
const code = reportSyncFailure2(err, (s) => process.stderr.write(s));
|
|
6120
6186
|
if (code !== null) {
|
package/dist/index.cjs
CHANGED
|
@@ -1703,6 +1703,22 @@ function loadToken() {
|
|
|
1703
1703
|
}
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
|
+
// src/cli/gate.ts
|
|
1707
|
+
function gateExit(verdict, runnerCode) {
|
|
1708
|
+
if (verdict?.statusEvent === "terminated") {
|
|
1709
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
1710
|
+
}
|
|
1711
|
+
const counts = verdict?.counts;
|
|
1712
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
1713
|
+
const n = (status) => counts[status] ?? 0;
|
|
1714
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
1715
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
1716
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
1717
|
+
}
|
|
1718
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
1719
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1706
1722
|
// src/cli/commands/sync.ts
|
|
1707
1723
|
var NotLoggedInError = class extends Error {
|
|
1708
1724
|
constructor() {
|
|
@@ -1795,7 +1811,18 @@ async function handleSync(deps = {}) {
|
|
|
1795
1811
|
}
|
|
1796
1812
|
const body = await res.json().catch(() => ({}));
|
|
1797
1813
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1798
|
-
|
|
1814
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
1815
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1816
|
+
return result;
|
|
1817
|
+
}
|
|
1818
|
+
async function readVerdict(doFetch, url, token) {
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
1821
|
+
const body = await res.json().catch(() => void 0);
|
|
1822
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
1823
|
+
} catch {
|
|
1824
|
+
return void 0;
|
|
1825
|
+
}
|
|
1799
1826
|
}
|
|
1800
1827
|
function reportSyncFailure(err, write) {
|
|
1801
1828
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
package/dist/index.d.cts
CHANGED
|
@@ -315,12 +315,19 @@ interface SyncDeps {
|
|
|
315
315
|
loadToken?: () => string | null;
|
|
316
316
|
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
317
317
|
fetchImpl?: typeof fetch;
|
|
318
|
+
/** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
|
|
319
|
+
failOnFailures?: boolean;
|
|
318
320
|
}
|
|
319
321
|
interface SyncResult {
|
|
320
322
|
/** Server-assigned run id. */
|
|
321
323
|
id: string;
|
|
322
324
|
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
323
325
|
url: string;
|
|
326
|
+
/** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
|
|
327
|
+
gate?: {
|
|
328
|
+
code: number;
|
|
329
|
+
line: string;
|
|
330
|
+
};
|
|
324
331
|
}
|
|
325
332
|
/**
|
|
326
333
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
package/dist/index.d.ts
CHANGED
|
@@ -315,12 +315,19 @@ interface SyncDeps {
|
|
|
315
315
|
loadToken?: () => string | null;
|
|
316
316
|
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
317
317
|
fetchImpl?: typeof fetch;
|
|
318
|
+
/** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
|
|
319
|
+
failOnFailures?: boolean;
|
|
318
320
|
}
|
|
319
321
|
interface SyncResult {
|
|
320
322
|
/** Server-assigned run id. */
|
|
321
323
|
id: string;
|
|
322
324
|
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
323
325
|
url: string;
|
|
326
|
+
/** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
|
|
327
|
+
gate?: {
|
|
328
|
+
code: number;
|
|
329
|
+
line: string;
|
|
330
|
+
};
|
|
324
331
|
}
|
|
325
332
|
/**
|
|
326
333
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
package/dist/index.js
CHANGED
|
@@ -1647,6 +1647,22 @@ function loadToken() {
|
|
|
1647
1647
|
}
|
|
1648
1648
|
}
|
|
1649
1649
|
|
|
1650
|
+
// src/cli/gate.ts
|
|
1651
|
+
function gateExit(verdict, runnerCode) {
|
|
1652
|
+
if (verdict?.statusEvent === "terminated") {
|
|
1653
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
1654
|
+
}
|
|
1655
|
+
const counts = verdict?.counts;
|
|
1656
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
1657
|
+
const n = (status) => counts[status] ?? 0;
|
|
1658
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
1659
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
1660
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
1661
|
+
}
|
|
1662
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
1663
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1650
1666
|
// src/cli/commands/sync.ts
|
|
1651
1667
|
var NotLoggedInError = class extends Error {
|
|
1652
1668
|
constructor() {
|
|
@@ -1739,7 +1755,18 @@ async function handleSync(deps = {}) {
|
|
|
1739
1755
|
}
|
|
1740
1756
|
const body = await res.json().catch(() => ({}));
|
|
1741
1757
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1742
|
-
|
|
1758
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
1759
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1760
|
+
return result;
|
|
1761
|
+
}
|
|
1762
|
+
async function readVerdict(doFetch, url, token) {
|
|
1763
|
+
try {
|
|
1764
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
1765
|
+
const body = await res.json().catch(() => void 0);
|
|
1766
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return void 0;
|
|
1769
|
+
}
|
|
1743
1770
|
}
|
|
1744
1771
|
function reportSyncFailure(err, write) {
|
|
1745
1772
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -1100,21 +1100,26 @@ async function startRun(passed, declared) {
|
|
|
1100
1100
|
},
|
|
1101
1101
|
flush,
|
|
1102
1102
|
async finish(reason) {
|
|
1103
|
-
if (done) return;
|
|
1103
|
+
if (done) return void 0;
|
|
1104
1104
|
done = true;
|
|
1105
1105
|
await flush();
|
|
1106
1106
|
await offer();
|
|
1107
|
+
let frozen;
|
|
1107
1108
|
if (runId !== null && !offline) {
|
|
1108
1109
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
1109
1110
|
event: "finish",
|
|
1110
1111
|
...reason !== void 0 ? { reason } : {}
|
|
1111
1112
|
});
|
|
1112
1113
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
1113
|
-
else
|
|
1114
|
-
|
|
1114
|
+
else {
|
|
1115
|
+
frozen = out.body?.summary ?? void 0;
|
|
1116
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
1117
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
1118
|
+
}
|
|
1115
1119
|
}
|
|
1116
1120
|
}
|
|
1117
1121
|
summarise();
|
|
1122
|
+
return frozen;
|
|
1118
1123
|
},
|
|
1119
1124
|
async leaveOpen() {
|
|
1120
1125
|
if (done) return;
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -221,6 +221,17 @@ interface RunRecord {
|
|
|
221
221
|
projectId?: string;
|
|
222
222
|
schemaVersion?: number;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
|
|
226
|
+
* release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
|
|
227
|
+
* muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
|
|
228
|
+
*/
|
|
229
|
+
interface FrozenSummary {
|
|
230
|
+
counts: Record<string, number>;
|
|
231
|
+
/** The expected cases that never reported. */
|
|
232
|
+
notRun: string[];
|
|
233
|
+
total: number;
|
|
234
|
+
}
|
|
224
235
|
/** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
|
|
225
236
|
* the platform refuses to resolve that by overwriting, and neither do we. */
|
|
226
237
|
interface SubmitCounts {
|
|
@@ -357,8 +368,11 @@ interface RunSession {
|
|
|
357
368
|
add(result: PendingResult): Promise<void>;
|
|
358
369
|
/** Send whatever is buffered. Safe to call when nothing is. */
|
|
359
370
|
flush(): Promise<void>;
|
|
360
|
-
/**
|
|
361
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
|
|
373
|
+
* `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
|
|
374
|
+
*/
|
|
375
|
+
finish(reason?: string): Promise<FrozenSummary | undefined>;
|
|
362
376
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
363
377
|
leaveOpen(): Promise<void>;
|
|
364
378
|
}
|
|
@@ -592,4 +606,4 @@ interface PlatformClient {
|
|
|
592
606
|
}
|
|
593
607
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
594
608
|
|
|
595
|
-
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
|
609
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type FrozenSummary, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -221,6 +221,17 @@ interface RunRecord {
|
|
|
221
221
|
projectId?: string;
|
|
222
222
|
schemaVersion?: number;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
|
|
226
|
+
* release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
|
|
227
|
+
* muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
|
|
228
|
+
*/
|
|
229
|
+
interface FrozenSummary {
|
|
230
|
+
counts: Record<string, number>;
|
|
231
|
+
/** The expected cases that never reported. */
|
|
232
|
+
notRun: string[];
|
|
233
|
+
total: number;
|
|
234
|
+
}
|
|
224
235
|
/** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
|
|
225
236
|
* the platform refuses to resolve that by overwriting, and neither do we. */
|
|
226
237
|
interface SubmitCounts {
|
|
@@ -357,8 +368,11 @@ interface RunSession {
|
|
|
357
368
|
add(result: PendingResult): Promise<void>;
|
|
358
369
|
/** Send whatever is buffered. Safe to call when nothing is. */
|
|
359
370
|
flush(): Promise<void>;
|
|
360
|
-
/**
|
|
361
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
|
|
373
|
+
* `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
|
|
374
|
+
*/
|
|
375
|
+
finish(reason?: string): Promise<FrozenSummary | undefined>;
|
|
362
376
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
363
377
|
leaveOpen(): Promise<void>;
|
|
364
378
|
}
|
|
@@ -592,4 +606,4 @@ interface PlatformClient {
|
|
|
592
606
|
}
|
|
593
607
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
594
608
|
|
|
595
|
-
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
|
609
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type FrozenSummary, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.js
CHANGED
|
@@ -1051,21 +1051,26 @@ async function startRun(passed, declared) {
|
|
|
1051
1051
|
},
|
|
1052
1052
|
flush,
|
|
1053
1053
|
async finish(reason) {
|
|
1054
|
-
if (done) return;
|
|
1054
|
+
if (done) return void 0;
|
|
1055
1055
|
done = true;
|
|
1056
1056
|
await flush();
|
|
1057
1057
|
await offer();
|
|
1058
|
+
let frozen;
|
|
1058
1059
|
if (runId !== null && !offline) {
|
|
1059
1060
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
1060
1061
|
event: "finish",
|
|
1061
1062
|
...reason !== void 0 ? { reason } : {}
|
|
1062
1063
|
});
|
|
1063
1064
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
1064
|
-
else
|
|
1065
|
-
|
|
1065
|
+
else {
|
|
1066
|
+
frozen = out.body?.summary ?? void 0;
|
|
1067
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
1068
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
1069
|
+
}
|
|
1066
1070
|
}
|
|
1067
1071
|
}
|
|
1068
1072
|
summarise();
|
|
1073
|
+
return frozen;
|
|
1069
1074
|
},
|
|
1070
1075
|
async leaveOpen() {
|
|
1071
1076
|
if (done) return;
|
package/package.json
CHANGED