@treeseed/sdk 0.8.2 → 0.8.4

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 +140 -26
  41. package/dist/workflow/runs.js +31 -0
  42. package/package.json +1 -1
  43. package/templates/github/deploy-processing.workflow.yml +115 -0
  44. package/templates/github/deploy-web.workflow.yml +111 -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;
@@ -1304,12 +1342,29 @@ function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
1304
1342
  }
1305
1343
  return { archived, blockers };
1306
1344
  }
1307
- function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
1345
+ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports, options = {}) {
1308
1346
  if (branch !== STAGING_BRANCH) return null;
1347
+ const currentHeads = Object.fromEntries([
1348
+ [rootRepo.name, rootRepo.commitSha ?? null],
1349
+ ...packageReports.map((report) => [report.name, report.commitSha ?? null])
1350
+ ]);
1309
1351
  return listInterruptedWorkflowRuns(root).find((journal) => {
1310
1352
  if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
1311
1353
  return false;
1312
1354
  }
1355
+ const classification = classifyWorkflowRunJournal(journal, {
1356
+ currentBranch: branch,
1357
+ currentHeads
1358
+ });
1359
+ if (classification.state !== "resumable") {
1360
+ if (options.archiveStale && classification.state === "stale") {
1361
+ archiveWorkflowRun(root, journal.runId, {
1362
+ ...classification,
1363
+ reasons: ["release auto-resume skipped stale failed release", ...classification.reasons]
1364
+ });
1365
+ }
1366
+ return false;
1367
+ }
1313
1368
  const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
1314
1369
  const nextStep = nextPendingJournalStep(journal);
1315
1370
  if (releaseRunHasCompletedMutation(journal)) {
@@ -1459,7 +1514,7 @@ function validateStagingWorkflowContracts(root) {
1459
1514
  return;
1460
1515
  }
1461
1516
  const missing = [];
1462
- for (const fileName of ["verify.yml", "deploy.yml"]) {
1517
+ for (const fileName of ["verify.yml", "deploy-web.yml", "deploy-processing.yml"]) {
1463
1518
  if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
1464
1519
  missing.push(fileName);
1465
1520
  }
@@ -2846,7 +2901,7 @@ async function workflowSave(helpers, input) {
2846
2901
  failure: planAutoResumeRun.failure
2847
2902
  } : null,
2848
2903
  workspaceLinks,
2849
- ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
2904
+ ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
2850
2905
  verifyMode: effectiveInput.verifyMode ?? "fast",
2851
2906
  ...worktreePayload(root, effectiveInput.worktreeMode),
2852
2907
  repositoryPlan,
@@ -2856,6 +2911,7 @@ async function workflowSave(helpers, input) {
2856
2911
  { id: "workspace-unlink", description: "Remove local workspace links before deployment install and lockfile updates" },
2857
2912
  ...repositoryPlan.plannedSteps,
2858
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}` }] : [],
2859
2915
  ...branch === STAGING_BRANCH ? [{ id: "release-candidate", description: "Run release-candidate readiness checks for the saved staging state" }] : [],
2860
2916
  { id: "workspace-link", description: "Restore local workspace links after save" },
2861
2917
  ...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
@@ -2892,7 +2948,7 @@ async function workflowSave(helpers, input) {
2892
2948
  gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
2893
2949
  gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
2894
2950
  verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
2895
- ciMode: effectiveInput.ciMode ?? "auto",
2951
+ ciMode: effectiveInput.ciMode ?? (branch === STAGING_BRANCH ? "hosted" : "auto"),
2896
2952
  worktreeMode: effectiveInput.worktreeMode ?? "auto",
2897
2953
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2898
2954
  workspaceLinks: effectiveInput.workspaceLinks ?? "auto",
@@ -2907,7 +2963,7 @@ async function workflowSave(helpers, input) {
2907
2963
  branch,
2908
2964
  resumable: true
2909
2965
  },
2910
- ...shouldUseHostedSaveCi(effectiveInput) ? [{
2966
+ ...shouldUseHostedSaveCi(effectiveInput, branch) ? [{
2911
2967
  id: "hosted-ci",
2912
2968
  description: `Wait for hosted save workflows on ${branch}`,
2913
2969
  repoName: rootRepo.name,
@@ -2962,7 +3018,29 @@ async function workflowSave(helpers, input) {
2962
3018
  verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
2963
3019
  commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
2964
3020
  workflowRunId: workflowRun.runId,
2965
- 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
2966
3044
  });
2967
3045
  } finally {
2968
3046
  ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
@@ -2987,23 +3065,26 @@ async function workflowSave(helpers, input) {
2987
3065
  lockfileValidation: repo.lockfileValidation
2988
3066
  }))
2989
3067
  };
2990
- 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
+ }
2991
3072
  helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
2992
3073
  return waitForWorkflowGates("save", [
2993
- ...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
3074
+ ...branch !== STAGING_BRANCH && savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
2994
3075
  name: savedRootRepo.name,
2995
3076
  repoPath: savedRootRepo.path,
2996
3077
  workflow: "verify.yml",
2997
3078
  branch,
2998
3079
  headSha: savedRootRepo.commitSha
2999
3080
  }] : [],
3000
- ...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({
3001
3082
  name: savedRootRepo.name,
3002
3083
  repoPath: savedRootRepo.path,
3003
3084
  workflow: "deploy.yml",
3004
3085
  branch,
3005
3086
  headSha: savedRootRepo.commitSha
3006
- }] : [],
3087
+ })] : [],
3007
3088
  ...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
3008
3089
  name: repo.name,
3009
3090
  repoPath: repo.path,
@@ -3075,7 +3156,7 @@ async function workflowSave(helpers, input) {
3075
3156
  workspaceLinks,
3076
3157
  commandReadiness,
3077
3158
  lockfileValidation,
3078
- ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
3159
+ ciMode: normalizeSaveCiMode(effectiveInput.ciMode, branch),
3079
3160
  verifyMode: effectiveInput.verifyMode ?? "fast",
3080
3161
  workflowGates: saveWorkflowGates?.workflowGates ?? [],
3081
3162
  releaseCandidate,
@@ -3541,13 +3622,13 @@ async function workflowStage(helpers, input) {
3541
3622
  });
3542
3623
  }
3543
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", [
3544
- {
3625
+ hostedDeployGate({
3545
3626
  name: rootRepo.name,
3546
3627
  repoPath: rootRepo.path,
3547
3628
  workflow: "deploy.yml",
3548
3629
  branch: STAGING_BRANCH,
3549
3630
  headSha: rootRepo.commitSha
3550
- },
3631
+ }),
3551
3632
  ...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
3552
3633
  name: report.name,
3553
3634
  repoPath: report.path,
@@ -3714,7 +3795,7 @@ async function workflowRelease(helpers, input) {
3714
3795
  const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
3715
3796
  const freshRelease = input.fresh === true && !explicitResumeRunId;
3716
3797
  const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
3717
- const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3798
+ const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports, { archiveStale: true }) : null;
3718
3799
  const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
3719
3800
  const effectiveInput = autoResumeRun ? {
3720
3801
  ...autoResumeRun.input,
@@ -3774,6 +3855,7 @@ async function workflowRelease(helpers, input) {
3774
3855
  },
3775
3856
  [
3776
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 },
3777
3859
  { id: "release-candidate", description: "Run release-candidate readiness checks", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3778
3860
  { id: "workspace-unlink", description: "Remove local workspace links before release", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
3779
3861
  ...mode === "recursive-workspace" ? [{ id: "prepare-release-metadata", description: "Rewrite stable release metadata", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : [],
@@ -3814,6 +3896,30 @@ async function workflowRelease(helpers, input) {
3814
3896
  const rootVersion = String(releasePlan.rootVersion);
3815
3897
  applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
3816
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
+ });
3817
3923
  const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
3818
3924
  if (!resumeAtRootGates && !isResume) {
3819
3925
  assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
@@ -3884,7 +3990,7 @@ async function workflowRelease(helpers, input) {
3884
3990
  rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
3885
3991
  rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
3886
3992
  const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
3887
- releaseDeployGate({
3993
+ hostedDeployGate({
3888
3994
  name: rootRepo.name,
3889
3995
  repoPath: rootRepo.path,
3890
3996
  workflow: "deploy.yml",
@@ -3897,6 +4003,7 @@ async function workflowRelease(helpers, input) {
3897
4003
  onProgress: (line, stream) => helpers.write(line, stream)
3898
4004
  }).then((workflowGates) => ({ workflowGates })));
3899
4005
  const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
4006
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
3900
4007
  const hostingAudit2 = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
3901
4008
  enabled: true,
3902
4009
  strict: false
@@ -3926,13 +4033,17 @@ async function workflowRelease(helpers, input) {
3926
4033
  repos: [],
3927
4034
  rootRepo,
3928
4035
  releaseCandidate,
4036
+ stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
3929
4037
  releaseBackMerge: releaseBackMerge2,
3930
4038
  hostedDeploymentState: hostedDeploymentState2,
3931
4039
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
3932
4040
  pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
3933
4041
  workspaceLinks: workspaceLinks2,
3934
4042
  ciMode,
3935
- workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
4043
+ workflowGates: [
4044
+ ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4045
+ ...Array.isArray(rootWorkflowGateResult2?.workflowGates) ? rootWorkflowGateResult2.workflowGates : []
4046
+ ],
3936
4047
  hostingAudit: hostingAudit2,
3937
4048
  ...worktreePayload(root, effectiveInput.worktreeMode)
3938
4049
  };
@@ -4190,7 +4301,7 @@ async function workflowRelease(helpers, input) {
4190
4301
  rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
4191
4302
  rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
4192
4303
  const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
4193
- releaseDeployGate({
4304
+ hostedDeployGate({
4194
4305
  name: rootRepo.name,
4195
4306
  repoPath: rootRepo.path,
4196
4307
  workflow: "deploy.yml",
@@ -4203,6 +4314,7 @@ async function workflowRelease(helpers, input) {
4203
4314
  onProgress: (line, stream) => helpers.write(line, stream)
4204
4315
  }).then((workflowGates) => ({ workflowGates })));
4205
4316
  const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
4317
+ ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
4206
4318
  const hostingAudit = await runReadOnlyHostingAuditForWorkflow("release", root, helpers, "prod", {
4207
4319
  enabled: true,
4208
4320
  strict: false
@@ -4245,6 +4357,7 @@ async function workflowRelease(helpers, input) {
4245
4357
  repos: packageReports,
4246
4358
  rootRepo,
4247
4359
  releaseCandidate,
4360
+ stagingWorkflowGates: stagingGateResult?.workflowGates ?? [],
4248
4361
  releaseBackMerge,
4249
4362
  hostedDeploymentState,
4250
4363
  finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
@@ -4256,6 +4369,7 @@ async function workflowRelease(helpers, input) {
4256
4369
  workspaceLinks,
4257
4370
  ciMode,
4258
4371
  workflowGates: [
4372
+ ...Array.isArray(stagingGateResult?.workflowGates) ? stagingGateResult.workflowGates : [],
4259
4373
  ...packageReports.flatMap((report) => report.workflowGates),
4260
4374
  ...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
4261
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.2",
3
+ "version": "0.8.4",
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,115 @@
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
+ - run: npm ci --ignore-scripts
99
+
100
+ - name: Build package artifacts
101
+ shell: bash
102
+ run: |
103
+ set -euo pipefail
104
+ for dir in packages/sdk packages/agent packages/cli; do
105
+ if test -f "${dir}/package.json"; then npm --prefix "${dir}" run build:dist; fi
106
+ done
107
+
108
+ - name: Run processing workflow action
109
+ shell: bash
110
+ run: |
111
+ set -euo pipefail
112
+ EXTRA_ARGS=()
113
+ if [[ -n "${TREESEED_WORKFLOW_PROJECT:-}" ]]; then EXTRA_ARGS+=(--project-id "${TREESEED_WORKFLOW_PROJECT}"); fi
114
+ if [[ -n "${TREESEED_WORKFLOW_PREVIEW_ID:-}" ]]; then EXTRA_ARGS+=(--preview-id "${TREESEED_WORKFLOW_PREVIEW_ID}"); fi
115
+ node ./packages/sdk/scripts/run-ts.mjs ./packages/sdk/scripts/tenant-workflow-action.ts --action "${TREESEED_WORKFLOW_ACTION}" --environment "${TREESEED_WORKFLOW_ENVIRONMENT}" "${EXTRA_ARGS[@]}"