@shipvo/cli 0.1.3 → 0.1.5
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/adopt-bare.d.ts +39 -0
- package/dist/adopt-bare.js +236 -0
- package/dist/adopt.d.ts +26 -0
- package/dist/adopt.js +257 -0
- package/dist/agents.d.ts +14 -0
- package/dist/agents.js +124 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +75 -0
- package/dist/autohalt.d.ts +5 -0
- package/dist/autohalt.js +84 -0
- package/dist/ci.d.ts +3 -0
- package/dist/ci.js +42 -0
- package/dist/cli.js +142 -88
- package/dist/config.d.ts +10 -0
- package/dist/config.js +38 -10
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +132 -7
- package/dist/eas.d.ts +29 -0
- package/dist/eas.js +258 -0
- package/dist/ensure-playwright.d.ts +21 -0
- package/dist/ensure-playwright.js +64 -0
- package/dist/explore.d.ts +84 -0
- package/dist/explore.js +247 -0
- package/dist/help.d.ts +4 -2
- package/dist/help.js +39 -3
- package/dist/init.js +501 -17
- package/dist/native.d.ts +3 -0
- package/dist/native.js +39 -0
- package/dist/policy.d.ts +4 -0
- package/dist/policy.js +17 -0
- package/dist/preview-recorder.d.ts +1 -0
- package/dist/preview-recorder.js +62 -0
- package/dist/preview.d.ts +35 -0
- package/dist/preview.js +346 -0
- package/dist/promote.d.ts +9 -0
- package/dist/promote.js +153 -0
- package/dist/publish.d.ts +1 -1
- package/dist/publish.js +73 -29
- package/dist/report-md.d.ts +26 -0
- package/dist/report-md.js +58 -0
- package/dist/run-bridge.d.ts +7 -0
- package/dist/run-bridge.js +374 -0
- package/dist/run-protocol.d.ts +32 -0
- package/dist/run-protocol.js +22 -0
- package/dist/run-ws.d.ts +8 -0
- package/dist/run-ws.js +89 -0
- package/dist/run.d.ts +86 -0
- package/dist/run.js +301 -0
- package/dist/sim.d.ts +31 -0
- package/dist/sim.js +124 -5
- package/dist/status.d.ts +64 -0
- package/dist/status.js +147 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.js +297 -0
- package/dist/templates/capacitor/github-actions/build.yml +122 -0
- package/dist/test.d.ts +46 -0
- package/dist/test.js +190 -0
- package/package.json +10 -5
package/dist/publish.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PUBLISH_HELP = "Usage: shipvo publish [flags]\n\nBuild, sign and publish an over-the-air release.\n\nFlags (write a value after a space, not
|
|
1
|
+
export declare const PUBLISH_HELP = "Usage: shipvo publish [flags]\n\nBuild, sign and publish an over-the-air release.\n\nFlags (write a value after a space, not =; on/off flags can be bare, or true/false):\n --bands b0,b1,b2 Cohorts to publish to (default b0,b1,b2)\n --platform android|ios|all Platform to publish (default android)\n --release N Release number (default: the next one)\n --min-shell X.Y.Z Oldest app shell that can run this release (default: version in app.json)\n --hermes-bytecode-version V Refuse bundles built for another Hermes version, for example 96 or 96,98\n --max-bases N How many older releases to build patches from (default 6)\n --skip-export Reuse the last build in dist-ota/\n --skip-web Do not publish the web copy for the simulator\n --dry-run Build and sign, write files locally, send nothing\n --dry-run-policy Check the policy in shipvo.json for these --bands, print the result, publish nothing\n --out <dir> Where --dry-run writes files (default .shipvo/dry-run)\n --help Show this help\n";
|
|
2
2
|
export declare function publish(argv: string[]): Promise<void>;
|
|
3
3
|
/** Bytes in plain units: KB below 1 MB, so a small bundle never reads as 0.00 MB. */
|
|
4
4
|
export declare function formatSize(bytes: number): string;
|
package/dist/publish.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { sha256Hex, signBytes } from "./index.js";
|
|
8
8
|
import { checkBundle, parseDeclared } from "./hermes.js";
|
|
9
|
+
import { requiredNativeModules } from "./native.js";
|
|
10
|
+
import { checkPublishPolicy } from "./policy.js";
|
|
9
11
|
import { args as parse, kv as kvFor, loadConfig, seed } from "./config.js";
|
|
10
12
|
const MIME = { ".html": "text/html", ".js": "text/javascript", ".json": "application/json", ".css": "text/css", ".ico": "image/x-icon", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".ttf": "font/ttf", ".svg": "image/svg+xml" };
|
|
11
13
|
const walk = (d) => readdirSync(d).flatMap((f) => (statSync(join(d, f)).isDirectory() ? walk(join(d, f)) : [join(d, f)]));
|
|
@@ -43,6 +45,15 @@ function checkFlags(argv, known) {
|
|
|
43
45
|
throw new Error(`Unknown flag ${bad.join(", ")}.${hint} Known flags: ${known.map((k) => "--" + k).join(" ")}`);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
/** An on/off flag: bare or "true" is on, "false" is off. Anything else is an error, so a typo never flips it silently. */
|
|
49
|
+
function switchFlag(a, name) {
|
|
50
|
+
const v = a[name];
|
|
51
|
+
if (v === undefined || v === "false")
|
|
52
|
+
return false;
|
|
53
|
+
if (v === "true")
|
|
54
|
+
return true;
|
|
55
|
+
throw new Error(`--${name} is an on/off flag: use --${name}, --${name} true or --${name} false (got "${v}")`);
|
|
56
|
+
}
|
|
46
57
|
/** A store that only writes files under `dir` (key `p/app/1/b0` becomes `dir/p/app/1/b0`). It reads back only what this run wrote (kept in memory), never old files, so the iOS pass can find the web artifact and a dry run never needs a control plane. */
|
|
47
58
|
function localKv(dir) {
|
|
48
59
|
const root = resolve(dir);
|
|
@@ -63,21 +74,22 @@ function localKv(dir) {
|
|
|
63
74
|
},
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
|
-
const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "out"];
|
|
77
|
+
const PUBLISH_FLAGS = ["bands", "platform", "release", "min-shell", "hermes-bytecode-version", "max-bases", "skip-export", "skip-web", "dry-run", "dry-run-policy", "out"];
|
|
67
78
|
export const PUBLISH_HELP = `Usage: shipvo publish [flags]
|
|
68
79
|
|
|
69
80
|
Build, sign and publish an over-the-air release.
|
|
70
81
|
|
|
71
|
-
Flags (write a value after a space, not
|
|
82
|
+
Flags (write a value after a space, not =; on/off flags can be bare, or true/false):
|
|
72
83
|
--bands b0,b1,b2 Cohorts to publish to (default b0,b1,b2)
|
|
73
84
|
--platform android|ios|all Platform to publish (default android)
|
|
74
85
|
--release N Release number (default: the next one)
|
|
75
86
|
--min-shell X.Y.Z Oldest app shell that can run this release (default: version in app.json)
|
|
76
87
|
--hermes-bytecode-version V Refuse bundles built for another Hermes version, for example 96 or 96,98
|
|
77
88
|
--max-bases N How many older releases to build patches from (default 6)
|
|
78
|
-
--skip-export
|
|
79
|
-
--skip-web
|
|
89
|
+
--skip-export Reuse the last build in dist-ota/
|
|
90
|
+
--skip-web Do not publish the web copy for the simulator
|
|
80
91
|
--dry-run Build and sign, write files locally, send nothing
|
|
92
|
+
--dry-run-policy Check the policy in shipvo.json for these --bands, print the result, publish nothing
|
|
81
93
|
--out <dir> Where --dry-run writes files (default .shipvo/dry-run)
|
|
82
94
|
--help Show this help
|
|
83
95
|
`;
|
|
@@ -88,7 +100,17 @@ export async function publish(argv) {
|
|
|
88
100
|
}
|
|
89
101
|
checkFlags(argv, PUBLISH_FLAGS);
|
|
90
102
|
const a = parse(argv), cfg = loadConfig(), SEED = seed();
|
|
91
|
-
|
|
103
|
+
if (switchFlag(a, "dry-run-policy")) {
|
|
104
|
+
const verdict = checkPublishPolicy(cfg.policy, (a.bands || "b0,b1,b2").split(","));
|
|
105
|
+
if (verdict.ok)
|
|
106
|
+
console.log(cfg.policy === undefined ? "No policy in shipvo.json: nothing to check." : "Policy allows this publish.");
|
|
107
|
+
else {
|
|
108
|
+
console.error(`Refused by policy rule ${verdict.rule}: ${verdict.reason}`);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const dry = switchFlag(a, "dry-run"), skipExport = switchFlag(a, "skip-export"), skipWeb = switchFlag(a, "skip-web");
|
|
92
114
|
if (a.out !== undefined && !dry)
|
|
93
115
|
throw new Error("--out only works with --dry-run");
|
|
94
116
|
if (a.out === "true")
|
|
@@ -100,10 +122,24 @@ export async function publish(argv) {
|
|
|
100
122
|
if (a["hermes-bytecode-version"] === "true")
|
|
101
123
|
throw new Error("--hermes-bytecode-version needs a value: pass a value like 96 (or 96,98)");
|
|
102
124
|
const declaredHermes = parseDeclared(a["hermes-bytecode-version"] ?? cfg.hermesBytecodeVersion);
|
|
125
|
+
const skippedNative = [];
|
|
126
|
+
const nativeModules = requiredNativeModules(".", skippedNative);
|
|
127
|
+
if (skippedNative.length)
|
|
128
|
+
console.error(`native modules not recorded (the range is not a plain numeric semver range, so the control plane cannot check it): ${skippedNative.join(", ")}`);
|
|
129
|
+
if (dry)
|
|
130
|
+
console.error(`native modules this release requires: ${nativeModules ? Object.entries(nativeModules).map(([n, r]) => `${n}@${r}`).join(", ") : "none found"}`);
|
|
103
131
|
const bands = (a.bands || "b0,b1,b2").split(",");
|
|
132
|
+
if (dry) {
|
|
133
|
+
const verdict = checkPublishPolicy(cfg.policy, bands);
|
|
134
|
+
if (!verdict.ok) {
|
|
135
|
+
console.error(`Refused by policy rule ${verdict.rule}: ${verdict.reason}`);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
104
140
|
const platforms = platformsOf(a);
|
|
105
141
|
const release = Number(a.release || 0) || (await currentRelease(kv, APP, MAJOR)) + 1;
|
|
106
|
-
const summary = { app: APP, release, bands, min_shell_version: MIN_SHELL, ...(dry ? { dry_run: true, out: outDir } : {}) };
|
|
142
|
+
const summary = { app: APP, release, bands, min_shell_version: MIN_SHELL, ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(dry ? { dry_run: true, out: outDir } : {}) };
|
|
107
143
|
if (dry)
|
|
108
144
|
console.error(`dry run: nothing is sent anywhere; files go to ${outDir}. No patches are built offline. Release is ${release}${a.release ? "" : " (pass --release N to match the control plane)"}.`);
|
|
109
145
|
let totalBytes = 0;
|
|
@@ -111,7 +147,7 @@ export async function publish(argv) {
|
|
|
111
147
|
for (const pf of platforms) {
|
|
112
148
|
// Android published alone keeps the original dist-ota/ layout, so existing CI keeps working.
|
|
113
149
|
const root = platforms.length === 1 && pf === "android" ? "dist-ota" : `dist-ota/${pf}`;
|
|
114
|
-
if (
|
|
150
|
+
if (!skipExport)
|
|
115
151
|
execSync(`npx expo export --platform ${pf} --output-dir ${root} --clear`, { stdio: "inherit" });
|
|
116
152
|
const jsDir = `${root}/_expo/static/js/${pf}`;
|
|
117
153
|
const file = readdirSync(jsDir).find((f) => f.endsWith(".hbc")) || readdirSync(jsDir).find((f) => f.endsWith(".js"));
|
|
@@ -121,8 +157,8 @@ export async function publish(argv) {
|
|
|
121
157
|
const hermesVersion = checkBundle(file, bytes, declaredHermes); // fails before anything is uploaded
|
|
122
158
|
const name = bundleName(release, pf), newSha = sha256Hex(bytes);
|
|
123
159
|
await kv.put(`b/${APP}/${name}`, bytes, "application/octet-stream");
|
|
124
|
-
// Patches: from recent releases
|
|
125
|
-
//
|
|
160
|
+
// Patches: from recent releases and each shell's embedded bundle; devices verify the patched result against the signed sha256.
|
|
161
|
+
// Releases are per platform. Embedded-bundle records are shared under base/<app>/ and tagged with a platform (no tag means Android).
|
|
126
162
|
const patches = [];
|
|
127
163
|
{
|
|
128
164
|
const MAX_BASES = Number(a["max-bases"] || 6);
|
|
@@ -136,14 +172,14 @@ export async function publish(argv) {
|
|
|
136
172
|
.filter((h) => h && h.release < release).sort((x, y) => y.release - x.release).slice(0, MAX_BASES - 2);
|
|
137
173
|
for (const h of hist)
|
|
138
174
|
bases.set(h.sha256, `b/${APP}/${bundleName(h.release, pf)}`);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
175
|
+
for (const k of await kv.keys(`base/${APP}/`)) {
|
|
176
|
+
try {
|
|
177
|
+
const b = JSON.parse((await kv.get(k)));
|
|
178
|
+
if ((b.platform ?? "android") === pf)
|
|
143
179
|
bases.set(b.sha256, b.key);
|
|
144
|
-
}
|
|
145
|
-
catch { }
|
|
146
180
|
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
147
183
|
// Only look for bsdiff4 when there is an older bundle to patch from; a first release needs no patches.
|
|
148
184
|
let bsdiff = false;
|
|
149
185
|
if (bases.size > 0) {
|
|
@@ -181,10 +217,10 @@ export async function publish(argv) {
|
|
|
181
217
|
}
|
|
182
218
|
// The agent-facing web artifact of the same commit, served at /w/<app>/r<N>/ for the simulator (built once, with the first platform).
|
|
183
219
|
let web = null;
|
|
184
|
-
if (
|
|
185
|
-
web = await publishWeb(kv, cfg, APP, release);
|
|
220
|
+
if (!skipWeb && pf === platforms[0]) {
|
|
221
|
+
web = await publishWeb(kv, cfg, APP, release, dry ? outDir : undefined);
|
|
186
222
|
}
|
|
187
|
-
else if (
|
|
223
|
+
else if (!skipWeb) {
|
|
188
224
|
try {
|
|
189
225
|
web = JSON.parse((await kv.get(`h/${APP}/r${release}`)))?.web ?? null;
|
|
190
226
|
}
|
|
@@ -194,10 +230,10 @@ export async function publish(argv) {
|
|
|
194
230
|
for (const band of bands) {
|
|
195
231
|
const body = JSON.stringify({ schema_version: "1.0", app_id: APP, shell_major: MAJOR, band, ...(pf === "ios" ? { platform: "ios" } : {}), release, issued_at,
|
|
196
232
|
core: { version: `1.0.${release}`, sha256: newSha, size: bytes.length, url: `b/${APP}/${name}`, patches },
|
|
197
|
-
addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(web ? { web } : {}) });
|
|
233
|
+
addons: [], screens: [], min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), ...(web ? { web } : {}) });
|
|
198
234
|
await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
|
|
199
235
|
}
|
|
200
|
-
await kv.put(pf === "ios" ? `h/${APP}/ios-r${release}` : `h/${APP}/r${release}`, JSON.stringify({ release, issued_at, sha256: newSha, size: bytes.length, min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), web }), "application/json");
|
|
236
|
+
await kv.put(pf === "ios" ? `h/${APP}/ios-r${release}` : `h/${APP}/r${release}`, JSON.stringify({ release, issued_at, sha256: newSha, size: bytes.length, min_shell_version: MIN_SHELL, ...(hermesVersion ? { hermes_bytecode_version: hermesVersion } : {}), ...(nativeModules ? { required_native_modules: nativeModules } : {}), web }), "application/json");
|
|
201
237
|
totalBytes += bytes.length;
|
|
202
238
|
summary[pf] = { bytes: bytes.length, patches: patches.map((p) => ({ from: p.from_sha256.slice(0, 12), kb: +(p.size / 1024).toFixed(1) })) };
|
|
203
239
|
if (web)
|
|
@@ -235,7 +271,7 @@ export function dryRunSummary(release, bytes, outDir, info = { bases: 0, bsdiff:
|
|
|
235
271
|
lines.push(`Patches built: ${info.patches}.`);
|
|
236
272
|
return lines.join("\n");
|
|
237
273
|
}
|
|
238
|
-
async function publishWeb(kv, cfg, APP, release) {
|
|
274
|
+
async function publishWeb(kv, cfg, APP, release, outDir) {
|
|
239
275
|
execSync("PN_SIM=1 npx expo export --platform web --output-dir dist-web --clear", { stdio: "inherit" });
|
|
240
276
|
const base = `w/${APP}/r${release}`;
|
|
241
277
|
const files = [];
|
|
@@ -262,7 +298,9 @@ async function publishWeb(kv, cfg, APP, release) {
|
|
|
262
298
|
idx.releases = [...new Set([...idx.releases, release])].sort((x, y) => x - y);
|
|
263
299
|
idx.latest = Math.max(...idx.releases);
|
|
264
300
|
await kv.put(idxKey, JSON.stringify(idx), "application/json");
|
|
265
|
-
|
|
301
|
+
// Show where the files are on disk, relative to this folder: the URL path (/w/...) is not a folder.
|
|
302
|
+
const where = outDir ? (relative(process.cwd(), resolve(outDir, base)) || ".") + "/" : `/${base}/`;
|
|
303
|
+
console.error(`web artifact: ${files.length} files at ${where}`);
|
|
266
304
|
// Store listing assets (icon, screenshots) and the release note, read by shipvo.dev/apps and list_apps.
|
|
267
305
|
const L = cfg.listing ?? {};
|
|
268
306
|
const store = `w/${APP}/store`;
|
|
@@ -287,16 +325,17 @@ async function publishWeb(kv, cfg, APP, release) {
|
|
|
287
325
|
return web;
|
|
288
326
|
}
|
|
289
327
|
export async function rollback(argv) {
|
|
290
|
-
checkFlags(argv, ["to", "bands", "platform"]);
|
|
291
|
-
const a = parse(argv), cfg = loadConfig(), SEED = seed(), kv = kvFor(cfg);
|
|
328
|
+
checkFlags(argv, ["to", "bands", "platform", "dry-run"]);
|
|
329
|
+
const a = parse(argv), dry = a["dry-run"] !== undefined, cfg = loadConfig(), SEED = seed(), kv = kvFor(cfg);
|
|
292
330
|
const APP = cfg.appId, MAJOR = cfg.shellMajor, to = Number(a.to);
|
|
293
331
|
if (!to) {
|
|
294
|
-
console.error("Usage: shipvo rollback --to <release> [--bands b0,b1,b2] [--platform android|ios|all]");
|
|
332
|
+
console.error("Usage: shipvo rollback --to <release> [--bands b0,b1,b2] [--platform android|ios|all] [--dry-run]");
|
|
295
333
|
process.exit(2);
|
|
296
334
|
}
|
|
297
335
|
const release = (await currentRelease(kv, APP, MAJOR)) + 1, issued_at = new Date().toISOString();
|
|
298
336
|
let minShell = "1.0.0";
|
|
299
|
-
|
|
337
|
+
const platforms = platformsOf(a), bands = (a.bands || "b0,b1,b2").split(",");
|
|
338
|
+
for (const pf of platforms) {
|
|
300
339
|
const bytes = await kv.getBytes(`b/${APP}/${bundleName(to, pf)}`);
|
|
301
340
|
if (!bytes) {
|
|
302
341
|
console.error(`Release ${to} ${pf} bundle not found`);
|
|
@@ -309,11 +348,16 @@ export async function rollback(argv) {
|
|
|
309
348
|
web = h.web ?? undefined;
|
|
310
349
|
}
|
|
311
350
|
catch { }
|
|
312
|
-
for (const band of
|
|
351
|
+
for (const band of bands) {
|
|
313
352
|
const body = JSON.stringify({ schema_version: "1.0", app_id: APP, shell_major: MAJOR, band, ...(pf === "ios" ? { platform: "ios" } : {}), release, issued_at,
|
|
314
353
|
core: { version: `1.0.${release}`, sha256: sha256Hex(bytes), size: bytes.length, url: `b/${APP}/${bundleName(to, pf)}` }, addons: [], screens: [], min_shell_version: minShell, ...(web ? { web } : {}) });
|
|
315
|
-
|
|
354
|
+
if (!dry)
|
|
355
|
+
await kv.put(pointerKey(APP, MAJOR, band, pf), JSON.stringify({ body, signature: signBytes(new TextEncoder().encode(body), SEED) }), "application/json");
|
|
316
356
|
}
|
|
317
357
|
}
|
|
358
|
+
if (dry) {
|
|
359
|
+
console.log([`Rollback preview for ${APP}:`, ` Rolling back to the code of release ${to}`, ` New release number: ${release}`, ` Platforms: ${platforms.join(", ")}`, ` Bands: ${bands.join(", ")}`, ` Min shell version: ${minShell}`, `Dry run: nothing was written.`].join("\n"));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
318
362
|
console.log(JSON.stringify({ app: APP, rolled_back_to_code_of: to, new_release: release, min_shell_version: minShell, issued_at }));
|
|
319
363
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Step = {
|
|
2
|
+
kind?: string;
|
|
3
|
+
target?: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
enter?: boolean;
|
|
6
|
+
line?: number;
|
|
7
|
+
};
|
|
8
|
+
type Flow = {
|
|
9
|
+
name?: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
steps?: Step[];
|
|
12
|
+
failedStep?: Step | null;
|
|
13
|
+
message?: string | null;
|
|
14
|
+
screenshots?: string[];
|
|
15
|
+
screenshot?: string | null;
|
|
16
|
+
};
|
|
17
|
+
export type TestReport = {
|
|
18
|
+
app?: string | null;
|
|
19
|
+
commit?: string | null;
|
|
20
|
+
release?: string | null;
|
|
21
|
+
time?: string | null;
|
|
22
|
+
parityLimits?: string[];
|
|
23
|
+
flows?: Flow[];
|
|
24
|
+
};
|
|
25
|
+
export declare function renderReportMarkdown(report: TestReport): string;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const one = (s) => s.replace(/\s+/g, " ").trim();
|
|
2
|
+
/** Inline code that survives backticks in the text. */
|
|
3
|
+
function code(s) {
|
|
4
|
+
const t = one(s);
|
|
5
|
+
const fence = "`".repeat(Math.max(0, ...[...t.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
|
|
6
|
+
return `${fence}${t.startsWith("`") || t.endsWith("`") ? ` ${t} ` : t}${fence}`;
|
|
7
|
+
}
|
|
8
|
+
function stepText(s) {
|
|
9
|
+
const kind = s.kind ?? "step";
|
|
10
|
+
if (kind === "tap")
|
|
11
|
+
return `tap "${s.target ?? ""}"`;
|
|
12
|
+
if (kind === "type")
|
|
13
|
+
return `type "${s.text ?? ""}" into "${s.target ?? ""}"${s.enter ? " and press enter" : ""}`;
|
|
14
|
+
const arg = s.text ?? s.target;
|
|
15
|
+
return arg !== undefined ? `${kind} "${arg}"` : kind;
|
|
16
|
+
}
|
|
17
|
+
function block(s) {
|
|
18
|
+
const fence = "`".repeat(Math.max(2, ...[...s.matchAll(/`+/g)].map((m) => m[0].length)) + 1);
|
|
19
|
+
return `${fence}\n${s}\n${fence}`;
|
|
20
|
+
}
|
|
21
|
+
export function renderReportMarkdown(report) {
|
|
22
|
+
const flows = Array.isArray(report.flows) ? report.flows : [];
|
|
23
|
+
const failed = flows.filter((f) => f.status === "failed");
|
|
24
|
+
const out = [`# Shipvo test report: ${failed.length ? "failed" : "all flows passed"}`, ""];
|
|
25
|
+
const meta = [];
|
|
26
|
+
if (report.app)
|
|
27
|
+
meta.push(`- App: ${code(report.app)}`);
|
|
28
|
+
meta.push(`- Release: ${report.release ? code(report.release) : "not set"}`);
|
|
29
|
+
meta.push(`- Commit: ${report.commit ? code(report.commit) : "unknown"}`);
|
|
30
|
+
if (report.time)
|
|
31
|
+
meta.push(`- Time: ${report.time}`);
|
|
32
|
+
meta.push(`- Flows: ${flows.length - failed.length} passed, ${failed.length} failed`);
|
|
33
|
+
out.push(...meta, "");
|
|
34
|
+
if (!failed.length)
|
|
35
|
+
out.push(flows.length ? `All ${flows.length} flow${flows.length === 1 ? "" : "s"} passed.` : "No flows were run.", "");
|
|
36
|
+
for (const f of failed) {
|
|
37
|
+
out.push(`## Failing flow: ${code(f.name ?? "unnamed flow")}`, "");
|
|
38
|
+
const bad = f.failedStep;
|
|
39
|
+
if (bad)
|
|
40
|
+
out.push(`Failed step${bad.line ? ` (line ${bad.line})` : ""}: ${code(stepText(bad))}`, "");
|
|
41
|
+
if (f.message)
|
|
42
|
+
out.push("Error:", "", block(f.message), "");
|
|
43
|
+
const shots = [...(f.screenshots ?? []), ...(f.screenshot ? [f.screenshot] : [])];
|
|
44
|
+
if (shots.length)
|
|
45
|
+
out.push(`Screenshots: ${shots.map(code).join(", ")}`, "");
|
|
46
|
+
if (f.steps?.length) {
|
|
47
|
+
out.push("Steps:", "");
|
|
48
|
+
f.steps.forEach((s, i) => {
|
|
49
|
+
const mark = bad && s.line !== undefined && s.line === bad.line ? " ← failed" : "";
|
|
50
|
+
out.push(`${i + 1}. ${code(stepText(s))}${mark}`);
|
|
51
|
+
});
|
|
52
|
+
out.push("");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (report.parityLimits?.length)
|
|
56
|
+
out.push("## Parity limits", "", ...report.parityLimits.map((l) => `- ${one(l)}`), "");
|
|
57
|
+
return out.join("\n");
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared screen rules (`@shipvo/sim`), as source that the page can run. Same copy the simulator uses.
|
|
3
|
+
* Only plain `export function` / `export const` lines are supported: `export default` and `export {…}` would not be stripped.
|
|
4
|
+
*/
|
|
5
|
+
export declare function screenRulesSource(): string;
|
|
6
|
+
/** The script text served at `/__shipvo/bridge.js`. It reads the session token from its own `data-token` attribute and then removes the attribute. */
|
|
7
|
+
export declare function bridgeScript(rules?: string): string;
|