@treeseed/sdk 0.12.43 → 0.12.45
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/guarantees/index.d.ts +83 -0
- package/dist/guarantees/index.js +688 -86
- package/dist/hosting/graph.js +40 -6
- package/dist/operations/services/git-workflow.d.ts +16 -1
- package/dist/operations/services/git-workflow.js +65 -5
- package/dist/operations/services/github-api.js +0 -19
- package/dist/operations/services/hosted-service-checks.js +17 -1
- package/dist/operations/services/live-hosted-service-checks.d.ts +4 -0
- package/dist/operations/services/live-hosted-service-checks.js +7 -4
- package/dist/operations/services/local-cleanup.js +3 -7
- package/dist/operations/services/package-adapters.d.ts +14 -0
- package/dist/operations/services/package-adapters.js +36 -3
- package/dist/operations/services/package-artifacts.d.ts +37 -0
- package/dist/operations/services/package-artifacts.js +99 -0
- package/dist/operations/services/railway-deploy.js +78 -18
- package/dist/operations/services/railway-source-policy.d.ts +19 -0
- package/dist/operations/services/railway-source-policy.js +66 -0
- package/dist/operations/services/repository-save-orchestrator.js +12 -5
- package/dist/platform/desired-state.js +2 -3
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/providers/railway-iac.d.ts +2 -0
- package/dist/reconcile/providers/railway-iac.js +31 -3
- package/dist/scenes/builtin-plugins.js +36 -5
- package/dist/scenes/device-matrix.js +2 -0
- package/dist/scenes/environment.js +1 -1
- package/dist/scenes/runner.js +28 -16
- package/dist/scenes/schema.js +31 -2
- package/dist/scenes/types.d.ts +25 -2
- package/dist/scenes/visual-audit-fixtures.js +9 -3
- package/dist/workflow/operations.d.ts +27 -92
- package/dist/workflow/operations.js +236 -144
- package/dist/workflow/runs.d.ts +1 -0
- package/dist/workflow/runs.js +57 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/dist/workflow.d.ts +2 -0
- package/package.json +4 -1
|
@@ -15,6 +15,19 @@ function mailpitApiUrl(mailpitUrl, pathname) {
|
|
|
15
15
|
function stringValue(value) {
|
|
16
16
|
return typeof value === "string" ? value : "";
|
|
17
17
|
}
|
|
18
|
+
function shortRuntimeHash(value) {
|
|
19
|
+
let hash = 2166136261;
|
|
20
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
21
|
+
hash ^= value.charCodeAt(index);
|
|
22
|
+
hash = Math.imul(hash, 16777619);
|
|
23
|
+
}
|
|
24
|
+
return (hash >>> 0).toString(36).padStart(7, "0").slice(0, 10);
|
|
25
|
+
}
|
|
26
|
+
function sceneRuntimeValue(value, context) {
|
|
27
|
+
const runSlug = context.runId.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 48);
|
|
28
|
+
const runShort = shortRuntimeHash(context.runId);
|
|
29
|
+
return value.replace(/\{\{\s*runId\s*\}\}/gu, context.runId).replace(/\{\{\s*runSlug\s*\}\}/gu, runSlug).replace(/\{\{\s*runShort\s*\}\}/gu, runShort);
|
|
30
|
+
}
|
|
18
31
|
function mailpitMessageId(value) {
|
|
19
32
|
if (!value || typeof value !== "object") return "";
|
|
20
33
|
const record = value;
|
|
@@ -57,6 +70,16 @@ function extractConfirmationUrl(body) {
|
|
|
57
70
|
const relative = body.match(/(?:\/auth\/confirm-email\?[^"' <>\n]+|\/team-invites\/[^"' <>\n]+\/accept)/u)?.[0];
|
|
58
71
|
return relative ?? null;
|
|
59
72
|
}
|
|
73
|
+
function resolveMailpitConfirmationUrl(confirmationUrl, context) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(confirmationUrl);
|
|
76
|
+
if (parsed.pathname === "/auth/confirm-email" || /^\/team-invites\/[^/]+\/accept$/u.test(parsed.pathname)) {
|
|
77
|
+
return context.resolveUrl(`${parsed.pathname}${parsed.search}${parsed.hash}`);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
return confirmationUrl.startsWith("/") ? context.resolveUrl(confirmationUrl) : confirmationUrl;
|
|
82
|
+
}
|
|
60
83
|
async function sleep(ms) {
|
|
61
84
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
85
|
}
|
|
@@ -68,7 +91,11 @@ async function navigateScenePage(page, url) {
|
|
|
68
91
|
let lastError = null;
|
|
69
92
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
70
93
|
try {
|
|
71
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
94
|
+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
95
|
+
const status = response?.status();
|
|
96
|
+
if (typeof status === "number" && status >= 400) {
|
|
97
|
+
throw sceneErrorDiagnostic("scene.navigation_http_error", `Navigation to ${response?.url() ?? url} returned HTTP ${status}.`, "workflow.action.goto");
|
|
98
|
+
}
|
|
72
99
|
return;
|
|
73
100
|
} catch (error) {
|
|
74
101
|
lastError = error;
|
|
@@ -76,7 +103,7 @@ async function navigateScenePage(page, url) {
|
|
|
76
103
|
await sleep(500 * attempt);
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
|
-
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
|
|
106
|
+
throw lastError && typeof lastError === "object" && "code" in lastError ? lastError : lastError instanceof Error ? lastError : new Error(String(lastError ?? `Navigation failed for ${url}`));
|
|
80
107
|
}
|
|
81
108
|
async function assertionReport(kind, action, selector) {
|
|
82
109
|
const startedAt = /* @__PURE__ */ new Date();
|
|
@@ -132,7 +159,7 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
132
159
|
if (!("fill" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected fill action.", "workflow.action.fill")] };
|
|
133
160
|
const locator = context.resolveSelector(action.fill);
|
|
134
161
|
await locator.waitFor({ state: "visible", timeout: 1e4 });
|
|
135
|
-
await locator.fill(action.fill.value);
|
|
162
|
+
await locator.fill(sceneRuntimeValue(action.fill.value, context));
|
|
136
163
|
return { ok: true, diagnostics: [] };
|
|
137
164
|
}
|
|
138
165
|
},
|
|
@@ -208,7 +235,11 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
208
235
|
summary: "Confirm the latest local Mailpit email by navigating the browser to its confirmation link.",
|
|
209
236
|
async run({ action, step, context }) {
|
|
210
237
|
if (!("mailpitConfirmLatest" in action)) return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.invalid_action", "Expected mailpitConfirmLatest action.", `workflow.${step.id}.action.mailpitConfirmLatest`)] };
|
|
211
|
-
const
|
|
238
|
+
const raw = action.mailpitConfirmLatest;
|
|
239
|
+
const mailpitUrl = sceneRuntimeValue(raw.mailpitUrl, context);
|
|
240
|
+
const email = sceneRuntimeValue(raw.email, context);
|
|
241
|
+
const subjectIncludes = raw.subjectIncludes ? sceneRuntimeValue(raw.subjectIncludes, context) : void 0;
|
|
242
|
+
const { displayInboxSeconds, displayMessageSeconds } = raw;
|
|
212
243
|
try {
|
|
213
244
|
const listResponse = await fetch(mailpitApiUrl(mailpitUrl, "/api/v1/messages"));
|
|
214
245
|
if (!listResponse.ok) {
|
|
@@ -233,7 +264,7 @@ function createBuiltInTreeseedScenePlugins() {
|
|
|
233
264
|
if (!confirmationUrl) {
|
|
234
265
|
return { ok: false, diagnostics: [sceneErrorDiagnostic("scene.mailpit_confirm_link_not_found", `No confirmation link was found in Mailpit message ${id}.`, `workflow.${step.id}.action.mailpitConfirmLatest`)] };
|
|
235
266
|
}
|
|
236
|
-
const resolvedUrl = confirmationUrl
|
|
267
|
+
const resolvedUrl = resolveMailpitConfirmationUrl(confirmationUrl, context);
|
|
237
268
|
const mailpitBase = mailpitUrl.endsWith("/") ? mailpitUrl : `${mailpitUrl}/`;
|
|
238
269
|
if (displayInboxSeconds && displayInboxSeconds > 0) {
|
|
239
270
|
const search = new URL("search", mailpitBase);
|
|
@@ -63,6 +63,8 @@ async function runTreeseedSceneDeviceMatrix(input) {
|
|
|
63
63
|
scene: input.scene,
|
|
64
64
|
environment: input.environment,
|
|
65
65
|
device,
|
|
66
|
+
browser: input.browser,
|
|
67
|
+
authRole: input.authRole,
|
|
66
68
|
record: input.record,
|
|
67
69
|
artifactMode: input.artifactMode,
|
|
68
70
|
mode: input.mode,
|
|
@@ -39,7 +39,7 @@ async function prepareTreeseedSceneEnvironment(input) {
|
|
|
39
39
|
baseUrl = healthUrl(existing);
|
|
40
40
|
} else {
|
|
41
41
|
try {
|
|
42
|
-
const result = await startTreeseedManagedDev({ cwd: input.projectRoot, surfaces: "web,api
|
|
42
|
+
const result = await startTreeseedManagedDev({ cwd: input.projectRoot, surfaces: "web,api", webRuntime: "local", env: input.env });
|
|
43
43
|
instances = result.instances;
|
|
44
44
|
const web = result.instances.find((entry) => entry.surface === "web" || entry.id === "web");
|
|
45
45
|
started = Boolean(web && running(web));
|
package/dist/scenes/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { sceneActionKind, sceneExpectationKinds } from "./schema.js";
|
|
4
4
|
import { planTreeseedScene, validateTreeseedScene } from "./planner.js";
|
|
@@ -126,6 +126,7 @@ function planForInput(input, validation) {
|
|
|
126
126
|
}
|
|
127
127
|
const scene = validation.scene;
|
|
128
128
|
const environment = input.environment ?? scene.target.environment;
|
|
129
|
+
const browser = input.browser ?? scene.target.browser;
|
|
129
130
|
const workflowSteps = scene.workflow.map((step) => ({
|
|
130
131
|
id: step.id,
|
|
131
132
|
title: step.title,
|
|
@@ -145,7 +146,7 @@ function planForInput(input, validation) {
|
|
|
145
146
|
title: scene.title,
|
|
146
147
|
environment,
|
|
147
148
|
baseUrl: scene.target.baseUrl,
|
|
148
|
-
browser
|
|
149
|
+
browser,
|
|
149
150
|
viewport: scene.target.viewport,
|
|
150
151
|
workflowSteps,
|
|
151
152
|
enabledActions: actionIds,
|
|
@@ -174,6 +175,20 @@ function planForInput(input, validation) {
|
|
|
174
175
|
function canContinueAfterFailure(scene, step) {
|
|
175
176
|
return scene.runtime.mode === "demo" || scene.runtime.mode === "training" || step.demoOnly === true || step.continueOnFailure === true || scene.runtime.failure.continueOnFailure === true;
|
|
176
177
|
}
|
|
178
|
+
function sceneWithRunOverrides(scene, input) {
|
|
179
|
+
if (!input.authRole) return scene;
|
|
180
|
+
return {
|
|
181
|
+
...scene,
|
|
182
|
+
setup: {
|
|
183
|
+
...scene.setup,
|
|
184
|
+
auth: {
|
|
185
|
+
...scene.setup.auth ?? {},
|
|
186
|
+
required: input.authRole !== "anonymous",
|
|
187
|
+
role: scene.setup.auth?.role ?? input.authRole
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
177
192
|
async function runTreeseedScene(input) {
|
|
178
193
|
const startedAt = now();
|
|
179
194
|
const validation = validationForInput(input);
|
|
@@ -188,7 +203,8 @@ async function runTreeseedScene(input) {
|
|
|
188
203
|
diagnostics: validation.diagnostics
|
|
189
204
|
});
|
|
190
205
|
}
|
|
191
|
-
const scene = validation.scene;
|
|
206
|
+
const scene = sceneWithRunOverrides(validation.scene, input);
|
|
207
|
+
const browser = input.browser ?? scene.target.browser;
|
|
192
208
|
const deviceResolution = resolveTreeseedSceneDeviceProfile({ scene, device: input.device });
|
|
193
209
|
if (!deviceResolution.profile && deviceResolution.diagnostics.some((entry) => entry.severity === "error")) {
|
|
194
210
|
return reportFromBlock({
|
|
@@ -197,7 +213,7 @@ async function runTreeseedScene(input) {
|
|
|
197
213
|
runId: input.runId ?? null,
|
|
198
214
|
startedAt,
|
|
199
215
|
environment: input.environment ?? scene.target.environment,
|
|
200
|
-
browser
|
|
216
|
+
browser,
|
|
201
217
|
diagnostics: deviceResolution.diagnostics
|
|
202
218
|
});
|
|
203
219
|
}
|
|
@@ -214,7 +230,7 @@ async function runTreeseedScene(input) {
|
|
|
214
230
|
runId: input.runId ?? null,
|
|
215
231
|
startedAt,
|
|
216
232
|
environment: plan.environment,
|
|
217
|
-
browser
|
|
233
|
+
browser,
|
|
218
234
|
device,
|
|
219
235
|
diagnostics: plan.diagnostics
|
|
220
236
|
});
|
|
@@ -263,7 +279,7 @@ async function runTreeseedScene(input) {
|
|
|
263
279
|
runId: paths.runId,
|
|
264
280
|
startedAt,
|
|
265
281
|
environment: plan.environment,
|
|
266
|
-
browser
|
|
282
|
+
browser,
|
|
267
283
|
device,
|
|
268
284
|
diagnostics: [...plan.diagnostics, ...setupDiagnostics],
|
|
269
285
|
artifacts,
|
|
@@ -289,7 +305,7 @@ async function runTreeseedScene(input) {
|
|
|
289
305
|
runId: paths.runId,
|
|
290
306
|
startedAt,
|
|
291
307
|
environment: plan.environment,
|
|
292
|
-
browser
|
|
308
|
+
browser,
|
|
293
309
|
device,
|
|
294
310
|
diagnostics: [...plan.diagnostics, ...setupDiagnostics, ...baseUrl.diagnostics],
|
|
295
311
|
artifacts,
|
|
@@ -338,7 +354,7 @@ async function runTreeseedScene(input) {
|
|
|
338
354
|
let sessionClosed = false;
|
|
339
355
|
try {
|
|
340
356
|
session = await adapter.launch({
|
|
341
|
-
browser
|
|
357
|
+
browser,
|
|
342
358
|
viewport: capture.viewport,
|
|
343
359
|
videoSize: capture.videoSize,
|
|
344
360
|
recordVideoDir: videoDir,
|
|
@@ -380,7 +396,7 @@ async function runTreeseedScene(input) {
|
|
|
380
396
|
timeline.push("network", entry, currentStepId);
|
|
381
397
|
});
|
|
382
398
|
if (tracePath) await session.startTracing?.();
|
|
383
|
-
if (scene.setup.auth?.role && scene.setup.auth.role !== "anonymous") {
|
|
399
|
+
if (scene.setup.auth?.role && scene.setup.auth.role !== "anonymous" && scene.setup.auth.seedOnly !== true) {
|
|
384
400
|
const signInDiagnostics = await signInTreeseedSceneVisualAuditRole({
|
|
385
401
|
page: session.page,
|
|
386
402
|
baseUrl: baseUrl.baseUrl,
|
|
@@ -446,6 +462,7 @@ async function runTreeseedScene(input) {
|
|
|
446
462
|
projectRoot: input.projectRoot,
|
|
447
463
|
scene,
|
|
448
464
|
environment: plan.environment,
|
|
465
|
+
runId: paths.runId,
|
|
449
466
|
session,
|
|
450
467
|
baseUrl: baseUrl.baseUrl,
|
|
451
468
|
timeline,
|
|
@@ -507,15 +524,10 @@ async function runTreeseedScene(input) {
|
|
|
507
524
|
await session.page.screenshot({ path: viewportScreenshotPath, fullPage: false });
|
|
508
525
|
artifacts.viewportScreenshotPaths?.push(viewportScreenshotPath);
|
|
509
526
|
timeline.push("screenshot.viewport", { path: viewportScreenshotPath }, step.id);
|
|
510
|
-
if (screenshotPath && recordingVideo) {
|
|
511
|
-
copyFileSync(viewportScreenshotPath, screenshotPath);
|
|
512
|
-
artifacts.screenshotPaths.push(screenshotPath);
|
|
513
|
-
timeline.push("screenshot", { path: screenshotPath, captureKind: "viewport-copy" }, step.id);
|
|
514
|
-
}
|
|
515
527
|
} catch {
|
|
516
528
|
}
|
|
517
529
|
}
|
|
518
|
-
if (screenshotPath
|
|
530
|
+
if (screenshotPath) {
|
|
519
531
|
try {
|
|
520
532
|
await session.page.screenshot({ path: screenshotPath, fullPage: true });
|
|
521
533
|
artifacts.screenshotPaths.push(screenshotPath);
|
|
@@ -637,7 +649,7 @@ async function runTreeseedScene(input) {
|
|
|
637
649
|
durationMs: duration(startedAt, finishedAt),
|
|
638
650
|
environment: plan.environment,
|
|
639
651
|
baseUrl: baseUrl.baseUrl,
|
|
640
|
-
browser
|
|
652
|
+
browser,
|
|
641
653
|
device,
|
|
642
654
|
capture,
|
|
643
655
|
workflowStatus,
|
package/dist/scenes/schema.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TREESEED_SCENE_SCHEMA_VERSION
|
|
7
7
|
} from "./types.js";
|
|
8
8
|
const FILESYSTEM_SAFE_SCENE_ID = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
9
|
-
const TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set(["schemaVersion", "id", "title", "description", "audience", "mode", "target", "devices", "setup", "artifacts", "workflow", "chapters", "overlays", "diagrams", "render", "runtime", "training", "visualAudit", "xScenario"]);
|
|
9
|
+
const TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set(["schemaVersion", "id", "title", "description", "audience", "journey", "mode", "target", "devices", "setup", "artifacts", "workflow", "chapters", "overlays", "diagrams", "render", "runtime", "training", "visualAudit", "xScenario"]);
|
|
10
10
|
const FILESYSTEM_SAFE_CHECKPOINT_ID = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
11
11
|
const DIAGRAM_PLACEMENTS = ["overlay", "interstitial", "standalone"];
|
|
12
12
|
const CAPTION_FORMATS = ["vtt", "srt"];
|
|
@@ -108,6 +108,34 @@ function stringArrayField(record, field, path, diagnostics) {
|
|
|
108
108
|
});
|
|
109
109
|
return strings;
|
|
110
110
|
}
|
|
111
|
+
function stateRefArray(record, field, path, diagnostics) {
|
|
112
|
+
const value = arrayField(record, field, path, diagnostics);
|
|
113
|
+
if (!value) return void 0;
|
|
114
|
+
const refs = [];
|
|
115
|
+
value.forEach((entry, index) => {
|
|
116
|
+
const entryPath = `${path}.${field}[${index}]`;
|
|
117
|
+
if (!isRecord(entry)) {
|
|
118
|
+
diagnostics.push(sceneErrorDiagnostic("scene.invalid_state_ref", `Expected ${field} entry to be an object.`, entryPath));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
refs.push({ key: requireString(entry, "key", entryPath, diagnostics), kind: requireString(entry, "kind", entryPath, diagnostics) });
|
|
122
|
+
});
|
|
123
|
+
return refs;
|
|
124
|
+
}
|
|
125
|
+
function parseJourney(record, diagnostics) {
|
|
126
|
+
const journey = objectField(record, "journey", "manifest", diagnostics);
|
|
127
|
+
if (!journey) return void 0;
|
|
128
|
+
const kind = optionalString(journey, "kind");
|
|
129
|
+
if (kind && !["service", "page", "visual-audit"].includes(kind)) diagnostics.push(sceneErrorDiagnostic("scene.invalid_journey_kind", `Unsupported journey kind: ${kind}.`, "journey.kind"));
|
|
130
|
+
return {
|
|
131
|
+
kind: kind === "page" || kind === "visual-audit" ? kind : "service",
|
|
132
|
+
proves: stringArrayField(journey, "proves", "journey", diagnostics),
|
|
133
|
+
minimumSteps: positiveNumberField(journey, "minimumSteps", void 0, "journey", diagnostics),
|
|
134
|
+
requiresInteractiveAction: booleanField(journey, "requiresInteractiveAction", false, "journey", diagnostics),
|
|
135
|
+
producesState: stateRefArray(journey, "producesState", "journey", diagnostics),
|
|
136
|
+
consumesState: stateRefArray(journey, "consumesState", "journey", diagnostics)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
111
139
|
function enumArrayField(record, field, allowed, defaultValue, path, diagnostics) {
|
|
112
140
|
const value = record[field];
|
|
113
141
|
if (value === void 0) return defaultValue;
|
|
@@ -379,7 +407,7 @@ function parseSetup(value, targetEnvironment, diagnostics) {
|
|
|
379
407
|
const dev = objectField(record, "dev", "setup", diagnostics);
|
|
380
408
|
if (dev) setup.dev = { required: booleanField(dev, "required", false, "setup.dev", diagnostics), command: optionalString(dev, "command"), reuseExisting: booleanField(dev, "reuseExisting", true, "setup.dev", diagnostics) };
|
|
381
409
|
const auth = objectField(record, "auth", "setup", diagnostics);
|
|
382
|
-
if (auth) setup.auth = { profile: optionalString(auth, "profile"), required: booleanField(auth, "required", false, "setup.auth", diagnostics), ...optionalString(auth, "role") ? { role: optionalString(auth, "role") } : {} };
|
|
410
|
+
if (auth) setup.auth = { profile: optionalString(auth, "profile"), required: booleanField(auth, "required", false, "setup.auth", diagnostics), seedOnly: booleanField(auth, "seedOnly", false, "setup.auth", diagnostics), ...optionalString(auth, "role") ? { role: optionalString(auth, "role") } : {} };
|
|
383
411
|
const seed = objectField(record, "seed", "setup", diagnostics);
|
|
384
412
|
if (seed) {
|
|
385
413
|
const environments = (arrayField(seed, "environments", "setup.seed", diagnostics) ?? [targetEnvironment]).map((entry, index) => parseEnvironment(entry, `setup.seed.environments[${index}]`, diagnostics, targetEnvironment));
|
|
@@ -970,6 +998,7 @@ function parseTreeseedSceneManifest(value, diagnostics) {
|
|
|
970
998
|
title,
|
|
971
999
|
description: optionalString(value, "description"),
|
|
972
1000
|
audience: stringArrayField(value, "audience", "manifest", diagnostics),
|
|
1001
|
+
journey: parseJourney(value, diagnostics),
|
|
973
1002
|
mode,
|
|
974
1003
|
target,
|
|
975
1004
|
devices,
|
package/dist/scenes/types.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export type TreeseedSceneSetup = {
|
|
|
99
99
|
profile?: string;
|
|
100
100
|
required: boolean;
|
|
101
101
|
role?: TreeseedSceneVisualAuditRole;
|
|
102
|
+
seedOnly?: boolean;
|
|
102
103
|
};
|
|
103
104
|
seed?: {
|
|
104
105
|
name?: string;
|
|
@@ -393,6 +394,20 @@ export type TreeseedSceneManifest = {
|
|
|
393
394
|
title: string;
|
|
394
395
|
description?: string;
|
|
395
396
|
audience: string[];
|
|
397
|
+
journey?: {
|
|
398
|
+
kind: 'service' | 'page' | 'visual-audit';
|
|
399
|
+
proves?: string[];
|
|
400
|
+
minimumSteps?: number;
|
|
401
|
+
requiresInteractiveAction?: boolean;
|
|
402
|
+
producesState?: Array<{
|
|
403
|
+
key: string;
|
|
404
|
+
kind: string;
|
|
405
|
+
}>;
|
|
406
|
+
consumesState?: Array<{
|
|
407
|
+
key: string;
|
|
408
|
+
kind: string;
|
|
409
|
+
}>;
|
|
410
|
+
};
|
|
396
411
|
mode: TreeseedSceneMode;
|
|
397
412
|
target: TreeseedSceneTarget;
|
|
398
413
|
devices: TreeseedSceneDeviceConfig;
|
|
@@ -469,6 +484,8 @@ export type TreeseedSceneRunOptions = {
|
|
|
469
484
|
scene: string | TreeseedSceneManifest;
|
|
470
485
|
environment?: TreeseedSceneEnvironment;
|
|
471
486
|
device?: TreeseedSceneDeviceProfileId;
|
|
487
|
+
browser?: TreeseedSceneBrowser;
|
|
488
|
+
authRole?: TreeseedSceneVisualAuditRole;
|
|
472
489
|
record?: boolean;
|
|
473
490
|
artifactMode?: 'full' | 'screenshots';
|
|
474
491
|
runId?: string;
|
|
@@ -491,6 +508,8 @@ export type TreeseedSceneDeviceMatrixOptions = {
|
|
|
491
508
|
scene: string;
|
|
492
509
|
environment?: TreeseedSceneEnvironment;
|
|
493
510
|
devices?: TreeseedSceneDeviceProfileId[];
|
|
511
|
+
browser?: TreeseedSceneBrowser;
|
|
512
|
+
authRole?: TreeseedSceneVisualAuditRole;
|
|
494
513
|
record?: boolean;
|
|
495
514
|
artifactMode?: 'full' | 'screenshots';
|
|
496
515
|
mode?: TreeseedSceneExecutionMode;
|
|
@@ -967,6 +986,7 @@ export type TreeseedSceneOperationWaitOptions = {
|
|
|
967
986
|
projectRoot: string;
|
|
968
987
|
scene: TreeseedSceneManifest;
|
|
969
988
|
environment: TreeseedSceneEnvironment;
|
|
989
|
+
runId: string;
|
|
970
990
|
baseUrl: string;
|
|
971
991
|
spec: TreeseedSceneOperationWaitSpec;
|
|
972
992
|
linkedOperationIds?: string[];
|
|
@@ -1812,9 +1832,12 @@ export type TreeseedSceneLocator = {
|
|
|
1812
1832
|
};
|
|
1813
1833
|
export type TreeseedScenePage = {
|
|
1814
1834
|
goto(url: string, options?: {
|
|
1815
|
-
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
|
|
1835
|
+
waitUntil?: 'load' | 'domcontentLoaded' | 'domcontentloaded' | 'networkidle';
|
|
1816
1836
|
timeout?: number;
|
|
1817
|
-
}): Promise<
|
|
1837
|
+
}): Promise<{
|
|
1838
|
+
status(): number;
|
|
1839
|
+
url(): string;
|
|
1840
|
+
} | null | undefined>;
|
|
1818
1841
|
url(): string;
|
|
1819
1842
|
locator(selector: string): TreeseedSceneLocator;
|
|
1820
1843
|
getByTestId(testId: string): TreeseedSceneLocator;
|
|
@@ -52,10 +52,11 @@ function configuredValue(input, name) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
function serviceHeaders(input) {
|
|
55
|
+
const localDevServiceSecret = input?.environment === "local" ? "treeseed-web-service-dev-secret" : null;
|
|
55
56
|
return {
|
|
56
57
|
"content-type": "application/json",
|
|
57
58
|
"x-treeseed-service-id": configuredValue(input, "TREESEED_API_WEB_SERVICE_ID") ?? configuredValue(input, "TREESEED_WEB_SERVICE_ID") ?? "web",
|
|
58
|
-
"x-treeseed-service-secret": configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
|
|
59
|
+
"x-treeseed-service-secret": process.env.TREESEED_API_WEB_SERVICE_SECRET?.trim() ?? process.env.TREESEED_WEB_SERVICE_SECRET?.trim() ?? localDevServiceSecret ?? configuredValue(input, "TREESEED_API_WEB_SERVICE_SECRET") ?? configuredValue(input, "TREESEED_WEB_SERVICE_SECRET") ?? "treeseed-web-service-dev-secret"
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
async function sleep(ms) {
|
|
@@ -123,12 +124,13 @@ async function seedVisualAuditFixtures(input) {
|
|
|
123
124
|
async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
124
125
|
const diagnostics = [];
|
|
125
126
|
const roles = [...new Set(input.roles)].filter((role) => role !== "anonymous");
|
|
126
|
-
|
|
127
|
+
const seedDiagnostics = await seedVisualAuditFixtures({
|
|
127
128
|
baseUrl: input.baseUrl,
|
|
128
129
|
roles,
|
|
129
130
|
projectRoot: input.projectRoot,
|
|
130
131
|
environment: input.environment
|
|
131
|
-
})
|
|
132
|
+
});
|
|
133
|
+
let roleSetupFailed = false;
|
|
132
134
|
for (const role of roles) {
|
|
133
135
|
const user = treeseedSceneVisualAuditUserForRole(role);
|
|
134
136
|
if (!user) continue;
|
|
@@ -138,6 +140,7 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
|
138
140
|
continue;
|
|
139
141
|
} catch (error) {
|
|
140
142
|
if (!isAuthFailure(error)) {
|
|
143
|
+
roleSetupFailed = true;
|
|
141
144
|
diagnostics.push(sceneWarningDiagnostic(
|
|
142
145
|
"scene.visual_audit_fixture_unavailable",
|
|
143
146
|
`Visual audit fixture setup for ${role} failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}. Authenticated screenshots require the local API and database to be healthy.`,
|
|
@@ -160,12 +163,14 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
|
160
163
|
if (payload.confirmationToken) {
|
|
161
164
|
await client.confirmWebEmail({ token: payload.confirmationToken });
|
|
162
165
|
} else if (payload.confirmationRequired) {
|
|
166
|
+
roleSetupFailed = true;
|
|
163
167
|
diagnostics.push(sceneWarningDiagnostic("scene.visual_audit_fixture_unavailable", `Visual audit fixture user ${user.email} requires email confirmation, but the local API did not return a confirmation token.`, "roles"));
|
|
164
168
|
}
|
|
165
169
|
} catch (error) {
|
|
166
170
|
try {
|
|
167
171
|
await client.webSignIn({ login: user.email, password: user.password });
|
|
168
172
|
} catch {
|
|
173
|
+
roleSetupFailed = true;
|
|
169
174
|
diagnostics.push(sceneWarningDiagnostic(
|
|
170
175
|
"scene.visual_audit_fixture_unavailable",
|
|
171
176
|
`Visual audit fixture setup for ${role} failed against ${input.baseUrl}: ${error instanceof Error ? error.message : String(error ?? "local fixture API is unavailable")}. Authenticated screenshots require the local API and database to be healthy.`,
|
|
@@ -174,6 +179,7 @@ async function ensureTreeseedSceneVisualAuditRoleFixtures(input) {
|
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
}
|
|
182
|
+
if (roleSetupFailed) diagnostics.unshift(...seedDiagnostics);
|
|
177
183
|
return diagnostics;
|
|
178
184
|
}
|
|
179
185
|
async function signInTreeseedSceneVisualAuditRole(input) {
|
|
@@ -529,10 +529,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
529
529
|
workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
|
|
530
530
|
sceneArtifacts: "full" | "screenshots";
|
|
531
531
|
localCleanup: import("../workflow-support.js").TreeseedLocalCleanupReport | null;
|
|
532
|
-
ciMode: "
|
|
532
|
+
ciMode: "off";
|
|
533
533
|
lane: string;
|
|
534
534
|
verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
|
|
535
|
-
releaseCandidateMode:
|
|
535
|
+
releaseCandidateMode: "skip";
|
|
536
536
|
applicationSelection: WorkflowApplicationSelection;
|
|
537
537
|
} & {
|
|
538
538
|
finalState?: WorkflowStatePayload;
|
|
@@ -589,10 +589,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
589
589
|
lockfileValidation: import("../operations/services/repository-save-orchestrator.js").RepositoryLockfileValidationResult | null;
|
|
590
590
|
}[];
|
|
591
591
|
};
|
|
592
|
-
ciMode: "
|
|
592
|
+
ciMode: "off";
|
|
593
593
|
lane: string;
|
|
594
594
|
verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
|
|
595
|
-
releaseCandidateMode:
|
|
595
|
+
releaseCandidateMode: "skip";
|
|
596
596
|
applicationSelection: WorkflowApplicationSelection;
|
|
597
597
|
workflowGates: Record<string, unknown>[] | {
|
|
598
598
|
name: string;
|
|
@@ -619,6 +619,10 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
619
619
|
status: string;
|
|
620
620
|
reused: number;
|
|
621
621
|
records: number;
|
|
622
|
+
} | {
|
|
623
|
+
mode: never;
|
|
624
|
+
status: "skipped";
|
|
625
|
+
reason: string;
|
|
622
626
|
} | null;
|
|
623
627
|
releaseProof: import("../operations.js").TreeseedProofRunResult | {
|
|
624
628
|
skipped: boolean;
|
|
@@ -696,10 +700,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
696
700
|
workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
|
|
697
701
|
sceneArtifacts: "full" | "screenshots";
|
|
698
702
|
localCleanup: import("../workflow-support.js").TreeseedLocalCleanupReport | null;
|
|
699
|
-
ciMode: "
|
|
703
|
+
ciMode: "off";
|
|
700
704
|
lane: string;
|
|
701
705
|
verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
|
|
702
|
-
releaseCandidateMode:
|
|
706
|
+
releaseCandidateMode: "skip";
|
|
703
707
|
applicationSelection: WorkflowApplicationSelection;
|
|
704
708
|
} & {
|
|
705
709
|
finalState?: WorkflowStatePayload;
|
|
@@ -756,10 +760,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
756
760
|
lockfileValidation: import("../operations/services/repository-save-orchestrator.js").RepositoryLockfileValidationResult | null;
|
|
757
761
|
}[];
|
|
758
762
|
};
|
|
759
|
-
ciMode: "
|
|
763
|
+
ciMode: "off";
|
|
760
764
|
lane: string;
|
|
761
765
|
verifyMode: "action-first" | "local-only" | import("../workflow.js").TreeseedWorkflowVerifyMode;
|
|
762
|
-
releaseCandidateMode:
|
|
766
|
+
releaseCandidateMode: "skip";
|
|
763
767
|
applicationSelection: WorkflowApplicationSelection;
|
|
764
768
|
workflowGates: Record<string, unknown>[] | {
|
|
765
769
|
name: string;
|
|
@@ -786,6 +790,10 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
786
790
|
status: string;
|
|
787
791
|
reused: number;
|
|
788
792
|
records: number;
|
|
793
|
+
} | {
|
|
794
|
+
mode: never;
|
|
795
|
+
status: "skipped";
|
|
796
|
+
reason: string;
|
|
789
797
|
} | null;
|
|
790
798
|
releaseProof: import("../operations.js").TreeseedProofRunResult | {
|
|
791
799
|
skipped: boolean;
|
|
@@ -1156,88 +1164,6 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
1156
1164
|
cached: boolean;
|
|
1157
1165
|
}[];
|
|
1158
1166
|
};
|
|
1159
|
-
productionHosting: {
|
|
1160
|
-
status: "skipped";
|
|
1161
|
-
reason: string;
|
|
1162
|
-
environment: "staging" | "prod";
|
|
1163
|
-
selectedApps: string[];
|
|
1164
|
-
selectedResources: {
|
|
1165
|
-
id: string;
|
|
1166
|
-
host: string;
|
|
1167
|
-
serviceType: string;
|
|
1168
|
-
placement: import("../hosting/contracts.js").TreeseedServicePlacement;
|
|
1169
|
-
serviceName: string | null;
|
|
1170
|
-
}[];
|
|
1171
|
-
reconcile?: undefined;
|
|
1172
|
-
postApplyStatus?: undefined;
|
|
1173
|
-
liveVerification?: undefined;
|
|
1174
|
-
} | {
|
|
1175
|
-
status: "reconciled";
|
|
1176
|
-
environment: "staging" | "prod";
|
|
1177
|
-
selectedApps: string[];
|
|
1178
|
-
selectedResources: {
|
|
1179
|
-
id: string;
|
|
1180
|
-
host: string;
|
|
1181
|
-
serviceType: string;
|
|
1182
|
-
placement: import("../hosting/contracts.js").TreeseedServicePlacement;
|
|
1183
|
-
serviceName: string | null;
|
|
1184
|
-
}[];
|
|
1185
|
-
reconcile: {
|
|
1186
|
-
target: TreeseedReconcileTarget;
|
|
1187
|
-
units: TreeseedDesiredUnit[];
|
|
1188
|
-
plans: import("../reconcile/contracts.js").TreeseedReconcilePlan[];
|
|
1189
|
-
results: TreeseedReconcileResult[];
|
|
1190
|
-
state: import("../reconcile/contracts.js").TreeseedReconcileStateRecord;
|
|
1191
|
-
timings: import("../timing.js").TreeseedTimingEntry[];
|
|
1192
|
-
};
|
|
1193
|
-
postApplyStatus: {
|
|
1194
|
-
target: TreeseedReconcileTarget;
|
|
1195
|
-
ready: boolean;
|
|
1196
|
-
blockers: string[];
|
|
1197
|
-
warnings: string[];
|
|
1198
|
-
units: {
|
|
1199
|
-
unitId: string;
|
|
1200
|
-
unitType: import("../reconcile/contracts.js").TreeseedReconcileUnitType;
|
|
1201
|
-
provider: string;
|
|
1202
|
-
status: import("../reconcile/contracts.js").TreeseedReconcileStatusKind;
|
|
1203
|
-
exists: boolean;
|
|
1204
|
-
locators: Record<string, string | null>;
|
|
1205
|
-
warnings: string[];
|
|
1206
|
-
verification: import("../reconcile/contracts.js").TreeseedUnitVerificationResult | null;
|
|
1207
|
-
}[];
|
|
1208
|
-
};
|
|
1209
|
-
liveVerification: import("../workflow-support.js").TreeseedLiveHostedServiceCheckReport;
|
|
1210
|
-
reason?: undefined;
|
|
1211
|
-
};
|
|
1212
|
-
productionApiGuarantees: {
|
|
1213
|
-
ok: true;
|
|
1214
|
-
environment: string;
|
|
1215
|
-
runId: string;
|
|
1216
|
-
outputRoot: string;
|
|
1217
|
-
counts: {
|
|
1218
|
-
planned: number;
|
|
1219
|
-
passed: number;
|
|
1220
|
-
failed: number;
|
|
1221
|
-
skipped: number;
|
|
1222
|
-
blocked: number;
|
|
1223
|
-
releaseBlockingFailures: number;
|
|
1224
|
-
};
|
|
1225
|
-
};
|
|
1226
|
-
productionWebVerification: Record<string, unknown> | null;
|
|
1227
|
-
productionFinalGuarantees: {
|
|
1228
|
-
ok: true;
|
|
1229
|
-
environment: string;
|
|
1230
|
-
runId: string;
|
|
1231
|
-
outputRoot: string;
|
|
1232
|
-
counts: {
|
|
1233
|
-
planned: number;
|
|
1234
|
-
passed: number;
|
|
1235
|
-
failed: number;
|
|
1236
|
-
skipped: number;
|
|
1237
|
-
blocked: number;
|
|
1238
|
-
releaseBlockingFailures: number;
|
|
1239
|
-
};
|
|
1240
|
-
};
|
|
1241
1167
|
backMerge: {
|
|
1242
1168
|
packages: {
|
|
1243
1169
|
status: string;
|
|
@@ -1404,7 +1330,7 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
|
|
|
1404
1330
|
} | null;
|
|
1405
1331
|
classification: import("./runs.js").TreeseedWorkflowRunClassification;
|
|
1406
1332
|
}[];
|
|
1407
|
-
prunedRuns: {
|
|
1333
|
+
prunedRuns: ({
|
|
1408
1334
|
runId: string;
|
|
1409
1335
|
command: TreeseedWorkflowRunCommand;
|
|
1410
1336
|
status: import("./runs.js").TreeseedWorkflowRunStatus;
|
|
@@ -1418,7 +1344,16 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
|
|
|
1418
1344
|
at: string;
|
|
1419
1345
|
} | null;
|
|
1420
1346
|
classification: import("./runs.js").TreeseedWorkflowRunClassification;
|
|
1421
|
-
}
|
|
1347
|
+
} | {
|
|
1348
|
+
runId: string;
|
|
1349
|
+
command: TreeseedWorkflowRunCommand;
|
|
1350
|
+
status: import("./runs.js").TreeseedWorkflowRunStatus;
|
|
1351
|
+
classification: {
|
|
1352
|
+
state: "stale";
|
|
1353
|
+
reasons: string[];
|
|
1354
|
+
classifiedAt: string;
|
|
1355
|
+
};
|
|
1356
|
+
})[];
|
|
1422
1357
|
markedObsoleteRun: {
|
|
1423
1358
|
runId: string;
|
|
1424
1359
|
command: TreeseedWorkflowRunCommand;
|