@treeseed/sdk 0.4.8 → 0.4.10

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/control-plane-client.d.ts +45 -0
  3. package/dist/control-plane-client.js +229 -0
  4. package/dist/control-plane.d.ts +94 -0
  5. package/dist/control-plane.js +125 -0
  6. package/dist/d1-store.d.ts +56 -1
  7. package/dist/d1-store.js +132 -0
  8. package/dist/dispatch.d.ts +4 -0
  9. package/dist/dispatch.js +180 -0
  10. package/dist/index.d.ts +14 -2
  11. package/dist/index.js +94 -4
  12. package/dist/operations/services/config-runtime.d.ts +10 -0
  13. package/dist/operations/services/config-runtime.js +62 -4
  14. package/dist/operations/services/deploy.d.ts +95 -3
  15. package/dist/operations/services/deploy.js +351 -10
  16. package/dist/operations/services/github-automation.d.ts +37 -1
  17. package/dist/operations/services/github-automation.js +71 -14
  18. package/dist/operations/services/project-platform.d.ts +835 -0
  19. package/dist/operations/services/project-platform.js +782 -0
  20. package/dist/operations/services/railway-deploy.d.ts +113 -18
  21. package/dist/operations/services/railway-deploy.js +357 -8
  22. package/dist/operations/services/runtime-tools.d.ts +25 -1
  23. package/dist/operations/services/runtime-tools.js +66 -5
  24. package/dist/operations/services/template-registry.d.ts +1 -1
  25. package/dist/operations/services/template-registry.js +17 -3
  26. package/dist/platform/books-data.d.ts +3 -4
  27. package/dist/platform/books-data.js +30 -4
  28. package/dist/platform/contracts.d.ts +56 -4
  29. package/dist/platform/deploy-config.js +109 -4
  30. package/dist/platform/deploy-runtime.d.ts +2 -0
  31. package/dist/platform/deploy-runtime.js +9 -1
  32. package/dist/platform/env.yaml +677 -0
  33. package/dist/platform/environment.js +57 -2
  34. package/dist/platform/plugin.d.ts +8 -0
  35. package/dist/platform/plugins/constants.d.ts +2 -0
  36. package/dist/platform/plugins/constants.js +2 -0
  37. package/dist/platform/plugins/runtime.d.ts +2 -0
  38. package/dist/platform/plugins/runtime.js +9 -1
  39. package/dist/platform/plugins.d.ts +1 -1
  40. package/dist/platform/plugins.js +4 -0
  41. package/dist/platform/published-content-pipeline.d.ts +84 -0
  42. package/dist/platform/published-content-pipeline.js +543 -0
  43. package/dist/platform/published-content.d.ts +223 -0
  44. package/dist/platform/published-content.js +588 -0
  45. package/dist/platform/tenant/runtime-config.d.ts +1 -1
  46. package/dist/platform/tenant/runtime-config.js +34 -1
  47. package/dist/platform/tenant-config.d.ts +2 -1
  48. package/dist/platform/tenant-config.js +17 -1
  49. package/dist/platform/utils/site-config-schema.js +104 -0
  50. package/dist/plugin-default.d.ts +2 -0
  51. package/dist/plugin-default.js +2 -0
  52. package/dist/remote.d.ts +65 -9
  53. package/dist/remote.js +104 -28
  54. package/dist/scripts/check-build-warnings.js +50 -0
  55. package/dist/scripts/config-treeseed.js +7 -0
  56. package/dist/scripts/tenant-workflow-action.js +71 -0
  57. package/dist/sdk-dispatch.d.ts +12 -0
  58. package/dist/sdk-dispatch.js +142 -0
  59. package/dist/sdk-types.d.ts +579 -7
  60. package/dist/sdk-types.js +53 -1
  61. package/dist/sdk.d.ts +17 -1
  62. package/dist/sdk.js +109 -0
  63. package/dist/stores/operational-store.d.ts +22 -2
  64. package/dist/stores/operational-store.js +235 -0
  65. package/dist/template-catalog.js +8 -1
  66. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +20 -0
  67. package/dist/types/cloudflare.d.ts +23 -0
  68. package/dist/workflow/operations.d.ts +12 -3
  69. package/dist/workflow/policy.d.ts +1 -1
  70. package/dist/workflow-state.js +2 -1
  71. package/package.json +7 -2
  72. package/templates/github/deploy.workflow.yml +442 -0
  73. package/templates/github/hosted-project.workflow.yml +77 -0
@@ -10,33 +10,128 @@ export declare function configuredRailwayServices(tenantRoot: any, scope: any):
10
10
  railwayEnvironment: string;
11
11
  buildCommand: string | null;
12
12
  startCommand: string | null;
13
+ schedule: string[];
14
+ hostingKind: string;
13
15
  } | null)[];
14
- export declare function validateRailwayServiceConfiguration(tenantRoot: any, scope: any): ({
15
- key: string;
16
- scope: string;
16
+ export declare function configuredRailwayScheduledJobs(tenantRoot: any, scope: any): {
17
+ service: string;
17
18
  projectId: string | null;
18
19
  projectName: string | null;
19
20
  serviceId: string | null;
20
21
  serviceName: string | null;
21
- rootDir: string;
22
- publicBaseUrl: string | null;
23
- railwayEnvironment: string;
24
- buildCommand: string | null;
25
- startCommand: string | null;
26
- } | null)[];
27
- export declare function validateRailwayDeployPrerequisites(tenantRoot: any, scope: any): ({
28
- key: string;
29
- scope: string;
22
+ environment: string;
23
+ environmentId: string | null;
24
+ expression: string;
25
+ command: string | null;
26
+ enabled: boolean;
27
+ logicalName: string;
28
+ }[];
29
+ export declare function resolveRailwayDeploymentProfile(tenantRoot: any): {
30
+ hostingKind: string;
31
+ managedTopology: string[];
32
+ };
33
+ export declare function validateRailwayServiceConfiguration(tenantRoot: any, scope: any): {
34
+ services: ({
35
+ key: string;
36
+ scope: string;
37
+ projectId: string | null;
38
+ projectName: string | null;
39
+ serviceId: string | null;
40
+ serviceName: string | null;
41
+ rootDir: string;
42
+ publicBaseUrl: string | null;
43
+ railwayEnvironment: string;
44
+ buildCommand: string | null;
45
+ startCommand: string | null;
46
+ schedule: string[];
47
+ hostingKind: string;
48
+ } | null)[];
49
+ schedules: {
50
+ service: string;
51
+ projectId: string | null;
52
+ projectName: string | null;
53
+ serviceId: string | null;
54
+ serviceName: string | null;
55
+ environment: string;
56
+ environmentId: string | null;
57
+ expression: string;
58
+ command: string | null;
59
+ enabled: boolean;
60
+ logicalName: string;
61
+ }[];
62
+ hostingKind: string;
63
+ managedTopology: string[];
64
+ };
65
+ export declare function validateRailwayDeployPrerequisites(tenantRoot: any, scope: any): {
66
+ services: ({
67
+ key: string;
68
+ scope: string;
69
+ projectId: string | null;
70
+ projectName: string | null;
71
+ serviceId: string | null;
72
+ serviceName: string | null;
73
+ rootDir: string;
74
+ publicBaseUrl: string | null;
75
+ railwayEnvironment: string;
76
+ buildCommand: string | null;
77
+ startCommand: string | null;
78
+ schedule: string[];
79
+ hostingKind: string;
80
+ } | null)[];
81
+ schedules: {
82
+ service: string;
83
+ projectId: string | null;
84
+ projectName: string | null;
85
+ serviceId: string | null;
86
+ serviceName: string | null;
87
+ environment: string;
88
+ environmentId: string | null;
89
+ expression: string;
90
+ command: string | null;
91
+ enabled: boolean;
92
+ logicalName: string;
93
+ }[];
94
+ hostingKind: string;
95
+ managedTopology: string[];
96
+ };
97
+ export declare function ensureRailwayScheduledJobs(tenantRoot: any, scope: any, { dryRun, fetchImpl, apiToken, apiUrl }?: {
98
+ dryRun?: boolean | undefined;
99
+ fetchImpl?: typeof fetch | undefined;
100
+ }): Promise<{
101
+ id: any;
102
+ status: string;
103
+ enabled: any;
104
+ command: any;
105
+ service: string;
30
106
  projectId: string | null;
31
107
  projectName: string | null;
32
108
  serviceId: string | null;
33
109
  serviceName: string | null;
34
- rootDir: string;
35
- publicBaseUrl: string | null;
36
- railwayEnvironment: string;
37
- buildCommand: string | null;
38
- startCommand: string | null;
39
- } | null)[];
110
+ environment: string;
111
+ environmentId: string | null;
112
+ expression: string;
113
+ logicalName: string;
114
+ }[]>;
115
+ export declare function verifyRailwayScheduledJobs(tenantRoot: any, scope: any, { fetchImpl, apiToken, apiUrl }?: {
116
+ fetchImpl?: typeof fetch | undefined;
117
+ }): Promise<{
118
+ ok: boolean;
119
+ checks: {
120
+ id: any;
121
+ ok: boolean;
122
+ service: string;
123
+ projectId: string | null;
124
+ projectName: string | null;
125
+ serviceId: string | null;
126
+ serviceName: string | null;
127
+ environment: string;
128
+ environmentId: string | null;
129
+ expression: string;
130
+ command: string | null;
131
+ enabled: boolean;
132
+ logicalName: string;
133
+ }[];
134
+ }>;
40
135
  export declare function planRailwayServiceDeploy(service: any): {
41
136
  command: string;
42
137
  args: any[];
@@ -2,10 +2,168 @@ import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { loadCliDeployConfig } from "./runtime-tools.js";
5
+ const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
5
6
  function normalizeScope(scope) {
6
7
  return scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
7
8
  }
8
- const RAILWAY_SERVICE_KEYS = ["api", "agents", "manager", "worker", "workdayStart", "workdayReport"];
9
+ const RAILWAY_SERVICE_KEYS = ["api", "agents", "manager", "worker", "runner", "workdayStart", "workdayReport"];
10
+ const HOSTED_PROJECT_SERVICE_KEYS = ["api", "manager", "worker", "agents"];
11
+ function normalizeScheduleExpressions(value) {
12
+ if (typeof value === "string" && value.trim()) {
13
+ return [value.trim()];
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
17
+ }
18
+ return [];
19
+ }
20
+ function envValue(name) {
21
+ const value = process.env[name];
22
+ return typeof value === "string" && value.trim() ? value.trim() : "";
23
+ }
24
+ function normalizeRailwaySchedule(schedule) {
25
+ if (!schedule || typeof schedule !== "object") {
26
+ return null;
27
+ }
28
+ const expression = String(
29
+ schedule.expression ?? schedule.schedule ?? schedule.cron ?? schedule.cronExpression ?? ""
30
+ ).trim();
31
+ if (!expression) {
32
+ return null;
33
+ }
34
+ return {
35
+ id: schedule.id ? String(schedule.id) : null,
36
+ name: schedule.name ? String(schedule.name) : null,
37
+ expression,
38
+ command: String(schedule.command ?? schedule.startCommand ?? "").trim() || null,
39
+ enabled: schedule.enabled !== false,
40
+ serviceId: schedule.serviceId ? String(schedule.serviceId) : schedule.service?.id ? String(schedule.service.id) : null,
41
+ serviceName: schedule.serviceName ? String(schedule.serviceName) : schedule.service?.name ? String(schedule.service.name) : null,
42
+ environmentId: schedule.environmentId ? String(schedule.environmentId) : schedule.environment?.id ? String(schedule.environment.id) : null,
43
+ environmentName: schedule.environmentName ? String(schedule.environmentName) : schedule.environment?.name ? String(schedule.environment.name) : null
44
+ };
45
+ }
46
+ function collectRailwaySchedules(value, seen = /* @__PURE__ */ new Set()) {
47
+ const matches = [];
48
+ const visit = (entry) => {
49
+ if (!entry || typeof entry !== "object") {
50
+ return;
51
+ }
52
+ if (seen.has(entry)) {
53
+ return;
54
+ }
55
+ seen.add(entry);
56
+ if (Array.isArray(entry)) {
57
+ for (const item of entry) {
58
+ visit(item);
59
+ }
60
+ return;
61
+ }
62
+ const normalized = normalizeRailwaySchedule(entry);
63
+ if (normalized) {
64
+ matches.push(normalized);
65
+ }
66
+ for (const child of Object.values(entry)) {
67
+ visit(child);
68
+ }
69
+ };
70
+ visit(value);
71
+ return matches;
72
+ }
73
+ async function railwayGraphqlRequest({
74
+ query,
75
+ variables,
76
+ apiToken = envValue("RAILWAY_API_TOKEN") || envValue("RAILWAY_TOKEN"),
77
+ apiUrl = envValue("TREESEED_RAILWAY_API_URL") || DEFAULT_RAILWAY_API_URL,
78
+ fetchImpl = fetch
79
+ }) {
80
+ if (!apiToken) {
81
+ throw new Error("Configure RAILWAY_API_TOKEN before invoking Railway GraphQL APIs.");
82
+ }
83
+ const response = await fetchImpl(apiUrl, {
84
+ method: "POST",
85
+ headers: {
86
+ authorization: `Bearer ${apiToken}`,
87
+ "content-type": "application/json"
88
+ },
89
+ body: JSON.stringify({ query, variables })
90
+ });
91
+ const payload = await response.json().catch(() => ({}));
92
+ if (!response.ok || Array.isArray(payload?.errors) && payload.errors.length > 0) {
93
+ throw new Error(
94
+ payload?.errors?.[0]?.message ?? `Railway GraphQL request failed with ${response.status}.`
95
+ );
96
+ }
97
+ return payload;
98
+ }
99
+ function defaultRailwayScheduleQueries() {
100
+ return {
101
+ listQuery: envValue("TREESEED_RAILWAY_SCHEDULE_LIST_QUERY") || `
102
+ query TreeseedScheduleList($serviceId: String!, $environmentId: String!, $projectId: String) {
103
+ service(id: $serviceId) {
104
+ id
105
+ name
106
+ cronTriggers {
107
+ edges {
108
+ node {
109
+ id
110
+ name
111
+ schedule
112
+ command
113
+ enabled
114
+ service { id name }
115
+ environment { id name }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ `.trim(),
122
+ createMutation: envValue("TREESEED_RAILWAY_SCHEDULE_CREATE_MUTATION") || `
123
+ mutation TreeseedScheduleCreate($serviceId: String!, $environmentId: String!, $name: String!, $schedule: String!, $command: String!, $enabled: Boolean!) {
124
+ cronTriggerCreate(
125
+ input: {
126
+ serviceId: $serviceId
127
+ environmentId: $environmentId
128
+ name: $name
129
+ schedule: $schedule
130
+ command: $command
131
+ enabled: $enabled
132
+ }
133
+ ) {
134
+ id
135
+ name
136
+ schedule
137
+ command
138
+ enabled
139
+ service { id name }
140
+ environment { id name }
141
+ }
142
+ }
143
+ `.trim(),
144
+ updateMutation: envValue("TREESEED_RAILWAY_SCHEDULE_UPDATE_MUTATION") || `
145
+ mutation TreeseedScheduleUpdate($id: String!, $name: String!, $schedule: String!, $command: String!, $enabled: Boolean!) {
146
+ cronTriggerUpdate(
147
+ id: $id
148
+ input: {
149
+ name: $name
150
+ schedule: $schedule
151
+ command: $command
152
+ enabled: $enabled
153
+ }
154
+ ) {
155
+ id
156
+ name
157
+ schedule
158
+ command
159
+ enabled
160
+ service { id name }
161
+ environment { id name }
162
+ }
163
+ }
164
+ `.trim()
165
+ };
166
+ }
9
167
  function runRailway(args, { cwd, capture = false, allowFailure = false } = {}) {
10
168
  const result = spawnSync("railway", args, {
11
169
  cwd,
@@ -21,12 +179,14 @@ function runRailway(args, { cwd, capture = false, allowFailure = false } = {}) {
21
179
  function configuredRailwayServices(tenantRoot, scope) {
22
180
  const deployConfig = loadCliDeployConfig(tenantRoot);
23
181
  const normalizedScope = normalizeScope(scope);
24
- return RAILWAY_SERVICE_KEYS.map((serviceKey) => {
182
+ const hostingKind = deployConfig.hosting?.kind ?? "self_hosted_project";
183
+ const serviceKeys = hostingKind === "hosted_project" ? HOSTED_PROJECT_SERVICE_KEYS : RAILWAY_SERVICE_KEYS;
184
+ return serviceKeys.map((serviceKey) => {
25
185
  const service = deployConfig.services?.[serviceKey];
26
186
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
27
187
  return null;
28
188
  }
29
- const defaultRootDir = ["api", "manager", "worker", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
189
+ const defaultRootDir = ["api", "manager", "worker", "runner", "workdayStart", "workdayReport"].includes(serviceKey) ? "." : "packages/core";
30
190
  const serviceRoot = resolve(tenantRoot, service.railway?.rootDir ?? service.rootDir ?? defaultRootDir);
31
191
  const railwayEnvironment = service.environments?.[normalizedScope]?.railwayEnvironment ?? normalizedScope;
32
192
  const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? null;
@@ -41,13 +201,49 @@ function configuredRailwayServices(tenantRoot, scope) {
41
201
  publicBaseUrl,
42
202
  railwayEnvironment,
43
203
  buildCommand: service.railway?.buildCommand ?? null,
44
- startCommand: service.railway?.startCommand ?? null
204
+ startCommand: service.railway?.startCommand ?? null,
205
+ schedule: normalizeScheduleExpressions(service.railway?.schedule),
206
+ hostingKind
45
207
  };
46
208
  }).filter(Boolean);
47
209
  }
210
+ function configuredRailwayScheduledJobs(tenantRoot, scope) {
211
+ return configuredRailwayServices(tenantRoot, scope).filter((service) => Array.isArray(service.schedule) && service.schedule.length > 0).flatMap(
212
+ (service) => service.schedule.map((expression, index) => ({
213
+ service: service.key,
214
+ projectId: service.projectId,
215
+ projectName: service.projectName,
216
+ serviceId: service.serviceId,
217
+ serviceName: service.serviceName,
218
+ environment: service.railwayEnvironment,
219
+ environmentId: envValue("TREESEED_RAILWAY_ENVIRONMENT_ID") || null,
220
+ expression,
221
+ command: service.startCommand,
222
+ enabled: true,
223
+ logicalName: `${service.key}:${index + 1}`
224
+ }))
225
+ );
226
+ }
227
+ function resolveRailwayDeploymentProfile(tenantRoot) {
228
+ const deployConfig = loadCliDeployConfig(tenantRoot);
229
+ const hostingKind = deployConfig.hosting?.kind ?? "self_hosted_project";
230
+ return {
231
+ hostingKind,
232
+ managedTopology: hostingKind === "hosted_project" ? [...HOSTED_PROJECT_SERVICE_KEYS] : [...RAILWAY_SERVICE_KEYS]
233
+ };
234
+ }
48
235
  function validateRailwayServiceConfiguration(tenantRoot, scope) {
49
236
  const services = configuredRailwayServices(tenantRoot, scope);
237
+ const { hostingKind, managedTopology } = resolveRailwayDeploymentProfile(tenantRoot);
50
238
  const issues = [];
239
+ const configuredKeys = new Set(services.map((service) => service.key));
240
+ if (hostingKind === "hosted_project") {
241
+ for (const serviceKey of HOSTED_PROJECT_SERVICE_KEYS) {
242
+ if (!configuredKeys.has(serviceKey)) {
243
+ issues.push(`${serviceKey}: hosted_project deployments require the ${serviceKey} Railway service to be configured.`);
244
+ }
245
+ }
246
+ }
51
247
  for (const service of services) {
52
248
  if (!service.serviceName && !service.serviceId) {
53
249
  issues.push(`${service.key}: set railway.serviceName or railway.serviceId in treeseed.site.yaml.`);
@@ -58,20 +254,169 @@ function validateRailwayServiceConfiguration(tenantRoot, scope) {
58
254
  if (!existsSync(service.rootDir)) {
59
255
  issues.push(`${service.key}: service root ${service.rootDir} does not exist.`);
60
256
  }
257
+ if (service.schedule?.length && !service.startCommand) {
258
+ issues.push(`${service.key}: scheduled Railway services require railway.startCommand in treeseed.site.yaml.`);
259
+ }
260
+ if (service.schedule?.length && !service.serviceId) {
261
+ issues.push(`${service.key}: scheduled Railway services require railway.serviceId for Railway API reconciliation.`);
262
+ }
263
+ if (service.schedule?.length && !envValue("TREESEED_RAILWAY_ENVIRONMENT_ID")) {
264
+ issues.push(`${service.key}: scheduled Railway services require TREESEED_RAILWAY_ENVIRONMENT_ID to be configured.`);
265
+ }
61
266
  }
62
267
  if (issues.length > 0) {
63
268
  throw new Error(`Railway service configuration is incomplete:
64
269
  - ${issues.join("\n- ")}`);
65
270
  }
66
- return services;
271
+ return {
272
+ services,
273
+ schedules: configuredRailwayScheduledJobs(tenantRoot, scope),
274
+ hostingKind,
275
+ managedTopology
276
+ };
67
277
  }
68
278
  function validateRailwayDeployPrerequisites(tenantRoot, scope) {
69
- const services = validateRailwayServiceConfiguration(tenantRoot, scope);
279
+ const validation = validateRailwayServiceConfiguration(tenantRoot, scope);
70
280
  const token = process.env.RAILWAY_API_TOKEN;
71
281
  if (typeof token !== "string" || token.trim().length === 0) {
72
282
  throw new Error("Configure RAILWAY_API_TOKEN before deploying Railway-managed services.");
73
283
  }
74
- return services;
284
+ return validation;
285
+ }
286
+ async function ensureRailwayScheduledJobs(tenantRoot, scope, { dryRun = false, fetchImpl = fetch, apiToken, apiUrl } = {}) {
287
+ const { schedules } = validateRailwayDeployPrerequisites(tenantRoot, scope);
288
+ const queries = defaultRailwayScheduleQueries();
289
+ const results = [];
290
+ for (const schedule of schedules) {
291
+ const variables = {
292
+ projectId: schedule.projectId,
293
+ serviceId: schedule.serviceId,
294
+ environmentId: schedule.environmentId
295
+ };
296
+ const listed = await railwayGraphqlRequest({
297
+ query: queries.listQuery,
298
+ variables,
299
+ apiToken,
300
+ apiUrl,
301
+ fetchImpl
302
+ });
303
+ const existing = collectRailwaySchedules(listed?.data).find(
304
+ (entry) => entry.id && entry.id === schedule.id || entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
305
+ );
306
+ const desired = {
307
+ name: schedule.logicalName,
308
+ schedule: schedule.expression,
309
+ command: schedule.command,
310
+ enabled: schedule.enabled !== false
311
+ };
312
+ const drifted = Boolean(
313
+ existing && (existing.expression !== desired.schedule || (existing.command ?? null) !== (desired.command ?? null) || existing.enabled !== desired.enabled)
314
+ );
315
+ if (dryRun) {
316
+ results.push({
317
+ ...schedule,
318
+ id: existing?.id ?? null,
319
+ status: existing ? drifted ? "planned_update" : "planned_noop" : "planned_create",
320
+ enabled: desired.enabled,
321
+ command: desired.command
322
+ });
323
+ continue;
324
+ }
325
+ if (!existing) {
326
+ const created = await railwayGraphqlRequest({
327
+ query: queries.createMutation,
328
+ variables: {
329
+ ...variables,
330
+ name: desired.name,
331
+ schedule: desired.schedule,
332
+ command: desired.command,
333
+ enabled: desired.enabled
334
+ },
335
+ apiToken,
336
+ apiUrl,
337
+ fetchImpl
338
+ });
339
+ const createdSchedule = collectRailwaySchedules(created?.data)[0];
340
+ if (!createdSchedule?.id) {
341
+ throw new Error(`Railway schedule create did not return an id for ${schedule.logicalName}.`);
342
+ }
343
+ results.push({
344
+ ...schedule,
345
+ id: createdSchedule.id,
346
+ status: "created",
347
+ enabled: createdSchedule.enabled,
348
+ command: createdSchedule.command ?? desired.command
349
+ });
350
+ continue;
351
+ }
352
+ if (drifted) {
353
+ const updated = await railwayGraphqlRequest({
354
+ query: queries.updateMutation,
355
+ variables: {
356
+ id: existing.id,
357
+ name: desired.name,
358
+ schedule: desired.schedule,
359
+ command: desired.command,
360
+ enabled: desired.enabled
361
+ },
362
+ apiToken,
363
+ apiUrl,
364
+ fetchImpl
365
+ });
366
+ const updatedSchedule = collectRailwaySchedules(updated?.data)[0];
367
+ if (!updatedSchedule?.id) {
368
+ throw new Error(`Railway schedule update did not return an id for ${schedule.logicalName}.`);
369
+ }
370
+ results.push({
371
+ ...schedule,
372
+ id: updatedSchedule.id,
373
+ status: "updated",
374
+ enabled: updatedSchedule.enabled,
375
+ command: updatedSchedule.command ?? desired.command
376
+ });
377
+ continue;
378
+ }
379
+ results.push({
380
+ ...schedule,
381
+ id: existing.id,
382
+ status: "noop",
383
+ enabled: existing.enabled,
384
+ command: existing.command ?? desired.command
385
+ });
386
+ }
387
+ return results;
388
+ }
389
+ async function verifyRailwayScheduledJobs(tenantRoot, scope, { fetchImpl = fetch, apiToken, apiUrl } = {}) {
390
+ const configured = configuredRailwayScheduledJobs(tenantRoot, scope);
391
+ const queries = defaultRailwayScheduleQueries();
392
+ const checks = [];
393
+ for (const schedule of configured) {
394
+ const listed = await railwayGraphqlRequest({
395
+ query: queries.listQuery,
396
+ variables: {
397
+ projectId: schedule.projectId,
398
+ serviceId: schedule.serviceId,
399
+ environmentId: schedule.environmentId
400
+ },
401
+ apiToken,
402
+ apiUrl,
403
+ fetchImpl
404
+ });
405
+ const existing = collectRailwaySchedules(listed?.data).find(
406
+ (entry) => entry.name && entry.name === schedule.logicalName || entry.expression === schedule.expression && entry.serviceId === schedule.serviceId && (!schedule.environmentId || entry.environmentId === schedule.environmentId)
407
+ );
408
+ checks.push({
409
+ ...schedule,
410
+ id: existing?.id ?? null,
411
+ ok: Boolean(
412
+ existing && existing.expression === schedule.expression && (existing.command ?? null) === (schedule.command ?? null) && existing.enabled !== false
413
+ )
414
+ });
415
+ }
416
+ return {
417
+ ok: checks.every((entry) => entry.ok === true),
418
+ checks
419
+ };
75
420
  }
76
421
  function planRailwayServiceDeploy(service) {
77
422
  const args = ["up", "--service", service.serviceName ?? service.serviceId, "--ci"];
@@ -115,9 +460,13 @@ function deployRailwayService(tenantRoot, service, { dryRun = false } = {}) {
115
460
  };
116
461
  }
117
462
  export {
463
+ configuredRailwayScheduledJobs,
118
464
  configuredRailwayServices,
119
465
  deployRailwayService,
466
+ ensureRailwayScheduledJobs,
120
467
  planRailwayServiceDeploy,
468
+ resolveRailwayDeploymentProfile,
121
469
  validateRailwayDeployPrerequisites,
122
- validateRailwayServiceConfiguration
470
+ validateRailwayServiceConfiguration,
471
+ verifyRailwayScheduledJobs
123
472
  };
@@ -35,14 +35,35 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
35
35
  slug: string;
36
36
  siteUrl: string;
37
37
  contactEmail: string;
38
+ hosting: {
39
+ kind: string;
40
+ registration: string;
41
+ marketBaseUrl: string | undefined;
42
+ teamId: string | undefined;
43
+ projectId: string | undefined;
44
+ } | undefined;
38
45
  cloudflare: {
39
46
  accountId: string;
40
47
  workerName: string | undefined;
41
- gatewayWorkerName: string | undefined;
42
48
  queueName: string | undefined;
43
49
  dlqName: string | undefined;
44
50
  d1Binding: string | undefined;
45
51
  queueBinding: string | undefined;
52
+ pages: {
53
+ projectName: string | undefined;
54
+ previewProjectName: string | undefined;
55
+ productionBranch: string;
56
+ stagingBranch: string;
57
+ buildOutputDir: string | undefined;
58
+ } | undefined;
59
+ r2: {
60
+ binding: string | undefined;
61
+ bucketName: string | undefined;
62
+ publicBaseUrl: string | undefined;
63
+ manifestKeyTemplate: string;
64
+ previewRootTemplate: string;
65
+ previewTtlHours: number;
66
+ } | undefined;
46
67
  };
47
68
  plugins: {
48
69
  package: string;
@@ -64,6 +85,8 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
64
85
  };
65
86
  deploy: string;
66
87
  content: {
88
+ runtime: string;
89
+ publish: string;
67
90
  docs: string;
68
91
  };
69
92
  site: string;
@@ -85,6 +108,7 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
85
108
  rootDir: string | undefined;
86
109
  buildCommand: string | undefined;
87
110
  startCommand: string | undefined;
111
+ schedule: any;
88
112
  };
89
113
  environments: {
90
114
  local: {