@treeseed/sdk 0.10.18 → 0.10.19

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.
@@ -199,7 +199,7 @@ export declare function getRailwayServiceInstance({ serviceId, environmentId, en
199
199
  sleepApplication: null;
200
200
  runtimeConfigSupported: false;
201
201
  }>;
202
- export declare function ensureRailwayServiceInstanceConfiguration({ serviceId, environmentId, buildCommand, startCommand, cronSchedule, rootDirectory, healthcheckPath, healthcheckTimeoutSeconds, healthcheckIntervalSeconds, restartPolicy, runtimeMode, env, fetchImpl, }: {
202
+ export declare function ensureRailwayServiceInstanceConfiguration({ serviceId, environmentId, buildCommand, startCommand, cronSchedule, rootDirectory, healthcheckPath, healthcheckTimeoutSeconds, healthcheckIntervalSeconds, restartPolicy, runtimeMode, env, fetchImpl, settleAttempts, settleDelayMs, }: {
203
203
  serviceId: string;
204
204
  environmentId: string;
205
205
  buildCommand?: string | null;
@@ -213,6 +213,8 @@ export declare function ensureRailwayServiceInstanceConfiguration({ serviceId, e
213
213
  runtimeMode?: string | null;
214
214
  env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
215
215
  fetchImpl?: typeof fetch;
216
+ settleAttempts?: number;
217
+ settleDelayMs?: number;
216
218
  }): Promise<{
217
219
  instance: {
218
220
  id: string | null;
@@ -838,7 +838,9 @@ async function ensureRailwayServiceInstanceConfiguration({
838
838
  restartPolicy,
839
839
  runtimeMode,
840
840
  env = process.env,
841
- fetchImpl = fetch
841
+ fetchImpl = fetch,
842
+ settleAttempts = 24,
843
+ settleDelayMs = 5e3
842
844
  }) {
843
845
  let current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
844
846
  if (!current.id) {
@@ -911,34 +913,32 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
911
913
  }
912
914
  throw error;
913
915
  }
914
- let instance = await getRailwayServiceInstance({
915
- serviceId,
916
- environmentId,
917
- env,
918
- fetchImpl
919
- });
920
- for (let attempt = 0; attempt < 5 && serviceInstanceDrifted(instance, desired); attempt += 1) {
921
- await new Promise((resolve) => setTimeout(resolve, 1e3));
916
+ let instance = current;
917
+ for (let attempt = 0; attempt <= settleAttempts; attempt += 1) {
922
918
  instance = await getRailwayServiceInstance({
923
919
  serviceId,
924
920
  environmentId,
925
921
  env,
926
922
  fetchImpl
927
923
  });
924
+ if (!serviceInstanceDrifted(instance, desired) || attempt >= settleAttempts) {
925
+ break;
926
+ }
927
+ await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
928
928
  }
929
929
  return {
930
930
  instance: {
931
931
  id: instance.id || current.id,
932
- buildCommand: instance.buildCommand ?? desired.buildCommand,
933
- startCommand: instance.startCommand ?? desired.startCommand,
934
- cronSchedule: instance.cronSchedule ?? desired.cronSchedule,
935
- rootDirectory: instance.rootDirectory ?? desired.rootDirectory,
936
- healthcheckPath: instance.healthcheckPath ?? desired.healthcheckPath,
937
- healthcheckTimeoutSeconds: instance.healthcheckTimeoutSeconds ?? desired.healthcheckTimeoutSeconds,
938
- healthcheckIntervalSeconds: instance.healthcheckIntervalSeconds ?? desired.healthcheckIntervalSeconds,
939
- restartPolicy: instance.restartPolicy ?? desired.restartPolicy,
940
- runtimeMode: instance.runtimeMode ?? desired.runtimeMode,
941
- sleepApplication: instance.sleepApplication ?? desired.sleepApplication,
932
+ buildCommand: instance.buildCommand,
933
+ startCommand: instance.startCommand,
934
+ cronSchedule: instance.cronSchedule,
935
+ rootDirectory: instance.rootDirectory,
936
+ healthcheckPath: instance.healthcheckPath,
937
+ healthcheckTimeoutSeconds: instance.healthcheckTimeoutSeconds,
938
+ healthcheckIntervalSeconds: instance.healthcheckIntervalSeconds,
939
+ restartPolicy: instance.restartPolicy,
940
+ runtimeMode: instance.runtimeMode,
941
+ sleepApplication: instance.sleepApplication,
942
942
  runtimeConfigSupported: instance.runtimeConfigSupported
943
943
  },
944
944
  updated: true
@@ -4,7 +4,16 @@ export declare function deriveRailwayMarketOperationsRunnerServiceName(baseServi
4
4
  export declare function deriveRailwayWorkerRunnerVolumeName(serviceName: any, environmentName?: string): string;
5
5
  export declare function deriveRailwayMarketOperationsRunnerVolumeName(serviceName: any, environmentName?: string): string;
6
6
  export declare function railwayServiceRuntimeStartCommand(service: any): any;
7
+ export declare function parseRailwayJsonOutput(output: any): any;
7
8
  export declare function collectRailwayDeploymentStatusChecks(statusPayload: any, scope: any, services: any): any;
9
+ export declare function listRailwayServiceVolumesWithCli({ cwd, serviceId, environmentId, name, mountPath, env, }: {
10
+ cwd: any;
11
+ serviceId: any;
12
+ environmentId: any;
13
+ name: any;
14
+ mountPath: any;
15
+ env?: NodeJS.ProcessEnv | undefined;
16
+ }): any;
8
17
  export declare function isUsableRailwayToken(value: any): boolean;
9
18
  export declare function resolveRailwayAuthToken(env?: NodeJS.ProcessEnv): string;
10
19
  export declare function buildRailwayCommandEnv(env?: NodeJS.ProcessEnv): {
@@ -225,6 +225,30 @@ function normalizeRailwayCliVolumeList(value, options) {
225
225
  }
226
226
  return value.volumes.map((entry) => normalizeRailwayCliVolume(entry, options)).filter(Boolean);
227
227
  }
228
+ function listRailwayServiceVolumesWithCli({
229
+ cwd,
230
+ serviceId,
231
+ environmentId,
232
+ name,
233
+ mountPath,
234
+ env = process.env
235
+ }) {
236
+ const listResult = runRailway(["volume", "--service", serviceId, "--environment", environmentId, "list", "--json"], {
237
+ cwd,
238
+ capture: true,
239
+ allowFailure: true,
240
+ env
241
+ });
242
+ if ((listResult.status ?? 1) !== 0) {
243
+ return [];
244
+ }
245
+ return normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
246
+ serviceId,
247
+ environmentId,
248
+ fallbackName: name,
249
+ fallbackMountPath: mountPath
250
+ });
251
+ }
228
252
  function isUsableRailwayToken(value) {
229
253
  return typeof value === "string" && value.trim().length >= 8;
230
254
  }
@@ -1758,7 +1782,43 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1758
1782
  }
1759
1783
  created = true;
1760
1784
  }
1761
- const instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1785
+ let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1786
+ if (!instance || instance.mountPath !== mountPath) {
1787
+ const attachResult = runRailway([...volumeArgs, "attach", "--volume", volume.id, "--yes", "--json"], {
1788
+ ...cliOptions,
1789
+ allowFailure: true
1790
+ });
1791
+ if ((attachResult.status ?? 1) !== 0) {
1792
+ const attachMessage = attachResult.stderr?.trim() || attachResult.stdout?.trim() || "";
1793
+ if (!/already mounted/iu.test(attachMessage)) {
1794
+ throw new Error(attachMessage || `Railway volume attach failed for ${serviceName} in ${environmentName}.`);
1795
+ }
1796
+ }
1797
+ const attachedVolume = (attachResult.status ?? 1) === 0 ? normalizeRailwayCliVolume(parseRailwayJsonOutput(attachResult.stdout ?? ""), {
1798
+ serviceId,
1799
+ environmentId,
1800
+ fallbackName: name,
1801
+ fallbackMountPath: mountPath
1802
+ }) : null;
1803
+ volume = attachedVolume ?? {
1804
+ ...volume,
1805
+ instances: [{
1806
+ ...instance ?? {
1807
+ id: volume.id,
1808
+ serviceId,
1809
+ environmentId,
1810
+ state: "READY",
1811
+ sizeGb: null,
1812
+ usedGb: null
1813
+ },
1814
+ serviceId,
1815
+ environmentId,
1816
+ mountPath
1817
+ }]
1818
+ };
1819
+ instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1820
+ updated = true;
1821
+ }
1762
1822
  if (volume.name !== name || instance?.mountPath !== mountPath) {
1763
1823
  const updateResult = runRailway([...volumeArgs, "update", "--volume", volume.id, "--name", name, "--mount-path", mountPath, "--json"], cliOptions);
1764
1824
  const updatedVolume = normalizeRailwayCliVolume(parseRailwayJsonOutput(updateResult.stdout ?? ""), {
@@ -1774,6 +1834,18 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1774
1834
  };
1775
1835
  updated = true;
1776
1836
  }
1837
+ const apiVolume = await waitForRailwayServiceVolumeMount({
1838
+ projectId,
1839
+ volumeId: volume.id,
1840
+ volumeName: name,
1841
+ serviceId,
1842
+ environmentId,
1843
+ mountPath,
1844
+ env
1845
+ });
1846
+ if (apiVolume) {
1847
+ volume = apiVolume;
1848
+ }
1777
1849
  return {
1778
1850
  volume,
1779
1851
  instance: volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null,
@@ -1781,6 +1853,33 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1781
1853
  updated
1782
1854
  };
1783
1855
  }
1856
+ async function waitForRailwayServiceVolumeMount({
1857
+ projectId,
1858
+ volumeId,
1859
+ volumeName,
1860
+ serviceId,
1861
+ environmentId,
1862
+ mountPath,
1863
+ env
1864
+ }) {
1865
+ for (let attempt = 0; attempt <= 24; attempt += 1) {
1866
+ const volumes = await listRailwayVolumes({ projectId, env });
1867
+ const match = volumes.find(
1868
+ (entry) => entry.id === volumeId || entry.name === volumeName || entry.instances.some(
1869
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1870
+ )
1871
+ ) ?? null;
1872
+ if (match?.instances.some(
1873
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1874
+ )) {
1875
+ return match;
1876
+ }
1877
+ if (attempt < 24) {
1878
+ await sleep(5e3);
1879
+ }
1880
+ }
1881
+ return null;
1882
+ }
1784
1883
  async function deployRailwayService(tenantRoot, service, {
1785
1884
  dryRun = false,
1786
1885
  write,
@@ -1928,6 +2027,8 @@ export {
1928
2027
  ensureRailwayServiceVolumeWithCliFallback,
1929
2028
  isRailwayTransientFailure,
1930
2029
  isUsableRailwayToken,
2030
+ listRailwayServiceVolumesWithCli,
2031
+ parseRailwayJsonOutput,
1931
2032
  planRailwayServiceDeploy,
1932
2033
  planRailwayServiceLink,
1933
2034
  railwayServiceRuntimeStartCommand,
@@ -33,6 +33,7 @@ import {
33
33
  deriveRailwayMarketOperationsRunnerVolumeName,
34
34
  ensureRailwayProjectContext,
35
35
  ensureRailwayServiceVolumeWithCliFallback,
36
+ listRailwayServiceVolumesWithCli,
36
37
  runRailway,
37
38
  validateRailwayDeployPrerequisites
38
39
  } from "../operations/services/railway-deploy.js";
@@ -2216,16 +2217,33 @@ async function verifyRailwayUnit(input) {
2216
2217
  const volumes = entry.project?.id ? await listRailwayVolumes({ projectId: entry.project.id, env: topology.env }) : [];
2217
2218
  const expectedServiceId = entry.service?.id ?? null;
2218
2219
  const expectedEnvironmentId = entry.environment?.id ?? null;
2219
- const mountedVolume = volumes.find((volume) => volume.instances.some(
2220
+ let mountedVolume = volumes.find((volume) => volume.instances.some(
2220
2221
  (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
2221
2222
  )) ?? null;
2223
+ let mountedVolumeSource = "api";
2224
+ if (!mountedVolume && serviceKey === "marketOperationsRunner" && expectedServiceId && expectedEnvironmentId) {
2225
+ ensureRailwayProjectContext(service, { env: topology.env, capture: true });
2226
+ const cliVolumes = listRailwayServiceVolumesWithCli({
2227
+ cwd: service.rootDir,
2228
+ serviceId: expectedServiceId,
2229
+ environmentId: expectedEnvironmentId,
2230
+ name: deriveRailwayMarketOperationsRunnerVolumeName(entry.service?.name ?? service.serviceName ?? service.key, entry.environment?.name ?? service.railwayEnvironment),
2231
+ mountPath: service.volumeMountPath,
2232
+ env: topology.env
2233
+ });
2234
+ mountedVolume = cliVolumes.find((volume) => volume.instances.some(
2235
+ (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
2236
+ )) ?? null;
2237
+ mountedVolumeSource = mountedVolume ? "cli" : "api";
2238
+ }
2222
2239
  checks.push(verificationCheck("railway.volume:data", "Railway service has persistent data volume mounted", "api", {
2223
2240
  exists: Boolean(mountedVolume),
2224
2241
  configured: Boolean(mountedVolume),
2225
2242
  expected: service.volumeMountPath,
2226
2243
  observed: mountedVolume ? {
2227
2244
  name: mountedVolume.name,
2228
- mountPath: service.volumeMountPath
2245
+ mountPath: service.volumeMountPath,
2246
+ source: mountedVolumeSource
2229
2247
  } : null,
2230
2248
  issues: mountedVolume ? [] : [`Railway service ${service.serviceName ?? service.key} is missing a persistent volume mounted at ${service.volumeMountPath}.`]
2231
2249
  }));
@@ -2247,7 +2265,13 @@ async function verifyRailwayUnit(input) {
2247
2265
  issues: Object.hasOwn(entry.currentVariables, key) ? [] : [`Railway secret ${key} is missing.`]
2248
2266
  }));
2249
2267
  }
2250
- return summarizeVerification(input.unit.unitId, checks);
2268
+ const verification = summarizeVerification(input.unit.unitId, checks);
2269
+ if (!verification.verified && attempt < 12 && railwayVerificationMaySettle(verification)) {
2270
+ attempt += 1;
2271
+ sleepMs(5e3);
2272
+ continue;
2273
+ }
2274
+ return verification;
2251
2275
  } catch (error) {
2252
2276
  if (attempt >= 2 || !isTransientRailwayReconcileError(error)) {
2253
2277
  throw error;
@@ -2257,6 +2281,11 @@ async function verifyRailwayUnit(input) {
2257
2281
  }
2258
2282
  }
2259
2283
  }
2284
+ function railwayVerificationMaySettle(verification) {
2285
+ return verification.checks.some(
2286
+ (check) => !check.verified && (check.key === "railway.instance" || check.key.startsWith("railway.instance.") || check.key === "railway.volume:data")
2287
+ );
2288
+ }
2260
2289
  function railwayStartCommandMatches(serviceKey, observed, expected) {
2261
2290
  if (observed === expected) {
2262
2291
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.10.18",
3
+ "version": "0.10.19",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {