@treeseed/sdk 0.12.18 → 0.12.20
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/operations/services/hosted-service-checks.d.ts +1 -0
- package/dist/operations/services/hosted-service-checks.js +4 -3
- package/dist/operations/services/live-hosted-service-checks.js +5 -4
- package/dist/reconcile/builtin-adapters.js +10 -2
- package/dist/reconcile/desired-state.js +8 -1
- package/dist/workflow/operations.d.ts +1 -1
- package/dist/workflow/operations.js +89 -5
- package/package.json +1 -1
|
@@ -65,6 +65,7 @@ export interface TreeseedHostedServiceCheckOptions {
|
|
|
65
65
|
appId?: string;
|
|
66
66
|
serviceKeys?: string[];
|
|
67
67
|
now?: Date;
|
|
68
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
68
69
|
valuesOverlay?: Record<string, string | undefined>;
|
|
69
70
|
observedRailwayServices?: Record<string, TreeseedObservedRailwayServiceState | undefined>;
|
|
70
71
|
httpChecks?: Record<string, {
|
|
@@ -145,7 +145,8 @@ function httpStatus(url, options) {
|
|
|
145
145
|
function collectTreeseedHostedServiceChecks(options) {
|
|
146
146
|
const target = options.target ?? "prod";
|
|
147
147
|
const tenantRoot = options.tenantRoot;
|
|
148
|
-
const
|
|
148
|
+
const configEnv = { ...process.env, ...options.env ?? {} };
|
|
149
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: target, env: configEnv }).deployConfig;
|
|
149
150
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
150
151
|
let machineValues = {};
|
|
151
152
|
try {
|
|
@@ -153,7 +154,7 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
153
154
|
} catch {
|
|
154
155
|
machineValues = {};
|
|
155
156
|
}
|
|
156
|
-
const values = { ...machineValues, ...options.valuesOverlay ?? {} };
|
|
157
|
+
const values = { ...machineValues, ...configEnv, ...options.valuesOverlay ?? {} };
|
|
157
158
|
const checks = [];
|
|
158
159
|
const selectedServiceKeys = new Set((options.serviceKeys ?? []).map((key) => key.trim()).filter(Boolean));
|
|
159
160
|
const selectedAppId = options.appId?.trim() || null;
|
|
@@ -211,7 +212,7 @@ function collectTreeseedHostedServiceChecks(options) {
|
|
|
211
212
|
}));
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
|
-
const configuredServices = configuredRailwayServices(tenantRoot, target).filter((service) => !selectedAppId || service.application?.id === selectedAppId).filter((service) => selectedServiceKeys.size === 0 || selectedServiceKeys.has(service.key));
|
|
215
|
+
const configuredServices = configuredRailwayServices(tenantRoot, target, configEnv).filter((service) => !selectedAppId || service.application?.id === selectedAppId).filter((service) => selectedServiceKeys.size === 0 || selectedServiceKeys.has(service.key));
|
|
215
216
|
for (const service of configuredServices) {
|
|
216
217
|
const serviceType = serviceTypeFor(service.key);
|
|
217
218
|
const observed = observedFor(options, service.serviceName);
|
|
@@ -218,7 +218,7 @@ async function collectRailwayObservations(options) {
|
|
|
218
218
|
try {
|
|
219
219
|
const workspace = await resolveRailwayWorkspaceContext({ env: options.env, fetchImpl: options.fetchImpl });
|
|
220
220
|
const projects = await listRailwayProjects({ workspaceId: workspace.id, env: options.env, fetchImpl: options.fetchImpl });
|
|
221
|
-
const configuredServices = configuredRailwayServices(options.tenantRoot, options.target).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key));
|
|
221
|
+
const configuredServices = configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key));
|
|
222
222
|
if (selectedServiceKeys.size === 0 || selectedServiceKeys.has("api") || selectedServiceKeys.has("operationsRunner")) {
|
|
223
223
|
for (const descriptor of treeseedDatabaseDescriptors(options.tenantRoot, options)) {
|
|
224
224
|
await verifyRailwayPostgresTopology({ descriptor, configuredServices, projects, options, issues });
|
|
@@ -347,7 +347,7 @@ async function collectRailwayObservations(options) {
|
|
|
347
347
|
};
|
|
348
348
|
}
|
|
349
349
|
if (selectedServiceKeys.size === 0) {
|
|
350
|
-
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot: options.tenantRoot, environment: options.target, env:
|
|
350
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot: options.tenantRoot, environment: options.target, env: options.env }).deployConfig;
|
|
351
351
|
const appConfigs = [
|
|
352
352
|
...!options.appId || options.appId === "web" ? [deployConfig] : [],
|
|
353
353
|
...discoverTreeseedApplications(options.tenantRoot).filter((application) => application.id === "api" && (!options.appId || options.appId === "api")).map((application) => application.config)
|
|
@@ -418,7 +418,7 @@ async function collectRailwayObservations(options) {
|
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
420
|
async function collectHttpObservations(options) {
|
|
421
|
-
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot: options.tenantRoot, environment: options.target, env:
|
|
421
|
+
const deployConfig = loadTreeseedPlatformConfig({ tenantRoot: options.tenantRoot, environment: options.target, env: options.env }).deployConfig;
|
|
422
422
|
const urls = /* @__PURE__ */ new Set();
|
|
423
423
|
const fallbacks = /* @__PURE__ */ new Map();
|
|
424
424
|
const selectedServiceKeys = selectedServiceKeySet(options);
|
|
@@ -440,7 +440,7 @@ async function collectHttpObservations(options) {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
|
-
for (const service of configuredRailwayServices(options.tenantRoot, options.target).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key))) {
|
|
443
|
+
for (const service of configuredRailwayServices(options.tenantRoot, options.target, options.env).filter((entry) => !options.appId || entry.application?.id === options.appId).filter((entry) => serviceIsSelected(selectedServiceKeys, entry.key))) {
|
|
444
444
|
const serviceConfig = deployConfig.services?.[service.key];
|
|
445
445
|
const domain = service.publicBaseUrl ?? serviceConfig?.environments?.[options.target]?.baseUrl ?? serviceConfig?.environments?.[options.target]?.domain ?? (service.key === "api" ? deployConfig.surfaces?.api?.environments?.[options.target]?.domain : null);
|
|
446
446
|
const baseUrl = urlForDomain(domain);
|
|
@@ -500,6 +500,7 @@ async function collectTreeseedLiveHostedServiceChecks(options) {
|
|
|
500
500
|
target: effectiveOptions.target,
|
|
501
501
|
appId: effectiveOptions.appId,
|
|
502
502
|
serviceKeys: effectiveOptions.serviceKeys,
|
|
503
|
+
env: effectiveOptions.env,
|
|
503
504
|
observedRailwayServices: railway.observed,
|
|
504
505
|
httpChecks
|
|
505
506
|
});
|
|
@@ -3308,12 +3308,20 @@ async function resolveRailwayTopologyForScope(input, scope, {
|
|
|
3308
3308
|
includeInstances = ensure,
|
|
3309
3309
|
includeVariables = false
|
|
3310
3310
|
} = {}) {
|
|
3311
|
+
const imageRefValues = resolveReconcileEnvironmentValues(input, scope);
|
|
3312
|
+
const imageRefFingerprint = [
|
|
3313
|
+
"TREESEED_API_IMAGE_REF",
|
|
3314
|
+
"TREESEED_OPERATIONS_RUNNER_IMAGE_REF",
|
|
3315
|
+
"TREESEED_AGENT_MANAGER_IMAGE_REF",
|
|
3316
|
+
"TREESEED_AGENT_RUNNER_IMAGE_REF",
|
|
3317
|
+
"TREESEED_PUBLIC_TREEDX_IMAGE_REF"
|
|
3318
|
+
].map((key) => `${key}=${imageRefValues[key] ?? ""}`).join("|");
|
|
3311
3319
|
const normalizedServiceKeys = Array.isArray(serviceKeys) && serviceKeys.length > 0 ? [...new Set(serviceKeys.map((value) => String(value).trim()).filter(Boolean))].sort() : ["__all__"];
|
|
3312
|
-
const cacheKey = `railway:topology:${scope}:${ensure ? "ensure" : "observe"}:${includeInstances ? "instances" : "no-instances"}:${includeVariables ? "variables" : "no-variables"}:${normalizedServiceKeys.join(",")}`;
|
|
3320
|
+
const cacheKey = `railway:topology:${scope}:${ensure ? "ensure" : "observe"}:${includeInstances ? "instances" : "no-instances"}:${includeVariables ? "variables" : "no-variables"}:${normalizedServiceKeys.join(",")}:${imageRefFingerprint}`;
|
|
3313
3321
|
return await providerCache(input, cacheKey, async () => {
|
|
3314
3322
|
const env = buildRailwayEnv(input, scope);
|
|
3315
3323
|
const deployState = loadDeployState(input.context.tenantRoot, input.context.deployConfig, { target: toDeployTarget(input.context.target) });
|
|
3316
|
-
const services =
|
|
3324
|
+
const services = configuredRailwayServicesForInput(input, scope).filter((service) => normalizedServiceKeys.includes("__all__") || normalizedServiceKeys.includes(service.key));
|
|
3317
3325
|
traceRailwayReconcile(env, "topology:start", `scope=${scope} ensure=${ensure ? "yes" : "no"} services=${services.map((service) => service.key).join(",")}`);
|
|
3318
3326
|
let workspace = null;
|
|
3319
3327
|
const knownProjects = [];
|
|
@@ -190,7 +190,7 @@ function deriveTreeseedDesiredUnits({
|
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
const scope = target.kind === "persistent" ? target.scope : "staging";
|
|
193
|
-
for (const configuredService of configuredRailwayServices(tenantRoot, scope)) {
|
|
193
|
+
for (const configuredService of configuredRailwayServices(tenantRoot, scope, env)) {
|
|
194
194
|
const serviceKey = configuredService.key;
|
|
195
195
|
const service = configuredService.serviceConfig ?? deployConfig.services?.[serviceKey];
|
|
196
196
|
const serviceState = legacyState.services?.[serviceKey];
|
|
@@ -217,6 +217,13 @@ function deriveTreeseedDesiredUnits({
|
|
|
217
217
|
serviceId: persistedServiceMatchesDesired ? serviceState?.serviceId ?? configuredService.serviceId : configuredService.serviceId,
|
|
218
218
|
serviceName: desiredServiceName,
|
|
219
219
|
rootDir: configuredService.rootDir ?? serviceState?.rootDir,
|
|
220
|
+
imageRef: configuredService.imageRef,
|
|
221
|
+
imageRefEnv: configuredService.serviceConfig?.railway?.imageRefEnv,
|
|
222
|
+
sourceMode: configuredService.sourceMode,
|
|
223
|
+
sourceRepo: configuredService.sourceRepo,
|
|
224
|
+
sourceBranch: configuredService.sourceBranch,
|
|
225
|
+
sourceCommit: configuredService.sourceCommit,
|
|
226
|
+
sourceRootDirectory: configuredService.sourceRootDirectory,
|
|
220
227
|
environment: normalizeRailwayEnvironmentName(configuredService.railwayEnvironment ?? serviceState?.environment),
|
|
221
228
|
buildCommand: configuredService.buildCommand,
|
|
222
229
|
startCommand: configuredService.startCommand,
|
|
@@ -91,7 +91,7 @@ export declare function workflowReleaseCandidate(helpers: WorkflowOperationHelpe
|
|
|
91
91
|
}>>;
|
|
92
92
|
type PublishedArtifactCheck = {
|
|
93
93
|
id: string;
|
|
94
|
-
kind: 'npm' | 'docker' | 'pypi' | 'crates' | 'hex';
|
|
94
|
+
kind: 'npm' | 'docker' | 'pypi' | 'crates' | 'hex' | 'github-tag';
|
|
95
95
|
name: string;
|
|
96
96
|
version: string;
|
|
97
97
|
url: string;
|
|
@@ -534,7 +534,7 @@ function selectorFromWorkflowHostingGraph(graph) {
|
|
|
534
534
|
}))]
|
|
535
535
|
};
|
|
536
536
|
}
|
|
537
|
-
async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId, operation = "save", envOverlay = {}) {
|
|
537
|
+
async function reconcileSaveHostedEnvironment(root, environment, helpers, workflowRunId, operation = "save", envOverlay = {}, options = {}) {
|
|
538
538
|
const target = createPersistentDeployTarget(environment);
|
|
539
539
|
const env = {
|
|
540
540
|
...helpers.context.env,
|
|
@@ -585,6 +585,7 @@ ${status.blockers.join("\n")}`, {
|
|
|
585
585
|
const live = await collectTreeseedLiveHostedServiceChecks({
|
|
586
586
|
tenantRoot: root,
|
|
587
587
|
target: environment,
|
|
588
|
+
appId: options.liveAppId,
|
|
588
589
|
strict: true,
|
|
589
590
|
requireLiveRailway: true,
|
|
590
591
|
requireLiveHttp: true,
|
|
@@ -2576,6 +2577,46 @@ async function verifyNpmArtifact(packageName, version) {
|
|
|
2576
2577
|
};
|
|
2577
2578
|
}
|
|
2578
2579
|
}
|
|
2580
|
+
async function fetchDockerRegistryManifestStatus(image, version) {
|
|
2581
|
+
const [namespace, repository] = image.split("/");
|
|
2582
|
+
if (!namespace || !repository) {
|
|
2583
|
+
return { ok: false, status: null, message: `Invalid Docker image name ${image}.` };
|
|
2584
|
+
}
|
|
2585
|
+
const tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${namespace}/${repository}:pull`;
|
|
2586
|
+
try {
|
|
2587
|
+
const tokenResponse = await fetchJsonForArtifact(tokenUrl);
|
|
2588
|
+
const token = typeof stringRecord(tokenResponse.json)?.token === "string" ? String(stringRecord(tokenResponse.json)?.token) : "";
|
|
2589
|
+
if (!tokenResponse.ok || !token) {
|
|
2590
|
+
return { ok: false, status: tokenResponse.status, message: `Docker registry token request failed for ${image}.` };
|
|
2591
|
+
}
|
|
2592
|
+
const controller = new AbortController();
|
|
2593
|
+
const timeout = setTimeout(() => controller.abort(), 2e4);
|
|
2594
|
+
try {
|
|
2595
|
+
const manifestResponse = await fetch(`https://registry-1.docker.io/v2/${namespace}/${repository}/manifests/${version}`, {
|
|
2596
|
+
method: "HEAD",
|
|
2597
|
+
headers: {
|
|
2598
|
+
accept: [
|
|
2599
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
2600
|
+
"application/vnd.oci.image.index.v1+json",
|
|
2601
|
+
"application/vnd.docker.distribution.manifest.v2+json"
|
|
2602
|
+
].join(", "),
|
|
2603
|
+
authorization: `Bearer ${token}`,
|
|
2604
|
+
"user-agent": "treeseed-release-verifier/1.0 (https://treeseed.dev)"
|
|
2605
|
+
},
|
|
2606
|
+
signal: controller.signal
|
|
2607
|
+
});
|
|
2608
|
+
return {
|
|
2609
|
+
ok: manifestResponse.ok,
|
|
2610
|
+
status: manifestResponse.status,
|
|
2611
|
+
...manifestResponse.ok ? {} : { message: `Docker registry manifest for ${image}:${version} is not pullable yet.` }
|
|
2612
|
+
};
|
|
2613
|
+
} finally {
|
|
2614
|
+
clearTimeout(timeout);
|
|
2615
|
+
}
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
return { ok: false, status: null, message: error instanceof Error ? error.message : String(error) };
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2579
2620
|
async function verifyDockerHubArtifact(image, version) {
|
|
2580
2621
|
const [namespace, repository] = image.split("/");
|
|
2581
2622
|
const url = `https://hub.docker.com/v2/repositories/${namespace}/${repository}/tags/${version}`;
|
|
@@ -2583,7 +2624,8 @@ async function verifyDockerHubArtifact(image, version) {
|
|
|
2583
2624
|
const response = await fetchJsonForArtifact(url);
|
|
2584
2625
|
const images = Array.isArray(stringRecord(response.json)?.images) ? stringRecord(response.json)?.images : [];
|
|
2585
2626
|
const architectures = new Set(images.map((entry) => stringRecord(entry)).map((entry) => typeof entry?.architecture === "string" ? entry.architecture : null).filter((entry) => Boolean(entry)));
|
|
2586
|
-
const
|
|
2627
|
+
const registry = await fetchDockerRegistryManifestStatus(image, version);
|
|
2628
|
+
const ok = response.ok && architectures.has("amd64") && architectures.has("arm64") && registry.ok;
|
|
2587
2629
|
return {
|
|
2588
2630
|
id: `docker:${image}:${version}`,
|
|
2589
2631
|
kind: "docker",
|
|
@@ -2591,8 +2633,8 @@ async function verifyDockerHubArtifact(image, version) {
|
|
|
2591
2633
|
version,
|
|
2592
2634
|
url,
|
|
2593
2635
|
ok,
|
|
2594
|
-
status: response.status,
|
|
2595
|
-
...ok ? {} : { message: `${image}:${version} was not found on Docker Hub with amd64 and arm64 images.` }
|
|
2636
|
+
status: registry.status ?? response.status,
|
|
2637
|
+
...ok ? {} : { message: registry.message ?? `${image}:${version} was not found on Docker Hub with amd64 and arm64 images.` }
|
|
2596
2638
|
};
|
|
2597
2639
|
} catch (error) {
|
|
2598
2640
|
return {
|
|
@@ -2607,6 +2649,33 @@ async function verifyDockerHubArtifact(image, version) {
|
|
|
2607
2649
|
};
|
|
2608
2650
|
}
|
|
2609
2651
|
}
|
|
2652
|
+
async function verifyGitHubTagArtifact(repository, version) {
|
|
2653
|
+
const url = `https://api.github.com/repos/${repository}/git/ref/tags/${encodeURIComponent(version)}`;
|
|
2654
|
+
try {
|
|
2655
|
+
const response = await fetchJsonForArtifact(url);
|
|
2656
|
+
return {
|
|
2657
|
+
id: `github-tag:${repository}:${version}`,
|
|
2658
|
+
kind: "github-tag",
|
|
2659
|
+
name: repository,
|
|
2660
|
+
version,
|
|
2661
|
+
url,
|
|
2662
|
+
ok: response.ok,
|
|
2663
|
+
status: response.status,
|
|
2664
|
+
...response.ok ? {} : { message: `GitHub tag ${repository}@${version} was not found.` }
|
|
2665
|
+
};
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
return {
|
|
2668
|
+
id: `github-tag:${repository}:${version}`,
|
|
2669
|
+
kind: "github-tag",
|
|
2670
|
+
name: repository,
|
|
2671
|
+
version,
|
|
2672
|
+
url,
|
|
2673
|
+
ok: false,
|
|
2674
|
+
status: null,
|
|
2675
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2610
2679
|
async function verifySimpleRegistryArtifact(input) {
|
|
2611
2680
|
try {
|
|
2612
2681
|
const response = await fetchJsonForArtifact(input.url);
|
|
@@ -2635,6 +2704,21 @@ async function verifySimpleRegistryArtifact(input) {
|
|
|
2635
2704
|
}
|
|
2636
2705
|
async function collectPublishedReleaseArtifactChecks(selectedVersions) {
|
|
2637
2706
|
const checks = [];
|
|
2707
|
+
const githubRepositories = {
|
|
2708
|
+
"@treeseed/sdk": "treeseed-ai/sdk",
|
|
2709
|
+
"@treeseed/ui": "treeseed-ai/ui",
|
|
2710
|
+
"@treeseed/core": "treeseed-ai/core",
|
|
2711
|
+
"@treeseed/admin": "treeseed-ai/admin",
|
|
2712
|
+
"@treeseed/cli": "treeseed-ai/cli",
|
|
2713
|
+
"@treeseed/agent": "treeseed-ai/agent",
|
|
2714
|
+
"@treeseed/api": "treeseed-ai/api",
|
|
2715
|
+
treedx: "treeseed-ai/treedx",
|
|
2716
|
+
"@treeseed/treedx": "treeseed-ai/treedx"
|
|
2717
|
+
};
|
|
2718
|
+
for (const [packageName, repository] of Object.entries(githubRepositories)) {
|
|
2719
|
+
const version = selectedVersions.get(packageName);
|
|
2720
|
+
if (version) checks.push(await verifyGitHubTagArtifact(repository, version));
|
|
2721
|
+
}
|
|
2638
2722
|
const npmPackages = ["@treeseed/sdk", "@treeseed/ui", "@treeseed/core", "@treeseed/admin", "@treeseed/cli", "@treeseed/agent"];
|
|
2639
2723
|
for (const packageName of npmPackages) {
|
|
2640
2724
|
const version = selectedVersions.get(packageName);
|
|
@@ -5528,7 +5612,7 @@ ${rendered}`);
|
|
|
5528
5612
|
packageReleases.push(packageRelease);
|
|
5529
5613
|
}
|
|
5530
5614
|
const publishedArtifacts = await executeJournalStep(root, workflowRun.runId, "verify-published-artifacts", () => verifyPublishedReleaseArtifacts(selectedVersions));
|
|
5531
|
-
const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", productionReleaseImageRefEnv(selectedVersions)));
|
|
5615
|
+
const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", productionReleaseImageRefEnv(selectedVersions), { liveAppId: "api" }));
|
|
5532
5616
|
const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
|
|
5533
5617
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
5534
5618
|
const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
|