@treeseed/sdk 0.12.25 → 0.12.27

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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/guarantees/index.js +15 -1
  3. package/dist/hosting/builtins.js +4 -4
  4. package/dist/hosting/contracts.d.ts +3 -3
  5. package/dist/hosting/graph.d.ts +4 -4
  6. package/dist/hosting/graph.js +17 -17
  7. package/dist/operations/agent-tools.d.ts +1 -1
  8. package/dist/operations/agent-tools.js +1 -1
  9. package/dist/operations/providers/default.js +2 -2
  10. package/dist/operations/services/config-runtime.d.ts +9 -9
  11. package/dist/operations/services/config-runtime.js +10 -10
  12. package/dist/operations/services/deploy.d.ts +8 -8
  13. package/dist/operations/services/deploy.js +151 -151
  14. package/dist/operations/services/github-automation.d.ts +6 -6
  15. package/dist/operations/services/github-automation.js +8 -8
  16. package/dist/operations/services/hub-launch.d.ts +1 -1
  17. package/dist/operations/services/hub-launch.js +1 -1
  18. package/dist/operations/services/package-adapters.d.ts +2 -2
  19. package/dist/operations/services/package-adapters.js +1 -1
  20. package/dist/operations/services/project-host-operations.d.ts +1 -1
  21. package/dist/operations/services/project-host-operations.js +1 -1
  22. package/dist/operations/services/project-platform.d.ts +8 -8
  23. package/dist/operations/services/project-platform.js +31 -31
  24. package/dist/operations/services/project-web-monitor.d.ts +1 -1
  25. package/dist/operations/services/project-web-monitor.js +4 -4
  26. package/dist/operations/services/railway-deploy.d.ts +4 -4
  27. package/dist/operations/services/railway-deploy.js +7 -7
  28. package/dist/operations/services/repository-save-orchestrator.js +3 -3
  29. package/dist/operations/services/template-secret-sync.d.ts +1 -1
  30. package/dist/operations/services/template-secret-sync.js +3 -3
  31. package/dist/reconcile/builtin-adapters.js +137 -44
  32. package/dist/reconcile/contracts.d.ts +1 -1
  33. package/dist/reconcile/engine.d.ts +4 -4
  34. package/dist/reconcile/engine.js +12 -12
  35. package/dist/reconcile/providers/release-private.d.ts +1 -1
  36. package/dist/scripts/tenant-deploy.js +9 -9
  37. package/dist/scripts/tenant-destroy.js +7 -7
  38. package/dist/scripts/tenant-workflow-action.js +4 -4
  39. package/dist/scripts/test-scaffold.js +2 -2
  40. package/dist/scripts/workspace-command-e2e.js +4 -4
  41. package/dist/sdk-types.d.ts +2 -2
  42. package/dist/treedx/generated/openapi-types.d.ts +11 -11
  43. package/dist/treedx/types.d.ts +15 -15
  44. package/dist/workflow/operations.d.ts +6 -4
  45. package/dist/workflow/operations.js +46 -23
  46. package/dist/workflow.d.ts +0 -9
  47. package/package.json +1 -1
@@ -56,10 +56,12 @@ import {
56
56
  deleteRailwayService,
57
57
  deleteRailwayVolume,
58
58
  deployRailwayServiceInstance,
59
+ inspectRailwayServiceDeploymentHealth,
59
60
  listRailwayEnvironmentServices,
60
61
  getRailwayServiceInstance,
61
62
  getRailwayProject,
62
63
  listRailwayCustomDomains,
64
+ listRailwayServiceDomains,
63
65
  listRailwayEnvironments,
64
66
  listRailwayProjects,
65
67
  listRailwayServices,
@@ -297,7 +299,7 @@ function buildPackageWorkflowAdapter() {
297
299
  },
298
300
  apply(input) {
299
301
  const packageId = typeof input.unit.spec.packageId === "string" ? input.unit.spec.packageId : typeof input.unit.metadata.packageId === "string" ? input.unit.metadata.packageId : null;
300
- if (packageId && input.context.dryRun !== true && input.diff.action !== "noop") {
302
+ if (packageId && input.context.planOnly !== true && input.diff.action !== "noop") {
301
303
  const sync = syncTreeseedPackageWorkflows({
302
304
  root: input.context.tenantRoot,
303
305
  packageId,
@@ -835,7 +837,7 @@ function buildLocalDockerComposeAdapter() {
835
837
  const composeFiles = observedOrResolvedComposeFiles(input);
836
838
  const cwd = String(input.observed.live.cwd ?? input.context.tenantRoot);
837
839
  const projectName = String(input.unit.spec.projectName ?? "treeseed-capacity-provider");
838
- if (input.context.dryRun !== true && composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile))) {
840
+ if (input.context.planOnly !== true && composeFiles.length > 0 && composeFiles.every((composeFile) => existsSync(composeFile))) {
839
841
  const result = runDockerCompose({
840
842
  composeFiles,
841
843
  projectName,
@@ -1269,7 +1271,7 @@ function buildLocalProcessAdapter() {
1269
1271
  },
1270
1272
  async destroy(input) {
1271
1273
  const surfaces = Array.isArray(input.unit.spec.surfaces) ? input.unit.spec.surfaces.filter((entry) => typeof entry === "string") : [String(input.unit.spec.processId ?? input.unit.logicalName)];
1272
- if (input.context.dryRun !== true) {
1274
+ if (input.context.planOnly !== true) {
1273
1275
  await runManagedDevAction({
1274
1276
  tenantRoot: input.context.tenantRoot,
1275
1277
  action: "stop",
@@ -1796,7 +1798,7 @@ function buildReleaseGateAdapter() {
1796
1798
  parentContext: input.context,
1797
1799
  selector,
1798
1800
  target: { kind: "persistent", scope: environment },
1799
- dryRun: input.context.dryRun === true
1801
+ planOnly: input.context.planOnly === true
1800
1802
  });
1801
1803
  return genericResult(input, { ...input.observed.live, nested, fingerprint: input.unit.spec.fingerprint });
1802
1804
  }
@@ -1905,7 +1907,7 @@ function buildWorkflowMetaAdapter() {
1905
1907
  parentContext: input.context,
1906
1908
  selector,
1907
1909
  target: input.context.target,
1908
- dryRun: input.context.dryRun === true
1910
+ planOnly: input.context.planOnly === true
1909
1911
  });
1910
1912
  return genericResult(input, { ...input.observed.live, nested, fingerprint });
1911
1913
  }
@@ -1999,7 +2001,7 @@ function buildCloudflareEnv(input) {
1999
2001
  };
2000
2002
  }
2001
2003
  function hasLiveResourceId(value) {
2002
- return typeof value === "string" && value.length > 0 && !value.startsWith("dryrun-") && !value.startsWith("local-") && !value.endsWith("-id") && !value.endsWith("-preview-id");
2004
+ return typeof value === "string" && value.length > 0 && !value.startsWith("plan-") && !value.startsWith("local-") && !value.endsWith("-id") && !value.endsWith("-preview-id");
2003
2005
  }
2004
2006
  function buildRailwayEnv(input, scope) {
2005
2007
  const values = resolveReconcileEnvironmentValues(input, scope);
@@ -2374,6 +2376,58 @@ async function ensureRailwayCustomDomain(input, service, domain, env, identifier
2374
2376
  }
2375
2377
  return ensured.domain;
2376
2378
  }
2379
+ async function observeRailwayCustomDomainLive(input, domain) {
2380
+ if (!domain) {
2381
+ return null;
2382
+ }
2383
+ const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
2384
+ const serviceKey = String(input.unit.metadata.serviceKey ?? "api").trim();
2385
+ const topology = await resolveRailwayTopologyForScope(input, scope, {
2386
+ refresh: true,
2387
+ serviceKeys: [serviceKey],
2388
+ includeInstances: false,
2389
+ includeVariables: false
2390
+ });
2391
+ const entry = topology.services.get(serviceKey) ?? null;
2392
+ const identifiers = {
2393
+ projectId: entry?.project?.id ?? null,
2394
+ environmentId: entry?.environment?.id ?? null,
2395
+ serviceId: entry?.service?.id ?? null
2396
+ };
2397
+ if (!identifiers.projectId || !identifiers.environmentId || !identifiers.serviceId) {
2398
+ return null;
2399
+ }
2400
+ const [customDomains, serviceDomains] = await Promise.all([
2401
+ listRailwayCustomDomains({
2402
+ projectId: identifiers.projectId,
2403
+ environmentId: identifiers.environmentId,
2404
+ serviceId: identifiers.serviceId,
2405
+ env: topology.env
2406
+ }),
2407
+ listRailwayServiceDomains({
2408
+ projectId: identifiers.projectId,
2409
+ environmentId: identifiers.environmentId,
2410
+ serviceId: identifiers.serviceId,
2411
+ env: topology.env
2412
+ })
2413
+ ]);
2414
+ const customDomain = customDomains.find((entry2) => entry2.domain === domain) ?? null;
2415
+ if (!customDomain) {
2416
+ return null;
2417
+ }
2418
+ const serviceDomain = serviceDomains.find((entry2) => entry2.kind === "service") ?? null;
2419
+ const normalized = normalizeRailwayDomainPayload({
2420
+ ...customDomain,
2421
+ serviceDomain: serviceDomain?.domain ?? null
2422
+ });
2423
+ return normalized?.domain ? normalized : customDomain;
2424
+ }
2425
+ function railwayCustomDomainHasDnsRequirements(live) {
2426
+ if (!live) {
2427
+ return false;
2428
+ }
2429
+ return Array.isArray(live.dnsRecords) && live.dnsRecords.length > 0 || typeof live.serviceDomain === "string" && live.serviceDomain.trim().length > 0 || typeof live.verificationDnsHost === "string" && live.verificationDnsHost.trim().length > 0 && typeof live.verificationToken === "string" && live.verificationToken.trim().length > 0;
2430
+ }
2377
2431
  function collectCloudflareEnvironmentSync(input) {
2378
2432
  const target = toDeployTarget(input.context.target);
2379
2433
  const scope = scopeFromTarget(target);
@@ -2434,7 +2488,7 @@ function verificationCheck(key, description, source, options) {
2434
2488
  };
2435
2489
  }
2436
2490
  function summarizeVerification(unitId, checks, warnings = []) {
2437
- const missing = checks.flatMap((check) => !check.exists ? [`${check.key}: ${check.description}`] : []);
2491
+ const missing = checks.flatMap((check) => !check.exists ? [`${check.key}: ${check.issues.join("; ") || check.description}`] : []);
2438
2492
  const drifted = checks.flatMap(
2439
2493
  (check) => check.exists && (!check.configured || !check.ready || !check.verified || check.issues.length > 0) ? [`${check.key}: ${check.issues.join("; ") || "verification failed"}`] : []
2440
2494
  );
@@ -2471,7 +2525,7 @@ function unsupportedVerification(unitId, message) {
2471
2525
  warnings: []
2472
2526
  };
2473
2527
  }
2474
- function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {}) {
2528
+ function syncPagesEnvironmentVariablesForTarget(input, { planOnly = false } = {}) {
2475
2529
  const target = toDeployTarget(input.context.target);
2476
2530
  if (target.kind !== "persistent") {
2477
2531
  return { vars: [], secrets: [] };
@@ -2493,7 +2547,7 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
2493
2547
  ...plainVars,
2494
2548
  ...secretVars
2495
2549
  };
2496
- if (dryRun || Object.keys(envVars).length === 0) {
2550
+ if (planOnly || Object.keys(envVars).length === 0) {
2497
2551
  return {
2498
2552
  vars: Object.keys(plainVars),
2499
2553
  secrets: Object.keys(secretVars)
@@ -2525,17 +2579,17 @@ function syncPagesEnvironmentVariablesForTarget(input, { dryRun = false } = {})
2525
2579
  secrets: Object.keys(secretVars)
2526
2580
  };
2527
2581
  }
2528
- function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2582
+ function reconcileCloudflareTarget(input, { planOnly = false } = {}) {
2529
2583
  const target = toDeployTarget(input.context.target);
2530
2584
  const deployConfig = input.context.deployConfig;
2531
2585
  const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
2532
2586
  const env = buildCloudflareEnv(input);
2533
- const kvNamespaces = dryRun ? [] : listKvNamespaces(input.context.tenantRoot, env);
2534
- const d1Databases = dryRun ? [] : listD1Databases(input.context.tenantRoot, env);
2535
- const queues = dryRun ? [] : listQueues(input.context.tenantRoot, env);
2536
- const buckets = dryRun ? [] : listR2Buckets(input.context.tenantRoot, env);
2537
- const pagesProjects = dryRun ? [] : listPagesProjects(input.context.tenantRoot, env);
2538
- const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(input.context.tenantRoot, env);
2587
+ const kvNamespaces = planOnly ? [] : listKvNamespaces(input.context.tenantRoot, env);
2588
+ const d1Databases = planOnly ? [] : listD1Databases(input.context.tenantRoot, env);
2589
+ const queues = planOnly ? [] : listQueues(input.context.tenantRoot, env);
2590
+ const buckets = planOnly ? [] : listR2Buckets(input.context.tenantRoot, env);
2591
+ const pagesProjects = planOnly ? [] : listPagesProjects(input.context.tenantRoot, env);
2592
+ const turnstileWidgets = planOnly ? [] : listTurnstileWidgets(input.context.tenantRoot, env);
2539
2593
  const runStep = (label, fn) => {
2540
2594
  try {
2541
2595
  return fn();
@@ -2559,9 +2613,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2559
2613
  state.kvNamespaces[binding].previewId = existing.id;
2560
2614
  return;
2561
2615
  }
2562
- if (dryRun) {
2563
- state.kvNamespaces[binding].id = `dryrun-${current.name}`;
2564
- state.kvNamespaces[binding].previewId = `dryrun-${current.name}`;
2616
+ if (planOnly) {
2617
+ state.kvNamespaces[binding].id = `plan-${current.name}`;
2618
+ state.kvNamespaces[binding].previewId = `plan-${current.name}`;
2565
2619
  return;
2566
2620
  }
2567
2621
  runWrangler(["kv", "namespace", "create", current.name], {
@@ -2591,9 +2645,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2591
2645
  current.previewDatabaseId = existing.previewDatabaseUuid ?? existing.uuid;
2592
2646
  return;
2593
2647
  }
2594
- if (dryRun) {
2595
- current.databaseId = `dryrun-${current.databaseName}`;
2596
- current.previewDatabaseId = `dryrun-${current.databaseName}`;
2648
+ if (planOnly) {
2649
+ current.databaseId = `plan-${current.databaseName}`;
2650
+ current.previewDatabaseId = `plan-${current.databaseName}`;
2597
2651
  return;
2598
2652
  }
2599
2653
  try {
@@ -2629,7 +2683,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2629
2683
  }
2630
2684
  let refreshedBuckets = buckets;
2631
2685
  const existing = refreshedBuckets.find((entry) => entry?.name === bucketName);
2632
- if (existing || dryRun) {
2686
+ if (existing || planOnly) {
2633
2687
  return;
2634
2688
  }
2635
2689
  try {
@@ -2652,7 +2706,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2652
2706
  }
2653
2707
  const existing = pagesProjects.find((entry) => entry?.name === current.projectName);
2654
2708
  if (existing) {
2655
- if (!dryRun && (existing.production_branch ?? "main") !== (current.productionBranch ?? "main")) {
2709
+ if (!planOnly && (existing.production_branch ?? "main") !== (current.productionBranch ?? "main")) {
2656
2710
  cloudflareApiRequest(
2657
2711
  `/accounts/${encodeURIComponent(env.CLOUDFLARE_ACCOUNT_ID)}/pages/projects/${encodeURIComponent(current.projectName)}`,
2658
2712
  {
@@ -2667,7 +2721,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2667
2721
  current.url = existing.subdomain ? `https://${existing.subdomain}` : current.url ?? `https://${current.projectName}.pages.dev`;
2668
2722
  return;
2669
2723
  }
2670
- if (dryRun) {
2724
+ if (planOnly) {
2671
2725
  current.url = `https://${current.projectName}.pages.dev`;
2672
2726
  return;
2673
2727
  }
@@ -2708,9 +2762,9 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2708
2762
  current.mode = "managed";
2709
2763
  current.managed = true;
2710
2764
  const existing = findTurnstileWidget(turnstileWidgets, current, current.name);
2711
- if (dryRun) {
2712
- current.sitekey = current.sitekey ?? `dryrun-${current.name}-sitekey`;
2713
- current.secret = current.secret ?? `dryrun-${current.name}-secret`;
2765
+ if (planOnly) {
2766
+ current.sitekey = current.sitekey ?? `plan-${current.name}-sitekey`;
2767
+ current.secret = current.secret ?? `plan-${current.name}-secret`;
2714
2768
  current.lastSyncedAt = nowIso();
2715
2769
  return;
2716
2770
  }
@@ -2743,7 +2797,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2743
2797
  runStep("r2", ensureR2Bucket);
2744
2798
  runStep("pages", ensurePagesProject);
2745
2799
  runStep("turnstile-widget", ensureTurnstileWidget);
2746
- runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { dryRun, env }));
2800
+ runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { planOnly, env }));
2747
2801
  state.readiness.configured = true;
2748
2802
  state.readiness.provisioned = hasProvisionedCloudflareResources(state);
2749
2803
  state.readiness.deployable = state.readiness.provisioned === true;
@@ -2760,7 +2814,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
2760
2814
  writeDeployState(input.context.tenantRoot, state, { target });
2761
2815
  return { state, summary: buildProvisioningSummary(deployConfig, state, target) };
2762
2816
  }
2763
- function syncCloudflareSecretsForTarget(input, { dryRun = false } = {}) {
2817
+ function syncCloudflareSecretsForTarget(input, { planOnly = false } = {}) {
2764
2818
  const target = toDeployTarget(input.context.target);
2765
2819
  const deployConfig = input.context.deployConfig;
2766
2820
  const state = loadDeployState(input.context.tenantRoot, deployConfig, { target });
@@ -3771,11 +3825,11 @@ async function reconcileStaleOperationsRunnerResourcesForProject(input, {
3771
3825
  });
3772
3826
  }
3773
3827
  }
3774
- async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKeys } = {}) {
3828
+ async function syncRailwayEnvironmentForScope(input, { planOnly = false, serviceKeys } = {}) {
3775
3829
  const scope = input.context.target.kind === "persistent" ? input.context.target.scope : "staging";
3776
3830
  const sync = collectRailwayEnvironmentSync(input);
3777
3831
  const selectedServiceKeys = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? [...new Set(serviceKeys.map((value) => String(value).trim()).filter(Boolean))] : void 0;
3778
- traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope} dryRun=${dryRun ? "yes" : "no"}`);
3832
+ traceRailwayReconcile(input.context.env, "sync:start", `scope=${scope} planOnly=${planOnly ? "yes" : "no"}`);
3779
3833
  const projectGroups = configuredRailwayProjectSyncGroups(input, scope, selectedServiceKeys);
3780
3834
  if (projectGroups.length === 0) {
3781
3835
  return {
@@ -3783,7 +3837,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3783
3837
  services: [],
3784
3838
  secrets: Object.keys(sync.secrets),
3785
3839
  variables: Object.keys(sync.variables),
3786
- dryRun,
3840
+ planOnly,
3787
3841
  workspace: ""
3788
3842
  };
3789
3843
  }
@@ -3792,7 +3846,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3792
3846
  for (const projectServices of projectGroups) {
3793
3847
  const serviceKeySet = [...new Set(projectServices.map((service) => service.key))];
3794
3848
  const topology = await resolveRailwayTopologyForScope(input, scope, {
3795
- ensure: !dryRun,
3849
+ ensure: !planOnly,
3796
3850
  ensureServices: false,
3797
3851
  serviceKeys: serviceKeySet,
3798
3852
  includeInstances: false,
@@ -3807,7 +3861,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3807
3861
  for (const service of projectServices) {
3808
3862
  railwayIacServiceInput(input, sync, service, scope);
3809
3863
  }
3810
- if (dryRun) {
3864
+ if (planOnly) {
3811
3865
  syncedServices.push(...projectServices);
3812
3866
  continue;
3813
3867
  }
@@ -3936,7 +3990,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false, serviceKe
3936
3990
  services: syncedServices,
3937
3991
  secrets: Object.keys(sync.secrets),
3938
3992
  variables: Object.keys(sync.variables),
3939
- dryRun,
3993
+ planOnly,
3940
3994
  workspace
3941
3995
  };
3942
3996
  }
@@ -4680,7 +4734,7 @@ function resolveDesiredDnsRecords(input) {
4680
4734
  }
4681
4735
  return desiredRecords;
4682
4736
  }
4683
- function observeCustomDomainUnit(input) {
4737
+ async function observeCustomDomainUnit(input) {
4684
4738
  switch (input.unit.unitType) {
4685
4739
  case "custom-domain:web": {
4686
4740
  const env = buildCloudflareEnv(input);
@@ -4703,7 +4757,8 @@ function observeCustomDomainUnit(input) {
4703
4757
  }
4704
4758
  case "custom-domain:api": {
4705
4759
  const domain = String(input.unit.spec.domain ?? "").trim();
4706
- const live = getCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
4760
+ const cached = getCustomDomainState(input, "railway", domain);
4761
+ const live = (cached && railwayCustomDomainHasDnsRequirements(cached) ? cached : null) ?? await observeRailwayCustomDomainLive(input, domain) ?? cached ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
4707
4762
  if (live?.domain) {
4708
4763
  storeCustomDomainState(input, "railway", domain, live);
4709
4764
  }
@@ -4746,7 +4801,7 @@ function observeDnsRecordUnit(input) {
4746
4801
  warnings: desiredRecords.length === 0 ? ["No desired DNS records were available for verification."] : []
4747
4802
  };
4748
4803
  }
4749
- function verifyCustomDomainUnit(input) {
4804
+ async function verifyCustomDomainUnit(input) {
4750
4805
  switch (input.unit.unitType) {
4751
4806
  case "custom-domain:web": {
4752
4807
  const domain = String(input.unit.spec.domain ?? "").trim();
@@ -4764,7 +4819,8 @@ function verifyCustomDomainUnit(input) {
4764
4819
  }
4765
4820
  case "custom-domain:api": {
4766
4821
  const domain = String(input.unit.spec.domain ?? "").trim();
4767
- const live = getCustomDomainState(input, "railway", domain) ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
4822
+ const cached = getCustomDomainState(input, "railway", domain);
4823
+ const live = (cached && railwayCustomDomainHasDnsRequirements(cached) ? cached : null) ?? await observeRailwayCustomDomainLive(input, domain) ?? cached ?? getPersistedCustomDomainState(input, "railway", domain) ?? input.persistedState?.lastObservedState ?? input.persistedState?.lastReconciledState ?? null;
4768
4824
  const dnsRecords = Array.isArray(live?.dnsRecords) ? live.dnsRecords : [];
4769
4825
  const certificateStatus = typeof live?.certificateStatus === "string" ? live.certificateStatus.trim().toUpperCase() : null;
4770
4826
  const verified = live?.verified === true;
@@ -4854,7 +4910,7 @@ async function reconcileCustomDomainUnit(input, diff) {
4854
4910
  const projectName = String(input.unit.spec.projectName ?? "").trim();
4855
4911
  const state = ensureCloudflarePagesDomain(env, projectName, domain) ?? { domain };
4856
4912
  storeCustomDomainState(input, "cloudflare", domain, state);
4857
- const observed = observeCustomDomainUnit(input);
4913
+ const observed = await observeCustomDomainUnit(input);
4858
4914
  return {
4859
4915
  unit: input.unit,
4860
4916
  observed,
@@ -4891,7 +4947,7 @@ async function reconcileCustomDomainUnit(input, diff) {
4891
4947
  }
4892
4948
  );
4893
4949
  storeCustomDomainState(input, "railway", String(input.unit.spec.domain ?? "").trim(), state);
4894
- const observed = observeCustomDomainUnit(input);
4950
+ const observed = await observeCustomDomainUnit(input);
4895
4951
  return {
4896
4952
  unit: input.unit,
4897
4953
  observed,
@@ -5034,13 +5090,50 @@ async function verifyRailwayUnit(input) {
5034
5090
  }));
5035
5091
  }
5036
5092
  const desiredRootDirectory = service.imageRef || service.sourceMode === "image" ? null : railwayServiceRootDirectory(input.context.tenantRoot, service);
5093
+ const normalizeRailwayRootDirectory = (value) => {
5094
+ const trimmed = String(value ?? "").trim();
5095
+ return trimmed || ".";
5096
+ };
5037
5097
  if (desiredRootDirectory) {
5098
+ const observedRootDirectory = normalizeRailwayRootDirectory(entry.instance?.rootDirectory);
5099
+ const desiredNormalizedRootDirectory = normalizeRailwayRootDirectory(desiredRootDirectory);
5038
5100
  checks.push(verificationCheck("railway.instance.root-directory", "Railway root directory matches desired config", "api", {
5039
5101
  exists: Boolean(entry.instance?.id),
5040
- configured: entry.instance?.rootDirectory === desiredRootDirectory,
5102
+ configured: observedRootDirectory === desiredNormalizedRootDirectory,
5041
5103
  expected: desiredRootDirectory,
5042
5104
  observed: entry.instance?.rootDirectory ?? null,
5043
- issues: entry.instance?.rootDirectory === desiredRootDirectory ? [] : ["Railway root directory does not match the desired value."]
5105
+ issues: observedRootDirectory === desiredNormalizedRootDirectory ? [] : ["Railway root directory does not match the desired value."]
5106
+ }));
5107
+ }
5108
+ if (service.sourceMode === "git" && entry.service?.id && entry.environment?.id) {
5109
+ const deployment = await inspectRailwayServiceDeploymentHealth({
5110
+ serviceId: entry.service.id,
5111
+ environmentId: entry.environment.id,
5112
+ env: topology.env
5113
+ }).catch((error) => ({
5114
+ ok: false,
5115
+ repo: null,
5116
+ branch: null,
5117
+ rootDirectory: null,
5118
+ message: error instanceof Error ? error.message : String(error)
5119
+ }));
5120
+ const repoMatches = service.sourceRepo ? deployment.repo === service.sourceRepo : true;
5121
+ const branchMatches = service.sourceBranch ? deployment.branch === service.sourceBranch : true;
5122
+ const rootMatches = desiredRootDirectory ? normalizeRailwayRootDirectory(deployment.rootDirectory) === normalizeRailwayRootDirectory(desiredRootDirectory) : true;
5123
+ const deploymentIssues = [
5124
+ repoMatches ? null : `Expected Railway Git deployment repo ${service.sourceRepo}, observed ${deployment.repo ?? "(unset)"}.`,
5125
+ branchMatches ? null : `Expected Railway Git deployment branch ${service.sourceBranch}, observed ${deployment.branch ?? "(unset)"}.`,
5126
+ rootMatches ? null : `Expected Railway deployment root ${desiredRootDirectory}, observed ${deployment.rootDirectory ?? "(unset)"}.`,
5127
+ deployment.repo || deployment.branch ? null : "Railway latest deployment does not expose a Git repository/branch; staging may still be using a Docker image source."
5128
+ ].filter((issue) => Boolean(issue));
5129
+ checks.push(verificationCheck("railway.service.source-mode", "Railway staging service deploys from Git source", "api", {
5130
+ exists: Boolean(entry.service?.id),
5131
+ configured: deploymentIssues.length === 0,
5132
+ ready: deploymentIssues.length === 0,
5133
+ verified: deploymentIssues.length === 0,
5134
+ expected: { sourceMode: "git", repo: service.sourceRepo ?? null, branch: service.sourceBranch ?? null, rootDirectory: desiredRootDirectory ?? null },
5135
+ observed: { repo: deployment.repo, branch: deployment.branch, rootDirectory: deployment.rootDirectory, message: deployment.message ?? null },
5136
+ issues: deploymentIssues
5044
5137
  }));
5045
5138
  }
5046
5139
  if (service.key === "api") {
@@ -128,7 +128,7 @@ export interface TreeseedReconcileRunContext {
128
128
  target: TreeseedReconcileTarget;
129
129
  deployConfig: TreeseedDeployConfig;
130
130
  launchEnv: NodeJS.ProcessEnv;
131
- dryRun?: boolean;
131
+ planOnly?: boolean;
132
132
  write?: (line: string) => void;
133
133
  session: Map<string, unknown>;
134
134
  }
@@ -30,7 +30,7 @@ export declare function planTreeseedReconciliation({ tenantRoot, target, env, sy
30
30
  state: TreeseedReconcileStateRecord;
31
31
  deployConfig: import("../platform/contracts.js").TreeseedDeployConfig;
32
32
  }>;
33
- export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, selector, units, write, dryRun, session, }: {
33
+ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, systems, selector, units, write, planOnly, session, }: {
34
34
  tenantRoot: string;
35
35
  target: TreeseedReconcileTarget;
36
36
  env?: NodeJS.ProcessEnv;
@@ -38,7 +38,7 @@ export declare function reconcileTreeseedTarget({ tenantRoot, target, env, syste
38
38
  selector?: TreeseedReconcileSelector;
39
39
  units?: TreeseedDesiredUnit[];
40
40
  write?: (line: string) => void;
41
- dryRun?: boolean;
41
+ planOnly?: boolean;
42
42
  session?: Map<string, unknown>;
43
43
  }): Promise<{
44
44
  target: TreeseedReconcileTarget;
@@ -83,11 +83,11 @@ export declare function collectTreeseedReconcileStatus({ tenantRoot, target, env
83
83
  verification: TreeseedUnitVerificationResult | null;
84
84
  }[];
85
85
  }>;
86
- export declare function reconcileTreeseedNestedTarget({ parentContext, selector, target, dryRun, }: {
86
+ export declare function reconcileTreeseedNestedTarget({ parentContext, selector, target, planOnly, }: {
87
87
  parentContext: TreeseedReconcileRunContext;
88
88
  selector: TreeseedReconcileSelector;
89
89
  target: TreeseedReconcileTarget;
90
- dryRun: boolean;
90
+ planOnly: boolean;
91
91
  }): Promise<{
92
92
  target: TreeseedReconcileTarget;
93
93
  units: TreeseedDesiredUnit[];
@@ -26,14 +26,14 @@ function formatVerificationFailure(verification) {
26
26
  ].filter(Boolean);
27
27
  return details.length > 0 ? `Verification failed (${details.join(" | ")})` : "Verification failed.";
28
28
  }
29
- function createRunContext(tenantRoot, target, launchEnv, write, dryRun = false, session) {
29
+ function createRunContext(tenantRoot, target, launchEnv, write, planOnly = false, session) {
30
30
  const { deployConfig } = deriveTreeseedDesiredUnits({ tenantRoot, target, env: launchEnv });
31
31
  return {
32
32
  tenantRoot,
33
33
  target,
34
34
  deployConfig,
35
35
  launchEnv,
36
- dryRun,
36
+ planOnly,
37
37
  write,
38
38
  session: session ?? /* @__PURE__ */ new Map()
39
39
  };
@@ -278,12 +278,12 @@ async function reconcileTreeseedTarget({
278
278
  selector,
279
279
  units,
280
280
  write,
281
- dryRun = false,
281
+ planOnly = false,
282
282
  session
283
283
  }) {
284
284
  const planned = await planTreeseedReconciliation({ tenantRoot, target, env, systems, selector, units, write });
285
285
  const registry = createTreeseedReconcileRegistry(planned.deployConfig);
286
- const context = createRunContext(tenantRoot, target, env, write, dryRun, session);
286
+ const context = createRunContext(tenantRoot, target, env, write, planOnly, session);
287
287
  const results = [];
288
288
  const verificationMap = /* @__PURE__ */ new Map();
289
289
  const timingEntries = [];
@@ -337,7 +337,7 @@ async function reconcileTreeseedTarget({
337
337
  let result;
338
338
  try {
339
339
  const stageStartMs = performance.now();
340
- if (dryRun) {
340
+ if (planOnly) {
341
341
  result = {
342
342
  unit: plan.unit,
343
343
  observed: plan.observed,
@@ -360,7 +360,7 @@ async function reconcileTreeseedTarget({
360
360
  unitTiming.children?.push({
361
361
  name: `${unitTiming.name}:apply`,
362
362
  durationMs: elapsedMs(stageStartMs),
363
- status: dryRun ? "skipped" : "success"
363
+ status: planOnly ? "skipped" : "success"
364
364
  });
365
365
  } catch (error) {
366
366
  unitTiming.children?.push({
@@ -373,7 +373,7 @@ async function reconcileTreeseedTarget({
373
373
  wrapAdapterFailure("apply", plan.unit.provider, plan.unit.unitType, plan.unit.unitId, error);
374
374
  }
375
375
  let refreshedObserved = result.observed;
376
- if (!dryRun) {
376
+ if (!planOnly) {
377
377
  try {
378
378
  const stageStartMs = performance.now();
379
379
  refreshedObserved = await Promise.resolve(adapter.refresh({
@@ -447,17 +447,17 @@ async function reconcileTreeseedTarget({
447
447
  unitTiming.status = verification.verified ? "success" : "failed";
448
448
  write?.(`Finished ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.logicalName}) in ${formatDurationMs(unitTiming.durationMs)}.`);
449
449
  if (!verification.verified) {
450
- if (!dryRun) {
450
+ if (!planOnly) {
451
451
  await persistVerifiedResult(persisted, verifiedResult);
452
452
  }
453
453
  throw new Error(`Treeseed reconcile verification failed for ${plan.unit.provider}:${plan.unit.unitType} (${plan.unit.unitId}): ${formatVerificationFailure(verification)}`);
454
454
  }
455
- if (!dryRun) {
455
+ if (!planOnly) {
456
456
  await persistVerifiedResult(persisted, verifiedResult);
457
457
  }
458
458
  results.push(verifiedResult);
459
459
  });
460
- if (!dryRun) {
460
+ if (!planOnly) {
461
461
  writeTreeseedReconcileState(tenantRoot, planned.state, env);
462
462
  }
463
463
  return {
@@ -579,7 +579,7 @@ async function reconcileTreeseedNestedTarget({
579
579
  parentContext,
580
580
  selector,
581
581
  target,
582
- dryRun
582
+ planOnly
583
583
  }) {
584
584
  const previousVerificationResults = parentContext.session.get("treeseed:verification-results");
585
585
  const previousTimings = parentContext.session.get("treeseed:timings");
@@ -590,7 +590,7 @@ async function reconcileTreeseedNestedTarget({
590
590
  env: parentContext.launchEnv,
591
591
  selector,
592
592
  write: parentContext.write,
593
- dryRun,
593
+ planOnly,
594
594
  session: parentContext.session
595
595
  });
596
596
  } finally {
@@ -61,7 +61,7 @@ export declare function runHostedReconcileGate(input: {
61
61
  parentContext: TreeseedReconcileRunContext;
62
62
  selector: TreeseedReconcileSelector;
63
63
  target: TreeseedReconcileTarget;
64
- dryRun: boolean;
64
+ planOnly: boolean;
65
65
  }): Promise<{
66
66
  target: TreeseedReconcileTarget;
67
67
  units: import("../contracts.js").TreeseedDesiredUnit[];
@@ -21,7 +21,7 @@ function writeDeployReport(payload) {
21
21
  }
22
22
  function parseArgs(argv) {
23
23
  const parsed = {
24
- dryRun: false,
24
+ planOnly: false,
25
25
  only: null,
26
26
  name: null,
27
27
  environment: null,
@@ -32,8 +32,8 @@ function parseArgs(argv) {
32
32
  const current = rest.shift();
33
33
  if (!current)
34
34
  continue;
35
- if (current === '--dry-run') {
36
- parsed.dryRun = true;
35
+ if (current === '--plan') {
36
+ parsed.planOnly = true;
37
37
  continue;
38
38
  }
39
39
  if (current === '--only') {
@@ -166,8 +166,8 @@ async function main() {
166
166
  }
167
167
  if (scope === 'local') {
168
168
  runTenantDeployPreflight({ cwd: tenantRoot, scope: 'local' });
169
- await runTenantWebBuild({ tenantRoot, scope: 'local', dryRun: options.dryRun, env: process.env });
170
- writeDeployReport({ ok: true, kind: 'success', scope, dryRun: options.dryRun, target: deployTargetLabel(target) });
169
+ await runTenantWebBuild({ tenantRoot, scope: 'local', planOnly: options.planOnly, env: process.env });
170
+ writeDeployReport({ ok: true, kind: 'success', scope, planOnly: options.planOnly, target: deployTargetLabel(target) });
171
171
  console.log('Treeseed local deploy completed as a build-only publish target.');
172
172
  return;
173
173
  }
@@ -177,7 +177,7 @@ async function main() {
177
177
  tenantRoot,
178
178
  scope,
179
179
  target,
180
- dryRun: options.dryRun,
180
+ planOnly: options.planOnly,
181
181
  env: process.env,
182
182
  });
183
183
  }
@@ -192,21 +192,21 @@ async function main() {
192
192
  }
193
193
  if (shouldRun('migrate')) {
194
194
  const result = await runTenantDataMigration(context);
195
- console.log(`${options.dryRun ? 'Planned' : 'Applied'} remote migrations for ${result.databaseName}.`);
195
+ console.log(`${options.planOnly ? 'Planned' : 'Applied'} remote migrations for ${result.databaseName}.`);
196
196
  }
197
197
  if (shouldRun('build')) {
198
198
  await runTenantWebBuild(context);
199
199
  }
200
200
  if (shouldRun('publish')) {
201
201
  await runTenantWebPublish(context);
202
- if (!options.dryRun) {
202
+ if (!options.planOnly) {
203
203
  finalizeDeploymentState(tenantRoot, { target });
204
204
  }
205
205
  }
206
206
  writeDeployReport({
207
207
  ok: true,
208
208
  kind: 'success',
209
- dryRun: options.dryRun,
209
+ planOnly: options.planOnly,
210
210
  only: options.only,
211
211
  target: deployTargetLabel(target),
212
212
  });