@treeseed/sdk 0.10.15 → 0.10.17

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 {
@@ -316,3 +316,23 @@ export declare function ensureRailwayCustomDomain({ projectId, environmentId, se
316
316
  domain: RailwayCustomDomainSummary;
317
317
  created: boolean;
318
318
  }>;
319
+ export declare function deleteRailwayCustomDomain({ domainId, env, fetchImpl, }: {
320
+ domainId: string;
321
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
322
+ fetchImpl?: typeof fetch;
323
+ }): Promise<Record<string, unknown>>;
324
+ export declare function deleteRailwayVolume({ volumeId, env, fetchImpl, }: {
325
+ volumeId: string;
326
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
327
+ fetchImpl?: typeof fetch;
328
+ }): Promise<Record<string, unknown>>;
329
+ export declare function deleteRailwayEnvironment({ environmentId, env, fetchImpl, }: {
330
+ environmentId: string;
331
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
332
+ fetchImpl?: typeof fetch;
333
+ }): Promise<Record<string, unknown>>;
334
+ export declare function deleteRailwayProject({ projectId, env, fetchImpl, }: {
335
+ projectId: string;
336
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
337
+ fetchImpl?: typeof fetch;
338
+ }): Promise<Record<string, unknown>>;
@@ -1195,11 +1195,13 @@ async function ensureRailwayServiceVolume({
1195
1195
  })).filter((candidate) => candidate.instances.length > 0);
1196
1196
  let volume = activeVolumes.find(
1197
1197
  (candidate) => candidate.instances.some((instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId)
1198
- ) ?? activeVolumes.find((candidate) => candidate.name === name) ?? null;
1198
+ ) ?? activeVolumes.find(
1199
+ (candidate) => candidate.name === name && candidate.instances.some((instance2) => instance2.environmentId === environmentId)
1200
+ ) ?? null;
1199
1201
  let created = false;
1200
1202
  let updated = false;
1201
- if (!volume) {
1202
- volume = await createRailwayVolume({
1203
+ const createReplacementVolume = async () => {
1204
+ const replacement = await createRailwayVolume({
1203
1205
  projectId,
1204
1206
  environmentId,
1205
1207
  serviceId,
@@ -1209,24 +1211,50 @@ async function ensureRailwayServiceVolume({
1209
1211
  fetchImpl
1210
1212
  });
1211
1213
  created = true;
1214
+ return replacement;
1215
+ };
1216
+ if (!volume) {
1217
+ volume = await createReplacementVolume();
1212
1218
  }
1213
1219
  if (volume.name && volume.name !== name) {
1214
- volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1220
+ try {
1221
+ volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1222
+ } catch (error) {
1223
+ if (!looksLikeRailwayMissingResource(error)) {
1224
+ throw error;
1225
+ }
1226
+ volume = await createReplacementVolume();
1227
+ }
1215
1228
  updated = true;
1216
1229
  }
1217
1230
  let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1218
1231
  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);
1232
+ try {
1233
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
1234
+ volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
1235
+ } catch (error) {
1236
+ if (!looksLikeRailwayMissingResource(error)) {
1237
+ throw error;
1238
+ }
1239
+ volume = await createReplacementVolume();
1240
+ }
1221
1241
  instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1222
1242
  updated = true;
1223
1243
  }
1224
1244
  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
- };
1245
+ try {
1246
+ await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
1247
+ volume = {
1248
+ ...volume,
1249
+ instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1250
+ };
1251
+ } catch (error) {
1252
+ if (!looksLikeRailwayMissingResource(error)) {
1253
+ throw error;
1254
+ }
1255
+ volume = await createReplacementVolume();
1256
+ instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1257
+ }
1230
1258
  updated = true;
1231
1259
  }
1232
1260
  return { volume, instance, created, updated };
@@ -1340,7 +1368,121 @@ mutation TreeseedRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
1340
1368
  }
1341
1369
  return { domain: created, created: true };
1342
1370
  }
1371
+ function looksLikeRailwayMissingResource(error) {
1372
+ const message = error instanceof Error ? error.message : String(error ?? "");
1373
+ return /not found|does not exist|could not find|unknown|invalid .*id/iu.test(message);
1374
+ }
1375
+ async function railwayDeleteMutation({
1376
+ query,
1377
+ variables,
1378
+ env,
1379
+ fetchImpl,
1380
+ missingResult
1381
+ }) {
1382
+ try {
1383
+ await railwayGraphqlRequest({
1384
+ query,
1385
+ variables,
1386
+ env,
1387
+ fetchImpl
1388
+ });
1389
+ return { status: "deleted" };
1390
+ } catch (error) {
1391
+ if (looksLikeRailwayMissingResource(error)) {
1392
+ return missingResult;
1393
+ }
1394
+ throw error;
1395
+ }
1396
+ }
1397
+ async function deleteRailwayCustomDomain({
1398
+ domainId,
1399
+ env = process.env,
1400
+ fetchImpl = fetch
1401
+ }) {
1402
+ if (!railwayConnectionLabel(domainId)) {
1403
+ return { status: "missing", id: domainId };
1404
+ }
1405
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_CUSTOM_DOMAIN_DELETE_MUTATION") || `
1406
+ mutation TreeseedRailwayCustomDomainDelete($id: String!) {
1407
+ customDomainDelete(id: $id)
1408
+ }
1409
+ `.trim();
1410
+ return railwayDeleteMutation({
1411
+ query: mutation,
1412
+ variables: { id: domainId },
1413
+ env,
1414
+ fetchImpl,
1415
+ missingResult: { status: "missing", id: domainId }
1416
+ });
1417
+ }
1418
+ async function deleteRailwayVolume({
1419
+ volumeId,
1420
+ env = process.env,
1421
+ fetchImpl = fetch
1422
+ }) {
1423
+ if (!railwayConnectionLabel(volumeId)) {
1424
+ return { status: "missing", id: volumeId };
1425
+ }
1426
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION") || `
1427
+ mutation TreeseedRailwayVolumeDelete($volumeId: String!) {
1428
+ volumeDelete(volumeId: $volumeId)
1429
+ }
1430
+ `.trim();
1431
+ return railwayDeleteMutation({
1432
+ query: mutation,
1433
+ variables: { volumeId },
1434
+ env,
1435
+ fetchImpl,
1436
+ missingResult: { status: "missing", id: volumeId }
1437
+ });
1438
+ }
1439
+ async function deleteRailwayEnvironment({
1440
+ environmentId,
1441
+ env = process.env,
1442
+ fetchImpl = fetch
1443
+ }) {
1444
+ if (!railwayConnectionLabel(environmentId)) {
1445
+ return { status: "missing", id: environmentId };
1446
+ }
1447
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_ENVIRONMENT_DELETE_MUTATION") || `
1448
+ mutation TreeseedRailwayEnvironmentDelete($id: String!) {
1449
+ environmentDelete(id: $id)
1450
+ }
1451
+ `.trim();
1452
+ return railwayDeleteMutation({
1453
+ query: mutation,
1454
+ variables: { id: environmentId },
1455
+ env,
1456
+ fetchImpl,
1457
+ missingResult: { status: "missing", id: environmentId }
1458
+ });
1459
+ }
1460
+ async function deleteRailwayProject({
1461
+ projectId,
1462
+ env = process.env,
1463
+ fetchImpl = fetch
1464
+ }) {
1465
+ if (!railwayConnectionLabel(projectId)) {
1466
+ return { status: "missing", id: projectId };
1467
+ }
1468
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_PROJECT_DELETE_MUTATION") || `
1469
+ mutation TreeseedRailwayProjectDelete($id: String!) {
1470
+ projectDelete(id: $id)
1471
+ }
1472
+ `.trim();
1473
+ return railwayDeleteMutation({
1474
+ query: mutation,
1475
+ variables: { id: projectId },
1476
+ env,
1477
+ fetchImpl,
1478
+ missingResult: { status: "missing", id: projectId }
1479
+ });
1480
+ }
1343
1481
  export {
1482
+ deleteRailwayCustomDomain,
1483
+ deleteRailwayEnvironment,
1484
+ deleteRailwayProject,
1485
+ deleteRailwayVolume,
1344
1486
  ensureRailwayCustomDomain,
1345
1487
  ensureRailwayEnvironment,
1346
1488
  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;
@@ -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;
@@ -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) {
@@ -587,7 +587,7 @@ function collectCloudflareEnvironmentSync(input) {
587
587
  const registry = collectTreeseedEnvironmentContext(input.context.tenantRoot);
588
588
  const state = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target });
589
589
  const generatedSecrets = buildSecretMap(input.context.deployConfig, state);
590
- const publicVars = buildPublicVars(input.context.deployConfig);
590
+ const publicVars = buildPublicVars(input.context.deployConfig, { target });
591
591
  const secrets = {};
592
592
  const vars = { ...publicVars };
593
593
  const secretNames = /* @__PURE__ */ new Set();
@@ -1578,26 +1578,40 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false } = {}) {
1578
1578
  });
1579
1579
  if (!volume.instance?.serviceId) {
1580
1580
  ensureRailwayProjectContext(entry.configuredService, { env: topology.env, capture: true });
1581
- const attachResult = runRailway([
1582
- "volume",
1583
- "--service",
1584
- entry.service.id,
1585
- "attach",
1586
- "--volume",
1587
- volume.volume.id,
1588
- "--yes",
1589
- "--json"
1590
- ], {
1591
- cwd: entry.configuredService.rootDir,
1592
- capture: true,
1593
- allowFailure: true,
1594
- env: topology.env
1595
- });
1596
- if ((attachResult.status ?? 1) !== 0) {
1597
- const attachMessage = attachResult.stderr?.trim() || attachResult.stdout?.trim() || "";
1598
- if (!/already mounted/iu.test(attachMessage)) {
1599
- throw new Error(attachMessage || `Railway volume attach failed for ${entry.service.name}.`);
1581
+ let attachMessage = "";
1582
+ let attached = false;
1583
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1584
+ const attachResult = runRailway([
1585
+ "volume",
1586
+ "--service",
1587
+ entry.service.id,
1588
+ "attach",
1589
+ "--volume",
1590
+ volume.volume.id,
1591
+ "--yes",
1592
+ "--json"
1593
+ ], {
1594
+ cwd: entry.configuredService.rootDir,
1595
+ capture: true,
1596
+ allowFailure: true,
1597
+ env: topology.env
1598
+ });
1599
+ if ((attachResult.status ?? 1) === 0) {
1600
+ attached = true;
1601
+ break;
1602
+ }
1603
+ attachMessage = attachResult.stderr?.trim() || attachResult.stdout?.trim() || "";
1604
+ if (/already mounted/iu.test(attachMessage)) {
1605
+ attached = true;
1606
+ break;
1600
1607
  }
1608
+ if (!/volume .*not found|not found|does not exist/iu.test(attachMessage)) {
1609
+ break;
1610
+ }
1611
+ sleepMs(2e3 * (attempt + 1));
1612
+ }
1613
+ if (!attached) {
1614
+ throw new Error(attachMessage || `Railway volume attach failed for ${entry.service.name}.`);
1601
1615
  }
1602
1616
  }
1603
1617
  }
@@ -2,7 +2,7 @@
2
2
  import readline from 'node:readline/promises';
3
3
  import { stdin as input, stdout as output } from 'node:process';
4
4
  import { applyTreeseedEnvironmentToProcess, assertTreeseedCommandEnvironment } from '../operations/services/config-runtime.js';
5
- import { cleanupDestroyedState, createPersistentDeployTarget, destroyCloudflareResources, loadDeployState, printDestroySummary, validateDestroyPrerequisites, } from '../operations/services/deploy.js';
5
+ import { cleanupDestroyedState, createPersistentDeployTarget, destroyTreeseedEnvironmentResources, loadDeployState, printDestroySummary, validateDestroyPrerequisites, } from '../operations/services/deploy.js';
6
6
  import { deriveCloudflareWorkerName } from '../platform/deploy-config.js';
7
7
  const tenantRoot = process.cwd();
8
8
  function parseArgs(argv) {
@@ -11,6 +11,7 @@ function parseArgs(argv) {
11
11
  force: false,
12
12
  skipConfirmation: false,
13
13
  confirm: null,
14
+ deleteData: false,
14
15
  removeBuildArtifacts: false,
15
16
  environment: null,
16
17
  };
@@ -27,6 +28,10 @@ function parseArgs(argv) {
27
28
  parsed.force = true;
28
29
  continue;
29
30
  }
31
+ if (current === '--delete-data') {
32
+ parsed.deleteData = true;
33
+ continue;
34
+ }
30
35
  if (current === '--skip-confirmation') {
31
36
  parsed.skipConfirmation = true;
32
37
  continue;
@@ -92,9 +97,10 @@ if (!options.skipConfirmation) {
92
97
  process.exit(1);
93
98
  }
94
99
  }
95
- const result = destroyCloudflareResources(tenantRoot, {
100
+ const result = await destroyTreeseedEnvironmentResources(tenantRoot, {
96
101
  dryRun: options.dryRun,
97
102
  force: options.force,
103
+ deleteData: options.deleteData,
98
104
  target,
99
105
  });
100
106
  printDestroySummary(result);