@treeseed/sdk 0.10.22 → 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/db/market-schema.js +3 -2
- package/dist/market-client.d.ts +4 -0
- package/dist/market-client.js +6 -0
- package/dist/operations/providers/default.js +26 -4
- package/dist/operations/repository-operations.js +6 -2
- 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 +2 -1
- package/dist/operations/services/deploy.d.ts +18 -1
- package/dist/operations/services/deploy.js +176 -24
- package/dist/operations/services/github-automation.d.ts +10 -1
- package/dist/operations/services/github-automation.js +18 -4
- package/dist/operations/services/hosting-audit.d.ts +2 -1
- package/dist/operations/services/hosting-audit.js +12 -1
- package/dist/operations/services/hub-launch.d.ts +1 -0
- package/dist/operations/services/hub-launch.js +1 -0
- package/dist/operations/services/hub-provider-launch.d.ts +9 -0
- package/dist/operations/services/hub-provider-launch.js +140 -40
- package/dist/operations/services/managed-host-security.d.ts +1 -1
- package/dist/operations/services/managed-host-security.js +4 -1
- package/dist/operations/services/project-platform.d.ts +25 -0
- package/dist/operations/services/project-platform.js +91 -23
- package/dist/operations/services/railway-api.js +2 -1
- package/dist/operations/services/railway-deploy.d.ts +32 -2
- package/dist/operations/services/railway-deploy.js +94 -27
- 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/platform/environment.d.ts +1 -1
- package/dist/platform/environment.js +1 -1
- package/dist/reconcile/builtin-adapters.js +155 -25
- package/dist/reconcile/contracts.d.ts +1 -1
- package/dist/reconcile/desired-state.js +17 -1
- package/dist/reconcile/engine.d.ts +2 -0
- package/dist/reconcile/engine.js +58 -3
- package/dist/reconcile/units.js +1 -0
- package/dist/sdk-types.d.ts +1 -1
- package/dist/sdk-types.js +2 -0
- package/dist/timing.d.ts +20 -0
- package/dist/timing.js +73 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +150 -0
- package/dist/workflow/operations.d.ts +2 -0
- package/drizzle/market/0000_market_control_plane.sql +3 -3
- package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +4 -0
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
import type { TreeseedDeployConfig, TreeseedTenantConfig } from './contracts.ts';
|
|
2
2
|
import { type LoadedTreeseedPluginEntry } from './plugins.ts';
|
|
3
3
|
export declare const TREESEED_ENVIRONMENT_SCOPES: readonly ["local", "staging", "prod"];
|
|
4
|
-
export declare const TREESEED_ENVIRONMENT_REQUIREMENTS: readonly ["required", "conditional", "optional"];
|
|
4
|
+
export declare const TREESEED_ENVIRONMENT_REQUIREMENTS: readonly ["required", "conditional", "optional", "generated"];
|
|
5
5
|
export declare const TREESEED_ENVIRONMENT_TARGETS: readonly ["local-runtime", "local-cloudflare", "github-secret", "github-variable", "cloudflare-secret", "cloudflare-var", "railway-secret", "railway-var", "config-file"];
|
|
6
6
|
export declare const TREESEED_ENVIRONMENT_PURPOSES: readonly ["dev", "save", "deploy", "destroy", "config"];
|
|
7
7
|
export declare const TREESEED_ENVIRONMENT_SENSITIVITY: readonly ["secret", "plain", "derived"];
|
|
@@ -8,7 +8,7 @@ import { loadTreeseedDeployConfig } from "./deploy-config.js";
|
|
|
8
8
|
import { loadTreeseedPlugins } from "./plugins.js";
|
|
9
9
|
import { loadTreeseedManifest } from "./tenant-config.js";
|
|
10
10
|
const TREESEED_ENVIRONMENT_SCOPES = ["local", "staging", "prod"];
|
|
11
|
-
const TREESEED_ENVIRONMENT_REQUIREMENTS = ["required", "conditional", "optional"];
|
|
11
|
+
const TREESEED_ENVIRONMENT_REQUIREMENTS = ["required", "conditional", "optional", "generated"];
|
|
12
12
|
const TREESEED_ENVIRONMENT_TARGETS = [
|
|
13
13
|
"local-runtime",
|
|
14
14
|
"local-cloudflare",
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
buildProvisioningSummary,
|
|
8
8
|
buildSecretMap,
|
|
9
9
|
cloudflareApiRequest,
|
|
10
|
+
createTurnstileWidget,
|
|
10
11
|
createBranchPreviewDeployTarget,
|
|
11
12
|
createPersistentDeployTarget,
|
|
12
13
|
destroyCloudflareResources,
|
|
14
|
+
getTurnstileWidget,
|
|
13
15
|
hasProvisionedCloudflareResources,
|
|
14
16
|
isWranglerAlreadyExistsError,
|
|
15
17
|
listD1Databases,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
listPagesProjects,
|
|
18
20
|
listQueues,
|
|
19
21
|
listR2Buckets,
|
|
22
|
+
listTurnstileWidgets,
|
|
20
23
|
mergeCloudflarePagesDeploymentConfig,
|
|
21
24
|
loadDeployState,
|
|
22
25
|
queueId,
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
resolveCloudflareZoneIdForHost,
|
|
27
30
|
runWrangler,
|
|
28
31
|
scopeFromTarget,
|
|
32
|
+
updateTurnstileWidget,
|
|
29
33
|
writeDeployState
|
|
30
34
|
} from "../operations/services/deploy.js";
|
|
31
35
|
import {
|
|
@@ -33,7 +37,6 @@ import {
|
|
|
33
37
|
deriveRailwayMarketOperationsRunnerVolumeName,
|
|
34
38
|
ensureRailwayProjectContext,
|
|
35
39
|
ensureRailwayServiceVolumeWithCliFallback,
|
|
36
|
-
listRailwayServiceVolumesWithCli,
|
|
37
40
|
runRailway,
|
|
38
41
|
validateRailwayDeployPrerequisites
|
|
39
42
|
} from "../operations/services/railway-deploy.js";
|
|
@@ -293,6 +296,30 @@ function getCloudflareKvById(env, namespaceId) {
|
|
|
293
296
|
);
|
|
294
297
|
return payload?.result ?? null;
|
|
295
298
|
}
|
|
299
|
+
function normalizeTurnstileDomains(value) {
|
|
300
|
+
return [...new Set((Array.isArray(value) ? value : []).map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean))].sort();
|
|
301
|
+
}
|
|
302
|
+
function turnstileDomainsEqual(left, right) {
|
|
303
|
+
return JSON.stringify(normalizeTurnstileDomains(left)) === JSON.stringify(normalizeTurnstileDomains(right));
|
|
304
|
+
}
|
|
305
|
+
function mergeTurnstileWidget(...widgets) {
|
|
306
|
+
const merged = {};
|
|
307
|
+
for (const widget of widgets) {
|
|
308
|
+
if (!widget) continue;
|
|
309
|
+
for (const [key, value] of Object.entries(widget)) {
|
|
310
|
+
if (value === void 0 || value === null) continue;
|
|
311
|
+
if (key === "domains" && !Array.isArray(value)) continue;
|
|
312
|
+
merged[key] = value;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
316
|
+
}
|
|
317
|
+
function findTurnstileWidget(widgets, current, desiredName) {
|
|
318
|
+
return widgets.find((entry) => {
|
|
319
|
+
if (!entry || typeof entry !== "object") return false;
|
|
320
|
+
return current?.sitekey && entry.sitekey === current.sitekey || desiredName && entry.name === desiredName;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
296
323
|
function listCloudflareQueuesViaApi(env) {
|
|
297
324
|
const accountId = env.CLOUDFLARE_ACCOUNT_ID;
|
|
298
325
|
if (!accountId) {
|
|
@@ -314,7 +341,8 @@ function cloudflareObservationSnapshot(input, forceRefresh = false) {
|
|
|
314
341
|
d1Databases: listD1Databases(input.context.tenantRoot, env),
|
|
315
342
|
queues: listCloudflareQueuesViaApi(env),
|
|
316
343
|
buckets: listR2Buckets(input.context.tenantRoot, env),
|
|
317
|
-
pagesProjects: listPagesProjects(input.context.tenantRoot, env)
|
|
344
|
+
pagesProjects: listPagesProjects(input.context.tenantRoot, env),
|
|
345
|
+
turnstileWidgets: listTurnstileWidgets(input.context.tenantRoot, env)
|
|
318
346
|
};
|
|
319
347
|
}, forceRefresh);
|
|
320
348
|
}
|
|
@@ -591,7 +619,10 @@ function collectCloudflareEnvironmentSync(input) {
|
|
|
591
619
|
const generatedSecrets = buildSecretMap(input.context.deployConfig, state);
|
|
592
620
|
const publicVars = buildPublicVars(input.context.deployConfig, { target });
|
|
593
621
|
const secrets = {};
|
|
594
|
-
const
|
|
622
|
+
const generatedTurnstileSiteKey = typeof state.turnstileWidgets?.formGuard?.sitekey === "string" ? state.turnstileWidgets.formGuard.sitekey : "";
|
|
623
|
+
const vars = {
|
|
624
|
+
...publicVars
|
|
625
|
+
};
|
|
595
626
|
const secretNames = /* @__PURE__ */ new Set();
|
|
596
627
|
const varNames = new Set(Object.keys(publicVars));
|
|
597
628
|
for (const entry of registry.entries) {
|
|
@@ -611,8 +642,13 @@ function collectCloudflareEnvironmentSync(input) {
|
|
|
611
642
|
varNames.add(entry.id);
|
|
612
643
|
}
|
|
613
644
|
}
|
|
645
|
+
if (generatedTurnstileSiteKey) {
|
|
646
|
+
vars.TREESEED_PUBLIC_TURNSTILE_SITE_KEY = generatedTurnstileSiteKey;
|
|
647
|
+
varNames.add("TREESEED_PUBLIC_TURNSTILE_SITE_KEY");
|
|
648
|
+
}
|
|
614
649
|
for (const [key, value] of Object.entries(generatedSecrets)) {
|
|
615
|
-
|
|
650
|
+
const exposeRuntimeSecret = key === "TREESEED_TURNSTILE_SECRET_KEY" || shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, key);
|
|
651
|
+
if (typeof value === "string" && value.length > 0 && exposeRuntimeSecret) {
|
|
616
652
|
secrets[key] = value;
|
|
617
653
|
secretNames.add(key);
|
|
618
654
|
}
|
|
@@ -735,6 +771,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
735
771
|
const queues = dryRun ? [] : listQueues(input.context.tenantRoot, env);
|
|
736
772
|
const buckets = dryRun ? [] : listR2Buckets(input.context.tenantRoot, env);
|
|
737
773
|
const pagesProjects = dryRun ? [] : listPagesProjects(input.context.tenantRoot, env);
|
|
774
|
+
const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(input.context.tenantRoot, env);
|
|
738
775
|
const runStep = (label, fn) => {
|
|
739
776
|
try {
|
|
740
777
|
return fn();
|
|
@@ -945,11 +982,59 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
|
|
|
945
982
|
}
|
|
946
983
|
current.url = `https://${current.projectName}.pages.dev`;
|
|
947
984
|
};
|
|
985
|
+
const ensureTurnstileWidget = () => {
|
|
986
|
+
if (deployConfig.turnstile?.enabled !== true) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const current = state.turnstileWidgets?.formGuard;
|
|
990
|
+
if (!current?.name) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const pagesHost = state.pages?.url ? new URL(state.pages.url).hostname : null;
|
|
994
|
+
const desiredDomains = normalizeTurnstileDomains([
|
|
995
|
+
...Array.isArray(current.domains) ? current.domains : [],
|
|
996
|
+
pagesHost
|
|
997
|
+
]);
|
|
998
|
+
current.domains = desiredDomains;
|
|
999
|
+
current.mode = "managed";
|
|
1000
|
+
current.managed = true;
|
|
1001
|
+
const existing = findTurnstileWidget(turnstileWidgets, current, current.name);
|
|
1002
|
+
if (dryRun) {
|
|
1003
|
+
current.sitekey = current.sitekey ?? `dryrun-${current.name}-sitekey`;
|
|
1004
|
+
current.secret = current.secret ?? `dryrun-${current.name}-secret`;
|
|
1005
|
+
current.lastSyncedAt = nowIso();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (existing?.sitekey) {
|
|
1009
|
+
const needsUpdate = existing.name !== current.name || existing.mode !== "managed" || !turnstileDomainsEqual(existing.domains, desiredDomains);
|
|
1010
|
+
const updated = needsUpdate ? updateTurnstileWidget(env, String(existing.sitekey), {
|
|
1011
|
+
name: current.name,
|
|
1012
|
+
domains: desiredDomains,
|
|
1013
|
+
mode: "managed"
|
|
1014
|
+
}) : existing;
|
|
1015
|
+
current.sitekey = String(updated?.sitekey ?? existing.sitekey);
|
|
1016
|
+
current.secret = String(updated?.secret ?? current.secret ?? "");
|
|
1017
|
+
current.lastSyncedAt = nowIso();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const created = createTurnstileWidget(env, {
|
|
1021
|
+
name: current.name,
|
|
1022
|
+
domains: desiredDomains,
|
|
1023
|
+
mode: "managed"
|
|
1024
|
+
});
|
|
1025
|
+
if (!created?.sitekey || !created?.secret) {
|
|
1026
|
+
throw new Error(`Unable to resolve created Turnstile widget keys for ${current.name}.`);
|
|
1027
|
+
}
|
|
1028
|
+
current.sitekey = String(created.sitekey);
|
|
1029
|
+
current.secret = String(created.secret);
|
|
1030
|
+
current.lastSyncedAt = nowIso();
|
|
1031
|
+
};
|
|
948
1032
|
runStep("kv-form-guard", () => ensureKv("FORM_GUARD_KV"));
|
|
949
1033
|
runStep("d1", ensureD1);
|
|
950
1034
|
runStep("queue", ensureQueue);
|
|
951
1035
|
runStep("r2", ensureR2Bucket);
|
|
952
1036
|
runStep("pages", ensurePagesProject);
|
|
1037
|
+
runStep("turnstile-widget", ensureTurnstileWidget);
|
|
953
1038
|
runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { dryRun, env }));
|
|
954
1039
|
state.readiness.configured = true;
|
|
955
1040
|
state.readiness.provisioned = hasProvisionedCloudflareResources(state);
|
|
@@ -989,7 +1074,7 @@ function syncCloudflareSecretsForTarget(input, { dryRun = false } = {}) {
|
|
|
989
1074
|
}
|
|
990
1075
|
function observeCloudflareUnit(input) {
|
|
991
1076
|
const snapshot = cloudflareObservationSnapshot(input);
|
|
992
|
-
const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects } = snapshot;
|
|
1077
|
+
const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets } = snapshot;
|
|
993
1078
|
switch (input.unit.unitType) {
|
|
994
1079
|
case "queue": {
|
|
995
1080
|
const liveQueue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
|
|
@@ -1042,6 +1127,17 @@ function observeCloudflareUnit(input) {
|
|
|
1042
1127
|
warnings: []
|
|
1043
1128
|
};
|
|
1044
1129
|
}
|
|
1130
|
+
case "turnstile-widget": {
|
|
1131
|
+
const current = state.turnstileWidgets?.formGuard ?? {};
|
|
1132
|
+
const liveWidget = findTurnstileWidget(turnstileWidgets, current, input.unit.spec.name);
|
|
1133
|
+
return {
|
|
1134
|
+
exists: Boolean(liveWidget?.sitekey || current?.sitekey),
|
|
1135
|
+
status: liveWidget?.sitekey ? "ready" : "pending",
|
|
1136
|
+
live: { ...current, ...liveWidget ?? {} },
|
|
1137
|
+
locators: { sitekey: String(liveWidget?.sitekey ?? current?.sitekey ?? "") || null },
|
|
1138
|
+
warnings: []
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1045
1141
|
case "pages-project": {
|
|
1046
1142
|
const liveProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
|
|
1047
1143
|
return {
|
|
@@ -1081,7 +1177,7 @@ function verifyCloudflareUnitOnce(input, postconditions) {
|
|
|
1081
1177
|
]);
|
|
1082
1178
|
}
|
|
1083
1179
|
const snapshot = cloudflareObservationSnapshot(input, true);
|
|
1084
|
-
const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, env } = snapshot;
|
|
1180
|
+
const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets, env } = snapshot;
|
|
1085
1181
|
switch (input.unit.unitType) {
|
|
1086
1182
|
case "queue": {
|
|
1087
1183
|
const queue = state.queues?.agentWork;
|
|
@@ -1149,6 +1245,49 @@ function verifyCloudflareUnitOnce(input, postconditions) {
|
|
|
1149
1245
|
})
|
|
1150
1246
|
]);
|
|
1151
1247
|
}
|
|
1248
|
+
case "turnstile-widget": {
|
|
1249
|
+
const current = state.turnstileWidgets?.formGuard ?? {};
|
|
1250
|
+
const cachedLive = findTurnstileWidget(turnstileWidgets, current, input.unit.spec.name);
|
|
1251
|
+
const refreshedListedLive = findTurnstileWidget(
|
|
1252
|
+
listTurnstileWidgets(input.context.tenantRoot, env),
|
|
1253
|
+
current,
|
|
1254
|
+
input.unit.spec.name
|
|
1255
|
+
);
|
|
1256
|
+
const refreshedLive = current.sitekey ? getTurnstileWidget(env, String(current.sitekey)) : null;
|
|
1257
|
+
const live = mergeTurnstileWidget(
|
|
1258
|
+
cachedLive,
|
|
1259
|
+
refreshedListedLive,
|
|
1260
|
+
refreshedLive
|
|
1261
|
+
);
|
|
1262
|
+
const pagesHost = state.pages?.url ? new URL(state.pages.url).hostname : null;
|
|
1263
|
+
const desiredDomains = normalizeTurnstileDomains([
|
|
1264
|
+
...Array.isArray(input.unit.spec.domains) ? input.unit.spec.domains : [],
|
|
1265
|
+
...Array.isArray(current.domains) ? current.domains : [],
|
|
1266
|
+
pagesHost
|
|
1267
|
+
]);
|
|
1268
|
+
return summarizeVerification(input.unit.unitId, [
|
|
1269
|
+
verificationCheck("turnstile.exists", "Turnstile widget exists by name and sitekey", "api", {
|
|
1270
|
+
exists: Boolean(live?.sitekey),
|
|
1271
|
+
expected: input.unit.spec.name ?? null,
|
|
1272
|
+
observed: live ? { name: live.name, sitekey: live.sitekey } : null,
|
|
1273
|
+
issues: live?.sitekey ? [] : [`Cloudflare Turnstile widget ${String(input.unit.spec.name ?? "(unset)")} was not found after reconcile.`]
|
|
1274
|
+
}),
|
|
1275
|
+
verificationCheck("turnstile.mode", "Turnstile widget mode is managed", "api", {
|
|
1276
|
+
exists: Boolean(live?.sitekey),
|
|
1277
|
+
configured: live?.mode === "managed",
|
|
1278
|
+
expected: "managed",
|
|
1279
|
+
observed: live?.mode ?? null,
|
|
1280
|
+
issues: live?.mode === "managed" ? [] : ["Turnstile widget mode does not match managed."]
|
|
1281
|
+
}),
|
|
1282
|
+
verificationCheck("turnstile.domains", "Turnstile widget domains match desired config", "api", {
|
|
1283
|
+
exists: Boolean(live?.sitekey),
|
|
1284
|
+
configured: turnstileDomainsEqual(live?.domains, desiredDomains),
|
|
1285
|
+
expected: desiredDomains,
|
|
1286
|
+
observed: normalizeTurnstileDomains(live?.domains),
|
|
1287
|
+
issues: turnstileDomainsEqual(live?.domains, desiredDomains) ? [] : ["Turnstile widget domains do not match desired config."]
|
|
1288
|
+
})
|
|
1289
|
+
]);
|
|
1290
|
+
}
|
|
1152
1291
|
case "content-store": {
|
|
1153
1292
|
const bucketName = state.content?.bucketName;
|
|
1154
1293
|
const live = buckets.find((entry) => entry?.name === bucketName);
|
|
@@ -1344,6 +1483,12 @@ function buildCloudflareAdapter(unitType) {
|
|
|
1344
1483
|
{ key: "kv.exists", description: "KV namespace exists by title and id" },
|
|
1345
1484
|
{ key: "kv.binding", description: "KV binding matches desired config" }
|
|
1346
1485
|
];
|
|
1486
|
+
case "turnstile-widget":
|
|
1487
|
+
return [
|
|
1488
|
+
{ key: "turnstile.exists", description: "Turnstile widget exists by name and sitekey" },
|
|
1489
|
+
{ key: "turnstile.mode", description: "Turnstile widget mode is managed" },
|
|
1490
|
+
{ key: "turnstile.domains", description: "Turnstile widget domains match desired config" }
|
|
1491
|
+
];
|
|
1347
1492
|
case "content-store":
|
|
1348
1493
|
return [
|
|
1349
1494
|
{ key: "r2.exists", description: "R2 bucket exists by name" },
|
|
@@ -1621,7 +1766,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false } = {}) {
|
|
|
1621
1766
|
preferCli: entry.configuredService.key === "marketOperationsRunner",
|
|
1622
1767
|
env: topology.env
|
|
1623
1768
|
});
|
|
1624
|
-
if (
|
|
1769
|
+
if (volume.instance?.serviceId !== entry.service.id || volume.instance?.environmentId !== entry.environment.id || volume.instance?.mountPath !== entry.configuredService.volumeMountPath) {
|
|
1625
1770
|
ensureRailwayProjectContext(entry.configuredService, { env: topology.env, capture: true });
|
|
1626
1771
|
let attachMessage = "";
|
|
1627
1772
|
let attached = false;
|
|
@@ -2258,25 +2403,9 @@ async function verifyRailwayUnit(input) {
|
|
|
2258
2403
|
const volumes = entry.project?.id ? await listRailwayVolumes({ projectId: entry.project.id, env: topology.env }) : [];
|
|
2259
2404
|
const expectedServiceId = entry.service?.id ?? null;
|
|
2260
2405
|
const expectedEnvironmentId = entry.environment?.id ?? null;
|
|
2261
|
-
|
|
2406
|
+
const mountedVolume = volumes.find((volume) => volume.instances.some(
|
|
2262
2407
|
(instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
|
|
2263
2408
|
)) ?? null;
|
|
2264
|
-
let mountedVolumeSource = "api";
|
|
2265
|
-
if (!mountedVolume && serviceKey === "marketOperationsRunner" && expectedServiceId && expectedEnvironmentId) {
|
|
2266
|
-
ensureRailwayProjectContext(service, { env: topology.env, capture: true });
|
|
2267
|
-
const cliVolumes = listRailwayServiceVolumesWithCli({
|
|
2268
|
-
cwd: service.rootDir,
|
|
2269
|
-
serviceId: expectedServiceId,
|
|
2270
|
-
environmentId: expectedEnvironmentId,
|
|
2271
|
-
name: deriveRailwayMarketOperationsRunnerVolumeName(entry.service?.name ?? service.serviceName ?? service.key, entry.environment?.name ?? service.railwayEnvironment),
|
|
2272
|
-
mountPath: service.volumeMountPath,
|
|
2273
|
-
env: topology.env
|
|
2274
|
-
});
|
|
2275
|
-
mountedVolume = cliVolumes.find((volume) => volume.instances.some(
|
|
2276
|
-
(instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
|
|
2277
|
-
)) ?? null;
|
|
2278
|
-
mountedVolumeSource = mountedVolume ? "cli" : "api";
|
|
2279
|
-
}
|
|
2280
2409
|
checks.push(verificationCheck("railway.volume:data", "Railway service has persistent data volume mounted", "api", {
|
|
2281
2410
|
exists: Boolean(mountedVolume),
|
|
2282
2411
|
configured: Boolean(mountedVolume),
|
|
@@ -2284,7 +2413,7 @@ async function verifyRailwayUnit(input) {
|
|
|
2284
2413
|
observed: mountedVolume ? {
|
|
2285
2414
|
name: mountedVolume.name,
|
|
2286
2415
|
mountPath: service.volumeMountPath,
|
|
2287
|
-
source:
|
|
2416
|
+
source: "api"
|
|
2288
2417
|
} : null,
|
|
2289
2418
|
issues: mountedVolume ? [] : [`Railway service ${service.serviceName ?? service.key} is missing a persistent volume mounted at ${service.volumeMountPath}.`]
|
|
2290
2419
|
}));
|
|
@@ -2494,6 +2623,7 @@ function createCloudflareReconcileAdapters() {
|
|
|
2494
2623
|
buildCloudflareAdapter("database"),
|
|
2495
2624
|
buildCloudflareAdapter("content-store"),
|
|
2496
2625
|
buildCloudflareAdapter("kv-form-guard"),
|
|
2626
|
+
buildCloudflareAdapter("turnstile-widget"),
|
|
2497
2627
|
buildCloudflareAdapter("pages-project"),
|
|
2498
2628
|
buildCloudflareAdapter("edge-worker"),
|
|
2499
2629
|
buildCustomDomainAdapter("custom-domain:web", "cloudflare"),
|
|
@@ -3,7 +3,7 @@ export type TreeseedReconcileProviderId = string;
|
|
|
3
3
|
export type TreeseedReconcileActionKind = 'noop' | 'create' | 'update' | 'reuse' | 'drift_correct' | 'destroy';
|
|
4
4
|
export type TreeseedReconcileStatusKind = 'pending' | 'ready' | 'drifted' | 'error';
|
|
5
5
|
export type TreeseedReconcileVerificationSource = 'cli' | 'api' | 'sdk' | 'derived';
|
|
6
|
-
export type TreeseedReconcileUnitType = 'web-ui' | 'api-runtime' | 'market-operations-runner-runtime' | 'workday-manager-runtime' | 'worker-runner-runtime' | 'edge-worker' | 'content-store' | 'queue' | 'database' | 'kv-form-guard' | 'pages-project' | 'custom-domain:web' | 'custom-domain:api' | 'dns-record' | 'railway-service:api' | 'railway-service:market-operations-runner' | 'railway-service:workday-manager' | 'railway-service:worker-runner';
|
|
6
|
+
export type TreeseedReconcileUnitType = 'web-ui' | 'api-runtime' | 'market-operations-runner-runtime' | 'workday-manager-runtime' | 'worker-runner-runtime' | 'edge-worker' | 'content-store' | 'queue' | 'database' | 'kv-form-guard' | 'turnstile-widget' | 'pages-project' | 'custom-domain:web' | 'custom-domain:api' | 'dns-record' | 'railway-service:api' | 'railway-service:market-operations-runner' | 'railway-service:workday-manager' | 'railway-service:worker-runner';
|
|
7
7
|
export type TreeseedReconcileTarget = {
|
|
8
8
|
kind: 'persistent';
|
|
9
9
|
scope: 'local' | 'staging' | 'prod';
|
|
@@ -76,6 +76,22 @@ function deriveTreeseedDesiredUnits({
|
|
|
76
76
|
secrets: {},
|
|
77
77
|
metadata: { bootstrapSystem: "web" }
|
|
78
78
|
});
|
|
79
|
+
const turnstileWidgetId = legacyState.turnstileWidgets?.formGuard?.name && deployConfig.turnstile?.enabled === true ? add({
|
|
80
|
+
unitId: createTreeseedReconcileUnitId("turnstile-widget", legacyState.turnstileWidgets.formGuard.name),
|
|
81
|
+
unitType: "turnstile-widget",
|
|
82
|
+
provider: "cloudflare",
|
|
83
|
+
identity,
|
|
84
|
+
target,
|
|
85
|
+
logicalName: legacyState.turnstileWidgets.formGuard.name,
|
|
86
|
+
dependencies: [],
|
|
87
|
+
spec: {
|
|
88
|
+
name: legacyState.turnstileWidgets.formGuard.name,
|
|
89
|
+
domains: legacyState.turnstileWidgets.formGuard.domains ?? [],
|
|
90
|
+
mode: "managed"
|
|
91
|
+
},
|
|
92
|
+
secrets: {},
|
|
93
|
+
metadata: { bootstrapSystem: "web" }
|
|
94
|
+
}) : null;
|
|
79
95
|
const contentStoreId = add({
|
|
80
96
|
unitId: createTreeseedReconcileUnitId("content-store", legacyState.content.bucketName ?? deployConfig.slug),
|
|
81
97
|
unitType: "content-store",
|
|
@@ -118,7 +134,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
118
134
|
identity,
|
|
119
135
|
target,
|
|
120
136
|
logicalName: legacyState.workerName,
|
|
121
|
-
dependencies: [queueId, databaseId, formGuardKvId, contentStoreId, pagesProjectId],
|
|
137
|
+
dependencies: [queueId, databaseId, formGuardKvId, ...turnstileWidgetId ? [turnstileWidgetId] : [], contentStoreId, pagesProjectId],
|
|
122
138
|
spec: {
|
|
123
139
|
workerName: legacyState.workerName
|
|
124
140
|
},
|
|
@@ -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/reconcile/units.js
CHANGED
package/dist/sdk-types.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const PROJECT_WEB_DEPLOYMENT_ACTIONS: readonly ["deploy_web", "pu
|
|
|
19
19
|
export declare const PROJECT_DEPLOYMENT_ENVIRONMENTS: readonly ["staging", "prod"];
|
|
20
20
|
export declare const PROJECT_DEPLOYMENT_STATUSES: readonly ["pending", "queued", "claimed", "dispatching", "running", "monitoring", "succeeded", "failed", "cancelled", "timed_out"];
|
|
21
21
|
export declare const PROJECT_INFRA_RESOURCE_PROVIDERS: readonly ["cloudflare", "railway", "github", "market"];
|
|
22
|
-
export declare const PROJECT_INFRA_RESOURCE_KINDS: readonly ["pages", "worker", "r2", "d1", "queue", "dlq", "railway_project", "railway_service", "railway_schedule"];
|
|
22
|
+
export declare const PROJECT_INFRA_RESOURCE_KINDS: readonly ["pages", "worker", "kv", "turnstile-widget", "r2", "d1", "queue", "dlq", "railway_project", "railway_service", "railway_schedule"];
|
|
23
23
|
export declare const AGENT_POOL_STATUSES: readonly ["pending", "active", "degraded", "offline"];
|
|
24
24
|
export type SdkBuiltinModelName = (typeof SDK_MODEL_NAMES)[number];
|
|
25
25
|
export type SdkModelName = SdkBuiltinModelName | (string & {});
|
package/dist/sdk-types.js
CHANGED
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;
|