@treeseed/sdk 0.12.53 → 0.12.54

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.
@@ -83,7 +83,8 @@ function publicTreeDxSourcePolicy(input, config, launchEnv) {
83
83
  imageRef,
84
84
  imageTagRef: "TREESEED_PUBLIC_TREEDX_IMAGE_REF"
85
85
  };
86
- assertApiRailwaySourcePolicy(input.environment, { key: "public-treedx-node-01", serviceName: "public-treedx-node-01", ...policy2 });
86
+ const serviceName = input.environment === "prod" ? "public-treedx-node-production-01" : "public-treedx-node-01";
87
+ assertApiRailwaySourcePolicy(input.environment, { key: serviceName, serviceName, ...policy2 });
87
88
  return policy2;
88
89
  }
89
90
  const policy = {
@@ -174,7 +175,9 @@ function resolveRailwayServiceSourceRoot(input, serviceKey, service) {
174
175
  function railwaySourcePolicy(input, serviceKey, service, imageRef) {
175
176
  const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
176
177
  const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
177
- const serviceName = service.railway?.serviceName ?? null;
178
+ const baseServiceName = service.railway?.serviceName ?? null;
179
+ const configuredEnvironmentServiceName = service.environments?.[input.environment]?.railwayServiceName;
180
+ const serviceName = typeof configuredEnvironmentServiceName === "string" && configuredEnvironmentServiceName.trim() ? configuredEnvironmentServiceName.trim() : input.environment === "prod" && ["api", "operationsRunner"].includes(serviceKey) && baseServiceName ? `${baseServiceName}-production` : baseServiceName;
178
181
  const repository = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : readPackageRepository(resolveRailwayServiceSourceRoot(input, serviceKey, service)) ?? readPackageRepository(input.tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
179
182
  const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
180
183
  const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
@@ -248,14 +251,18 @@ function marketProjectGroup(environment, config) {
248
251
  const apiService = config.services?.api && typeof config.services.api === "object" ? config.services.api : null;
249
252
  const railwayProjectName = typeof apiService?.railway?.projectName === "string" && apiService.railway.projectName.trim() ? apiService.railway.projectName.trim() : void 0;
250
253
  const projectName = railwayProjectName ?? config.slug ?? "treeseed-api";
254
+ const environmentProjectName = (scope) => {
255
+ const configured = apiService?.environments?.[scope]?.railwayProjectName;
256
+ return typeof configured === "string" && configured.trim() ? configured.trim() : projectName;
257
+ };
251
258
  return {
252
259
  id: "treeseed-control-plane",
253
260
  label: "Treeseed control plane",
254
261
  hostId: environment === "local" ? "local-process" : "railway",
255
262
  environments: {
256
263
  local: { projectName: `${projectName}-local`, environmentName: "local" },
257
- staging: { projectName, environmentName: "staging" },
258
- prod: { projectName, environmentName: "production" }
264
+ staging: { projectName: environmentProjectName("staging"), environmentName: "staging" },
265
+ prod: { projectName: environmentProjectName("prod"), environmentName: "production" }
259
266
  },
260
267
  metadata: { stableProjectName: projectName }
261
268
  };
@@ -377,6 +384,9 @@ function buildProfileFromDeployConfig(input) {
377
384
  const imageRefEnv = railwayImageRefEnvForService(serviceKey);
378
385
  const imageRef = service.railway?.imageRef ?? (input.environment === "prod" && imageRefEnv ? launchEnv[imageRefEnv] ?? null : null) ?? defaultRailwayImageRefForService(serviceKey, input.environment) ?? null;
379
386
  const sourcePolicy = railwaySourcePolicy(input, serviceKey, service, imageRef);
387
+ const environmentConfig = service.environments?.[input.environment];
388
+ const baseServiceName = service.railway?.serviceName ?? null;
389
+ const effectiveServiceName = environmentConfig?.railwayServiceName ?? (input.environment === "prod" && ["api", "operationsRunner"].includes(serviceKey) && baseServiceName ? `${baseServiceName}-production` : baseServiceName);
380
390
  services.push({
381
391
  id: serviceKey,
382
392
  label: placement === "runner-capacity" ? "Runner Capacity" : serviceKey === "api" ? "API Runtime" : serviceKey,
@@ -400,7 +410,7 @@ function buildProfileFromDeployConfig(input) {
400
410
  volumeMountPath: service.railway?.volumeMountPath ?? null,
401
411
  runnerPool: service.railway?.runnerPool ?? null,
402
412
  resourceType: service.railway?.resourceType ?? null,
403
- serviceName: service.railway?.serviceName ?? null,
413
+ serviceName: effectiveServiceName,
404
414
  serviceTargets: service.railway?.serviceTargets ?? null
405
415
  },
406
416
  secretRefs: serviceKey === "treeseedDatabase" ? ["TREESEED_DATABASE_URL"] : [],
@@ -462,7 +472,7 @@ function buildProfileFromDeployConfig(input) {
462
472
  const treeDxSourcePolicy = publicTreeDxSourcePolicy(input, config, launchEnv);
463
473
  const treeDxNodeUnits = Array.from({ length: treeDxNodePool.bootstrapCount }, (_, offset) => {
464
474
  const nodeIndex = offset + 1;
465
- const serviceName = indexedName("public-treedx-node", nodeIndex);
475
+ const serviceName = indexedName(input.environment === "prod" ? "public-treedx-node-production" : "public-treedx-node", nodeIndex);
466
476
  return {
467
477
  id: serviceName,
468
478
  label: `Public TreeDX node ${String(nodeIndex).padStart(2, "0")}`,
@@ -143,7 +143,8 @@ function collectTreeseedDeploymentReadiness(options) {
143
143
  checks.push(check("hosting:api:healthcheckPath", api.config?.healthcheckPath, "/healthz", "API healthcheck path is /healthz.", "Set services.api.railway.healthcheckPath to /healthz.", "healthcheckPath"));
144
144
  }
145
145
  if (railwayApi) {
146
- checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, "treeseed-api", "Railway API service uses the canonical name.", "Set services.api.railway.serviceName to treeseed-api.", "serviceName"));
146
+ const expectedServiceName = environment === "prod" ? "treeseed-api-production" : "treeseed-api";
147
+ checks.push(check("railway-config:api:serviceName", railwayApi.serviceName, expectedServiceName, "Railway API service uses the environment-isolated canonical name.", `Set the API service identity to ${expectedServiceName} for ${environment}.`, "serviceName"));
147
148
  checks.push(check("railway-config:api:rootDirectory", relRoot(tenantRoot, railwayApi.rootDir), expectedApiRailwayRoot, "Railway API effective rootDirectory points at the API app.", "Set services.api.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
148
149
  }
149
150
  if (!runner) {
@@ -176,7 +177,8 @@ function collectTreeseedDeploymentReadiness(options) {
176
177
  checks.push(check("hosting:operationsRunner:volumeMountPath", runner.config?.volumeMountPath, "/data", "Runner volume mount path is stable.", "Set services.operationsRunner.railway.volumeMountPath to /data.", "volumeMountPath"));
177
178
  }
178
179
  if (railwayRunner) {
179
- checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, "treeseed-api-operations-runner-01", "Railway runner service instance uses the canonical name.", "Set the runner service instance name to treeseed-api-operations-runner-01.", "serviceName"));
180
+ const expectedServiceName = environment === "prod" ? "treeseed-api-operations-runner-production-01" : "treeseed-api-operations-runner-01";
181
+ checks.push(check("railway-config:operationsRunner:serviceName", railwayRunner.serviceName, expectedServiceName, "Railway runner service instance uses the environment-isolated canonical name.", `Set the runner service identity to ${expectedServiceName} for ${environment}.`, "serviceName"));
180
182
  checks.push(check("railway-config:operationsRunner:rootDirectory", relRoot(tenantRoot, railwayRunner.rootDir), expectedRunnerRailwayRoot, "Railway runner effective rootDirectory points at the API app.", "Set services.operationsRunner.railway.rootDir relative to the owning API manifest.", "rootDirectory"));
181
183
  }
182
184
  if (!database) {
@@ -166,6 +166,15 @@ export declare function ensureRemoteBranchFromBase(repoDir: any, branchName: any
166
166
  };
167
167
  export declare function deleteLocalBranch(repoDir: any, branchName: any): void;
168
168
  export declare function deleteRemoteBranch(repoDir: any, branchName: any): boolean;
169
+ export declare function deleteRemoteBranchIfMerged(repoDir: any, branchName: any, targetBranch: any, expectedHead: any, options?: {
170
+ fetch?: boolean;
171
+ }): boolean;
172
+ export declare function inspectMergedRemoteTaskBranches(repoDir: any): {
173
+ branch: string;
174
+ head: string;
175
+ current: boolean;
176
+ mergedInto: string | null;
177
+ }[];
169
178
  export declare function mergeCurrentBranchIntoStaging(cwd: any, featureBranch: any): {
170
179
  repoDir: string;
171
180
  targetBranch: string;
@@ -202,7 +211,9 @@ export declare function squashMergeBranchIntoStaging(cwd: any, featureBranch: an
202
211
  export declare function currentManagedBranch(cwd?: any): string;
203
212
  export declare function isTaskBranch(branchName: any): boolean;
204
213
  export declare function assertFeatureBranch(cwd?: any): string;
205
- export declare function listTaskBranches(repoDir: any): {
214
+ export declare function listTaskBranches(repoDir: any, options?: {
215
+ fetch?: boolean;
216
+ }): {
206
217
  name: string;
207
218
  head: string;
208
219
  lastCommitDate: string;
@@ -564,6 +564,39 @@ function deleteRemoteBranch(repoDir, branchName) {
564
564
  runGit(["push", "origin", "--delete", branchName], { cwd: repoDir });
565
565
  return true;
566
566
  }
567
+ function deleteRemoteBranchIfMerged(repoDir, branchName, targetBranch, expectedHead, options = {}) {
568
+ if (!remoteBranchExists(repoDir, branchName)) return false;
569
+ if (options.fetch !== false) fetchOrigin(repoDir);
570
+ const observedHead = remoteHeadCommit(repoDir, branchName);
571
+ if (!expectedHead || observedHead !== expectedHead) {
572
+ throw new Error(`Refusing to delete origin/${branchName}: expected ${expectedHead || "(missing)"}, observed ${observedHead || "(missing)"}.`);
573
+ }
574
+ const targetHead = remoteHeadCommit(repoDir, targetBranch);
575
+ if (!targetHead) {
576
+ throw new Error(`Refusing to delete origin/${branchName}: origin/${targetBranch} is missing.`);
577
+ }
578
+ const ancestry = runGitAllowFailure(["merge-base", "--is-ancestor", observedHead, targetHead], { cwd: repoDir });
579
+ if (ancestry.status !== 0) {
580
+ throw new Error(`Refusing to delete origin/${branchName}: ${observedHead} is not merged into origin/${targetBranch} (${targetHead}).`);
581
+ }
582
+ ensureWritableOrigin(repoDir);
583
+ runGit(["push", `--force-with-lease=refs/heads/${branchName}:${expectedHead}`, "origin", "--delete", branchName], { cwd: repoDir });
584
+ return true;
585
+ }
586
+ function inspectMergedRemoteTaskBranches(repoDir) {
587
+ fetchOrigin(repoDir);
588
+ const targets = [STAGING_BRANCH, PRODUCTION_BRANCH].map((branch) => ({ branch, head: remoteHeadCommit(repoDir, branch) })).filter((target) => Boolean(target.head));
589
+ return listTaskBranches(repoDir, { fetch: false }).filter((branch) => branch.remote).map((branch) => {
590
+ const head = remoteHeadCommit(repoDir, branch.name);
591
+ const mergedTarget = head ? targets.find((target) => runGitAllowFailure(["merge-base", "--is-ancestor", head, target.head], { cwd: repoDir }).status === 0) : void 0;
592
+ return {
593
+ branch: branch.name,
594
+ head,
595
+ current: branch.current,
596
+ mergedInto: mergedTarget?.branch ?? null
597
+ };
598
+ });
599
+ }
567
600
  function mergeCurrentBranchIntoStaging(cwd, featureBranch) {
568
601
  return squashMergeBranchIntoStaging(cwd, featureBranch, `stage: ${featureBranch}`);
569
602
  }
@@ -635,10 +668,12 @@ function assertFeatureBranch(cwd = workspaceRoot()) {
635
668
  function gitLines(repoDir, args) {
636
669
  return runGit(args, { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
637
670
  }
638
- function listTaskBranches(repoDir) {
639
- try {
640
- runGit(["fetch", "origin"], { cwd: repoDir, capture: true });
641
- } catch {
671
+ function listTaskBranches(repoDir, options = {}) {
672
+ if (options.fetch !== false) {
673
+ try {
674
+ runGit(["fetch", "origin"], { cwd: repoDir, capture: true });
675
+ } catch {
676
+ }
642
677
  }
643
678
  const local = new Set(
644
679
  gitLines(repoDir, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]).filter(isTaskBranch)
@@ -649,7 +684,7 @@ function listTaskBranches(repoDir) {
649
684
  const current = currentBranch(repoDir);
650
685
  const branches = [.../* @__PURE__ */ new Set([...local, ...remote])].sort((left, right) => left.localeCompare(right));
651
686
  return branches.map((branchName) => {
652
- const ref = local.has(branchName) ? branchName : `origin/${branchName}`;
687
+ const ref = local.has(branchName) ? `refs/heads/${branchName}` : `refs/remotes/origin/${branchName}`;
653
688
  return {
654
689
  name: branchName,
655
690
  head: runGit(["rev-parse", ref], { cwd: repoDir, capture: true }).trim(),
@@ -780,12 +815,14 @@ export {
780
815
  currentManagedBranch,
781
816
  deleteLocalBranch,
782
817
  deleteRemoteBranch,
818
+ deleteRemoteBranchIfMerged,
783
819
  ensureLocalBranchTracking,
784
820
  ensureRemoteBranchFromBase,
785
821
  fetchOrigin,
786
822
  gitWorkflowRoot,
787
823
  headCommit,
788
824
  inspectDetachedHeadRepair,
825
+ inspectMergedRemoteTaskBranches,
789
826
  isTaskBranch,
790
827
  listTaskBranches,
791
828
  mergeBranchDownIntoFeature,
@@ -127,6 +127,11 @@ export declare function skippedGitHubActionsGate(gate: GitHubActionsWorkflowGate
127
127
  cached: boolean;
128
128
  };
129
129
  export declare function formatGitHubActionsGateFailure(gate: GitHubActionsWorkflowGate, result: Record<string, unknown>): string;
130
+ export declare function isRetryableGitHubActionsSetupFailure(result: Record<string, unknown>): boolean;
131
+ export declare function rerunGitHubActionsFailedJobs(result: Record<string, unknown>, env?: NodeJS.ProcessEnv | Record<string, string | undefined>): Promise<{
132
+ repository: string;
133
+ runId: number;
134
+ }>;
130
135
  export declare function createGitHubActionsGateProgressReporter(gate: GitHubActionsWorkflowGate, options?: {
131
136
  operation?: string;
132
137
  now?: () => number;
@@ -433,6 +433,28 @@ Failed jobs: ${failedJobs.join(", ")}` : "";
433
433
  Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
434
434
  return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
435
435
  }
436
+ function isRetryableGitHubActionsSetupFailure(result) {
437
+ const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs : [];
438
+ return failedJobs.length > 0 && failedJobs.every((job) => {
439
+ const steps = Array.isArray(job.steps) ? job.steps : [];
440
+ return steps.length === 1 && steps[0]?.name === "Set up job" && steps[0]?.conclusion === "failure";
441
+ });
442
+ }
443
+ async function rerunGitHubActionsFailedJobs(result, env = process.env) {
444
+ const repository = String(result.repository ?? "");
445
+ const runId = Number(result.runId);
446
+ if (!repository || !Number.isSafeInteger(runId) || runId <= 0) {
447
+ throw new Error("Cannot retry GitHub Actions setup failure without a repository and run ID.");
448
+ }
449
+ const { owner, name: repo } = parseGitHubRepositorySlug(repository);
450
+ const client = createGitHubApiClient({ env });
451
+ await client.request("POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun-failed-jobs", {
452
+ owner,
453
+ repo,
454
+ run_id: runId
455
+ });
456
+ return { repository, runId };
457
+ }
436
458
  function formatElapsed(seconds) {
437
459
  const safe = Math.max(0, Math.round(seconds));
438
460
  if (safe < 60) return `${safe}s`;
@@ -560,6 +582,8 @@ export {
560
582
  createGitHubActionsGateProgressReporter,
561
583
  formatGitHubActionsGateFailure,
562
584
  inspectGitHubActionsVerification,
585
+ isRetryableGitHubActionsSetupFailure,
586
+ rerunGitHubActionsFailedJobs,
563
587
  skippedGitHubActionsGate,
564
588
  waitForGitHubActionsGate
565
589
  };
@@ -683,7 +683,9 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
683
683
  service.environments?.[normalizedScope]?.railwayEnvironment
684
684
  );
685
685
  const publicBaseUrl = service.environments?.[normalizedScope]?.baseUrl ?? service.publicBaseUrl ?? (serviceKey === "api" ? configuredApiPublicBaseUrl(deployConfig, normalizedScope) : null);
686
- const configuredServiceName = service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`);
686
+ const environmentConfig = service.environments?.[normalizedScope];
687
+ const baseServiceName = service.railway?.serviceName ?? (serviceKey === "workerRunner" ? deriveRailwayWorkerRunnerServiceName(identity.deploymentKey) : `${identity.deploymentKey}-${railwayServiceNameSuffix(serviceKey)}`);
688
+ const configuredServiceName = environmentConfig?.railwayServiceName ?? (normalizedScope === "prod" && ["api", "operationsRunner"].includes(serviceKey) ? `${serviceKey === "operationsRunner" ? String(baseServiceName).replace(/-\d+$/u, "") : baseServiceName}-production` : baseServiceName);
687
689
  const configuredRunnerPool = service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
688
690
  const runnerPool = serviceKey === "operationsRunner" || serviceKey === "capacityProviderRunner" ? {
689
691
  bootstrapCount: Math.max(1, Number.parseInt(String(configuredRunnerPool?.bootstrapCount ?? (serviceKey === "capacityProviderRunner" ? 1 : OPERATIONS_RUNNER_BOOTSTRAP_COUNT)), 10) || (serviceKey === "capacityProviderRunner" ? 1 : OPERATIONS_RUNNER_BOOTSTRAP_COUNT)),
@@ -706,7 +708,8 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
706
708
  serviceKey,
707
709
  service,
708
710
  serviceRoot,
709
- imageRef
711
+ imageRef,
712
+ serviceName
710
713
  });
711
714
  const resolvedImageRef = sourcePolicy.sourceMode === "image" ? imageRef : null;
712
715
  return {
@@ -716,7 +719,7 @@ function configuredRailwayServicesForConfig(tenantRoot, scope, deployConfig, app
716
719
  serviceConfig: service,
717
720
  scope: normalizedScope,
718
721
  projectId: service.railway?.projectId ?? null,
719
- projectName: service.railway?.projectName ?? identity.deploymentKey,
722
+ projectName: environmentConfig?.railwayProjectName ?? service.railway?.projectName ?? identity.deploymentKey,
720
723
  serviceId: service.railway?.serviceId ?? null,
721
724
  serviceName,
722
725
  runnerId: serviceKey === "operationsRunner" || serviceKey === "capacityProviderRunner" ? serviceName : null,
@@ -786,7 +789,7 @@ function configuredPublicTreeDxRailwayServices({ tenantRoot, scope, deployConfig
786
789
  const baseImageRef = envValue("TREESEED_PUBLIC_TREEDX_IMAGE_REF", imageRefEnv) || "treeseed/treedx";
787
790
  return Array.from({ length: bootstrapCount }, (_, offset) => {
788
791
  const index = offset + 1;
789
- const serviceName = `${PUBLIC_TREEDX_NODE_SERVICE_KEY_PREFIX}${String(index).padStart(2, "0")}`;
792
+ const serviceName = scope === "prod" ? `public-treedx-node-production-${String(index).padStart(2, "0")}` : `${PUBLIC_TREEDX_NODE_SERVICE_KEY_PREFIX}${String(index).padStart(2, "0")}`;
790
793
  const service = {
791
794
  key: serviceName,
792
795
  serviceName,
@@ -893,11 +896,11 @@ function headCommitSafe(cwd) {
893
896
  return null;
894
897
  }
895
898
  }
896
- function resolveRailwayServiceSourcePolicy({ tenantRoot, scope, serviceKey, service, serviceRoot, imageRef }) {
899
+ function resolveRailwayServiceSourcePolicy({ tenantRoot, scope, serviceKey, service, serviceRoot, imageRef, serviceName: effectiveServiceName }) {
897
900
  const configuredMode = typeof service.railway?.sourceMode === "string" ? service.railway.sourceMode : null;
898
901
  const configuredSource = service.railway?.source && typeof service.railway.source === "object" && !Array.isArray(service.railway.source) ? service.railway.source : {};
899
902
  const configuredRepo = typeof service.railway?.sourceRepo === "string" ? service.railway.sourceRepo : typeof configuredSource.repository === "string" ? configuredSource.repository : typeof configuredSource.repo === "string" ? configuredSource.repo : null;
900
- const serviceName = service.railway?.serviceName ?? null;
903
+ const serviceName = effectiveServiceName ?? service.railway?.serviceName ?? null;
901
904
  const packageRepository = configuredRepo ?? readTreeseedPackageRepository(serviceRoot) ?? readTreeseedPackageRepository(tenantRoot) ?? apiRailwayDefaultSourceRepo({ key: serviceKey, serviceName });
902
905
  const dockerfilePath = service.railway?.dockerfilePath ?? apiRailwayDefaultDockerfilePath({ key: serviceKey, serviceName });
903
906
  const apiPackageSourceEligible = ["api", "operationsRunner"].includes(serviceKey);
@@ -1,7 +1,7 @@
1
1
  function isApiRailwaySourcePolicyService(service) {
2
2
  const key = String(service.key ?? "").trim();
3
3
  const serviceName = String(service.serviceName ?? "").trim();
4
- return key.startsWith("public-treedx-node-") || serviceName === "treeseed-api" || /^treeseed-api-operations-runner-\d+$/u.test(serviceName) || /^public-treedx-node-\d+$/u.test(serviceName);
4
+ return key.startsWith("public-treedx-node-") || /^treeseed-api(?:-production)?$/u.test(serviceName) || /^treeseed-api-operations-runner(?:-production)?-\d+$/u.test(serviceName) || /^public-treedx-node(?:-production)?-\d+$/u.test(serviceName);
5
5
  }
6
6
  function isImmutableRailwayImageRef(value) {
7
7
  const imageRef = typeof value === "string" ? value.trim() : "";
@@ -11,15 +11,15 @@ function isImmutableRailwayImageRef(value) {
11
11
  }
12
12
  function apiRailwayDefaultSourceRepo(service) {
13
13
  const serviceName = String(service.serviceName ?? "").trim();
14
- if (serviceName === "treeseed-api" || /^treeseed-api-operations-runner-\d+$/u.test(serviceName)) return "treeseed-ai/api";
15
- if (/^public-treedx-node-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "treeseed-ai/treedx";
14
+ if (String(service.key ?? "") === "api" || String(service.key ?? "") === "operationsRunner" || /^treeseed-api(?:-production)?$/u.test(serviceName) || /^treeseed-api-operations-runner(?:-production)?-\d+$/u.test(serviceName)) return "treeseed-ai/api";
15
+ if (/^public-treedx-node(?:-production)?-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "treeseed-ai/treedx";
16
16
  return null;
17
17
  }
18
18
  function apiRailwayDefaultDockerfilePath(service) {
19
19
  const serviceName = String(service.serviceName ?? "").trim();
20
- if (serviceName === "treeseed-api") return "/Dockerfile.api";
21
- if (/^treeseed-api-operations-runner-\d+$/u.test(serviceName)) return "/Dockerfile.operations-runner";
22
- if (/^public-treedx-node-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "/Dockerfile";
20
+ if (String(service.key ?? "") === "api" || /^treeseed-api(?:-production)?$/u.test(serviceName)) return "/Dockerfile.api";
21
+ if (String(service.key ?? "") === "operationsRunner" || /^treeseed-api-operations-runner(?:-production)?-\d+$/u.test(serviceName)) return "/Dockerfile.operations-runner";
22
+ if (/^public-treedx-node(?:-production)?-\d+$/u.test(serviceName) || String(service.key ?? "").startsWith("public-treedx-node-")) return "/Dockerfile";
23
23
  return null;
24
24
  }
25
25
  function assertApiRailwaySourcePolicy(scope, service) {
@@ -28,6 +28,7 @@ function assertApiRailwaySourcePolicy(scope, service) {
28
28
  const label = service.serviceName ?? service.key ?? "Railway service";
29
29
  if (normalizedScope === "staging") {
30
30
  const issues = [
31
+ /-production(?:-|$)/u.test(String(service.serviceName ?? "")) ? "serviceName must use the staging-only identity" : null,
31
32
  service.sourceMode === "git" ? null : "sourceMode must be git",
32
33
  service.imageRef ? "imageRef must be empty" : null,
33
34
  service.sourceRepo ? null : "sourceRepo must be set",
@@ -42,6 +43,7 @@ function assertApiRailwaySourcePolicy(scope, service) {
42
43
  }
43
44
  if (normalizedScope === "prod") {
44
45
  const issues = [
46
+ /-production(?:-|$)/u.test(String(service.serviceName ?? "")) ? null : "serviceName must use a production-only identity",
45
47
  service.sourceMode === "image" ? null : "sourceMode must be image",
46
48
  isImmutableRailwayImageRef(service.imageRef) ? null : "imageRef must be an immutable released image tag",
47
49
  service.sourceRepo ? "sourceRepo must be empty" : null,
@@ -212,6 +212,8 @@ export interface TreeseedManagedServiceEnvironmentConfig {
212
212
  baseUrl?: string;
213
213
  domain?: string;
214
214
  railwayEnvironment?: string;
215
+ railwayProjectName?: string;
216
+ railwayServiceName?: string;
215
217
  }
216
218
  export type TreeseedLocalRuntimeMode = 'auto' | 'provider' | 'local';
217
219
  export interface TreeseedLocalRuntimeConfig {
@@ -332,7 +332,9 @@ function parseServiceEnvironmentConfig(value, label) {
332
332
  return {
333
333
  baseUrl: optionalString(record.baseUrl),
334
334
  domain: optionalString(record.domain),
335
- railwayEnvironment: optionalString(record.railwayEnvironment)
335
+ railwayEnvironment: optionalString(record.railwayEnvironment),
336
+ railwayProjectName: optionalString(record.railwayProjectName),
337
+ railwayServiceName: optionalString(record.railwayServiceName)
336
338
  };
337
339
  }
338
340
  function parseLocalRuntimeConfig(value, label) {
@@ -9,7 +9,7 @@ import { type TreeseedReconcileResult } from '../reconcile/index.js';
9
9
  import { type TreeseedWorkflowRunCommand, type TreeseedWorkflowRunJournal } from './runs.js';
10
10
  import { type TreeseedWorkflowMode } from './session.js';
11
11
  import { type TreeseedManagedRepository } from '../operations/services/managed-repositories.js';
12
- import type { TreeseedCloseInput, TreeseedCiInput, TreeseedConfigInput, TreeseedDestroyInput, TreeseedExportInput, TreeseedReleaseCandidateInput, TreeseedReleaseInput, TreeseedRecoverInput, TreeseedResumeInput, TreeseedSaveInput, TreeseedStageInput, TreeseedSwitchInput, TreeseedTaskBranchMetadata, TreeseedUpdateInput, TreeseedWorkflowContext, TreeseedWorkflowCiMode, TreeseedWorkflowDevInput, TreeseedWorkflowExecutionMode, TreeseedWorkflowOperationId, TreeseedWorkflowResult, TreeseedWorkflowWorktreeMode, TreeseedReleaseCandidateMode, TreeseedProofInput } from '../workflow.js';
12
+ import type { TreeseedCloseInput, TreeseedCiInput, TreeseedConfigInput, TreeseedDestroyInput, TreeseedExportInput, TreeseedReleaseCandidateInput, TreeseedReleaseInput, TreeseedRecoverInput, TreeseedResumeInput, TreeseedSaveInput, TreeseedStageInput, TreeseedSwitchInput, TreeseedTaskBranchMetadata, TreeseedTasksInput, TreeseedUpdateInput, TreeseedWorkflowContext, TreeseedWorkflowCiMode, TreeseedWorkflowDevInput, TreeseedWorkflowExecutionMode, TreeseedWorkflowOperationId, TreeseedWorkflowResult, TreeseedWorkflowWorktreeMode, TreeseedReleaseCandidateMode, TreeseedProofInput } from '../workflow.js';
13
13
  type WorkflowWrite = NonNullable<TreeseedWorkflowContext['write']>;
14
14
  type WorkflowStatePayload = ReturnType<typeof resolveTreeseedWorkflowState>;
15
15
  type ReleaseCandidateMode = TreeseedReleaseCandidateMode;
@@ -137,7 +137,7 @@ export declare function destroyTreeseedBranchPreview(input: {
137
137
  }>;
138
138
  export declare function workflowStatus(helpers: WorkflowOperationHelpers, input?: TreeseedWorkflowStatusOptions): Promise<TreeseedWorkflowResult<import("../workflow-state.js").TreeseedWorkflowState>>;
139
139
  export declare function workflowCi(helpers: WorkflowOperationHelpers, input?: TreeseedCiInput): Promise<TreeseedWorkflowResult<TreeseedCiResult>>;
140
- export declare function workflowTasks(helpers: WorkflowOperationHelpers): Promise<TreeseedWorkflowResult<{
140
+ export declare function workflowTasks(helpers: WorkflowOperationHelpers, input?: TreeseedTasksInput): Promise<TreeseedWorkflowResult<{
141
141
  tasks: TreeseedTaskBranchMetadata[];
142
142
  workstreams: Array<{
143
143
  id: string;
@@ -517,14 +517,9 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
517
517
  rootRepo: import("../operations/services/repository-save-orchestrator.js").RepositorySavePlanRepo;
518
518
  blockers: string[];
519
519
  autoResumeCandidate: {
520
- runId: string;
521
- branch: string | null;
522
- failure: {
523
- code: string;
524
- message: string;
525
- details: Record<string, unknown> | null;
526
- at: string;
527
- } | null;
520
+ runId: any;
521
+ branch: any;
522
+ failure: any;
528
523
  } | null;
529
524
  workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
530
525
  sceneArtifacts: "full" | "screenshots";
@@ -650,14 +645,9 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
650
645
  branchName: string | null;
651
646
  message: string;
652
647
  autoResumeCandidate: {
653
- runId: string;
654
- branch: string | null;
655
- failure: {
656
- code: string;
657
- message: string;
658
- details: Record<string, unknown> | null;
659
- at: string;
660
- } | null;
648
+ runId: any;
649
+ branch: any;
650
+ failure: any;
661
651
  } | null;
662
652
  } & {
663
653
  finalState?: WorkflowStatePayload;
@@ -688,14 +678,9 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
688
678
  rootRepo: import("../operations/services/repository-save-orchestrator.js").RepositorySavePlanRepo;
689
679
  blockers: string[];
690
680
  autoResumeCandidate: {
691
- runId: string;
692
- branch: string | null;
693
- failure: {
694
- code: string;
695
- message: string;
696
- details: Record<string, unknown> | null;
697
- at: string;
698
- } | null;
681
+ runId: any;
682
+ branch: any;
683
+ failure: any;
699
684
  } | null;
700
685
  workspaceLinks: import("../operations/services/workspace-dependency-mode.js").WorkspaceDependencyModeReport;
701
686
  sceneArtifacts: "full" | "screenshots";
@@ -901,14 +886,9 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
901
886
  phases: string[];
902
887
  blockers: string[];
903
888
  autoResumeCandidate: {
904
- runId: string;
905
- branch: string | null;
906
- failure: {
907
- code: string;
908
- message: string;
909
- details: Record<string, unknown> | null;
910
- at: string;
911
- } | null;
889
+ runId: any;
890
+ branch: any;
891
+ failure: any;
912
892
  } | null;
913
893
  legacyMutationPathDisabled: boolean;
914
894
  } & {
@@ -61,6 +61,8 @@ import {
61
61
  createDeprecatedTaskTag,
62
62
  deleteLocalBranch,
63
63
  deleteRemoteBranch,
64
+ deleteRemoteBranchIfMerged,
65
+ inspectMergedRemoteTaskBranches,
64
66
  ensureLocalBranchTracking,
65
67
  gitWorkflowRoot,
66
68
  headCommit,
@@ -80,6 +82,8 @@ import { resolveGitHubCredentialForRepository } from "../operations/services/git
80
82
  import {
81
83
  formatGitHubActionsGateFailure,
82
84
  inspectGitHubActionsVerification,
85
+ isRetryableGitHubActionsSetupFailure,
86
+ rerunGitHubActionsFailedJobs,
83
87
  skippedGitHubActionsGate,
84
88
  waitForGitHubActionsGate
85
89
  } from "../operations/services/github-actions-verification.js";
@@ -459,11 +463,21 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
459
463
  continue;
460
464
  }
461
465
  }
462
- const result = await waitForGitHubActionsGate(gateWithTimeout, {
466
+ const gateEnv = githubWorkflowGateEnv(options.root, gateWithTimeout);
467
+ let result = await waitForGitHubActionsGate(gateWithTimeout, {
463
468
  operation,
464
- env: githubWorkflowGateEnv(options.root, gateWithTimeout),
469
+ env: gateEnv,
465
470
  onProgress: options.onProgress
466
471
  });
472
+ if (result.status === "completed" && result.conclusion !== "success" && isRetryableGitHubActionsSetupFailure(result)) {
473
+ const retry = await rerunGitHubActionsFailedJobs(result, gateEnv);
474
+ options.onProgress?.(`[${operation}][gate][${gateWithTimeout.name}] Retrying GitHub-hosted setup failure once for run ${retry.runId}.`);
475
+ result = await waitForGitHubActionsGate(gateWithTimeout, {
476
+ operation,
477
+ env: gateEnv,
478
+ onProgress: options.onProgress
479
+ });
480
+ }
467
481
  const normalized = {
468
482
  name: gateWithTimeout.name,
469
483
  ...result,
@@ -1765,6 +1779,24 @@ function findAutoResumableTaskRun(root, command, branch) {
1765
1779
  if (!branch) return null;
1766
1780
  return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
1767
1781
  }
1782
+ function rejectImplicitWorkflowResume(operation, journal) {
1783
+ if (!journal) return;
1784
+ workflowError(
1785
+ operation,
1786
+ "resume_unavailable",
1787
+ `Treeseed ${operation} found interrupted run ${journal.runId} for this branch and will not auto-resume recorded inputs. Run \`trsd resume ${journal.runId}\` to continue it, or \`trsd recover --obsolete ${journal.runId} --reason "superseded by a fresh ${operation}"\` before starting a new ${operation}.`,
1788
+ {
1789
+ details: {
1790
+ recovery: {
1791
+ resumable: true,
1792
+ runId: journal.runId,
1793
+ resumeCommand: `trsd resume ${journal.runId}`,
1794
+ obsoleteCommand: `trsd recover --obsolete ${journal.runId} --reason "superseded by a fresh ${operation}"`
1795
+ }
1796
+ }
1797
+ }
1798
+ );
1799
+ }
1768
1800
  function stringRecord(value) {
1769
1801
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1770
1802
  }
@@ -3446,8 +3478,37 @@ async function workflowCi(helpers, input = {}) {
3446
3478
  }
3447
3479
  });
3448
3480
  }
3449
- async function workflowTasks(helpers) {
3450
- return withContextEnv(helpers.context.env, () => createTasksResult(helpers.cwd()));
3481
+ async function workflowTasks(helpers, input = {}) {
3482
+ return withContextEnv(helpers.context.env, () => {
3483
+ const cwd = helpers.cwd();
3484
+ if (!input.cleanupMerged) return createTasksResult(cwd);
3485
+ const live = input.cleanupMerged === "live";
3486
+ const repos = [
3487
+ { name: "@treeseed/market", dir: repoRoot(cwd) },
3488
+ ...checkedOutStagePromotionRepos(cwd).map((repo) => ({ name: repo.name, dir: repo.dir }))
3489
+ ];
3490
+ const branchCleanup = repos.map((repo) => {
3491
+ const branches = inspectMergedRemoteTaskBranches(repo.dir).map((branch) => {
3492
+ if (branch.current) {
3493
+ return { ...branch, status: "preserved", reason: "branch is currently checked out" };
3494
+ }
3495
+ if (!branch.head || !branch.mergedInto) {
3496
+ return { ...branch, status: "preserved", reason: "branch is not merged into staging or main" };
3497
+ }
3498
+ if (!live) {
3499
+ return { ...branch, status: "planned", reason: `exact head is merged into ${branch.mergedInto}` };
3500
+ }
3501
+ deleteRemoteBranchIfMerged(repo.dir, branch.branch, branch.mergedInto, branch.head, { fetch: false });
3502
+ return { ...branch, status: "deleted", reason: `exact head was merged into ${branch.mergedInto}` };
3503
+ });
3504
+ return { repository: repo.name, path: repo.dir, branches };
3505
+ });
3506
+ return buildWorkflowResult("tasks", cwd, { tasks: [], workstreams: [], branchCleanup }, {
3507
+ executionMode: live ? "execute" : "plan",
3508
+ includeFinalState: false,
3509
+ summary: live ? "Merged remote task branches were cleaned safely." : "Merged remote task branch cleanup plan ready."
3510
+ });
3511
+ });
3451
3512
  }
3452
3513
  async function workflowConfig(helpers, input = {}) {
3453
3514
  try {
@@ -4464,7 +4525,8 @@ async function workflowSave(helpers, input) {
4464
4525
  const executionMode = normalizeExecutionMode(input);
4465
4526
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
4466
4527
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableSaveRun(root, branch) : null;
4467
- const planAutoResumeRun = executionMode === "plan" ? findAutoResumableSaveRun(root, branch) : null;
4528
+ rejectImplicitWorkflowResume("save", autoResumeRun);
4529
+ const planAutoResumeRun = null;
4468
4530
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
4469
4531
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "save", effectiveInput);
4470
4532
  const message = String(effectiveInput.message ?? "").trim();
@@ -4914,7 +4976,8 @@ async function workflowClose(helpers, input) {
4914
4976
  const session = resolveTreeseedWorkflowSession(root);
4915
4977
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
4916
4978
  const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
4917
- const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
4979
+ rejectImplicitWorkflowResume("close", autoResumeRun);
4980
+ const planAutoResumeRun = null;
4918
4981
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
4919
4982
  const message = ensureMessage("close", effectiveInput.message, "a close reason");
4920
4983
  if (executionMode === "plan") {
@@ -5153,8 +5216,8 @@ function internalPackageDependencies(repoPath) {
5153
5216
  return [...names].sort();
5154
5217
  }
5155
5218
  function normalizeStageCleanupMode(input) {
5156
- if (input.cleanupMode === "success" || input.deleteBranch === true) return "success";
5157
- return "manual";
5219
+ if (input.cleanupMode === "manual" || input.deleteBranch === false) return "manual";
5220
+ return "success";
5158
5221
  }
5159
5222
  function stageCandidateManifestPath(root, runId) {
5160
5223
  return {
@@ -5347,7 +5410,7 @@ function cleanupStageSourceBranches(root, branchName, manifest) {
5347
5410
  for (const repo of checkedOutStagePromotionRepos(root)) {
5348
5411
  const manifestRepo = manifest.packages.find((entry) => entry.name === repo.name);
5349
5412
  if (!manifestRepo) continue;
5350
- const remoteDeleted = deleteRemoteBranch(repo.dir, branchName);
5413
+ const remoteDeleted = deleteRemoteBranchIfMerged(repo.dir, branchName, STAGING_BRANCH, manifestRepo.commit);
5351
5414
  if ((currentBranch(repo.dir) || null) === branchName) {
5352
5415
  syncBranchWithOrigin(repo.dir, STAGING_BRANCH);
5353
5416
  }
@@ -5363,7 +5426,7 @@ function cleanupStageSourceBranches(root, branchName, manifest) {
5363
5426
  });
5364
5427
  }
5365
5428
  const gitRoot = repoRoot(root);
5366
- const rootRemoteDeleted = deleteRemoteBranch(gitRoot, branchName);
5429
+ const rootRemoteDeleted = deleteRemoteBranchIfMerged(gitRoot, branchName, STAGING_BRANCH, manifest.root.commit);
5367
5430
  const managedWorktree = managedWorkflowWorktreeMetadata(root);
5368
5431
  const worktreeCleanup = managedWorktree ? removeManagedWorkflowWorktree(root, { deleteBranch: false }) : { removed: false, reason: "not-managed" };
5369
5432
  const branchDeletionRoot = managedWorktree?.primaryRoot ? repoRoot(managedWorktree.primaryRoot) : gitRoot;
@@ -5395,8 +5458,9 @@ async function workflowStage(helpers, input) {
5395
5458
  const session = resolveTreeseedWorkflowSession(root);
5396
5459
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
5397
5460
  const rawAutoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
5461
+ rejectImplicitWorkflowResume("stage", rawAutoResumeRun);
5398
5462
  const autoResumeRun = rawAutoResumeRun?.steps.some((step) => step.id === "preflight") ? rawAutoResumeRun : null;
5399
- const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", session.branchName) : null;
5463
+ const planAutoResumeRun = null;
5400
5464
  const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
5401
5465
  const localCleanup = maybeRunLocalWorkflowCleanup(helpers, root, "stage", effectiveInput);
5402
5466
  const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
@@ -36,6 +36,9 @@ export type TreeseedReleaseCandidateVerifyDriver = 'auto' | 'local' | 'action';
36
36
  export type TreeseedStageVerifyMode = 'action' | 'local' | 'none';
37
37
  export type TreeseedStageCiMode = 'off' | 'hosted';
38
38
  export type TreeseedStageCleanupMode = 'success' | 'manual';
39
+ export type TreeseedTasksInput = {
40
+ cleanupMerged?: 'plan' | 'live';
41
+ };
39
42
  export type TreeseedWorkflowContext = {
40
43
  cwd?: string;
41
44
  env?: NodeJS.ProcessEnv;
@@ -298,12 +301,14 @@ export declare class TreeseedWorkflowSdk {
298
301
  execute(operation: TreeseedWorkflowOperationId, input?: Record<string, unknown>): Promise<TreeseedWorkflowResult<import("./workflow-state.js").TreeseedWorkflowState> | TreeseedWorkflowResult<TreeseedCiResult> | TreeseedWorkflowResult<{
299
302
  tasks: TreeseedTaskBranchMetadata[];
300
303
  workstreams: TreeseedWorkflowWorkstreamSummary[];
304
+ branchCleanup?: unknown;
301
305
  }> | TreeseedWorkflowResult<Record<string, unknown>>>;
302
306
  status(input?: TreeseedWorkflowStatusOptions): Promise<TreeseedWorkflowResult<ReturnType<typeof resolveTreeseedWorkflowState>>>;
303
307
  ci(input?: TreeseedCiInput): Promise<TreeseedWorkflowResult<TreeseedCiResult>>;
304
- tasks(): Promise<TreeseedWorkflowResult<{
308
+ tasks(input?: TreeseedTasksInput): Promise<TreeseedWorkflowResult<{
305
309
  tasks: TreeseedTaskBranchMetadata[];
306
310
  workstreams: TreeseedWorkflowWorkstreamSummary[];
311
+ branchCleanup?: unknown;
307
312
  }>>;
308
313
  config(input?: TreeseedConfigInput): Promise<TreeseedWorkflowResult>;
309
314
  switchTask(input: TreeseedSwitchInput): Promise<TreeseedWorkflowResult>;
package/dist/workflow.js CHANGED
@@ -49,7 +49,7 @@ class TreeseedWorkflowSdk {
49
49
  case "ci":
50
50
  return this.ci(input);
51
51
  case "tasks":
52
- return this.tasks();
52
+ return this.tasks(input);
53
53
  case "config":
54
54
  return this.config(input);
55
55
  case "switch":
@@ -88,8 +88,8 @@ class TreeseedWorkflowSdk {
88
88
  async ci(input = {}) {
89
89
  return workflowCi(this.helpers(), input);
90
90
  }
91
- async tasks() {
92
- return workflowTasks(this.helpers());
91
+ async tasks(input = {}) {
92
+ return workflowTasks(this.helpers(), input);
93
93
  }
94
94
  async config(input = {}) {
95
95
  return workflowConfig(this.helpers(), input);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.53",
3
+ "version": "0.12.54",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {