@treeseed/sdk 0.12.60 → 0.12.62

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 -245
  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 +5 -5
  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 +51 -15
  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 +2 -2
  22. package/dist/operations/services/railway-deploy.js +36 -91
  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 +519 -684
  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 +147 -0
  35. package/dist/reconcile/providers/railway-iac.js +289 -16
  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 +441 -105
  40. package/dist/workflow/runs.d.ts +5 -0
  41. package/dist/workflow/runs.js +9 -3
  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,
@@ -479,6 +478,14 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
479
478
  env: gateEnv,
480
479
  onProgress: options.onProgress
481
480
  });
481
+ } else if (result.status === "completed" && result.conclusion !== "success" && options.retryFailedOnce) {
482
+ const retry = await rerunGitHubActionsFailedJobs(result, gateEnv);
483
+ options.onProgress?.(`[${operation}][gate][${gateWithTimeout.name}] Retrying failed jobs once for adopted immutable release run ${retry.runId}.`);
484
+ result = await waitForGitHubActionsGate(gateWithTimeout, {
485
+ operation,
486
+ env: gateEnv,
487
+ onProgress: options.onProgress
488
+ });
482
489
  }
483
490
  const normalized = {
484
491
  name: gateWithTimeout.name,
@@ -551,11 +558,12 @@ function selectorFromWorkflowHostingGraph(graph) {
551
558
  ].filter((hostId) => hostId !== "smtp" && hostId !== "local-process" && hostId !== "local-docker"))],
552
559
  serviceId: [...new Set(graph.units.flatMap((unit) => [
553
560
  unit.id,
561
+ typeof unit.config.poolKey === "string" ? unit.config.poolKey : null,
554
562
  typeof unit.config.serviceName === "string" ? unit.config.serviceName : null
555
563
  ]).concat(domainServiceIds).filter((value) => Boolean(value)))],
556
564
  serviceType: [...new Set(graph.units.flatMap((unit) => {
557
565
  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"];
566
+ if (unit.id === "operationsRunner" || unit.config.poolKey === "operationsRunner") return ["operations-runner-runtime", "railway-service:operations-runner"];
559
567
  if (unit.placement === "runner-capacity") return ["api-runtime", "operations-runner-runtime", "railway-service:api", "railway-service:operations-runner"];
560
568
  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
569
  return [];
@@ -707,6 +715,40 @@ ${liveFailures.join("\n")}`, {
707
715
  }
708
716
  return live;
709
717
  }
718
+ async function verifyReleaseApiEnvironmentIsolation(root, helpers, releaseImageRefs) {
719
+ const reports = {};
720
+ for (const environment of ["prod", "staging"]) {
721
+ const env = {
722
+ ...helpers.context.env,
723
+ ...collectTreeseedConfigSeedValues(root, environment, helpers.context.env),
724
+ ...environment === "prod" ? releaseImageRefs : {}
725
+ };
726
+ helpers.write(`[release][railway] read-only ${environment} source-invariance verification started.`, "stderr");
727
+ const report = await collectTreeseedLiveHostedServiceChecks({
728
+ tenantRoot: root,
729
+ target: environment,
730
+ appId: "api",
731
+ serviceKeys: ["api", "operationsRunner", "public-treedx-node-01"],
732
+ strict: true,
733
+ requireLiveRailway: true,
734
+ requireLiveHttp: true,
735
+ env
736
+ });
737
+ const failures = [
738
+ ...report.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
739
+ ...report.liveObservation.issues
740
+ ];
741
+ if (failures.length > 0) {
742
+ workflowError("release", "hosted_live_verification_failed", `${environment} API source-invariance verification failed after production deployment:
743
+ ${failures.join("\n")}`, {
744
+ details: { environment, report }
745
+ });
746
+ }
747
+ reports[environment] = report;
748
+ helpers.write(`[release][railway] read-only ${environment} source-invariance verification passed.`, "stderr");
749
+ }
750
+ return { status: "verified", order: ["prod", "staging"], reports };
751
+ }
710
752
  function productionReleaseImageRefEnv(selectedVersions) {
711
753
  const refs = {};
712
754
  const apiVersion = selectedVersions.get("@treeseed/api");
@@ -1285,6 +1327,62 @@ function ensureReleaseTag(repoDir, tagName, commitSha, message) {
1285
1327
  remote: remoteCommit ? "existing" : "pushed"
1286
1328
  };
1287
1329
  }
1330
+ async function adoptPublishedPackageRelease(pkg, version) {
1331
+ const commitSha = tagCommitSha(pkg.dir, version);
1332
+ if (!commitSha) return null;
1333
+ const remoteCommit = remoteTagCommit(pkg.dir, version);
1334
+ if (remoteCommit !== commitSha) {
1335
+ throw new Error(`Release tag ${version} for ${pkg.name} is inconsistent: local=${commitSha}, origin=${remoteCommit ?? "(missing)"}.`);
1336
+ }
1337
+ let taggedManifest;
1338
+ try {
1339
+ taggedManifest = JSON.parse(runGit(["show", `${commitSha}:package.json`], { cwd: pkg.dir, capture: true }));
1340
+ } catch (error) {
1341
+ throw new Error(`Release tag ${version} for ${pkg.name} does not contain a valid package.json: ${error instanceof Error ? error.message : String(error)}`);
1342
+ }
1343
+ if (taggedManifest.name !== pkg.name || taggedManifest.version !== version) {
1344
+ throw new Error(`Release tag ${version} does not identify ${pkg.name}@${version}.`);
1345
+ }
1346
+ if (hasMeaningfulChanges(pkg.dir)) {
1347
+ throw new Error(`Cannot adopt published ${pkg.name}@${version} while ${pkg.dir} has uncommitted changes.`);
1348
+ }
1349
+ const publishedArtifacts = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
1350
+ const previousStagingHead = remoteHeadCommit(pkg.dir, STAGING_BRANCH);
1351
+ if (previousStagingHead !== commitSha) {
1352
+ runGit([
1353
+ "push",
1354
+ `--force-with-lease=refs/heads/${STAGING_BRANCH}:${previousStagingHead}`,
1355
+ "origin",
1356
+ `${commitSha}:refs/heads/${STAGING_BRANCH}`
1357
+ ], { cwd: pkg.dir });
1358
+ }
1359
+ const previousProductionHead = remoteHeadCommit(pkg.dir, PRODUCTION_BRANCH);
1360
+ if (previousProductionHead !== commitSha) {
1361
+ promoteCommitToProductionBranch(pkg.dir, commitSha);
1362
+ }
1363
+ if (currentBranch(pkg.dir) !== STAGING_BRANCH) {
1364
+ checkoutBranch(pkg.dir, STAGING_BRANCH);
1365
+ }
1366
+ if (headCommit(pkg.dir) !== commitSha) {
1367
+ runGit(["reset", "--hard", commitSha], { cwd: pkg.dir });
1368
+ }
1369
+ const observedStagingHead = remoteHeadCommit(pkg.dir, STAGING_BRANCH);
1370
+ const observedProductionHead = remoteHeadCommit(pkg.dir, PRODUCTION_BRANCH);
1371
+ if (headCommit(pkg.dir) !== commitSha || observedStagingHead !== commitSha || observedProductionHead !== commitSha) {
1372
+ throw new Error(`Published release adoption failed for ${pkg.name}@${version}; local=${headCommit(pkg.dir)}, staging=${observedStagingHead}, main=${observedProductionHead}, expected=${commitSha}.`);
1373
+ }
1374
+ return {
1375
+ name: pkg.name,
1376
+ version,
1377
+ commit: { commitSha, status: "adopted-published-tag" },
1378
+ tag: { tagName: version, local: "existing", remote: "existing" },
1379
+ branches: {
1380
+ staging: previousStagingHead === commitSha ? "existing" : "restored",
1381
+ production: previousProductionHead === commitSha ? "existing" : "restored"
1382
+ },
1383
+ publishedArtifacts
1384
+ };
1385
+ }
1288
1386
  function promoteCommitToProductionBranch(repoDir, commitSha) {
1289
1387
  const expectedBefore = remoteBranchExists(repoDir, PRODUCTION_BRANCH) ? remoteHeadCommit(repoDir, PRODUCTION_BRANCH) : null;
1290
1388
  const lease = expectedBefore ? `--force-with-lease=refs/heads/${PRODUCTION_BRANCH}:${expectedBefore}` : "--force-with-lease";
@@ -1797,7 +1895,32 @@ function gateForSavedRootReport(report, branch, scope) {
1797
1895
  }
1798
1896
  function findAutoResumableTaskRun(root, command, branch) {
1799
1897
  if (!branch) return null;
1800
- return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
1898
+ const currentHeads = Object.fromEntries([
1899
+ ["@treeseed/market", runGit(["rev-parse", "HEAD"], { cwd: repoRoot(root), capture: true }).trim()],
1900
+ ...checkedOutWorkspacePackageRepos(root).map((repo) => [
1901
+ repo.name,
1902
+ runGit(["rev-parse", "HEAD"], { cwd: repo.dir, capture: true }).trim()
1903
+ ])
1904
+ ]);
1905
+ return listInterruptedWorkflowRuns(root).find((journal) => {
1906
+ if (journal.command !== command || !journal.resumable || journal.session.branchName !== branch) {
1907
+ return false;
1908
+ }
1909
+ const classification = classifyWorkflowRunJournal(journal, {
1910
+ currentBranch: branch,
1911
+ currentHeads
1912
+ });
1913
+ if (classification.state === "resumable") {
1914
+ return true;
1915
+ }
1916
+ if (classification.state === "stale") {
1917
+ archiveWorkflowRun(root, journal.runId, {
1918
+ ...classification,
1919
+ reasons: [`${command} implicit resume skipped stale failed run`, ...classification.reasons]
1920
+ });
1921
+ }
1922
+ return false;
1923
+ }) ?? null;
1801
1924
  }
1802
1925
  function rejectImplicitWorkflowResume(operation, journal) {
1803
1926
  if (!journal) return;
@@ -1980,7 +2103,8 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
1980
2103
  }
1981
2104
  const classification = classifyWorkflowRunJournal(journal, {
1982
2105
  currentBranch: branch,
1983
- currentHeads
2106
+ currentHeads,
2107
+ acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
1984
2108
  });
1985
2109
  if (classification.state !== "resumable") {
1986
2110
  if (options.archiveStale && classification.state === "stale") {
@@ -2002,6 +2126,45 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, opt
2002
2126
  return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
2003
2127
  }) ?? null;
2004
2128
  }
2129
+ function acceptedPublishedReleaseHeads(root, journal, currentHeads) {
2130
+ if (journal.command !== "release") return {};
2131
+ const plan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
2132
+ const plannedVersions = stringRecord(plan?.plannedVersions);
2133
+ if (!plannedVersions) return {};
2134
+ const repos = new Map(checkedOutWorkspacePackageRepos(root).map((repo) => [repo.name, repo.dir]));
2135
+ const accepted = {};
2136
+ for (const [name, versionValue] of Object.entries(plannedVersions)) {
2137
+ if (name === "@treeseed/market" || typeof versionValue !== "string") continue;
2138
+ const repoDir = repos.get(name);
2139
+ const currentHead = currentHeads[name];
2140
+ if (!repoDir || !currentHead) continue;
2141
+ let manifestVersion = null;
2142
+ try {
2143
+ manifestVersion = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8")).version ?? null;
2144
+ } catch {
2145
+ continue;
2146
+ }
2147
+ const tagHead = tagCommitSha(repoDir, versionValue);
2148
+ const remoteTagHead = remoteTagCommit(repoDir, versionValue);
2149
+ const productionHead = remoteHeadCommit(repoDir, PRODUCTION_BRANCH);
2150
+ const stagingHead = remoteHeadCommit(repoDir, STAGING_BRANCH);
2151
+ const exactPublishedHead = tagHead === currentHead && productionHead === currentHead && stagingHead === currentHead;
2152
+ const boundedInterruptedRetry = tagHead.length > 0 && remoteTagHead === tagHead && !hasMeaningfulChanges(repoDir) && [productionHead, stagingHead].every((head) => head === currentHead || head === tagHead);
2153
+ if (manifestVersion !== versionValue || !exactPublishedHead && !boundedInterruptedRetry) {
2154
+ if (process.env.TREESEED_RECONCILE_TRACE === "1") {
2155
+ process.stderr.write(`[release][resume-adoption] package=${name} accepted=false manifest=${manifestVersion === versionValue} tag=${tagHead === currentHead} remoteTag=${remoteTagHead === tagHead} main=${productionHead === currentHead} staging=${stagingHead === currentHead} bounded=${boundedInterruptedRetry}
2156
+ `);
2157
+ }
2158
+ continue;
2159
+ }
2160
+ accepted[name] = currentHead;
2161
+ if (process.env.TREESEED_RECONCILE_TRACE === "1") {
2162
+ process.stderr.write(`[release][resume-adoption] package=${name} accepted=true
2163
+ `);
2164
+ }
2165
+ }
2166
+ return accepted;
2167
+ }
2005
2168
  async function executeJournalStep(root, runId, stepId, action, options = {}) {
2006
2169
  const current = readWorkflowRunJournal(root, runId);
2007
2170
  const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
@@ -2011,17 +2174,48 @@ async function executeJournalStep(root, runId, stepId, action, options = {}) {
2011
2174
  if (step.status === "completed" && !options.rerunCompleted) {
2012
2175
  return step.data ?? null;
2013
2176
  }
2014
- const data = await Promise.resolve(action());
2177
+ const startedAt = /* @__PURE__ */ new Date();
2178
+ const retryCount = Number(step.retryCount ?? 0) + (step.startedAt ? 1 : 0);
2179
+ updateWorkflowRunJournal(root, runId, (journal) => ({
2180
+ ...journal,
2181
+ steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, startedAt: startedAt.toISOString(), retryCount, lastFailure: null } : entry)
2182
+ }));
2183
+ refreshWorkflowLock(root, runId);
2184
+ const lockHeartbeat = setInterval(() => refreshWorkflowLock(root, runId), 15e3);
2185
+ lockHeartbeat.unref();
2186
+ process.stderr.write(`[workflow][step] start ${stepId} attempt=${retryCount + 1}
2187
+ `);
2188
+ let data;
2189
+ try {
2190
+ data = await Promise.resolve(action());
2191
+ } catch (error) {
2192
+ clearInterval(lockHeartbeat);
2193
+ const elapsedMs2 = Date.now() - startedAt.getTime();
2194
+ const message = error instanceof Error ? error.message : String(error);
2195
+ updateWorkflowRunJournal(root, runId, (journal) => ({
2196
+ ...journal,
2197
+ steps: journal.steps.map((entry) => entry.id === stepId ? { ...entry, elapsedMs: elapsedMs2, lastFailure: message } : entry)
2198
+ }));
2199
+ process.stderr.write(`[workflow][step] fail ${stepId} elapsed=${Math.ceil(elapsedMs2 / 1e3)}s retries=${retryCount}
2200
+ `);
2201
+ throw error;
2202
+ }
2203
+ clearInterval(lockHeartbeat);
2204
+ const elapsedMs = Date.now() - startedAt.getTime();
2015
2205
  updateWorkflowRunJournal(root, runId, (journal) => ({
2016
2206
  ...journal,
2017
2207
  steps: journal.steps.map((entry) => entry.id === stepId ? {
2018
2208
  ...entry,
2019
2209
  status: "completed",
2020
2210
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2211
+ elapsedMs,
2212
+ lastFailure: null,
2021
2213
  data: data ?? null
2022
2214
  } : entry)
2023
2215
  }));
2024
2216
  refreshWorkflowLock(root, runId);
2217
+ process.stderr.write(`[workflow][step] complete ${stepId} elapsed=${Math.ceil(elapsedMs / 1e3)}s retries=${retryCount}
2218
+ `);
2025
2219
  return data;
2026
2220
  }
2027
2221
  function skipJournalStep(root, runId, stepId, data = null) {
@@ -2185,7 +2379,7 @@ function productionPackageDeployGates(root, versions) {
2185
2379
  })];
2186
2380
  });
2187
2381
  }
2188
- function prepareAdapterReleaseMetadata(root, pkg, version) {
2382
+ async function prepareAdapterReleaseMetadata(root, pkg, version) {
2189
2383
  const adapter = discoverTreeseedPackageAdapters(root).find((entry) => entry.id === pkg.name || entry.name === pkg.name);
2190
2384
  if (adapter?.kind === "beam-elixir-rust" && existsSync(resolve(pkg.dir, "scripts", "bump-release-version.ts"))) {
2191
2385
  const tsx = resolve(root, "node_modules/.bin/tsx");
@@ -2199,7 +2393,7 @@ function prepareAdapterReleaseMetadata(root, pkg, version) {
2199
2393
  return {
2200
2394
  status: "npm-install",
2201
2395
  adapter: adapter?.id ?? pkg.name,
2202
- ...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2396
+ ...await runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
2203
2397
  };
2204
2398
  }
2205
2399
  return { status: "skipped", adapter: adapter?.id ?? pkg.name, reason: "no package metadata updater" };
@@ -2237,42 +2431,96 @@ function npmCommandForWorkflowSpawn(args) {
2237
2431
  ]
2238
2432
  };
2239
2433
  }
2240
- function runReleaseNpmInstall(repoDir, options = {}) {
2241
- if (shouldSkipReleaseInstall()) {
2242
- return { status: "skipped", reason: "disabled" };
2434
+ function lockfileRootMatchesManifest(repoDir) {
2435
+ try {
2436
+ const manifest = JSON.parse(readFileSync(resolve(repoDir, "package.json"), "utf8"));
2437
+ const lockfile = JSON.parse(readFileSync(resolve(repoDir, "package-lock.json"), "utf8"));
2438
+ const root = lockfile.packages?.[""];
2439
+ if (!root || root.version !== manifest.version) return false;
2440
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
2441
+ const expected = manifest[field] ?? {};
2442
+ const observed = root[field] ?? {};
2443
+ if (JSON.stringify(expected) !== JSON.stringify(observed)) return false;
2444
+ }
2445
+ return true;
2446
+ } catch {
2447
+ return false;
2243
2448
  }
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, {
2449
+ }
2450
+ function waitForReleaseInstall(command, args, repoDir, attempt) {
2451
+ return new Promise((resolvePromise) => {
2452
+ const startedAt = Date.now();
2453
+ let settled = false;
2454
+ const child = spawn(command, args, {
2249
2455
  cwd: repoDir,
2250
2456
  env: {
2251
2457
  ...process.env,
2252
2458
  npm_config_audit: "false",
2253
- npm_config_fetch_retries: "4",
2459
+ npm_config_fetch_retries: "2",
2254
2460
  npm_config_fund: "false",
2255
- npm_config_foreground_scripts: "true",
2461
+ npm_config_foreground_scripts: "false",
2256
2462
  npm_config_loglevel: "warn",
2257
2463
  npm_config_maxsockets: "4",
2258
- npm_config_prefer_online: "true",
2464
+ npm_config_prefer_offline: "true",
2259
2465
  npm_config_progress: "false"
2260
2466
  },
2261
- stdio: "pipe",
2262
- encoding: "utf8"
2467
+ stdio: ["ignore", "pipe", "pipe"]
2263
2468
  });
2469
+ const output = [];
2470
+ child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
2471
+ child.stderr?.on("data", (chunk) => output.push(Buffer.from(chunk)));
2472
+ const heartbeat = setInterval(() => {
2473
+ process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
2474
+ `);
2475
+ }, 15e3);
2476
+ child.once("close", (status) => {
2477
+ if (settled) return;
2478
+ settled = true;
2479
+ clearInterval(heartbeat);
2480
+ resolvePromise({ status, detail: Buffer.concat(output).toString("utf8").trim() });
2481
+ });
2482
+ child.once("error", (error) => {
2483
+ if (settled) return;
2484
+ settled = true;
2485
+ clearInterval(heartbeat);
2486
+ output.push(Buffer.from(error.message));
2487
+ resolvePromise({ status: null, detail: Buffer.concat(output).toString("utf8").trim() });
2488
+ });
2489
+ });
2490
+ }
2491
+ async function runReleaseNpmInstall(repoDir, options = {}) {
2492
+ if (shouldSkipReleaseInstall()) {
2493
+ return { status: "skipped", reason: "disabled" };
2494
+ }
2495
+ if (repoDir === options.workspaceRoot && lockfileRootMatchesManifest(repoDir)) {
2496
+ return { status: "skipped", reason: "root-lockfile-already-matches", attempts: 0 };
2497
+ }
2498
+ const baseArgs = ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
2499
+ const propagationDelaysMs = [15e3, 3e4, 6e4, 12e4, 18e4];
2500
+ const startedAt = Date.now();
2501
+ let lastDetail = "";
2502
+ for (let attempt = 1; attempt <= propagationDelaysMs.length + 1; attempt += 1) {
2503
+ const args = [...baseArgs, attempt === 1 ? "--prefer-offline" : "--prefer-online"];
2504
+ const spawnCommand = npmCommandForWorkflowSpawn(args);
2505
+ process.stderr.write(`[release][restore] repository=${repoDir} phase=package-lock attempt=${attempt} elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
2506
+ `);
2507
+ const result = await waitForReleaseInstall(spawnCommand.command, spawnCommand.args, repoDir, attempt);
2264
2508
  if (result.status === 0) {
2265
2509
  return { status: "completed", reason: null, attempts: attempt };
2266
2510
  }
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" });
2511
+ lastDetail = result.detail;
2512
+ const propagationDelayMs = propagationDelaysMs[attempt - 1];
2513
+ if (!/No matching version found|notarget|ETARGET|E404/u.test(lastDetail) || propagationDelayMs == null) break;
2514
+ let remainingMs = propagationDelayMs;
2515
+ while (remainingMs > 0) {
2516
+ process.stderr.write(`[release][restore] repository=${repoDir} phase=registry-propagation nextAttempt=${attempt + 1} remaining=${Math.ceil(remainingMs / 1e3)}s elapsed=${Math.ceil((Date.now() - startedAt) / 1e3)}s
2517
+ `);
2518
+ const sliceMs = Math.min(15e3, remainingMs);
2519
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, sliceMs));
2520
+ remainingMs -= sliceMs;
2521
+ }
2274
2522
  }
2275
- throw new Error(lastDetail || `npm ${args.join(" ")} failed`);
2523
+ throw new Error(lastDetail || `npm ${baseArgs.join(" ")} failed`);
2276
2524
  }
2277
2525
  function pathIsWithin(parent, candidate) {
2278
2526
  const path = relative(parent, candidate);
@@ -4543,7 +4791,7 @@ async function workflowSave(helpers, input) {
4543
4791
  const recursiveWorkspace = session.mode === "recursive-workspace";
4544
4792
  const mode = session.mode;
4545
4793
  const executionMode = normalizeExecutionMode(input);
4546
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
4794
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
4547
4795
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
4548
4796
  rejectImplicitWorkflowResume("save", autoResumeRun);
4549
4797
  const planAutoResumeRun = null;
@@ -4708,7 +4956,10 @@ async function workflowSave(helpers, input) {
4708
4956
  resumable: true
4709
4957
  }] : []
4710
4958
  ],
4711
- autoResumeRun ? {
4959
+ explicitResumeRunId ? {
4960
+ ...helpers.context,
4961
+ workflow: { ...helpers.context.workflow ?? {}, resumeRunId: explicitResumeRunId }
4962
+ } : autoResumeRun ? {
4712
4963
  ...helpers.context,
4713
4964
  workflow: {
4714
4965
  ...helpers.context.workflow ?? {},
@@ -4994,7 +5245,7 @@ async function workflowClose(helpers, input) {
4994
5245
  const root = workspaceRoot(tenantRoot);
4995
5246
  const executionMode = normalizeExecutionMode(input);
4996
5247
  const session = resolveTreeseedWorkflowSession(root);
4997
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5248
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
4998
5249
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
4999
5250
  rejectImplicitWorkflowResume("close", autoResumeRun);
5000
5251
  const planAutoResumeRun = null;
@@ -5081,7 +5332,13 @@ async function workflowClose(helpers, input) {
5081
5332
  { id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
5082
5333
  ...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
5083
5334
  ],
5084
- autoResumeRun ? {
5335
+ explicitResumeRunId ? {
5336
+ ...helpers.context,
5337
+ workflow: {
5338
+ ...helpers.context.workflow ?? {},
5339
+ resumeRunId: explicitResumeRunId
5340
+ }
5341
+ } : autoResumeRun ? {
5085
5342
  ...helpers.context,
5086
5343
  workflow: {
5087
5344
  ...helpers.context.workflow ?? {},
@@ -5192,6 +5449,7 @@ async function workflowClose(helpers, input) {
5192
5449
  }
5193
5450
  function stagingCandidateWorkflowGates(root, manifest) {
5194
5451
  const gates = [];
5452
+ const adapters = discoverTreeseedPackageAdapters(root);
5195
5453
  const add = (name, repoPath, headSha, workflow, deploy = false) => {
5196
5454
  if (!workflowFileExists(repoPath, workflow)) return;
5197
5455
  const gate = { name, repoPath, workflow, branch: STAGING_BRANCH, headSha };
@@ -5199,10 +5457,11 @@ function stagingCandidateWorkflowGates(root, manifest) {
5199
5457
  };
5200
5458
  for (const pkg of manifest.packages) {
5201
5459
  const repoPath = resolve(root, pkg.path);
5460
+ const adapter = adapters.find((candidate) => candidate.id === pkg.name || candidate.name === pkg.name);
5202
5461
  if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit) {
5203
5462
  add(pkg.name, repoPath, pkg.commit, "verify.yml");
5204
5463
  }
5205
- if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5464
+ if (manifest.stagingHeadsBefore[pkg.name] !== pkg.commit && adapter?.capabilities.deploy === true && existsSync(resolve(repoPath, "treeseed.site.yaml"))) {
5206
5465
  add(pkg.name, repoPath, pkg.commit, "deploy.yml", true);
5207
5466
  }
5208
5467
  }
@@ -5476,7 +5735,7 @@ async function workflowStage(helpers, input) {
5476
5735
  const root = workspaceRoot(tenantRoot);
5477
5736
  const executionMode = normalizeExecutionMode(input);
5478
5737
  const session = resolveTreeseedWorkflowSession(root);
5479
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5738
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
5480
5739
  const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
5481
5740
  rejectImplicitWorkflowResume("stage", rawAutoResumeRun);
5482
5741
  const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
@@ -5891,7 +6150,9 @@ async function workflowRelease(helpers, input) {
5891
6150
  const rootRepo = createWorkspaceRootRepoReport(root);
5892
6151
  const packageReports = createWorkspacePackageReports(root);
5893
6152
  const releaseHelperRepos = checkedOutReleaseHelperRepos(root);
5894
- const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
6153
+ const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? input.resumeRunId ?? null;
6154
+ const explicitResumeJournal = explicitResumeRunId ? readWorkflowRunJournal(root, explicitResumeRunId) : null;
6155
+ const recordedReleasePlan = explicitResumeJournal?.command === "release" ? stringRecord(explicitResumeJournal.steps.find((step) => step.id === "release-plan")?.data) : null;
5895
6156
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: false }) : null;
5896
6157
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
5897
6158
  const effectiveInput = autoResumeRun ? {
@@ -5902,7 +6163,7 @@ async function workflowRelease(helpers, input) {
5902
6163
  const level = effectiveInput.bump ?? "patch";
5903
6164
  const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
5904
6165
  const packageSelection = session.packageSelection;
5905
- const plannedRelease = buildReleasePlanSnapshot({
6166
+ const plannedRelease = recordedReleasePlan ?? buildReleasePlanSnapshot({
5906
6167
  root,
5907
6168
  mode: session.mode,
5908
6169
  level,
@@ -5914,12 +6175,14 @@ async function workflowRelease(helpers, input) {
5914
6175
  blockers: []
5915
6176
  });
5916
6177
  const selectedPackageNames = releasePlanPackageSelection(plannedRelease.packageSelection).selected;
5917
- const blockers = collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
6178
+ const blockers = explicitResumeJournal ? [] : collectReleasePlanBlockers(session, session.mode, selectedPackageNames, {
5918
6179
  level,
5919
6180
  repairVersionLine: effectiveInput.repairVersionLine === true
5920
6181
  });
5921
- blockers.push(...collectReleaseHelperRepoBlockers(root));
5922
- blockers.push(...stageCandidateAttestationBlockers(root));
6182
+ if (!explicitResumeJournal) {
6183
+ blockers.push(...collectReleaseHelperRepoBlockers(root));
6184
+ blockers.push(...stageCandidateAttestationBlockers(root));
6185
+ }
5923
6186
  const selectedVersions = releasePlanVersionMap(plannedRelease.plannedVersions);
5924
6187
  const releaseImageVersions = productionReleaseImageRefVersions(root, selectedVersions);
5925
6188
  const releaseImageRefs = productionReleaseImageRefEnv(releaseImageVersions);
@@ -6018,13 +6281,20 @@ ${blockers.join("\n")}`, {
6018
6281
  },
6019
6282
  { 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
6283
  { 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 },
6284
+ { 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
6285
  { 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
6286
  { id: "release-root", description: `Release market ${plannedRelease.rootVersion}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
6023
6287
  { id: "publish-wait", description: "Wait for production release workflows", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
6024
6288
  { id: "release-back-merge", description: "Back-merge production release history into staging", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
6025
6289
  { id: "workspace-link", description: "Restore local workspace links after release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
6026
6290
  ],
6027
- autoResumeRun ? {
6291
+ explicitResumeRunId ? {
6292
+ ...helpers.context,
6293
+ workflow: {
6294
+ ...helpers.context.workflow ?? {},
6295
+ resumeRunId: explicitResumeRunId
6296
+ }
6297
+ } : autoResumeRun ? {
6028
6298
  ...helpers.context,
6029
6299
  workflow: {
6030
6300
  ...helpers.context.workflow ?? {},
@@ -6083,57 +6353,113 @@ ${rendered}`);
6083
6353
  });
6084
6354
  const packageReleases = [];
6085
6355
  const packageRepoByName = new Map(checkedOutWorkspacePackageRepos(root).map((entry) => [entry.name, entry]));
6086
- for (const packageName of selectedPackageNames) {
6356
+ const pendingPackageReleases = new Set(selectedPackageNames.filter((name) => selectedVersions.has(name) && packageRepoByName.has(name)));
6357
+ const completedPackageReleases = /* @__PURE__ */ new Set();
6358
+ const packageReleaseResults = /* @__PURE__ */ new Map();
6359
+ const selectedPackageDependencies = new Map([...pendingPackageReleases].map((packageName) => {
6087
6360
  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)
6361
+ if (!pkg) return [packageName, []];
6362
+ const manifest = readJsonFile(resolve(pkg.dir, "package.json"));
6363
+ if (!manifest) {
6364
+ throw new Error(`Release package manifest is missing or invalid for ${packageName}.`);
6365
+ }
6366
+ const dependencyNames = /* @__PURE__ */ new Set([
6367
+ ...Object.keys(manifest.dependencies ?? {}),
6368
+ ...Object.keys(manifest.optionalDependencies ?? {}),
6369
+ ...Object.keys(manifest.peerDependencies ?? {})
6370
+ ]);
6371
+ const dependencies = [...dependencyNames].filter((name) => pendingPackageReleases.has(name));
6372
+ if (packageName !== "@treeseed/sdk" && pendingPackageReleases.has("@treeseed/sdk") && !dependencies.includes("@treeseed/sdk")) {
6373
+ dependencies.push("@treeseed/sdk");
6374
+ }
6375
+ if (packageName === "@treeseed/api" && pendingPackageReleases.has("@treeseed/cli") && !dependencies.includes("@treeseed/cli")) {
6376
+ dependencies.push("@treeseed/cli");
6377
+ }
6378
+ return [packageName, dependencies];
6379
+ }));
6380
+ while (pendingPackageReleases.size > 0) {
6381
+ const eligible = selectedPackageNames.filter((packageName) => pendingPackageReleases.has(packageName) && (selectedPackageDependencies.get(packageName) ?? []).every((dependency) => completedPackageReleases.has(dependency)));
6382
+ if (eligible.length === 0) {
6383
+ throw new Error(`Release package dependency graph is cyclic or unresolved: ${[...pendingPackageReleases].join(", ")}.`);
6384
+ }
6385
+ const batch = eligible.slice(0, 2);
6386
+ helpers.write(`[release][packages] starting batch ${batch.join(", ")} (concurrency=${batch.length}/2).`, "stderr");
6387
+ const results = await Promise.all(batch.map(async (packageName) => {
6388
+ const pkg = packageRepoByName.get(packageName);
6389
+ const version = selectedVersions.get(pkg.name);
6390
+ const packageRelease = await executeJournalStep(root, workflowRun.runId, `release-${pkg.name}`, async () => {
6391
+ const adopted = await adoptPublishedPackageRelease(pkg, version);
6392
+ if (adopted) {
6393
+ helpers.write(`[release][packages] adopted verified published release ${pkg.name}@${version}.`, "stderr");
6394
+ if (pkg.name === "@treeseed/api") {
6395
+ const publishWait3 = await waitForWorkflowGates("release", [{
6396
+ name: pkg.name,
6397
+ repoPath: pkg.dir,
6398
+ workflow: releaseWorkflowForPackage(root, pkg.name),
6399
+ branch: version,
6400
+ headSha: String(stringRecord(adopted.commit)?.commitSha ?? tagCommitSha(pkg.dir, version))
6401
+ }], ciMode, {
6402
+ root,
6403
+ runId: workflowRun.runId,
6404
+ onProgress: (line, stream) => helpers.write(line, stream),
6405
+ retryFailedOnce: true
6406
+ });
6407
+ return { ...adopted, publishWait: publishWait3 };
6408
+ }
6409
+ return adopted;
6410
+ }
6411
+ const metadata = await prepareAdapterReleaseMetadata(root, pkg, version);
6412
+ const changelog = updateReleaseChangelog(pkg.dir, {
6413
+ version,
6414
+ sourceRef: `origin/${PRODUCTION_BRANCH}`,
6415
+ targetRef: "HEAD"
6416
+ });
6417
+ const commit = commitAllIfChanged(pkg.dir, releaseAdminMessage({
6418
+ subject: `release: ${pkg.name} ${version}`,
6419
+ version,
6420
+ tagName: version,
6421
+ sourceRef: STAGING_BRANCH,
6422
+ targetRef: PRODUCTION_BRANCH,
6423
+ changelog
6424
+ }));
6425
+ pushBranch(pkg.dir, STAGING_BRANCH);
6426
+ const promotion = promoteCommitToProductionBranch(pkg.dir, commit.commitSha);
6427
+ const tag = ensureReleaseTag(pkg.dir, version, commit.commitSha, `release: ${pkg.name} ${version}`);
6428
+ const publishGate = {
6429
+ name: pkg.name,
6430
+ repoPath: pkg.dir,
6431
+ workflow: releaseWorkflowForPackage(root, pkg.name),
6432
+ branch: version,
6433
+ headSha: commit.commitSha
6434
+ };
6435
+ const publishWait2 = await waitForWorkflowGates("release", [publishGate], ciMode, {
6436
+ root,
6437
+ runId: workflowRun.runId,
6438
+ onProgress: (line, stream) => helpers.write(line, stream)
6439
+ });
6440
+ const publishedArtifacts2 = await verifyPublishedReleaseArtifacts(/* @__PURE__ */ new Map([[pkg.name, version]]));
6441
+ return {
6442
+ name: pkg.name,
6443
+ path: relative(root, pkg.dir),
6444
+ version,
6445
+ changelog,
6446
+ metadata,
6447
+ commit,
6448
+ promotion,
6449
+ tag,
6450
+ publishWait: publishWait2,
6451
+ publishedArtifacts: publishedArtifacts2
6452
+ };
6120
6453
  });
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);
6454
+ return [packageName, packageRelease];
6455
+ }));
6456
+ for (const [packageName, packageRelease] of results) {
6457
+ pendingPackageReleases.delete(packageName);
6458
+ completedPackageReleases.add(packageName);
6459
+ packageReleaseResults.set(packageName, packageRelease);
6460
+ }
6136
6461
  }
6462
+ packageReleases.push(...selectedPackageNames.map((name) => packageReleaseResults.get(name)).filter((entry) => Boolean(entry)));
6137
6463
  const managedHelperReleases = await executeJournalStep(root, workflowRun.runId, "release-helper-repos", () => {
6138
6464
  const releases = releaseHelperRepos.map((repo) => releaseHelperRepoToProduction(repo));
6139
6465
  syncAllCheckedOutReleaseHelperRepos(root, STAGING_BRANCH);
@@ -6151,6 +6477,10 @@ ${rendered}`);
6151
6477
  onProgress: (line, stream) => helpers.write(line, stream)
6152
6478
  }).then((workflowGates) => ({ workflowGates }));
6153
6479
  });
6480
+ 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", {
6481
+ status: "skipped",
6482
+ reason: "--verify-deployed-resources was not requested"
6483
+ }), { status: "skipped", reason: "--verify-deployed-resources was not requested" });
6154
6484
  const productionImageRefs = await executeJournalStep(root, workflowRun.runId, "persist-production-image-refs", async () => {
6155
6485
  const persisted = persistProductionReleaseImageRefs(root, releaseImageRefs);
6156
6486
  if (helpers.context.env) {
@@ -6171,8 +6501,8 @@ ${failures.join("\n")}`, {
6171
6501
  }
6172
6502
  return { persisted, readiness };
6173
6503
  });
6174
- const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
6175
- const rootInstall = runReleaseNpmInstall(root, { workspaceRoot: root });
6504
+ const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", async () => {
6505
+ const rootInstall = await runReleaseNpmInstall(root, { workspaceRoot: root });
6176
6506
  const changelog = updateReleaseChangelog(repoRoot(root), {
6177
6507
  version: plannedRelease.rootVersion,
6178
6508
  sourceRef: `origin/${PRODUCTION_BRANCH}`,
@@ -6244,6 +6574,7 @@ ${failures.join("\n")}`, {
6244
6574
  publishWait: publishWait.workflowGates,
6245
6575
  publishedArtifacts,
6246
6576
  productionPackageDeployWorkflows,
6577
+ apiEnvironmentIsolation,
6247
6578
  productionImageRefs,
6248
6579
  backMerge,
6249
6580
  workspaceLinks,
@@ -6309,7 +6640,8 @@ async function workflowResume(helpers, input) {
6309
6640
  );
6310
6641
  const classification = classifyWorkflowRunJournal(journal, {
6311
6642
  currentBranch: session.branchName,
6312
- currentHeads
6643
+ currentHeads,
6644
+ acceptedReleaseHeads: acceptedPublishedReleaseHeads(root, journal, currentHeads)
6313
6645
  });
6314
6646
  if (classification.state !== "resumable") {
6315
6647
  workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
@@ -6339,7 +6671,10 @@ async function workflowResume(helpers, input) {
6339
6671
  case "stage":
6340
6672
  return workflowStage(resumedHelpers, journal.input);
6341
6673
  case "release":
6342
- return workflowRelease(resumedHelpers, journal.input);
6674
+ return workflowRelease(resumedHelpers, {
6675
+ ...journal.input,
6676
+ resumeRunId: runId
6677
+ });
6343
6678
  case "destroy":
6344
6679
  return workflowDestroy(resumedHelpers, journal.input);
6345
6680
  default:
@@ -6397,17 +6732,21 @@ async function workflowRecover(helpers, input = {}) {
6397
6732
  return null;
6398
6733
  }).filter((entry) => entry !== null);
6399
6734
  const journals = listWorkflowRunJournals(root);
6735
+ const actionableJournals = journals.filter((journal) => journal.status !== "completed" && !journal.classification?.archivedAt);
6400
6736
  const session = resolveTreeseedWorkflowSession(root);
6401
6737
  const currentHeads = Object.fromEntries(
6402
6738
  [createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
6403
6739
  );
6404
- const classifiedRuns = classifyWorkflowRunJournals(root, {
6405
- currentBranch: session.branchName,
6406
- currentHeads
6407
- });
6740
+ const classifiedRuns = actionableJournals.map((journal) => ({
6741
+ journal,
6742
+ classification: classifyWorkflowRunJournal(journal, {
6743
+ currentBranch: session.branchName,
6744
+ currentHeads
6745
+ })
6746
+ }));
6408
6747
  const markedObsoleteRun = input.obsoleteRunId ? (() => {
6409
- const entry = classifiedRuns.find((candidate) => candidate.journal.runId === input.obsoleteRunId);
6410
- if (!entry) {
6748
+ const journal = journals.find((candidate) => candidate.runId === input.obsoleteRunId);
6749
+ if (!journal) {
6411
6750
  workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
6412
6751
  }
6413
6752
  const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
@@ -6416,17 +6755,14 @@ async function workflowRecover(helpers, input = {}) {
6416
6755
  reasons: [reason],
6417
6756
  classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
6418
6757
  };
6419
- archiveWorkflowRun(root, entry.journal.runId, classification);
6758
+ archiveWorkflowRun(root, journal.runId, classification);
6420
6759
  return {
6421
- runId: entry.journal.runId,
6422
- command: entry.journal.command,
6760
+ runId: journal.runId,
6761
+ command: journal.command,
6423
6762
  reason
6424
6763
  };
6425
6764
  })() : null;
6426
- const effectiveClassifiedRuns = markedObsoleteRun ? classifyWorkflowRunJournals(root, {
6427
- currentBranch: session.branchName,
6428
- currentHeads
6429
- }) : classifiedRuns;
6765
+ const effectiveClassifiedRuns = markedObsoleteRun ? classifiedRuns.filter((entry) => entry.journal.runId !== markedObsoleteRun.runId) : classifiedRuns;
6430
6766
  const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
6431
6767
  runId: journal.runId,
6432
6768
  command: journal.command,