@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.
@@ -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 deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: target, env: process.env }).deployConfig;
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: process.env }).deployConfig;
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: process.env }).deployConfig;
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 = configuredRailwayServices(input.context.tenantRoot, scope).filter((service) => normalizedServiceKeys.includes("__all__") || normalizedServiceKeys.includes(service.key));
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 ok = response.ok && architectures.has("amd64") && architectures.has("arm64");
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {