@treeseed/sdk 0.12.17 → 0.12.19

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.
@@ -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,
@@ -12,7 +12,7 @@ import type { TreeseedCloseInput, TreeseedCiInput, TreeseedConfigInput, Treeseed
12
12
  type WorkflowWrite = NonNullable<TreeseedWorkflowContext['write']>;
13
13
  type WorkflowStatePayload = ReturnType<typeof resolveTreeseedWorkflowState>;
14
14
  type ReleaseCandidateMode = TreeseedReleaseCandidateMode;
15
- export type TreeseedWorkflowErrorCode = 'validation_failed' | 'merge_conflict' | 'missing_runtime_auth' | 'deployment_timeout' | 'confirmation_required' | 'unsupported_transport' | 'unsupported_state' | 'workflow_locked' | 'resume_unavailable' | 'workflow_contract_missing' | 'github_workflow_failed' | 'github_auth_unavailable' | 'hosted_reconcile_failed' | 'hosted_live_verification_failed';
15
+ export type TreeseedWorkflowErrorCode = 'validation_failed' | 'merge_conflict' | 'missing_runtime_auth' | 'deployment_timeout' | 'confirmation_required' | 'unsupported_transport' | 'unsupported_state' | 'workflow_locked' | 'resume_unavailable' | 'workflow_contract_missing' | 'github_workflow_failed' | 'github_auth_unavailable' | 'release_gate_failed' | 'hosted_reconcile_failed' | 'hosted_live_verification_failed';
16
16
  export declare class TreeseedWorkflowError extends Error {
17
17
  code: TreeseedWorkflowErrorCode;
18
18
  operation: TreeseedWorkflowOperationId;
@@ -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;
@@ -1153,6 +1153,20 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
1153
1153
  liveVerification: import("../workflow-support.js").TreeseedLiveHostedServiceCheckReport;
1154
1154
  reason?: undefined;
1155
1155
  };
1156
+ productionApiGuarantees: {
1157
+ ok: true;
1158
+ environment: string;
1159
+ runId: string;
1160
+ outputRoot: string;
1161
+ counts: {
1162
+ planned: number;
1163
+ passed: number;
1164
+ failed: number;
1165
+ skipped: number;
1166
+ blocked: number;
1167
+ releaseBlockingFailures: number;
1168
+ };
1169
+ };
1156
1170
  backMerge: {
1157
1171
  packages: {
1158
1172
  status: string;
@@ -159,6 +159,7 @@ import {
159
159
  } from "./session.js";
160
160
  import { checkedOutManagedWorkflowRepos } from "../operations/services/managed-repositories.js";
161
161
  import { runTreeseedLocalCleanup } from "../operations/services/local-cleanup.js";
162
+ import { runTreeseedGuarantees } from "../guarantees/index.js";
162
163
  import {
163
164
  classifyTreeseedBranchRole,
164
165
  resolveTreeseedWorkflowPaths
@@ -633,6 +634,49 @@ function productionReleaseImageRefEnv(selectedVersions) {
633
634
  }
634
635
  return refs;
635
636
  }
637
+ async function runReleaseApiGuarantees(root, environment, helpers, operation, sceneArtifacts) {
638
+ const env = {
639
+ ...helpers.context.env,
640
+ ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env)
641
+ };
642
+ env.TREESEED_ACCEPTANCE_SERVICE_ID ??= env.TREESEED_API_WEB_SERVICE_ID ?? env.TREESEED_WEB_SERVICE_ID;
643
+ env.TREESEED_ACCEPTANCE_SERVICE_SECRET ??= env.TREESEED_API_WEB_SERVICE_SECRET ?? env.TREESEED_WEB_SERVICE_SECRET;
644
+ if (!env.TREESEED_ACCEPTANCE_SERVICE_ID || !env.TREESEED_ACCEPTANCE_SERVICE_SECRET) {
645
+ workflowError(operation, "release_gate_failed", `${environment} API release guarantees cannot run because API acceptance service credentials are missing.`, {
646
+ details: {
647
+ environment,
648
+ missing: [
649
+ !env.TREESEED_ACCEPTANCE_SERVICE_ID ? "TREESEED_ACCEPTANCE_SERVICE_ID" : null,
650
+ !env.TREESEED_ACCEPTANCE_SERVICE_SECRET ? "TREESEED_ACCEPTANCE_SERVICE_SECRET" : null
651
+ ].filter((value) => Boolean(value))
652
+ }
653
+ });
654
+ }
655
+ helpers.write(`[${operation}][workflow] Running ${environment} API release guarantees before root deployment.`);
656
+ return await withContextEnv(env, async () => {
657
+ const report = await runTreeseedGuarantees({
658
+ workspaceRoot: root,
659
+ filter: { ownerPackage: "@treeseed/api" },
660
+ environment,
661
+ evidenceTarget: "release",
662
+ sceneArtifacts
663
+ });
664
+ if (!report.ok) {
665
+ const diagnostics = report.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20).map((entry) => `${entry.code}: ${entry.message}${entry.sourcePath ? ` (${entry.sourcePath})` : ""}`);
666
+ workflowError(operation, "release_gate_failed", `API release guarantees for ${environment} failed:
667
+ ${diagnostics.join("\n") || `See ${report.outputRoot}`}`, {
668
+ details: { environment, outputRoot: report.outputRoot, counts: report.counts, diagnostics: report.diagnostics }
669
+ });
670
+ }
671
+ return {
672
+ ok: report.ok,
673
+ environment: report.environment,
674
+ runId: report.runId,
675
+ outputRoot: report.outputRoot,
676
+ counts: report.counts
677
+ };
678
+ });
679
+ }
636
680
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
637
681
  const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
638
682
  const releaseRecord = stringRecord(rootRelease) ?? {};
@@ -2532,6 +2576,46 @@ async function verifyNpmArtifact(packageName, version) {
2532
2576
  };
2533
2577
  }
2534
2578
  }
2579
+ async function fetchDockerRegistryManifestStatus(image, version) {
2580
+ const [namespace, repository] = image.split("/");
2581
+ if (!namespace || !repository) {
2582
+ return { ok: false, status: null, message: `Invalid Docker image name ${image}.` };
2583
+ }
2584
+ const tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${namespace}/${repository}:pull`;
2585
+ try {
2586
+ const tokenResponse = await fetchJsonForArtifact(tokenUrl);
2587
+ const token = typeof stringRecord(tokenResponse.json)?.token === "string" ? String(stringRecord(tokenResponse.json)?.token) : "";
2588
+ if (!tokenResponse.ok || !token) {
2589
+ return { ok: false, status: tokenResponse.status, message: `Docker registry token request failed for ${image}.` };
2590
+ }
2591
+ const controller = new AbortController();
2592
+ const timeout = setTimeout(() => controller.abort(), 2e4);
2593
+ try {
2594
+ const manifestResponse = await fetch(`https://registry-1.docker.io/v2/${namespace}/${repository}/manifests/${version}`, {
2595
+ method: "HEAD",
2596
+ headers: {
2597
+ accept: [
2598
+ "application/vnd.docker.distribution.manifest.list.v2+json",
2599
+ "application/vnd.oci.image.index.v1+json",
2600
+ "application/vnd.docker.distribution.manifest.v2+json"
2601
+ ].join(", "),
2602
+ authorization: `Bearer ${token}`,
2603
+ "user-agent": "treeseed-release-verifier/1.0 (https://treeseed.dev)"
2604
+ },
2605
+ signal: controller.signal
2606
+ });
2607
+ return {
2608
+ ok: manifestResponse.ok,
2609
+ status: manifestResponse.status,
2610
+ ...manifestResponse.ok ? {} : { message: `Docker registry manifest for ${image}:${version} is not pullable yet.` }
2611
+ };
2612
+ } finally {
2613
+ clearTimeout(timeout);
2614
+ }
2615
+ } catch (error) {
2616
+ return { ok: false, status: null, message: error instanceof Error ? error.message : String(error) };
2617
+ }
2618
+ }
2535
2619
  async function verifyDockerHubArtifact(image, version) {
2536
2620
  const [namespace, repository] = image.split("/");
2537
2621
  const url = `https://hub.docker.com/v2/repositories/${namespace}/${repository}/tags/${version}`;
@@ -2539,7 +2623,8 @@ async function verifyDockerHubArtifact(image, version) {
2539
2623
  const response = await fetchJsonForArtifact(url);
2540
2624
  const images = Array.isArray(stringRecord(response.json)?.images) ? stringRecord(response.json)?.images : [];
2541
2625
  const architectures = new Set(images.map((entry) => stringRecord(entry)).map((entry) => typeof entry?.architecture === "string" ? entry.architecture : null).filter((entry) => Boolean(entry)));
2542
- const ok = response.ok && architectures.has("amd64") && architectures.has("arm64");
2626
+ const registry = await fetchDockerRegistryManifestStatus(image, version);
2627
+ const ok = response.ok && architectures.has("amd64") && architectures.has("arm64") && registry.ok;
2543
2628
  return {
2544
2629
  id: `docker:${image}:${version}`,
2545
2630
  kind: "docker",
@@ -2547,8 +2632,8 @@ async function verifyDockerHubArtifact(image, version) {
2547
2632
  version,
2548
2633
  url,
2549
2634
  ok,
2550
- status: response.status,
2551
- ...ok ? {} : { message: `${image}:${version} was not found on Docker Hub with amd64 and arm64 images.` }
2635
+ status: registry.status ?? response.status,
2636
+ ...ok ? {} : { message: registry.message ?? `${image}:${version} was not found on Docker Hub with amd64 and arm64 images.` }
2552
2637
  };
2553
2638
  } catch (error) {
2554
2639
  return {
@@ -2563,6 +2648,33 @@ async function verifyDockerHubArtifact(image, version) {
2563
2648
  };
2564
2649
  }
2565
2650
  }
2651
+ async function verifyGitHubTagArtifact(repository, version) {
2652
+ const url = `https://api.github.com/repos/${repository}/git/ref/tags/${encodeURIComponent(version)}`;
2653
+ try {
2654
+ const response = await fetchJsonForArtifact(url);
2655
+ return {
2656
+ id: `github-tag:${repository}:${version}`,
2657
+ kind: "github-tag",
2658
+ name: repository,
2659
+ version,
2660
+ url,
2661
+ ok: response.ok,
2662
+ status: response.status,
2663
+ ...response.ok ? {} : { message: `GitHub tag ${repository}@${version} was not found.` }
2664
+ };
2665
+ } catch (error) {
2666
+ return {
2667
+ id: `github-tag:${repository}:${version}`,
2668
+ kind: "github-tag",
2669
+ name: repository,
2670
+ version,
2671
+ url,
2672
+ ok: false,
2673
+ status: null,
2674
+ message: error instanceof Error ? error.message : String(error)
2675
+ };
2676
+ }
2677
+ }
2566
2678
  async function verifySimpleRegistryArtifact(input) {
2567
2679
  try {
2568
2680
  const response = await fetchJsonForArtifact(input.url);
@@ -2591,6 +2703,21 @@ async function verifySimpleRegistryArtifact(input) {
2591
2703
  }
2592
2704
  async function collectPublishedReleaseArtifactChecks(selectedVersions) {
2593
2705
  const checks = [];
2706
+ const githubRepositories = {
2707
+ "@treeseed/sdk": "treeseed-ai/sdk",
2708
+ "@treeseed/ui": "treeseed-ai/ui",
2709
+ "@treeseed/core": "treeseed-ai/core",
2710
+ "@treeseed/admin": "treeseed-ai/admin",
2711
+ "@treeseed/cli": "treeseed-ai/cli",
2712
+ "@treeseed/agent": "treeseed-ai/agent",
2713
+ "@treeseed/api": "treeseed-ai/api",
2714
+ treedx: "treeseed-ai/treedx",
2715
+ "@treeseed/treedx": "treeseed-ai/treedx"
2716
+ };
2717
+ for (const [packageName, repository] of Object.entries(githubRepositories)) {
2718
+ const version = selectedVersions.get(packageName);
2719
+ if (version) checks.push(await verifyGitHubTagArtifact(repository, version));
2720
+ }
2594
2721
  const npmPackages = ["@treeseed/sdk", "@treeseed/ui", "@treeseed/core", "@treeseed/admin", "@treeseed/cli", "@treeseed/agent"];
2595
2722
  for (const packageName of npmPackages) {
2596
2723
  const version = selectedVersions.get(packageName);
@@ -5372,10 +5499,11 @@ ${blockers.join("\n")}`, {
5372
5499
  resumable: true
5373
5500
  };
5374
5501
  }),
5502
+ { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5503
+ { id: "production-hosting", description: "Reconcile and live-verify production hosted resources before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5504
+ { id: "production-api-guarantees", description: "Run production API release guarantees before root deploy", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5375
5505
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5376
5506
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5377
- { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5378
- { id: "production-hosting", description: "Reconcile and live-verify production hosted resources after publish", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5379
5507
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5380
5508
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5381
5509
  ],
@@ -5482,6 +5610,9 @@ ${rendered}`);
5482
5610
  });
5483
5611
  packageReleases.push(packageRelease);
5484
5612
  }
5613
+ const publishedArtifacts = await executeJournalStep(root, workflowRun.runId, "verify-published-artifacts", () => verifyPublishedReleaseArtifacts(selectedVersions));
5614
+ const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", productionReleaseImageRefEnv(selectedVersions)));
5615
+ const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
5485
5616
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
5486
5617
  const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5487
5618
  const changelog = updateReleaseChangelog(repoRoot(root), {
@@ -5532,8 +5663,6 @@ ${rendered}`);
5532
5663
  runId: workflowRun.runId,
5533
5664
  onProgress: (line, stream) => helpers.write(line, stream)
5534
5665
  }).then((workflowGates) => ({ workflowGates })));
5535
- const publishedArtifacts = await executeJournalStep(root, workflowRun.runId, "verify-published-artifacts", () => verifyPublishedReleaseArtifacts(selectedVersions));
5536
- const productionHosting = await executeJournalStep(root, workflowRun.runId, "production-hosting", () => reconcileSaveHostedEnvironment(root, "prod", helpers, workflowRun.runId, "release", productionReleaseImageRefEnv(selectedVersions)));
5537
5666
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
5538
5667
  const packageBackMerges = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
5539
5668
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -5561,6 +5690,7 @@ ${rendered}`);
5561
5690
  publishWait: publishWait.workflowGates,
5562
5691
  publishedArtifacts,
5563
5692
  productionHosting,
5693
+ productionApiGuarantees,
5564
5694
  backMerge,
5565
5695
  workspaceLinks,
5566
5696
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.17",
3
+ "version": "0.12.19",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {