@treeseed/sdk 0.12.54 → 0.12.55

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.
@@ -59,7 +59,7 @@ export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any,
59
59
  };
60
60
  message: string;
61
61
  }>;
62
- export declare function configuredRailwayServices(tenantRoot: any, scope: any, envOverlay?: {}): unknown[];
62
+ export declare function configuredRailwayServices(tenantRoot: any, scope: any, envOverlay?: {}, options?: {}): unknown[];
63
63
  export declare function configuredRailwayScheduledJobs(tenantRoot: any, scope: any, { phase }?: {
64
64
  phase?: string | undefined;
65
65
  }): any[];
@@ -641,8 +641,9 @@ query TreeseedRailwayDeploymentStatus($projectId: String!) {
641
641
  });
642
642
  return payload.data?.project ?? null;
643
643
  }
644
- function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, application = null, machineConfigRoot = tenantRoot, envOverlay = {}) {
644
+ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, application = null, machineConfigRoot = tenantRoot, envOverlay = {}, options = {}) {
645
645
  const normalizedScope = normalizeScope(scope);
646
+ const identityOnly = options.identityOnly === true;
646
647
  const imageRefKeys = [
647
648
  "TREESEED_API_IMAGE_REF",
648
649
  "TREESEED_OPERATIONS_RUNNER_IMAGE_REF",
@@ -702,7 +703,13 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
702
703
  const configuredImageRefEnv = service.railway?.imageRefEnv ?? railwayImageRefEnvForService(serviceKey);
703
704
  const canUseImageRefEnv = normalizedScope === "prod" || serviceKey === "capacityProviderManager" || serviceKey === "capacityProviderRunner";
704
705
  const imageRef = service.railway?.imageRef ?? (canUseImageRefEnv && configuredImageRefEnv ? envValue(configuredImageRefEnv, imageRefEnv) || null : null) ?? defaultRailwayImageRef(serviceKey, normalizedScope, imageRefEnv);
705
- const sourcePolicy = resolveRailwayServiceSourcePolicy({
706
+ const sourcePolicy = identityOnly ? {
707
+ sourceMode: normalizedScope === "prod" ? "image" : "git",
708
+ sourceRepo: null,
709
+ sourceBranch: null,
710
+ sourceCommit: null,
711
+ sourceRootDirectory: null
712
+ } : resolveRailwayServiceSourcePolicy({
706
713
  tenantRoot,
707
714
  scope: normalizedScope,
708
715
  serviceKey,
@@ -760,11 +767,12 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
760
767
  hostingKind,
761
768
  application,
762
769
  imageRefEnv,
763
- workspaceRoot: machineConfigRoot
770
+ workspaceRoot: machineConfigRoot,
771
+ identityOnly
764
772
  })
765
773
  ];
766
774
  }
767
- function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig, identity, hostingKind, application, imageRefEnv, workspaceRoot }) {
775
+ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig, identity, hostingKind, application, imageRefEnv, workspaceRoot, identityOnly = false }) {
768
776
  if (deployConfig.hosting?.kind !== "treeseed_control_plane") {
769
777
  return [];
770
778
  }
@@ -803,7 +811,9 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
803
811
  buildCommand: sourceMode === "git" ? railway.buildCommand ?? null : null,
804
812
  startCommand: sourceMode === "git" ? railway.startCommand ?? null : null
805
813
  };
806
- assertApiRailwaySourcePolicy(scope, service);
814
+ if (!identityOnly) {
815
+ assertApiRailwaySourcePolicy(scope, service);
816
+ }
807
817
  return {
808
818
  key: service.key,
809
819
  instanceKey: serviceName,
@@ -957,9 +967,9 @@ function resolveRailwayCapacityProviderRoot(tenantRoot, service) {
957
967
  );
958
968
  return found ?? resolve(tenantRoot, "packages", "agent");
959
969
  }
960
- function configuredRailwayServices(tenantRoot, scope, envOverlay = {}) {
970
+ function configuredRailwayServices(tenantRoot, scope, envOverlay = {}, options = {}) {
961
971
  const deployConfig = loadCliDeployConfig(tenantRoot);
962
- const direct = configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, null, tenantRoot, envOverlay);
972
+ const direct = configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, null, tenantRoot, envOverlay, options);
963
973
  const nested = discoverTreeseedApplications(tenantRoot).filter((application) => application.root !== resolve(tenantRoot)).flatMap((application) => configuredRailwayServicesForConfig(
964
974
  application.root,
965
975
  scope,
@@ -970,7 +980,8 @@ function configuredRailwayServices(tenantRoot, scope, envOverlay = {}) {
970
980
  relativeRoot: application.relativeRoot
971
981
  },
972
982
  tenantRoot,
973
- envOverlay
983
+ envOverlay,
984
+ options
974
985
  ));
975
986
  return [...direct, ...nested];
976
987
  }
@@ -3657,6 +3657,24 @@ function configuredRailwayProjectSyncGroups(input, scope, serviceKeys) {
3657
3657
  }
3658
3658
  return [...grouped.values()];
3659
3659
  }
3660
+ function configuredRailwaySiblingResourceNames(input, scope, projectName) {
3661
+ const siblingScope = scope === "prod" ? "staging" : scope === "staging" ? "prod" : null;
3662
+ if (!siblingScope) return [];
3663
+ const siblingServices = configuredRailwayServices(
3664
+ input.context.tenantRoot,
3665
+ siblingScope,
3666
+ resolveReconcileEnvironmentValues(input, siblingScope),
3667
+ { identityOnly: true }
3668
+ ).filter((service) => service.enabled !== false).filter((service) => !isRailwayCapacityProviderService(service)).filter((service) => service.projectName === projectName);
3669
+ const database = configuredRailwayIacDatabase(input, siblingServices);
3670
+ return [.../* @__PURE__ */ new Set([
3671
+ ...siblingServices.flatMap((service) => [
3672
+ service.serviceName,
3673
+ ...service.volumeMountPath ? [`${service.serviceName}-volume`] : []
3674
+ ]),
3675
+ ...database ? [database.serviceName, `${database.serviceName}-volume`] : []
3676
+ ])];
3677
+ }
3660
3678
  function railwayIacServiceInput(input, sync, service, scope) {
3661
3679
  if (scope === "staging" && service.imageRef) {
3662
3680
  throw new Error(`Railway staging service ${service.serviceName} must build from Git source, not Docker image ${service.imageRef}.`);
@@ -3931,11 +3949,13 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
3931
3949
  })),
3932
3950
  database: databaseForIac
3933
3951
  };
3952
+ const siblingResourceNames = configuredRailwaySiblingResourceNames(input, scope, project.name);
3934
3953
  const {
3935
3954
  applyRailwayIacProject,
3936
3955
  cleanupRailwayIacRender,
3937
3956
  planRailwayIacProject,
3938
3957
  renderRailwayIacProject,
3958
+ selectRailwayIacRetainedResources,
3939
3959
  validateRailwayIacChangeSet
3940
3960
  } = await import("./providers/railway-iac.js");
3941
3961
  let effectiveIacInput = iacInput;
@@ -3986,6 +4006,33 @@ async function syncRailwayEnvironmentForScope(input, { planOnly = false, service
3986
4006
  ]))
3987
4007
  });
3988
4008
  }
4009
+ const retainedResources = selectRailwayIacRetainedResources(plan, siblingResourceNames);
4010
+ if (retainedResources.length > 0) {
4011
+ traceRailwayReconcile(
4012
+ topology.env,
4013
+ "sync:iac-retain-sibling-environment",
4014
+ `${project.name}/${environment.name}:${retainedResources.map((resource) => resource.name).join(",")}`
4015
+ );
4016
+ cleanupRailwayIacRender(rendered);
4017
+ effectiveIacInput = { ...effectiveIacInput, retainedResources };
4018
+ rendered = renderRailwayIacProject(effectiveIacInput);
4019
+ plan = await planRailwayIacProject(effectiveIacInput, rendered);
4020
+ if (!plan.ok) {
4021
+ const diagnostics = plan.diagnostics.map((entry) => entry.message).filter(Boolean).join("; ");
4022
+ throw new Error(`Railway IaC plan failed for ${project.name}/${environment.name}${diagnostics ? `: ${diagnostics}` : "."}`);
4023
+ }
4024
+ validation = validateRailwayIacChangeSet(plan.changeSet, {
4025
+ services: rendered.serviceNames,
4026
+ volumes: rendered.volumeNames,
4027
+ database: rendered.databaseName,
4028
+ scope,
4029
+ serviceSourceModes: Object.fromEntries(effectiveIacInput.services.map((service) => [service.serviceName, service.sourceMode ?? null])),
4030
+ serviceSourceRefs: Object.fromEntries(effectiveIacInput.services.map((service) => [
4031
+ service.serviceName,
4032
+ service.sourceMode === "git" && service.sourceRepo ? `github:${service.sourceRepo}:${service.sourceBranch ?? ""}:${service.sourceRootDirectory ?? ""}:${service.sourceCommit ?? ""}` : service.sourceMode === "image" && service.imageRef ? `image:${service.imageRef}` : null
4033
+ ]))
4034
+ });
4035
+ }
3989
4036
  if (!validation.ok) {
3990
4037
  throw new Error(`Railway IaC plan rejected for ${project.name}/${environment.name}: ${validation.blockedReasons.join("; ")}`);
3991
4038
  }
@@ -1,4 +1,4 @@
1
- import { type RailwayChangeSet, type RailwayIacApplyResponse, type RailwayIacPlanResponse } from 'railway/iac';
1
+ import { type RailwayChangeSet, type RailwayIacApplyResponse, type RailwayIacPlanResponse, type ResourceNode } from 'railway/iac';
2
2
  export type TreeseedRailwayIacService = {
3
3
  key: string;
4
4
  serviceName: string;
@@ -37,6 +37,7 @@ export type TreeseedRailwayIacProjectInput = {
37
37
  railwayApiUrl?: string | null;
38
38
  services: TreeseedRailwayIacService[];
39
39
  database: TreeseedRailwayIacDatabase | null;
40
+ retainedResources?: ResourceNode[];
40
41
  region?: string | null;
41
42
  };
42
43
  export type TreeseedRailwayIacRenderResult = {
@@ -47,6 +48,7 @@ export type TreeseedRailwayIacRenderResult = {
47
48
  serviceNames: string[];
48
49
  volumeNames: string[];
49
50
  databaseName: string | null;
51
+ retainedResourceNames: string[];
50
52
  source: string;
51
53
  };
52
54
  export type RailwayIacValidationResult = {
@@ -56,6 +58,7 @@ export type RailwayIacValidationResult = {
56
58
  allowedDrift: string[];
57
59
  };
58
60
  export declare function renderRailwayIacProject(input: TreeseedRailwayIacProjectInput): TreeseedRailwayIacRenderResult;
61
+ export declare function selectRailwayIacRetainedResources(plan: Pick<RailwayIacPlanResponse, 'changeSet' | 'currentGraph'>, allowedNames: Iterable<string>): ResourceNode[];
59
62
  export declare function validateRailwayIacChangeSet(changeSet: RailwayChangeSet | undefined, desiredNames: {
60
63
  services: string[];
61
64
  volumes: string[];
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { runRailwayIac } from "railway/iac";
3
+ import {
4
+ runRailwayIac
5
+ } from "railway/iac";
4
6
  import { assertApiRailwaySourcePolicy, isApiRailwaySourcePolicyService } from "../../operations/services/railway-source-policy.js";
5
7
  function js(value) {
6
8
  return JSON.stringify(value);
@@ -115,6 +117,16 @@ function renderRailwayIacProject(input) {
115
117
  const volumeNames = [];
116
118
  const databaseVariableName = input.database ? "db" : null;
117
119
  const databaseEnvName = input.database?.environmentVariable ?? null;
120
+ const desiredResourceNames = /* @__PURE__ */ new Set([
121
+ ...input.services.map((service) => service.serviceName),
122
+ ...input.services.filter((service) => service.volumeMountPath).map((service) => `${service.serviceName}-volume`),
123
+ ...input.database ? [input.database.serviceName, `${input.database.serviceName}-volume`] : []
124
+ ]);
125
+ const retainedResources = (input.retainedResources ?? []).filter((resource) => !desiredResourceNames.has(resource.name));
126
+ if (retainedResources.length > 0) {
127
+ declarations.push(` const retainedResources = ${js(retainedResources)};`);
128
+ resources.push("...retainedResources");
129
+ }
118
130
  if (input.database) {
119
131
  const postgresVolumeName = `${input.database.serviceName}-volume`;
120
132
  const postgresMountPath = input.database.mountPath?.trim() || "/var/lib/postgresql/data";
@@ -205,9 +217,15 @@ ${declarations.join("\n")}
205
217
  serviceNames: input.services.map((service) => service.serviceName),
206
218
  volumeNames,
207
219
  databaseName: input.database?.serviceName ?? null,
220
+ retainedResourceNames: retainedResources.map((resource) => resource.name),
208
221
  source
209
222
  };
210
223
  }
224
+ function selectRailwayIacRetainedResources(plan, allowedNames) {
225
+ const allowed = new Set(allowedNames);
226
+ const deletedAllowedNames = new Set((plan.changeSet?.changes ?? []).filter((change) => change.kind === "resource.delete").map((change) => changeName(change)).filter((name) => allowed.has(name)));
227
+ return (plan.currentGraph?.resources ?? []).filter((resource) => deletedAllowedNames.has(resource.name));
228
+ }
211
229
  function changeName(change) {
212
230
  return String(change?.resource?.name ?? change?.previous?.name ?? change?.address ?? change?.path ?? "");
213
231
  }
@@ -322,5 +340,6 @@ export {
322
340
  cleanupRailwayIacRender,
323
341
  planRailwayIacProject,
324
342
  renderRailwayIacProject,
343
+ selectRailwayIacRetainedResources,
325
344
  validateRailwayIacChangeSet
326
345
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.54",
3
+ "version": "0.12.55",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {