@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.
- package/README.md +1 -1
- package/dist/control-plane-client.d.ts +45 -0
- package/dist/control-plane-client.js +229 -0
- package/dist/control-plane.d.ts +94 -0
- package/dist/control-plane.js +125 -0
- package/dist/d1-store.d.ts +56 -1
- package/dist/d1-store.js +132 -0
- package/dist/dispatch.d.ts +4 -0
- package/dist/dispatch.js +180 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +94 -4
- package/dist/operations/services/config-runtime.d.ts +10 -0
- package/dist/operations/services/config-runtime.js +62 -4
- package/dist/operations/services/deploy.d.ts +95 -3
- package/dist/operations/services/deploy.js +351 -10
- package/dist/operations/services/github-automation.d.ts +37 -1
- package/dist/operations/services/github-automation.js +71 -14
- package/dist/operations/services/project-platform.d.ts +835 -0
- package/dist/operations/services/project-platform.js +782 -0
- package/dist/operations/services/railway-deploy.d.ts +113 -18
- package/dist/operations/services/railway-deploy.js +357 -8
- package/dist/operations/services/runtime-tools.d.ts +25 -1
- package/dist/operations/services/runtime-tools.js +66 -5
- package/dist/operations/services/template-registry.d.ts +1 -1
- package/dist/operations/services/template-registry.js +17 -3
- package/dist/platform/books-data.d.ts +3 -4
- package/dist/platform/books-data.js +30 -4
- package/dist/platform/contracts.d.ts +56 -4
- package/dist/platform/deploy-config.js +109 -4
- package/dist/platform/deploy-runtime.d.ts +2 -0
- package/dist/platform/deploy-runtime.js +9 -1
- package/dist/platform/env.yaml +677 -0
- package/dist/platform/environment.js +57 -2
- package/dist/platform/plugin.d.ts +8 -0
- package/dist/platform/plugins/constants.d.ts +2 -0
- package/dist/platform/plugins/constants.js +2 -0
- package/dist/platform/plugins/runtime.d.ts +2 -0
- package/dist/platform/plugins/runtime.js +9 -1
- package/dist/platform/plugins.d.ts +1 -1
- package/dist/platform/plugins.js +4 -0
- package/dist/platform/published-content-pipeline.d.ts +84 -0
- package/dist/platform/published-content-pipeline.js +543 -0
- package/dist/platform/published-content.d.ts +223 -0
- package/dist/platform/published-content.js +588 -0
- package/dist/platform/tenant/runtime-config.d.ts +1 -1
- package/dist/platform/tenant/runtime-config.js +34 -1
- package/dist/platform/tenant-config.d.ts +2 -1
- package/dist/platform/tenant-config.js +17 -1
- package/dist/platform/utils/site-config-schema.js +104 -0
- package/dist/plugin-default.d.ts +2 -0
- package/dist/plugin-default.js +2 -0
- package/dist/remote.d.ts +65 -9
- package/dist/remote.js +104 -28
- package/dist/scripts/check-build-warnings.js +50 -0
- package/dist/scripts/config-treeseed.js +7 -0
- package/dist/scripts/tenant-workflow-action.js +71 -0
- package/dist/sdk-dispatch.d.ts +12 -0
- package/dist/sdk-dispatch.js +142 -0
- package/dist/sdk-types.d.ts +579 -7
- package/dist/sdk-types.js +53 -1
- package/dist/sdk.d.ts +17 -1
- package/dist/sdk.js +109 -0
- package/dist/stores/operational-store.d.ts +22 -2
- package/dist/stores/operational-store.js +235 -0
- package/dist/template-catalog.js +8 -1
- package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +20 -0
- package/dist/types/cloudflare.d.ts +23 -0
- package/dist/workflow/operations.d.ts +12 -3
- package/dist/workflow/policy.d.ts +1 -1
- package/dist/workflow-state.js +2 -1
- package/package.json +7 -2
- package/templates/github/deploy.workflow.yml +442 -0
- 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
|
|
15
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
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
|
-
|
|
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
|
|
271
|
+
return {
|
|
272
|
+
services,
|
|
273
|
+
schedules: configuredRailwayScheduledJobs(tenantRoot, scope),
|
|
274
|
+
hostingKind,
|
|
275
|
+
managedTopology
|
|
276
|
+
};
|
|
67
277
|
}
|
|
68
278
|
function validateRailwayDeployPrerequisites(tenantRoot, scope) {
|
|
69
|
-
const
|
|
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
|
|
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: {
|