@treeseed/sdk 0.6.39 → 0.6.40

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 (44) hide show
  1. package/dist/capacity.d.ts +53 -0
  2. package/dist/capacity.js +100 -0
  3. package/dist/control-plane-client.d.ts +41 -1
  4. package/dist/control-plane-client.js +154 -0
  5. package/dist/control-plane.d.ts +6 -1
  6. package/dist/control-plane.js +39 -2
  7. package/dist/d1-store.d.ts +63 -1
  8. package/dist/d1-store.js +190 -1
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +12 -0
  11. package/dist/operations/services/config-runtime.js +2 -2
  12. package/dist/operations/services/deploy.js +3 -2
  13. package/dist/operations/services/knowledge-coop-launch.js +5 -28
  14. package/dist/operations/services/package-reference-policy.d.ts +68 -0
  15. package/dist/operations/services/package-reference-policy.js +135 -0
  16. package/dist/operations/services/project-platform.d.ts +14 -0
  17. package/dist/operations/services/project-platform.js +3 -2
  18. package/dist/operations/services/railway-api.d.ts +33 -0
  19. package/dist/operations/services/railway-api.js +273 -0
  20. package/dist/operations/services/railway-deploy.d.ts +22 -0
  21. package/dist/operations/services/railway-deploy.js +81 -19
  22. package/dist/operations/services/release-candidate.d.ts +2 -0
  23. package/dist/operations/services/release-candidate.js +28 -0
  24. package/dist/operations/services/runtime-tools.js +1 -1
  25. package/dist/operations-registry.js +1 -0
  26. package/dist/reconcile/bootstrap-systems.js +1 -1
  27. package/dist/reconcile/builtin-adapters.js +5 -9
  28. package/dist/reconcile/contracts.d.ts +1 -1
  29. package/dist/reconcile/desired-state.js +9 -17
  30. package/dist/reconcile/state.js +4 -4
  31. package/dist/reconcile/units.js +4 -8
  32. package/dist/sdk-types.d.ts +566 -3
  33. package/dist/sdk.d.ts +12 -1
  34. package/dist/sdk.js +44 -0
  35. package/dist/stores/operational-store.d.ts +12 -1
  36. package/dist/stores/operational-store.js +283 -5
  37. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +5 -24
  38. package/dist/types/agents.d.ts +27 -0
  39. package/dist/workflow/operations.d.ts +94 -2
  40. package/dist/workflow/operations.js +90 -32
  41. package/dist/workflow-state.js +3 -5
  42. package/dist/workflow.d.ts +8 -1
  43. package/dist/workflow.js +6 -0
  44. package/package.json +5 -1
@@ -244,6 +244,13 @@ export declare function deployProjectPlatform(options: ProjectPlatformActionOpti
244
244
  healthcheckPath: string | null;
245
245
  healthcheckTimeoutSeconds: number | null;
246
246
  runtimeMode: string | null;
247
+ volume: {
248
+ id: string;
249
+ name: string;
250
+ mountPath: any;
251
+ created: boolean;
252
+ updated: boolean;
253
+ } | null;
247
254
  } | null;
248
255
  } | undefined)[];
249
256
  }>;
@@ -737,6 +744,13 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
737
744
  healthcheckPath: string | null;
738
745
  healthcheckTimeoutSeconds: number | null;
739
746
  runtimeMode: string | null;
747
+ volume: {
748
+ id: string;
749
+ name: string;
750
+ mountPath: any;
751
+ created: boolean;
752
+ updated: boolean;
753
+ } | null;
740
754
  } | null;
741
755
  } | undefined)[];
742
756
  }>;
@@ -631,9 +631,10 @@ function probeR2(tenantRoot, siteConfig, state, target) {
631
631
  }
632
632
  }
633
633
  function probeScaleConfiguration(siteConfig, state) {
634
- const worker = state.services?.worker ?? {};
634
+ const worker = state.services?.workerRunner ?? state.services?.worker ?? {};
635
+ const workerConfig = siteConfig.services?.workerRunner ?? siteConfig.services?.worker ?? {};
635
636
  const scalerKind = String(process.env.TREESEED_WORKER_POOL_SCALER ?? "").trim();
636
- if (scalerKind !== "railway" && siteConfig.services?.worker?.provider !== "railway") {
637
+ if (scalerKind !== "railway" && workerConfig.provider !== "railway") {
637
638
  return {
638
639
  ok: true,
639
640
  skipped: true,
@@ -53,6 +53,20 @@ export type RailwayCustomDomainSummary = {
53
53
  verificationToken: string | null;
54
54
  dnsRecords: RailwayCustomDomainDnsRecord[];
55
55
  };
56
+ export type RailwayVolumeInstanceSummary = {
57
+ id: string;
58
+ serviceId: string | null;
59
+ environmentId: string | null;
60
+ mountPath: string | null;
61
+ sizeGb: number | null;
62
+ usedGb: number | null;
63
+ };
64
+ export type RailwayVolumeSummary = {
65
+ id: string;
66
+ name: string;
67
+ projectId: string | null;
68
+ instances: RailwayVolumeInstanceSummary[];
69
+ };
56
70
  export declare function isUsableRailwayToken(value: string | undefined | null): boolean;
57
71
  export declare function resolveRailwayApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
58
72
  export declare function resolveRailwayApiUrl(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
@@ -235,6 +249,25 @@ export declare function upsertRailwayVariables({ projectId, environmentId, servi
235
249
  env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
236
250
  fetchImpl?: typeof fetch;
237
251
  }): Promise<void>;
252
+ export declare function listRailwayVolumes({ projectId, env, fetchImpl, }: {
253
+ projectId: string;
254
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
255
+ fetchImpl?: typeof fetch;
256
+ }): Promise<RailwayVolumeSummary[]>;
257
+ export declare function ensureRailwayServiceVolume({ projectId, environmentId, serviceId, name, mountPath, env, fetchImpl, }: {
258
+ projectId: string;
259
+ environmentId: string;
260
+ serviceId: string;
261
+ name: string;
262
+ mountPath: string;
263
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
264
+ fetchImpl?: typeof fetch;
265
+ }): Promise<{
266
+ volume: RailwayVolumeSummary;
267
+ instance: RailwayVolumeInstanceSummary | null;
268
+ created: boolean;
269
+ updated: boolean;
270
+ }>;
238
271
  export declare function listRailwayCustomDomains({ projectId, environmentId, serviceId, env, fetchImpl, }: {
239
272
  projectId: string;
240
273
  environmentId: string;
@@ -191,6 +191,83 @@ function normalizeRailwayCustomDomain(node) {
191
191
  dnsRecords
192
192
  };
193
193
  }
194
+ function normalizeRailwayVolumeInstance(node) {
195
+ const id = railwayConnectionLabel(node.id);
196
+ if (!id) {
197
+ return null;
198
+ }
199
+ const sizeGb = normalizeRailwayNumber(node.sizeGb ?? node.sizeGB ?? node.size_gb ?? node.capacityGb ?? node.capacityGB);
200
+ const usedGb = normalizeRailwayNumber(node.usedGb ?? node.usedGB ?? node.used_gb ?? node.currentUsageGb ?? node.currentUsageGB);
201
+ return {
202
+ id,
203
+ serviceId: railwayConnectionLabel(node.serviceId) || railwayConnectionLabel(node.service?.id) || null,
204
+ environmentId: railwayConnectionLabel(node.environmentId) || railwayConnectionLabel(node.environment?.id) || null,
205
+ mountPath: railwayConnectionLabel(node.mountPath) || railwayConnectionLabel(node.mount_path) || null,
206
+ sizeGb,
207
+ usedGb
208
+ };
209
+ }
210
+ function normalizeVolumeInstances(value) {
211
+ const direct = Array.isArray(value) ? value : null;
212
+ if (direct) {
213
+ return direct.map((entry) => entry && typeof entry === "object" ? normalizeRailwayVolumeInstance(entry) : null).filter(Boolean);
214
+ }
215
+ return normalizeConnectionNodes(value, normalizeRailwayVolumeInstance);
216
+ }
217
+ function normalizeRailwayVolume(node) {
218
+ const id = railwayConnectionLabel(node.id);
219
+ if (!id) {
220
+ return null;
221
+ }
222
+ return {
223
+ id,
224
+ name: railwayConnectionLabel(node.name),
225
+ projectId: railwayConnectionLabel(node.projectId) || null,
226
+ instances: [
227
+ ...normalizeVolumeInstances(node.instances),
228
+ ...normalizeVolumeInstances(node.volumeInstances),
229
+ ...normalizeVolumeInstances(node.volume_instances)
230
+ ]
231
+ };
232
+ }
233
+ function collectRailwayVolumes(value, seen = /* @__PURE__ */ new Set()) {
234
+ const volumes = [];
235
+ const visit = (entry) => {
236
+ if (!entry || typeof entry !== "object") {
237
+ return;
238
+ }
239
+ if (seen.has(entry)) {
240
+ return;
241
+ }
242
+ seen.add(entry);
243
+ if (Array.isArray(entry)) {
244
+ for (const item of entry) {
245
+ visit(item);
246
+ }
247
+ return;
248
+ }
249
+ const record = entry;
250
+ const volume = normalizeRailwayVolume(record);
251
+ if (volume && (record.volumeInstances !== void 0 || record.instances !== void 0 || record.projectId !== void 0 || record.name !== void 0)) {
252
+ volumes.push(volume);
253
+ }
254
+ for (const child of Object.values(record)) {
255
+ visit(child);
256
+ }
257
+ };
258
+ visit(value);
259
+ const byId = /* @__PURE__ */ new Map();
260
+ for (const volume of volumes) {
261
+ const existing = byId.get(volume.id);
262
+ byId.set(volume.id, existing ? {
263
+ ...existing,
264
+ name: existing.name || volume.name,
265
+ projectId: existing.projectId || volume.projectId,
266
+ instances: [...existing.instances, ...volume.instances]
267
+ } : volume);
268
+ }
269
+ return [...byId.values()];
270
+ }
194
271
  async function railwayGraphqlRequest({
195
272
  query,
196
273
  variables,
@@ -811,6 +888,200 @@ mutation TreeseedRailwayVariableCollectionUpsert($input: VariableCollectionUpser
811
888
  fetchImpl
812
889
  });
813
890
  }
891
+ async function listRailwayVolumes({
892
+ projectId,
893
+ env = process.env,
894
+ fetchImpl = fetch
895
+ }) {
896
+ const query = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_LIST_QUERY") || `
897
+ query TreeseedRailwayVolumeList($projectId: String!) {
898
+ project(id: $projectId) {
899
+ id
900
+ volumes {
901
+ edges {
902
+ node {
903
+ id
904
+ name
905
+ projectId
906
+ volumeInstances {
907
+ edges {
908
+ node {
909
+ id
910
+ serviceId
911
+ environmentId
912
+ mountPath
913
+ }
914
+ }
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ }
921
+ `.trim();
922
+ const payload = await railwayGraphqlRequest({
923
+ query,
924
+ variables: { projectId },
925
+ env,
926
+ fetchImpl
927
+ });
928
+ return collectRailwayVolumes(payload.data);
929
+ }
930
+ async function createRailwayVolume({
931
+ projectId,
932
+ environmentId,
933
+ serviceId,
934
+ name,
935
+ mountPath,
936
+ env = process.env,
937
+ fetchImpl = fetch
938
+ }) {
939
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_CREATE_MUTATION") || `
940
+ mutation TreeseedRailwayVolumeCreate($input: VolumeCreateInput!) {
941
+ volumeCreate(input: $input) {
942
+ id
943
+ name
944
+ projectId
945
+ volumeInstances {
946
+ edges {
947
+ node {
948
+ id
949
+ serviceId
950
+ environmentId
951
+ mountPath
952
+ }
953
+ }
954
+ }
955
+ }
956
+ }
957
+ `.trim();
958
+ const payload = await railwayGraphqlRequest({
959
+ query: mutation,
960
+ variables: {
961
+ input: {
962
+ projectId,
963
+ environmentId,
964
+ serviceId,
965
+ name,
966
+ mountPath
967
+ }
968
+ },
969
+ env,
970
+ fetchImpl
971
+ });
972
+ const volume = collectRailwayVolumes(payload.data)[0] ?? null;
973
+ if (!volume) {
974
+ throw new Error(`Railway volume create did not return a usable volume for ${name}.`);
975
+ }
976
+ return volume;
977
+ }
978
+ async function updateRailwayVolumeName({
979
+ volumeId,
980
+ name,
981
+ env = process.env,
982
+ fetchImpl = fetch
983
+ }) {
984
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_UPDATE_MUTATION") || `
985
+ mutation TreeseedRailwayVolumeUpdate($id: String!, $input: VolumeUpdateInput!) {
986
+ volumeUpdate(id: $id, input: $input) {
987
+ id
988
+ name
989
+ projectId
990
+ volumeInstances {
991
+ edges {
992
+ node {
993
+ id
994
+ serviceId
995
+ environmentId
996
+ mountPath
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ `.trim();
1003
+ const payload = await railwayGraphqlRequest({
1004
+ query: mutation,
1005
+ variables: {
1006
+ id: volumeId,
1007
+ input: { name }
1008
+ },
1009
+ env,
1010
+ fetchImpl
1011
+ });
1012
+ return collectRailwayVolumes(payload.data)[0] ?? null;
1013
+ }
1014
+ async function updateRailwayVolumeInstanceMountPath({
1015
+ instanceId,
1016
+ mountPath,
1017
+ env = process.env,
1018
+ fetchImpl = fetch
1019
+ }) {
1020
+ const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_INSTANCE_UPDATE_MUTATION") || `
1021
+ mutation TreeseedRailwayVolumeInstanceUpdate($id: String!, $input: VolumeInstanceUpdateInput!) {
1022
+ volumeInstanceUpdate(id: $id, input: $input) {
1023
+ id
1024
+ serviceId
1025
+ environmentId
1026
+ mountPath
1027
+ }
1028
+ }
1029
+ `.trim();
1030
+ await railwayGraphqlRequest({
1031
+ query: mutation,
1032
+ variables: {
1033
+ id: instanceId,
1034
+ input: { mountPath }
1035
+ },
1036
+ env,
1037
+ fetchImpl
1038
+ });
1039
+ }
1040
+ async function ensureRailwayServiceVolume({
1041
+ projectId,
1042
+ environmentId,
1043
+ serviceId,
1044
+ name,
1045
+ mountPath,
1046
+ env = process.env,
1047
+ fetchImpl = fetch
1048
+ }) {
1049
+ if (!mountPath.startsWith("/")) {
1050
+ throw new Error(`Railway volume mount path must be absolute: ${mountPath}`);
1051
+ }
1052
+ const volumes = await listRailwayVolumes({ projectId, env, fetchImpl });
1053
+ let volume = volumes.find(
1054
+ (candidate) => candidate.instances.some((instance2) => instance2.serviceId === serviceId && instance2.environmentId === environmentId)
1055
+ ) ?? volumes.find((candidate) => candidate.name === name) ?? null;
1056
+ let created = false;
1057
+ let updated = false;
1058
+ if (!volume) {
1059
+ volume = await createRailwayVolume({
1060
+ projectId,
1061
+ environmentId,
1062
+ serviceId,
1063
+ name,
1064
+ mountPath,
1065
+ env,
1066
+ fetchImpl
1067
+ });
1068
+ created = true;
1069
+ }
1070
+ if (volume.name && volume.name !== name) {
1071
+ volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
1072
+ updated = true;
1073
+ }
1074
+ const instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
1075
+ if (instance && instance.mountPath !== mountPath) {
1076
+ await updateRailwayVolumeInstanceMountPath({ instanceId: instance.id, mountPath, env, fetchImpl });
1077
+ volume = {
1078
+ ...volume,
1079
+ instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
1080
+ };
1081
+ updated = true;
1082
+ }
1083
+ return { volume, instance, created, updated };
1084
+ }
814
1085
  async function listRailwayCustomDomains({
815
1086
  projectId,
816
1087
  environmentId,
@@ -863,6 +1134,7 @@ export {
863
1134
  ensureRailwayProject,
864
1135
  ensureRailwayService,
865
1136
  ensureRailwayServiceInstanceConfiguration,
1137
+ ensureRailwayServiceVolume,
866
1138
  getRailwayAuthProfile,
867
1139
  getRailwayProject,
868
1140
  getRailwayServiceInstance,
@@ -872,6 +1144,7 @@ export {
872
1144
  listRailwayProjects,
873
1145
  listRailwayServices,
874
1146
  listRailwayVariables,
1147
+ listRailwayVolumes,
875
1148
  normalizeRailwayEnvironmentName,
876
1149
  railwayGraphqlRequest,
877
1150
  resolveRailwayApiToken,
@@ -1,4 +1,7 @@
1
1
  import { type TreeseedBootstrapTaskPrefix, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
2
+ export declare function deriveRailwayWorkerRunnerServiceName(projectSlug: any, index?: number): string;
3
+ export declare function deriveRailwayWorkerRunnerVolumeName(serviceName: any): string;
4
+ export declare function railwayServiceRuntimeStartCommand(service: any): any;
2
5
  export declare function isUsableRailwayToken(value: any): boolean;
3
6
  export declare function resolveRailwayAuthToken(env?: NodeJS.ProcessEnv): string;
4
7
  export declare function buildRailwayCommandEnv(env?: NodeJS.ProcessEnv): {
@@ -55,6 +58,10 @@ export declare function configuredRailwayServices(tenantRoot: any, scope: any):
55
58
  runtimeMode: any;
56
59
  schedule: string[];
57
60
  hostingKind: string;
61
+ runnerPool: {
62
+ bootstrapIndex: number;
63
+ volumeMountPath: string;
64
+ } | null;
58
65
  } | null)[];
59
66
  export declare function configuredRailwayScheduledJobs(tenantRoot: any, scope: any, { phase }?: {
60
67
  phase?: string | undefined;
@@ -95,6 +102,10 @@ export declare function validateRailwayServiceConfiguration(tenantRoot: any, sco
95
102
  runtimeMode: any;
96
103
  schedule: string[];
97
104
  hostingKind: string;
105
+ runnerPool: {
106
+ bootstrapIndex: number;
107
+ volumeMountPath: string;
108
+ } | null;
98
109
  } | null)[];
99
110
  schedules: {
100
111
  service: string;
@@ -134,6 +145,10 @@ export declare function validateRailwayDeployPrerequisites(tenantRoot: any, scop
134
145
  runtimeMode: any;
135
146
  schedule: string[];
136
147
  hostingKind: string;
148
+ runnerPool: {
149
+ bootstrapIndex: number;
150
+ volumeMountPath: string;
151
+ } | null;
137
152
  } | null)[];
138
153
  schedules: {
139
154
  service: string;
@@ -296,5 +311,12 @@ export declare function deployRailwayService(tenantRoot: any, service: any, { dr
296
311
  healthcheckPath: string | null;
297
312
  healthcheckTimeoutSeconds: number | null;
298
313
  runtimeMode: string | null;
314
+ volume: {
315
+ id: string;
316
+ name: string;
317
+ mountPath: any;
318
+ created: boolean;
319
+ updated: boolean;
320
+ } | null;
299
321
  } | null;
300
322
  }>;
@@ -10,6 +10,7 @@ import {
10
10
  ensureRailwayProject,
11
11
  ensureRailwayService,
12
12
  ensureRailwayServiceInstanceConfiguration,
13
+ ensureRailwayServiceVolume,
13
14
  listRailwayEnvironments,
14
15
  listRailwayProjects,
15
16
  listRailwayServices,
@@ -18,7 +19,8 @@ import {
18
19
  resolveRailwayApiToken,
19
20
  resolveRailwayApiUrl,
20
21
  resolveRailwayWorkspace,
21
- resolveRailwayWorkspaceContext
22
+ resolveRailwayWorkspaceContext,
23
+ upsertRailwayVariables
22
24
  } from "./railway-api.js";
23
25
  function normalizeScope(scope) {
24
26
  return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
@@ -26,13 +28,29 @@ function normalizeScope(scope) {
26
28
  function resolveRailwayEnvironmentForScope(scope, configuredEnvironment) {
27
29
  return normalizeRailwayEnvironmentName(configuredEnvironment || normalizeScope(scope));
28
30
  }
29
- const RAILWAY_SERVICE_KEYS = ["api", "manager", "worker", "workdayStart", "workdayReport"];
30
- const HOSTED_PROJECT_SERVICE_KEYS = ["api", "manager", "worker"];
31
+ const RAILWAY_SERVICE_KEYS = ["api", "workdayManager", "workerRunner"];
32
+ const HOSTED_PROJECT_SERVICE_KEYS = ["api", "workdayManager", "workerRunner"];
33
+ const WORKER_RUNNER_BOOTSTRAP_INDEX = 1;
34
+ const WORKER_RUNNER_VOLUME_MOUNT_PATH = "/data";
31
35
  function shouldManageRailwaySchedules(scope, phase = "deploy") {
32
- return phase === "deploy" && normalizeRailwayEnvironmentName(scope) === "production";
36
+ const environment = normalizeRailwayEnvironmentName(scope);
37
+ return phase === "deploy" && (environment === "staging" || environment === "production");
33
38
  }
34
39
  function railwayServiceNameSuffix(serviceKey) {
35
- return serviceKey === "workdayStart" ? "workday-start" : serviceKey === "workdayReport" ? "workday-report" : serviceKey;
40
+ return serviceKey === "workdayManager" ? "workday-manager" : serviceKey === "workerRunner" ? "worker-runner" : serviceKey;
41
+ }
42
+ function deriveRailwayWorkerRunnerServiceName(projectSlug, index = WORKER_RUNNER_BOOTSTRAP_INDEX) {
43
+ const normalizedIndex = Math.max(1, Number.parseInt(String(index), 10) || WORKER_RUNNER_BOOTSTRAP_INDEX);
44
+ return `${projectSlug}-worker-runner-${String(normalizedIndex).padStart(2, "0")}`;
45
+ }
46
+ function deriveRailwayWorkerRunnerVolumeName(serviceName) {
47
+ return `${serviceName}-data`;
48
+ }
49
+ function railwayServiceRuntimeStartCommand(service) {
50
+ if (service.key === "workdayManager" && Array.isArray(service.schedule) && service.schedule.length > 0) {
51
+ return `node -e "console.log('workday-manager scheduled-only service idle')"`;
52
+ }
53
+ return service.startCommand;
36
54
  }
37
55
  function normalizeScheduleExpressions(value) {
38
56
  if (typeof value === "string" && value.trim()) {
@@ -455,7 +473,7 @@ function configuredRailwayServices(tenantRoot, scope) {
455
473
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
456
474
  return null;
457
475
  }
458
- const defaultRootDir = ["api", "manager", "worker", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
476
+ const defaultRootDir = ["api", "workdayManager", "workerRunner"].includes(serviceKey) ? "." : "packages/core";
459
477
  const serviceRoot = resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir);
460
478
  const railwayEnvironment = resolveRailwayEnvironmentForScope(
461
479
  normalizedScope,
@@ -468,7 +486,7 @@ function configuredRailwayServices(tenantRoot, scope) {
468
486
  projectId: service.railway?.projectId ?? null,
469
487
  projectName: service.railway?.projectName ?? identity.deploymentKey,
470
488
  serviceId: service.railway?.serviceId ?? null,
471
- serviceName: service.railway?.serviceName ?? `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`,
489
+ serviceName: service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`),
472
490
  rootDir: serviceRoot,
473
491
  publicBaseUrl,
474
492
  railwayEnvironment,
@@ -480,7 +498,11 @@ function configuredRailwayServices(tenantRoot, scope) {
480
498
  restartPolicy: service.railway?.restartPolicy ?? null,
481
499
  runtimeMode: service.railway?.runtimeMode ?? null,
482
500
  schedule: normalizeScheduleExpressions(service.railway?.schedule),
483
- hostingKind
501
+ hostingKind,
502
+ runnerPool: serviceKey === "workerRunner" ? {
503
+ bootstrapIndex: WORKER_RUNNER_BOOTSTRAP_INDEX,
504
+ volumeMountPath: WORKER_RUNNER_VOLUME_MOUNT_PATH
505
+ } : null
484
506
  };
485
507
  }).filter(Boolean);
486
508
  }
@@ -879,7 +901,8 @@ async function resolveRailwayDeployProjectContext(service, { env = process.env }
879
901
  }
880
902
  async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, service, { env = process.env } = {}) {
881
903
  const wantsInstanceConfig = service.buildCommand || service.startCommand || service.rootDir || service.healthcheckPath || service.healthcheckTimeoutSeconds !== null || service.healthcheckTimeoutSeconds !== void 0 || service.healthcheckIntervalSeconds !== null || service.healthcheckIntervalSeconds !== void 0 || service.restartPolicy || service.runtimeMode;
882
- if (!wantsInstanceConfig) {
904
+ const wantsRunnerVolume = service.key === "workerRunner";
905
+ if (!wantsInstanceConfig && !wantsRunnerVolume) {
883
906
  return null;
884
907
  }
885
908
  const workspace = await resolveRailwayWorkspaceContext({ env });
@@ -922,11 +945,11 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
922
945
  env
923
946
  }).then((result) => result.service);
924
947
  }
925
- return await ensureRailwayServiceInstanceConfiguration({
948
+ const runtimeConfiguration = wantsInstanceConfig ? await ensureRailwayServiceInstanceConfiguration({
926
949
  serviceId: railwayService.id,
927
950
  environmentId: environment.id,
928
951
  buildCommand: service.buildCommand,
929
- startCommand: service.startCommand,
952
+ startCommand: railwayServiceRuntimeStartCommand(service),
930
953
  rootDirectory: relativeRailwayRootDir(tenantRoot, service.rootDir),
931
954
  healthcheckPath: service.healthcheckPath,
932
955
  healthcheckTimeoutSeconds: service.healthcheckTimeoutSeconds,
@@ -934,7 +957,42 @@ async function syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, ser
934
957
  restartPolicy: service.restartPolicy,
935
958
  runtimeMode: service.runtimeMode,
936
959
  env
937
- });
960
+ }) : null;
961
+ const volumeMountPath = service.runnerPool?.volumeMountPath ?? WORKER_RUNNER_VOLUME_MOUNT_PATH;
962
+ const volumeConfiguration = wantsRunnerVolume ? await ensureRailwayServiceVolume({
963
+ projectId: project.id,
964
+ environmentId: environment.id,
965
+ serviceId: railwayService.id,
966
+ name: deriveRailwayWorkerRunnerVolumeName(railwayService.name),
967
+ mountPath: volumeMountPath,
968
+ env
969
+ }) : null;
970
+ if (wantsRunnerVolume) {
971
+ await upsertRailwayVariables({
972
+ projectId: project.id,
973
+ environmentId: environment.id,
974
+ serviceId: railwayService.id,
975
+ variables: {
976
+ TREESEED_RUNNER_SERVICE_NAME: railwayService.name,
977
+ TREESEED_RUNNER_VOLUME_ROOT: volumeMountPath,
978
+ TREESEED_RUNNER_VOLUME_NAME: volumeConfiguration?.volume.name ?? deriveRailwayWorkerRunnerVolumeName(railwayService.name),
979
+ TREESEED_WORKER_IDLE_EXIT_MS: configuredEnvValue(env, "TREESEED_WORKER_IDLE_EXIT_MS") || "60000",
980
+ ...volumeConfiguration?.volume.id ? { TREESEED_RUNNER_VOLUME_ID: volumeConfiguration.volume.id } : {}
981
+ },
982
+ env
983
+ });
984
+ }
985
+ return {
986
+ instance: runtimeConfiguration?.instance ?? null,
987
+ updated: Boolean(runtimeConfiguration?.updated || volumeConfiguration?.updated || volumeConfiguration?.created),
988
+ volume: volumeConfiguration ? {
989
+ id: volumeConfiguration.volume.id,
990
+ name: volumeConfiguration.volume.name,
991
+ mountPath: volumeConfiguration.instance?.mountPath ?? volumeMountPath,
992
+ created: volumeConfiguration.created,
993
+ updated: volumeConfiguration.updated
994
+ } : null
995
+ };
938
996
  }
939
997
  async function deployRailwayService(tenantRoot, service, {
940
998
  dryRun = false,
@@ -954,13 +1012,16 @@ async function deployRailwayService(tenantRoot, service, {
954
1012
  }
955
1013
  const deployService = await resolveRailwayDeployProjectContext(service, { env });
956
1014
  const plan = planRailwayServiceDeploy(deployService, { env });
1015
+ const commandEnv = buildRailwayCommandEnv({ ...process.env, ...env });
957
1016
  const taskPrefix = prefix ?? {
958
1017
  scope: normalizeScope(deployService.scope ?? deployService.railwayEnvironment ?? "railway"),
959
1018
  system: deployService.key === "api" ? "api" : "agents",
960
1019
  task: `${deployService.key}-railway-deploy`,
961
1020
  stage: "deploy"
962
1021
  };
963
- const commandEnv = buildRailwayCommandEnv({ ...process.env, ...env });
1022
+ const runtimeConfiguration = await syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, deployService, {
1023
+ env: commandEnv
1024
+ });
964
1025
  if (deployService.buildCommand) {
965
1026
  const buildResult = await runPrefixedCommand("bash", ["-lc", deployService.buildCommand], {
966
1027
  cwd: deployService.rootDir,
@@ -996,9 +1057,6 @@ async function deployRailwayService(tenantRoot, service, {
996
1057
  if (lastFailure) {
997
1058
  throw new Error(lastFailure.stderr?.trim() || lastFailure.stdout?.trim() || `railway ${plan.args.join(" ")} failed`);
998
1059
  }
999
- const runtimeConfiguration = await syncRailwayServiceRuntimeConfigurationAfterDeploy(tenantRoot, deployService, {
1000
- env: commandEnv
1001
- });
1002
1060
  return {
1003
1061
  service: deployService.key,
1004
1062
  status: "deployed",
@@ -1007,9 +1065,10 @@ async function deployRailwayService(tenantRoot, service, {
1007
1065
  publicBaseUrl: deployService.publicBaseUrl,
1008
1066
  runtimeConfiguration: runtimeConfiguration ? {
1009
1067
  updated: runtimeConfiguration.updated,
1010
- healthcheckPath: runtimeConfiguration.instance.healthcheckPath,
1011
- healthcheckTimeoutSeconds: runtimeConfiguration.instance.healthcheckTimeoutSeconds,
1012
- runtimeMode: runtimeConfiguration.instance.runtimeMode
1068
+ healthcheckPath: runtimeConfiguration.instance?.healthcheckPath ?? null,
1069
+ healthcheckTimeoutSeconds: runtimeConfiguration.instance?.healthcheckTimeoutSeconds ?? null,
1070
+ runtimeMode: runtimeConfiguration.instance?.runtimeMode ?? null,
1071
+ volume: runtimeConfiguration.volume ?? null
1013
1072
  } : null
1014
1073
  };
1015
1074
  }
@@ -1018,6 +1077,8 @@ export {
1018
1077
  configuredRailwayScheduledJobs,
1019
1078
  configuredRailwayServices,
1020
1079
  deployRailwayService,
1080
+ deriveRailwayWorkerRunnerServiceName,
1081
+ deriveRailwayWorkerRunnerVolumeName,
1021
1082
  ensureRailwayEnvironmentExists,
1022
1083
  ensureRailwayProjectContext,
1023
1084
  ensureRailwayProjectExists,
@@ -1026,6 +1087,7 @@ export {
1026
1087
  isRailwayTransientFailure,
1027
1088
  isUsableRailwayToken,
1028
1089
  planRailwayServiceDeploy,
1090
+ railwayServiceRuntimeStartCommand,
1029
1091
  resolveRailwayAuthToken,
1030
1092
  resolveRailwayDeploymentProfile,
1031
1093
  runRailway,
@@ -8,6 +8,7 @@ export type ReleaseCandidateFailure = {
8
8
  };
9
9
  export type ReleaseCandidateFingerprint = {
10
10
  key: string;
11
+ policyVersion: string;
11
12
  rootSha: string | null;
12
13
  packageShas: Record<string, string | null>;
13
14
  plannedVersions: Record<string, string>;
@@ -36,4 +37,5 @@ export type ReleaseCandidateInput = {
36
37
  export declare function buildReleaseCandidateFingerprint(input: ReleaseCandidateInput): ReleaseCandidateFingerprint;
37
38
  export declare function readCachedReleaseCandidateReport(root: string, key: string): ReleaseCandidateReport | null;
38
39
  export declare function writeReleaseCandidateReport(root: string, report: ReleaseCandidateReport): ReleaseCandidateReport;
40
+ export declare function collectReleaseCandidateOutputFailures(line: string): string[];
39
41
  export declare function runReleaseCandidateGate(input: ReleaseCandidateInput): Promise<ReleaseCandidateReport>;