@treeseed/sdk 0.4.6 → 0.4.7
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.
- package/dist/operations/services/template-registry.d.ts +2 -0
- package/dist/operations/services/template-registry.js +55 -6
- package/dist/sdk-types.d.ts +1 -0
- package/dist/template-catalog.js +1 -0
- package/dist/workflow/operations.d.ts +241 -169
- package/dist/workflow/operations.js +245 -111
- package/dist/workflow/policy.d.ts +12 -0
- package/dist/workflow/policy.js +58 -0
- package/dist/workflow-state.d.ts +19 -1
- package/dist/workflow-state.js +57 -39
- package/dist/workflow.d.ts +3 -0
- package/dist/workflow.js +2 -1
- package/package.json +1 -1
|
@@ -73,6 +73,10 @@ import {
|
|
|
73
73
|
} from "../operations/services/workspace-save.js";
|
|
74
74
|
import { run, workspaceRoot } from "../operations/services/workspace-tools.js";
|
|
75
75
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
76
|
+
import {
|
|
77
|
+
classifyTreeseedBranchRole,
|
|
78
|
+
resolveTreeseedWorkflowPaths
|
|
79
|
+
} from "./policy.js";
|
|
76
80
|
class TreeseedWorkflowError extends Error {
|
|
77
81
|
code;
|
|
78
82
|
operation;
|
|
@@ -125,17 +129,6 @@ function withContextEnv(env, action) {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
|
-
function runChild(command, args, context, cwd, label) {
|
|
129
|
-
const result = spawnSync(command, args, {
|
|
130
|
-
cwd,
|
|
131
|
-
env: { ...process.env, ...context.env ?? {} },
|
|
132
|
-
stdio: "inherit"
|
|
133
|
-
});
|
|
134
|
-
if (result.status !== 0) {
|
|
135
|
-
workflowError("dev", "unsupported_state", `${label} failed.`, { exitCode: result.status ?? 1 });
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
132
|
function runNodeScript(scriptName, context, cwd, label) {
|
|
140
133
|
const result = spawnSync(process.execPath, [packageScriptPath(scriptName)], {
|
|
141
134
|
cwd,
|
|
@@ -157,6 +150,52 @@ function normalizeConfigScopes(input) {
|
|
|
157
150
|
}
|
|
158
151
|
return ["local", "staging", "prod"].filter((scope) => requested.includes(scope));
|
|
159
152
|
}
|
|
153
|
+
function resolveWorkflowStateSnapshot(cwd) {
|
|
154
|
+
return resolveTreeseedWorkflowState(cwd);
|
|
155
|
+
}
|
|
156
|
+
function resolveProjectRootOrThrow(operation, cwd) {
|
|
157
|
+
const resolved = resolveTreeseedWorkflowPaths(cwd);
|
|
158
|
+
if (!resolved.tenantRoot) {
|
|
159
|
+
workflowError(operation, "validation_failed", `Treeseed ${operation} requires a Treeseed project. Run the command from inside a tenant or initialize one first.`);
|
|
160
|
+
}
|
|
161
|
+
return resolved.cwd;
|
|
162
|
+
}
|
|
163
|
+
function resolveRepoState(repoDir) {
|
|
164
|
+
const branchName = currentBranch(repoDir) || null;
|
|
165
|
+
return {
|
|
166
|
+
repoDir,
|
|
167
|
+
branchName,
|
|
168
|
+
branchRole: classifyTreeseedBranchRole(branchName, repoDir),
|
|
169
|
+
dirtyWorktree: gitStatusPorcelain(repoDir).length > 0
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function buildWorkflowResult(operation, cwd, payload, nextSteps) {
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
operation,
|
|
176
|
+
payload: {
|
|
177
|
+
...payload,
|
|
178
|
+
finalState: resolveWorkflowStateSnapshot(cwd)
|
|
179
|
+
},
|
|
180
|
+
nextSteps
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function ensureLocalReadinessOrThrow(operation, tenantRoot) {
|
|
184
|
+
const state = resolveWorkflowStateSnapshot(tenantRoot);
|
|
185
|
+
if (!state.readiness.local.ready) {
|
|
186
|
+
workflowError(
|
|
187
|
+
operation,
|
|
188
|
+
"validation_failed",
|
|
189
|
+
[
|
|
190
|
+
`Treeseed ${operation} requires the local environment to be configured.`,
|
|
191
|
+
...state.readiness.local.blockers,
|
|
192
|
+
"Run `treeseed config --environment local` first."
|
|
193
|
+
].join("\n"),
|
|
194
|
+
{ details: { readiness: state.readiness.local } }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return state;
|
|
198
|
+
}
|
|
160
199
|
function dedupeRepairActions(actions) {
|
|
161
200
|
const seen = /* @__PURE__ */ new Set();
|
|
162
201
|
return actions.filter((action) => {
|
|
@@ -342,6 +381,56 @@ function resolveDestroyConfirmation(context, expected, input) {
|
|
|
342
381
|
}
|
|
343
382
|
return false;
|
|
344
383
|
}
|
|
384
|
+
function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
385
|
+
try {
|
|
386
|
+
if (remoteBranchExists(repoDir, branch)) {
|
|
387
|
+
run("git", ["pull", "--rebase", "origin", branch], { cwd: repoDir });
|
|
388
|
+
run("git", ["push", "origin", branch], { cwd: repoDir });
|
|
389
|
+
return {
|
|
390
|
+
remoteBranchExisted: true,
|
|
391
|
+
pulledRebase: true,
|
|
392
|
+
pushed: true,
|
|
393
|
+
createdRemoteBranch: false,
|
|
394
|
+
conflicts: false
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
run("git", ["push", "-u", "origin", branch], { cwd: repoDir });
|
|
398
|
+
return {
|
|
399
|
+
remoteBranchExisted: false,
|
|
400
|
+
pulledRebase: false,
|
|
401
|
+
pushed: true,
|
|
402
|
+
createdRemoteBranch: true,
|
|
403
|
+
conflicts: false
|
|
404
|
+
};
|
|
405
|
+
} catch {
|
|
406
|
+
const report = collectMergeConflictReport(repoDir);
|
|
407
|
+
throw new TreeseedWorkflowError(operation, "merge_conflict", formatMergeConflictReport(report, repoDir, branch), {
|
|
408
|
+
details: { branch, report },
|
|
409
|
+
exitCode: 12
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
|
|
414
|
+
const tenantRoot = resolveProjectRootOrThrow(operation, helpers.cwd());
|
|
415
|
+
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
416
|
+
const before = resolveRepoState(repoDir);
|
|
417
|
+
if (!before.dirtyWorktree) {
|
|
418
|
+
return { performed: false, save: null };
|
|
419
|
+
}
|
|
420
|
+
if (input.autoSave === false) {
|
|
421
|
+
workflowError(operation, "validation_failed", `Treeseed ${operation} requires a clean worktree or autoSave enabled.`);
|
|
422
|
+
}
|
|
423
|
+
const saveResult = await workflowSave(helpers, {
|
|
424
|
+
message: operation === "close" ? `close: ${input.message}` : input.message,
|
|
425
|
+
verify: input.verify === true,
|
|
426
|
+
refreshPreview: false,
|
|
427
|
+
preview: input.preview
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
performed: true,
|
|
431
|
+
save: saveResult.payload
|
|
432
|
+
};
|
|
433
|
+
}
|
|
345
434
|
async function workflowStatus(helpers) {
|
|
346
435
|
return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
|
|
347
436
|
}
|
|
@@ -351,7 +440,7 @@ async function workflowTasks(helpers) {
|
|
|
351
440
|
async function workflowConfig(helpers, input = {}) {
|
|
352
441
|
try {
|
|
353
442
|
return await withContextEnv(helpers.context.env, async () => {
|
|
354
|
-
const tenantRoot = helpers.cwd();
|
|
443
|
+
const tenantRoot = resolveProjectRootOrThrow("config", helpers.cwd());
|
|
355
444
|
const scopes = normalizeConfigScopes(input);
|
|
356
445
|
const sync = input.syncProviders ?? input.sync ?? "all";
|
|
357
446
|
const printEnv = input.printEnv === true;
|
|
@@ -442,10 +531,10 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
442
531
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
|
|
443
532
|
const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
444
533
|
const state = resolveTreeseedWorkflowState(tenantRoot);
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
534
|
+
return buildWorkflowResult(
|
|
535
|
+
"config",
|
|
536
|
+
tenantRoot,
|
|
537
|
+
{
|
|
449
538
|
mode: "configure",
|
|
450
539
|
scopes,
|
|
451
540
|
sync,
|
|
@@ -455,14 +544,15 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
455
544
|
preflight,
|
|
456
545
|
toolHealth,
|
|
457
546
|
result: wizardResult,
|
|
458
|
-
state
|
|
547
|
+
state,
|
|
548
|
+
readiness: state.readiness
|
|
459
549
|
},
|
|
460
|
-
|
|
550
|
+
createNextSteps([
|
|
461
551
|
...scopes.includes("local") ? [{ operation: "dev", reason: "Start the local Treeseed runtime on the initialized local environment." }] : [],
|
|
462
552
|
...scopes.includes("staging") ? [{ operation: "status", reason: "Confirm staging readiness after initializing shared services." }] : [],
|
|
463
553
|
{ operation: "switch", reason: "Create or resume a task branch once the runtime foundation is ready.", input: { branch: "feature/my-change", preview: true } }
|
|
464
554
|
])
|
|
465
|
-
|
|
555
|
+
);
|
|
466
556
|
});
|
|
467
557
|
} catch (error) {
|
|
468
558
|
toError("config", error);
|
|
@@ -471,7 +561,7 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
471
561
|
async function workflowSwitch(helpers, input) {
|
|
472
562
|
try {
|
|
473
563
|
return withContextEnv(helpers.context.env, () => {
|
|
474
|
-
const tenantRoot = helpers.cwd();
|
|
564
|
+
const tenantRoot = resolveProjectRootOrThrow("switch", helpers.cwd());
|
|
475
565
|
const branchName = String(input.branch ?? input.branchName ?? "").trim();
|
|
476
566
|
if (!branchName) {
|
|
477
567
|
workflowError("switch", "validation_failed", "Treeseed switch requires a branch name.");
|
|
@@ -503,10 +593,10 @@ async function workflowSwitch(helpers, input) {
|
|
|
503
593
|
previewResult = deployBranchPreview(tenantRoot, branchName, helpers.context, { initialize: true });
|
|
504
594
|
}
|
|
505
595
|
const state = resolveTreeseedWorkflowState(tenantRoot);
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
596
|
+
return buildWorkflowResult(
|
|
597
|
+
"switch",
|
|
598
|
+
tenantRoot,
|
|
599
|
+
{
|
|
510
600
|
branchName,
|
|
511
601
|
created,
|
|
512
602
|
resumed,
|
|
@@ -517,13 +607,16 @@ async function workflowSwitch(helpers, input) {
|
|
|
517
607
|
lastDeploymentTimestamp: state.preview.lastDeploymentTimestamp
|
|
518
608
|
},
|
|
519
609
|
previewResult,
|
|
520
|
-
|
|
610
|
+
preconditions: {
|
|
611
|
+
cleanWorktreeRequired: true,
|
|
612
|
+
baseBranch: STAGING_BRANCH
|
|
613
|
+
}
|
|
521
614
|
},
|
|
522
|
-
|
|
523
|
-
state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change" } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
|
|
615
|
+
createNextSteps([
|
|
616
|
+
state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change", preview: true } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
|
|
524
617
|
{ operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
|
|
525
618
|
])
|
|
526
|
-
|
|
619
|
+
);
|
|
527
620
|
});
|
|
528
621
|
} catch (error) {
|
|
529
622
|
toError("switch", error);
|
|
@@ -535,7 +628,8 @@ async function workflowDev(helpers, input = {}) {
|
|
|
535
628
|
if (helpers.context.transport === "api") {
|
|
536
629
|
workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
|
|
537
630
|
}
|
|
538
|
-
const tenantRoot = helpers.cwd();
|
|
631
|
+
const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
|
|
632
|
+
const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
|
|
539
633
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
|
|
540
634
|
assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
|
|
541
635
|
const args = [packageScriptPath("tenant-dev")];
|
|
@@ -553,38 +647,42 @@ async function workflowDev(helpers, input = {}) {
|
|
|
553
647
|
stdio: input.stdio ?? "inherit",
|
|
554
648
|
detached: process.platform !== "win32"
|
|
555
649
|
});
|
|
556
|
-
return {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
650
|
+
return buildWorkflowResult("dev", tenantRoot, {
|
|
651
|
+
watch: input.watch === true,
|
|
652
|
+
background: true,
|
|
653
|
+
command: process.execPath,
|
|
654
|
+
args,
|
|
655
|
+
cwd: tenantRoot,
|
|
656
|
+
pid: child.pid ?? null,
|
|
657
|
+
exitCode: null,
|
|
658
|
+
runtime: {
|
|
659
|
+
mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
|
|
660
|
+
apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
|
|
661
|
+
webUrl: "http://127.0.0.1:8787"
|
|
662
|
+
},
|
|
663
|
+
readiness: readiness.readiness.local
|
|
664
|
+
});
|
|
569
665
|
}
|
|
570
666
|
const result = spawnSync(process.execPath, args, {
|
|
571
667
|
cwd: tenantRoot,
|
|
572
668
|
env,
|
|
573
669
|
stdio: input.stdio ?? "inherit"
|
|
574
670
|
});
|
|
575
|
-
return {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
671
|
+
return buildWorkflowResult("dev", tenantRoot, {
|
|
672
|
+
watch: input.watch === true,
|
|
673
|
+
background: false,
|
|
674
|
+
command: process.execPath,
|
|
675
|
+
args,
|
|
676
|
+
cwd: tenantRoot,
|
|
677
|
+
pid: null,
|
|
678
|
+
exitCode: result.status ?? 1,
|
|
679
|
+
runtime: {
|
|
680
|
+
mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
|
|
681
|
+
apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
|
|
682
|
+
webUrl: "http://127.0.0.1:8787"
|
|
683
|
+
},
|
|
684
|
+
readiness: readiness.readiness.local
|
|
685
|
+
});
|
|
588
686
|
});
|
|
589
687
|
} catch (error) {
|
|
590
688
|
toError("dev", error);
|
|
@@ -593,7 +691,7 @@ async function workflowDev(helpers, input = {}) {
|
|
|
593
691
|
async function workflowSave(helpers, input) {
|
|
594
692
|
try {
|
|
595
693
|
return withContextEnv(helpers.context.env, () => {
|
|
596
|
-
const tenantRoot = helpers.cwd();
|
|
694
|
+
const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
|
|
597
695
|
const message = ensureMessage("save", input.message, "a commit message");
|
|
598
696
|
const optionsHotfix = input.hotfix === true;
|
|
599
697
|
const root = workspaceRoot(tenantRoot);
|
|
@@ -616,45 +714,49 @@ async function workflowSave(helpers, input) {
|
|
|
616
714
|
if (input.verify !== false) {
|
|
617
715
|
runWorkspaceSavePreflight({ cwd: root });
|
|
618
716
|
}
|
|
619
|
-
|
|
620
|
-
|
|
717
|
+
const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
|
|
718
|
+
let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
719
|
+
let commitCreated = false;
|
|
720
|
+
if (hadMeaningfulChanges) {
|
|
721
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
722
|
+
run("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
723
|
+
head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
724
|
+
commitCreated = true;
|
|
621
725
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
726
|
+
const branchSync = syncCurrentBranchToOrigin("save", gitRoot, branch);
|
|
727
|
+
let previewAction = { status: "skipped" };
|
|
728
|
+
if (beforeState.branchRole === "feature" && branch) {
|
|
729
|
+
if (input.preview === true) {
|
|
730
|
+
previewAction = {
|
|
731
|
+
status: beforeState.preview.enabled ? "refreshed" : "created",
|
|
732
|
+
details: deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled })
|
|
733
|
+
};
|
|
734
|
+
} else if (input.refreshPreview !== false && beforeState.preview.enabled) {
|
|
735
|
+
previewAction = {
|
|
736
|
+
status: "refreshed",
|
|
737
|
+
details: deployBranchPreview(root, branch, helpers.context, { initialize: false })
|
|
738
|
+
};
|
|
631
739
|
}
|
|
632
|
-
} catch {
|
|
633
|
-
const report = collectMergeConflictReport(gitRoot);
|
|
634
|
-
throw new TreeseedWorkflowError("save", "merge_conflict", formatMergeConflictReport(report, gitRoot, branch), {
|
|
635
|
-
details: { branch, report },
|
|
636
|
-
exitCode: 12
|
|
637
|
-
});
|
|
638
740
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
return {
|
|
644
|
-
ok: true,
|
|
645
|
-
operation: "save",
|
|
646
|
-
payload: {
|
|
741
|
+
return buildWorkflowResult(
|
|
742
|
+
"save",
|
|
743
|
+
root,
|
|
744
|
+
{
|
|
647
745
|
branch,
|
|
648
746
|
scope,
|
|
649
747
|
hotfix: optionsHotfix,
|
|
650
748
|
message,
|
|
651
749
|
commitSha: head,
|
|
652
|
-
|
|
750
|
+
commitCreated,
|
|
751
|
+
noChanges: !hadMeaningfulChanges,
|
|
752
|
+
branchSync,
|
|
753
|
+
previewAction,
|
|
754
|
+
mergeConflict: null
|
|
653
755
|
},
|
|
654
|
-
|
|
756
|
+
createNextSteps([
|
|
655
757
|
branch === STAGING_BRANCH ? { operation: "release", reason: "Promote the validated staging branch into production.", input: { bump: "patch" } } : branch === PRODUCTION_BRANCH ? { operation: "status", reason: "Inspect production state after the explicit hotfix save." } : { operation: "stage", reason: "Merge the verified task branch into staging.", input: { message: "describe the resolution" } }
|
|
656
758
|
])
|
|
657
|
-
|
|
759
|
+
);
|
|
658
760
|
});
|
|
659
761
|
} catch (error) {
|
|
660
762
|
toError("save", error);
|
|
@@ -662,9 +764,13 @@ async function workflowSave(helpers, input) {
|
|
|
662
764
|
}
|
|
663
765
|
async function workflowClose(helpers, input) {
|
|
664
766
|
try {
|
|
665
|
-
return withContextEnv(helpers.context.env, () => {
|
|
666
|
-
const tenantRoot = helpers.cwd();
|
|
767
|
+
return withContextEnv(helpers.context.env, async () => {
|
|
768
|
+
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
667
769
|
const message = ensureMessage("close", input.message, "a close reason");
|
|
770
|
+
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
771
|
+
message,
|
|
772
|
+
autoSave: input.autoSave
|
|
773
|
+
});
|
|
668
774
|
const featureBranch = assertFeatureBranch(tenantRoot);
|
|
669
775
|
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
670
776
|
assertCleanWorktree(tenantRoot);
|
|
@@ -675,21 +781,24 @@ async function workflowClose(helpers, input) {
|
|
|
675
781
|
if (input.deleteBranch !== false) {
|
|
676
782
|
deleteLocalBranch(repoDir, featureBranch);
|
|
677
783
|
}
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
784
|
+
return buildWorkflowResult(
|
|
785
|
+
"close",
|
|
786
|
+
tenantRoot,
|
|
787
|
+
{
|
|
682
788
|
branchName: featureBranch,
|
|
683
789
|
message,
|
|
790
|
+
autoSaved: autoSave.performed,
|
|
791
|
+
autoSaveResult: autoSave.save,
|
|
684
792
|
deprecatedTag,
|
|
685
793
|
previewCleanup,
|
|
686
794
|
remoteDeleted,
|
|
687
|
-
localDeleted: input.deleteBranch !== false
|
|
795
|
+
localDeleted: input.deleteBranch !== false,
|
|
796
|
+
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
688
797
|
},
|
|
689
|
-
|
|
798
|
+
createNextSteps([
|
|
690
799
|
{ operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
|
|
691
800
|
])
|
|
692
|
-
|
|
801
|
+
);
|
|
693
802
|
});
|
|
694
803
|
} catch (error) {
|
|
695
804
|
toError("close", error);
|
|
@@ -697,12 +806,25 @@ async function workflowClose(helpers, input) {
|
|
|
697
806
|
}
|
|
698
807
|
async function workflowStage(helpers, input) {
|
|
699
808
|
try {
|
|
700
|
-
return withContextEnv(helpers.context.env, () => {
|
|
701
|
-
const tenantRoot = helpers.cwd();
|
|
809
|
+
return withContextEnv(helpers.context.env, async () => {
|
|
810
|
+
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
702
811
|
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
812
|
+
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
813
|
+
message,
|
|
814
|
+
autoSave: input.autoSave
|
|
815
|
+
});
|
|
703
816
|
const featureBranch = assertFeatureBranch(tenantRoot);
|
|
704
817
|
runWorkspaceSavePreflight({ cwd: tenantRoot });
|
|
705
|
-
|
|
818
|
+
let repoDir;
|
|
819
|
+
try {
|
|
820
|
+
repoDir = mergeCurrentBranchIntoStaging(tenantRoot, featureBranch);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
const report = collectMergeConflictReport(gitWorkflowRoot(tenantRoot));
|
|
823
|
+
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, gitWorkflowRoot(tenantRoot), STAGING_BRANCH), {
|
|
824
|
+
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
825
|
+
exitCode: 12
|
|
826
|
+
});
|
|
827
|
+
}
|
|
706
828
|
const stagingWait = input.waitForStaging === false ? { status: "skipped", reason: "disabled" } : waitForStagingAutomation(repoDir);
|
|
707
829
|
const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
|
|
708
830
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
@@ -710,24 +832,27 @@ async function workflowStage(helpers, input) {
|
|
|
710
832
|
if (input.deleteBranch !== false) {
|
|
711
833
|
deleteLocalBranch(repoDir, featureBranch);
|
|
712
834
|
}
|
|
713
|
-
return
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
835
|
+
return buildWorkflowResult(
|
|
836
|
+
"stage",
|
|
837
|
+
tenantRoot,
|
|
838
|
+
{
|
|
717
839
|
branchName: featureBranch,
|
|
718
840
|
mergeTarget: STAGING_BRANCH,
|
|
719
841
|
message,
|
|
842
|
+
autoSaved: autoSave.performed,
|
|
843
|
+
autoSaveResult: autoSave.save,
|
|
720
844
|
deprecatedTag,
|
|
721
845
|
stagingWait,
|
|
722
846
|
previewCleanup,
|
|
723
847
|
remoteDeleted,
|
|
724
|
-
localDeleted: input.deleteBranch !== false
|
|
848
|
+
localDeleted: input.deleteBranch !== false,
|
|
849
|
+
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
725
850
|
},
|
|
726
|
-
|
|
851
|
+
createNextSteps([
|
|
727
852
|
{ operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
|
|
728
853
|
{ operation: "status", reason: "Inspect staging readiness after the task branch merge." }
|
|
729
854
|
])
|
|
730
|
-
|
|
855
|
+
);
|
|
731
856
|
});
|
|
732
857
|
} catch (error) {
|
|
733
858
|
toError("stage", error);
|
|
@@ -737,7 +862,7 @@ async function workflowRelease(helpers, input) {
|
|
|
737
862
|
try {
|
|
738
863
|
return withContextEnv(helpers.context.env, () => {
|
|
739
864
|
const level = input.bump ?? "patch";
|
|
740
|
-
const root =
|
|
865
|
+
const root = resolveProjectRootOrThrow("release", helpers.cwd());
|
|
741
866
|
const gitRoot = repoRoot(root);
|
|
742
867
|
prepareReleaseBranches(root);
|
|
743
868
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
@@ -750,23 +875,32 @@ async function workflowRelease(helpers, input) {
|
|
|
750
875
|
run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
|
|
751
876
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
752
877
|
mergeStagingIntoMain(root);
|
|
878
|
+
const releasedCommit = run("git", ["rev-parse", PRODUCTION_BRANCH], { cwd: gitRoot, capture: true }).trim();
|
|
753
879
|
run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
|
|
754
880
|
run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
881
|
+
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
882
|
+
return buildWorkflowResult(
|
|
883
|
+
"release",
|
|
884
|
+
root,
|
|
885
|
+
{
|
|
759
886
|
level,
|
|
760
887
|
rootVersion,
|
|
761
888
|
releaseTag: rootVersion,
|
|
889
|
+
releasedCommit,
|
|
762
890
|
stagingBranch: STAGING_BRANCH,
|
|
763
891
|
productionBranch: PRODUCTION_BRANCH,
|
|
764
|
-
touchedPackages: [...plan.touched]
|
|
892
|
+
touchedPackages: [...plan.touched],
|
|
893
|
+
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
894
|
+
pushStatus: {
|
|
895
|
+
stagingPushed: true,
|
|
896
|
+
productionPushed: true,
|
|
897
|
+
tagPushed: true
|
|
898
|
+
}
|
|
765
899
|
},
|
|
766
|
-
|
|
900
|
+
createNextSteps([
|
|
767
901
|
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
768
902
|
])
|
|
769
|
-
|
|
903
|
+
);
|
|
770
904
|
});
|
|
771
905
|
} catch (error) {
|
|
772
906
|
toError("release", error);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type TreeseedWorkflowBranchRole = 'feature' | 'staging' | 'main' | 'detached' | 'none';
|
|
2
|
+
export type TreeseedResolvedWorkflowPaths = {
|
|
3
|
+
requestedCwd: string;
|
|
4
|
+
tenantRoot: string | null;
|
|
5
|
+
cwd: string;
|
|
6
|
+
repoRoot: string | null;
|
|
7
|
+
branchName: string | null;
|
|
8
|
+
branchRole: TreeseedWorkflowBranchRole;
|
|
9
|
+
};
|
|
10
|
+
export declare function classifyTreeseedBranchRole(branchName: string | null, repoDir: string | null): TreeseedWorkflowBranchRole;
|
|
11
|
+
export declare function resolveTreeseedWorkflowPaths(startCwd: string): TreeseedResolvedWorkflowPaths;
|
|
12
|
+
export declare function workflowEnvironmentForBranchRole(branchRole: TreeseedWorkflowBranchRole): "local" | "staging" | "prod" | "none";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { findNearestTreeseedRoot, run } from "../operations/services/workspace-tools.js";
|
|
2
|
+
import { currentBranch, repoRoot } from "../operations/services/workspace-save.js";
|
|
3
|
+
import { PRODUCTION_BRANCH, STAGING_BRANCH } from "../operations/services/git-workflow.js";
|
|
4
|
+
function safeRepoRoot(cwd) {
|
|
5
|
+
try {
|
|
6
|
+
return repoRoot(cwd);
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function repoHasHead(repoDir) {
|
|
12
|
+
try {
|
|
13
|
+
run("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoDir, capture: true });
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function classifyTreeseedBranchRole(branchName, repoDir) {
|
|
20
|
+
if (!repoDir) {
|
|
21
|
+
return "none";
|
|
22
|
+
}
|
|
23
|
+
if (!branchName) {
|
|
24
|
+
return repoHasHead(repoDir) ? "detached" : "none";
|
|
25
|
+
}
|
|
26
|
+
if (branchName === STAGING_BRANCH) {
|
|
27
|
+
return "staging";
|
|
28
|
+
}
|
|
29
|
+
if (branchName === PRODUCTION_BRANCH) {
|
|
30
|
+
return "main";
|
|
31
|
+
}
|
|
32
|
+
return "feature";
|
|
33
|
+
}
|
|
34
|
+
function resolveTreeseedWorkflowPaths(startCwd) {
|
|
35
|
+
const tenantRoot = findNearestTreeseedRoot(startCwd);
|
|
36
|
+
const cwd = tenantRoot ?? startCwd;
|
|
37
|
+
const gitRoot = safeRepoRoot(cwd);
|
|
38
|
+
const branchName = gitRoot ? currentBranch(gitRoot) || null : null;
|
|
39
|
+
return {
|
|
40
|
+
requestedCwd: startCwd,
|
|
41
|
+
tenantRoot,
|
|
42
|
+
cwd,
|
|
43
|
+
repoRoot: gitRoot,
|
|
44
|
+
branchName,
|
|
45
|
+
branchRole: classifyTreeseedBranchRole(branchName, gitRoot)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function workflowEnvironmentForBranchRole(branchRole) {
|
|
49
|
+
if (branchRole === "staging") return "staging";
|
|
50
|
+
if (branchRole === "main") return "prod";
|
|
51
|
+
if (branchRole === "feature") return "local";
|
|
52
|
+
return "none";
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
classifyTreeseedBranchRole,
|
|
56
|
+
resolveTreeseedWorkflowPaths,
|
|
57
|
+
workflowEnvironmentForBranchRole
|
|
58
|
+
};
|
package/dist/workflow-state.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TreeseedWorkflowNextStep } from './workflow.ts';
|
|
2
|
-
|
|
2
|
+
import { type TreeseedWorkflowBranchRole } from './workflow/policy.ts';
|
|
3
|
+
export type TreeseedBranchRole = TreeseedWorkflowBranchRole;
|
|
3
4
|
export type TreeseedWorkflowRecommendation = TreeseedWorkflowNextStep;
|
|
4
5
|
export type TreeseedWorkflowState = {
|
|
5
6
|
cwd: string;
|
|
@@ -44,6 +45,23 @@ export type TreeseedWorkflowState = {
|
|
|
44
45
|
devVars: boolean;
|
|
45
46
|
};
|
|
46
47
|
releaseReady: boolean;
|
|
48
|
+
readiness: {
|
|
49
|
+
local: {
|
|
50
|
+
ready: boolean;
|
|
51
|
+
blockers: string[];
|
|
52
|
+
warnings: string[];
|
|
53
|
+
};
|
|
54
|
+
staging: {
|
|
55
|
+
ready: boolean;
|
|
56
|
+
blockers: string[];
|
|
57
|
+
warnings: string[];
|
|
58
|
+
};
|
|
59
|
+
prod: {
|
|
60
|
+
ready: boolean;
|
|
61
|
+
blockers: string[];
|
|
62
|
+
warnings: string[];
|
|
63
|
+
};
|
|
64
|
+
};
|
|
47
65
|
rollbackCandidates: Array<{
|
|
48
66
|
scope: 'staging' | 'prod';
|
|
49
67
|
commit: string | null;
|