@treeseed/sdk 0.12.15 → 0.12.17

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({
@@ -60,7 +60,7 @@ function resolvePublicTreeDxRoot(input) {
60
60
  ].filter((candidate) => Boolean(candidate));
61
61
  return candidates.find((candidate) => existsSync(resolve(candidate, "treeseed.package.yaml")) || existsSync(resolve(candidate, ".git"))) ?? candidates[0];
62
62
  }
63
- function publicTreeDxSourcePolicy(input, config) {
63
+ function publicTreeDxSourcePolicy(input, config, launchEnv) {
64
64
  const railway = config.publicTreeDxFederation?.railway ?? {};
65
65
  const configuredSource = railway.source && typeof railway.source === "object" && !Array.isArray(railway.source) ? railway.source : {};
66
66
  const configuredMode = typeof railway.sourceMode === "string" ? railway.sourceMode : null;
@@ -68,6 +68,7 @@ function publicTreeDxSourcePolicy(input, config) {
68
68
  const repository = typeof railway.sourceRepo === "string" ? railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(treeDxRoot) ?? "treeseed-ai/treedx";
69
69
  const sourceMode = configuredMode === "git" || configuredMode === "image" ? configuredMode : input.environment === "staging" ? "git" : "image";
70
70
  if (sourceMode !== "git") {
71
+ const imageRef = typeof launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF === "string" && launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() ? launchEnv.TREESEED_PUBLIC_TREEDX_IMAGE_REF.trim() : null;
71
72
  return {
72
73
  sourceMode: "image",
73
74
  sourceRepo: null,
@@ -75,6 +76,7 @@ function publicTreeDxSourcePolicy(input, config) {
75
76
  sourceCommit: null,
76
77
  sourceRootDirectory: null,
77
78
  image: "treeseed/treedx",
79
+ imageRef,
78
80
  imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
79
81
  };
80
82
  }
@@ -110,6 +112,7 @@ function railwayImageRefEnvForService(serviceKey) {
110
112
  if (serviceKey === "operationsRunner") return "TREESEED_OPERATIONS_RUNNER_IMAGE_REF";
111
113
  if (serviceKey === "capacityProviderManager") return "TREESEED_AGENT_MANAGER_IMAGE_REF";
112
114
  if (serviceKey === "capacityProviderRunner") return "TREESEED_AGENT_RUNNER_IMAGE_REF";
115
+ if (String(serviceKey).startsWith("public-treedx-node-")) return "TREESEED_PUBLIC_TREEDX_IMAGE_REF";
113
116
  return null;
114
117
  }
115
118
  function defaultRailwayImageRefForService(serviceKey, environment) {
@@ -295,14 +298,17 @@ function buildProfileFromDeployConfig(input) {
295
298
  publicTreeDxProjectGroup(environment, config),
296
299
  privateTreeDxProjectGroup()
297
300
  ];
298
- const railwayImageRefEnvKeys = [...new Set(Object.keys(config.services ?? {}).map((serviceKey) => railwayImageRefEnvForService(serviceKey)).filter((value) => Boolean(value)))];
301
+ const railwayImageRefEnvKeys = [.../* @__PURE__ */ new Set([
302
+ ...Object.keys(config.services ?? {}).map((serviceKey) => railwayImageRefEnvForService(serviceKey)).filter((value) => Boolean(value)),
303
+ ...config.hosting?.kind === "treeseed_control_plane" ? ["TREESEED_PUBLIC_TREEDX_IMAGE_REF"] : []
304
+ ])];
299
305
  let railwayImageRefEnv = {};
300
306
  try {
301
307
  railwayImageRefEnv = resolveTreeseedMachineEnvironmentValues(input.configRoot ?? input.tenantRoot, environment, railwayImageRefEnvKeys);
302
308
  } catch {
303
309
  railwayImageRefEnv = {};
304
310
  }
305
- const launchEnv = mergeRecord(process.env, railwayImageRefEnv, input.env);
311
+ const launchEnv = mergeRecord(railwayImageRefEnv, process.env, input.env);
306
312
  if (config.surfaces?.web && config.surfaces.web.enabled !== false) {
307
313
  services.push({
308
314
  id: "web",
@@ -419,7 +425,7 @@ function buildProfileFromDeployConfig(input) {
419
425
  }
420
426
  if (config.hosting?.kind === "treeseed_control_plane") {
421
427
  const treeDxNodePool = publicTreeDxNodePool(config);
422
- const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config);
428
+ const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
423
429
  const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
424
430
  const nodeIndex = offset + 1;
425
431
  const serviceName = indexedName("public-treedx-node", nodeIndex);
@@ -431,6 +437,7 @@ function buildProfileFromDeployConfig(input) {
431
437
  projectGroupId: "public-treedx-federation",
432
438
  config: {
433
439
  ...treeDxSourcePolicy.image ? { image: treeDxSourcePolicy.image } : {},
440
+ ...treeDxSourcePolicy.imageRef ? { imageRef: treeDxSourcePolicy.imageRef } : {},
434
441
  ...treeDxSourcePolicy.imageTagRef ? { imageTagRef: treeDxSourcePolicy.imageTagRef } : {},
435
442
  sourceMode: treeDxSourcePolicy.sourceMode,
436
443
  ...treeDxSourcePolicy.sourceRepo ? { sourceRepo: treeDxSourcePolicy.sourceRepo } : {},
@@ -629,7 +629,7 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
629
629
  } catch {
630
630
  machineEnv = {};
631
631
  }
632
- const imageRefEnv = { ...process.env, ...machineEnv, ...envOverlay };
632
+ const imageRefEnv = { ...machineEnv, ...process.env, ...envOverlay };
633
633
  let identity;
634
634
  try {
635
635
  identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget(normalizedScope));
@@ -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,
@@ -1708,7 +1708,7 @@ function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
1708
1708
  ]);
1709
1709
  const archived = [];
1710
1710
  const blockers = [];
1711
- for (const journal of listInterruptedWorkflowRuns(root).filter((entry) => entry.command === "release")) {
1711
+ for (const journal of listInterruptedWorkflowRuns(root, { recentLimit: 50 }).filter((entry) => entry.command === "release")) {
1712
1712
  const classification = classifyWorkflowRunJournal(journal, {
1713
1713
  currentBranch: branch,
1714
1714
  currentHeads
@@ -1741,7 +1741,7 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
1741
1741
  [rootRepo.name, rootRepo.commitSha ?? null],
1742
1742
  ...packageReports.map((report) => [report.name, report.commitSha ?? null])
1743
1743
  ]);
1744
- return listInterruptedWorkflowRuns(root).find((journal) => {
1744
+ return listInterruptedWorkflowRuns(root, { recentLimit: 50 }).find((journal) => {
1745
1745
  if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
1746
1746
  return false;
1747
1747
  }
@@ -5246,22 +5246,32 @@ ${blockers.join("\n")}`, {
5246
5246
  async function workflowRelease(helpers, input) {
5247
5247
  try {
5248
5248
  return await withContextEnv(helpers.context.env, async () => {
5249
+ const traceReleaseStartup = (phase) => {
5250
+ console.error(`[release][startup] ${phase}`);
5251
+ };
5252
+ traceReleaseStartup("resolve workspace");
5249
5253
  const root = workspaceRoot(resolveProjectRootOrThrow("release", helpers.cwd()));
5254
+ traceReleaseStartup("resolve session");
5250
5255
  const session = resolveTreeseedWorkflowSession(root);
5251
5256
  const executionMode = normalizeExecutionMode(input);
5257
+ traceReleaseStartup("inspect root repo");
5252
5258
  const rootRepo = createWorkspaceRootRepoReport(root);
5259
+ traceReleaseStartup("inspect package repos");
5253
5260
  const packageReports = createWorkspacePackageReports(root);
5254
5261
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5262
+ traceReleaseStartup("check auto resume");
5255
5263
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
5256
5264
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
5257
5265
  const effectiveInput = autoResumeRun ? {
5258
5266
  ...autoResumeRun.input,
5259
5267
  ciMode: input.ciMode ?? autoResumeRun.input.ciMode
5260
5268
  } : input;
5269
+ traceReleaseStartup("local cleanup");
5261
5270
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "release", effectiveInput);
5262
5271
  const level = effectiveInput.bump ?? "patch";
5263
5272
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
5264
5273
  const packageSelection = session.packageSelection;
5274
+ traceReleaseStartup("build release plan");
5265
5275
  const plannedRelease = buildReleasePlanSnapshot({
5266
5276
  root,
5267
5277
  mode: session.mode,
@@ -5274,10 +5284,12 @@ async function workflowRelease(helpers, input) {
5274
5284
  blockers: []
5275
5285
  });
5276
5286
  const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5287
+ traceReleaseStartup("collect blockers");
5277
5288
  const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
5278
5289
  level,
5279
5290
  repairVersionLine: effectiveInput.repairVersionLine === true
5280
5291
  });
5292
+ traceReleaseStartup("collect production readiness");
5281
5293
  const plannedReadiness = collectTreeseedDeploymentReadiness({
5282
5294
  tenantRoot: root,
5283
5295
  environment: "prod",
@@ -5314,13 +5326,16 @@ async function workflowRelease(helpers, input) {
5314
5326
  releaseBasePayload
5315
5327
  );
5316
5328
  }
5329
+ traceReleaseStartup("validate blockers");
5317
5330
  if (blockers.length > 0) {
5318
5331
  workflowError("release", "validation_failed", `Treeseed release cannot continue until blockers are resolved:
5319
5332
  ${blockers.join("\n")}`, {
5320
5333
  details: { blockers, releasePlan: plannedRelease }
5321
5334
  });
5322
5335
  }
5336
+ traceReleaseStartup("prepare fresh release");
5323
5337
  const freshPreparation = input.fresh === true ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
5338
+ traceReleaseStartup("compute versions");
5324
5339
  const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
5325
5340
  const stableVersions = new Map([
5326
5341
  ...releasePlanStableDependencyVersionMap(plannedRelease).entries(),
@@ -5331,6 +5346,7 @@ ${blockers.join("\n")}`, {
5331
5346
  ...stableVersions.entries()
5332
5347
  ]);
5333
5348
  const selectedPackageSet = new Set(selectedPackageNames);
5349
+ traceReleaseStartup("acquire workflow run");
5334
5350
  const workflowRun = acquireWorkflowRun(
5335
5351
  "release",
5336
5352
  session,
@@ -131,4 +131,7 @@ export declare function createWorkflowRunJournal(root: string, options: {
131
131
  steps: Omit<TreeseedWorkflowRunStep, 'status' | 'completedAt' | 'data'>[];
132
132
  }): TreeseedWorkflowRunJournal;
133
133
  export declare function listWorkflowRunJournals(root: string): TreeseedWorkflowRunJournal[];
134
- export declare function listInterruptedWorkflowRuns(root: string): TreeseedWorkflowRunJournal[];
134
+ export declare function listRecentWorkflowRunJournals(root: string, limit?: number): TreeseedWorkflowRunJournal[];
135
+ export declare function listInterruptedWorkflowRuns(root: string, options?: {
136
+ recentLimit?: number;
137
+ }): TreeseedWorkflowRunJournal[];
@@ -576,8 +576,42 @@ function listWorkflowRunJournals(root) {
576
576
  }
577
577
  return [...byId.values()].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
578
578
  }
579
- function listInterruptedWorkflowRuns(root) {
580
- return listWorkflowRunJournals(root).filter((journal) => journal.status === "failed" && journal.resumable);
579
+ function listRecentWorkflowRunJournalsForScope(root, scope, limit) {
580
+ const runsDir = workflowRunsRoot(root, null, scope);
581
+ if (!existsSync(runsDir)) {
582
+ return [];
583
+ }
584
+ return readdirSync(runsDir).filter((entry) => entry.endsWith(".json")).map((entry) => {
585
+ const path = resolve(runsDir, entry);
586
+ try {
587
+ return { path, mtimeMs: statSync(path).mtimeMs };
588
+ } catch {
589
+ return null;
590
+ }
591
+ }).filter((entry) => entry != null).sort((left, right) => right.mtimeMs - left.mtimeMs).slice(0, Math.max(1, limit)).map((entry) => {
592
+ try {
593
+ if (statSync(entry.path).size > 5 * 1024 * 1024) {
594
+ const archivedSummary = archivedWorkflowRunSummary(entry.path);
595
+ if (archivedSummary) return archivedSummary;
596
+ }
597
+ } catch {
598
+ return null;
599
+ }
600
+ return safeJsonParse(entry.path);
601
+ }).filter((entry) => entry != null);
602
+ }
603
+ function listRecentWorkflowRunJournals(root, limit = 50) {
604
+ const local = listRecentWorkflowRunJournalsForScope(root, "worktree", limit);
605
+ const shared = listRecentWorkflowRunJournalsForScope(root, "shared", limit);
606
+ const byId = /* @__PURE__ */ new Map();
607
+ for (const journal of [...local, ...shared]) {
608
+ byId.set(journal.runId, journal);
609
+ }
610
+ return [...byId.values()].sort((left, right) => right.createdAt.localeCompare(left.createdAt)).slice(0, Math.max(1, limit));
611
+ }
612
+ function listInterruptedWorkflowRuns(root, options = {}) {
613
+ const journals = options.recentLimit ? listRecentWorkflowRunJournals(root, options.recentLimit) : listWorkflowRunJournals(root);
614
+ return journals.filter((journal) => journal.status === "failed" && journal.resumable);
581
615
  }
582
616
  export {
583
617
  acquireWorkflowLock,
@@ -591,6 +625,7 @@ export {
591
625
  inspectWorkflowLock,
592
626
  isSharedWorkflowCommand,
593
627
  listInterruptedWorkflowRuns,
628
+ listRecentWorkflowRunJournals,
594
629
  listWorkflowRunJournals,
595
630
  readWorkflowRunJournal,
596
631
  refreshWorkflowLock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.15",
3
+ "version": "0.12.17",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {