@sdods/cli 0.9.0 → 0.10.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/dist/.tsbuildinfo +1 -1
- package/dist/commands/browsers.js +1 -1
- package/dist/commands/init.js +4 -4
- package/dist/commands/report.js +161 -14
- package/dist/commands/run.js +8 -20
- package/dist/commands/schedule.js +1 -1
- package/dist/commands/trace.js +2 -2
- package/dist/gates.d.ts +11 -0
- package/dist/gates.js +29 -0
- package/package.json +10 -10
- package/skills/sdods-start-ui/SKILL.md +5 -4
|
@@ -75,7 +75,7 @@ export async function browserStatuses(names = DOWNLOADED_BROWSERS) {
|
|
|
75
75
|
return names.map((name) => {
|
|
76
76
|
const engine = ENGINE_OF[name] ?? 'chromium';
|
|
77
77
|
const channel = CHANNEL_OF[name] ?? null;
|
|
78
|
-
let executable
|
|
78
|
+
let executable;
|
|
79
79
|
try {
|
|
80
80
|
executable = channel ? channelExecutable(channel) : pw[engine].executablePath();
|
|
81
81
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -140,12 +140,12 @@ export async function initWorkspace(target, flags) {
|
|
|
140
140
|
// 1.63 is the floor, not a preference: below it an npm-installed workspace records every
|
|
141
141
|
// Gherkin step SKIPPED in messages.ndjson (see runner-compat.ts).
|
|
142
142
|
'@playwright/test': flags.link && flags.pm === 'bun' ? 'link:@playwright/test' : '^1.63.0',
|
|
143
|
-
'playwright-bdd': flags.link && flags.pm === 'bun' ? 'link:playwright-bdd' : '^9.2.
|
|
143
|
+
'playwright-bdd': flags.link && flags.pm === 'bun' ? 'link:playwright-bdd' : '^9.2.1',
|
|
144
144
|
},
|
|
145
145
|
devDependencies: {
|
|
146
|
-
'@types/node': '^22.
|
|
147
|
-
typescript: '~
|
|
148
|
-
tsx: '^4.23.
|
|
146
|
+
'@types/node': '^22.20.2',
|
|
147
|
+
typescript: '~6.0.3',
|
|
148
|
+
tsx: '^4.23.13',
|
|
149
149
|
},
|
|
150
150
|
trustedDependencies: [
|
|
151
151
|
'@playwright/test',
|
package/dist/commands/report.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
3
4
|
import { execa } from 'execa';
|
|
4
5
|
import pc from 'picocolors';
|
|
5
|
-
import { runFiles } from '@sdods/contracts';
|
|
6
|
-
import { SdodsError } from '@sdods/core';
|
|
6
|
+
import { legacyRunFiles, runFiles, } from '@sdods/contracts';
|
|
7
|
+
import { SdodsError, collectGateEvidence, evaluateGates, gateReportsFromRunnerJson, gateTotalsFromRunnerStats, hasGates, ingestGateJudge, } from '@sdods/core';
|
|
7
8
|
import { createContext } from '../context.js';
|
|
9
|
+
import { gateFailedError, printGates } from '../gates.js';
|
|
8
10
|
import { json, ok, out, table, warn } from '../ui.js';
|
|
9
11
|
function artifactsRoot(rootDir) {
|
|
10
12
|
return resolve(rootDir, process.env.SDODS_ARTIFACTS_DIR ?? '.sdods/runs');
|
|
@@ -35,6 +37,82 @@ async function openPath(p) {
|
|
|
35
37
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
36
38
|
await execa(cmd, [p], { shell: process.platform === 'win32' }).catch(() => undefined);
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The process `report merge` judges: `--process` (with `-p`, or the project in a run.json), or the
|
|
42
|
+
* process a run.json names when it declares gates. `undefined` when there is nothing to judge.
|
|
43
|
+
*/
|
|
44
|
+
function resolveMergeProcess(ctx, input) {
|
|
45
|
+
const manifest = input.manifestDirs
|
|
46
|
+
.map((d) => readJsonFile(join(d, runFiles.manifest)))
|
|
47
|
+
.find(Boolean);
|
|
48
|
+
const name = input.process ?? manifest?.process;
|
|
49
|
+
if (!name)
|
|
50
|
+
return undefined;
|
|
51
|
+
const slug = input.project ?? manifest?.projectSlug;
|
|
52
|
+
if (!slug)
|
|
53
|
+
throw new SdodsError('CONFIG_INVALID', `--process ${name} needs the project that defines it, and no run.json names one.`, {
|
|
54
|
+
hint: `Pass -p <slug>: sdods report merge --process ${name} -p <slug> <dirs...>`,
|
|
55
|
+
exitCode: 2,
|
|
56
|
+
});
|
|
57
|
+
let proc;
|
|
58
|
+
try {
|
|
59
|
+
proc = ctx.registry.processOf(slug, name);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// Named explicitly, an unknown process is an error. Inferred from a run.json this checkout
|
|
63
|
+
// cannot resolve, it only means there is nothing here to judge it by; the merge still runs.
|
|
64
|
+
if (input.process)
|
|
65
|
+
throw e;
|
|
66
|
+
warn(`run.json names process "${name}" of ${slug}, which this checkout does not define: gates not judged.`);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (!hasGates(proc.gates)) {
|
|
70
|
+
// Named explicitly, a process without gates is a mistake worth saying; inferred, it is not.
|
|
71
|
+
if (input.process)
|
|
72
|
+
warn(`process "${name}" of ${slug} declares no gates, so there is nothing to judge.`);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return { slug, process: proc };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A merge workspace for `playwright merge-reports`: the blob reports of every directory copied into
|
|
79
|
+
* one (it reads exactly one directory; prefixed, because two runs' shard 1 carry the same file
|
|
80
|
+
* name), and a merge config that pins the root the report paths are relative to. Blobs recorded
|
|
81
|
+
* under different checkouts, such as CI's Playwright container (`/__w/…`) and its bare runner
|
|
82
|
+
* (`/home/runner/work/…`), otherwise refuse to merge ("recorded with different test directories").
|
|
83
|
+
*/
|
|
84
|
+
function mergeWorkspace(dirs, rootDir) {
|
|
85
|
+
const dir = mkdtempSync(join(tmpdir(), 'sdods-merge-'));
|
|
86
|
+
const reports = join(dir, 'reports');
|
|
87
|
+
mkdirSync(reports);
|
|
88
|
+
dirs.forEach((from, i) => {
|
|
89
|
+
for (const name of readdirSync(from))
|
|
90
|
+
if (name.endsWith('.zip'))
|
|
91
|
+
copyFileSync(join(from, name), join(reports, `${i + 1}-${name}`));
|
|
92
|
+
});
|
|
93
|
+
const config = join(dir, 'merge.config.mjs');
|
|
94
|
+
writeFileSync(config, `export default ${JSON.stringify({ testDir: rootDir })};\n`);
|
|
95
|
+
return { reports, config, dir };
|
|
96
|
+
}
|
|
97
|
+
/** The error Playwright printed, rather than the last lines of its stack. */
|
|
98
|
+
function mergeFailureHint(stderr) {
|
|
99
|
+
const lines = (stderr ?? '').split('\n');
|
|
100
|
+
const at = lines.findIndex((l) => /^\s*Error\b/.test(l));
|
|
101
|
+
const picked = at >= 0 ? lines.slice(at, at + 12).filter((l) => !/^\s+at /.test(l)) : lines.slice(-3);
|
|
102
|
+
return picked.join(' ').replace(/\s+/g, ' ').trim() || undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Writes the merged verdict where `sdods run --process` writes one, so ingest records it the same way. */
|
|
105
|
+
function recordMergedGates(runDir, gates) {
|
|
106
|
+
writeFileSync(join(runDir, runFiles.gates), JSON.stringify(gates, null, 2));
|
|
107
|
+
const manifestFile = join(runDir, runFiles.manifest);
|
|
108
|
+
const manifest = readJsonFile(manifestFile);
|
|
109
|
+
if (manifest)
|
|
110
|
+
writeFileSync(manifestFile, JSON.stringify({ ...manifest, gatesPassed: gates.passed }, null, 2));
|
|
111
|
+
const summaryFile = join(runDir, runFiles.summary);
|
|
112
|
+
const summary = readJsonFile(summaryFile);
|
|
113
|
+
if (summary)
|
|
114
|
+
writeFileSync(summaryFile, JSON.stringify({ ...summary, gates }, null, 2));
|
|
115
|
+
}
|
|
38
116
|
export function register(program) {
|
|
39
117
|
const report = program
|
|
40
118
|
.command('report')
|
|
@@ -172,12 +250,25 @@ export function register(program) {
|
|
|
172
250
|
artifactsRoot: root,
|
|
173
251
|
replace: Boolean(opts.replace),
|
|
174
252
|
env: opts.env,
|
|
253
|
+
// A run with no gates.json whose process declares gates is judged here, as the run would have been.
|
|
254
|
+
gates: ingestGateJudge({
|
|
255
|
+
manifest: manifest ?? m.readManifest(join(root, opts.runId)),
|
|
256
|
+
runDir: join(root, opts.runId),
|
|
257
|
+
processOf: (slug, name) => ctx.registry.processOf(slug, name),
|
|
258
|
+
}),
|
|
175
259
|
});
|
|
176
260
|
if (ctx.opts.json)
|
|
177
261
|
return json(res);
|
|
178
262
|
ok(`Ingested run ${res.runId} (${res.projectSlug}): ${res.scenarios} scenario(s), ${res.attempts} attempt(s), ${res.steps} step(s), ${res.artifacts} artifact(s), ${res.healEvents} heal event(s)`);
|
|
179
263
|
const t = res.totals;
|
|
180
264
|
out(`${pc.green(`${t.passed} passed`)} ${pc.red(`${t.failed} failed`)} ${pc.yellow(`${t.flaky} flaky`)} ${pc.dim(`${t.skipped} skipped`)} → ${res.status}`);
|
|
265
|
+
if (res.gates)
|
|
266
|
+
out(`gates · process ${res.gates.process}: ${res.gates.passed
|
|
267
|
+
? pc.green('passed')
|
|
268
|
+
: pc.red(`FAILED (${res.gates.rows
|
|
269
|
+
.filter((r) => !r.passed)
|
|
270
|
+
.map((r) => r.gate)
|
|
271
|
+
.join(', ')})`)}`);
|
|
181
272
|
if (res.filesSkipped.length)
|
|
182
273
|
out(pc.dim(`skipped (already ingested): ${res.filesSkipped.join(', ')}`));
|
|
183
274
|
if (res.parseErrors)
|
|
@@ -189,9 +280,11 @@ export function register(program) {
|
|
|
189
280
|
});
|
|
190
281
|
report
|
|
191
282
|
.command('merge <dirs...>')
|
|
192
|
-
.description('Merge sharded run reports into one HTML report and JUnit file')
|
|
283
|
+
.description('Merge sharded run reports into one HTML report and JUnit file, and judge process gates on the whole run')
|
|
193
284
|
.option('--run <id>', 'run id to write the merged report into (default: latest)')
|
|
194
285
|
.option('--reporter <list>', 'reporters for the merged output', 'html,junit')
|
|
286
|
+
.option('--process <name>', "evaluate this process's gates over the merged shards; exit 1 when one is not met (default: the process in run.json, when it declares gates)")
|
|
287
|
+
.option('-p, --project <slug>', 'project that defines --process (default: from run.json)')
|
|
195
288
|
.action(async (dirs, opts, cmd) => {
|
|
196
289
|
const ctx = createContext(cmd);
|
|
197
290
|
const root = artifactsRoot(ctx.rootDir);
|
|
@@ -212,28 +305,82 @@ export function register(program) {
|
|
|
212
305
|
exitCode: 2,
|
|
213
306
|
});
|
|
214
307
|
}
|
|
308
|
+
const shardDirs = dirs.map((d) => resolve(ctx.rootDir, d));
|
|
309
|
+
// The run directory each shard report sits in (`<run>/shard-reports`) holds that shard's
|
|
310
|
+
// @a11y and @perf scenario reports, which is the gate evidence the merged report lacks.
|
|
311
|
+
const shardRunDirs = shardDirs
|
|
312
|
+
.filter((d) => [runFiles.shardReports, legacyRunFiles.shardReports].includes(basename(d)))
|
|
313
|
+
.map((d) => dirname(d));
|
|
314
|
+
const gated = resolveMergeProcess(ctx, {
|
|
315
|
+
process: opts.process,
|
|
316
|
+
project: opts.project,
|
|
317
|
+
manifestDirs: [runDir, ...shardRunDirs],
|
|
318
|
+
});
|
|
319
|
+
const reporters = String(opts.reporter)
|
|
320
|
+
.split(',')
|
|
321
|
+
.map((r) => r.trim())
|
|
322
|
+
.filter(Boolean);
|
|
323
|
+
const mergedJson = join(runDir, runFiles.mergedResults);
|
|
324
|
+
if (gated && !reporters.includes('json'))
|
|
325
|
+
reporters.push('json');
|
|
326
|
+
if (gated)
|
|
327
|
+
mkdirSync(runDir, { recursive: true });
|
|
215
328
|
// Shard reports are produced by `sdods run --reporter blob --shard i/n`; merging them
|
|
216
329
|
// rebuilds one HTML report and JUnit file for the whole matrix.
|
|
330
|
+
const workspace = mergeWorkspace(shardDirs, ctx.rootDir);
|
|
217
331
|
const args = [
|
|
218
332
|
'playwright',
|
|
219
333
|
'merge-reports',
|
|
334
|
+
'--config',
|
|
335
|
+
workspace.config,
|
|
220
336
|
'--reporter',
|
|
221
|
-
|
|
222
|
-
|
|
337
|
+
reporters.join(','),
|
|
338
|
+
workspace.reports,
|
|
223
339
|
];
|
|
224
340
|
const res = await execa('npx', args, {
|
|
225
341
|
cwd: ctx.rootDir,
|
|
226
342
|
reject: false,
|
|
227
|
-
env: {
|
|
343
|
+
env: {
|
|
344
|
+
...process.env,
|
|
345
|
+
PLAYWRIGHT_HTML_OUTPUT_DIR: join(runDir, runFiles.htmlReport),
|
|
346
|
+
...(gated ? { PLAYWRIGHT_JSON_OUTPUT_FILE: mergedJson } : {}),
|
|
347
|
+
},
|
|
228
348
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
349
|
+
let gates;
|
|
350
|
+
try {
|
|
351
|
+
if (res.exitCode !== 0) {
|
|
352
|
+
throw new SdodsError('RUN_FAILED', `Merging shard reports failed (exit ${res.exitCode}).`, { hint: mergeFailureHint(res.stderr) });
|
|
353
|
+
}
|
|
354
|
+
if (gated) {
|
|
355
|
+
const merged = readJsonFile(mergedJson);
|
|
356
|
+
gates = evaluateGates({
|
|
357
|
+
process: gated.process.name,
|
|
358
|
+
gates: gated.process.gates,
|
|
359
|
+
totals: gateTotalsFromRunnerStats(merged?.stats),
|
|
360
|
+
// The shards' run directories, and the reports attached in the blobs themselves: a
|
|
361
|
+
// download of the shard reports alone still carries its evidence.
|
|
362
|
+
evidence: collectGateEvidence([runDir, ...shardRunDirs], gateReportsFromRunnerJson(merged)),
|
|
363
|
+
});
|
|
364
|
+
recordMergedGates(runDir, gates);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
rmSync(workspace.dir, { recursive: true, force: true });
|
|
233
369
|
}
|
|
234
370
|
if (ctx.opts.json)
|
|
235
|
-
|
|
236
|
-
|
|
371
|
+
json({
|
|
372
|
+
runId,
|
|
373
|
+
merged: dirs.length,
|
|
374
|
+
htmlReport: join(runDir, runFiles.htmlReport),
|
|
375
|
+
...(gates ? { gates } : {}),
|
|
376
|
+
});
|
|
377
|
+
else {
|
|
378
|
+
ok(`Merged ${dirs.length} shard report dir(s) into ${join(runDir, runFiles.htmlReport)}`);
|
|
379
|
+
if (gates)
|
|
380
|
+
printGates(gates);
|
|
381
|
+
}
|
|
382
|
+
if (gates && !gates.passed)
|
|
383
|
+
throw gateFailedError(gates, join(runDir, runFiles.gates));
|
|
237
384
|
});
|
|
238
385
|
report
|
|
239
386
|
.command('traceability')
|
package/dist/commands/run.js
CHANGED
|
@@ -7,6 +7,7 @@ import { SdodsError, CLI_OVERRIDES_ENV, VERSION, checkPoolCapacity, collectGateE
|
|
|
7
7
|
import { analyzeChangeImpact } from '@sdods/mcp';
|
|
8
8
|
import { createContext } from '../context.js';
|
|
9
9
|
import { browserStatuses } from './browsers.js';
|
|
10
|
+
import { gateFailedError, printGates } from '../gates.js';
|
|
10
11
|
import { maybeNotify, notifyRun } from '../notify.js';
|
|
11
12
|
import { installedPlaywrightVersion, stepResultsWarning } from '../runner-compat.js';
|
|
12
13
|
import { collect, json, out, parseIntFlag, renderError, table, warn } from '../ui.js';
|
|
@@ -447,12 +448,13 @@ export async function runCommand(flags, cmd) {
|
|
|
447
448
|
const summary = readSummary(runDir, runId, exitCode, Date.now() - started);
|
|
448
449
|
// Process gates are judged here, from the run that just finished, before anything publishes it.
|
|
449
450
|
// One shard of several holds only part of the run, so its pass rate and its a11y/perf evidence
|
|
450
|
-
// are not the run's; a sharded process is gated where the shards are merged
|
|
451
|
+
// are not the run's; a sharded process is gated where the shards are merged
|
|
452
|
+
// (`sdods report merge --process`), not per shard.
|
|
451
453
|
const shardTotal = cfg.runtime.shard?.total ?? 1;
|
|
452
454
|
let gates;
|
|
453
455
|
if (proc && hasGates(proc.gates) && exitCode !== 130) {
|
|
454
456
|
if (shardTotal > 1) {
|
|
455
|
-
warn(`process "${proc.name}" gates are not evaluated on shard ${cfg.runtime.shard?.current}/${shardTotal}: one shard is not the whole run
|
|
457
|
+
warn(`process "${proc.name}" gates are not evaluated on shard ${cfg.runtime.shard?.current}/${shardTotal}: one shard is not the whole run. Judge them on the merged shards with \`sdods report merge --process ${proc.name}\`.`);
|
|
456
458
|
}
|
|
457
459
|
else {
|
|
458
460
|
gates = evaluateGates({
|
|
@@ -550,17 +552,8 @@ export async function runCommand(flags, cmd) {
|
|
|
550
552
|
json({ runId, runDir, exitCode: finalExit, summary, manifest, notify, gates });
|
|
551
553
|
}
|
|
552
554
|
else {
|
|
553
|
-
if (gates)
|
|
554
|
-
|
|
555
|
-
out(pc.bold(`gates · process ${gates.process}`));
|
|
556
|
-
table(gates.rows.map((r) => ({
|
|
557
|
-
gate: r.gate,
|
|
558
|
-
threshold: r.threshold,
|
|
559
|
-
actual: r.actual,
|
|
560
|
-
result: r.passed ? 'pass' : 'FAIL',
|
|
561
|
-
detail: r.detail ?? '',
|
|
562
|
-
})));
|
|
563
|
-
}
|
|
555
|
+
if (gates)
|
|
556
|
+
printGates(gates);
|
|
564
557
|
const t = summary?.totals;
|
|
565
558
|
out('');
|
|
566
559
|
out(`${finalExit === 0 ? pc.green('✔ passed') : finalExit === 130 ? pc.yellow('■ cancelled') : finalExit === 2 ? pc.yellow('■ nothing ran') : pc.red('✖ failed')} ${t ? `${t.passed} passed, ${t.failed} failed, ${t.skipped} skipped, ${t.flaky} flaky` : ''} ${pc.dim(`(${Math.round((Date.now() - started) / 1000)}s)`)}`);
|
|
@@ -568,13 +561,8 @@ export async function runCommand(flags, cmd) {
|
|
|
568
561
|
out(pc.dim(`html report: ${join(runDir, runFiles.htmlReport, 'index.html')}`));
|
|
569
562
|
out(pc.dim(`dashboard: ${join(runDir, runFiles.dashboard, 'index.html')}`));
|
|
570
563
|
}
|
|
571
|
-
if (gates && !gates.passed)
|
|
572
|
-
|
|
573
|
-
renderError(new SdodsError('GATE_FAILED', `Process "${gates.process}" did not meet ${failed.length} gate(s): ${failed.map((r) => `${r.gate} (${r.actual}, needs ${r.threshold})`).join('; ')}.`, {
|
|
574
|
-
hint: `The verdict is in ${join(runDir, runFiles.gates)}. Gates are set under processes[].gates in sdods.project.yaml.`,
|
|
575
|
-
docsPath: '/docs/guides/processes-and-testing-types',
|
|
576
|
-
}), Boolean(ctx.opts.json));
|
|
577
|
-
}
|
|
564
|
+
if (gates && !gates.passed)
|
|
565
|
+
renderError(gateFailedError(gates, join(runDir, runFiles.gates)), Boolean(ctx.opts.json));
|
|
578
566
|
return finalExit;
|
|
579
567
|
}
|
|
580
568
|
/**
|
|
@@ -279,7 +279,7 @@ export function register(program) {
|
|
|
279
279
|
if (opts.target === 'github') {
|
|
280
280
|
const file = join(ctx.rootDir, '.github', 'workflows', `scheduled-${e.project}-${e.name}.yml`);
|
|
281
281
|
mkdirSync(join(ctx.rootDir, '.github', 'workflows'), { recursive: true });
|
|
282
|
-
writeFileSync(file, `name: scheduled ${e.project} ${e.name}\non:\n schedule:\n - cron: '${e.cron}'\n workflow_dispatch: {}\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@
|
|
282
|
+
writeFileSync(file, `name: scheduled ${e.project} ${e.name}\non:\n schedule:\n - cron: '${e.cron}'\n workflow_dispatch: {}\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: oven-sh/setup-bun@v2\n - uses: actions/setup-node@v7\n with: { node-version: 22 }\n - run: bun install\n - run: bun run sdods browsers install --with-deps\n - run: bun run sdods run ${e.args.join(' ')} --trigger schedule --run-id sched-${e.name}-\${{ github.run_id }}\n env:\n SDODS_SERVER_URL: \${{ secrets.SDODS_SERVER_URL }}\n SDODS_TOKEN: \${{ secrets.SDODS_TOKEN }}\n - uses: actions/upload-artifact@v7\n if: always()\n with: { name: sdods-${e.project}-${e.name}, path: .sdods/runs }\n`);
|
|
283
283
|
written.push(file);
|
|
284
284
|
}
|
|
285
285
|
else if (opts.target === 'launchd') {
|
package/dist/commands/trace.js
CHANGED
|
@@ -56,8 +56,8 @@ export function register(program) {
|
|
|
56
56
|
.option('--list', 'list trace files instead of opening them')
|
|
57
57
|
.action(async (zip, opts, cmd) => {
|
|
58
58
|
const ctx = createContext(cmd);
|
|
59
|
-
let zips
|
|
60
|
-
let source
|
|
59
|
+
let zips;
|
|
60
|
+
let source;
|
|
61
61
|
if (zip) {
|
|
62
62
|
const p = resolvePath(ctx.opts.cwd ?? process.cwd(), zip);
|
|
63
63
|
if (!existsSync(p))
|
package/dist/gates.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GateResult } from '@sdods/contracts';
|
|
2
|
+
import { SdodsError } from '@sdods/core';
|
|
3
|
+
/**
|
|
4
|
+
* How a process gate verdict reads on the terminal, and the error a breach fails with. Shared by
|
|
5
|
+
* `sdods run --process` (an unsharded run) and `sdods report merge --process` (the shards of one),
|
|
6
|
+
* so a gate looks the same wherever it was judged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function printGates(gates: GateResult): void;
|
|
9
|
+
/** The `GATE_FAILED` error for a verdict that did not pass. Exit 1: a gate not met is a failure. */
|
|
10
|
+
export declare function gateFailedError(gates: GateResult, verdictFile: string): SdodsError;
|
|
11
|
+
//# sourceMappingURL=gates.d.ts.map
|
package/dist/gates.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { SdodsError } from '@sdods/core';
|
|
3
|
+
import { out, table } from './ui.js';
|
|
4
|
+
/**
|
|
5
|
+
* How a process gate verdict reads on the terminal, and the error a breach fails with. Shared by
|
|
6
|
+
* `sdods run --process` (an unsharded run) and `sdods report merge --process` (the shards of one),
|
|
7
|
+
* so a gate looks the same wherever it was judged.
|
|
8
|
+
*/
|
|
9
|
+
export function printGates(gates) {
|
|
10
|
+
out('');
|
|
11
|
+
out(pc.bold(`gates · process ${gates.process}`));
|
|
12
|
+
table(gates.rows.map((r) => ({
|
|
13
|
+
gate: r.gate,
|
|
14
|
+
threshold: r.threshold,
|
|
15
|
+
actual: r.actual,
|
|
16
|
+
result: r.passed ? 'pass' : 'FAIL',
|
|
17
|
+
detail: r.detail ?? '',
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
20
|
+
/** The `GATE_FAILED` error for a verdict that did not pass. Exit 1: a gate not met is a failure. */
|
|
21
|
+
export function gateFailedError(gates, verdictFile) {
|
|
22
|
+
const failed = gates.rows.filter((r) => !r.passed);
|
|
23
|
+
return new SdodsError('GATE_FAILED', `Process "${gates.process}" did not meet ${failed.length} gate(s): ${failed.map((r) => `${r.gate} (${r.actual}, needs ${r.threshold})`).join('; ')}.`, {
|
|
24
|
+
hint: `The verdict is in ${verdictFile}. Gates are set under processes[].gates in sdods.project.yaml.`,
|
|
25
|
+
docsPath: '/docs/guides/processes-and-testing-types',
|
|
26
|
+
exitCode: 1,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=gates.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -24,22 +24,22 @@
|
|
|
24
24
|
"node": ">=22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@sdods/contracts": "0.
|
|
28
|
-
"@sdods/core": "0.
|
|
27
|
+
"@sdods/contracts": "0.10.0",
|
|
28
|
+
"@sdods/core": "0.10.0",
|
|
29
29
|
"commander": "^15.0.0",
|
|
30
30
|
"execa": "^10.0.1",
|
|
31
31
|
"picocolors": "^1.1.1",
|
|
32
32
|
"semver": "^7.8.5",
|
|
33
|
-
"yaml": "^2.9.
|
|
34
|
-
"zod": "^4.5
|
|
33
|
+
"yaml": "^2.9.1",
|
|
34
|
+
"zod": "^4.6.5",
|
|
35
35
|
"@playwright/test": "^1.63.0",
|
|
36
36
|
"playwright-core": "^1.63.0",
|
|
37
|
-
"@sdods/integrations": "0.
|
|
38
|
-
"@sdods/mcp": "0.
|
|
39
|
-
"@sdods/agents": "0.
|
|
40
|
-
"@sdods/db": "0.
|
|
37
|
+
"@sdods/integrations": "0.10.0",
|
|
38
|
+
"@sdods/mcp": "0.10.0",
|
|
39
|
+
"@sdods/agents": "0.10.0",
|
|
40
|
+
"@sdods/db": "0.10.0",
|
|
41
41
|
"csv-parse": "^7.0.2",
|
|
42
|
-
"@sdods/server": "0.
|
|
42
|
+
"@sdods/server": "0.10.0"
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://sdods.com",
|
|
45
45
|
"bugs": {
|
|
@@ -161,11 +161,12 @@ Defaults: **`http://127.0.0.1:4444`** for the CLI, **`http://localhost:8080`** f
|
|
|
161
161
|
image binds `0.0.0.0:8080`). `--open` launches the browser (`open` / `start` / `xdg-open` by
|
|
162
162
|
platform). `--host 0.0.0.0` exposes it on the LAN — only do that deliberately.
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
The image is published for `linux/amd64` and `linux/arm64`, so the same command runs natively on
|
|
165
|
+
Apple silicon and arm64 servers. Tags up to `0.9.0` carry amd64 only, and Docker refuses that pull
|
|
166
|
+
on arm64 rather than emulating; for those, add the platform flag:
|
|
166
167
|
|
|
167
168
|
```bash
|
|
168
|
-
docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server
|
|
169
|
+
docker run --rm --platform linux/amd64 -p 8080:8080 -v sdods-data:/data ghcr.io/siri1410/sdods-server:0.9.0
|
|
169
170
|
```
|
|
170
171
|
|
|
171
172
|
If the run must come from a source checkout, `bun run web:build` is not optional — see
|
|
@@ -278,7 +279,7 @@ the API running **without** the UI bundle — that is the source-checkout case,
|
|
|
278
279
|
| Fresh install, but `/login` says "ask an admin" | the login page does not redirect to `/setup` | open the `/setup?token=…` URL from the `serve` output, or `sdods users create --admin` |
|
|
279
280
|
| Global CLI errors on a repo project (`CONFIG_INVALID`, unknown browser) | version skew between the global CLI and the checkout | use `bun run sdods` inside a checkout; upgrade the global one otherwise |
|
|
280
281
|
| `sdods: command not found` after installing | the bin dir is not on PATH — `~/.local/bin`, or `%LOCALAPPDATA%\SDODS\bin` on Windows | add it, or re-run the installer with `--modify-path` / `-ModifyPath` |
|
|
281
|
-
| Docker: `no matching manifest for linux/arm64/v8` | amd64-only
|
|
282
|
+
| Docker: `no matching manifest for linux/arm64/v8` | an amd64-only tag (`0.9.0` or earlier) on Apple silicon or arm64 | pull a newer tag, or add `--platform linux/amd64` |
|
|
282
283
|
| UI loads, runs never start | browser engines missing | `sdods browsers install -b chromium` (the positional `sdods browsers install chromium` also works on current versions, and failed with `too many arguments` before). Bare `sdods browsers install` installs all engines; on Linux `--with-deps` needs root |
|
|
283
284
|
| Node too old | SDODS needs Node 22+ | `nvm use 22` / `fnm use 22`. On macOS/Linux the installer also takes `--install-node`; **`install.ps1` has no such flag** — it exits and tells you to run `winget install OpenJS.NodeJS.LTS`, `scoop install nodejs-lts` or `nvm install 22` |
|
|
284
285
|
| Anything else | — | `sdods doctor` first; it reports what it found, not what it expected |
|