@treeseed/sdk 0.10.16 → 0.10.18
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/dist/api/config.js +10 -3
- package/dist/operations/services/config-runtime.js +6 -1
- package/dist/operations/services/deploy.d.ts +107 -27
- package/dist/operations/services/deploy.js +852 -28
- package/dist/operations/services/project-platform.d.ts +4 -2
- package/dist/operations/services/project-platform.js +56 -27
- package/dist/operations/services/railway-api.d.ts +21 -6
- package/dist/operations/services/railway-api.js +208 -23
- package/dist/operations/services/railway-deploy.d.ts +24 -4
- package/dist/operations/services/railway-deploy.js +13 -4
- package/dist/platform/environment.js +3 -0
- package/dist/reconcile/builtin-adapters.js +43 -33
- package/dist/scripts/tenant-destroy.js +8 -2
- package/dist/workflow/operations.d.ts +92 -0
- package/dist/workflow/operations.js +11 -3
- package/dist/workflow.d.ts +1 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +8 -1
|
@@ -3,6 +3,7 @@ import type { TreeseedRunnableBootstrapSystem } from '../../reconcile/index.ts';
|
|
|
3
3
|
import { type TreeseedBootstrapExecution, type TreeseedBootstrapWriter } from './bootstrap-runner.ts';
|
|
4
4
|
export type ProjectPlatformScope = 'local' | 'staging' | 'prod';
|
|
5
5
|
export type ProjectPlatformAction = 'deploy_web' | 'publish_content' | 'monitor';
|
|
6
|
+
type ProjectPlatformContentPublishMode = 'production' | 'editorial_overlay';
|
|
6
7
|
export interface ProjectPlatformActionOptions {
|
|
7
8
|
tenantRoot: string;
|
|
8
9
|
scope: ProjectPlatformScope;
|
|
@@ -292,7 +293,7 @@ export declare function resolveRailwayServiceDeployDependencies({ includeDataDep
|
|
|
292
293
|
export declare function publishProjectContent(options: ProjectPlatformActionOptions): Promise<{
|
|
293
294
|
ok: boolean;
|
|
294
295
|
scope: ProjectPlatformScope;
|
|
295
|
-
mode:
|
|
296
|
+
mode: ProjectPlatformContentPublishMode;
|
|
296
297
|
revision: string;
|
|
297
298
|
previewId: string | null;
|
|
298
299
|
previewUrl: string | null;
|
|
@@ -454,7 +455,7 @@ export declare function syncControlPlaneState(options: ProjectPlatformActionOpti
|
|
|
454
455
|
export declare function runProjectPlatformAction(action: ProjectPlatformAction, options: ProjectPlatformActionOptions): Promise<{
|
|
455
456
|
ok: boolean;
|
|
456
457
|
scope: ProjectPlatformScope;
|
|
457
|
-
mode:
|
|
458
|
+
mode: ProjectPlatformContentPublishMode;
|
|
458
459
|
revision: string;
|
|
459
460
|
previewId: string | null;
|
|
460
461
|
previewUrl: string | null;
|
|
@@ -809,3 +810,4 @@ export declare function runProjectPlatformAction(action: ProjectPlatformAction,
|
|
|
809
810
|
} | null;
|
|
810
811
|
} | undefined)[];
|
|
811
812
|
}>;
|
|
813
|
+
export {};
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
purgePublishedContentCaches,
|
|
28
28
|
resolveConfiguredCloudflareAccountId,
|
|
29
29
|
resolveConfiguredSurfaceBaseUrl,
|
|
30
|
+
resolveTreeseedResourceIdentity,
|
|
30
31
|
syncCloudflareSecrets,
|
|
31
32
|
writeDeployState
|
|
32
33
|
} from "./deploy.js";
|
|
@@ -220,7 +221,8 @@ function prepareTenantCloudflareDeploy({
|
|
|
220
221
|
env: {
|
|
221
222
|
...process.env,
|
|
222
223
|
...env,
|
|
223
|
-
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
|
|
224
|
+
CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig),
|
|
225
|
+
...target.kind === "persistent" && target.scope !== "local" ? { TREESEED_CONTENT_SERVING_MODE: "published_runtime" } : {}
|
|
224
226
|
},
|
|
225
227
|
write
|
|
226
228
|
};
|
|
@@ -464,8 +466,8 @@ function resolveReporter(tenantRoot, explicit) {
|
|
|
464
466
|
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
465
467
|
return createControlPlaneReporter({ deployConfig });
|
|
466
468
|
}
|
|
467
|
-
function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer, filePath) {
|
|
468
|
-
|
|
469
|
+
async function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer, filePath, options) {
|
|
470
|
+
await runPrefixedWranglerWithRetry(tenantRoot, [
|
|
469
471
|
"r2",
|
|
470
472
|
"object",
|
|
471
473
|
"put",
|
|
@@ -478,7 +480,11 @@ function uploadObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, pointer
|
|
|
478
480
|
filePath,
|
|
479
481
|
"--content-type",
|
|
480
482
|
pointer.contentType ?? inferContentType(filePath)
|
|
481
|
-
],
|
|
483
|
+
], {
|
|
484
|
+
env: wranglerEnv,
|
|
485
|
+
write: options.write,
|
|
486
|
+
prefix: options.prefix
|
|
487
|
+
});
|
|
482
488
|
}
|
|
483
489
|
function deleteObject(tenantRoot, wranglerPath, wranglerEnv, bucketName, objectKey) {
|
|
484
490
|
runWrangler(tenantRoot, [
|
|
@@ -794,11 +800,11 @@ function probeScaleConfiguration(siteConfig, state) {
|
|
|
794
800
|
serviceName: worker.serviceName ?? null
|
|
795
801
|
};
|
|
796
802
|
}
|
|
797
|
-
async function publishContent(options, reporter) {
|
|
803
|
+
async function publishContent(options, reporter, publishOptions = {}) {
|
|
798
804
|
const target = runTenantPublishContentPreflight(options);
|
|
799
805
|
const siteConfig = loadCliDeployConfig(options.tenantRoot);
|
|
800
806
|
const tenantConfig = loadTreeseedManifest(resolve(options.tenantRoot, "src", "manifest.yaml"));
|
|
801
|
-
const teamId =
|
|
807
|
+
const teamId = resolveTreeseedResourceIdentity(siteConfig, target).teamId;
|
|
802
808
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
803
809
|
const commitSha = currentCommit(options.tenantRoot);
|
|
804
810
|
const branchName = currentRef(options.tenantRoot);
|
|
@@ -827,7 +833,8 @@ async function publishContent(options, reporter) {
|
|
|
827
833
|
sourceRef: branchName,
|
|
828
834
|
previewId
|
|
829
835
|
});
|
|
830
|
-
const
|
|
836
|
+
const publishMode = publishOptions.mode ?? (options.scope === "staging" ? "editorial_overlay" : "production");
|
|
837
|
+
const built = publishMode === "editorial_overlay" ? await pipeline.buildEditorialOverlay({ previousManifest, previewId }) : await pipeline.buildProductionRevision({ previousManifest });
|
|
831
838
|
const changedEntrySet = "manifest" in built ? changedEntries(previousManifest, built.manifest.entries) : [];
|
|
832
839
|
const changedArtifactSet = "manifest" in built ? changedArtifacts(previousManifest, built.manifest.artifacts ?? []) : [];
|
|
833
840
|
const tombstones = "manifest" in built ? built.manifest.tombstones ?? [] : [];
|
|
@@ -910,20 +917,29 @@ async function publishContent(options, reporter) {
|
|
|
910
917
|
const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-content-publish-"));
|
|
911
918
|
try {
|
|
912
919
|
if (!options.dryRun) {
|
|
920
|
+
const uploadOptions = {
|
|
921
|
+
write: options.write,
|
|
922
|
+
prefix: {
|
|
923
|
+
scope: options.scope,
|
|
924
|
+
system: "content",
|
|
925
|
+
task: "publish",
|
|
926
|
+
stage: "upload"
|
|
927
|
+
}
|
|
928
|
+
};
|
|
913
929
|
for (const object of built.objects) {
|
|
914
930
|
const filePath = writeTempFile(tempRoot, objectFileName(object.pointer), toBuffer(object.body));
|
|
915
|
-
uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, object.pointer, filePath);
|
|
931
|
+
await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, object.pointer, filePath, uploadOptions);
|
|
916
932
|
}
|
|
917
933
|
for (const alias of stableObjectUploads) {
|
|
918
934
|
const filePath = writeTempFile(tempRoot, objectFileName(alias.pointer), alias.body);
|
|
919
|
-
uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, alias.pointer, filePath);
|
|
935
|
+
await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, alias.pointer, filePath, uploadOptions);
|
|
920
936
|
}
|
|
921
937
|
for (const objectKey of deletedObjectKeys) {
|
|
922
938
|
deleteObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, objectKey);
|
|
923
939
|
}
|
|
924
940
|
if ("overlay" in built) {
|
|
925
941
|
const overlayFile = writeTempFile(tempRoot, "overlay.json", Buffer.from(JSON.stringify(built.overlay, null, 2)));
|
|
926
|
-
uploadObject(
|
|
942
|
+
await uploadObject(
|
|
927
943
|
options.tenantRoot,
|
|
928
944
|
wranglerPath,
|
|
929
945
|
wranglerEnv,
|
|
@@ -934,23 +950,24 @@ async function publishContent(options, reporter) {
|
|
|
934
950
|
size: statSync(overlayFile).size,
|
|
935
951
|
contentType: "application/json"
|
|
936
952
|
},
|
|
937
|
-
overlayFile
|
|
953
|
+
overlayFile,
|
|
954
|
+
uploadOptions
|
|
938
955
|
);
|
|
939
956
|
} else {
|
|
940
957
|
const manifestFile = writeTempFile(tempRoot, "manifest.json", Buffer.from(JSON.stringify(built.manifest, null, 2)));
|
|
941
958
|
const snapshotKey = locator.manifestKey.replace(/\/common\.json$/u, `/manifests/${built.manifest.revision}.json`);
|
|
942
|
-
uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
|
|
959
|
+
await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
|
|
943
960
|
objectKey: snapshotKey,
|
|
944
961
|
sha256: stableHash(readFileSync(manifestFile)),
|
|
945
962
|
size: statSync(manifestFile).size,
|
|
946
963
|
contentType: "application/json"
|
|
947
|
-
}, manifestFile);
|
|
948
|
-
uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
|
|
964
|
+
}, manifestFile, uploadOptions);
|
|
965
|
+
await uploadObject(options.tenantRoot, wranglerPath, wranglerEnv, bucketName, {
|
|
949
966
|
objectKey: locator.manifestKey,
|
|
950
967
|
sha256: stableHash(readFileSync(manifestFile)),
|
|
951
968
|
size: statSync(manifestFile).size,
|
|
952
969
|
contentType: "application/json"
|
|
953
|
-
}, manifestFile);
|
|
970
|
+
}, manifestFile, uploadOptions);
|
|
954
971
|
if (contentPurgeUrls.size > 0) {
|
|
955
972
|
try {
|
|
956
973
|
purgePublishedContentCaches(options.tenantRoot, [...contentPurgeUrls].filter(Boolean), { target });
|
|
@@ -960,12 +977,14 @@ async function publishContent(options, reporter) {
|
|
|
960
977
|
}
|
|
961
978
|
}
|
|
962
979
|
const state = loadDeployState(options.tenantRoot, siteConfig, { target });
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
980
|
+
if (!options.dryRun) {
|
|
981
|
+
state.content.lastPublishedManifestRevision = "overlay" in built ? built.overlay.previewId : built.manifest.revision;
|
|
982
|
+
state.content.lastPublishedManifestSha256 = stableHash(
|
|
983
|
+
JSON.stringify("overlay" in built ? built.overlay : built.manifest)
|
|
984
|
+
);
|
|
985
|
+
writeDeployState(options.tenantRoot, state, { target });
|
|
986
|
+
}
|
|
987
|
+
const previewToken = publishMode === "editorial_overlay" && process.env.TREESEED_EDITORIAL_PREVIEW_SECRET ? signEditorialPreviewToken({
|
|
969
988
|
teamId,
|
|
970
989
|
previewId,
|
|
971
990
|
expiresAt: "overlay" in built ? built.overlay.expiresAt ?? new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString() : new Date(Date.now() + resolvePublishedContentPreviewTtlHours(siteConfig) * 60 * 60 * 1e3).toISOString()
|
|
@@ -980,9 +999,9 @@ async function publishContent(options, reporter) {
|
|
|
980
999
|
commitSha,
|
|
981
1000
|
triggeredByType: "project_runner",
|
|
982
1001
|
metadata: {
|
|
983
|
-
mode:
|
|
1002
|
+
mode: publishMode,
|
|
984
1003
|
revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
|
|
985
|
-
previewId:
|
|
1004
|
+
previewId: publishMode === "editorial_overlay" ? previewId : null,
|
|
986
1005
|
previewUrl,
|
|
987
1006
|
entries: ("overlay" in built ? built.overlay.entries : built.manifest.entries).length,
|
|
988
1007
|
artifacts: ("overlay" in built ? built.overlay.artifacts : built.manifest.artifacts)?.length ?? 0,
|
|
@@ -994,9 +1013,9 @@ async function publishContent(options, reporter) {
|
|
|
994
1013
|
return {
|
|
995
1014
|
ok: true,
|
|
996
1015
|
scope: options.scope,
|
|
997
|
-
mode:
|
|
1016
|
+
mode: publishMode,
|
|
998
1017
|
revision: "overlay" in built ? built.overlay.previewId : built.manifest.revision,
|
|
999
|
-
previewId:
|
|
1018
|
+
previewId: publishMode === "editorial_overlay" ? previewId : null,
|
|
1000
1019
|
previewUrl,
|
|
1001
1020
|
target: deployTargetLabel(target)
|
|
1002
1021
|
};
|
|
@@ -1214,13 +1233,23 @@ async function deployProjectPlatform(options) {
|
|
|
1214
1233
|
}
|
|
1215
1234
|
if (cloudflareContext && selectedSystems.has("web")) {
|
|
1216
1235
|
const context = cloudflareContext;
|
|
1236
|
+
const contentNodeId = "content:publish-runtime";
|
|
1237
|
+
nodes.push({
|
|
1238
|
+
id: contentNodeId,
|
|
1239
|
+
dependencies: selectedSystems.has("data") ? ["data:d1-migrate"] : [],
|
|
1240
|
+
run: () => publishContent({
|
|
1241
|
+
...options,
|
|
1242
|
+
reporter,
|
|
1243
|
+
bootstrapSystems: ["web"]
|
|
1244
|
+
}, reporter, { mode: "production" })
|
|
1245
|
+
});
|
|
1217
1246
|
nodes.push({
|
|
1218
1247
|
id: "web:build",
|
|
1219
1248
|
run: () => runTenantWebBuild(context)
|
|
1220
1249
|
});
|
|
1221
1250
|
nodes.push({
|
|
1222
1251
|
id: "web:publish",
|
|
1223
|
-
dependencies: ["web:build", ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
|
|
1252
|
+
dependencies: ["web:build", contentNodeId, ...selectedSystems.has("data") ? ["data:d1-migrate"] : []],
|
|
1224
1253
|
run: () => runTenantWebPublish(context)
|
|
1225
1254
|
});
|
|
1226
1255
|
}
|
|
@@ -1452,7 +1481,7 @@ async function runProjectPlatformAction(action, options) {
|
|
|
1452
1481
|
const previousWorkflowAction = process.env.TREESEED_WORKFLOW_ACTION;
|
|
1453
1482
|
const previousWorkflowPlane = process.env.TREESEED_WORKFLOW_PLANE;
|
|
1454
1483
|
process.env.TREESEED_WORKFLOW_ACTION = action;
|
|
1455
|
-
process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane ?? "
|
|
1484
|
+
process.env.TREESEED_WORKFLOW_PLANE = previousWorkflowPlane ?? "all";
|
|
1456
1485
|
applyTreeseedEnvironmentToProcess({ tenantRoot: options.tenantRoot, scope: options.scope, override: true });
|
|
1457
1486
|
const reporter = resolveReporter(options.tenantRoot, options.reporter);
|
|
1458
1487
|
try {
|
|
@@ -288,12 +288,7 @@ export declare function ensureRailwayServiceVolume({ projectId, environmentId, s
|
|
|
288
288
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
289
289
|
fetchImpl?: typeof fetch;
|
|
290
290
|
}): Promise<{
|
|
291
|
-
volume:
|
|
292
|
-
instances: RailwayVolumeInstanceSummary[];
|
|
293
|
-
id: string;
|
|
294
|
-
name: string;
|
|
295
|
-
projectId: string | null;
|
|
296
|
-
} | null;
|
|
291
|
+
volume: RailwayVolumeSummary | null;
|
|
297
292
|
instance: RailwayVolumeInstanceSummary | null;
|
|
298
293
|
created: boolean;
|
|
299
294
|
updated: boolean;
|
|
@@ -316,3 +311,23 @@ export declare function ensureRailwayCustomDomain({ projectId, environmentId, se
|
|
|
316
311
|
domain: RailwayCustomDomainSummary;
|
|
317
312
|
created: boolean;
|
|
318
313
|
}>;
|
|
314
|
+
export declare function deleteRailwayCustomDomain({ domainId, env, fetchImpl, }: {
|
|
315
|
+
domainId: string;
|
|
316
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
317
|
+
fetchImpl?: typeof fetch;
|
|
318
|
+
}): Promise<Record<string, unknown>>;
|
|
319
|
+
export declare function deleteRailwayVolume({ volumeId, env, fetchImpl, }: {
|
|
320
|
+
volumeId: string;
|
|
321
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
322
|
+
fetchImpl?: typeof fetch;
|
|
323
|
+
}): Promise<Record<string, unknown>>;
|
|
324
|
+
export declare function deleteRailwayEnvironment({ environmentId, env, fetchImpl, }: {
|
|
325
|
+
environmentId: string;
|
|
326
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
327
|
+
fetchImpl?: typeof fetch;
|
|
328
|
+
}): Promise<Record<string, unknown>>;
|
|
329
|
+
export declare function deleteRailwayProject({ projectId, env, fetchImpl, }: {
|
|
330
|
+
projectId: string;
|
|
331
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
332
|
+
fetchImpl?: typeof fetch;
|
|
333
|
+
}): Promise<Record<string, unknown>>;
|
|
@@ -840,7 +840,13 @@ async function ensureRailwayServiceInstanceConfiguration({
|
|
|
840
840
|
env = process.env,
|
|
841
841
|
fetchImpl = fetch
|
|
842
842
|
}) {
|
|
843
|
-
|
|
843
|
+
let current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
|
|
844
|
+
if (!current.id) {
|
|
845
|
+
for (let attempt = 0; attempt < 8 && !current.id; attempt += 1) {
|
|
846
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
847
|
+
current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
844
850
|
if (!current.id) {
|
|
845
851
|
return { instance: current, updated: false };
|
|
846
852
|
}
|
|
@@ -905,12 +911,21 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
|
|
|
905
911
|
}
|
|
906
912
|
throw error;
|
|
907
913
|
}
|
|
908
|
-
|
|
914
|
+
let instance = await getRailwayServiceInstance({
|
|
909
915
|
serviceId,
|
|
910
916
|
environmentId,
|
|
911
917
|
env,
|
|
912
918
|
fetchImpl
|
|
913
919
|
});
|
|
920
|
+
for (let attempt = 0; attempt < 5 && serviceInstanceDrifted(instance, desired); attempt += 1) {
|
|
921
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
922
|
+
instance = await getRailwayServiceInstance({
|
|
923
|
+
serviceId,
|
|
924
|
+
environmentId,
|
|
925
|
+
env,
|
|
926
|
+
fetchImpl
|
|
927
|
+
});
|
|
928
|
+
}
|
|
914
929
|
return {
|
|
915
930
|
instance: {
|
|
916
931
|
id: instance.id || current.id,
|
|
@@ -929,6 +944,9 @@ mutation TreeseedRailwayServiceInstanceUpdateLegacy($serviceId: String!, $enviro
|
|
|
929
944
|
updated: true
|
|
930
945
|
};
|
|
931
946
|
}
|
|
947
|
+
function serviceInstanceDrifted(current, desired) {
|
|
948
|
+
return desired.buildCommand !== null && desired.buildCommand !== current.buildCommand || desired.startCommand !== null && desired.startCommand !== current.startCommand || desired.cronSchedule !== null && desired.cronSchedule !== current.cronSchedule || desired.rootDirectory !== null && desired.rootDirectory !== current.rootDirectory || desired.healthcheckPath !== null && desired.healthcheckPath !== current.healthcheckPath || desired.healthcheckTimeoutSeconds !== null && desired.healthcheckTimeoutSeconds !== current.healthcheckTimeoutSeconds || desired.runtimeMode !== null && desired.runtimeMode !== current.runtimeMode;
|
|
949
|
+
}
|
|
932
950
|
async function listRailwayVariables({
|
|
933
951
|
projectId,
|
|
934
952
|
environmentId,
|
|
@@ -1193,44 +1211,97 @@ async function ensureRailwayServiceVolume({
|
|
|
1193
1211
|
...candidate,
|
|
1194
1212
|
instances: candidate.instances.filter(isActiveRailwayVolumeInstance)
|
|
1195
1213
|
})).filter((candidate) => candidate.instances.length > 0);
|
|
1196
|
-
let volume = activeVolumes.find(
|
|
1197
|
-
(candidate) => candidate.instances.some((instance2) => instance2.
|
|
1198
|
-
) ??
|
|
1214
|
+
let volume = findRailwayVolumeForService(volumes, serviceId, environmentId) ?? activeVolumes.find(
|
|
1215
|
+
(candidate) => candidate.name === name && candidate.instances.some((instance2) => instance2.environmentId === environmentId)
|
|
1216
|
+
) ?? null;
|
|
1199
1217
|
let created = false;
|
|
1200
1218
|
let updated = false;
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1219
|
+
const createReplacementVolume = async () => {
|
|
1220
|
+
let replacement;
|
|
1221
|
+
try {
|
|
1222
|
+
replacement = await createRailwayVolume({
|
|
1223
|
+
projectId,
|
|
1224
|
+
environmentId,
|
|
1225
|
+
serviceId,
|
|
1226
|
+
name,
|
|
1227
|
+
mountPath,
|
|
1228
|
+
env,
|
|
1229
|
+
fetchImpl
|
|
1230
|
+
});
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
if (!looksLikeRailwayVolumeCreateRace(error)) {
|
|
1233
|
+
throw error;
|
|
1234
|
+
}
|
|
1235
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1236
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
1237
|
+
const refreshed = await listRailwayVolumes({ projectId, env, fetchImpl });
|
|
1238
|
+
const existing = findRailwayVolumeForService(refreshed, serviceId, environmentId);
|
|
1239
|
+
if (existing) {
|
|
1240
|
+
return existing;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
throw error;
|
|
1244
|
+
}
|
|
1211
1245
|
created = true;
|
|
1246
|
+
return replacement;
|
|
1247
|
+
};
|
|
1248
|
+
if (!volume) {
|
|
1249
|
+
volume = await createReplacementVolume();
|
|
1212
1250
|
}
|
|
1213
1251
|
if (volume.name && volume.name !== name) {
|
|
1214
|
-
|
|
1252
|
+
try {
|
|
1253
|
+
volume = await updateRailwayVolumeName({ volumeId: volume.id, name, env, fetchImpl }) ?? { ...volume, name };
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (!looksLikeRailwayMissingResource(error)) {
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
volume = await createReplacementVolume();
|
|
1259
|
+
}
|
|
1215
1260
|
updated = true;
|
|
1216
1261
|
}
|
|
1217
1262
|
let instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
|
|
1218
1263
|
if (!instance && volume.instances.some((entry) => entry.environmentId === environmentId)) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1264
|
+
try {
|
|
1265
|
+
await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, serviceId, mountPath, env, fetchImpl });
|
|
1266
|
+
volume = await listRailwayVolumes({ projectId, env, fetchImpl }).then((refreshed) => refreshed.find((candidate) => candidate.id === volume?.id) ?? volume);
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (!looksLikeRailwayMissingResource(error)) {
|
|
1269
|
+
throw error;
|
|
1270
|
+
}
|
|
1271
|
+
volume = await createReplacementVolume();
|
|
1272
|
+
}
|
|
1221
1273
|
instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
|
|
1222
1274
|
updated = true;
|
|
1223
1275
|
}
|
|
1224
1276
|
if (instance && instance.mountPath !== mountPath) {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1277
|
+
try {
|
|
1278
|
+
await updateRailwayVolumeInstanceMountPath({ volumeId: volume.id, mountPath, env, fetchImpl });
|
|
1279
|
+
volume = {
|
|
1280
|
+
...volume,
|
|
1281
|
+
instances: volume.instances.map((entry) => entry.id === instance.id ? { ...entry, mountPath } : entry)
|
|
1282
|
+
};
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
if (!looksLikeRailwayMissingResource(error)) {
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
volume = await createReplacementVolume();
|
|
1288
|
+
instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? null;
|
|
1289
|
+
}
|
|
1230
1290
|
updated = true;
|
|
1231
1291
|
}
|
|
1232
1292
|
return { volume, instance, created, updated };
|
|
1233
1293
|
}
|
|
1294
|
+
function findRailwayVolumeForService(volumes, serviceId, environmentId) {
|
|
1295
|
+
return volumes.find(
|
|
1296
|
+
(candidate) => candidate.instances.some(
|
|
1297
|
+
(instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && isActiveRailwayVolumeInstance(instance)
|
|
1298
|
+
)
|
|
1299
|
+
) ?? null;
|
|
1300
|
+
}
|
|
1301
|
+
function looksLikeRailwayVolumeCreateRace(error) {
|
|
1302
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
1303
|
+
return /would have \d+ volumes attached|can only have one volume|not authorized/iu.test(message);
|
|
1304
|
+
}
|
|
1234
1305
|
async function listRailwayCustomDomains({
|
|
1235
1306
|
projectId,
|
|
1236
1307
|
environmentId,
|
|
@@ -1340,7 +1411,121 @@ mutation TreeseedRailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
|
|
|
1340
1411
|
}
|
|
1341
1412
|
return { domain: created, created: true };
|
|
1342
1413
|
}
|
|
1414
|
+
function looksLikeRailwayMissingResource(error) {
|
|
1415
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
1416
|
+
return /not found|does not exist|could not find|unknown|invalid .*id/iu.test(message);
|
|
1417
|
+
}
|
|
1418
|
+
async function railwayDeleteMutation({
|
|
1419
|
+
query,
|
|
1420
|
+
variables,
|
|
1421
|
+
env,
|
|
1422
|
+
fetchImpl,
|
|
1423
|
+
missingResult
|
|
1424
|
+
}) {
|
|
1425
|
+
try {
|
|
1426
|
+
await railwayGraphqlRequest({
|
|
1427
|
+
query,
|
|
1428
|
+
variables,
|
|
1429
|
+
env,
|
|
1430
|
+
fetchImpl
|
|
1431
|
+
});
|
|
1432
|
+
return { status: "deleted" };
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
if (looksLikeRailwayMissingResource(error)) {
|
|
1435
|
+
return missingResult;
|
|
1436
|
+
}
|
|
1437
|
+
throw error;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
async function deleteRailwayCustomDomain({
|
|
1441
|
+
domainId,
|
|
1442
|
+
env = process.env,
|
|
1443
|
+
fetchImpl = fetch
|
|
1444
|
+
}) {
|
|
1445
|
+
if (!railwayConnectionLabel(domainId)) {
|
|
1446
|
+
return { status: "missing", id: domainId };
|
|
1447
|
+
}
|
|
1448
|
+
const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_CUSTOM_DOMAIN_DELETE_MUTATION") || `
|
|
1449
|
+
mutation TreeseedRailwayCustomDomainDelete($id: String!) {
|
|
1450
|
+
customDomainDelete(id: $id)
|
|
1451
|
+
}
|
|
1452
|
+
`.trim();
|
|
1453
|
+
return railwayDeleteMutation({
|
|
1454
|
+
query: mutation,
|
|
1455
|
+
variables: { id: domainId },
|
|
1456
|
+
env,
|
|
1457
|
+
fetchImpl,
|
|
1458
|
+
missingResult: { status: "missing", id: domainId }
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
async function deleteRailwayVolume({
|
|
1462
|
+
volumeId,
|
|
1463
|
+
env = process.env,
|
|
1464
|
+
fetchImpl = fetch
|
|
1465
|
+
}) {
|
|
1466
|
+
if (!railwayConnectionLabel(volumeId)) {
|
|
1467
|
+
return { status: "missing", id: volumeId };
|
|
1468
|
+
}
|
|
1469
|
+
const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_VOLUME_DELETE_MUTATION") || `
|
|
1470
|
+
mutation TreeseedRailwayVolumeDelete($volumeId: String!) {
|
|
1471
|
+
volumeDelete(volumeId: $volumeId)
|
|
1472
|
+
}
|
|
1473
|
+
`.trim();
|
|
1474
|
+
return railwayDeleteMutation({
|
|
1475
|
+
query: mutation,
|
|
1476
|
+
variables: { volumeId },
|
|
1477
|
+
env,
|
|
1478
|
+
fetchImpl,
|
|
1479
|
+
missingResult: { status: "missing", id: volumeId }
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
async function deleteRailwayEnvironment({
|
|
1483
|
+
environmentId,
|
|
1484
|
+
env = process.env,
|
|
1485
|
+
fetchImpl = fetch
|
|
1486
|
+
}) {
|
|
1487
|
+
if (!railwayConnectionLabel(environmentId)) {
|
|
1488
|
+
return { status: "missing", id: environmentId };
|
|
1489
|
+
}
|
|
1490
|
+
const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_ENVIRONMENT_DELETE_MUTATION") || `
|
|
1491
|
+
mutation TreeseedRailwayEnvironmentDelete($id: String!) {
|
|
1492
|
+
environmentDelete(id: $id)
|
|
1493
|
+
}
|
|
1494
|
+
`.trim();
|
|
1495
|
+
return railwayDeleteMutation({
|
|
1496
|
+
query: mutation,
|
|
1497
|
+
variables: { id: environmentId },
|
|
1498
|
+
env,
|
|
1499
|
+
fetchImpl,
|
|
1500
|
+
missingResult: { status: "missing", id: environmentId }
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
async function deleteRailwayProject({
|
|
1504
|
+
projectId,
|
|
1505
|
+
env = process.env,
|
|
1506
|
+
fetchImpl = fetch
|
|
1507
|
+
}) {
|
|
1508
|
+
if (!railwayConnectionLabel(projectId)) {
|
|
1509
|
+
return { status: "missing", id: projectId };
|
|
1510
|
+
}
|
|
1511
|
+
const mutation = configuredEnvValue(env, "TREESEED_RAILWAY_PROJECT_DELETE_MUTATION") || `
|
|
1512
|
+
mutation TreeseedRailwayProjectDelete($id: String!) {
|
|
1513
|
+
projectDelete(id: $id)
|
|
1514
|
+
}
|
|
1515
|
+
`.trim();
|
|
1516
|
+
return railwayDeleteMutation({
|
|
1517
|
+
query: mutation,
|
|
1518
|
+
variables: { id: projectId },
|
|
1519
|
+
env,
|
|
1520
|
+
fetchImpl,
|
|
1521
|
+
missingResult: { status: "missing", id: projectId }
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1343
1524
|
export {
|
|
1525
|
+
deleteRailwayCustomDomain,
|
|
1526
|
+
deleteRailwayEnvironment,
|
|
1527
|
+
deleteRailwayProject,
|
|
1528
|
+
deleteRailwayVolume,
|
|
1344
1529
|
ensureRailwayCustomDomain,
|
|
1345
1530
|
ensureRailwayEnvironment,
|
|
1346
1531
|
ensureRailwayPostgresService,
|
|
@@ -14,10 +14,13 @@ export declare function buildRailwayDeployCommandEnv(env?: NodeJS.ProcessEnv): {
|
|
|
14
14
|
[key: string]: string | undefined;
|
|
15
15
|
};
|
|
16
16
|
export declare function isRailwayTransientFailure(result: any): boolean;
|
|
17
|
-
export declare function runRailway(args: any, { cwd, capture, allowFailure, input, env }?: {
|
|
17
|
+
export declare function runRailway(args: any, { cwd, capture, allowFailure, input, env, retryTransient, retryAttempts, retryDelayMs }?: {
|
|
18
18
|
capture?: boolean | undefined;
|
|
19
19
|
allowFailure?: boolean | undefined;
|
|
20
|
-
|
|
20
|
+
retryTransient?: boolean | undefined;
|
|
21
|
+
retryAttempts?: number | undefined;
|
|
22
|
+
retryDelayMs?: number | undefined;
|
|
23
|
+
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
21
24
|
export declare function waitForRailwayManagedDeploymentsSettled(tenantRoot: any, scope: any, { services, env, timeoutMs, pollMs, onProgress, }?: {
|
|
22
25
|
services?: unknown[] | undefined;
|
|
23
26
|
env?: NodeJS.ProcessEnv | undefined;
|
|
@@ -53,11 +56,11 @@ export declare function ensureRailwayEnvironmentExists(service: any, { env }?: {
|
|
|
53
56
|
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
54
57
|
export declare function ensureRailwayServiceExists(service: any, { env }?: {
|
|
55
58
|
env?: NodeJS.ProcessEnv | undefined;
|
|
56
|
-
}): import("child_process").SpawnSyncReturns<string
|
|
59
|
+
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
57
60
|
export declare function ensureRailwayDatabaseServiceExists(service: any, { database, env, }?: {
|
|
58
61
|
database?: string | undefined;
|
|
59
62
|
env?: NodeJS.ProcessEnv | undefined;
|
|
60
|
-
}): import("child_process").SpawnSyncReturns<string
|
|
63
|
+
}): import("child_process").SpawnSyncReturns<string> | null;
|
|
61
64
|
export declare function ensureRailwayProjectContext(service: any, { env, allowFailure, capture }?: {
|
|
62
65
|
env?: NodeJS.ProcessEnv | undefined;
|
|
63
66
|
allowFailure?: boolean | undefined;
|
|
@@ -141,6 +144,23 @@ export declare function planRailwayServiceLink(service: any, { env }?: {
|
|
|
141
144
|
args: string[];
|
|
142
145
|
cwd: any;
|
|
143
146
|
};
|
|
147
|
+
export declare function ensureRailwayServiceVolumeWithCliFallback({ tenantRoot, projectId, environmentId, environmentName, serviceId, serviceName, name, mountPath, preferCli, env, }: {
|
|
148
|
+
tenantRoot: any;
|
|
149
|
+
projectId: any;
|
|
150
|
+
environmentId: any;
|
|
151
|
+
environmentName: any;
|
|
152
|
+
serviceId: any;
|
|
153
|
+
serviceName: any;
|
|
154
|
+
name: any;
|
|
155
|
+
mountPath: any;
|
|
156
|
+
preferCli?: boolean | undefined;
|
|
157
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
158
|
+
}): Promise<{
|
|
159
|
+
volume: any;
|
|
160
|
+
instance: any;
|
|
161
|
+
created: boolean;
|
|
162
|
+
updated: boolean;
|
|
163
|
+
}>;
|
|
144
164
|
export declare function deployRailwayService(tenantRoot: any, service: any, { dryRun, write, prefix, env, }?: {
|
|
145
165
|
dryRun?: boolean;
|
|
146
166
|
write?: TreeseedBootstrapWriter;
|
|
@@ -332,7 +332,7 @@ function isRailwayTransientFailure(result) {
|
|
|
332
332
|
if (!message.trim() && result?.status === 1) {
|
|
333
333
|
return true;
|
|
334
334
|
}
|
|
335
|
-
return /timed out|failed to fetch|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(message);
|
|
335
|
+
return /timed out|failed to fetch|error decoding response body|expected value at line 1 column 1|temporarily unavailable|econnreset|etimedout|failed to stream build logs|failed to retrieve build log/iu.test(message);
|
|
336
336
|
}
|
|
337
337
|
function sleepSync(milliseconds) {
|
|
338
338
|
if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
@@ -412,7 +412,7 @@ mutation TreeseedScheduleUpdate($id: String!, $name: String!, $schedule: String!
|
|
|
412
412
|
`.trim()
|
|
413
413
|
};
|
|
414
414
|
}
|
|
415
|
-
function runRailway(args, { cwd, capture = false, allowFailure = false, input, env } = {}) {
|
|
415
|
+
function runRailway(args, { cwd, capture = false, allowFailure = false, input, env, retryTransient = true, retryAttempts = 3, retryDelayMs = 2e3 } = {}) {
|
|
416
416
|
const effectiveEnv = buildRailwayCommandEnv({ ...process.env, ...env ?? {} });
|
|
417
417
|
const railway = resolveTreeseedToolCommand("railway", { env: effectiveEnv });
|
|
418
418
|
if (!railway) {
|
|
@@ -425,8 +425,16 @@ function runRailway(args, { cwd, capture = false, allowFailure = false, input, e
|
|
|
425
425
|
env: spawnEnv,
|
|
426
426
|
input
|
|
427
427
|
});
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
let result = null;
|
|
429
|
+
const maxAttempts = retryTransient && !allowFailure ? Math.max(1, Number(retryAttempts) || 1) : 1;
|
|
430
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
431
|
+
result = runWithEnv(effectiveEnv);
|
|
432
|
+
if (result.status === 0 || allowFailure || !isRailwayTransientFailure(result) || attempt === maxAttempts) {
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
sleepSync((Number(retryDelayMs) || 0) * attempt);
|
|
436
|
+
}
|
|
437
|
+
if (result?.status !== 0 && !allowFailure) {
|
|
430
438
|
throw new Error(result.stderr?.trim() || result.stdout?.trim() || `railway ${args.join(" ")} failed`);
|
|
431
439
|
}
|
|
432
440
|
return result;
|
|
@@ -1917,6 +1925,7 @@ export {
|
|
|
1917
1925
|
ensureRailwayProjectExists,
|
|
1918
1926
|
ensureRailwayScheduledJobs,
|
|
1919
1927
|
ensureRailwayServiceExists,
|
|
1928
|
+
ensureRailwayServiceVolumeWithCliFallback,
|
|
1920
1929
|
isRailwayTransientFailure,
|
|
1921
1930
|
isUsableRailwayToken,
|
|
1922
1931
|
planRailwayServiceDeploy,
|
|
@@ -68,6 +68,9 @@ function platformSurfaceEnabled(context, surface) {
|
|
|
68
68
|
}
|
|
69
69
|
function activeWorkflowPlane() {
|
|
70
70
|
const plane = process.env.TREESEED_WORKFLOW_PLANE;
|
|
71
|
+
if (plane === "all") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
71
74
|
return plane === "web" || plane === "processing" ? plane : null;
|
|
72
75
|
}
|
|
73
76
|
function workflowPlaneAllows(plane) {
|