@treeseed/sdk 0.12.16 → 0.12.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.
@@ -54,7 +54,7 @@ function defaultVerify(input) {
54
54
  issues: []
55
55
  }
56
56
  ];
57
- if (input.unit.host.id === "railway" && input.environment === "prod" && unitConfig(input).sourceMode === "image") {
57
+ if (input.unit.host.id === "railway" && input.environment === "prod" && unitConfig(input).sourceMode === "image" && input.unit.serviceType.id !== "relational-database" && unitConfig(input).resourceType !== "postgres") {
58
58
  const imageRef = unitConfig(input).imageRef;
59
59
  const hasImageRef = typeof imageRef === "string" && imageRef.trim().length > 0;
60
60
  checks.push({
@@ -2134,7 +2134,7 @@ function getPersistedCustomDomainState(input, provider, domain) {
2134
2134
  }
2135
2135
  if (provider === "railway") {
2136
2136
  try {
2137
- const state = loadTreeseedReconcileState(input.context.tenantRoot, input.context.target);
2137
+ const state = loadTreeseedReconcileState(input.context.tenantRoot, input.context.target, input.context.launchEnv);
2138
2138
  const unitId = createTreeseedReconcileUnitId("custom-domain:api", domain);
2139
2139
  const unit = state.units[unitId];
2140
2140
  const reconciled = unit?.lastReconciledState;
@@ -204,7 +204,7 @@ async function refreshTreeseedUnits({
204
204
  const units = filterUnitsBySelector(baseUnits, selector);
205
205
  const deployConfig = derived.deployConfig;
206
206
  const registry = createTreeseedReconcileRegistry(deployConfig);
207
- const reconcileState = loadTreeseedReconcileState(tenantRoot, target);
207
+ const reconcileState = loadTreeseedReconcileState(tenantRoot, target, env);
208
208
  const context = createRunContext(tenantRoot, target, env, write);
209
209
  const observations = /* @__PURE__ */ new Map();
210
210
  await runByDependencyLevel(topologicallySortDesiredUnits(units), async (unit) => {
@@ -294,7 +294,7 @@ async function reconcileTreeseedTarget({
294
294
  const persistVerifiedResult = async (persisted, verifiedResult) => {
295
295
  persistChain = persistChain.then(() => {
296
296
  persistResult(planned.state, persisted, verifiedResult);
297
- writeTreeseedReconcileState(tenantRoot, planned.state);
297
+ writeTreeseedReconcileState(tenantRoot, planned.state, env);
298
298
  });
299
299
  await persistChain;
300
300
  };
@@ -458,7 +458,7 @@ async function reconcileTreeseedTarget({
458
458
  results.push(verifiedResult);
459
459
  });
460
460
  if (!dryRun) {
461
- writeTreeseedReconcileState(tenantRoot, planned.state);
461
+ writeTreeseedReconcileState(tenantRoot, planned.state, env);
462
462
  }
463
463
  return {
464
464
  target,
@@ -477,10 +477,10 @@ async function destroyTreeseedTargetUnits({
477
477
  units: explicitUnits,
478
478
  write
479
479
  }) {
480
- const { units: allUnits, deployConfig } = deriveTreeseedDesiredUnits({ tenantRoot, target });
480
+ const { units: allUnits, deployConfig } = deriveTreeseedDesiredUnits({ tenantRoot, target, env });
481
481
  const units = filterUnitsBySelector(explicitUnits ?? allUnits, selector);
482
482
  const registry = createTreeseedReconcileRegistry(deployConfig);
483
- const reconcileState = loadTreeseedReconcileState(tenantRoot, target);
483
+ const reconcileState = loadTreeseedReconcileState(tenantRoot, target, env);
484
484
  const context = createRunContext(tenantRoot, target, env, write);
485
485
  const results = [];
486
486
  for (const unit of reverseTopologicallySortedUnits(units)) {
@@ -1,7 +1,7 @@
1
1
  import type { TreeseedDesiredUnit, TreeseedReconcileStateRecord, TreeseedReconcileTarget, TreeseedUnitPersistedState } from './contracts.js';
2
2
  export declare function migrateLegacyDeployStateUnits(legacyState: Record<string, any>, target: TreeseedReconcileTarget): Record<string, TreeseedUnitPersistedState>;
3
- export declare function loadTreeseedReconcileState(tenantRoot: string, target: TreeseedReconcileTarget): TreeseedReconcileStateRecord;
4
- export declare function writeTreeseedReconcileState(tenantRoot: string, reconcileState: TreeseedReconcileStateRecord): void;
3
+ export declare function loadTreeseedReconcileState(tenantRoot: string, target: TreeseedReconcileTarget, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): TreeseedReconcileStateRecord;
4
+ export declare function writeTreeseedReconcileState(tenantRoot: string, reconcileState: TreeseedReconcileStateRecord, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): void;
5
5
  export declare function ensureTreeseedPersistedUnitState(reconcileState: TreeseedReconcileStateRecord, unit: TreeseedDesiredUnit): TreeseedUnitPersistedState;
6
6
  export declare function updateTreeseedPersistedUnitState(reconcileState: TreeseedReconcileStateRecord, state: TreeseedUnitPersistedState): void;
7
7
  export declare function desiredUnitSpecHash(unit: TreeseedDesiredUnit): string;
@@ -227,8 +227,8 @@ function migrateLegacyDeployStateUnits(legacyState, target) {
227
227
  }
228
228
  return units;
229
229
  }
230
- function loadTreeseedReconcileState(tenantRoot, target) {
231
- const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: target.kind === "persistent" ? target.scope : "staging", env: process.env }).deployConfig;
230
+ function loadTreeseedReconcileState(tenantRoot, target, env = process.env) {
231
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: target.kind === "persistent" ? target.scope : "staging", env }).deployConfig;
232
232
  const legacyState = loadDeployState(tenantRoot, deployConfig, { target });
233
233
  const persistedUnits = legacyState.units && typeof legacyState.units === "object" ? legacyState.units : migrateLegacyDeployStateUnits(legacyState, target);
234
234
  return {
@@ -238,8 +238,8 @@ function loadTreeseedReconcileState(tenantRoot, target) {
238
238
  units: { ...persistedUnits }
239
239
  };
240
240
  }
241
- function writeTreeseedReconcileState(tenantRoot, reconcileState) {
242
- const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: reconcileState.target.kind === "persistent" ? reconcileState.target.scope : "staging", env: process.env }).deployConfig;
241
+ function writeTreeseedReconcileState(tenantRoot, reconcileState, env = process.env) {
242
+ const deployConfig = loadTreeseedPlatformConfig({ tenantRoot, environment: reconcileState.target.kind === "persistent" ? reconcileState.target.scope : "staging", env }).deployConfig;
243
243
  const legacyState = loadDeployState(tenantRoot, deployConfig, { target: reconcileState.target });
244
244
  writeDeployState(tenantRoot, {
245
245
  ...legacyState,
@@ -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;
@@ -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) ?? {};
@@ -5372,10 +5416,11 @@ ${blockers.join("\n")}`, {
5372
5416
  resumable: true
5373
5417
  };
5374
5418
  }),
5419
+ { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
5420
+ { 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 },
5421
+ { 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
5422
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5376
5423
  { 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
5424
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5380
5425
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
5381
5426
  ],
@@ -5482,6 +5527,9 @@ ${rendered}`);
5482
5527
  });
5483
5528
  packageReleases.push(packageRelease);
5484
5529
  }
5530
+ 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)));
5532
+ const productionApiGuarantees = await executeJournalStep(root, workflowRun.runId, "production-api-guarantees", () => runReleaseApiGuarantees(root, "prod", helpers, "release", normalizeSceneArtifactsMode(effectiveInput.sceneArtifacts)));
5485
5533
  const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
5486
5534
  const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
5487
5535
  const changelog = updateReleaseChangelog(repoRoot(root), {
@@ -5532,8 +5580,6 @@ ${rendered}`);
5532
5580
  runId: workflowRun.runId,
5533
5581
  onProgress: (line, stream) => helpers.write(line, stream)
5534
5582
  }).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
5583
  const backMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => {
5538
5584
  const packageBackMerges = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageSet.has(pkg.name)).map((pkg) => backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
5539
5585
  subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
@@ -5561,6 +5607,7 @@ ${rendered}`);
5561
5607
  publishWait: publishWait.workflowGates,
5562
5608
  publishedArtifacts,
5563
5609
  productionHosting,
5610
+ productionApiGuarantees,
5564
5611
  backMerge,
5565
5612
  workspaceLinks,
5566
5613
  releasedCommit: String(rootRelease.commit.commitSha ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {