@beryl-so/cli 0.38.0 → 0.41.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 +7 -4
- package/dist/commands/account.js +40 -7
- package/dist/commands/environments.js +11 -2
- package/dist/commands/runs.js +23 -3
- package/dist/commands/watch.js +100 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,13 +194,13 @@ Manage a project's environments: the URLs and auth Beryl runs tests against.
|
|
|
194
194
|
|
|
195
195
|
### schedule
|
|
196
196
|
|
|
197
|
-
Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).
|
|
197
|
+
Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own environment, cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).
|
|
198
198
|
|
|
199
199
|
| Command | Summary | MCP tool |
|
|
200
200
|
| --- | --- | --- |
|
|
201
201
|
| `beryl schedule list` | List the project's schedules | `schedule_list` |
|
|
202
202
|
| `beryl schedule add` | Add a schedule (daily, or weekly on a given day) | `schedule_add` |
|
|
203
|
-
| `beryl schedule update <schedule-id>` | Change a schedule's cadence, time, timezone, or
|
|
203
|
+
| `beryl schedule update <schedule-id>` | Change a schedule's cadence, time, timezone, groups, or environment | `schedule_update` |
|
|
204
204
|
| `beryl schedule remove <schedule-id>` | Remove a schedule (its tests and groups stay) | `schedule_remove` |
|
|
205
205
|
|
|
206
206
|
### tests
|
|
@@ -359,11 +359,14 @@ Send product feedback to the Beryl team.
|
|
|
359
359
|
|
|
360
360
|
### billing
|
|
361
361
|
|
|
362
|
-
Review a workspace's plan usage, subscription, and invoices.
|
|
362
|
+
Review a workspace's plan usage, subscription, and invoices; upgrade or switch plans.
|
|
363
363
|
|
|
364
364
|
| Command | Summary | MCP tool |
|
|
365
365
|
| --- | --- | --- |
|
|
366
|
-
| `beryl billing
|
|
366
|
+
| `beryl billing plans` | List the plans: price, cloud run minutes, storage, overage terms | none |
|
|
367
|
+
| `beryl billing usage` | Show plan usage: cloud run minutes, storage held, and this month's overage | none |
|
|
368
|
+
| `beryl billing checkout` | Get a Stripe Checkout link that moves a Free workspace onto Team or Business | none |
|
|
369
|
+
| `beryl billing plan` | Switch a paid workspace between Team and Business in place (prorated) | none |
|
|
367
370
|
| `beryl billing subscription` | Show the workspace's subscription | none |
|
|
368
371
|
| `beryl billing invoices` | List recent invoices | none |
|
|
369
372
|
| `beryl billing portal` | Get a Stripe billing-portal link for the workspace | none |
|
package/dist/commands/account.js
CHANGED
|
@@ -43,16 +43,53 @@ export const accountCommands = [
|
|
|
43
43
|
return { data: await ctx.client.post("/feedback", { message: arg(input, "message") }) };
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
name: "billing plans",
|
|
48
|
+
summary: "List the plans: price, cloud run minutes, storage, overage terms",
|
|
49
|
+
groupSummary: "Review a workspace's plan usage, subscription, and invoices; upgrade or switch plans.",
|
|
50
|
+
async run(ctx) {
|
|
51
|
+
return { data: await ctx.client.get("/billing/plans") };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
46
54
|
{
|
|
47
55
|
name: "billing usage",
|
|
48
|
-
summary: "Show plan usage:
|
|
56
|
+
summary: "Show plan usage: cloud run minutes, storage held, and this month's overage",
|
|
49
57
|
scope: "workspace",
|
|
50
|
-
groupSummary: "Review a workspace's plan usage, subscription, and invoices.",
|
|
51
58
|
async run(ctx, input) {
|
|
52
59
|
const ws = await ctx.requireWorkspace(input);
|
|
53
60
|
return { data: await ctx.client.get(`/workspaces/${ws}/billing/usage`) };
|
|
54
61
|
},
|
|
55
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "billing checkout",
|
|
65
|
+
summary: "Get a Stripe Checkout link that moves a Free workspace onto Team or Business",
|
|
66
|
+
scope: "workspace",
|
|
67
|
+
flags: [
|
|
68
|
+
{ name: "plan", type: "string", description: "team or business", required: true },
|
|
69
|
+
],
|
|
70
|
+
async run(ctx, input) {
|
|
71
|
+
const ws = await ctx.requireWorkspace(input);
|
|
72
|
+
const plan = flagStr(input, "plan");
|
|
73
|
+
if (plan !== "team" && plan !== "business")
|
|
74
|
+
throw new UsageError("--plan must be team or business");
|
|
75
|
+
return { data: await ctx.client.post(`/workspaces/${ws}/billing/checkout`, { plan }) };
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "billing plan",
|
|
80
|
+
summary: "Switch a paid workspace between Team and Business in place (prorated)",
|
|
81
|
+
scope: "workspace",
|
|
82
|
+
flags: [
|
|
83
|
+
{ name: "plan", type: "string", description: "team or business", required: true },
|
|
84
|
+
],
|
|
85
|
+
async run(ctx, input) {
|
|
86
|
+
const ws = await ctx.requireWorkspace(input);
|
|
87
|
+
const plan = flagStr(input, "plan");
|
|
88
|
+
if (plan !== "team" && plan !== "business")
|
|
89
|
+
throw new UsageError("--plan must be team or business");
|
|
90
|
+
return { data: await ctx.client.post(`/workspaces/${ws}/billing/plan`, { plan }) };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
56
93
|
{
|
|
57
94
|
name: "billing subscription",
|
|
58
95
|
summary: "Show the workspace's subscription",
|
|
@@ -82,11 +119,7 @@ export const accountCommands = [
|
|
|
82
119
|
scope: "workspace",
|
|
83
120
|
async run(ctx, input) {
|
|
84
121
|
const ws = await ctx.requireWorkspace(input);
|
|
85
|
-
return {
|
|
86
|
-
data: await ctx.client.post(`/workspaces/${ws}/billing/portal`, {
|
|
87
|
-
return_url: "https://beryl.so/settings/billing",
|
|
88
|
-
}),
|
|
89
|
-
};
|
|
122
|
+
return { data: await ctx.client.post(`/workspaces/${ws}/billing/portal`, {}) };
|
|
90
123
|
},
|
|
91
124
|
},
|
|
92
125
|
];
|
|
@@ -107,7 +107,7 @@ export const environmentCommands = [
|
|
|
107
107
|
name: "schedule list",
|
|
108
108
|
summary: "List the project's schedules",
|
|
109
109
|
scope: "project",
|
|
110
|
-
groupSummary: "Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).",
|
|
110
|
+
groupSummary: "Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own environment, cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).",
|
|
111
111
|
async run(ctx, input) {
|
|
112
112
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
113
113
|
return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/schedules`) };
|
|
@@ -120,15 +120,22 @@ export const environmentCommands = [
|
|
|
120
120
|
flags: [
|
|
121
121
|
...SCHEDULE_CADENCE_FLAGS,
|
|
122
122
|
{ name: "groups", type: "string", description: SCHEDULE_GROUPS_FLAG },
|
|
123
|
+
{
|
|
124
|
+
name: "env",
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Environment id to run against (default: the project's default environment)",
|
|
127
|
+
},
|
|
123
128
|
],
|
|
124
129
|
examples: [
|
|
125
130
|
"beryl schedule add --frequency daily --hour 6 --tz UTC",
|
|
131
|
+
"beryl schedule add --env <env-id> --frequency daily --hour 6 --tz UTC",
|
|
126
132
|
'beryl schedule add --groups "Smoke,Checkout" --frequency weekly --day 0 --hour 9 --minute 15 --tz UTC',
|
|
127
133
|
],
|
|
128
134
|
async run(ctx, input) {
|
|
129
135
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
130
136
|
return {
|
|
131
137
|
data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/schedules`, {
|
|
138
|
+
environment_id: flagStr(input, "env") ?? null,
|
|
132
139
|
frequency: flagStr(input, "frequency") ?? "daily",
|
|
133
140
|
day_of_week: flagNum(input, "day") ?? null,
|
|
134
141
|
run_hour: flagNum(input, "hour"),
|
|
@@ -141,12 +148,13 @@ export const environmentCommands = [
|
|
|
141
148
|
},
|
|
142
149
|
{
|
|
143
150
|
name: "schedule update",
|
|
144
|
-
summary: "Change a schedule's cadence, time, timezone, or
|
|
151
|
+
summary: "Change a schedule's cadence, time, timezone, groups, or environment",
|
|
145
152
|
scope: "project",
|
|
146
153
|
args: [{ name: "schedule-id", description: "Schedule id (see schedule list)", required: true }],
|
|
147
154
|
flags: [
|
|
148
155
|
...SCHEDULE_CADENCE_FLAGS,
|
|
149
156
|
{ name: "groups", type: "string", description: SCHEDULE_GROUPS_FLAG },
|
|
157
|
+
{ name: "env", type: "string", description: "Move the schedule to this environment id" },
|
|
150
158
|
{
|
|
151
159
|
name: "all-tests",
|
|
152
160
|
type: "boolean",
|
|
@@ -169,6 +177,7 @@ export const environmentCommands = [
|
|
|
169
177
|
: existing.group_ids;
|
|
170
178
|
return {
|
|
171
179
|
data: await ctx.client.put(`${projectPath(workspaceId, projectId)}/schedules/${scheduleId}`, {
|
|
180
|
+
...(flagStr(input, "env") ? { environment_id: flagStr(input, "env") } : {}),
|
|
172
181
|
frequency: flagStr(input, "frequency") ?? existing.frequency,
|
|
173
182
|
day_of_week: flagNum(input, "day") ?? existing.day_of_week,
|
|
174
183
|
run_hour: flagNum(input, "hour") ?? existing.run_hour,
|
package/dist/commands/runs.js
CHANGED
|
@@ -74,6 +74,12 @@ export const runCommands = [
|
|
|
74
74
|
},
|
|
75
75
|
{ name: "watch", type: "boolean", description: "Follow progress and exit non-zero on failure" },
|
|
76
76
|
{ name: "timeout", type: "number", description: "With --watch: max minutes to wait" },
|
|
77
|
+
{
|
|
78
|
+
name: "fail-fast",
|
|
79
|
+
type: "boolean",
|
|
80
|
+
description: "With --watch: cancel the run and exit 1 on the first failing test, instead of " +
|
|
81
|
+
"waiting for the rest of the suite. Built for CI gates; off by default",
|
|
82
|
+
},
|
|
77
83
|
{
|
|
78
84
|
name: "retries",
|
|
79
85
|
type: "number",
|
|
@@ -118,7 +124,10 @@ export const runCommands = [
|
|
|
118
124
|
}));
|
|
119
125
|
if (!flagBool(input, "watch"))
|
|
120
126
|
return { data: created };
|
|
121
|
-
return await watchRun(ctx, workspaceId, projectId, created.id, flagNum(input, "timeout")
|
|
127
|
+
return await watchRun(ctx, workspaceId, projectId, created.id, flagNum(input, "timeout"), {
|
|
128
|
+
failFast: flagBool(input, "fail-fast"),
|
|
129
|
+
ci: process.env.GITHUB_ACTIONS === "true",
|
|
130
|
+
});
|
|
122
131
|
},
|
|
123
132
|
},
|
|
124
133
|
{
|
|
@@ -575,10 +584,21 @@ export const runCommands = [
|
|
|
575
584
|
description: "Replays what already happened, then follows live. Exits 0 only if every test passed.",
|
|
576
585
|
scope: "project",
|
|
577
586
|
args: [{ name: "run-id", description: "Run id", required: true }],
|
|
578
|
-
flags: [
|
|
587
|
+
flags: [
|
|
588
|
+
{ name: "timeout", type: "number", description: "Max minutes to wait" },
|
|
589
|
+
{
|
|
590
|
+
name: "fail-fast",
|
|
591
|
+
type: "boolean",
|
|
592
|
+
description: "Cancel the run and exit 1 on the first failing test, instead of waiting for " +
|
|
593
|
+
"the rest of the suite. Built for CI gates; off by default",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
579
596
|
async run(ctx, input) {
|
|
580
597
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
581
|
-
return await watchRun(ctx, workspaceId, projectId, arg(input, "run-id"), flagNum(input, "timeout")
|
|
598
|
+
return await watchRun(ctx, workspaceId, projectId, arg(input, "run-id"), flagNum(input, "timeout"), {
|
|
599
|
+
failFast: flagBool(input, "fail-fast"),
|
|
600
|
+
ci: process.env.GITHUB_ACTIONS === "true",
|
|
601
|
+
});
|
|
582
602
|
},
|
|
583
603
|
},
|
|
584
604
|
{
|
package/dist/commands/watch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { CliError } from "../errors.js";
|
|
2
3
|
import { sseStream } from "../sse.js";
|
|
3
4
|
import { dim, green, red, statusColor, yellow } from "../output.js";
|
|
@@ -10,8 +11,29 @@ function withTimeout(minutes) {
|
|
|
10
11
|
const PROGRESS_WAIT_S = 25;
|
|
11
12
|
const HEARTBEAT_MS = 180_000;
|
|
12
13
|
const SETTLED = new Set(["passed", "failed", "errored", "cancelled"]);
|
|
14
|
+
const FAILING = new Set(["failed", "errored"]);
|
|
13
15
|
const TERMINAL_RUN = new Set(["completed", "failed", "cancelled"]);
|
|
14
16
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
17
|
+
const CI_ERROR_MAX_LEN = 4_000;
|
|
18
|
+
function runPageUrl(apiUrl, projectId, runId) {
|
|
19
|
+
if (!apiUrl)
|
|
20
|
+
return null;
|
|
21
|
+
let host;
|
|
22
|
+
try {
|
|
23
|
+
host = new URL(apiUrl).hostname;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const webapp = host === "api.beryl.so"
|
|
29
|
+
? "https://beryl.so"
|
|
30
|
+
: host === "dev.beryl.so"
|
|
31
|
+
? "https://preview.beryl.so"
|
|
32
|
+
: null;
|
|
33
|
+
return webapp ? `${webapp}/projects/${projectId}/runs/${runId}` : null;
|
|
34
|
+
}
|
|
35
|
+
const escapeAnnotationData = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
36
|
+
const escapeAnnotationProp = (s) => escapeAnnotationData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
15
37
|
function sleep(ms) {
|
|
16
38
|
return new Promise((r) => setTimeout(r, ms));
|
|
17
39
|
}
|
|
@@ -25,16 +47,18 @@ function settledCount(c) {
|
|
|
25
47
|
// Watching a cloud run is a poll, not a stream: each request parks server-side
|
|
26
48
|
// for up to PROGRESS_WAIT_S and returns when a test settles, so a dropped
|
|
27
49
|
// request or an api deploy costs one retry instead of the whole watch.
|
|
28
|
-
export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
50
|
+
export async function watchRun(ctx, ws, project, runId, timeoutMinutes, opts = {}) {
|
|
29
51
|
const path = projectPath(ws, project);
|
|
30
52
|
const deadline = timeoutMinutes ? Date.now() + timeoutMinutes * 60_000 : undefined;
|
|
31
53
|
const tty = Boolean(process.stderr.isTTY) && !ctx.json;
|
|
54
|
+
const ci = Boolean(opts.ci) && !ctx.json;
|
|
32
55
|
const printed = new Set();
|
|
33
56
|
let since = 0;
|
|
34
57
|
let backoffMs = 2_000;
|
|
35
58
|
let lastHeartbeat = Date.now();
|
|
36
59
|
let spin = 0;
|
|
37
60
|
let run = null;
|
|
61
|
+
let knownTotal = null;
|
|
38
62
|
const clearStatusLine = () => {
|
|
39
63
|
if (tty)
|
|
40
64
|
process.stderr.write("\r\u001b[2K");
|
|
@@ -47,13 +71,21 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
47
71
|
const mark = ok ? green("✓") : muted ? yellow("⚠") : red("✗");
|
|
48
72
|
const label = muted ? yellow(`${r.status} (quarantined)`) : statusColor(r.status);
|
|
49
73
|
const duration = r.duration_ms ? dim(` ${Math.round(r.duration_ms / 1000)}s`) : "";
|
|
50
|
-
const
|
|
74
|
+
const seq = ci ? dim(`${printed.size}/${knownTotal ?? "?"} `) : "";
|
|
75
|
+
const fullError = ci && !ok && !muted;
|
|
76
|
+
const errorText = r.error_message && !fullError ? ` ${String(r.error_message).slice(0, 120)}` : "";
|
|
51
77
|
const error = errorText ? (muted ? dim(errorText) : red(errorText)) : "";
|
|
52
|
-
ctx.err(`${mark} ${label} ${dim(r.title || r.id)}${duration}${error}`);
|
|
78
|
+
ctx.err(`${seq}${mark} ${label} ${dim(r.title || r.id)}${duration}${error}`);
|
|
79
|
+
if (fullError && r.error_message) {
|
|
80
|
+
const detail = String(r.error_message).slice(0, CI_ERROR_MAX_LEN);
|
|
81
|
+
ctx.err(red(detail.replace(/^/gm, " ")));
|
|
82
|
+
process.stdout.write(`::error title=${escapeAnnotationProp(r.title || r.id)}::${escapeAnnotationData(detail)}\n`);
|
|
83
|
+
}
|
|
53
84
|
};
|
|
54
85
|
if (!ctx.json)
|
|
55
86
|
ctx.err(dim(`watching run ${runId}`));
|
|
56
87
|
let lastCounters = {};
|
|
88
|
+
let failFastStop = false;
|
|
57
89
|
while (true) {
|
|
58
90
|
if (deadline && Date.now() > deadline) {
|
|
59
91
|
clearStatusLine();
|
|
@@ -72,7 +104,12 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
72
104
|
backoffMs = Math.min(backoffMs * 2, 30_000);
|
|
73
105
|
continue;
|
|
74
106
|
}
|
|
75
|
-
|
|
107
|
+
// Not every event carries counters, so an empty snapshot means "no news",
|
|
108
|
+
// not "zero settled": keep the last populated one.
|
|
109
|
+
if (progress.counters && Object.keys(progress.counters).length > 0)
|
|
110
|
+
lastCounters = progress.counters;
|
|
111
|
+
if (lastCounters.total)
|
|
112
|
+
knownTotal = lastCounters.total;
|
|
76
113
|
const version = Number(progress.version ?? 0);
|
|
77
114
|
const changed = version !== since || TERMINAL_RUN.has(progress.status);
|
|
78
115
|
since = version;
|
|
@@ -88,6 +125,8 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
88
125
|
backoffMs = Math.min(backoffMs * 2, 30_000);
|
|
89
126
|
continue;
|
|
90
127
|
}
|
|
128
|
+
if (run?.total_tests)
|
|
129
|
+
knownTotal = run.total_tests;
|
|
91
130
|
for (const r of run?.test_results ?? []) {
|
|
92
131
|
if (SETTLED.has(r.status) && !printed.has(r.id)) {
|
|
93
132
|
printed.add(r.id);
|
|
@@ -96,13 +135,30 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
96
135
|
ctx.out(JSON.stringify({ event: "test_completed", ...r }));
|
|
97
136
|
else
|
|
98
137
|
printRow(r);
|
|
138
|
+
if (opts.failFast && FAILING.has(r.status) && !r.quarantined) {
|
|
139
|
+
failFastStop = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (failFastStop) {
|
|
145
|
+
clearStatusLine();
|
|
146
|
+
if (!ctx.json)
|
|
147
|
+
ctx.err(red(`fail-fast: cancelling run ${runId}`));
|
|
148
|
+
try {
|
|
149
|
+
await ctx.client.post(`${path}/runs/${runId}/cancel`);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Best-effort: the cancel may race the worker or the run may already be
|
|
153
|
+
// terminal; the failure that triggered it decides the exit code anyway.
|
|
99
154
|
}
|
|
155
|
+
break;
|
|
100
156
|
}
|
|
101
157
|
if (run && TERMINAL_RUN.has(run.status))
|
|
102
158
|
break;
|
|
103
159
|
}
|
|
104
|
-
const done = settledCount(lastCounters);
|
|
105
|
-
const total = lastCounters.total
|
|
160
|
+
const done = Math.max(settledCount(lastCounters), printed.size);
|
|
161
|
+
const total = lastCounters.total || knownTotal || "?";
|
|
106
162
|
if (tty) {
|
|
107
163
|
spin = (spin + 1) % SPINNER.length;
|
|
108
164
|
const reds = (lastCounters.failed ?? 0) + (lastCounters.errored ?? 0);
|
|
@@ -125,7 +181,7 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
125
181
|
flaky: run?.summary?.flaky ?? 0,
|
|
126
182
|
healed: run?.summary?.healed ?? 0,
|
|
127
183
|
};
|
|
128
|
-
const finalStatus = run?.status ?? "unknown";
|
|
184
|
+
const finalStatus = failFastStop ? "failed" : (run?.status ?? "unknown");
|
|
129
185
|
// Quarantined reds are excluded from `failed` by the API (they land in their own
|
|
130
186
|
// counter), so they never reach this sum and never flip the exit code, but they
|
|
131
187
|
// are always reported, so a green build never silently hides a muted failure.
|
|
@@ -142,8 +198,44 @@ export async function watchRun(ctx, ws, project, runId, timeoutMinutes) {
|
|
|
142
198
|
: green(`${passed} passed`) + extra;
|
|
143
199
|
if (!ctx.json)
|
|
144
200
|
ctx.err(`\n${statusColor(finalStatus)}: ${summary}`);
|
|
201
|
+
const link = runPageUrl(ctx.client.baseUrl, project, runId);
|
|
202
|
+
if (ci && link)
|
|
203
|
+
ctx.err(dim(link));
|
|
204
|
+
if (ci && process.env.GITHUB_STEP_SUMMARY) {
|
|
205
|
+
const failures = (run?.test_results ?? []).filter((r) => FAILING.has(r.status) && !r.quarantined);
|
|
206
|
+
const md = [
|
|
207
|
+
`### Beryl run ${finalStatus}${failFastStop ? " (fail-fast)" : ""}`,
|
|
208
|
+
"",
|
|
209
|
+
"| passed | failed | errored | cancelled | quarantined | total |",
|
|
210
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
211
|
+
`| ${counters.passed} | ${counters.failed} | ${counters.errored} | ${counters.cancelled} | ${counters.quarantined} | ${counters.total} |`,
|
|
212
|
+
...(link ? ["", `[Open the run in Beryl](${link})`] : []),
|
|
213
|
+
...failures.flatMap((r) => [
|
|
214
|
+
"",
|
|
215
|
+
`**✗ ${r.title || r.id}**`,
|
|
216
|
+
"```",
|
|
217
|
+
String(r.error_message ?? "").slice(0, CI_ERROR_MAX_LEN),
|
|
218
|
+
"```",
|
|
219
|
+
]),
|
|
220
|
+
"",
|
|
221
|
+
];
|
|
222
|
+
try {
|
|
223
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md.join("\n"));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// The summary is decoration; never fail the watch over it.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
145
229
|
const exitCode = finalStatus === "completed" && failed === 0 ? 0 : 1;
|
|
146
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
data: {
|
|
232
|
+
run_id: runId,
|
|
233
|
+
status: finalStatus,
|
|
234
|
+
...(failFastStop ? { fail_fast: true } : {}),
|
|
235
|
+
...counters,
|
|
236
|
+
},
|
|
237
|
+
exitCode,
|
|
238
|
+
};
|
|
147
239
|
}
|
|
148
240
|
export async function watchExploration(ctx, ws, project, explorationId, timeoutMinutes) {
|
|
149
241
|
const signal = withTimeout(timeoutMinutes);
|
package/package.json
CHANGED