@treeseed/sdk 0.8.3 → 0.8.5

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 (47) hide show
  1. package/dist/capacity.d.ts +33 -0
  2. package/dist/fixture-support.d.ts +1 -1
  3. package/dist/fixture-support.js +5 -5
  4. package/dist/managed-dependencies.js +132 -10
  5. package/dist/operations/services/bootstrap-runner.js +7 -1
  6. package/dist/operations/services/config-runtime.js +13 -4
  7. package/dist/operations/services/github-actions-verification.d.ts +3 -0
  8. package/dist/operations/services/github-actions-verification.js +3 -0
  9. package/dist/operations/services/github-api.d.ts +4 -1
  10. package/dist/operations/services/github-api.js +26 -8
  11. package/dist/operations/services/github-automation.d.ts +14 -5
  12. package/dist/operations/services/github-automation.js +45 -11
  13. package/dist/operations/services/hub-provider-launch.js +9 -8
  14. package/dist/operations/services/project-platform.d.ts +93 -210
  15. package/dist/operations/services/project-platform.js +74 -34
  16. package/dist/operations/services/railway-deploy.d.ts +25 -2
  17. package/dist/operations/services/railway-deploy.js +312 -20
  18. package/dist/operations/services/repository-save-orchestrator.d.ts +8 -0
  19. package/dist/operations/services/repository-save-orchestrator.js +40 -3
  20. package/dist/operations/services/runtime-paths.d.ts +1 -0
  21. package/dist/operations/services/runtime-paths.js +3 -1
  22. package/dist/operations/services/runtime-tools.d.ts +1 -0
  23. package/dist/operations/services/runtime-tools.js +2 -0
  24. package/dist/operations/services/template-registry.js +3 -0
  25. package/dist/platform/contracts.d.ts +9 -0
  26. package/dist/platform/deploy-config.js +28 -0
  27. package/dist/platform/env.yaml +1 -745
  28. package/dist/platform/environment.js +69 -9
  29. package/dist/reconcile/builtin-adapters.js +7 -2
  30. package/dist/scripts/install-managed-dependencies.js +12 -0
  31. package/dist/scripts/tenant-workflow-action.js +11 -9
  32. package/dist/scripts/test-scaffold.js +3 -1
  33. package/dist/scripts/workflow-commands.test.js +10 -6
  34. package/dist/scripts/workspace-command-e2e.js +1 -1
  35. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +1 -0
  36. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +1 -1
  37. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +7 -6
  38. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +6 -0
  39. package/dist/workflow/operations.d.ts +41 -8
  40. package/dist/workflow/operations.js +119 -24
  41. package/dist/workflow/runs.js +31 -0
  42. package/package.json +1 -1
  43. package/templates/github/deploy-processing.workflow.yml +120 -0
  44. package/templates/github/deploy-web.workflow.yml +116 -0
  45. package/templates/github/hosted-project.workflow.yml +4 -4
  46. package/templates/github/deploy.managed.workflow.yml +0 -208
  47. package/templates/github/deploy.workflow.yml +0 -746
@@ -209,7 +209,8 @@ function readPackageScript(root, packageDir, scriptName) {
209
209
  function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
210
210
  const packages = [
211
211
  { name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
212
- { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/api.js", "dist/plugin-default.js"] },
212
+ { name: "@treeseed/agent", dir: "packages/agent", artifacts: ["dist/api/index.js", "dist/services/worker.js"] },
213
+ { name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/plugin-default.js"] },
213
214
  { name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
214
215
  ];
215
216
  for (const entry of packages) {
@@ -301,6 +302,10 @@ function normalizeCiMode(mode, operation) {
301
302
  if (mode === "hosted" || mode === "off") return mode;
302
303
  return operation === "save" ? "off" : "hosted";
303
304
  }
305
+ function normalizeSaveCiMode(mode, branch) {
306
+ if (mode === "hosted" || mode === "off") return mode;
307
+ return branch === STAGING_BRANCH ? "hosted" : "off";
308
+ }
304
309
  function normalizeSaveVerifyMode(mode) {
305
310
  switch (mode) {
306
311
  case "skip":
@@ -319,8 +324,8 @@ function normalizeSaveVerifyMode(mode) {
319
324
  return "skip";
320
325
  }
321
326
  }
322
- function shouldUseHostedSaveCi(input) {
323
- return normalizeCiMode(input.ciMode, "save") === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
327
+ function shouldUseHostedSaveCi(input, branch) {
328
+ return normalizeSaveCiMode(input.ciMode, branch) === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both" || input.verifyDeployedResources === true;
324
329
  }
325
330
  function worktreePayload(root, requestedMode) {
326
331
  const metadata = managedWorkflowWorktreeMetadata(root);
@@ -425,11 +430,11 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
425
430
  }
426
431
  return results;
427
432
  }
428
- const RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
429
- function releaseDeployGate(gate) {
433
+ const HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
434
+ function hostedDeployGate(gate) {
430
435
  return {
431
436
  ...gate,
432
- timeoutSeconds: gate.timeoutSeconds ?? RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS
437
+ timeoutSeconds: gate.timeoutSeconds ?? HOSTED_DEPLOY_GATE_TIMEOUT_SECONDS
433
438
  };
434
439
  }
435
440
  function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
@@ -441,7 +446,7 @@ function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGa
441
446
  { scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
442
447
  { scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
443
448
  ]) {
444
- const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
449
+ const gate = gates.find((candidate) => (candidate.workflow === "deploy-web.yml" || candidate.workflow === "deploy-processing.yml") && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
445
450
  const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
446
451
  if (!gate || !timestamp) {
447
452
  continue;
@@ -478,7 +483,7 @@ function ensureTreeseedCommandReadiness(root) {
478
483
  { id: "sdk", path: resolve(root, "node_modules/@treeseed/sdk/package.json") },
479
484
  { id: "sdk-workflow-support", path: resolve(root, "node_modules/@treeseed/sdk/dist/workflow-support.js") },
480
485
  { id: "core", path: resolve(root, "node_modules/@treeseed/core/package.json") },
481
- { id: "core-api", path: resolve(root, "node_modules/@treeseed/core/dist/api.js") },
486
+ { id: "agent-api", path: resolve(root, "node_modules/@treeseed/agent/dist/api/index.js") },
482
487
  { id: "cli", path: resolve(root, "node_modules/@treeseed/cli/package.json") },
483
488
  { id: "cli-entrypoint", path: resolve(root, "node_modules/@treeseed/cli/dist/cli/main.js") },
484
489
  { id: "trsd-bin", path: resolve(root, "node_modules/.bin/trsd") }
@@ -838,7 +843,7 @@ function defaultCiWorkflows(kind, branch) {
838
843
  return ["verify.yml"];
839
844
  }
840
845
  if (branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
841
- return ["deploy.yml"];
846
+ return ["deploy-web.yml", "deploy-processing.yml"];
842
847
  }
843
848
  return ["verify.yml"];
844
849
  }
@@ -1147,8 +1152,41 @@ function nextPendingJournalStep(journal) {
1147
1152
  }
1148
1153
  function findAutoResumableSaveRun(root, branch) {
1149
1154
  if (!branch) return null;
1155
+ if (branch === STAGING_BRANCH && (hasMeaningfulChanges(repoRoot(root)) || checkedOutWorkspacePackageRepos(root).some((repo) => hasMeaningfulChanges(repo.dir)))) {
1156
+ return null;
1157
+ }
1150
1158
  return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
1151
1159
  }
1160
+ function gatesForSavedPackageReports(reports) {
1161
+ return reports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
1162
+ name: repo.name,
1163
+ repoPath: repo.path,
1164
+ workflow: "verify.yml",
1165
+ branch: String(repo.branch),
1166
+ headSha: String(repo.commitSha)
1167
+ }));
1168
+ }
1169
+ function gateForSavedRootReport(report, branch, scope) {
1170
+ if (!branch || scope === "local" || !report.pushed || !report.commitSha) {
1171
+ return [];
1172
+ }
1173
+ if (branch === STAGING_BRANCH) {
1174
+ return [hostedDeployGate({
1175
+ name: report.name,
1176
+ repoPath: report.path,
1177
+ workflow: "deploy.yml",
1178
+ branch,
1179
+ headSha: report.commitSha
1180
+ })];
1181
+ }
1182
+ return [{
1183
+ name: report.name,
1184
+ repoPath: report.path,
1185
+ workflow: "verify.yml",
1186
+ branch,
1187
+ headSha: report.commitSha
1188
+ }];
1189
+ }
1152
1190
  function findAutoResumableTaskRun(root, command, branch) {
1153
1191
  if (!branch) return null;
1154
1192
  return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
@@ -1476,7 +1514,7 @@ function validateStagingWorkflowContracts(root) {
1476
1514
  return;
1477
1515
  }
1478
1516
  const missing = [];
1479
- for (const fileName of ["verify.yml", "deploy.yml"]) {
1517
+ for (const fileName of ["verify.yml", "deploy-web.yml", "deploy-processing.yml"]) {
1480
1518
  if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
1481
1519
  missing.push(fileName);
1482
1520
  }
@@ -2863,7 +2901,7 @@ async function workflowSave(helpers, input) {
2863
2901
  failure: planAutoResumeRun.failure
2864
2902
  } : null,
2865
2903
  workspaceLinks,
2866
- ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2904
+ ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
2867
2905
  verifyMode: effectiveInput.verifyMode ?? "fast",
2868
2906
  ...worktreePayload(root, effectiveInput.worktreeMode),
2869
2907
  repositoryPlan,
@@ -2873,6 +2911,7 @@ async function workflowSave(helpers, input) {
2873
2911
  { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
2874
2912
  ...repositoryPlan.plannedSteps,
2875
2913
  { id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
2914
+ ...shouldUseHostedSaveCi(effectiveInput, branch) ? [{ id: "hosted-ci", description: `Wait for hosted save workflows on ${branch}` }] : [],
2876
2915
  ...branch === STAGING_BRANCH ? [{ id: "release-candidate", description: "Run release-candidate readiness checks for the saved staging state" }] : [],
2877
2916
  { id: "workspace-link", description: "Restore local workspace links after save" },
2878
2917
  ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
@@ -2909,7 +2948,7 @@ async function workflowSave(helpers, input) {
2909
2948
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2910
2949
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2911
2950
  verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
2912
- ciMode: effectiveInput.ciMode ?? "auto",
2951
+ ciMode: effectiveInput.ciMode ?? (branch === STAGING_BRANCH ? "hosted" : "auto"),
2913
2952
  worktreeMode: effectiveInput.worktreeMode ?? "auto",
2914
2953
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2915
2954
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto",
@@ -2924,7 +2963,7 @@ async function workflowSave(helpers, input) {
2924
2963
  branch,
2925
2964
  resumable: true
2926
2965
  },
2927
- ...shouldUseHostedSaveCi(effectiveInput) ? [{
2966
+ ...shouldUseHostedSaveCi(effectiveInput, branch) ? [{
2928
2967
  id: "hosted-ci",
2929
2968
  description: `Wait for hosted save workflows on ${branch}`,
2930
2969
  repoName: rootRepo.name,
@@ -2979,7 +3018,29 @@ async function workflowSave(helpers, input) {
2979
3018
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2980
3019
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2981
3020
  workflowRunId: workflowRun.runId,
2982
- onProgress: (line, stream) => helpers.write(line, stream)
3021
+ onProgress: (line, stream) => helpers.write(line, stream),
3022
+ onWaveSaved: branch === STAGING_BRANCH && shouldUseHostedSaveCi(effectiveInput, branch) ? async ({ nodes, reports, rootRepo: waveRootRepo }) => {
3023
+ const packageReportsForWave = reports.filter((repo, index) => nodes[index]?.kind === "package");
3024
+ const rootReportForWave = nodes.some((node) => node.kind === "project") ? waveRootRepo : null;
3025
+ const gates = [
3026
+ ...gatesForSavedPackageReports(packageReportsForWave),
3027
+ ...rootReportForWave ? gateForSavedRootReport(rootReportForWave, branch, scope) : []
3028
+ ];
3029
+ if (gates.length === 0) {
3030
+ return [];
3031
+ }
3032
+ const packageNames = packageReportsForWave.map((repo) => repo.name).join(", ");
3033
+ if (packageNames) {
3034
+ helpers.write(`[save][workflow] Waiting for hosted package gates before saving dependents: ${packageNames}.`);
3035
+ } else if (rootReportForWave) {
3036
+ helpers.write("[save][workflow] Waiting for hosted market deploy gate.");
3037
+ }
3038
+ return waitForWorkflowGates("save", gates, "hosted", {
3039
+ root,
3040
+ runId: workflowRun.runId,
3041
+ onProgress: (line, stream) => helpers.write(line, stream)
3042
+ });
3043
+ } : void 0
2983
3044
  });
2984
3045
  } finally {
2985
3046
  ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
@@ -3004,23 +3065,26 @@ async function workflowSave(helpers, input) {
3004
3065
  lockfileValidation: repo.lockfileValidation
3005
3066
  }))
3006
3067
  };
3007
- const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => {
3068
+ const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => {
3069
+ if (branch === STAGING_BRANCH) {
3070
+ return { workflowGates: saveResult?.workflowGates ?? [] };
3071
+ }
3008
3072
  helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
3009
3073
  return waitForWorkflowGates("save", [
3010
- ...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
3074
+ ...branch !== STAGING_BRANCH && savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
3011
3075
  name: savedRootRepo.name,
3012
3076
  repoPath: savedRootRepo.path,
3013
3077
  workflow: "verify.yml",
3014
3078
  branch,
3015
3079
  headSha: savedRootRepo.commitSha
3016
3080
  }] : [],
3017
- ...effectiveInput.verifyDeployedResources === true && scope !== "local" && savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
3081
+ ...(branch === STAGING_BRANCH || effectiveInput.verifyDeployedResources === true) && scope !== "local" && savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [hostedDeployGate({
3018
3082
  name: savedRootRepo.name,
3019
3083
  repoPath: savedRootRepo.path,
3020
3084
  workflow: "deploy.yml",
3021
3085
  branch,
3022
3086
  headSha: savedRootRepo.commitSha
3023
- }] : [],
3087
+ })] : [],
3024
3088
  ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
3025
3089
  name: repo.name,
3026
3090
  repoPath: repo.path,
@@ -3092,7 +3156,7 @@ async function workflowSave(helpers, input) {
3092
3156
  workspaceLinks,
3093
3157
  commandReadiness,
3094
3158
  lockfileValidation,
3095
- ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
3159
+ ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
3096
3160
  verifyMode: effectiveInput.verifyMode ?? "fast",
3097
3161
  workflowGates: saveWorkflowGates?.workflowGates ?? [],
3098
3162
  releaseCandidate,
@@ -3558,13 +3622,13 @@ async function workflowStage(helpers, input) {
3558
3622
  });
3559
3623
  }
3560
3624
  const stageWorkflowGateResult = !waitForStaging ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForWorkflowGates("stage", [
3561
- {
3625
+ hostedDeployGate({
3562
3626
  name: rootRepo.name,
3563
3627
  repoPath: rootRepo.path,
3564
3628
  workflow: "deploy.yml",
3565
3629
  branch: STAGING_BRANCH,
3566
3630
  headSha: rootRepo.commitSha
3567
- },
3631
+ }),
3568
3632
  ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
3569
3633
  name: report.name,
3570
3634
  repoPath: report.path,
@@ -3791,6 +3855,7 @@ async function workflowRelease(helpers, input) {
3791
3855
  },
3792
3856
  [
3793
3857
  { id: "release-plan", description: "Record release plan", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3858
+ { id: "release-staging-gates", description: "Verify current staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3794
3859
  { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3795
3860
  { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3796
3861
  ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
@@ -3831,6 +3896,30 @@ async function workflowRelease(helpers, input) {
3831
3896
  const rootVersion = String(releasePlan.rootVersion);
3832
3897
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3833
3898
  assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
3899
+ const stagingGateResult = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-staging-gates") : await executeJournalStep(root, workflowRun.runId, "release-staging-gates", () => {
3900
+ helpers.write("[release][workflow] Verifying current staging gates before production release.");
3901
+ const packageGates = checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => ({
3902
+ name: pkg.name,
3903
+ repoPath: pkg.dir,
3904
+ workflow: "verify.yml",
3905
+ branch: STAGING_BRANCH,
3906
+ headSha: headCommit(pkg.dir)
3907
+ }));
3908
+ return waitForWorkflowGates("release", [
3909
+ hostedDeployGate({
3910
+ name: rootRepo.name,
3911
+ repoPath: rootRepo.path,
3912
+ workflow: "deploy.yml",
3913
+ branch: STAGING_BRANCH,
3914
+ headSha: headCommit(gitRoot)
3915
+ }),
3916
+ ...packageGates
3917
+ ], ciMode, {
3918
+ root,
3919
+ runId: workflowRun.runId,
3920
+ onProgress: (line, stream) => helpers.write(line, stream)
3921
+ }).then((workflowGates) => ({ workflowGates }));
3922
+ });
3834
3923
  const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
3835
3924
  if (!resumeAtRootGates && !isResume) {
3836
3925
  assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
@@ -3901,7 +3990,7 @@ async function workflowRelease(helpers, input) {
3901
3990
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
3902
3991
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3903
3992
  const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3904
- releaseDeployGate({
3993
+ hostedDeployGate({
3905
3994
  name: rootRepo.name,
3906
3995
  repoPath: rootRepo.path,
3907
3996
  workflow: "deploy.yml",
@@ -3944,13 +4033,17 @@ async function workflowRelease(helpers, input) {
3944
4033
  repos: [],
3945
4034
  rootRepo,
3946
4035
  releaseCandidate,
4036
+ stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
3947
4037
  releaseBackMerge: releaseBackMerge2,
3948
4038
  hostedDeploymentState: hostedDeploymentState2,
3949
4039
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
3950
4040
  pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
3951
4041
  workspaceLinks: workspaceLinks2,
3952
4042
  ciMode,
3953
- workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
4043
+ workflowGates: [
4044
+ ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4045
+ ...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
4046
+ ],
3954
4047
  hostingAudit: hostingAudit2,
3955
4048
  ...worktreePayload(root, effectiveInput.worktreeMode)
3956
4049
  };
@@ -4208,7 +4301,7 @@ async function workflowRelease(helpers, input) {
4208
4301
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
4209
4302
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
4210
4303
  const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
4211
- releaseDeployGate({
4304
+ hostedDeployGate({
4212
4305
  name: rootRepo.name,
4213
4306
  repoPath: rootRepo.path,
4214
4307
  workflow: "deploy.yml",
@@ -4264,6 +4357,7 @@ async function workflowRelease(helpers, input) {
4264
4357
  repos: packageReports,
4265
4358
  rootRepo,
4266
4359
  releaseCandidate,
4360
+ stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4267
4361
  releaseBackMerge,
4268
4362
  hostedDeploymentState,
4269
4363
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
@@ -4275,6 +4369,7 @@ async function workflowRelease(helpers, input) {
4275
4369
  workspaceLinks,
4276
4370
  ciMode,
4277
4371
  workflowGates: [
4372
+ ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4278
4373
  ...packageReports.flatMap((report) => report.workflowGates),
4279
4374
  ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
4280
4375
  ],
@@ -257,6 +257,28 @@ function expectedPackageHeadAfterReleaseGate(journal, packageName) {
257
257
  if (typeof data?.commitSha === "string") return data.commitSha;
258
258
  return null;
259
259
  }
260
+ function savePartialFailureData(journal) {
261
+ const details = stringRecord(journal.failure?.details);
262
+ return stringRecord(details?.partialFailure);
263
+ }
264
+ function collectSaveExpectedHeads(journal) {
265
+ const heads = {};
266
+ const saveData = stringRecord(journal.steps.find((step) => step.id === "save-repositories")?.data);
267
+ const partialFailure = savePartialFailureData(journal);
268
+ const source = saveData ?? partialFailure;
269
+ const rootRepo = stringRecord(source?.rootRepo);
270
+ if (typeof rootRepo?.commitSha === "string") {
271
+ heads["@treeseed/market"] = rootRepo.commitSha;
272
+ }
273
+ const repos = Array.isArray(source?.repos) ? source.repos : [];
274
+ for (const entry of repos) {
275
+ const repo = stringRecord(entry);
276
+ if (typeof repo?.name === "string" && typeof repo.commitSha === "string") {
277
+ heads[repo.name] = repo.commitSha;
278
+ }
279
+ }
280
+ return heads;
281
+ }
260
282
  function classifyWorkflowRunJournal(journal, options = {}) {
261
283
  const reasons = [];
262
284
  const now = options.now ?? nowIso();
@@ -326,6 +348,15 @@ function classifyWorkflowRunJournal(journal, options = {}) {
326
348
  }
327
349
  }
328
350
  }
351
+ if (journal.command === "save" && options.currentHeads) {
352
+ const expectedHeads = collectSaveExpectedHeads(journal);
353
+ for (const [name, expectedHead] of Object.entries(expectedHeads)) {
354
+ const currentHead = options.currentHeads[name];
355
+ if (currentHead && expectedHead && currentHead !== expectedHead) {
356
+ reasons.push(`${name} head changed from ${expectedHead} to ${currentHead}`);
357
+ }
358
+ }
359
+ }
329
360
  return {
330
361
  state: reasons.length > 0 ? "stale" : "resumable",
331
362
  reasons: reasons.length > 0 ? reasons : releaseGateOnlyCompletion ? ["release commits already exist; remaining release gates can be rechecked"] : ["workflow run can be resumed"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -0,0 +1,120 @@
1
+ name: Treeseed Processing Deploy
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ environment:
7
+ required: true
8
+ type: string
9
+ action_kind:
10
+ required: true
11
+ type: string
12
+ project_id:
13
+ required: false
14
+ type: string
15
+ preview_id:
16
+ required: false
17
+ type: string
18
+ workflow_dispatch:
19
+ inputs:
20
+ environment:
21
+ required: true
22
+ default: staging
23
+ type: choice
24
+ options:
25
+ - staging
26
+ - prod
27
+ action_kind:
28
+ required: true
29
+ default: deploy_processing
30
+ type: choice
31
+ options:
32
+ - deploy_processing
33
+ - monitor
34
+ project_id:
35
+ required: false
36
+ type: string
37
+ preview_id:
38
+ required: false
39
+ type: string
40
+
41
+ jobs:
42
+ __WORKING_DIRECTORY_BLOCK__ processing:
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ contents: read
46
+ environment: ${{ inputs.environment == 'prod' && 'production' || 'staging' }}
47
+ env:
48
+ TREESEED_BOOTSTRAP_MODE: auto
49
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
50
+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
51
+ RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
52
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
53
+ TREESEED_RAILWAY_WORKSPACE: ${{ vars.TREESEED_RAILWAY_WORKSPACE }}
54
+ TREESEED_API_BASE_URL: ${{ vars.TREESEED_API_BASE_URL }}
55
+ TREESEED_API_AUTH_SECRET: ${{ secrets.TREESEED_API_AUTH_SECRET }}
56
+ TREESEED_API_D1_DATABASE_ID: ${{ vars.TREESEED_API_D1_DATABASE_ID }}
57
+ TREESEED_API_WEB_SERVICE_ID: ${{ vars.TREESEED_API_WEB_SERVICE_ID }}
58
+ TREESEED_API_WEB_SERVICE_SECRET: ${{ secrets.TREESEED_API_WEB_SERVICE_SECRET }}
59
+ TREESEED_API_WEB_ASSERTION_SECRET: ${{ secrets.TREESEED_API_WEB_ASSERTION_SECRET }}
60
+ TREESEED_API_BOOTSTRAP_ADMIN_ALLOWLIST: ${{ vars.TREESEED_API_BOOTSTRAP_ADMIN_ALLOWLIST }}
61
+ TREESEED_PROJECT_ID: ${{ inputs.project_id || vars.TREESEED_PROJECT_ID }}
62
+ TREESEED_PROJECT_RUNNER_TOKEN: ${{ secrets.TREESEED_PROJECT_RUNNER_TOKEN }}
63
+ TREESEED_WORKER_POOL_SCALER: ${{ vars.TREESEED_WORKER_POOL_SCALER }}
64
+ TREESEED_AGENT_POOL_MIN_WORKERS: ${{ vars.TREESEED_AGENT_POOL_MIN_WORKERS }}
65
+ TREESEED_AGENT_POOL_MAX_WORKERS: ${{ vars.TREESEED_AGENT_POOL_MAX_WORKERS }}
66
+ TREESEED_AGENT_POOL_TARGET_QUEUE_DEPTH: ${{ vars.TREESEED_AGENT_POOL_TARGET_QUEUE_DEPTH }}
67
+ TREESEED_AGENT_POOL_COOLDOWN_SECONDS: ${{ vars.TREESEED_AGENT_POOL_COOLDOWN_SECONDS }}
68
+ TREESEED_WORKDAY_TIMEZONE: ${{ vars.TREESEED_WORKDAY_TIMEZONE }}
69
+ TREESEED_WORKDAY_WINDOWS_JSON: ${{ vars.TREESEED_WORKDAY_WINDOWS_JSON }}
70
+ TREESEED_WORKDAY_TASK_CREDIT_BUDGET: ${{ vars.TREESEED_WORKDAY_TASK_CREDIT_BUDGET }}
71
+ TREESEED_MANAGER_MAX_QUEUED_TASKS: ${{ vars.TREESEED_MANAGER_MAX_QUEUED_TASKS }}
72
+ TREESEED_MANAGER_MAX_QUEUED_CREDITS: ${{ vars.TREESEED_MANAGER_MAX_QUEUED_CREDITS }}
73
+ TREESEED_MANAGER_PRIORITY_MODELS: ${{ vars.TREESEED_MANAGER_PRIORITY_MODELS }}
74
+ TREESEED_TASK_CREDIT_WEIGHTS_JSON: ${{ vars.TREESEED_TASK_CREDIT_WEIGHTS_JSON }}
75
+ TREESEED_RAILWAY_PROJECT_ID: ${{ vars.TREESEED_RAILWAY_PROJECT_ID }}
76
+ TREESEED_RAILWAY_ENVIRONMENT_ID: ${{ vars.TREESEED_RAILWAY_ENVIRONMENT_ID }}
77
+ TREESEED_RAILWAY_WORKER_SERVICE_ID: ${{ vars.TREESEED_RAILWAY_WORKER_SERVICE_ID }}
78
+ TREESEED_CAPACITY_PROVIDER_ID: ${{ vars.TREESEED_CAPACITY_PROVIDER_ID }}
79
+ TREESEED_CAPACITY_PROVIDER_TEAM_ID: ${{ vars.TREESEED_CAPACITY_PROVIDER_TEAM_ID }}
80
+ TREESEED_CAPACITY_PROVIDER_SERVICE_BASE_URL: ${{ vars.TREESEED_CAPACITY_PROVIDER_SERVICE_BASE_URL }}
81
+ TREESEED_PROCESSING_DRAIN: ${{ vars.TREESEED_PROCESSING_DRAIN }}
82
+ TREESEED_WORKFLOW_ACTION: ${{ inputs.action_kind }}
83
+ TREESEED_WORKFLOW_ENVIRONMENT: ${{ inputs.environment }}
84
+ TREESEED_WORKFLOW_PLANE: processing
85
+ TREESEED_WORKFLOW_PROJECT: ${{ inputs.project_id || vars.TREESEED_PROJECT_ID }}
86
+ TREESEED_WORKFLOW_PREVIEW_ID: ${{ inputs.preview_id }}
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ with:
90
+ submodules: recursive
91
+
92
+ - uses: actions/setup-node@v4
93
+ with:
94
+ node-version: 22
95
+ cache: npm
96
+ cache-dependency-path: __CACHE_DEPENDENCY_PATH__
97
+
98
+ - name: Install dependencies
99
+ shell: bash
100
+ run: |
101
+ set -euo pipefail
102
+ node -e "const fs = require('fs'); for (const file of ['packages/sdk/package.json', 'packages/agent/package.json', 'packages/core/package.json', 'packages/cli/package.json']) { if (!fs.existsSync(file)) continue; const p = JSON.parse(fs.readFileSync(file, 'utf8')); if (p.scripts) delete p.scripts.prepare; fs.writeFileSync(file, JSON.stringify(p, null, '\t') + '\n'); }"
103
+ npm ci --ignore-scripts
104
+
105
+ - name: Build package artifacts
106
+ shell: bash
107
+ run: |
108
+ set -euo pipefail
109
+ for dir in packages/sdk packages/agent packages/core packages/cli; do
110
+ if test -f "${dir}/package.json"; then npm --prefix "${dir}" run build:dist; fi
111
+ done
112
+
113
+ - name: Run processing workflow action
114
+ shell: bash
115
+ run: |
116
+ set -euo pipefail
117
+ EXTRA_ARGS=()
118
+ if [[ -n "${TREESEED_WORKFLOW_PROJECT:-}" ]]; then EXTRA_ARGS+=(--project-id "${TREESEED_WORKFLOW_PROJECT}"); fi
119
+ if [[ -n "${TREESEED_WORKFLOW_PREVIEW_ID:-}" ]]; then EXTRA_ARGS+=(--preview-id "${TREESEED_WORKFLOW_PREVIEW_ID}"); fi
120
+ node ./packages/sdk/scripts/run-ts.mjs ./packages/sdk/scripts/tenant-workflow-action.ts --action "${TREESEED_WORKFLOW_ACTION}" --environment "${TREESEED_WORKFLOW_ENVIRONMENT}" "${EXTRA_ARGS[@]}"
@@ -0,0 +1,116 @@
1
+ name: Treeseed Web Deploy
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ environment:
7
+ required: true
8
+ type: string
9
+ action_kind:
10
+ required: true
11
+ type: string
12
+ project_id:
13
+ required: false
14
+ type: string
15
+ preview_id:
16
+ required: false
17
+ type: string
18
+ workflow_dispatch:
19
+ inputs:
20
+ environment:
21
+ required: true
22
+ default: staging
23
+ type: choice
24
+ options:
25
+ - staging
26
+ - prod
27
+ action_kind:
28
+ required: true
29
+ default: deploy_web
30
+ type: choice
31
+ options:
32
+ - deploy_web
33
+ - publish_content
34
+ - monitor
35
+ project_id:
36
+ required: false
37
+ type: string
38
+ preview_id:
39
+ required: false
40
+ type: string
41
+
42
+ jobs:
43
+ __WORKING_DIRECTORY_BLOCK__ web:
44
+ runs-on: ubuntu-latest
45
+ permissions:
46
+ contents: read
47
+ environment: ${{ inputs.environment == 'prod' && 'production' || 'staging' }}
48
+ env:
49
+ TREESEED_BOOTSTRAP_MODE: auto
50
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
51
+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
52
+ TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME: ${{ vars.TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME }}
53
+ TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME: ${{ vars.TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME }}
54
+ TREESEED_CONTENT_BUCKET_NAME: ${{ vars.TREESEED_CONTENT_BUCKET_NAME }}
55
+ TREESEED_CONTENT_BUCKET_BINDING: ${{ vars.TREESEED_CONTENT_BUCKET_BINDING }}
56
+ TREESEED_FORM_TOKEN_SECRET: ${{ secrets.TREESEED_FORM_TOKEN_SECRET }}
57
+ TREESEED_EDITORIAL_PREVIEW_SECRET: ${{ secrets.TREESEED_EDITORIAL_PREVIEW_SECRET }}
58
+ TREESEED_PUBLIC_TURNSTILE_SITE_KEY: ${{ vars.TREESEED_PUBLIC_TURNSTILE_SITE_KEY }}
59
+ TREESEED_TURNSTILE_SECRET_KEY: ${{ secrets.TREESEED_TURNSTILE_SECRET_KEY }}
60
+ TREESEED_SMTP_HOST: ${{ vars.TREESEED_SMTP_HOST }}
61
+ TREESEED_SMTP_PORT: ${{ vars.TREESEED_SMTP_PORT }}
62
+ TREESEED_SMTP_USERNAME: ${{ vars.TREESEED_SMTP_USERNAME }}
63
+ TREESEED_SMTP_PASSWORD: ${{ secrets.TREESEED_SMTP_PASSWORD }}
64
+ TREESEED_SMTP_FROM: ${{ vars.TREESEED_SMTP_FROM }}
65
+ TREESEED_SMTP_REPLY_TO: ${{ vars.TREESEED_SMTP_REPLY_TO }}
66
+ TREESEED_PROJECT_DOMAINS: ${{ vars.TREESEED_PROJECT_DOMAINS }}
67
+ TREESEED_BETTER_AUTH_SECRET: ${{ secrets.TREESEED_BETTER_AUTH_SECRET }}
68
+ TREESEED_WEB_SERVICE_SECRET: ${{ secrets.TREESEED_WEB_SERVICE_SECRET }}
69
+ TREESEED_WEB_ASSERTION_SECRET: ${{ secrets.TREESEED_WEB_ASSERTION_SECRET }}
70
+ TREESEED_WEB_CSRF_SECRET: ${{ secrets.TREESEED_WEB_CSRF_SECRET }}
71
+ TREESEED_CENTRAL_MARKET_API_BASE_URL: ${{ vars.TREESEED_CENTRAL_MARKET_API_BASE_URL || 'https://api.treeseed.ai' }}
72
+ TREESEED_MARKET_API_BASE_URL: ${{ vars.TREESEED_MARKET_API_BASE_URL || vars.TREESEED_CENTRAL_MARKET_API_BASE_URL || 'https://api.treeseed.ai' }}
73
+ TREESEED_CATALOG_MARKET_API_BASE_URLS: ${{ vars.TREESEED_CATALOG_MARKET_API_BASE_URLS || vars.TREESEED_MARKET_API_BASE_URL || vars.TREESEED_CENTRAL_MARKET_API_BASE_URL || 'https://api.treeseed.ai' }}
74
+ TREESEED_HOSTING_KIND: ${{ vars.TREESEED_HOSTING_KIND }}
75
+ TREESEED_HOSTING_REGISTRATION: ${{ vars.TREESEED_HOSTING_REGISTRATION }}
76
+ TREESEED_HOSTING_TEAM_ID: ${{ vars.TREESEED_HOSTING_TEAM_ID }}
77
+ TREESEED_PROJECT_ID: ${{ inputs.project_id || vars.TREESEED_PROJECT_ID }}
78
+ TREESEED_WORKFLOW_ACTION: ${{ inputs.action_kind }}
79
+ TREESEED_WORKFLOW_ENVIRONMENT: ${{ inputs.environment }}
80
+ TREESEED_WORKFLOW_PLANE: web
81
+ TREESEED_WORKFLOW_PROJECT: ${{ inputs.project_id || vars.TREESEED_PROJECT_ID }}
82
+ TREESEED_WORKFLOW_PREVIEW_ID: ${{ inputs.preview_id }}
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+ with:
86
+ submodules: recursive
87
+
88
+ - uses: actions/setup-node@v4
89
+ with:
90
+ node-version: 22
91
+ cache: npm
92
+ cache-dependency-path: __CACHE_DEPENDENCY_PATH__
93
+
94
+ - name: Install dependencies
95
+ shell: bash
96
+ run: |
97
+ set -euo pipefail
98
+ node -e "const fs = require('fs'); for (const file of ['packages/sdk/package.json', 'packages/agent/package.json', 'packages/core/package.json', 'packages/cli/package.json']) { if (!fs.existsSync(file)) continue; const p = JSON.parse(fs.readFileSync(file, 'utf8')); if (p.scripts) delete p.scripts.prepare; fs.writeFileSync(file, JSON.stringify(p, null, '\t') + '\n'); }"
99
+ npm ci --ignore-scripts
100
+
101
+ - name: Build package artifacts
102
+ shell: bash
103
+ run: |
104
+ set -euo pipefail
105
+ for dir in packages/sdk packages/agent packages/core packages/cli; do
106
+ if test -f "${dir}/package.json"; then npm --prefix "${dir}" run build:dist; fi
107
+ done
108
+
109
+ - name: Run web workflow action
110
+ shell: bash
111
+ run: |
112
+ set -euo pipefail
113
+ EXTRA_ARGS=()
114
+ if [[ -n "${TREESEED_WORKFLOW_PROJECT:-}" ]]; then EXTRA_ARGS+=(--project-id "${TREESEED_WORKFLOW_PROJECT}"); fi
115
+ if [[ -n "${TREESEED_WORKFLOW_PREVIEW_ID:-}" ]]; then EXTRA_ARGS+=(--preview-id "${TREESEED_WORKFLOW_PREVIEW_ID}"); fi
116
+ node ./packages/sdk/scripts/run-ts.mjs ./packages/sdk/scripts/tenant-workflow-action.ts --action "${TREESEED_WORKFLOW_ACTION}" --environment "${TREESEED_WORKFLOW_ENVIRONMENT}" "${EXTRA_ARGS[@]}"
@@ -14,7 +14,7 @@ on:
14
14
  workflow_file:
15
15
  description: Workflow file to dispatch in the tenant repository
16
16
  required: false
17
- default: deploy.yml
17
+ default: deploy-web.yml
18
18
  type: string
19
19
  project_id:
20
20
  description: Treeseed project id recorded in the market control plane
@@ -31,11 +31,11 @@ on:
31
31
  action_kind:
32
32
  description: Requested orchestration action
33
33
  required: true
34
- default: deploy_code
34
+ default: deploy_web
35
35
  type: choice
36
36
  options:
37
- - provision
38
- - deploy_code
37
+ - deploy_web
38
+ - deploy_processing
39
39
  - publish_content
40
40
  - monitor
41
41