@treeseed/sdk 0.10.16 → 0.10.18

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.
@@ -3,6 +3,7 @@ import type { TreeseedRunnableBootstrapSystem } from '../../reconcile/index.ts';
3
3
  import { type TreeseedBootstrapExecution, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
4
4
  export type ProjectPlatformScope = 'local' | 'staging' | 'prod';
5
5
  export type ProjectPlatformAction = 'deploy_web' | 'publish_content' | 'monitor';
6
+ type ProjectPlatformContentPublishMode = 'production' | 'editorial_overlay';
6
7
  export interface ProjectPlatformActionOptions {
7
8
  tenantRoot: string;
8
9
  scope: ProjectPlatformScope;
@@ -292,7 +293,7 @@ export declare function resolveRailwayServiceDeployDependencies({ includeDataDep
292
293
  export declare function publishProjectContent(options: ProjectPlatformActionOptions): Promise<{
293
294
  ok: boolean;
294
295
  scope: ProjectPlatformScope;
295
- mode: string;
296
+ mode: ProjectPlatformContentPublishMode;
296
297
  revision: string;
297
298
  previewId: string | null;
298
299
  previewUrl: string | null;
@@ -454,7 +455,7 @@ export declare function syncControlPlaneState(options: ProjectPlatformActionOpti
454
455
  export declare function runProjectPlatformAction(action: ProjectPlatformAction, options: ProjectPlatformActionOptions): Promise<{
455
456
  ok: boolean;
456
457
  scope: ProjectPlatformScope;
457
- mode: string;
458
+ mode: ProjectPlatformContentPublishMode;
458
459
  revision: string;
459
460
  previewId: string | null;
460
461
  previewUrl: string | null;
@@ -809,3 +810,4 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
809
810
  } | null;
810
811
  } | undefined)[];
811
812
  }>;
813
+ export {};
@@ -27,6 +27,7 @@ import {
27
27
  purgePublishedContentCaches,
28
28
  resolveConfiguredCloudflareAccountId,
29
29
  resolveConfiguredSurfaceBaseUrl,
30
+ resolveTreeseedResourceIdentity,
30
31
  syncCloudflareSecrets,
31
32
  writeDeployState
32
33
  } from "./deploy.js";
@@ -220,7 +221,8 @@ function prepareTenantCloudflareDeploy({
220
221
  env: {
221
222
  ...process.env,
222
223
  ...env,
223
- CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
224
+ CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig),
225
+ ...target.kind === "persistent" && target.scope !== "local" ? { TREESEED_CONTENT_SERVING_MODE: "published_runtime" } : {}
224
226
  },
225
227
  write
226
228
  };
@@ -464,8 +466,8 @@ function resolveReporter(tenantRoot, explicit) {
464
466
  const deployConfig = loadCliDeployConfig(tenantRoot);
465
467
  return createControlPlaneReporter({ deployConfig });
466
468
  }
467
- function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer, filePath) {
468
- runWrangler(tenantRoot, [
469
+ async function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer, filePath, options) {
470
+ await runPrefixedWranglerWithRetry(tenantRoot, [
469
471
  "r2",
470
472
  "object",
471
473
  "put",
@@ -478,7 +480,11 @@ function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer
478
480
  filePath,
479
481
  "--content-type",
480
482
  pointer.contentType ?? inferContentType(filePath)
481
- ], wranglerEnv);
483
+ ], {
484
+ env: wranglerEnv,
485
+ write: options.write,
486
+ prefix: options.prefix
487
+ });
482
488
  }
483
489
  function deleteObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, objectKey) {
484
490
  runWrangler(tenantRoot, [
@@ -794,11 +800,11 @@ function probeScaleConfiguration(siteConfig, state) {
794
800
  serviceName: worker.serviceName ?? null
795
801
  };
796
802
  }
797
- async function publishContent(options, reporter) {
803
+ async function publishContent(options, reporter, publishOptions = {}) {
798
804
  const target = runTenantPublishContentPreflight(options);
799
805
  const siteConfig = loadCliDeployConfig(options.tenantRoot);
800
806
  const tenantConfig = loadTreeseedManifest(resolve(options.tenantRoot, "src", "manifest.yaml"));
801
- const teamId = String(process.env.TREESEED_HOSTING_TEAM_ID ?? siteConfig.hosting?.teamId ?? siteConfig.slug).trim() || siteConfig.slug;
807
+ const teamId = resolveTreeseedResourceIdentity(siteConfig, target).teamId;
802
808
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
803
809
  const commitSha = currentCommit(options.tenantRoot);
804
810
  const branchName = currentRef(options.tenantRoot);
@@ -827,7 +833,8 @@ async function publishContent(options, reporter) {
827
833
  sourceRef: branchName,
828
834
  previewId
829
835
  });
830
- const built = options.scope === "staging" ? await pipeline.buildEditorialOverlay({ previousManifest, previewId }) : await pipeline.buildProductionRevision({ previousManifest });
836
+ const publishMode = publishOptions.mode ?? (options.scope === "staging" ? "editorial_overlay" : "production");
837
+ const built = publishMode === "editorial_overlay" ? await pipeline.buildEditorialOverlay({ previousManifest, previewId }) : await pipeline.buildProductionRevision({ previousManifest });
831
838
  const changedEntrySet = "manifest" in built ? changedEntries(previousManifest, built.manifest.entries) : [];
832
839
  const changedArtifactSet = "manifest" in built ? changedArtifacts(previousManifest, built.manifest.artifacts ?? []) : [];
833
840
  const tombstones = "manifest" in built ? built.manifest.tombstones ?? [] : [];
@@ -910,20 +917,29 @@ async function publishContent(options, reporter) {
910
917
  const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-content-publish-"));
911
918
  try {
912
919
  if (!options.dryRun) {
920
+ const uploadOptions = {
921
+ write: options.write,
922
+ prefix: {
923
+ scope: options.scope,
924
+ system: "content",
925
+ task: "publish",
926
+ stage: "upload"
927
+ }
928
+ };
913
929
  for (const object of built.objects) {
914
930
  const filePath = writeTempFile(tempRoot, objectFileName(object.pointer), toBuffer(object.body));
915
- uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, object.pointer, filePath);
931
+ await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, object.pointer, filePath, uploadOptions);
916
932
  }
917
933
  for (const alias of stableObjectUploads) {
918
934
  const filePath = writeTempFile(tempRoot, objectFileName(alias.pointer), alias.body);
919
- uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, alias.pointer, filePath);
935
+ await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, alias.pointer, filePath, uploadOptions);
920
936
  }
921
937
  for (const objectKey of deletedObjectKeys) {
922
938
  deleteObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, objectKey);
923
939
  }
924
940
  if ("overlay" in built) {
925
941
  const overlayFile = writeTempFile(tempRoot, "overlay.json", Buffer.from(JSON.stringify(built.overlay, null, 2)));
926
- uploadObject(
942
+ await uploadObject(
927
943
  options.tenantRoot,
928
944
  wranglerPath,
929
945
  wranglerEnv,
@@ -934,23 +950,24 @@ async function publishContent(options, reporter) {
934
950
  size: statSync(overlayFile).size,
935
951
  contentType: "application/json"
936
952
  },
937
- overlayFile
953
+ overlayFile,
954
+ uploadOptions
938
955
  );
939
956
  } else {
940
957
  const manifestFile = writeTempFile(tempRoot, "manifest.json", Buffer.from(JSON.stringify(built.manifest, null, 2)));
941
958
  const snapshotKey = locator.manifestKey.replace(/\/common\.json$/u, `/manifests/${built.manifest.revision}.json`);
942
- uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
959
+ await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
943
960
  objectKey: snapshotKey,
944
961
  sha256: stableHash(readFileSync(manifestFile)),
945
962
  size: statSync(manifestFile).size,
946
963
  contentType: "application/json"
947
- }, manifestFile);
948
- uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
964
+ }, manifestFile, uploadOptions);
965
+ await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
949
966
  objectKey: locator.manifestKey,
950
967
  sha256: stableHash(readFileSync(manifestFile)),
951
968
  size: statSync(manifestFile).size,
952
969
  contentType: "application/json"
953
- }, manifestFile);
970
+ }, manifestFile, uploadOptions);
954
971
  if (contentPurgeUrls.size > 0) {
955
972
  try {
956
973
  purgePublishedContentCaches(options.tenantRoot, [...contentPurgeUrls].filter(Boolean), { target });
@@ -960,12 +977,14 @@ async function publishContent(options, reporter) {
960
977
  }
961
978
  }
962
979
  const state = loadDeployState(options.tenantRoot, siteConfig, { target });
963
- state.content.lastPublishedManifestRevision = "overlay" in built ? built.overlay.previewId : built.manifest.revision;
964
- state.content.lastPublishedManifestSha256 = stableHash(
965
- JSON.stringify("overlay" in built ? built.overlay : built.manifest)
966
- );
967
- writeDeployState(options.tenantRoot, state, { target });
968
- const previewToken = options.scope === "staging" && process.env.TREESEED_EDITORIAL_PREVIEW_SECRET ? signEditorialPreviewToken({
980
+ if (!options.dryRun) {
981
+ state.content.lastPublishedManifestRevision = "overlay" in built ? built.overlay.previewId : built.manifest.revision;
982
+ state.content.lastPublishedManifestSha256 = stableHash(
983
+ JSON.stringify("overlay" in built ? built.overlay : built.manifest)
984
+ );
985
+ writeDeployState(options.tenantRoot, state, { target });
986
+ }
987
+ const previewToken = publishMode === "editorial_overlay" && process.env.TREESEED_EDITORIAL_PREVIEW_SECRET ? signEditorialPreviewToken({
969
988
  teamId,
970
989
  previewId,
971
990
  expiresAt: "overlay" in built ? built.overlay.expiresAt ?? new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString() : new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString()
@@ -980,9 +999,9 @@ async function publishContent(options, reporter) {
980
999
  commitSha,
981
1000
  triggeredByType: "project_runner",
982
1001
  metadata: {
983
- mode: options.scope === "staging" ? "editorial_overlay" : "production",
1002
+ mode: publishMode,
984
1003
  revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
985
- previewId: options.scope === "staging" ? previewId : null,
1004
+ previewId: publishMode === "editorial_overlay" ? previewId : null,
986
1005
  previewUrl,
987
1006
  entries: ("overlay" in built ? built.overlay.entries : built.manifest.entries).length,
988
1007
  artifacts: ("overlay" in built ? built.overlay.artifacts : built.manifest.artifacts)?.length ?? 0,
@@ -994,9 +1013,9 @@ async function publishContent(options, reporter) {
994
1013
  return {
995
1014
  ok: true,
996
1015
  scope: options.scope,
997
- mode: options.scope === "staging" ? "editorial_overlay" : "production",
1016
+ mode: publishMode,
998
1017
  revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
999
- previewId: options.scope === "staging" ? previewId : null,
1018
+ previewId: publishMode === "editorial_overlay" ? previewId : null,
1000
1019
  previewUrl,
1001
1020
  target: deployTargetLabel(target)
1002
1021
  };
@@ -1214,13 +1233,23 @@ async function deployProjectPlatform(options) {
1214
1233
  }
1215
1234
  if (cloudflareContext && selectedSystems.has("web")) {
1216
1235
  const context = cloudflareContext;
1236
+ const contentNodeId = "content:publish-runtime";
1237
+ nodes.push({
1238
+ id: contentNodeId,
1239
+ dependencies: selectedSystems.has("data") ? ["data:d1-migrate"] : [],
1240
+ run: () => publishContent({
1241
+ ...options,
1242
+ reporter,
1243
+ bootstrapSystems: ["web"]
1244
+ }, reporter, { mode: "production" })
1245
+ });
1217
1246
  nodes.push({
1218
1247
  id: "web:build",
1219
1248
  run: () => runTenantWebBuild(context)
1220
1249
  });
1221
1250
  nodes.push({
1222
1251
  id: "web:publish",
1223
- dependencies: ["web:build", ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
1252
+ dependencies: ["web:build", contentNodeId, ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
1224
1253
  run: () => runTenantWebPublish(context)
1225
1254
  });
1226
1255
  }
@@ -1452,7 +1481,7 @@ async function runProjectPlatformAction(action, options) {
1452
1481
  const previousWorkflowAction = process.env.TREESEED_WORKFLOW_ACTION;
1453
1482
  const previousWorkflowPlane = process.env.TREESEED_WORKFLOW_PLANE;
1454
1483
  process.env.TREESEED_WORKFLOW_ACTION = action;
1455
- process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane ?? "web";
1484
+ process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane ?? "all";
1456
1485
  applyTreeseedEnvironmentToProcess({ tenantRoot: options.tenantRoot, scope: options.scope, override: true });
1457
1486
  const reporter = resolveReporter(options.tenantRoot, options.reporter);
1458
1487
  try {
@@ -288,12 +288,7 @@ export declare function ensureRailwayServiceVolume({ projectId, environmentId, s
288
288
  env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
289
289
  fetchImpl?: typeof fetch;
290
290
  }): Promise<{
291
- volume: {
292
- instances: RailwayVolumeInstanceSummary[];
293
- id: string;
294
- name: string;
295
- projectId: string | null;
296
- } | null;
291
+ volume: RailwayVolumeSummary | null;
297
292
  instance: RailwayVolumeInstanceSummary | null;
298
293
  created: boolean;
299
294
  updated: boolean;
@@ -316,3 +311,23 @@ export declare function ensureRailwayCustomDomain({ projectId, environmentId, se
316
311
  domain: RailwayCustomDomainSummary;
317
312
  created: boolean;
318
313
  }>;
314
+ export declare function deleteRailwayCustomDomain({ domainId, env, fetchImpl, }: {
315
+ domainId: string;
316
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
317
+ fetchImpl?: typeof fetch;
318
+ }): Promise<Record<string, unknown>>;
319
+ export declare function deleteRailwayVolume({ volumeId, env, fetchImpl, }: {
320
+ volumeId: string;
321
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
322
+ fetchImpl?: typeof fetch;
323
+ }): Promise<Record<string, unknown>>;
324
+ export declare function deleteRailwayEnvironment({ environmentId, env, fetchImpl, }: {
325
+ environmentId: string;
326
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
327
+ fetchImpl?: typeof fetch;
328
+ }): Promise<Record<string, unknown>>;
329
+ export declare function deleteRailwayProject({ projectId, env, fetchImpl, }: {
330
+ projectId: string;
331
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
332
+ fetchImpl?: typeof fetch;
333
+ }): Promise<Record<string, unknown>>;
@@ -840,7 +840,13 @@ async function ensureRailwayServiceInstanceConfiguration({
840
840
  env = process.env,
841
841
  fetchImpl = fetch
842
842
  }) {
843
- const current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
843
+ let current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
844
+ if (!current.id) {
845
+ for (let attempt = 0; attempt < 8 && !current.id; attempt += 1) {
846
+ await new Promise((resolve) => setTimeout(resolve, 1500));
847
+ current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
848
+ }
849
+ }
844
850
  if (!current.id) {
845
851
  return { instance: current, updated: false };
846
852
  }
@@ -905,12 +911,21 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
905
911
  }
906
912
  throw error;
907
913
  }
908
- const instance = await getRailwayServiceInstance({
914
+ let instance = await getRailwayServiceInstance({
909
915
  serviceId,
910
916
  environmentId,
911
917
  env,
912
918
  fetchImpl
913
919
  });
920
+ for (let attempt = 0; attempt < 5 && serviceInstanceDrifted(instance, desired); attempt += 1) {
921
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
922
+ instance = await getRailwayServiceInstance({
923
+ serviceId,
924
+ environmentId,
925
+ env,
926
+ fetchImpl
927
+ });
928
+ }
914
929
  return {
915
930
  instance: {
916
931
  id: instance.id || current.id,
@@ -929,6 +944,9 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
929
944
  updated: true
930
945
  };
931
946
  }
947
+ function serviceInstanceDrifted(current, desired) {
948
+ return desired.buildCommand !== null && desired.buildCommand !== current.buildCommand || desired.startCommand !== null && desired.startCommand !== current.startCommand || desired.cronSchedule !== null && desired.cronSchedule !== current.cronSchedule || desired.rootDirectory !== null && desired.rootDirectory !== current.rootDirectory || desired.healthcheckPath !== null && desired.healthcheckPath !== current.healthcheckPath || desired.healthcheckTimeoutSeconds !== null && desired.healthcheckTimeoutSeconds !== current.healthcheckTimeoutSeconds || desired.runtimeMode !== null && desired.runtimeMode !== current.runtimeMode;
949
+ }
932
950
  async function listRailwayVariables({
933
951
  projectId,
934
952
  environmentId,
@@ -1193,44 +1211,97 @@ async function ensureRailwayServiceVolume({
1193
1211
  ...candidate,
1194
1212
  instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
1195
1213
  })).filter((candidate) => candidate.instances.length > 0);
1196
- let volume = activeVolumes.find(
1197
- (candidate) => candidate.instances.some((instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId)
1198
- ) ?? activeVolumes.find((candidate) => candidate.name === name) ?? null;
1214
+ let volume = findRailwayVolumeForService(volumes, serviceId, environmentId) ?? activeVolumes.find(
1215
+ (candidate) => candidate.name === name && candidate.instances.some((instance2) => instance2.environmentId === environmentId)
1216
+ ) ?? null;
1199
1217
  let created = false;
1200
1218
  let updated = false;
1201
- if (!volume) {
1202
- volume = await createRailwayVolume({
1203
- projectId,
1204
- environmentId,
1205
- serviceId,
1206
- name,
1207
- mountPath,
1208
- env,
1209
- fetchImpl
1210
- });
1219
+ const createReplacementVolume = async () => {
1220
+ let replacement;
1221
+ try {
1222
+ replacement = await createRailwayVolume({
1223
+ projectId,
1224
+ environmentId,
1225
+ serviceId,
1226
+ name,
1227
+ mountPath,
1228
+ env,
1229
+ fetchImpl
1230
+ });
1231
+ } catch (error) {
1232
+ if (!looksLikeRailwayVolumeCreateRace(error)) {
1233
+ throw error;
1234
+ }
1235
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1236
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1237
+ const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
1238
+ const existing = findRailwayVolumeForService(refreshed, serviceId, environmentId);
1239
+ if (existing) {
1240
+ return existing;
1241
+ }
1242
+ }
1243
+ throw error;
1244
+ }
1211
1245
  created = true;
1246
+ return replacement;
1247
+ };
1248
+ if (!volume) {
1249
+ volume = await createReplacementVolume();
1212
1250
  }
1213
1251
  if (volume.name && volume.name !== name) {
1214
- volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1252
+ try {
1253
+ volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1254
+ } catch (error) {
1255
+ if (!looksLikeRailwayMissingResource(error)) {
1256
+ throw error;
1257
+ }
1258
+ volume = await createReplacementVolume();
1259
+ }
1215
1260
  updated = true;
1216
1261
  }
1217
1262
  let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1218
1263
  if (!instance && volume.instances.some((entry) => entry.environmentId === environmentId)) {
1219
- await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1220
- volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1264
+ try {
1265
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1266
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1267
+ } catch (error) {
1268
+ if (!looksLikeRailwayMissingResource(error)) {
1269
+ throw error;
1270
+ }
1271
+ volume = await createReplacementVolume();
1272
+ }
1221
1273
  instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1222
1274
  updated = true;
1223
1275
  }
1224
1276
  if (instance && instance.mountPath !== mountPath) {
1225
- await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
1226
- volume = {
1227
- ...volume,
1228
- instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1229
- };
1277
+ try {
1278
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
1279
+ volume = {
1280
+ ...volume,
1281
+ instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1282
+ };
1283
+ } catch (error) {
1284
+ if (!looksLikeRailwayMissingResource(error)) {
1285
+ throw error;
1286
+ }
1287
+ volume = await createReplacementVolume();
1288
+ instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1289
+ }
1230
1290
  updated = true;
1231
1291
  }
1232
1292
  return { volume, instance, created, updated };
1233
1293
  }
1294
+ function findRailwayVolumeForService(volumes, serviceId, environmentId) {
1295
+ return volumes.find(
1296
+ (candidate) => candidate.instances.some(
1297
+ (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && isActiveRailwayVolumeInstance(instance)
1298
+ )
1299
+ ) ?? null;
1300
+ }
1301
+ function looksLikeRailwayVolumeCreateRace(error) {
1302
+ const message = error instanceof Error ? error.message : String(error ?? "");
1303
+ return /would have \d+ volumes attached|can only have one volume|not authorized/iu.test(message);
1304
+ }
1234
1305
  async function listRailwayCustomDomains({
1235
1306
  projectId,
1236
1307
  environmentId,
@@ -1340,7 +1411,121 @@ mutation TreeseedRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
1340
1411
  }
1341
1412
  return { domain: created, created: true };
1342
1413
  }
1414
+ function looksLikeRailwayMissingResource(error) {
1415
+ const message = error instanceof Error ? error.message : String(error ?? "");
1416
+ return /not found|does not exist|could not find|unknown|invalid .*id/iu.test(message);
1417
+ }
1418
+ async function railwayDeleteMutation({
1419
+ query,
1420
+ variables,
1421
+ env,
1422
+ fetchImpl,
1423
+ missingResult
1424
+ }) {
1425
+ try {
1426
+ await railwayGraphqlRequest({
1427
+ query,
1428
+ variables,
1429
+ env,
1430
+ fetchImpl
1431
+ });
1432
+ return { status: "deleted" };
1433
+ } catch (error) {
1434
+ if (looksLikeRailwayMissingResource(error)) {
1435
+ return missingResult;
1436
+ }
1437
+ throw error;
1438
+ }
1439
+ }
1440
+ async function deleteRailwayCustomDomain({
1441
+ domainId,
1442
+ env = process.env,
1443
+ fetchImpl = fetch
1444
+ }) {
1445
+ if (!railwayConnectionLabel(domainId)) {
1446
+ return { status: "missing", id: domainId };
1447
+ }
1448
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_CUSTOM_DOMAIN_DELETE_MUTATION") || `
1449
+ mutation TreeseedRailwayCustomDomainDelete($id: String!) {
1450
+ customDomainDelete(id: $id)
1451
+ }
1452
+ `.trim();
1453
+ return railwayDeleteMutation({
1454
+ query: mutation,
1455
+ variables: { id: domainId },
1456
+ env,
1457
+ fetchImpl,
1458
+ missingResult: { status: "missing", id: domainId }
1459
+ });
1460
+ }
1461
+ async function deleteRailwayVolume({
1462
+ volumeId,
1463
+ env = process.env,
1464
+ fetchImpl = fetch
1465
+ }) {
1466
+ if (!railwayConnectionLabel(volumeId)) {
1467
+ return { status: "missing", id: volumeId };
1468
+ }
1469
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION") || `
1470
+ mutation TreeseedRailwayVolumeDelete($volumeId: String!) {
1471
+ volumeDelete(volumeId: $volumeId)
1472
+ }
1473
+ `.trim();
1474
+ return railwayDeleteMutation({
1475
+ query: mutation,
1476
+ variables: { volumeId },
1477
+ env,
1478
+ fetchImpl,
1479
+ missingResult: { status: "missing", id: volumeId }
1480
+ });
1481
+ }
1482
+ async function deleteRailwayEnvironment({
1483
+ environmentId,
1484
+ env = process.env,
1485
+ fetchImpl = fetch
1486
+ }) {
1487
+ if (!railwayConnectionLabel(environmentId)) {
1488
+ return { status: "missing", id: environmentId };
1489
+ }
1490
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_ENVIRONMENT_DELETE_MUTATION") || `
1491
+ mutation TreeseedRailwayEnvironmentDelete($id: String!) {
1492
+ environmentDelete(id: $id)
1493
+ }
1494
+ `.trim();
1495
+ return railwayDeleteMutation({
1496
+ query: mutation,
1497
+ variables: { id: environmentId },
1498
+ env,
1499
+ fetchImpl,
1500
+ missingResult: { status: "missing", id: environmentId }
1501
+ });
1502
+ }
1503
+ async function deleteRailwayProject({
1504
+ projectId,
1505
+ env = process.env,
1506
+ fetchImpl = fetch
1507
+ }) {
1508
+ if (!railwayConnectionLabel(projectId)) {
1509
+ return { status: "missing", id: projectId };
1510
+ }
1511
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_PROJECT_DELETE_MUTATION") || `
1512
+ mutation TreeseedRailwayProjectDelete($id: String!) {
1513
+ projectDelete(id: $id)
1514
+ }
1515
+ `.trim();
1516
+ return railwayDeleteMutation({
1517
+ query: mutation,
1518
+ variables: { id: projectId },
1519
+ env,
1520
+ fetchImpl,
1521
+ missingResult: { status: "missing", id: projectId }
1522
+ });
1523
+ }
1343
1524
  export {
1525
+ deleteRailwayCustomDomain,
1526
+ deleteRailwayEnvironment,
1527
+ deleteRailwayProject,
1528
+ deleteRailwayVolume,
1344
1529
  ensureRailwayCustomDomain,
1345
1530
  ensureRailwayEnvironment,
1346
1531
  ensureRailwayPostgresService,
@@ -14,10 +14,13 @@ export declare function buildRailwayDeployCommandEnv(env?: NodeJS.ProcessEnv): {
14
14
  [key: string]: string | undefined;
15
15
  };
16
16
  export declare function isRailwayTransientFailure(result: any): boolean;
17
- export declare function runRailway(args: any, { cwd, capture, allowFailure, input, env }?: {
17
+ export declare function runRailway(args: any, { cwd, capture, allowFailure, input, env, retryTransient, retryAttempts, retryDelayMs }?: {
18
18
  capture?: boolean | undefined;
19
19
  allowFailure?: boolean | undefined;
20
- }): import("child_process").SpawnSyncReturns<string>;
20
+ retryTransient?: boolean | undefined;
21
+ retryAttempts?: number | undefined;
22
+ retryDelayMs?: number | undefined;
23
+ }): import("child_process").SpawnSyncReturns<string> | null;
21
24
  export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, onProgress, }?: {
22
25
  services?: unknown[] | undefined;
23
26
  env?: NodeJS.ProcessEnv | undefined;
@@ -53,11 +56,11 @@ export declare function ensureRailwayEnvironmentExists(service: any, { env }?: {
53
56
  }): import("child_process").SpawnSyncReturns<string> | null;
54
57
  export declare function ensureRailwayServiceExists(service: any, { env }?: {
55
58
  env?: NodeJS.ProcessEnv | undefined;
56
- }): import("child_process").SpawnSyncReturns<string>;
59
+ }): import("child_process").SpawnSyncReturns<string> | null;
57
60
  export declare function ensureRailwayDatabaseServiceExists(service: any, { database, env, }?: {
58
61
  database?: string | undefined;
59
62
  env?: NodeJS.ProcessEnv | undefined;
60
- }): import("child_process").SpawnSyncReturns<string>;
63
+ }): import("child_process").SpawnSyncReturns<string> | null;
61
64
  export declare function ensureRailwayProjectContext(service: any, { env, allowFailure, capture }?: {
62
65
  env?: NodeJS.ProcessEnv | undefined;
63
66
  allowFailure?: boolean | undefined;
@@ -141,6 +144,23 @@ export declare function planRailwayServiceLink(service: any, { env }?: {
141
144
  args: string[];
142
145
  cwd: any;
143
146
  };
147
+ export declare function ensureRailwayServiceVolumeWithCliFallback({ tenantRoot, projectId, environmentId, environmentName, serviceId, serviceName, name, mountPath, preferCli, env, }: {
148
+ tenantRoot: any;
149
+ projectId: any;
150
+ environmentId: any;
151
+ environmentName: any;
152
+ serviceId: any;
153
+ serviceName: any;
154
+ name: any;
155
+ mountPath: any;
156
+ preferCli?: boolean | undefined;
157
+ env?: NodeJS.ProcessEnv | undefined;
158
+ }): Promise<{
159
+ volume: any;
160
+ instance: any;
161
+ created: boolean;
162
+ updated: boolean;
163
+ }>;
144
164
  export declare function deployRailwayService(tenantRoot: any, service: any, { dryRun, write, prefix, env, }?: {
145
165
  dryRun?: boolean;
146
166
  write?: TreeseedBootstrapWriter;
@@ -332,7 +332,7 @@ function isRailwayTransientFailure(result) {
332
332
  if (!message.trim() && result?.status === 1) {
333
333
  return true;
334
334
  }
335
- return /timed out|failed to fetch|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(message);
335
+ return /timed out|failed to fetch|error decoding response body|expected value at line 1 column 1|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(message);
336
336
  }
337
337
  function sleepSync(milliseconds) {
338
338
  if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
@@ -412,7 +412,7 @@ mutation TreeseedScheduleUpdate($id: String!, $name: String!, $schedule: String!
412
412
  `.trim()
413
413
  };
414
414
  }
415
- function runRailway(args, { cwd, capture = false, allowFailure = false, input, env } = {}) {
415
+ function runRailway(args, { cwd, capture = false, allowFailure = false, input, env, retryTransient = true, retryAttempts = 3, retryDelayMs = 2e3 } = {}) {
416
416
  const effectiveEnv = buildRailwayCommandEnv({ ...process.env, ...env ?? {} });
417
417
  const railway = resolveTreeseedToolCommand("railway", { env: effectiveEnv });
418
418
  if (!railway) {
@@ -425,8 +425,16 @@ function runRailway(args, { cwd, capture = false, allowFailure = false, input, e
425
425
  env: spawnEnv,
426
426
  input
427
427
  });
428
- const result = runWithEnv(effectiveEnv);
429
- if (result.status !== 0 && !allowFailure) {
428
+ let result = null;
429
+ const maxAttempts = retryTransient && !allowFailure ? Math.max(1, Number(retryAttempts) || 1) : 1;
430
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
431
+ result = runWithEnv(effectiveEnv);
432
+ if (result.status === 0 || allowFailure || !isRailwayTransientFailure(result) || attempt === maxAttempts) {
433
+ break;
434
+ }
435
+ sleepSync((Number(retryDelayMs) || 0) * attempt);
436
+ }
437
+ if (result?.status !== 0 && !allowFailure) {
430
438
  throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${args.join(" ")} failed`);
431
439
  }
432
440
  return result;
@@ -1917,6 +1925,7 @@ export {
1917
1925
  ensureRailwayProjectExists,
1918
1926
  ensureRailwayScheduledJobs,
1919
1927
  ensureRailwayServiceExists,
1928
+ ensureRailwayServiceVolumeWithCliFallback,
1920
1929
  isRailwayTransientFailure,
1921
1930
  isUsableRailwayToken,
1922
1931
  planRailwayServiceDeploy,
@@ -68,6 +68,9 @@ function platformSurfaceEnabled(context, surface) {
68
68
  }
69
69
  function activeWorkflowPlane() {
70
70
  const plane = process.env.TREESEED_WORKFLOW_PLANE;
71
+ if (plane === "all") {
72
+ return null;
73
+ }
71
74
  return plane === "web" || plane === "processing" ? plane : null;
72
75
  }
73
76
  function workflowPlaneAllows(plane) {