@treeseed/sdk 0.12.59 → 0.12.61

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.
Files changed (44) hide show
  1. package/dist/guarantees/index.js +59 -56
  2. package/dist/hosting/contracts.d.ts +0 -19
  3. package/dist/hosting/graph.d.ts +1 -86
  4. package/dist/hosting/graph.js +96 -247
  5. package/dist/local-dev/managed-dev.js +30 -8
  6. package/dist/managed-dependencies.d.ts +3 -0
  7. package/dist/managed-dependencies.js +294 -20
  8. package/dist/operations/services/deploy.js +6 -6
  9. package/dist/operations/services/deployment-readiness.js +5 -4
  10. package/dist/operations/services/git-runner.d.ts +2 -0
  11. package/dist/operations/services/git-runner.js +23 -2
  12. package/dist/operations/services/hosted-service-checks.js +28 -0
  13. package/dist/operations/services/live-hosted-service-checks.js +56 -14
  14. package/dist/operations/services/local-cleanup.d.ts +1 -0
  15. package/dist/operations/services/local-cleanup.js +28 -9
  16. package/dist/operations/services/package-adapters.js +3 -3
  17. package/dist/operations/services/railway-api.d.ts +72 -28
  18. package/dist/operations/services/railway-api.js +321 -876
  19. package/dist/operations/services/railway-cli.d.ts +47 -0
  20. package/dist/operations/services/railway-cli.js +142 -0
  21. package/dist/operations/services/railway-deploy.d.ts +5 -0
  22. package/dist/operations/services/railway-deploy.js +47 -75
  23. package/dist/operations/services/railway-source-policy.d.ts +6 -0
  24. package/dist/operations/services/railway-source-policy.js +52 -9
  25. package/dist/operations/services/repository-save-orchestrator.d.ts +2 -0
  26. package/dist/operations/services/repository-save-orchestrator.js +45 -14
  27. package/dist/operations-types.d.ts +3 -1
  28. package/dist/platform/contracts.d.ts +1 -0
  29. package/dist/platform/deploy-config.js +2 -1
  30. package/dist/reconcile/builtin-adapters.js +524 -680
  31. package/dist/reconcile/desired-state.js +5 -3
  32. package/dist/reconcile/engine.js +34 -28
  33. package/dist/reconcile/live-acceptance.js +2 -11
  34. package/dist/reconcile/providers/railway-iac.d.ts +148 -0
  35. package/dist/reconcile/providers/railway-iac.js +294 -18
  36. package/dist/scenes/runner.js +11 -11
  37. package/dist/scripts/build-dist.js +22 -0
  38. package/dist/workflow/operations.d.ts +12 -0
  39. package/dist/workflow/operations.js +265 -90
  40. package/dist/workflow/runs.d.ts +4 -0
  41. package/dist/workflow/runs.js +25 -0
  42. package/dist/workflow-support.d.ts +1 -1
  43. package/dist/workflow-support.js +3 -1
  44. package/package.json +1 -2
@@ -153,7 +153,6 @@ import {
153
153
  archiveWorkflowRun,
154
154
  cacheWorkflowGateResult,
155
155
  classifyWorkflowRunJournal,
156
- classifyWorkflowRunJournals,
157
156
  createWorkflowRunJournal,
158
157
  generateWorkflowRunId,
159
158
  getCachedSuccessfulWorkflowGate,
@@ -551,11 +550,12 @@ function selectorFromWorkflowHostingGraph(graph) {
551
550
  ].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
552
551
  serviceId: [...new Set(graph.units.flatMap((unit) => [
553
552
  unit.id,
553
+ typeof unit.config.poolKey === "string" ? unit.config.poolKey : null,
554
554
  typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
555
555
  ]).concat(domainServiceIds).filter((value) => Boolean(value)))],
556
556
  serviceType: [...new Set(graph.units.flatMap((unit) => {
557
557
  if (unit.id === "api") return ["api-runtime", "railway-service:api", "custom-domain:api", "dns-record"];
558
- if (unit.id === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
558
+ if (unit.id === "operationsRunner" || unit.config.poolKey === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
559
559
  if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
560
560
  if (unit.host.id === "cloudflare") return ["web-ui", "edge-worker", "content-store", "database", "kv-form-guard", "turnstile-widget", "pages-project", "custom-domain:web", "dns-record"];
561
561
  return [];
@@ -707,6 +707,40 @@ ${liveFailures.join("\n")}`, {
707
707
  }
708
708
  return live;
709
709
  }
710
+ async function verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs) {
711
+ const reports = {};
712
+ for (const environment of ["prod", "staging"]) {
713
+ const env = {
714
+ ...helpers.context.env,
715
+ ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env),
716
+ ...environment === "prod" ? releaseImageRefs : {}
717
+ };
718
+ helpers.write(`[release][railway] read-only ${environment} source-invariance verification started.`, "stderr");
719
+ const report = await collectTreeseedLiveHostedServiceChecks({
720
+ tenantRoot: root,
721
+ target: environment,
722
+ appId: "api",
723
+ serviceKeys: ["api", "operationsRunner", "public-treedx-node-01"],
724
+ strict: true,
725
+ requireLiveRailway: true,
726
+ requireLiveHttp: true,
727
+ env
728
+ });
729
+ const failures = [
730
+ ...report.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
731
+ ...report.liveObservation.issues
732
+ ];
733
+ if (failures.length > 0) {
734
+ workflowError("release", "hosted_live_verification_failed", `${environment} API source-invariance verification failed after production deployment:
735
+ ${failures.join("\n")}`, {
736
+ details: { environment, report }
737
+ });
738
+ }
739
+ reports[environment] = report;
740
+ helpers.write(`[release][railway] read-only ${environment} source-invariance verification passed.`, "stderr");
741
+ }
742
+ return { status: "verified", order: ["prod", "staging"], reports };
743
+ }
710
744
  function productionReleaseImageRefEnv(selectedVersions) {
711
745
  const refs = {};
712
746
  const apiVersion = selectedVersions.get("@treeseed/api");
@@ -1797,7 +1831,32 @@ function gateForSavedRootReport(report, branch, scope) {
1797
1831
  }
1798
1832
  function findAutoResumableTaskRun(root, command, branch) {
1799
1833
  if (!branch) return null;
1800
- return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
1834
+ const currentHeads = Object.fromEntries([
1835
+ ["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
1836
+ ...checkedOutWorkspacePackageRepos(root).map((repo) => [
1837
+ repo.name,
1838
+ runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
1839
+ ])
1840
+ ]);
1841
+ return listInterruptedWorkflowRuns(root).find((journal) => {
1842
+ if (journal.command !== command || !journal.resumable || journal.session.branchName !== branch) {
1843
+ return false;
1844
+ }
1845
+ const classification = classifyWorkflowRunJournal(journal, {
1846
+ currentBranch: branch,
1847
+ currentHeads
1848
+ });
1849
+ if (classification.state === "resumable") {
1850
+ return true;
1851
+ }
1852
+ if (classification.state === "stale") {
1853
+ archiveWorkflowRun(root, journal.runId, {
1854
+ ...classification,
1855
+ reasons: [`${command} implicit resume skipped stale failed run`, ...classification.reasons]
1856
+ });
1857
+ }
1858
+ return false;
1859
+ }) ?? null;
1801
1860
  }
1802
1861
  function rejectImplicitWorkflowResume(operation, journal) {
1803
1862
  if (!journal) return;
@@ -2011,17 +2070,48 @@ async function executeJournalStep(root, runId, stepId, action, options = {}) {
2011
2070
  if (step.status === "completed" && !options.rerunCompleted) {
2012
2071
  return step.data ?? null;
2013
2072
  }
2014
- const data = await Promise.resolve(action());
2073
+ const startedAt = /* @__PURE__ */ new Date();
2074
+ const retryCount = Number(step.retryCount ?? 0) + (step.startedAt ? 1 : 0);
2075
+ updateWorkflowRunJournal(root, runId, (journal) => ({
2076
+ ...journal,
2077
+ steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, startedAt: startedAt.toISOString(), retryCount, lastFailure: null } : entry)
2078
+ }));
2079
+ refreshWorkflowLock(root, runId);
2080
+ const lockHeartbeat = setInterval(() => refreshWorkflowLock(root, runId), 15e3);
2081
+ lockHeartbeat.unref();
2082
+ process.stderr.write(`[workflow][step] start ${stepId} attempt=${retryCount + 1}
2083
+ `);
2084
+ let data;
2085
+ try {
2086
+ data = await Promise.resolve(action());
2087
+ } catch (error) {
2088
+ clearInterval(lockHeartbeat);
2089
+ const elapsedMs2 = Date.now() - startedAt.getTime();
2090
+ const message = error instanceof Error ? error.message : String(error);
2091
+ updateWorkflowRunJournal(root, runId, (journal) => ({
2092
+ ...journal,
2093
+ steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, elapsedMs: elapsedMs2, lastFailure: message } : entry)
2094
+ }));
2095
+ process.stderr.write(`[workflow][step] fail ${stepId} elapsed=${Math.ceil(elapsedMs2 / 1e3)}s retries=${retryCount}
2096
+ `);
2097
+ throw error;
2098
+ }
2099
+ clearInterval(lockHeartbeat);
2100
+ const elapsedMs = Date.now() - startedAt.getTime();
2015
2101
  updateWorkflowRunJournal(root, runId, (journal) => ({
2016
2102
  ...journal,
2017
2103
  steps: journal.steps.map((entry) => entry.id === stepId ? {
2018
2104
  ...entry,
2019
2105
  status: "completed",
2020
2106
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2107
+ elapsedMs,
2108
+ lastFailure: null,
2021
2109
  data: data ?? null
2022
2110
  } : entry)
2023
2111
  }));
2024
2112
  refreshWorkflowLock(root, runId);
2113
+ process.stderr.write(`[workflow][step] complete ${stepId} elapsed=${Math.ceil(elapsedMs / 1e3)}s retries=${retryCount}
2114
+ `);
2025
2115
  return data;
2026
2116
  }
2027
2117
  function skipJournalStep(root, runId, stepId, data = null) {
@@ -2185,7 +2275,7 @@ function productionPackageDeployGates(root, versions) {
2185
2275
  })];
2186
2276
  });
2187
2277
  }
2188
- function prepareAdapterReleaseMetadata(root, pkg, version) {
2278
+ async function prepareAdapterReleaseMetadata(root, pkg, version) {
2189
2279
  const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
2190
2280
  if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
2191
2281
  const tsx = resolve(root, "node_modules/.bin/tsx");
@@ -2199,7 +2289,7 @@ function prepareAdapterReleaseMetadata(root, pkg, version) {
2199
2289
  return {
2200
2290
  status: "npm-install",
2201
2291
  adapter: adapter?.id ?? pkg.name,
2202
- ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2292
+ ...await runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2203
2293
  };
2204
2294
  }
2205
2295
  return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
@@ -2237,40 +2327,83 @@ function npmCommandForWorkflowSpawn(args) {
2237
2327
  ]
2238
2328
  };
2239
2329
  }
2240
- function runReleaseNpmInstall(repoDir, options = {}) {
2241
- if (shouldSkipReleaseInstall()) {
2242
- return { status: "skipped", reason: "disabled" };
2330
+ function lockfileRootMatchesManifest(repoDir) {
2331
+ try {
2332
+ const manifest = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
2333
+ const lockfile = JSON.parse(readFileSync(resolve(repoDir, "package-lock.json"), "utf8"));
2334
+ const root = lockfile.packages?.[""];
2335
+ if (!root || root.version !== manifest.version) return false;
2336
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
2337
+ const expected = manifest[field] ?? {};
2338
+ const observed = root[field] ?? {};
2339
+ if (JSON.stringify(expected) !== JSON.stringify(observed)) return false;
2340
+ }
2341
+ return true;
2342
+ } catch {
2343
+ return false;
2243
2344
  }
2244
- const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
2245
- const spawnCommand = npmCommandForWorkflowSpawn(args);
2246
- let lastDetail = "";
2247
- for (let attempt = 1; attempt <= 10; attempt += 1) {
2248
- const result = spawnSync(spawnCommand.command, spawnCommand.args, {
2345
+ }
2346
+ function waitForReleaseInstall(command, args, repoDir, attempt) {
2347
+ return new Promise((resolvePromise) => {
2348
+ const startedAt = Date.now();
2349
+ let settled = false;
2350
+ const child = spawn(command, args, {
2249
2351
  cwd: repoDir,
2250
2352
  env: {
2251
2353
  ...process.env,
2252
2354
  npm_config_audit: "false",
2253
- npm_config_fetch_retries: "4",
2355
+ npm_config_fetch_retries: "2",
2254
2356
  npm_config_fund: "false",
2255
- npm_config_foreground_scripts: "true",
2357
+ npm_config_foreground_scripts: "false",
2256
2358
  npm_config_loglevel: "warn",
2257
2359
  npm_config_maxsockets: "4",
2258
- npm_config_prefer_online: "true",
2360
+ npm_config_prefer_offline: "true",
2259
2361
  npm_config_progress: "false"
2260
2362
  },
2261
- stdio: "pipe",
2262
- encoding: "utf8"
2363
+ stdio: ["ignore", "pipe", "pipe"]
2364
+ });
2365
+ const output = [];
2366
+ child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
2367
+ child.stderr?.on("data", (chunk) => output.push(Buffer.from(chunk)));
2368
+ const heartbeat = setInterval(() => {
2369
+ process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
2370
+ `);
2371
+ }, 15e3);
2372
+ child.once("close", (status) => {
2373
+ if (settled) return;
2374
+ settled = true;
2375
+ clearInterval(heartbeat);
2376
+ resolvePromise({ status, detail: Buffer.concat(output).toString("utf8").trim() });
2377
+ });
2378
+ child.once("error", (error) => {
2379
+ if (settled) return;
2380
+ settled = true;
2381
+ clearInterval(heartbeat);
2382
+ output.push(Buffer.from(error.message));
2383
+ resolvePromise({ status: null, detail: Buffer.concat(output).toString("utf8").trim() });
2263
2384
  });
2385
+ });
2386
+ }
2387
+ async function runReleaseNpmInstall(repoDir, options = {}) {
2388
+ if (shouldSkipReleaseInstall()) {
2389
+ return { status: "skipped", reason: "disabled" };
2390
+ }
2391
+ if (repoDir === options.workspaceRoot && lockfileRootMatchesManifest(repoDir)) {
2392
+ return { status: "skipped", reason: "root-lockfile-already-matches", attempts: 0 };
2393
+ }
2394
+ const args = ["install", "--package-lock-only", "--ignore-scripts", "--prefer-offline", "--workspaces=false", "--no-audit", "--no-fund"];
2395
+ const spawnCommand = npmCommandForWorkflowSpawn(args);
2396
+ let lastDetail = "";
2397
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
2398
+ process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=0s
2399
+ `);
2400
+ const result = await waitForReleaseInstall(spawnCommand.command, spawnCommand.args, repoDir, attempt);
2264
2401
  if (result.status === 0) {
2265
2402
  return { status: "completed", reason: null, attempts: attempt };
2266
2403
  }
2267
- lastDetail = [
2268
- result.error?.message,
2269
- result.stderr?.trim(),
2270
- result.stdout?.trim()
2271
- ].filter(Boolean).join("\n");
2272
- if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 10) break;
2273
- spawnSync("sleep", ["30"], { stdio: "ignore" });
2404
+ lastDetail = result.detail;
2405
+ if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || attempt === 3) break;
2406
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 15e3));
2274
2407
  }
2275
2408
  throw new Error(lastDetail || `npm ${args.join(" ")} failed`);
2276
2409
  }
@@ -5192,6 +5325,7 @@ async function workflowClose(helpers, input) {
5192
5325
  }
5193
5326
  function stagingCandidateWorkflowGates(root, manifest) {
5194
5327
  const gates = [];
5328
+ const adapters = discoverTreeseedPackageAdapters(root);
5195
5329
  const add = (name, repoPath, headSha, workflow, deploy = false) => {
5196
5330
  if (!workflowFileExists(repoPath, workflow)) return;
5197
5331
  const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
@@ -5199,10 +5333,11 @@ function stagingCandidateWorkflowGates(root, manifest) {
5199
5333
  };
5200
5334
  for (const pkg of manifest.packages) {
5201
5335
  const repoPath = resolve(root, pkg.path);
5336
+ const adapter = adapters.find((candidate) => candidate.id === pkg.name || candidate.name === pkg.name);
5202
5337
  if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
5203
5338
  add(pkg.name, repoPath, pkg.commit, "verify.yml");
5204
5339
  }
5205
- if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5340
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && adapter?.capabilities.deploy === true && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5206
5341
  add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
5207
5342
  }
5208
5343
  }
@@ -6018,6 +6153,7 @@ ${blockers.join("\n")}`, {
6018
6153
  },
6019
6154
  { id: "verify-published-artifacts", description: "Verify immutable registry artifacts exist after publish workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
6020
6155
  { id: "production-package-deploy-workflows", description: "Wait for production package deploy workflows before live verification", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
6156
+ { id: "verify-api-environment-isolation", description: "Verify production images and staging Git sources remained isolated", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
6021
6157
  { id: "persist-production-image-refs", description: "Persist released production image refs and verify deployment readiness", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
6022
6158
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
6023
6159
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
@@ -6083,57 +6219,90 @@ ${rendered}`);
6083
6219
  });
6084
6220
  const packageReleases = [];
6085
6221
  const packageRepoByName = new Map(checkedOutWorkspacePackageRepos(root).map((entry) => [entry.name, entry]));
6086
- for (const packageName of selectedPackageNames) {
6222
+ const pendingPackageReleases = new Set(selectedPackageNames.filter((name) => selectedVersions.has(name) && packageRepoByName.has(name)));
6223
+ const completedPackageReleases = /* @__PURE__ */ new Set();
6224
+ const packageReleaseResults = /* @__PURE__ */ new Map();
6225
+ const selectedPackageDependencies = new Map([...pendingPackageReleases].map((packageName) => {
6087
6226
  const pkg = packageRepoByName.get(packageName);
6088
- if (!pkg) continue;
6089
- const version = selectedVersions.get(pkg.name);
6090
- if (!version) continue;
6091
- const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
6092
- const metadata = prepareAdapterReleaseMetadata(root, pkg, version);
6093
- const changelog = updateReleaseChangelog(pkg.dir, {
6094
- version,
6095
- sourceRef: `origin/${PRODUCTION_BRANCH}`,
6096
- targetRef: "HEAD"
6097
- });
6098
- const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
6099
- subject: `release: ${pkg.name} ${version}`,
6100
- version,
6101
- tagName: version,
6102
- sourceRef: STAGING_BRANCH,
6103
- targetRef: PRODUCTION_BRANCH,
6104
- changelog
6105
- }));
6106
- pushBranch(pkg.dir, STAGING_BRANCH);
6107
- const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
6108
- const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
6109
- const publishGate = {
6110
- name: pkg.name,
6111
- repoPath: pkg.dir,
6112
- workflow: releaseWorkflowForPackage(root, pkg.name),
6113
- branch: version,
6114
- headSha: commit.commitSha
6115
- };
6116
- const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
6117
- root,
6118
- runId: workflowRun.runId,
6119
- onProgress: (line, stream) => helpers.write(line, stream)
6227
+ if (!pkg) return [packageName, []];
6228
+ const manifest = readJsonFile(resolve(pkg.dir, "package.json"));
6229
+ if (!manifest) {
6230
+ throw new Error(`Release package manifest is missing or invalid for ${packageName}.`);
6231
+ }
6232
+ const dependencyNames = /* @__PURE__ */ new Set([
6233
+ ...Object.keys(manifest.dependencies ?? {}),
6234
+ ...Object.keys(manifest.optionalDependencies ?? {}),
6235
+ ...Object.keys(manifest.peerDependencies ?? {})
6236
+ ]);
6237
+ const dependencies = [...dependencyNames].filter((name) => pendingPackageReleases.has(name));
6238
+ if (packageName !== "@treeseed/sdk" && pendingPackageReleases.has("@treeseed/sdk") && !dependencies.includes("@treeseed/sdk")) {
6239
+ dependencies.push("@treeseed/sdk");
6240
+ }
6241
+ return [packageName, dependencies];
6242
+ }));
6243
+ while (pendingPackageReleases.size > 0) {
6244
+ const eligible = selectedPackageNames.filter((packageName) => pendingPackageReleases.has(packageName) && (selectedPackageDependencies.get(packageName) ?? []).every((dependency) => completedPackageReleases.has(dependency)));
6245
+ if (eligible.length === 0) {
6246
+ throw new Error(`Release package dependency graph is cyclic or unresolved: ${[...pendingPackageReleases].join(", ")}.`);
6247
+ }
6248
+ const batch = eligible.slice(0, 2);
6249
+ helpers.write(`[release][packages] starting batch ${batch.join(", ")} (concurrency=${batch.length}/2).`, "stderr");
6250
+ const results = await Promise.all(batch.map(async (packageName) => {
6251
+ const pkg = packageRepoByName.get(packageName);
6252
+ const version = selectedVersions.get(pkg.name);
6253
+ const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
6254
+ const metadata = await prepareAdapterReleaseMetadata(root, pkg, version);
6255
+ const changelog = updateReleaseChangelog(pkg.dir, {
6256
+ version,
6257
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
6258
+ targetRef: "HEAD"
6259
+ });
6260
+ const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
6261
+ subject: `release: ${pkg.name} ${version}`,
6262
+ version,
6263
+ tagName: version,
6264
+ sourceRef: STAGING_BRANCH,
6265
+ targetRef: PRODUCTION_BRANCH,
6266
+ changelog
6267
+ }));
6268
+ pushBranch(pkg.dir, STAGING_BRANCH);
6269
+ const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
6270
+ const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
6271
+ const publishGate = {
6272
+ name: pkg.name,
6273
+ repoPath: pkg.dir,
6274
+ workflow: releaseWorkflowForPackage(root, pkg.name),
6275
+ branch: version,
6276
+ headSha: commit.commitSha
6277
+ };
6278
+ const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
6279
+ root,
6280
+ runId: workflowRun.runId,
6281
+ onProgress: (line, stream) => helpers.write(line, stream)
6282
+ });
6283
+ const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
6284
+ return {
6285
+ name: pkg.name,
6286
+ path: relative(root, pkg.dir),
6287
+ version,
6288
+ changelog,
6289
+ metadata,
6290
+ commit,
6291
+ promotion,
6292
+ tag,
6293
+ publishWait: publishWait2,
6294
+ publishedArtifacts: publishedArtifacts2
6295
+ };
6120
6296
  });
6121
- const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
6122
- return {
6123
- name: pkg.name,
6124
- path: relative(root, pkg.dir),
6125
- version,
6126
- changelog,
6127
- metadata,
6128
- commit,
6129
- promotion,
6130
- tag,
6131
- publishWait: publishWait2,
6132
- publishedArtifacts: publishedArtifacts2
6133
- };
6134
- });
6135
- packageReleases.push(packageRelease);
6297
+ return [packageName, packageRelease];
6298
+ }));
6299
+ for (const [packageName, packageRelease] of results) {
6300
+ pendingPackageReleases.delete(packageName);
6301
+ completedPackageReleases.add(packageName);
6302
+ packageReleaseResults.set(packageName, packageRelease);
6303
+ }
6136
6304
  }
6305
+ packageReleases.push(...selectedPackageNames.map((name) => packageReleaseResults.get(name)).filter((entry) => Boolean(entry)));
6137
6306
  const managedHelperReleases = await executeJournalStep(root, workflowRun.runId, "release-helper-repos", () => {
6138
6307
  const releases = releaseHelperRepos.map((repo) => releaseHelperRepoToProduction(repo));
6139
6308
  syncAllCheckedOutReleaseHelperRepos(root, STAGING_BRANCH);
@@ -6151,6 +6320,10 @@ ${rendered}`);
6151
6320
  onProgress: (line, stream) => helpers.write(line, stream)
6152
6321
  }).then((workflowGates) => ({ workflowGates }));
6153
6322
  });
6323
+ const apiEnvironmentIsolation = effectiveInput.verifyDeployedResources === true ? await executeJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", () => verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs)) : (skipJournalStep(root, workflowRun.runId, "verify-api-environment-isolation", {
6324
+ status: "skipped",
6325
+ reason: "--verify-deployed-resources was not requested"
6326
+ }), { status: "skipped", reason: "--verify-deployed-resources was not requested" });
6154
6327
  const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
6155
6328
  const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
6156
6329
  if (helpers.context.env) {
@@ -6171,8 +6344,8 @@ ${failures.join("\n")}`, {
6171
6344
  }
6172
6345
  return { persisted, readiness };
6173
6346
  });
6174
- const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
6175
- const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
6347
+ const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", async () => {
6348
+ const rootInstall = await runReleaseNpmInstall(root, { workspaceRoot: root });
6176
6349
  const changelog = updateReleaseChangelog(repoRoot(root), {
6177
6350
  version: plannedRelease.rootVersion,
6178
6351
  sourceRef: `origin/${PRODUCTION_BRANCH}`,
@@ -6244,6 +6417,7 @@ ${failures.join("\n")}`, {
6244
6417
  publishWait: publishWait.workflowGates,
6245
6418
  publishedArtifacts,
6246
6419
  productionPackageDeployWorkflows,
6420
+ apiEnvironmentIsolation,
6247
6421
  productionImageRefs,
6248
6422
  backMerge,
6249
6423
  workspaceLinks,
@@ -6397,17 +6571,21 @@ async function workflowRecover(helpers, input = {}) {
6397
6571
  return null;
6398
6572
  }).filter((entry) => entry !== null);
6399
6573
  const journals = listWorkflowRunJournals(root);
6574
+ const actionableJournals = journals.filter((journal) => journal.status !== "completed" && !journal.classification?.archivedAt);
6400
6575
  const session = resolveTreeseedWorkflowSession(root);
6401
6576
  const currentHeads = Object.fromEntries(
6402
6577
  [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
6403
6578
  );
6404
- const classifiedRuns = classifyWorkflowRunJournals(root, {
6405
- currentBranch: session.branchName,
6406
- currentHeads
6407
- });
6579
+ const classifiedRuns = actionableJournals.map((journal) => ({
6580
+ journal,
6581
+ classification: classifyWorkflowRunJournal(journal, {
6582
+ currentBranch: session.branchName,
6583
+ currentHeads
6584
+ })
6585
+ }));
6408
6586
  const markedObsoleteRun = input.obsoleteRunId ? (() => {
6409
- const entry = classifiedRuns.find((candidate) => candidate.journal.runId === input.obsoleteRunId);
6410
- if (!entry) {
6587
+ const journal = journals.find((candidate) => candidate.runId === input.obsoleteRunId);
6588
+ if (!journal) {
6411
6589
  workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
6412
6590
  }
6413
6591
  const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
@@ -6416,17 +6594,14 @@ async function workflowRecover(helpers, input = {}) {
6416
6594
  reasons: [reason],
6417
6595
  classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
6418
6596
  };
6419
- archiveWorkflowRun(root, entry.journal.runId, classification);
6597
+ archiveWorkflowRun(root, journal.runId, classification);
6420
6598
  return {
6421
- runId: entry.journal.runId,
6422
- command: entry.journal.command,
6599
+ runId: journal.runId,
6600
+ command: journal.command,
6423
6601
  reason
6424
6602
  };
6425
6603
  })() : null;
6426
- const effectiveClassifiedRuns = markedObsoleteRun ? classifyWorkflowRunJournals(root, {
6427
- currentBranch: session.branchName,
6428
- currentHeads
6429
- }) : classifiedRuns;
6604
+ const effectiveClassifiedRuns = markedObsoleteRun ? classifiedRuns.filter((entry) => entry.journal.runId !== markedObsoleteRun.runId) : classifiedRuns;
6430
6605
  const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
6431
6606
  runId: journal.runId,
6432
6607
  command: journal.command,
@@ -30,7 +30,11 @@ export type TreeseedWorkflowRunStep = {
30
30
  branch: string | null;
31
31
  resumable: boolean;
32
32
  status: 'pending' | 'completed' | 'skipped';
33
+ startedAt?: string | null;
33
34
  completedAt: string | null;
35
+ elapsedMs?: number | null;
36
+ retryCount?: number;
37
+ lastFailure?: string | null;
34
38
  data: Record<string, unknown> | null;
35
39
  };
36
40
  export type TreeseedWorkflowRunJournal = {
@@ -435,6 +435,18 @@ function collectSaveExpectedHeads(journal) {
435
435
  }
436
436
  return heads;
437
437
  }
438
+ function collectStageExpectedHeads(journal) {
439
+ const heads = {};
440
+ const promotion = stringRecord(journal.steps.find((step) => step.id === "promote-to-staging")?.data);
441
+ const results = Array.isArray(promotion?.results) ? promotion.results : [];
442
+ for (const entry of results) {
443
+ const repo = stringRecord(entry);
444
+ if (typeof repo?.name === "string" && typeof repo.commitSha === "string" && repo.verified === true) {
445
+ heads[repo.name] = repo.commitSha;
446
+ }
447
+ }
448
+ return heads;
449
+ }
438
450
  function classifyWorkflowRunJournal(journal, options = {}) {
439
451
  const reasons = [];
440
452
  const now = options.now ?? nowIso();
@@ -516,6 +528,15 @@ function classifyWorkflowRunJournal(journal, options = {}) {
516
528
  }
517
529
  }
518
530
  }
531
+ if (journal.command === "stage" && options.currentHeads) {
532
+ const expectedHeads = collectStageExpectedHeads(journal);
533
+ for (const [name, expectedHead] of Object.entries(expectedHeads)) {
534
+ const currentHead = options.currentHeads[name];
535
+ if (currentHead && currentHead !== expectedHead) {
536
+ reasons.push(`${name} head changed from staged candidate ${expectedHead} to ${currentHead}`);
537
+ }
538
+ }
539
+ }
519
540
  return {
520
541
  state: reasons.length > 0 ? "stale" : "resumable",
521
542
  reasons: reasons.length > 0 ? reasons : releaseGateOnlyCompletion ? ["release commits already exist; remaining release gates can be rechecked"] : ["workflow run can be resumed"],
@@ -598,7 +619,11 @@ function createWorkflowRunJournal(root, options) {
598
619
  steps: options.steps.map((step) => ({
599
620
  ...step,
600
621
  status: "pending",
622
+ startedAt: null,
601
623
  completedAt: null,
624
+ elapsedMs: null,
625
+ retryCount: 0,
626
+ lastFailure: null,
602
627
  data: null
603
628
  })),
604
629
  failure: null,
@@ -10,7 +10,7 @@ export { readTreeseedVerificationCache, treeseedVerificationCacheKey, writeTrees
10
10
  export { assertDeploymentInitialized, cleanupDestroyedState, createBranchPreviewDeployTarget, createPersistentDeployTarget, deployTargetLabel, destroyCloudflareResources, ensureGeneratedWranglerConfig, finalizeDeploymentState, loadDeployState, printDeploySummary, printDestroySummary, provisionCloudflareResources, runRemoteD1Migrations, syncCloudflareSecrets, validateDeployPrerequisites, validateDestroyPrerequisites, } from './operations/services/deploy.js';
11
11
  export { assertCleanWorktree, assertFeatureBranch, branchExists, checkoutBranch, createFeatureBranchFromStaging, currentManagedBranch, deleteLocalBranch, deleteRemoteBranch, ensureLocalBranchTracking, gitWorkflowRoot, listTaskBranches, mergeCurrentBranchIntoStaging, mergeStagingIntoMain, prepareReleaseBranches, PRODUCTION_BRANCH, pushBranch, remoteBranchExists, STAGING_BRANCH, syncBranchWithOrigin, waitForStagingAutomation, } from './operations/services/git-workflow.js';
12
12
  export { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, } from './operations/services/runtime-tools.js';
13
- export { configuredRailwayServices, validateRailwayDeployPrerequisites, } from './operations/services/railway-deploy.js';
13
+ export { configuredRailwayServices, validateRailwayDeployPrerequisites, waitForRailwayManagedDeploymentsSettled, } from './operations/services/railway-deploy.js';
14
14
  export { getRailwayAuthProfile, listRailwayEnvironments, listRailwayProjects, listRailwayServices, listRailwayVariables, resolveRailwayApiToken, resolveRailwayApiUrl, resolveRailwayWorkspace, resolveRailwayWorkspaceContext, } from './operations/services/railway-api.js';
15
15
  export { githubRepositoryCredentialEnvName, resolveGitHubCredentialForRepository, type TreeseedGitHubCredentialResolution, } from './operations/services/github-credentials.js';
16
16
  export { createGitHubApiClient, ensureGitHubActionsEnvironment, getLatestGitHubWorkflowRun, listGitHubEnvironmentSecretNames, listGitHubEnvironmentVariableNames, type GitHubWorkflowDispatchResult, type GitHubWorkflowRunSummary, } from './operations/services/github-api.js';
@@ -113,7 +113,8 @@ import {
113
113
  } from "./operations/services/runtime-tools.js";
114
114
  import {
115
115
  configuredRailwayServices,
116
- validateRailwayDeployPrerequisites
116
+ validateRailwayDeployPrerequisites,
117
+ waitForRailwayManagedDeploymentsSettled
117
118
  } from "./operations/services/railway-deploy.js";
118
119
  import {
119
120
  getRailwayAuthProfile,
@@ -379,6 +380,7 @@ export {
379
380
  validateRailwayDeployPrerequisites,
380
381
  validateTreeseedPackageManifests,
381
382
  verifyTreeseedPackageArtifact,
383
+ waitForRailwayManagedDeploymentsSettled,
382
384
  waitForStagingAutomation,
383
385
  warnDeprecatedTreeseedLocalEnvFiles,
384
386
  withTreeseedKeyAgentAutopromptDisabled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.59",
3
+ "version": "0.12.61",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -66,7 +66,6 @@
66
66
  "@github/copilot": "1.0.39",
67
67
  "@github/copilot-language-server": "1.480.0",
68
68
  "@github/copilot-sdk": "0.3.0",
69
- "@railway/cli": "5.23.2",
70
69
  "@remotion/bundler": "4.0.477",
71
70
  "@remotion/renderer": "4.0.477",
72
71
  "drizzle-orm": "^0.45.2",