@treeseed/sdk 0.8.6 → 0.8.7
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.
|
@@ -256,7 +256,7 @@ export declare function validateTreeseedCommandEnvironment({ tenantRoot, scope,
|
|
|
256
256
|
}): {
|
|
257
257
|
registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
|
|
258
258
|
values: {};
|
|
259
|
-
validation:
|
|
259
|
+
validation: any;
|
|
260
260
|
};
|
|
261
261
|
export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }: {
|
|
262
262
|
tenantRoot: any;
|
|
@@ -265,7 +265,7 @@ export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, pu
|
|
|
265
265
|
}): {
|
|
266
266
|
registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
|
|
267
267
|
values: {};
|
|
268
|
-
validation:
|
|
268
|
+
validation: any;
|
|
269
269
|
};
|
|
270
270
|
export declare function ensureTreeseedActVerificationTooling({ tenantRoot, installIfMissing, env, write }?: {
|
|
271
271
|
tenantRoot?: string | undefined;
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
} from "./railway-deploy.js";
|
|
35
35
|
import {
|
|
36
36
|
normalizeRailwayEnvironmentName,
|
|
37
|
-
resolveRailwayWorkspace
|
|
37
|
+
resolveRailwayWorkspace,
|
|
38
|
+
resolveRailwayWorkspaceContext
|
|
38
39
|
} from "./railway-api.js";
|
|
39
40
|
import {
|
|
40
41
|
createGitHubApiClient,
|
|
@@ -1585,20 +1586,52 @@ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false
|
|
|
1585
1586
|
function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
|
|
1586
1587
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1587
1588
|
const values = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
|
|
1588
|
-
const validation = validateTreeseedEnvironmentValues({
|
|
1589
|
+
const validation = filterValidationByWorkflowPlane(validateTreeseedEnvironmentValues({
|
|
1589
1590
|
values,
|
|
1590
1591
|
scope,
|
|
1591
1592
|
purpose,
|
|
1592
1593
|
deployConfig: registry.context.deployConfig,
|
|
1593
1594
|
tenantConfig: registry.context.tenantConfig,
|
|
1594
1595
|
plugins: registry.context.plugins
|
|
1595
|
-
});
|
|
1596
|
+
}));
|
|
1596
1597
|
return {
|
|
1597
1598
|
registry,
|
|
1598
1599
|
values,
|
|
1599
1600
|
validation
|
|
1600
1601
|
};
|
|
1601
1602
|
}
|
|
1603
|
+
function filterValidationByWorkflowPlane(validation) {
|
|
1604
|
+
const plane = process.env.TREESEED_WORKFLOW_PLANE;
|
|
1605
|
+
if (plane !== "web" && plane !== "processing") {
|
|
1606
|
+
return validation;
|
|
1607
|
+
}
|
|
1608
|
+
const problemApplies = (problem) => doesEntryApplyToWorkflowPlane(problem.entry, plane);
|
|
1609
|
+
const missing = validation.missing.filter(problemApplies);
|
|
1610
|
+
const invalid = validation.invalid.filter(problemApplies);
|
|
1611
|
+
const entries = validation.entries.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
|
|
1612
|
+
const required = validation.required.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
|
|
1613
|
+
return {
|
|
1614
|
+
...validation,
|
|
1615
|
+
ok: missing.length === 0 && invalid.length === 0,
|
|
1616
|
+
entries,
|
|
1617
|
+
required,
|
|
1618
|
+
missing,
|
|
1619
|
+
invalid
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function doesEntryApplyToWorkflowPlane(entry, plane) {
|
|
1623
|
+
const targets = new Set(entry.targets ?? []);
|
|
1624
|
+
const hasProcessingTarget = targets.has("railway-secret") || targets.has("railway-var");
|
|
1625
|
+
const hasWebTarget = targets.has("cloudflare-secret") || targets.has("cloudflare-var") || targets.has("local-cloudflare");
|
|
1626
|
+
const hasWorkflowTarget = targets.has("github-secret") || targets.has("github-variable");
|
|
1627
|
+
if (plane === "web") {
|
|
1628
|
+
return !hasProcessingTarget || hasWebTarget || hasWorkflowTarget;
|
|
1629
|
+
}
|
|
1630
|
+
if (plane === "processing") {
|
|
1631
|
+
return !hasWebTarget || hasProcessingTarget || hasWorkflowTarget;
|
|
1632
|
+
}
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1602
1635
|
function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
|
|
1603
1636
|
const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
|
|
1604
1637
|
if (report.validation.ok) {
|
|
@@ -1894,23 +1927,26 @@ async function checkRailwayConnection({ tenantRoot, env }) {
|
|
|
1894
1927
|
const checkPromise = (async () => {
|
|
1895
1928
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1896
1929
|
try {
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
if (!whoami.ok) {
|
|
1900
|
-
if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
|
|
1901
|
-
return providerConnectionResult(
|
|
1902
|
-
"railway",
|
|
1903
|
-
false,
|
|
1904
|
-
"Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1905
|
-
{ skipped: true, warning: true, rateLimited: true }
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
throw new Error(whoami.detail || "Railway CLI authentication check failed.");
|
|
1909
|
-
}
|
|
1910
|
-
const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
|
|
1911
|
-
return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
|
|
1930
|
+
const workspace = await resolveRailwayWorkspaceContext({ env, workspace: workspaceName });
|
|
1931
|
+
return providerConnectionResult("railway", true, `Railway API token can access workspace ${workspace.name}. Project and service existence will be reconciled during bootstrap.`);
|
|
1912
1932
|
} catch (error) {
|
|
1913
1933
|
const detail = error instanceof Error ? error.message : "Railway API check failed.";
|
|
1934
|
+
if (/rate.?limit|too many requests|429/iu.test(detail || "")) {
|
|
1935
|
+
return providerConnectionResult(
|
|
1936
|
+
"railway",
|
|
1937
|
+
false,
|
|
1938
|
+
"Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1939
|
+
{ skipped: true, warning: true, rateLimited: true }
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
if (attempt >= 2 && isTransientProviderConnectionError(detail)) {
|
|
1943
|
+
return providerConnectionResult(
|
|
1944
|
+
"railway",
|
|
1945
|
+
false,
|
|
1946
|
+
"Railway connectivity preflight hit transient API failures; bootstrap will continue and rely on API-backed reconcile verification.",
|
|
1947
|
+
{ skipped: true, warning: true, transient: true }
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1914
1950
|
if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
|
|
1915
1951
|
return providerConnectionResult("railway", false, detail);
|
|
1916
1952
|
}
|
|
@@ -1044,22 +1044,44 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
|
|
|
1044
1044
|
function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
|
|
1045
1045
|
const requestScript = `import { readFileSync } from 'node:fs';
|
|
1046
1046
|
const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
|
|
1047
|
+
function errorMessage(error) {
|
|
1048
|
+
const parts = [];
|
|
1049
|
+
if (error && typeof error.message === 'string') parts.push(error.message);
|
|
1050
|
+
const cause = error?.cause;
|
|
1051
|
+
if (cause && typeof cause.message === 'string') parts.push(cause.message);
|
|
1052
|
+
if (cause && typeof cause.code === 'string') parts.push(cause.code);
|
|
1053
|
+
if (Array.isArray(cause?.errors)) {
|
|
1054
|
+
for (const entry of cause.errors) {
|
|
1055
|
+
if (entry && typeof entry.message === 'string') parts.push(entry.message);
|
|
1056
|
+
if (entry && typeof entry.code === 'string') parts.push(entry.code);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
|
|
1061
1060
|
}
|
|
1062
|
-
|
|
1061
|
+
try {
|
|
1062
|
+
const response = await fetch(input.url, {
|
|
1063
|
+
method: input.method,
|
|
1064
|
+
headers: {
|
|
1065
|
+
authorization: 'Bearer ' + input.token,
|
|
1066
|
+
'content-type': 'application/json',
|
|
1067
|
+
},
|
|
1068
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
1069
|
+
});
|
|
1070
|
+
const rawBody = await response.text();
|
|
1071
|
+
let payload;
|
|
1072
|
+
try {
|
|
1073
|
+
payload = rawBody ? JSON.parse(rawBody) : {};
|
|
1074
|
+
} catch {
|
|
1075
|
+
payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
|
|
1076
|
+
}
|
|
1077
|
+
process.stdout.write(JSON.stringify({ ok: response.ok, payload }));
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
process.stdout.write(JSON.stringify({
|
|
1080
|
+
ok: false,
|
|
1081
|
+
transient: true,
|
|
1082
|
+
payload: { success: false, errors: [{ message: errorMessage(error) }] },
|
|
1083
|
+
}));
|
|
1084
|
+
}`;
|
|
1063
1085
|
const requestInput = JSON.stringify({
|
|
1064
1086
|
url: `https://api.cloudflare.com/client/v4${path}`,
|
|
1065
1087
|
method,
|
|
@@ -1067,6 +1089,11 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
|
|
|
1067
1089
|
token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
|
|
1068
1090
|
});
|
|
1069
1091
|
const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
|
|
1092
|
+
const formatPayloadErrors = (payload) => Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "";
|
|
1093
|
+
const summarizeChildError = (text) => {
|
|
1094
|
+
const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1095
|
+
return lines.find((line) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|typeerror|error/iu.test(line)) ?? lines[0] ?? "";
|
|
1096
|
+
};
|
|
1070
1097
|
let attempt = 0;
|
|
1071
1098
|
for (; ; ) {
|
|
1072
1099
|
const response = spawnSync(
|
|
@@ -1085,29 +1112,48 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
|
|
|
1085
1112
|
}
|
|
1086
1113
|
);
|
|
1087
1114
|
if (response.error?.code === "ETIMEDOUT") {
|
|
1088
|
-
if (attempt <
|
|
1115
|
+
if (attempt < 4) {
|
|
1089
1116
|
attempt += 1;
|
|
1117
|
+
sleepSync(500 * attempt);
|
|
1090
1118
|
continue;
|
|
1091
1119
|
}
|
|
1092
1120
|
if (!allowFailure) {
|
|
1093
|
-
throw new Error(`Cloudflare API request timed out: ${method} ${path}`);
|
|
1121
|
+
throw new Error(`Cloudflare API request timed out after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1094
1122
|
}
|
|
1095
1123
|
return null;
|
|
1096
1124
|
}
|
|
1097
1125
|
const stderr = response.stderr?.trim() || "";
|
|
1098
1126
|
if (response.status !== 0) {
|
|
1099
|
-
if (attempt <
|
|
1127
|
+
if (attempt < 4 && isTransient(stderr)) {
|
|
1100
1128
|
attempt += 1;
|
|
1129
|
+
sleepSync(500 * attempt);
|
|
1101
1130
|
continue;
|
|
1102
1131
|
}
|
|
1103
1132
|
if (!allowFailure) {
|
|
1104
|
-
|
|
1133
|
+
const detail = summarizeChildError(stderr);
|
|
1134
|
+
throw new Error(detail ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${detail}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1105
1135
|
}
|
|
1106
1136
|
}
|
|
1107
|
-
|
|
1137
|
+
let parsed;
|
|
1138
|
+
try {
|
|
1139
|
+
parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
|
|
1140
|
+
} catch {
|
|
1141
|
+
parsed = {
|
|
1142
|
+
ok: false,
|
|
1143
|
+
payload: {
|
|
1144
|
+
success: false,
|
|
1145
|
+
errors: [{ message: response.stdout?.trim() || stderr || "empty response" }]
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const details = formatPayloadErrors(parsed.payload);
|
|
1150
|
+
if (!parsed.ok && parsed.transient && attempt < 4 && isTransient(details)) {
|
|
1151
|
+
attempt += 1;
|
|
1152
|
+
sleepSync(500 * attempt);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1108
1155
|
if (!parsed.ok && !allowFailure) {
|
|
1109
|
-
|
|
1110
|
-
throw new Error(details || `Cloudflare API request failed: ${method} ${path}`);
|
|
1156
|
+
throw new Error(details ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${details}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
|
|
1111
1157
|
}
|
|
1112
1158
|
return parsed.payload;
|
|
1113
1159
|
}
|
|
@@ -506,7 +506,7 @@ function parseFallbackDeployConfig(configPath) {
|
|
|
506
506
|
slug: expectString(record.slug, "slug"),
|
|
507
507
|
siteUrl: expectString(record.siteUrl, "siteUrl"),
|
|
508
508
|
contactEmail: expectString(record.contactEmail, "contactEmail"),
|
|
509
|
-
hosting: parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
|
|
509
|
+
hosting: parsedHosting?.kind === "market_control_plane" ? { ...parsedHosting, registration: "none" } : parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
|
|
510
510
|
hub,
|
|
511
511
|
runtime,
|
|
512
512
|
cloudflare: {
|
|
@@ -541,7 +541,7 @@ function parseDeployConfig(raw) {
|
|
|
541
541
|
const turnstile = optionalRecord(parsed.turnstile, "turnstile") ?? {};
|
|
542
542
|
optionalBoolean(turnstile.enabled, "turnstile.enabled");
|
|
543
543
|
const normalizedHosting = normalizeLegacyHostingFromPlanes(hub, runtime);
|
|
544
|
-
const compatibilityHosting = hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
|
|
544
|
+
const compatibilityHosting = hosting?.kind === "market_control_plane" ? { ...hosting, registration: "none" } : hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
|
|
545
545
|
return {
|
|
546
546
|
name: expectString(parsed.name, "name"),
|
|
547
547
|
slug: expectString(parsed.slug, "slug"),
|