@treeseed/sdk 0.10.23 → 0.10.24
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/operations/services/bootstrap-runner.d.ts +5 -1
- package/dist/operations/services/bootstrap-runner.js +34 -5
- package/dist/operations/services/config-runtime.d.ts +1 -0
- package/dist/operations/services/project-platform.d.ts +25 -0
- package/dist/operations/services/project-platform.js +75 -23
- package/dist/operations/services/railway-deploy.d.ts +30 -1
- package/dist/operations/services/railway-deploy.js +79 -9
- package/dist/operations/services/template-registry.js +33 -3
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +8 -1
- package/dist/platform/deploy-runtime.js +1 -0
- package/dist/reconcile/engine.d.ts +2 -0
- package/dist/reconcile/engine.js +58 -3
- package/dist/timing.d.ts +20 -0
- package/dist/timing.js +73 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +150 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +4 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TreeseedTimingEntry } from '../../timing.ts';
|
|
1
2
|
export type TreeseedBootstrapExecution = 'parallel' | 'sequential';
|
|
2
3
|
export type TreeseedBootstrapStream = 'stdout' | 'stderr';
|
|
3
4
|
export type TreeseedBootstrapWriter = (line: string, stream?: TreeseedBootstrapStream) => void;
|
|
@@ -11,13 +12,16 @@ export type TreeseedBootstrapDagNode<TResult = unknown> = {
|
|
|
11
12
|
id: string;
|
|
12
13
|
dependencies?: string[];
|
|
13
14
|
run: () => Promise<TResult> | TResult;
|
|
15
|
+
label?: string;
|
|
14
16
|
};
|
|
15
17
|
export declare function formatTreeseedBootstrapPrefix(prefix: TreeseedBootstrapTaskPrefix): string;
|
|
16
18
|
export declare function formatTreeseedBootstrapLine(prefix: TreeseedBootstrapTaskPrefix, line: string): string;
|
|
17
19
|
export declare function writeTreeseedBootstrapLine(write: TreeseedBootstrapWriter | undefined, prefix: TreeseedBootstrapTaskPrefix, line: string, stream?: TreeseedBootstrapStream): void;
|
|
18
|
-
export declare function runTreeseedBootstrapDag<TResult = unknown>({ nodes, execution, }: {
|
|
20
|
+
export declare function runTreeseedBootstrapDag<TResult = unknown>({ nodes, execution, write, timings, }: {
|
|
19
21
|
nodes: Array<TreeseedBootstrapDagNode<TResult>>;
|
|
20
22
|
execution?: TreeseedBootstrapExecution;
|
|
23
|
+
write?: TreeseedBootstrapWriter;
|
|
24
|
+
timings?: TreeseedTimingEntry[];
|
|
21
25
|
}): Promise<Map<string, TResult>>;
|
|
22
26
|
export declare function sleep(milliseconds: number): Promise<void>;
|
|
23
27
|
export declare function runPrefixedCommand(command: string, args: string[], { cwd, env, input, write, prefix, }: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { elapsedMs, formatDurationMs } from "../../timing.js";
|
|
2
3
|
function formatTreeseedBootstrapPrefix(prefix) {
|
|
3
4
|
return `[${prefix.scope}][${prefix.system}][${prefix.task}][${prefix.stage}]`;
|
|
4
5
|
}
|
|
@@ -43,19 +44,47 @@ function dependencyLevels(nodes) {
|
|
|
43
44
|
}
|
|
44
45
|
async function runTreeseedBootstrapDag({
|
|
45
46
|
nodes,
|
|
46
|
-
execution = "parallel"
|
|
47
|
+
execution = "parallel",
|
|
48
|
+
write,
|
|
49
|
+
timings
|
|
47
50
|
}) {
|
|
48
51
|
const results = /* @__PURE__ */ new Map();
|
|
52
|
+
const recordNode = async (node) => {
|
|
53
|
+
const label = node.label ?? node.id;
|
|
54
|
+
const startMs = performance.now();
|
|
55
|
+
const entry = {
|
|
56
|
+
name: `bootstrap:${label}`,
|
|
57
|
+
durationMs: 0,
|
|
58
|
+
status: "running",
|
|
59
|
+
metadata: { nodeId: node.id, dependencies: node.dependencies ?? [] }
|
|
60
|
+
};
|
|
61
|
+
timings?.push(entry);
|
|
62
|
+
write?.(`[bootstrap][${node.id}] started`);
|
|
63
|
+
try {
|
|
64
|
+
const result = await Promise.resolve(node.run());
|
|
65
|
+
entry.durationMs = elapsedMs(startMs);
|
|
66
|
+
entry.status = "success";
|
|
67
|
+
write?.(`[bootstrap][${node.id}] completed in ${formatDurationMs(entry.durationMs)}`);
|
|
68
|
+
results.set(node.id, result);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
entry.durationMs = elapsedMs(startMs);
|
|
71
|
+
entry.status = "failed";
|
|
72
|
+
entry.metadata = {
|
|
73
|
+
...entry.metadata ?? {},
|
|
74
|
+
error: error instanceof Error ? error.message : String(error)
|
|
75
|
+
};
|
|
76
|
+
write?.(`[bootstrap][${node.id}] failed after ${formatDurationMs(entry.durationMs)}`, "stderr");
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
49
80
|
for (const level of dependencyLevels(nodes)) {
|
|
50
81
|
if (execution === "sequential") {
|
|
51
82
|
for (const node of level) {
|
|
52
|
-
|
|
83
|
+
await recordNode(node);
|
|
53
84
|
}
|
|
54
85
|
continue;
|
|
55
86
|
}
|
|
56
|
-
await Promise.all(level.map(
|
|
57
|
-
results.set(node.id, await Promise.resolve(node.run()));
|
|
58
|
-
}));
|
|
87
|
+
await Promise.all(level.map(recordNode));
|
|
59
88
|
}
|
|
60
89
|
return results;
|
|
61
90
|
}
|
|
@@ -385,6 +385,7 @@ export declare function initializeTreeseedPersistentEnvironment({ tenantRoot, sc
|
|
|
385
385
|
plans: import("../../reconcile/contracts.ts").TreeseedReconcilePlan[];
|
|
386
386
|
results: import("../../reconcile/contracts.ts").TreeseedReconcileResult[];
|
|
387
387
|
state: import("../../reconcile/contracts.ts").TreeseedReconcileStateRecord;
|
|
388
|
+
timings: import("../../timing.ts").TreeseedTimingEntry[];
|
|
388
389
|
};
|
|
389
390
|
secrets: string[];
|
|
390
391
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TreeseedTimingEntry } from '../../timing.ts';
|
|
1
2
|
import { type ControlPlaneReporter } from '../../control-plane.ts';
|
|
2
3
|
import type { TreeseedRunnableBootstrapSystem } from '../../reconcile/index.ts';
|
|
3
4
|
import { type TreeseedBootstrapExecution, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
|
|
@@ -59,6 +60,7 @@ export declare function provisionProjectPlatform(options: ProjectPlatformActionO
|
|
|
59
60
|
plans: import("../../reconcile/contracts.ts").TreeseedReconcilePlan[];
|
|
60
61
|
results: import("../../reconcile/contracts.ts").TreeseedReconcileResult[];
|
|
61
62
|
state: import("../../reconcile/contracts.ts").TreeseedReconcileStateRecord;
|
|
63
|
+
timings: TreeseedTimingEntry[];
|
|
62
64
|
};
|
|
63
65
|
verification: {
|
|
64
66
|
target: import("../../reconcile/contracts.ts").TreeseedReconcileTarget;
|
|
@@ -76,6 +78,7 @@ export declare function provisionProjectPlatform(options: ProjectPlatformActionO
|
|
|
76
78
|
verification: import("../../reconcile/contracts.ts").TreeseedUnitVerificationResult | null;
|
|
77
79
|
}[];
|
|
78
80
|
};
|
|
81
|
+
timings: TreeseedTimingEntry[];
|
|
79
82
|
railway: {
|
|
80
83
|
services: any[];
|
|
81
84
|
schedules: any[];
|
|
@@ -104,6 +107,10 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
|
|
|
104
107
|
objectKey: string;
|
|
105
108
|
skipped?: undefined;
|
|
106
109
|
reason?: undefined;
|
|
110
|
+
} | {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
skipped: boolean;
|
|
113
|
+
reason: string;
|
|
107
114
|
};
|
|
108
115
|
queue: {
|
|
109
116
|
ok: boolean;
|
|
@@ -241,6 +248,7 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
|
|
|
241
248
|
};
|
|
242
249
|
};
|
|
243
250
|
};
|
|
251
|
+
timings: TreeseedTimingEntry[];
|
|
244
252
|
};
|
|
245
253
|
hostingRepair: {
|
|
246
254
|
ok: boolean;
|
|
@@ -285,6 +293,7 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
|
|
|
285
293
|
} | null;
|
|
286
294
|
} | null;
|
|
287
295
|
} | undefined)[];
|
|
296
|
+
timings: TreeseedTimingEntry[];
|
|
288
297
|
}>;
|
|
289
298
|
export declare function resolveRailwayServiceDeployDependencies({ includeDataDependency, previousRailwayDeployNodeId, }: {
|
|
290
299
|
includeDataDependency: boolean;
|
|
@@ -313,6 +322,10 @@ export declare function monitorProjectPlatform(options: ProjectPlatformActionOpt
|
|
|
313
322
|
objectKey: string;
|
|
314
323
|
skipped?: undefined;
|
|
315
324
|
reason?: undefined;
|
|
325
|
+
} | {
|
|
326
|
+
ok: boolean;
|
|
327
|
+
skipped: boolean;
|
|
328
|
+
reason: string;
|
|
316
329
|
};
|
|
317
330
|
queue: {
|
|
318
331
|
ok: boolean;
|
|
@@ -450,6 +463,7 @@ export declare function monitorProjectPlatform(options: ProjectPlatformActionOpt
|
|
|
450
463
|
};
|
|
451
464
|
};
|
|
452
465
|
};
|
|
466
|
+
timings: TreeseedTimingEntry[];
|
|
453
467
|
}>;
|
|
454
468
|
export declare function syncControlPlaneState(options: ProjectPlatformActionOptions): Promise<void>;
|
|
455
469
|
export declare function runProjectPlatformAction(action: ProjectPlatformAction, options: ProjectPlatformActionOptions): Promise<{
|
|
@@ -474,6 +488,10 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
474
488
|
objectKey: string;
|
|
475
489
|
skipped?: undefined;
|
|
476
490
|
reason?: undefined;
|
|
491
|
+
} | {
|
|
492
|
+
ok: boolean;
|
|
493
|
+
skipped: boolean;
|
|
494
|
+
reason: string;
|
|
477
495
|
};
|
|
478
496
|
queue: {
|
|
479
497
|
ok: boolean;
|
|
@@ -611,6 +629,7 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
611
629
|
};
|
|
612
630
|
};
|
|
613
631
|
};
|
|
632
|
+
timings: TreeseedTimingEntry[];
|
|
614
633
|
} | {
|
|
615
634
|
ok: boolean;
|
|
616
635
|
scope: ProjectPlatformScope;
|
|
@@ -628,6 +647,10 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
628
647
|
objectKey: string;
|
|
629
648
|
skipped?: undefined;
|
|
630
649
|
reason?: undefined;
|
|
650
|
+
} | {
|
|
651
|
+
ok: boolean;
|
|
652
|
+
skipped: boolean;
|
|
653
|
+
reason: string;
|
|
631
654
|
};
|
|
632
655
|
queue: {
|
|
633
656
|
ok: boolean;
|
|
@@ -765,6 +788,7 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
765
788
|
};
|
|
766
789
|
};
|
|
767
790
|
};
|
|
791
|
+
timings: TreeseedTimingEntry[];
|
|
768
792
|
};
|
|
769
793
|
hostingRepair: {
|
|
770
794
|
ok: boolean;
|
|
@@ -809,5 +833,6 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
809
833
|
} | null;
|
|
810
834
|
} | null;
|
|
811
835
|
} | undefined)[];
|
|
836
|
+
timings: TreeseedTimingEntry[];
|
|
812
837
|
}>;
|
|
813
838
|
export {};
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, extname, join, resolve } from "node:path";
|
|
6
|
+
import { elapsedMs, formatTimingMarkdown, formatTimingSummary } from "../../timing.js";
|
|
6
7
|
import {
|
|
7
8
|
createControlPlaneReporter
|
|
8
9
|
} from "../../control-plane.js";
|
|
@@ -52,6 +53,39 @@ const PROCESSING_PLATFORM_BOOTSTRAP_SYSTEMS = ["api", "agents"];
|
|
|
52
53
|
function stableHash(value) {
|
|
53
54
|
return createHash("sha256").update(value).digest("hex");
|
|
54
55
|
}
|
|
56
|
+
function recordTiming(timings, name, startMs, status = "success", metadata) {
|
|
57
|
+
const entry = {
|
|
58
|
+
name,
|
|
59
|
+
durationMs: elapsedMs(startMs),
|
|
60
|
+
status,
|
|
61
|
+
...metadata ? { metadata } : {}
|
|
62
|
+
};
|
|
63
|
+
timings.push(entry);
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
async function timedPhase(timings, name, run, metadata) {
|
|
67
|
+
const startMs = performance.now();
|
|
68
|
+
try {
|
|
69
|
+
const result = await Promise.resolve(run());
|
|
70
|
+
recordTiming(timings, name, startMs, "success", metadata);
|
|
71
|
+
return result;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
recordTiming(timings, name, startMs, "failed", {
|
|
74
|
+
...metadata ?? {},
|
|
75
|
+
error: error instanceof Error ? error.message : String(error)
|
|
76
|
+
});
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function writeProviderTimingSummary(options, timings) {
|
|
81
|
+
const text = formatTimingSummary(timings);
|
|
82
|
+
options.write?.(text);
|
|
83
|
+
const summaryPath = String(options.env?.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? process.env.TREESEED_PROVIDER_TIMING_SUMMARY_PATH ?? "").trim();
|
|
84
|
+
if (!summaryPath) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(summaryPath, formatTimingMarkdown(timings), { flag: "a" });
|
|
88
|
+
}
|
|
55
89
|
function inferEnvironmentFromBranch(tenantRoot) {
|
|
56
90
|
const branch = currentManagedBranch(tenantRoot);
|
|
57
91
|
if (branch === STAGING_BRANCH) {
|
|
@@ -1024,25 +1058,30 @@ async function publishContent(options, reporter, publishOptions = {}) {
|
|
|
1024
1058
|
}
|
|
1025
1059
|
}
|
|
1026
1060
|
async function provisionProjectPlatform(options) {
|
|
1061
|
+
const timings = [];
|
|
1027
1062
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1028
1063
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
1029
1064
|
const siteConfig = loadCliDeployConfig(options.tenantRoot);
|
|
1030
1065
|
const bootstrapSystems = resolveProjectPlatformBootstrapSystems(options, siteConfig);
|
|
1031
1066
|
const selectedSystems = new Set(bootstrapSystems);
|
|
1032
1067
|
const env = { ...process.env, ...options.env ?? {} };
|
|
1033
|
-
const summary = await reconcileTreeseedTarget({
|
|
1068
|
+
const summary = await timedPhase(timings, "provision:reconcile", () => reconcileTreeseedTarget({
|
|
1034
1069
|
tenantRoot: options.tenantRoot,
|
|
1035
1070
|
target,
|
|
1036
1071
|
env,
|
|
1037
|
-
systems: bootstrapSystems
|
|
1038
|
-
|
|
1039
|
-
|
|
1072
|
+
systems: bootstrapSystems,
|
|
1073
|
+
write: options.write
|
|
1074
|
+
}));
|
|
1075
|
+
timings.push(...summary.timings ?? []);
|
|
1076
|
+
const verification = await timedPhase(timings, "provision:collect-reconcile-status", () => collectTreeseedReconcileStatus({
|
|
1040
1077
|
tenantRoot: options.tenantRoot,
|
|
1041
1078
|
target,
|
|
1042
1079
|
env,
|
|
1043
1080
|
systems: bootstrapSystems
|
|
1081
|
+
}));
|
|
1082
|
+
await timedPhase(timings, "provision:ensure-wrangler-config", () => {
|
|
1083
|
+
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
1044
1084
|
});
|
|
1045
|
-
ensureGeneratedWranglerConfig(options.tenantRoot, { target });
|
|
1046
1085
|
const shouldValidateRailway = selectedSystems.has("api") || selectedSystems.has("agents");
|
|
1047
1086
|
const railwayValidation = shouldValidateRailway ? options.scope === "local" ? validateRailwayServiceConfiguration(options.tenantRoot, options.scope) : validateRailwayDeployPrerequisites(options.tenantRoot, options.scope, { env }) : { services: [] };
|
|
1048
1087
|
const railwaySchedules = [];
|
|
@@ -1173,6 +1212,7 @@ async function provisionProjectPlatform(options) {
|
|
|
1173
1212
|
target: deployTargetLabel(target),
|
|
1174
1213
|
summary,
|
|
1175
1214
|
verification,
|
|
1215
|
+
timings,
|
|
1176
1216
|
reconcileActions: summary.results.map((result) => ({
|
|
1177
1217
|
unitId: result.unit.unitId,
|
|
1178
1218
|
action: result.action,
|
|
@@ -1190,6 +1230,7 @@ async function provisionProjectPlatform(options) {
|
|
|
1190
1230
|
target: deployTargetLabel(target),
|
|
1191
1231
|
summary,
|
|
1192
1232
|
verification,
|
|
1233
|
+
timings,
|
|
1193
1234
|
railway: {
|
|
1194
1235
|
services: railwayValidation.services.map((service) => service.key),
|
|
1195
1236
|
schedules: railwaySchedules,
|
|
@@ -1198,6 +1239,8 @@ async function provisionProjectPlatform(options) {
|
|
|
1198
1239
|
};
|
|
1199
1240
|
}
|
|
1200
1241
|
async function deployProjectPlatform(options) {
|
|
1242
|
+
const timings = [];
|
|
1243
|
+
const deployStartMs = performance.now();
|
|
1201
1244
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1202
1245
|
const commitSha = currentCommit(options.tenantRoot);
|
|
1203
1246
|
const branchName = currentRef(options.tenantRoot);
|
|
@@ -1216,7 +1259,8 @@ async function deployProjectPlatform(options) {
|
|
|
1216
1259
|
metadata: { scope: options.scope }
|
|
1217
1260
|
});
|
|
1218
1261
|
if (!options.skipProvision) {
|
|
1219
|
-
await provisionProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
1262
|
+
const provision = await timedPhase(timings, "deploy:provision", () => provisionProjectPlatform({ ...options, reporter, bootstrapSystems }));
|
|
1263
|
+
timings.push(...provision.timings ?? []);
|
|
1220
1264
|
}
|
|
1221
1265
|
const nodes = [];
|
|
1222
1266
|
let cloudflareContext = null;
|
|
@@ -1335,7 +1379,7 @@ async function deployProjectPlatform(options) {
|
|
|
1335
1379
|
}
|
|
1336
1380
|
});
|
|
1337
1381
|
}
|
|
1338
|
-
await runTreeseedBootstrapDag({ nodes, execution });
|
|
1382
|
+
await runTreeseedBootstrapDag({ nodes, execution, write, timings });
|
|
1339
1383
|
const serviceResults = selectedRailwayServiceKeys.map((serviceKey) => serviceResultsByKey.get(serviceKey)).filter(Boolean);
|
|
1340
1384
|
if (options.scope !== "local" && !options.dryRun && (selectedSystems.has("web") || serviceResults.length > 0)) {
|
|
1341
1385
|
finalizeDeploymentState(options.tenantRoot, {
|
|
@@ -1358,8 +1402,11 @@ async function deployProjectPlatform(options) {
|
|
|
1358
1402
|
scheduleVerification: railwayScheduleVerification
|
|
1359
1403
|
});
|
|
1360
1404
|
}
|
|
1361
|
-
const monitor = await monitorProjectPlatform({ ...options, reporter, bootstrapSystems });
|
|
1362
|
-
|
|
1405
|
+
const monitor = await timedPhase(timings, "deploy:monitor", () => monitorProjectPlatform({ ...options, reporter, bootstrapSystems }));
|
|
1406
|
+
timings.push(...monitor.timings ?? []);
|
|
1407
|
+
const hostingRepair = await timedPhase(timings, "deploy:hosting-repair", () => repairHostingAfterSuccessfulDeploy(options, bootstrapSystems));
|
|
1408
|
+
recordTiming(timings, "deploy:total", deployStartMs);
|
|
1409
|
+
writeProviderTimingSummary(options, timings);
|
|
1363
1410
|
await reportDeployment(reporter, {
|
|
1364
1411
|
environment: options.scope,
|
|
1365
1412
|
deploymentKind: "code",
|
|
@@ -1371,7 +1418,8 @@ async function deployProjectPlatform(options) {
|
|
|
1371
1418
|
scope: options.scope,
|
|
1372
1419
|
railway: options.scope === "local" ? [] : configuredRailwayServices(options.tenantRoot, options.scope).map((service) => service.key).filter((serviceKey) => serviceKey === "api" ? selectedSystems.has("api") : selectedSystems.has("agents")),
|
|
1373
1420
|
monitor,
|
|
1374
|
-
hostingRepair
|
|
1421
|
+
hostingRepair,
|
|
1422
|
+
timings
|
|
1375
1423
|
},
|
|
1376
1424
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1377
1425
|
});
|
|
@@ -1380,7 +1428,8 @@ async function deployProjectPlatform(options) {
|
|
|
1380
1428
|
scope: options.scope,
|
|
1381
1429
|
monitor,
|
|
1382
1430
|
hostingRepair,
|
|
1383
|
-
serviceResults
|
|
1431
|
+
serviceResults,
|
|
1432
|
+
timings
|
|
1384
1433
|
};
|
|
1385
1434
|
}
|
|
1386
1435
|
function resolveRailwayServiceDeployDependencies({
|
|
@@ -1397,6 +1446,7 @@ async function publishProjectContent(options) {
|
|
|
1397
1446
|
return publishContent(options, reporter);
|
|
1398
1447
|
}
|
|
1399
1448
|
async function monitorProjectPlatform(options) {
|
|
1449
|
+
const timings = [];
|
|
1400
1450
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1401
1451
|
const env = { ...process.env, ...options.env ?? {} };
|
|
1402
1452
|
const target = createPersistentDeployTarget(options.scope === "local" ? "staging" : options.scope);
|
|
@@ -1408,23 +1458,23 @@ async function monitorProjectPlatform(options) {
|
|
|
1408
1458
|
const webProbeUrl = resolveImmediatePagesProbeUrl(siteConfig, state, target);
|
|
1409
1459
|
const apiBaseUrl = resolveImmediateApiProbeUrl(siteConfig, state, target);
|
|
1410
1460
|
const apiMonitorEndpoints = resolveApiMonitorEndpoints(siteConfig, apiBaseUrl);
|
|
1411
|
-
const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await verifyRailwayManagedResources(options.tenantRoot, options.scope, {
|
|
1461
|
+
const railwayResources = options.scope === "local" || !apiSelected && !agentsSelected ? { ok: true, skipped: true, reason: options.scope === "local" ? "local_scope" : "railway_not_selected" } : await timedPhase(timings, "monitor:railway-resources", () => verifyRailwayManagedResources(options.tenantRoot, options.scope, {
|
|
1412
1462
|
env,
|
|
1413
1463
|
settleDeployments: true,
|
|
1414
1464
|
onProgress: options.write
|
|
1415
|
-
});
|
|
1465
|
+
}));
|
|
1416
1466
|
const skippedApiCheck = apiSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "api_not_selected" };
|
|
1417
1467
|
const skippedAgentCheck = agentsSelected ? { ok: false, skipped: true, reason: "api_url_unconfigured" } : { ok: true, skipped: true, reason: "agents_not_selected" };
|
|
1418
1468
|
const skippedD1Check = apiMonitorEndpoints.processingAgentApi ? { ok: true, skipped: true, reason: "processing_agent_api" } : skippedApiCheck;
|
|
1419
1469
|
const checks = {
|
|
1420
|
-
pages: await probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 }),
|
|
1421
|
-
apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
|
|
1422
|
-
apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 }) : skippedApiCheck,
|
|
1423
|
-
d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 }) : skippedD1Check,
|
|
1424
|
-
agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 }) : skippedAgentCheck,
|
|
1425
|
-
r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : probeR2(options.tenantRoot, siteConfig, state, target),
|
|
1426
|
-
queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : probeQueue(siteConfig, state),
|
|
1427
|
-
scaleProbe: probeScaleConfiguration(siteConfig, state),
|
|
1470
|
+
pages: await timedPhase(timings, "monitor:probe-pages", () => probeHttp(webProbeUrl, { attempts: 3, delayMs: 5e3 })),
|
|
1471
|
+
apiHealth: apiSelected && apiMonitorEndpoints.apiHealth ? await timedPhase(timings, "monitor:probe-api-health", () => probeHttp(apiMonitorEndpoints.apiHealth, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
|
|
1472
|
+
apiReady: apiSelected && apiMonitorEndpoints.apiReady ? await timedPhase(timings, "monitor:probe-api-ready", () => probeHttp(apiMonitorEndpoints.apiReady, { attempts: 8, delayMs: 1e4 })) : skippedApiCheck,
|
|
1473
|
+
d1Health: apiSelected && apiMonitorEndpoints.d1Health ? await timedPhase(timings, "monitor:probe-d1-health", () => probeHttp(apiMonitorEndpoints.d1Health, { attempts: 8, delayMs: 1e4 })) : skippedD1Check,
|
|
1474
|
+
agentHealth: agentsSelected && apiMonitorEndpoints.agentHealth ? await timedPhase(timings, "monitor:probe-agent-health", () => probeHttp(apiMonitorEndpoints.agentHealth, { attempts: 8, delayMs: 1e4 })) : skippedAgentCheck,
|
|
1475
|
+
r2: options.dryRun ? { ok: true, skipped: true, reason: "dry_run" } : timedPhase(timings, "monitor:probe-r2", () => probeR2(options.tenantRoot, siteConfig, state, target)),
|
|
1476
|
+
queue: options.dryRun ? Promise.resolve({ ok: true, skipped: true, reason: "dry_run" }) : timedPhase(timings, "monitor:probe-queue", () => probeQueue(siteConfig, state)),
|
|
1477
|
+
scaleProbe: await timedPhase(timings, "monitor:probe-scale", () => probeScaleConfiguration(siteConfig, state)),
|
|
1428
1478
|
railwayResources,
|
|
1429
1479
|
readiness: state.readiness,
|
|
1430
1480
|
apiMonitor: {
|
|
@@ -1464,14 +1514,16 @@ ${failedChecks.join("\n")}`);
|
|
|
1464
1514
|
metadata: {
|
|
1465
1515
|
mode: "monitor",
|
|
1466
1516
|
target: deployTargetLabel(target),
|
|
1467
|
-
checks: resolvedChecks
|
|
1517
|
+
checks: resolvedChecks,
|
|
1518
|
+
timings
|
|
1468
1519
|
},
|
|
1469
1520
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1470
1521
|
});
|
|
1471
1522
|
return {
|
|
1472
1523
|
ok,
|
|
1473
1524
|
target: deployTargetLabel(target),
|
|
1474
|
-
checks: resolvedChecks
|
|
1525
|
+
checks: resolvedChecks,
|
|
1526
|
+
timings
|
|
1475
1527
|
};
|
|
1476
1528
|
}
|
|
1477
1529
|
async function syncControlPlaneState(options) {
|
|
@@ -31,18 +31,47 @@ export declare function runRailway(args: any, { cwd, capture, allowFailure, inpu
|
|
|
31
31
|
retryAttempts?: number | undefined;
|
|
32
32
|
retryDelayMs?: number | undefined;
|
|
33
33
|
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
34
|
-
export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, onProgress, }?: {
|
|
34
|
+
export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, fetchImpl, onProgress, }?: {
|
|
35
35
|
services?: unknown[] | undefined;
|
|
36
36
|
env?: NodeJS.ProcessEnv | undefined;
|
|
37
37
|
timeoutMs?: number | undefined;
|
|
38
38
|
pollMs?: number | undefined;
|
|
39
|
+
fetchImpl?: typeof fetch | undefined;
|
|
39
40
|
}): Promise<{
|
|
41
|
+
ok: boolean;
|
|
42
|
+
checks: {
|
|
43
|
+
type: string;
|
|
44
|
+
service: any;
|
|
45
|
+
serviceName: any;
|
|
46
|
+
environment: string;
|
|
47
|
+
ok: boolean;
|
|
48
|
+
status: string;
|
|
49
|
+
settle: {
|
|
50
|
+
durationMs: number;
|
|
51
|
+
pollCount: number;
|
|
52
|
+
finalStatus: string;
|
|
53
|
+
};
|
|
54
|
+
message: string;
|
|
55
|
+
}[];
|
|
56
|
+
settle?: undefined;
|
|
57
|
+
message?: undefined;
|
|
58
|
+
} | {
|
|
40
59
|
ok: boolean;
|
|
41
60
|
checks: any;
|
|
61
|
+
settle: {
|
|
62
|
+
durationMs: number;
|
|
63
|
+
pollCount: number;
|
|
64
|
+
status: string;
|
|
65
|
+
};
|
|
42
66
|
message?: undefined;
|
|
43
67
|
} | {
|
|
44
68
|
ok: boolean;
|
|
45
69
|
checks: any;
|
|
70
|
+
settle: {
|
|
71
|
+
durationMs: number;
|
|
72
|
+
pollCount: number;
|
|
73
|
+
status: string;
|
|
74
|
+
};
|
|
46
75
|
message: string;
|
|
47
76
|
}>;
|
|
48
77
|
export declare function setRailwaySecretVariable({ cwd, service, environment, key, value, env, capture, allowFailure }: {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveRailwayWorkspaceContext,
|
|
25
25
|
upsertRailwayVariables
|
|
26
26
|
} from "./railway-api.js";
|
|
27
|
+
import { elapsedMs, formatDurationMs } from "../../timing.js";
|
|
27
28
|
function normalizeScope(scope) {
|
|
28
29
|
return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
|
|
29
30
|
}
|
|
@@ -171,6 +172,26 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
|
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
const deployment = instance.latestDeployment ?? null;
|
|
175
|
+
if (!deployment) {
|
|
176
|
+
return {
|
|
177
|
+
type: "deployment-status",
|
|
178
|
+
service: service.key,
|
|
179
|
+
serviceName: service.serviceName,
|
|
180
|
+
environment: normalizeRailwayEnvironmentName(environment.name),
|
|
181
|
+
ok: true,
|
|
182
|
+
skipped: true,
|
|
183
|
+
status: "no_active_deployment",
|
|
184
|
+
observed: {
|
|
185
|
+
status: null,
|
|
186
|
+
deploymentId: null,
|
|
187
|
+
deploymentCreatedAt: null,
|
|
188
|
+
deploymentStopped: null,
|
|
189
|
+
instanceStatuses: [],
|
|
190
|
+
volumeMounts: []
|
|
191
|
+
},
|
|
192
|
+
message: `Railway service ${service.serviceName} has no active deployment to wait for.`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
174
195
|
const status = String(deployment?.status ?? "").trim().toUpperCase();
|
|
175
196
|
const instanceStatuses = Array.isArray(deployment?.instances) ? deployment.instances.map((entry) => String(entry?.status ?? "").trim()).filter(Boolean) : [];
|
|
176
197
|
const ok = railwayStatusDeploymentSettled(status);
|
|
@@ -183,6 +204,8 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
|
|
|
183
204
|
status: status || "missing_deployment",
|
|
184
205
|
observed: {
|
|
185
206
|
status: status || null,
|
|
207
|
+
deploymentId: deployment?.id ?? null,
|
|
208
|
+
deploymentCreatedAt: deployment?.createdAt ?? null,
|
|
186
209
|
deploymentStopped: deployment?.deploymentStopped ?? null,
|
|
187
210
|
instanceStatuses,
|
|
188
211
|
volumeMounts: Array.isArray(deployment?.meta?.volumeMounts) ? deployment.meta.volumeMounts : []
|
|
@@ -474,8 +497,10 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
474
497
|
env = process.env,
|
|
475
498
|
timeoutMs = 6e5,
|
|
476
499
|
pollMs = 15e3,
|
|
500
|
+
fetchImpl = fetch,
|
|
477
501
|
onProgress
|
|
478
502
|
} = {}) {
|
|
503
|
+
const startMs = performance.now();
|
|
479
504
|
const deadline = Date.now() + timeoutMs;
|
|
480
505
|
const projectId = services.find((service) => typeof service.projectId === "string" && service.projectId.trim())?.projectId ?? null;
|
|
481
506
|
if (!projectId) {
|
|
@@ -488,6 +513,11 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
488
513
|
environment: resolveRailwayEnvironmentForScope(scope),
|
|
489
514
|
ok: false,
|
|
490
515
|
status: "missing_project",
|
|
516
|
+
settle: {
|
|
517
|
+
durationMs: elapsedMs(startMs),
|
|
518
|
+
pollCount: 0,
|
|
519
|
+
finalStatus: "missing_project"
|
|
520
|
+
},
|
|
491
521
|
message: `Railway deployment status for ${service.serviceName} cannot be checked without a project id.`
|
|
492
522
|
}))
|
|
493
523
|
};
|
|
@@ -495,12 +525,15 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
495
525
|
let checks = [];
|
|
496
526
|
let lastError = null;
|
|
497
527
|
let lastSummary = "";
|
|
528
|
+
let pollCount = 0;
|
|
498
529
|
for (; ; ) {
|
|
499
530
|
lastError = null;
|
|
531
|
+
pollCount += 1;
|
|
500
532
|
try {
|
|
501
533
|
const statusPayload = await fetchRailwayProjectDeploymentStatus({
|
|
502
534
|
projectId,
|
|
503
|
-
env
|
|
535
|
+
env,
|
|
536
|
+
fetchImpl
|
|
504
537
|
});
|
|
505
538
|
checks = collectRailwayDeploymentStatusChecks(statusPayload, scope, services);
|
|
506
539
|
} catch (error) {
|
|
@@ -512,28 +545,63 @@ async function waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
|
512
545
|
environment: resolveRailwayEnvironmentForScope(scope),
|
|
513
546
|
ok: false,
|
|
514
547
|
status: "status_error",
|
|
548
|
+
settle: {
|
|
549
|
+
durationMs: elapsedMs(startMs),
|
|
550
|
+
pollCount,
|
|
551
|
+
finalStatus: "status_error"
|
|
552
|
+
},
|
|
515
553
|
message: error instanceof Error ? error.message : String(error)
|
|
516
554
|
}));
|
|
517
555
|
}
|
|
518
556
|
const summary = formatRailwayDeploymentStatusSummary(scope, checks);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
557
|
+
const progress = `${summary} poll=${pollCount} elapsed=${formatDurationMs(elapsedMs(startMs))}`;
|
|
558
|
+
if (progress !== lastSummary || !checks.every((entry) => entry.ok === true || entry.skipped === true)) {
|
|
559
|
+
onProgress?.(progress, "stdout");
|
|
560
|
+
lastSummary = progress;
|
|
522
561
|
}
|
|
523
|
-
if (checks.every((entry) => entry.ok === true)) {
|
|
524
|
-
return {
|
|
562
|
+
if (checks.every((entry) => entry.ok === true || entry.skipped === true)) {
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
checks: checks.map((check) => ({
|
|
566
|
+
...check,
|
|
567
|
+
settle: {
|
|
568
|
+
durationMs: elapsedMs(startMs),
|
|
569
|
+
pollCount,
|
|
570
|
+
finalStatus: check.status,
|
|
571
|
+
fastSkipped: check.skipped === true
|
|
572
|
+
}
|
|
573
|
+
})),
|
|
574
|
+
settle: {
|
|
575
|
+
durationMs: elapsedMs(startMs),
|
|
576
|
+
pollCount,
|
|
577
|
+
status: checks.every((entry) => entry.skipped === true) ? "skipped" : "settled"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
525
580
|
}
|
|
526
581
|
if (Date.now() >= deadline) {
|
|
527
582
|
return {
|
|
528
583
|
ok: false,
|
|
529
|
-
checks
|
|
584
|
+
checks: checks.map((check) => ({
|
|
585
|
+
...check,
|
|
586
|
+
settle: {
|
|
587
|
+
durationMs: elapsedMs(startMs),
|
|
588
|
+
pollCount,
|
|
589
|
+
finalStatus: check.status,
|
|
590
|
+
timeout: true
|
|
591
|
+
}
|
|
592
|
+
})),
|
|
593
|
+
settle: {
|
|
594
|
+
durationMs: elapsedMs(startMs),
|
|
595
|
+
pollCount,
|
|
596
|
+
status: "timeout"
|
|
597
|
+
},
|
|
530
598
|
message: lastError instanceof Error ? lastError.message : "Railway deployments did not settle before the monitor timeout."
|
|
531
599
|
};
|
|
532
600
|
}
|
|
533
601
|
await sleep(pollMs);
|
|
534
602
|
}
|
|
535
603
|
}
|
|
536
|
-
async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env }) {
|
|
604
|
+
async function fetchRailwayProjectDeploymentStatus({ projectId, env = process.env, fetchImpl = fetch }) {
|
|
537
605
|
const payload = await railwayGraphqlRequest({
|
|
538
606
|
query: `
|
|
539
607
|
query TreeseedRailwayDeploymentStatus($projectId: String!) {
|
|
@@ -571,7 +639,8 @@ query TreeseedRailwayDeploymentStatus($projectId: String!) {
|
|
|
571
639
|
}
|
|
572
640
|
`.trim(),
|
|
573
641
|
variables: { projectId },
|
|
574
|
-
env
|
|
642
|
+
env,
|
|
643
|
+
fetchImpl
|
|
575
644
|
});
|
|
576
645
|
return payload.data?.project ?? null;
|
|
577
646
|
}
|
|
@@ -1300,6 +1369,7 @@ async function verifyRailwayManagedResources(tenantRoot, scope, {
|
|
|
1300
1369
|
const settled = await waitForRailwayManagedDeploymentsSettled(tenantRoot, scope, {
|
|
1301
1370
|
services: deploymentStatusServices.length > 0 ? deploymentStatusServices : services,
|
|
1302
1371
|
env: effectiveEnv,
|
|
1372
|
+
fetchImpl,
|
|
1303
1373
|
timeoutMs: settleTimeoutMs,
|
|
1304
1374
|
pollMs: settlePollMs,
|
|
1305
1375
|
onProgress
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
cliPackageVersion,
|
|
12
12
|
agentPackageVersion,
|
|
13
13
|
corePackageVersion,
|
|
14
|
+
cliPackageRoot,
|
|
14
15
|
localTemplateArtifactsRoot,
|
|
15
16
|
sdkPackageVersion
|
|
16
17
|
} from "./runtime-paths.js";
|
|
@@ -37,10 +38,31 @@ function listFiles(root) {
|
|
|
37
38
|
return files;
|
|
38
39
|
}
|
|
39
40
|
function listTemplateArtifactIds() {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const packagedIds = existsSync(localTemplateArtifactsRoot) ? readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
42
|
+
const localStarterIds = listLocalStarterArtifacts().map((entry) => entry.id);
|
|
43
|
+
return [.../* @__PURE__ */ new Set([...packagedIds, ...localStarterIds])].sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
|
|
44
|
+
}
|
|
45
|
+
const LOCAL_STARTER_ID_TO_DIRECTORY = {
|
|
46
|
+
"starter-research": "research",
|
|
47
|
+
"starter-engineering": "engineering",
|
|
48
|
+
"starter-information-hub": "information-hub"
|
|
49
|
+
};
|
|
50
|
+
function localStartersRoot() {
|
|
51
|
+
return resolve(cliPackageRoot, "..", "..", "starters");
|
|
52
|
+
}
|
|
53
|
+
function resolveLocalStarterArtifactRoot(id) {
|
|
54
|
+
const directory = LOCAL_STARTER_ID_TO_DIRECTORY[id];
|
|
55
|
+
if (!directory) {
|
|
56
|
+
return null;
|
|
42
57
|
}
|
|
43
|
-
|
|
58
|
+
const artifactRoot = resolve(localStartersRoot(), directory);
|
|
59
|
+
return existsSync(resolve(artifactRoot, "template.config.json")) && existsSync(resolve(artifactRoot, "template")) ? artifactRoot : null;
|
|
60
|
+
}
|
|
61
|
+
function listLocalStarterArtifacts() {
|
|
62
|
+
return Object.keys(LOCAL_STARTER_ID_TO_DIRECTORY).map((id) => {
|
|
63
|
+
const artifactRoot = resolveLocalStarterArtifactRoot(id);
|
|
64
|
+
return artifactRoot ? { id, artifactRoot } : null;
|
|
65
|
+
}).filter((entry) => Boolean(entry));
|
|
44
66
|
}
|
|
45
67
|
function isTextFile(filePath) {
|
|
46
68
|
return !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot|pdf|zip|gz)$/iu.test(filePath);
|
|
@@ -166,6 +188,14 @@ function materializeR2TemplateSource(product) {
|
|
|
166
188
|
);
|
|
167
189
|
}
|
|
168
190
|
function resolveTemplateDefinitionPaths(product, options) {
|
|
191
|
+
const localStarterArtifactRoot = resolveLocalStarterArtifactRoot(product.id);
|
|
192
|
+
if (localStarterArtifactRoot) {
|
|
193
|
+
return {
|
|
194
|
+
artifactRoot: localStarterArtifactRoot,
|
|
195
|
+
manifestPath: resolve(localStarterArtifactRoot, "template.config.json"),
|
|
196
|
+
templateRoot: resolve(localStarterArtifactRoot, "template")
|
|
197
|
+
};
|
|
198
|
+
}
|
|
169
199
|
if (existsSync(product.artifactManifestPath) && existsSync(product.templateRoot)) {
|
|
170
200
|
return {
|
|
171
201
|
artifactRoot: product.artifactRoot,
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from "./plugins/constants.js";
|
|
10
10
|
const deployConfigFieldAliases = {
|
|
11
11
|
siteUrl: { key: "siteUrl", aliases: ["site_url"] },
|
|
12
|
-
contactEmail: { key: "contactEmail", aliases: ["contact_email"] }
|
|
12
|
+
contactEmail: { key: "contactEmail", aliases: ["contact_email"] },
|
|
13
|
+
projectRoot: { key: "projectRoot", aliases: ["project_root"] }
|
|
13
14
|
};
|
|
14
15
|
const hostingFieldAliases = {
|
|
15
16
|
kind: { key: "kind", aliases: ["kind"] },
|
|
@@ -547,6 +548,7 @@ function parseDeployConfig(raw) {
|
|
|
547
548
|
slug: expectString(parsed.slug, "slug"),
|
|
548
549
|
siteUrl: expectString(parsed.siteUrl, "siteUrl"),
|
|
549
550
|
contactEmail: expectString(parsed.contactEmail, "contactEmail"),
|
|
551
|
+
projectRoot: optionalString(parsed.projectRoot),
|
|
550
552
|
hosting: compatibilityHosting,
|
|
551
553
|
hub,
|
|
552
554
|
runtime,
|
|
@@ -630,10 +632,15 @@ function loadTreeseedDeployConfig(configPath = "treeseed.site.yaml") {
|
|
|
630
632
|
function loadTreeseedDeployConfigFromPath(resolvedConfigPath) {
|
|
631
633
|
const tenantRoot = dirname(resolvedConfigPath);
|
|
632
634
|
const parsed = parseDeployConfig(readFileSync(resolvedConfigPath, "utf8"));
|
|
635
|
+
const projectRoot = parsed.projectRoot ? resolve(tenantRoot, parsed.projectRoot) : tenantRoot;
|
|
633
636
|
Object.defineProperty(parsed, "__tenantRoot", {
|
|
634
637
|
value: tenantRoot,
|
|
635
638
|
enumerable: false
|
|
636
639
|
});
|
|
640
|
+
Object.defineProperty(parsed, "__projectRoot", {
|
|
641
|
+
value: projectRoot,
|
|
642
|
+
enumerable: false
|
|
643
|
+
});
|
|
637
644
|
Object.defineProperty(parsed, "__configPath", {
|
|
638
645
|
value: resolvedConfigPath,
|
|
639
646
|
enumerable: false
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TreeseedObservedUnitState, TreeseedReconcilePlan, TreeseedReconcileResult, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitVerificationResult } from './contracts.ts';
|
|
2
2
|
import { type TreeseedRunnableBootstrapSystem } from './bootstrap-systems.ts';
|
|
3
|
+
import { type TreeseedTimingEntry } from '../timing.ts';
|
|
3
4
|
export declare function observeTreeseedUnits({ tenantRoot, target, env, systems, write, }: {
|
|
4
5
|
tenantRoot: string;
|
|
5
6
|
target: TreeseedReconcileTarget;
|
|
@@ -417,6 +418,7 @@ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, syste
|
|
|
417
418
|
plans: TreeseedReconcilePlan[];
|
|
418
419
|
results: TreeseedReconcileResult[];
|
|
419
420
|
state: TreeseedReconcileStateRecord;
|
|
421
|
+
timings: TreeseedTimingEntry[];
|
|
420
422
|
}>;
|
|
421
423
|
export declare function destroyTreeseedTargetUnits({ tenantRoot, target, env, write, }: {
|
|
422
424
|
tenantRoot: string;
|
package/dist/reconcile/engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { deriveTreeseedDesiredUnits } from "./desired-state.js";
|
|
|
3
3
|
import { ensureTreeseedPersistedUnitState, desiredUnitSpecHash, loadTreeseedReconcileState, updateTreeseedPersistedUnitState, writeTreeseedReconcileState } from "./state.js";
|
|
4
4
|
import { reverseTopologicallySortedUnits, topologicallySortDesiredUnits } from "./units.js";
|
|
5
5
|
import { filterTreeseedDesiredUnitsByBootstrapSystems } from "./bootstrap-systems.js";
|
|
6
|
+
import { elapsedMs, formatDurationMs } from "../timing.js";
|
|
6
7
|
function nowIso() {
|
|
7
8
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
8
9
|
}
|
|
@@ -209,7 +210,9 @@ async function reconcileTreeseedTarget({
|
|
|
209
210
|
const context = createRunContext(tenantRoot, target, env, write);
|
|
210
211
|
const results = [];
|
|
211
212
|
const verificationMap = /* @__PURE__ */ new Map();
|
|
213
|
+
const timingEntries = [];
|
|
212
214
|
context.session.set("treeseed:verification-results", verificationMap);
|
|
215
|
+
context.session.set("treeseed:timings", timingEntries);
|
|
213
216
|
const planByUnitId = new Map(planned.plans.map((plan) => [plan.unit.unitId, plan]));
|
|
214
217
|
let persistChain = Promise.resolve();
|
|
215
218
|
const persistVerifiedResult = async (persisted, verifiedResult) => {
|
|
@@ -221,20 +224,43 @@ async function reconcileTreeseedTarget({
|
|
|
221
224
|
};
|
|
222
225
|
await runByDependencyLevel(topologicallySortDesiredUnits(planned.units), async (unit) => {
|
|
223
226
|
const plan = planByUnitId.get(unit.unitId);
|
|
224
|
-
|
|
227
|
+
const unitTiming = {
|
|
228
|
+
name: `reconcile:${plan.unit.provider}:${plan.unit.unitType}:${plan.unit.logicalName}`,
|
|
229
|
+
durationMs: 0,
|
|
230
|
+
status: "running",
|
|
231
|
+
children: [],
|
|
232
|
+
metadata: { unitId: plan.unit.unitId, action: plan.diff.action }
|
|
233
|
+
};
|
|
234
|
+
const unitStartMs = performance.now();
|
|
235
|
+
timingEntries.push(unitTiming);
|
|
236
|
+
write?.(`Reconciling ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
|
|
225
237
|
const adapter = registry.get(plan.unit.unitType, plan.unit.provider);
|
|
226
238
|
const persisted = ensureTreeseedPersistedUnitState(planned.state, plan.unit);
|
|
227
239
|
try {
|
|
240
|
+
const stageStartMs = performance.now();
|
|
228
241
|
await Promise.resolve(adapter.validate?.({
|
|
229
242
|
context,
|
|
230
243
|
unit: plan.unit,
|
|
231
244
|
persistedState: persisted
|
|
232
245
|
}));
|
|
246
|
+
unitTiming.children?.push({
|
|
247
|
+
name: `${unitTiming.name}:validate`,
|
|
248
|
+
durationMs: elapsedMs(stageStartMs),
|
|
249
|
+
status: "success"
|
|
250
|
+
});
|
|
233
251
|
} catch (error) {
|
|
252
|
+
unitTiming.children?.push({
|
|
253
|
+
name: `${unitTiming.name}:validate`,
|
|
254
|
+
durationMs: elapsedMs(unitStartMs),
|
|
255
|
+
status: "failed"
|
|
256
|
+
});
|
|
257
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
258
|
+
unitTiming.status = "failed";
|
|
234
259
|
wrapAdapterFailure("validate", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
235
260
|
}
|
|
236
261
|
let result;
|
|
237
262
|
try {
|
|
263
|
+
const stageStartMs = performance.now();
|
|
238
264
|
result = await Promise.resolve(adapter.reconcile({
|
|
239
265
|
context,
|
|
240
266
|
unit: plan.unit,
|
|
@@ -242,10 +268,22 @@ async function reconcileTreeseedTarget({
|
|
|
242
268
|
observed: plan.observed,
|
|
243
269
|
diff: plan.diff
|
|
244
270
|
}));
|
|
271
|
+
unitTiming.children?.push({
|
|
272
|
+
name: `${unitTiming.name}:reconcile`,
|
|
273
|
+
durationMs: elapsedMs(stageStartMs),
|
|
274
|
+
status: "success"
|
|
275
|
+
});
|
|
245
276
|
} catch (error) {
|
|
277
|
+
unitTiming.children?.push({
|
|
278
|
+
name: `${unitTiming.name}:reconcile`,
|
|
279
|
+
durationMs: elapsedMs(unitStartMs),
|
|
280
|
+
status: "failed"
|
|
281
|
+
});
|
|
282
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
283
|
+
unitTiming.status = "failed";
|
|
246
284
|
wrapAdapterFailure("reconcile", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
247
285
|
}
|
|
248
|
-
write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType}...`);
|
|
286
|
+
write?.(`Verifying ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName})...`);
|
|
249
287
|
const postconditions = await Promise.resolve(adapter.requiredPostconditions?.({
|
|
250
288
|
context,
|
|
251
289
|
unit: plan.unit,
|
|
@@ -253,6 +291,7 @@ async function reconcileTreeseedTarget({
|
|
|
253
291
|
}) ?? []);
|
|
254
292
|
let verification;
|
|
255
293
|
try {
|
|
294
|
+
const stageStartMs = performance.now();
|
|
256
295
|
verification = await Promise.resolve(adapter.verify({
|
|
257
296
|
context,
|
|
258
297
|
unit: plan.unit,
|
|
@@ -262,7 +301,19 @@ async function reconcileTreeseedTarget({
|
|
|
262
301
|
result,
|
|
263
302
|
postconditions
|
|
264
303
|
}));
|
|
304
|
+
unitTiming.children?.push({
|
|
305
|
+
name: `${unitTiming.name}:verify`,
|
|
306
|
+
durationMs: elapsedMs(stageStartMs),
|
|
307
|
+
status: verification.verified ? "success" : "failed"
|
|
308
|
+
});
|
|
265
309
|
} catch (error) {
|
|
310
|
+
unitTiming.children?.push({
|
|
311
|
+
name: `${unitTiming.name}:verify`,
|
|
312
|
+
durationMs: elapsedMs(unitStartMs),
|
|
313
|
+
status: "failed"
|
|
314
|
+
});
|
|
315
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
316
|
+
unitTiming.status = "failed";
|
|
266
317
|
wrapAdapterFailure("verify", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
|
|
267
318
|
}
|
|
268
319
|
const verifiedResult = {
|
|
@@ -274,6 +325,9 @@ async function reconcileTreeseedTarget({
|
|
|
274
325
|
]
|
|
275
326
|
};
|
|
276
327
|
verificationMap.set(plan.unit.unitId, verification);
|
|
328
|
+
unitTiming.durationMs = elapsedMs(unitStartMs);
|
|
329
|
+
unitTiming.status = verification.verified ? "success" : "failed";
|
|
330
|
+
write?.(`Finished ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName}) in ${formatDurationMs(unitTiming.durationMs)}.`);
|
|
277
331
|
if (!verification.verified) {
|
|
278
332
|
await persistVerifiedResult(persisted, verifiedResult);
|
|
279
333
|
throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
|
|
@@ -287,7 +341,8 @@ async function reconcileTreeseedTarget({
|
|
|
287
341
|
units: planned.units,
|
|
288
342
|
plans: planned.plans,
|
|
289
343
|
results,
|
|
290
|
-
state: planned.state
|
|
344
|
+
state: planned.state,
|
|
345
|
+
timings: timingEntries
|
|
291
346
|
};
|
|
292
347
|
}
|
|
293
348
|
async function destroyTreeseedTargetUnits({
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type TreeseedTimingEntry = {
|
|
2
|
+
name: string;
|
|
3
|
+
durationMs: number;
|
|
4
|
+
status?: string;
|
|
5
|
+
metadata?: Record<string, unknown>;
|
|
6
|
+
children?: TreeseedTimingEntry[];
|
|
7
|
+
};
|
|
8
|
+
export declare function nowMs(): number;
|
|
9
|
+
export declare function elapsedMs(startMs: number): number;
|
|
10
|
+
export declare function formatDurationMs(durationMs: number): string;
|
|
11
|
+
export declare function summarizeSlowestTimings(entries: TreeseedTimingEntry[], limit?: number): TreeseedTimingEntry[];
|
|
12
|
+
export declare function flattenTimings(entries: TreeseedTimingEntry[]): TreeseedTimingEntry[];
|
|
13
|
+
export declare function formatTimingSummary(entries: TreeseedTimingEntry[], { title, limit }?: {
|
|
14
|
+
title?: string | undefined;
|
|
15
|
+
limit?: number | undefined;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function formatTimingMarkdown(entries: TreeseedTimingEntry[], { title, limit }?: {
|
|
18
|
+
title?: string | undefined;
|
|
19
|
+
limit?: number | undefined;
|
|
20
|
+
}): string;
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function nowMs() {
|
|
2
|
+
return performance.now();
|
|
3
|
+
}
|
|
4
|
+
function elapsedMs(startMs) {
|
|
5
|
+
return Math.max(0, performance.now() - startMs);
|
|
6
|
+
}
|
|
7
|
+
function formatDurationMs(durationMs) {
|
|
8
|
+
const value = Math.max(0, Number(durationMs) || 0);
|
|
9
|
+
if (value < 1e3) {
|
|
10
|
+
return `${Math.round(value)}ms`;
|
|
11
|
+
}
|
|
12
|
+
if (value < 6e4) {
|
|
13
|
+
return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}s`;
|
|
14
|
+
}
|
|
15
|
+
const minutes = Math.floor(value / 6e4);
|
|
16
|
+
const seconds = Math.round(value % 6e4 / 1e3);
|
|
17
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
18
|
+
}
|
|
19
|
+
function summarizeSlowestTimings(entries, limit = 8) {
|
|
20
|
+
return flattenTimings(entries).sort((left, right) => right.durationMs - left.durationMs).slice(0, Math.max(0, limit));
|
|
21
|
+
}
|
|
22
|
+
function flattenTimings(entries) {
|
|
23
|
+
const flattened = [];
|
|
24
|
+
const visit = (entry) => {
|
|
25
|
+
flattened.push(entry);
|
|
26
|
+
for (const child of entry.children ?? []) {
|
|
27
|
+
visit(child);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
visit(entry);
|
|
32
|
+
}
|
|
33
|
+
return flattened;
|
|
34
|
+
}
|
|
35
|
+
function formatTimingSummary(entries, { title = "Provider deploy timing summary", limit = 12 } = {}) {
|
|
36
|
+
const slowest = summarizeSlowestTimings(entries, limit);
|
|
37
|
+
const lines = [`${title}:`];
|
|
38
|
+
if (slowest.length === 0) {
|
|
39
|
+
lines.push("- no timed steps recorded");
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
for (const entry of slowest) {
|
|
43
|
+
const status = entry.status ? ` [${entry.status}]` : "";
|
|
44
|
+
lines.push(`- ${entry.name}: ${formatDurationMs(entry.durationMs)}${status}`);
|
|
45
|
+
}
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
function formatTimingMarkdown(entries, { title = "Provider deploy timing summary", limit = 20 } = {}) {
|
|
49
|
+
const slowest = summarizeSlowestTimings(entries, limit);
|
|
50
|
+
const lines = [`### ${title}`, "", "| Step | Duration | Status |", "| --- | ---: | --- |"];
|
|
51
|
+
if (slowest.length === 0) {
|
|
52
|
+
lines.push("| No timed steps recorded | 0ms | skipped |");
|
|
53
|
+
return `${lines.join("\n")}
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of slowest) {
|
|
57
|
+
lines.push(`| ${escapeMarkdownCell(entry.name)} | ${formatDurationMs(entry.durationMs)} | ${escapeMarkdownCell(entry.status ?? "")} |`);
|
|
58
|
+
}
|
|
59
|
+
return `${lines.join("\n")}
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function escapeMarkdownCell(value) {
|
|
63
|
+
return value.replace(/\\/gu, "\\\\").replace(/\|/gu, "\\|").replace(/\n/gu, " ");
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
elapsedMs,
|
|
67
|
+
flattenTimings,
|
|
68
|
+
formatDurationMs,
|
|
69
|
+
formatTimingMarkdown,
|
|
70
|
+
formatTimingSummary,
|
|
71
|
+
nowMs,
|
|
72
|
+
summarizeSlowestTimings
|
|
73
|
+
};
|
|
@@ -47,6 +47,156 @@
|
|
|
47
47
|
"relatedBooks": [],
|
|
48
48
|
"relatedKnowledge": [],
|
|
49
49
|
"relatedObjectives": []
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "starter-research",
|
|
53
|
+
"displayName": "TreeSeed Research",
|
|
54
|
+
"description": "First-party TreeSeed starter for freestanding research projects and knowledge-pack publishing.",
|
|
55
|
+
"summary": "A research starter for building source-backed books, gathered assets, synthesis notes, and reusable knowledge packs.",
|
|
56
|
+
"status": "live",
|
|
57
|
+
"featured": true,
|
|
58
|
+
"category": "starter",
|
|
59
|
+
"audience": [
|
|
60
|
+
"researchers",
|
|
61
|
+
"maintainers"
|
|
62
|
+
],
|
|
63
|
+
"tags": [
|
|
64
|
+
"starter",
|
|
65
|
+
"research",
|
|
66
|
+
"books",
|
|
67
|
+
"knowledge-packs",
|
|
68
|
+
"agents"
|
|
69
|
+
],
|
|
70
|
+
"publisher": {
|
|
71
|
+
"id": "treeseed",
|
|
72
|
+
"name": "TreeSeed",
|
|
73
|
+
"url": "https://treeseed.ai"
|
|
74
|
+
},
|
|
75
|
+
"publisherVerified": true,
|
|
76
|
+
"templateVersion": "1.0.0",
|
|
77
|
+
"templateApiVersion": 1,
|
|
78
|
+
"minCliVersion": "0.1.1",
|
|
79
|
+
"minCoreVersion": "0.1.2",
|
|
80
|
+
"fulfillment": {
|
|
81
|
+
"mode": "git",
|
|
82
|
+
"source": {
|
|
83
|
+
"kind": "git",
|
|
84
|
+
"repoUrl": "https://github.com/treeseed-ai/starter-research.git",
|
|
85
|
+
"directory": ".",
|
|
86
|
+
"ref": "main",
|
|
87
|
+
"integrity": "pending-external-repo"
|
|
88
|
+
},
|
|
89
|
+
"hooksPolicy": "builtin_only",
|
|
90
|
+
"supportsReconcile": true
|
|
91
|
+
},
|
|
92
|
+
"offer": {
|
|
93
|
+
"priceModel": "free",
|
|
94
|
+
"license": "AGPL-3.0-only",
|
|
95
|
+
"support": "community"
|
|
96
|
+
},
|
|
97
|
+
"relatedBooks": [],
|
|
98
|
+
"relatedKnowledge": [],
|
|
99
|
+
"relatedObjectives": []
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "starter-engineering",
|
|
103
|
+
"displayName": "TreeSeed Engineering",
|
|
104
|
+
"description": "First-party TreeSeed starter for integrated software guidance, docs, and engineering agents.",
|
|
105
|
+
"summary": "An engineering starter for guiding software projects with research, architecture, implementation, review, and release agents.",
|
|
106
|
+
"status": "live",
|
|
107
|
+
"featured": true,
|
|
108
|
+
"category": "starter",
|
|
109
|
+
"audience": [
|
|
110
|
+
"engineers",
|
|
111
|
+
"maintainers"
|
|
112
|
+
],
|
|
113
|
+
"tags": [
|
|
114
|
+
"starter",
|
|
115
|
+
"engineering",
|
|
116
|
+
"software",
|
|
117
|
+
"docs",
|
|
118
|
+
"agents"
|
|
119
|
+
],
|
|
120
|
+
"publisher": {
|
|
121
|
+
"id": "treeseed",
|
|
122
|
+
"name": "TreeSeed",
|
|
123
|
+
"url": "https://treeseed.ai"
|
|
124
|
+
},
|
|
125
|
+
"publisherVerified": true,
|
|
126
|
+
"templateVersion": "1.0.0",
|
|
127
|
+
"templateApiVersion": 1,
|
|
128
|
+
"minCliVersion": "0.1.1",
|
|
129
|
+
"minCoreVersion": "0.1.2",
|
|
130
|
+
"fulfillment": {
|
|
131
|
+
"mode": "git",
|
|
132
|
+
"source": {
|
|
133
|
+
"kind": "git",
|
|
134
|
+
"repoUrl": "https://github.com/treeseed-ai/starter-engineering.git",
|
|
135
|
+
"directory": ".",
|
|
136
|
+
"ref": "main",
|
|
137
|
+
"integrity": "pending-external-repo"
|
|
138
|
+
},
|
|
139
|
+
"hooksPolicy": "builtin_only",
|
|
140
|
+
"supportsReconcile": true
|
|
141
|
+
},
|
|
142
|
+
"offer": {
|
|
143
|
+
"priceModel": "free",
|
|
144
|
+
"license": "AGPL-3.0-only",
|
|
145
|
+
"support": "community"
|
|
146
|
+
},
|
|
147
|
+
"relatedBooks": [],
|
|
148
|
+
"relatedKnowledge": [],
|
|
149
|
+
"relatedObjectives": []
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "starter-information-hub",
|
|
153
|
+
"displayName": "TreeSeed Information Hub",
|
|
154
|
+
"description": "First-party TreeSeed starter for recurring information retrieval, distillation, and downstream distribution.",
|
|
155
|
+
"summary": "An information distribution starter for retrieving updates over time, distilling derived knowledge, and packaging it for downstream projects.",
|
|
156
|
+
"status": "live",
|
|
157
|
+
"featured": true,
|
|
158
|
+
"category": "starter",
|
|
159
|
+
"audience": [
|
|
160
|
+
"curators",
|
|
161
|
+
"maintainers"
|
|
162
|
+
],
|
|
163
|
+
"tags": [
|
|
164
|
+
"starter",
|
|
165
|
+
"information-hub",
|
|
166
|
+
"distribution",
|
|
167
|
+
"knowledge-packs",
|
|
168
|
+
"agents"
|
|
169
|
+
],
|
|
170
|
+
"publisher": {
|
|
171
|
+
"id": "treeseed",
|
|
172
|
+
"name": "TreeSeed",
|
|
173
|
+
"url": "https://treeseed.ai"
|
|
174
|
+
},
|
|
175
|
+
"publisherVerified": true,
|
|
176
|
+
"templateVersion": "1.0.0",
|
|
177
|
+
"templateApiVersion": 1,
|
|
178
|
+
"minCliVersion": "0.1.1",
|
|
179
|
+
"minCoreVersion": "0.1.2",
|
|
180
|
+
"fulfillment": {
|
|
181
|
+
"mode": "git",
|
|
182
|
+
"source": {
|
|
183
|
+
"kind": "git",
|
|
184
|
+
"repoUrl": "https://github.com/treeseed-ai/starter-information-hub.git",
|
|
185
|
+
"directory": ".",
|
|
186
|
+
"ref": "main",
|
|
187
|
+
"integrity": "pending-external-repo"
|
|
188
|
+
},
|
|
189
|
+
"hooksPolicy": "builtin_only",
|
|
190
|
+
"supportsReconcile": true
|
|
191
|
+
},
|
|
192
|
+
"offer": {
|
|
193
|
+
"priceModel": "free",
|
|
194
|
+
"license": "AGPL-3.0-only",
|
|
195
|
+
"support": "community"
|
|
196
|
+
},
|
|
197
|
+
"relatedBooks": [],
|
|
198
|
+
"relatedKnowledge": [],
|
|
199
|
+
"relatedObjectives": []
|
|
50
200
|
}
|
|
51
201
|
],
|
|
52
202
|
"meta": {
|
package/package.json
CHANGED
|
@@ -120,7 +120,11 @@ __WORKING_DIRECTORY_BLOCK__ web:
|
|
|
120
120
|
shell: bash
|
|
121
121
|
run: |
|
|
122
122
|
set -euo pipefail
|
|
123
|
+
TIMING_SUMMARY="${RUNNER_TEMP:-/tmp}/treeseed-provider-timing.md"
|
|
124
|
+
: > "${TIMING_SUMMARY}"
|
|
125
|
+
export TREESEED_PROVIDER_TIMING_SUMMARY_PATH="${TIMING_SUMMARY}"
|
|
123
126
|
EXTRA_ARGS=()
|
|
124
127
|
if [[ -n "${TREESEED_WORKFLOW_PROJECT:-}" ]]; then EXTRA_ARGS+=(--project-id "${TREESEED_WORKFLOW_PROJECT}"); fi
|
|
125
128
|
if [[ -n "${TREESEED_WORKFLOW_PREVIEW_ID:-}" ]]; then EXTRA_ARGS+=(--preview-id "${TREESEED_WORKFLOW_PREVIEW_ID}"); fi
|
|
126
129
|
node ./packages/sdk/scripts/run-ts.mjs ./packages/sdk/scripts/tenant-workflow-action.ts --action "${TREESEED_WORKFLOW_ACTION}" --environment "${TREESEED_WORKFLOW_ENVIRONMENT}" "${EXTRA_ARGS[@]}"
|
|
130
|
+
if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -s "${TIMING_SUMMARY}" ]]; then cat "${TIMING_SUMMARY}" >> "${GITHUB_STEP_SUMMARY}"; fi
|