@treeseed/sdk 0.4.12 → 0.5.0
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/control-plane-client.d.ts +60 -1
- package/dist/control-plane-client.js +59 -0
- package/dist/control-plane.d.ts +1 -1
- package/dist/control-plane.js +11 -4
- package/dist/d1-store.d.ts +58 -0
- package/dist/d1-store.js +64 -0
- package/dist/dispatch.js +6 -0
- package/dist/graph/schema.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +32 -0
- package/dist/knowledge-coop.d.ts +223 -0
- package/dist/knowledge-coop.js +82 -0
- package/dist/model-registry.js +79 -0
- package/dist/operations/providers/default.js +128 -7
- package/dist/operations/services/config-runtime.d.ts +102 -24
- package/dist/operations/services/config-runtime.js +896 -160
- package/dist/operations/services/deploy.d.ts +223 -15
- package/dist/operations/services/deploy.js +626 -55
- package/dist/operations/services/git-workflow.d.ts +47 -3
- package/dist/operations/services/git-workflow.js +125 -19
- package/dist/operations/services/github-automation.d.ts +85 -0
- package/dist/operations/services/github-automation.js +220 -1
- package/dist/operations/services/key-agent.d.ts +118 -0
- package/dist/operations/services/key-agent.js +476 -0
- package/dist/operations/services/knowledge-coop-launch.d.ts +90 -0
- package/dist/operations/services/knowledge-coop-launch.js +753 -0
- package/dist/operations/services/knowledge-coop-packaging.d.ts +59 -0
- package/dist/operations/services/knowledge-coop-packaging.js +234 -0
- package/dist/operations/services/local-dev.d.ts +0 -1
- package/dist/operations/services/local-dev.js +1 -14
- package/dist/operations/services/project-platform.d.ts +42 -182
- package/dist/operations/services/project-platform.js +162 -59
- package/dist/operations/services/railway-deploy.d.ts +1 -0
- package/dist/operations/services/railway-deploy.js +31 -13
- package/dist/operations/services/runtime-tools.d.ts +52 -5
- package/dist/operations/services/runtime-tools.js +186 -26
- package/dist/operations/services/watch-dev.js +2 -4
- package/dist/operations/services/workspace-preflight.d.ts +4 -4
- package/dist/operations/services/workspace-preflight.js +22 -20
- package/dist/operations/services/workspace-save.d.ts +10 -1
- package/dist/operations/services/workspace-save.js +54 -3
- package/dist/operations/services/workspace-tools.d.ts +1 -0
- package/dist/operations/services/workspace-tools.js +20 -5
- package/dist/operations-registry.js +15 -8
- package/dist/operations-types.d.ts +2 -2
- package/dist/platform/contracts.d.ts +39 -3
- package/dist/platform/deploy-config.d.ts +12 -1
- package/dist/platform/deploy-config.js +214 -15
- package/dist/platform/deploy-runtime.d.ts +1 -0
- package/dist/platform/deploy-runtime.js +10 -2
- package/dist/platform/env.yaml +93 -61
- package/dist/platform/environment.d.ts +13 -2
- package/dist/platform/environment.js +90 -20
- package/dist/platform/plugins/constants.d.ts +1 -0
- package/dist/platform/plugins/constants.js +7 -6
- package/dist/platform/tenant/runtime-config.js +8 -1
- package/dist/platform/tenant-config.js +4 -0
- package/dist/platform/utils/site-config-schema.js +18 -0
- package/dist/plugin-default.js +2 -2
- package/dist/scripts/key-agent.js +165 -0
- package/dist/scripts/tenant-build.js +4 -1
- package/dist/scripts/tenant-check.js +4 -1
- package/dist/scripts/tenant-deploy.js +43 -4
- package/dist/scripts/tenant-dev.js +0 -1
- package/dist/scripts/workspace-start-warning.js +2 -2
- package/dist/sdk-types.d.ts +2 -2
- package/dist/sdk-types.js +2 -0
- package/dist/sdk.d.ts +13 -0
- package/dist/sdk.js +40 -0
- package/dist/stores/knowledge-coop-store.d.ts +56 -0
- package/dist/stores/knowledge-coop-store.js +482 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +6 -2
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +4 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +25 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +22 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +11 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +17 -0
- package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +17 -10
- package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +69 -7
- package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +1 -0
- package/dist/workflow/operations.d.ts +592 -243
- package/dist/workflow/operations.js +1908 -219
- package/dist/workflow/runs.d.ts +90 -0
- package/dist/workflow/runs.js +242 -0
- package/dist/workflow/session.d.ts +31 -0
- package/dist/workflow/session.js +97 -0
- package/dist/workflow-state.d.ts +88 -2
- package/dist/workflow-state.js +288 -26
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +32 -2
- package/dist/workflow.d.ts +93 -3
- package/dist/workflow.js +12 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +11 -1
- package/dist/scripts/sync-dev-vars.js +0 -6
- package/dist/scripts/workspace-close.js +0 -24
- package/dist/scripts/workspace-release.js +0 -42
- package/dist/scripts/workspace-start.js +0 -71
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import {
|
|
@@ -9,12 +9,20 @@ import {
|
|
|
9
9
|
checkTreeseedProviderConnections,
|
|
10
10
|
collectTreeseedConfigContext,
|
|
11
11
|
collectTreeseedPrintEnvReport,
|
|
12
|
+
ensureTreeseedSecretSessionForConfig,
|
|
12
13
|
ensureTreeseedActVerificationTooling,
|
|
13
14
|
ensureTreeseedGitignoreEntries,
|
|
14
15
|
finalizeTreeseedConfig,
|
|
15
16
|
getTreeseedMachineConfigPaths,
|
|
16
|
-
|
|
17
|
+
inspectTreeseedKeyAgentStatus,
|
|
18
|
+
loadTreeseedMachineConfig,
|
|
19
|
+
resolveTreeseedLaunchEnvironment,
|
|
20
|
+
resolveTreeseedRemoteSession,
|
|
21
|
+
rotateTreeseedMachineKey,
|
|
22
|
+
setTreeseedRemoteSession,
|
|
23
|
+
writeTreeseedMachineConfig
|
|
17
24
|
} from "../operations/services/config-runtime.js";
|
|
25
|
+
import { ControlPlaneClient } from "../control-plane-client.js";
|
|
18
26
|
import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
|
|
19
27
|
import {
|
|
20
28
|
assertDeploymentInitialized,
|
|
@@ -33,32 +41,36 @@ import {
|
|
|
33
41
|
} from "../operations/services/deploy.js";
|
|
34
42
|
import {
|
|
35
43
|
assertCleanWorktree,
|
|
44
|
+
assertCleanWorktrees,
|
|
36
45
|
assertFeatureBranch,
|
|
37
46
|
branchExists,
|
|
38
47
|
checkoutBranch,
|
|
48
|
+
checkoutTaskBranchFromStaging,
|
|
39
49
|
createDeprecatedTaskTag,
|
|
40
|
-
createFeatureBranchFromStaging,
|
|
41
|
-
currentManagedBranch,
|
|
42
50
|
deleteLocalBranch,
|
|
43
51
|
deleteRemoteBranch,
|
|
44
52
|
ensureLocalBranchTracking,
|
|
45
53
|
gitWorkflowRoot,
|
|
54
|
+
headCommit,
|
|
46
55
|
listTaskBranches,
|
|
47
|
-
|
|
56
|
+
mergeBranchIntoTarget,
|
|
48
57
|
mergeStagingIntoMain,
|
|
49
58
|
prepareReleaseBranches,
|
|
50
59
|
PRODUCTION_BRANCH,
|
|
51
60
|
pushBranch,
|
|
52
61
|
remoteBranchExists,
|
|
53
62
|
STAGING_BRANCH,
|
|
63
|
+
squashMergeBranchIntoStaging,
|
|
54
64
|
syncBranchWithOrigin,
|
|
55
65
|
waitForStagingAutomation
|
|
56
66
|
} from "../operations/services/git-workflow.js";
|
|
67
|
+
import { waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
|
|
57
68
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
|
|
58
69
|
import { runTenantDeployPreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
|
|
59
70
|
import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
|
|
60
71
|
import {
|
|
61
72
|
applyWorkspaceVersionChanges,
|
|
73
|
+
assertWorkspaceVersionConsistency,
|
|
62
74
|
collectMergeConflictReport,
|
|
63
75
|
currentBranch,
|
|
64
76
|
formatMergeConflictReport,
|
|
@@ -69,8 +81,30 @@ import {
|
|
|
69
81
|
planWorkspaceReleaseBump,
|
|
70
82
|
repoRoot
|
|
71
83
|
} from "../operations/services/workspace-save.js";
|
|
72
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
changedWorkspacePackages,
|
|
86
|
+
publishableWorkspacePackages,
|
|
87
|
+
run,
|
|
88
|
+
sortWorkspacePackages,
|
|
89
|
+
workspaceRoot
|
|
90
|
+
} from "../operations/services/workspace-tools.js";
|
|
73
91
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
92
|
+
import {
|
|
93
|
+
acquireWorkflowLock,
|
|
94
|
+
createWorkflowRunJournal,
|
|
95
|
+
generateWorkflowRunId,
|
|
96
|
+
inspectWorkflowLock,
|
|
97
|
+
listInterruptedWorkflowRuns,
|
|
98
|
+
listWorkflowRunJournals,
|
|
99
|
+
readWorkflowRunJournal,
|
|
100
|
+
refreshWorkflowLock,
|
|
101
|
+
releaseWorkflowLock,
|
|
102
|
+
updateWorkflowRunJournal
|
|
103
|
+
} from "./runs.js";
|
|
104
|
+
import {
|
|
105
|
+
checkedOutWorkspacePackageRepos,
|
|
106
|
+
resolveTreeseedWorkflowSession
|
|
107
|
+
} from "./session.js";
|
|
74
108
|
import {
|
|
75
109
|
classifyTreeseedBranchRole,
|
|
76
110
|
resolveTreeseedWorkflowPaths
|
|
@@ -167,17 +201,81 @@ function resolveRepoState(repoDir) {
|
|
|
167
201
|
dirtyWorktree: gitStatusPorcelain(repoDir).length > 0
|
|
168
202
|
};
|
|
169
203
|
}
|
|
170
|
-
function
|
|
204
|
+
function createRepoReport(name, path, branch, dirty) {
|
|
205
|
+
return {
|
|
206
|
+
name,
|
|
207
|
+
path,
|
|
208
|
+
branch,
|
|
209
|
+
dirty,
|
|
210
|
+
created: false,
|
|
211
|
+
resumed: false,
|
|
212
|
+
merged: false,
|
|
213
|
+
verified: false,
|
|
214
|
+
committed: false,
|
|
215
|
+
pushed: false,
|
|
216
|
+
deletedLocal: false,
|
|
217
|
+
deletedRemote: false,
|
|
218
|
+
tagName: null,
|
|
219
|
+
commitSha: branch ? headCommit(path) : null,
|
|
220
|
+
skippedReason: null,
|
|
221
|
+
publishWait: null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function createWorkspaceRootRepoReport(root) {
|
|
225
|
+
const gitRoot = repoRoot(root);
|
|
226
|
+
return createRepoReport("@treeseed/market", gitRoot, currentBranch(gitRoot) || null, hasMeaningfulChanges(gitRoot));
|
|
227
|
+
}
|
|
228
|
+
function createWorkspacePackageReports(root) {
|
|
229
|
+
return checkedOutWorkspacePackageRepos(root).map((pkg) => createRepoReport(pkg.name, pkg.dir, currentBranch(pkg.dir) || null, hasMeaningfulChanges(pkg.dir)));
|
|
230
|
+
}
|
|
231
|
+
function findReportByName(reports, name) {
|
|
232
|
+
return reports.find((report) => report.name === name) ?? null;
|
|
233
|
+
}
|
|
234
|
+
function findReportByPath(reports, path) {
|
|
235
|
+
return reports.find((report) => report.path === path) ?? null;
|
|
236
|
+
}
|
|
237
|
+
function assertWorkspaceClean(root) {
|
|
238
|
+
const repoDirs = [repoRoot(root), ...checkedOutWorkspacePackageRepos(root).map((pkg) => pkg.dir)];
|
|
239
|
+
assertCleanWorktrees(repoDirs);
|
|
240
|
+
return repoDirs;
|
|
241
|
+
}
|
|
242
|
+
function buildWorkflowResult(operation, cwd, payload, options = {}) {
|
|
243
|
+
const resolvedPayload = options.includeFinalState ?? true ? {
|
|
244
|
+
...payload,
|
|
245
|
+
finalState: resolveWorkflowStateSnapshot(cwd)
|
|
246
|
+
} : payload;
|
|
171
247
|
return {
|
|
248
|
+
schemaVersion: 1,
|
|
249
|
+
kind: "treeseed.workflow.result",
|
|
250
|
+
command: operation,
|
|
251
|
+
executionMode: options.executionMode ?? "execute",
|
|
252
|
+
runId: options.runId ?? null,
|
|
172
253
|
ok: true,
|
|
173
254
|
operation,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
nextSteps
|
|
255
|
+
summary: options.summary,
|
|
256
|
+
facts: options.facts,
|
|
257
|
+
payload: resolvedPayload,
|
|
258
|
+
result: resolvedPayload,
|
|
259
|
+
nextSteps: options.nextSteps,
|
|
260
|
+
recovery: options.recovery ?? null,
|
|
261
|
+
errors: options.errors ?? []
|
|
179
262
|
};
|
|
180
263
|
}
|
|
264
|
+
function normalizeExecutionMode(input) {
|
|
265
|
+
return input?.plan === true || input?.dryRun === true ? "plan" : "execute";
|
|
266
|
+
}
|
|
267
|
+
function submodulePointerForRef(repoDir, ref, relativeDir) {
|
|
268
|
+
try {
|
|
269
|
+
const output = run("git", ["ls-tree", ref, relativeDir], { cwd: repoDir, capture: true }).trim();
|
|
270
|
+
if (!output) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const match = output.match(/^[0-9]{6}\s+commit\s+([0-9a-f]{40})\t/u);
|
|
274
|
+
return match?.[1] ?? null;
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
181
279
|
function ensureLocalReadinessOrThrow(operation, tenantRoot) {
|
|
182
280
|
const state = resolveWorkflowStateSnapshot(tenantRoot);
|
|
183
281
|
if (!state.readiness.local.ready) {
|
|
@@ -207,12 +305,10 @@ function createNextSteps(steps) {
|
|
|
207
305
|
}
|
|
208
306
|
function createStatusResult(cwd) {
|
|
209
307
|
const state = resolveTreeseedWorkflowState(cwd);
|
|
210
|
-
return {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
nextSteps: createNextSteps(state.recommendations)
|
|
215
|
-
};
|
|
308
|
+
return buildWorkflowResult("status", cwd, state, {
|
|
309
|
+
nextSteps: createNextSteps(state.recommendations),
|
|
310
|
+
includeFinalState: false
|
|
311
|
+
});
|
|
216
312
|
}
|
|
217
313
|
function createTasksResult(cwd) {
|
|
218
314
|
const tenantRoot = cwd;
|
|
@@ -223,6 +319,21 @@ function createTasksResult(cwd) {
|
|
|
223
319
|
const previewState = loadDeployState(tenantRoot, deployConfig, {
|
|
224
320
|
target: createBranchPreviewDeployTarget(branch.name)
|
|
225
321
|
});
|
|
322
|
+
const packages = checkedOutWorkspacePackageRepos(tenantRoot).map((pkg) => {
|
|
323
|
+
const packageBranches = listTaskBranches(pkg.dir);
|
|
324
|
+
const match = packageBranches.find((candidate) => candidate.name === branch.name) ?? null;
|
|
325
|
+
const pointer = submodulePointerForRef(repoDir, branch.name, pkg.relativeDir);
|
|
326
|
+
return {
|
|
327
|
+
name: pkg.name,
|
|
328
|
+
path: pkg.relativeDir,
|
|
329
|
+
local: match?.local === true,
|
|
330
|
+
remote: match?.remote === true,
|
|
331
|
+
current: match?.current === true,
|
|
332
|
+
head: match?.head ?? null,
|
|
333
|
+
pointer,
|
|
334
|
+
aligned: pointer != null && match?.head != null ? pointer === match.head : match != null
|
|
335
|
+
};
|
|
336
|
+
});
|
|
226
337
|
return {
|
|
227
338
|
...branch,
|
|
228
339
|
ageDays: ageDays(branch.lastCommitDate),
|
|
@@ -231,10 +342,179 @@ function createTasksResult(cwd) {
|
|
|
231
342
|
enabled: previewState.previewEnabled === true || previewState.readiness?.initialized === true,
|
|
232
343
|
url: previewState.lastDeployedUrl ?? null,
|
|
233
344
|
lastDeploymentTimestamp: previewState.lastDeploymentTimestamp ?? null
|
|
234
|
-
}
|
|
345
|
+
},
|
|
346
|
+
packages
|
|
235
347
|
};
|
|
236
348
|
});
|
|
237
|
-
|
|
349
|
+
const workstreams = tasks.map((task) => ({
|
|
350
|
+
id: task.name,
|
|
351
|
+
title: task.name.replace(/^task\//u, "").replace(/[-_]+/gu, " "),
|
|
352
|
+
linkedDirectRefs: [],
|
|
353
|
+
branch: task.name,
|
|
354
|
+
local: task.local,
|
|
355
|
+
remote: task.remote,
|
|
356
|
+
current: task.current,
|
|
357
|
+
previewUrl: task.preview.url,
|
|
358
|
+
lastSaveAt: task.lastCommitDate ?? null,
|
|
359
|
+
verificationResult: task.dirtyCurrent ? "needs_attention" : task.head ? "ready" : "unknown",
|
|
360
|
+
stagingCandidate: task.name === STAGING_BRANCH,
|
|
361
|
+
archived: false
|
|
362
|
+
}));
|
|
363
|
+
return buildWorkflowResult("tasks", cwd, { tasks, workstreams }, { includeFinalState: false });
|
|
364
|
+
}
|
|
365
|
+
function normalizeOptionalString(value) {
|
|
366
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
367
|
+
}
|
|
368
|
+
async function connectTreeseedMarketProject(helpers, tenantRoot, input, context) {
|
|
369
|
+
const machineConfig = loadTreeseedMachineConfig(tenantRoot);
|
|
370
|
+
const marketSettings = machineConfig.settings?.market && typeof machineConfig.settings.market === "object" ? machineConfig.settings.market : {};
|
|
371
|
+
const remoteSettings = machineConfig.settings?.remote && typeof machineConfig.settings.remote === "object" ? machineConfig.settings.remote : { activeHostId: "official", executionMode: "prefer-local", hosts: [] };
|
|
372
|
+
const baseUrl = normalizeOptionalString(input.marketBaseUrl) ?? normalizeOptionalString(marketSettings.baseUrl) ?? normalizeOptionalString(remoteSettings.hosts?.find?.((entry) => entry?.official === true)?.baseUrl) ?? normalizeOptionalString(remoteSettings.hosts?.find?.((entry) => entry?.id === remoteSettings.activeHostId)?.baseUrl);
|
|
373
|
+
if (!baseUrl) {
|
|
374
|
+
workflowError(
|
|
375
|
+
"config",
|
|
376
|
+
"validation_failed",
|
|
377
|
+
"Treeseed config --connect-market requires a market base URL. Pass --market-base-url or configure an authenticated remote host first."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const hostId = normalizeOptionalString(marketSettings.hostId) ?? "knowledge-coop";
|
|
381
|
+
const activeRemoteSession = resolveTreeseedRemoteSession(tenantRoot, hostId) ?? resolveTreeseedRemoteSession(tenantRoot, remoteSettings.activeHostId) ?? resolveTreeseedRemoteSession(tenantRoot, "official");
|
|
382
|
+
const accessToken = normalizeOptionalString(input.marketAccessToken) ?? normalizeOptionalString(activeRemoteSession?.accessToken);
|
|
383
|
+
if (!accessToken) {
|
|
384
|
+
workflowError(
|
|
385
|
+
"config",
|
|
386
|
+
"validation_failed",
|
|
387
|
+
"Treeseed config --connect-market requires a market access token. Authenticate to the Knowledge Coop control-plane first or pass --market-access-token."
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const projectId = normalizeOptionalString(input.marketProjectId) ?? normalizeOptionalString(marketSettings.projectId);
|
|
391
|
+
if (!projectId) {
|
|
392
|
+
workflowError(
|
|
393
|
+
"config",
|
|
394
|
+
"validation_failed",
|
|
395
|
+
"Treeseed config --connect-market requires --market-project-id or an existing settings.market.projectId value."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
const teamId = normalizeOptionalString(input.marketTeamId) ?? normalizeOptionalString(marketSettings.teamId);
|
|
399
|
+
const projectSlug = normalizeOptionalString(input.marketProjectSlug) ?? normalizeOptionalString(marketSettings.projectSlug) ?? normalizeOptionalString(machineConfig.project?.slug) ?? projectId;
|
|
400
|
+
const teamSlug = normalizeOptionalString(input.marketTeamSlug) ?? normalizeOptionalString(marketSettings.teamSlug);
|
|
401
|
+
const projectApiBaseUrl = normalizeOptionalString(input.marketProjectApiBaseUrl) ?? normalizeOptionalString(marketSettings.projectApiBaseUrl);
|
|
402
|
+
const client = new ControlPlaneClient({
|
|
403
|
+
baseUrl,
|
|
404
|
+
accessToken
|
|
405
|
+
});
|
|
406
|
+
const connectionResult = await client.upsertProjectConnection(projectId, {
|
|
407
|
+
mode: "hybrid",
|
|
408
|
+
projectApiBaseUrl,
|
|
409
|
+
executionOwner: "project_runner",
|
|
410
|
+
metadata: {
|
|
411
|
+
pairingSource: "treeseed_config_connect_market",
|
|
412
|
+
tenantRoot,
|
|
413
|
+
tenantSlug: normalizeOptionalString(machineConfig.project?.slug),
|
|
414
|
+
repoSlug: normalizeOptionalString(machineConfig.project?.slug),
|
|
415
|
+
teamId,
|
|
416
|
+
teamSlug,
|
|
417
|
+
projectSlug,
|
|
418
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
419
|
+
},
|
|
420
|
+
rotateRunnerToken: input.rotateRunnerToken === true
|
|
421
|
+
});
|
|
422
|
+
const hosts = Array.isArray(remoteSettings.hosts) ? [...remoteSettings.hosts] : [];
|
|
423
|
+
const updatedHost = {
|
|
424
|
+
id: hostId,
|
|
425
|
+
label: "Knowledge Coop",
|
|
426
|
+
baseUrl,
|
|
427
|
+
official: false
|
|
428
|
+
};
|
|
429
|
+
const existingHostIndex = hosts.findIndex(
|
|
430
|
+
(entry) => String(entry?.id ?? "") === hostId || String(entry?.baseUrl ?? "").replace(/\/+$/u, "") === baseUrl.replace(/\/+$/u, "")
|
|
431
|
+
);
|
|
432
|
+
if (existingHostIndex >= 0) {
|
|
433
|
+
hosts.splice(existingHostIndex, 1, {
|
|
434
|
+
...hosts[existingHostIndex],
|
|
435
|
+
...updatedHost
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
hosts.unshift(updatedHost);
|
|
439
|
+
}
|
|
440
|
+
if (normalizeOptionalString(input.marketAccessToken)) {
|
|
441
|
+
setTreeseedRemoteSession(tenantRoot, {
|
|
442
|
+
hostId,
|
|
443
|
+
accessToken,
|
|
444
|
+
refreshToken: activeRemoteSession?.refreshToken ?? "",
|
|
445
|
+
expiresAt: activeRemoteSession?.expiresAt ?? "",
|
|
446
|
+
principal: activeRemoteSession?.principal ?? null
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const runnerHostId = `market-runner:${projectId}`;
|
|
450
|
+
if (connectionResult.runnerToken) {
|
|
451
|
+
setTreeseedRemoteSession(tenantRoot, {
|
|
452
|
+
hostId: runnerHostId,
|
|
453
|
+
accessToken: connectionResult.runnerToken,
|
|
454
|
+
refreshToken: "",
|
|
455
|
+
expiresAt: "",
|
|
456
|
+
principal: {
|
|
457
|
+
id: `runner:${projectId}`,
|
|
458
|
+
displayName: "Knowledge Coop Project Runner",
|
|
459
|
+
scopes: [],
|
|
460
|
+
roles: ["project_runner"],
|
|
461
|
+
permissions: [],
|
|
462
|
+
metadata: { projectId }
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
machineConfig.settings.remote = {
|
|
467
|
+
...remoteSettings,
|
|
468
|
+
activeHostId: hostId,
|
|
469
|
+
hosts
|
|
470
|
+
};
|
|
471
|
+
machineConfig.settings.market = {
|
|
472
|
+
baseUrl,
|
|
473
|
+
hostId,
|
|
474
|
+
teamId,
|
|
475
|
+
teamSlug,
|
|
476
|
+
projectId,
|
|
477
|
+
projectSlug,
|
|
478
|
+
projectApiBaseUrl: connectionResult.connection?.projectApiBaseUrl ?? projectApiBaseUrl ?? null,
|
|
479
|
+
connectionMode: connectionResult.connection?.mode ?? "hybrid",
|
|
480
|
+
executionOwner: connectionResult.connection?.executionOwner ?? "project_runner",
|
|
481
|
+
runnerHostId,
|
|
482
|
+
runnerReady: Boolean(connectionResult.runnerToken || resolveTreeseedRemoteSession(tenantRoot, runnerHostId)?.accessToken),
|
|
483
|
+
runnerRegisteredAt: connectionResult.connection?.runnerRegisteredAt ?? null,
|
|
484
|
+
runnerLastSeenAt: connectionResult.connection?.runnerLastSeenAt ?? null,
|
|
485
|
+
launchPhase: null,
|
|
486
|
+
lastSuccessfulPhase: null,
|
|
487
|
+
githubRepository: null,
|
|
488
|
+
workflowBootstrapReady: false,
|
|
489
|
+
approvalBlockers: [],
|
|
490
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
491
|
+
};
|
|
492
|
+
writeTreeseedMachineConfig(tenantRoot, machineConfig);
|
|
493
|
+
const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
494
|
+
return buildWorkflowResult(
|
|
495
|
+
"config",
|
|
496
|
+
tenantRoot,
|
|
497
|
+
{
|
|
498
|
+
mode: "connect-market",
|
|
499
|
+
scopes: context.scopes,
|
|
500
|
+
sync: context.sync,
|
|
501
|
+
configPath,
|
|
502
|
+
keyPath,
|
|
503
|
+
repairs: context.repairs,
|
|
504
|
+
preflight: context.preflight,
|
|
505
|
+
toolHealth: context.toolHealth,
|
|
506
|
+
market: machineConfig.settings.market,
|
|
507
|
+
connection: connectionResult.connection,
|
|
508
|
+
runnerTokenIssued: Boolean(connectionResult.runnerToken)
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
summary: "Knowledge Coop project pairing completed.",
|
|
512
|
+
nextSteps: createNextSteps([
|
|
513
|
+
{ operation: "status", reason: "Confirm the new market connection, runner health, and current workstream posture." },
|
|
514
|
+
{ operation: "tasks", reason: "Inspect the branch-backed workstreams that will now sync into the Knowledge Coop UI." }
|
|
515
|
+
])
|
|
516
|
+
}
|
|
517
|
+
);
|
|
238
518
|
}
|
|
239
519
|
function maybePrint(write, line, stream = "stdout") {
|
|
240
520
|
if (!line) return;
|
|
@@ -259,6 +539,210 @@ function toError(operation, error) {
|
|
|
259
539
|
}
|
|
260
540
|
throw new TreeseedWorkflowError(operation, "unsupported_state", String(error));
|
|
261
541
|
}
|
|
542
|
+
function workflowSessionSnapshot(session) {
|
|
543
|
+
return {
|
|
544
|
+
root: session.root,
|
|
545
|
+
mode: session.mode,
|
|
546
|
+
branchName: session.branchName,
|
|
547
|
+
repos: [session.rootRepo, ...session.packageRepos].map((repo) => ({
|
|
548
|
+
name: repo.name,
|
|
549
|
+
path: repo.path,
|
|
550
|
+
branchName: repo.branchName
|
|
551
|
+
}))
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function nextPendingJournalStep(journal) {
|
|
555
|
+
return journal.steps.find((step) => step.status === "pending") ?? null;
|
|
556
|
+
}
|
|
557
|
+
async function executeJournalStep(root, runId, stepId, action) {
|
|
558
|
+
const current = readWorkflowRunJournal(root, runId);
|
|
559
|
+
const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
|
|
560
|
+
if (!current || !step) {
|
|
561
|
+
throw new Error(`Unknown workflow step "${stepId}" for run ${runId}.`);
|
|
562
|
+
}
|
|
563
|
+
if (step.status === "completed") {
|
|
564
|
+
return step.data ?? null;
|
|
565
|
+
}
|
|
566
|
+
const data = await Promise.resolve(action());
|
|
567
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
568
|
+
...journal,
|
|
569
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? {
|
|
570
|
+
...entry,
|
|
571
|
+
status: "completed",
|
|
572
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
573
|
+
data: data ?? null
|
|
574
|
+
} : entry)
|
|
575
|
+
}));
|
|
576
|
+
refreshWorkflowLock(root, runId);
|
|
577
|
+
return data;
|
|
578
|
+
}
|
|
579
|
+
function skipJournalStep(root, runId, stepId, data = null) {
|
|
580
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
581
|
+
...journal,
|
|
582
|
+
steps: journal.steps.map((entry) => entry.id === stepId ? {
|
|
583
|
+
...entry,
|
|
584
|
+
status: "skipped",
|
|
585
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
586
|
+
data
|
|
587
|
+
} : entry)
|
|
588
|
+
}));
|
|
589
|
+
refreshWorkflowLock(root, runId);
|
|
590
|
+
}
|
|
591
|
+
function acquireWorkflowRun(operation, session, input, steps, context) {
|
|
592
|
+
const resumeRunId = context.workflow?.resumeRunId;
|
|
593
|
+
if (resumeRunId) {
|
|
594
|
+
const existing = readWorkflowRunJournal(session.root, resumeRunId);
|
|
595
|
+
if (!existing || existing.command !== operation) {
|
|
596
|
+
workflowError(operation, "resume_unavailable", `Treeseed ${operation} cannot resume run ${resumeRunId}.`, {
|
|
597
|
+
details: { runId: resumeRunId, command: operation }
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const lockResult2 = acquireWorkflowLock(session.root, operation, resumeRunId);
|
|
601
|
+
if (!lockResult2.acquired) {
|
|
602
|
+
workflowError(operation, "workflow_locked", `Treeseed ${operation} is blocked by active run ${lockResult2.lock.runId}.`, {
|
|
603
|
+
details: {
|
|
604
|
+
lock: lockResult2.lock,
|
|
605
|
+
recovery: {
|
|
606
|
+
resumable: true,
|
|
607
|
+
runId: lockResult2.lock.runId,
|
|
608
|
+
command: lockResult2.lock.command,
|
|
609
|
+
recoverCommand: "treeseed recover",
|
|
610
|
+
resumeCommand: `treeseed resume ${lockResult2.lock.runId}`
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
runId: resumeRunId,
|
|
617
|
+
session,
|
|
618
|
+
journal: existing,
|
|
619
|
+
resumed: true
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const runId = generateWorkflowRunId(operation);
|
|
623
|
+
const lockResult = acquireWorkflowLock(session.root, operation, runId);
|
|
624
|
+
if (!lockResult.acquired) {
|
|
625
|
+
workflowError(operation, "workflow_locked", `Treeseed ${operation} is blocked by active run ${lockResult.lock.runId}.`, {
|
|
626
|
+
details: {
|
|
627
|
+
lock: lockResult.lock,
|
|
628
|
+
recovery: {
|
|
629
|
+
resumable: true,
|
|
630
|
+
runId: lockResult.lock.runId,
|
|
631
|
+
command: lockResult.lock.command,
|
|
632
|
+
recoverCommand: "treeseed recover",
|
|
633
|
+
resumeCommand: `treeseed resume ${lockResult.lock.runId}`
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const journal = createWorkflowRunJournal(session.root, {
|
|
639
|
+
runId,
|
|
640
|
+
command: operation,
|
|
641
|
+
input,
|
|
642
|
+
session: workflowSessionSnapshot(session),
|
|
643
|
+
steps
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
runId,
|
|
647
|
+
session,
|
|
648
|
+
journal,
|
|
649
|
+
resumed: false
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function completeWorkflowRun(root, runId, result) {
|
|
653
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
654
|
+
...journal,
|
|
655
|
+
status: "completed",
|
|
656
|
+
result,
|
|
657
|
+
failure: null
|
|
658
|
+
}));
|
|
659
|
+
releaseWorkflowLock(root, runId);
|
|
660
|
+
}
|
|
661
|
+
function failWorkflowRun(root, runId, error, recovery) {
|
|
662
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
663
|
+
const code = error instanceof TreeseedWorkflowError ? error.code : "unsupported_state";
|
|
664
|
+
const details = error instanceof TreeseedWorkflowError ? {
|
|
665
|
+
...error.details ?? {},
|
|
666
|
+
recovery: recovery ?? error.details?.recovery ?? null
|
|
667
|
+
} : recovery ? { recovery } : null;
|
|
668
|
+
updateWorkflowRunJournal(root, runId, (journal) => ({
|
|
669
|
+
...journal,
|
|
670
|
+
status: "failed",
|
|
671
|
+
failure: {
|
|
672
|
+
code,
|
|
673
|
+
message,
|
|
674
|
+
details,
|
|
675
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
676
|
+
}
|
|
677
|
+
}));
|
|
678
|
+
releaseWorkflowLock(root, runId);
|
|
679
|
+
}
|
|
680
|
+
function validatePackageReleaseWorkflows(root, packageNames) {
|
|
681
|
+
if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const missing = checkedOutWorkspacePackageRepos(root).filter((pkg) => packageNames.includes(pkg.name)).filter((pkg) => !existsSync(resolve(pkg.dir, ".github", "workflows", "publish.yml"))).map((pkg) => pkg.name);
|
|
685
|
+
if (missing.length > 0) {
|
|
686
|
+
workflowError("release", "workflow_contract_missing", `Treeseed release requires .github/workflows/publish.yml in: ${missing.join(", ")}.`, {
|
|
687
|
+
details: {
|
|
688
|
+
missing
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function validateStagingWorkflowContracts(root) {
|
|
694
|
+
if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const missing = [];
|
|
698
|
+
for (const fileName of ["verify.yml", "deploy.yml"]) {
|
|
699
|
+
if (!existsSync(resolve(root, ".github", "workflows", fileName))) {
|
|
700
|
+
missing.push(fileName);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (missing.length > 0) {
|
|
704
|
+
workflowError("stage", "workflow_contract_missing", `Treeseed stage requires standardized root workflows: ${missing.join(", ")}.`, {
|
|
705
|
+
details: { missing }
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function assertSessionBranchSafety(operation, session, {
|
|
710
|
+
requireCleanPackages = false,
|
|
711
|
+
requireCurrentBranch = false,
|
|
712
|
+
allowPackageReposWithoutOrigin = false
|
|
713
|
+
} = {}) {
|
|
714
|
+
const detached = session.packageRepos.filter((repo) => repo.detached).map((repo) => repo.name);
|
|
715
|
+
if (detached.length > 0) {
|
|
716
|
+
workflowError(operation, "validation_failed", `Detached package heads detected: ${detached.join(", ")}.`, {
|
|
717
|
+
details: { detached }
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (requireCleanPackages) {
|
|
721
|
+
const dirty = session.packageRepos.filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
722
|
+
if (dirty.length > 0) {
|
|
723
|
+
workflowError(operation, "validation_failed", `Dirty package repos block ${operation}: ${dirty.join(", ")}.`, {
|
|
724
|
+
details: { dirty }
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (requireCurrentBranch && session.branchName) {
|
|
729
|
+
const missing = session.packageRepos.filter((repo) => repo.branchName !== session.branchName).map((repo) => ({ name: repo.name, branchName: repo.branchName }));
|
|
730
|
+
if (missing.length > 0) {
|
|
731
|
+
workflowError(operation, "validation_failed", `Package branch alignment is required for ${operation}.`, {
|
|
732
|
+
details: { expectedBranch: session.branchName, repos: missing }
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const missingOriginRepos = [
|
|
737
|
+
session.rootRepo,
|
|
738
|
+
...allowPackageReposWithoutOrigin ? [] : session.packageRepos
|
|
739
|
+
].filter((repo) => !repo.hasOriginRemote).map((repo) => repo.name);
|
|
740
|
+
if (missingOriginRepos.length > 0 && operation !== "destroy") {
|
|
741
|
+
workflowError(operation, "validation_failed", `Missing origin remote on: ${missingOriginRepos.join(", ")}.`, {
|
|
742
|
+
details: { missingOrigin: missingOriginRepos }
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
262
746
|
function previewStateFor(tenantRoot, branchName) {
|
|
263
747
|
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
264
748
|
return loadDeployState(tenantRoot, deployConfig, {
|
|
@@ -365,9 +849,11 @@ function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
|
365
849
|
}
|
|
366
850
|
async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
|
|
367
851
|
const tenantRoot = resolveProjectRootOrThrow(operation, helpers.cwd());
|
|
368
|
-
const
|
|
852
|
+
const root = workspaceRoot(tenantRoot);
|
|
853
|
+
const repoDir = gitWorkflowRoot(root);
|
|
369
854
|
const before = resolveRepoState(repoDir);
|
|
370
|
-
|
|
855
|
+
const packageDirty = checkedOutWorkspacePackageRepos(root).some((pkg) => hasMeaningfulChanges(pkg.dir));
|
|
856
|
+
if (!before.dirtyWorktree && !packageDirty) {
|
|
371
857
|
return { performed: false, save: null };
|
|
372
858
|
}
|
|
373
859
|
if (input.autoSave === false) {
|
|
@@ -384,6 +870,142 @@ async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
|
|
|
384
870
|
save: saveResult.payload
|
|
385
871
|
};
|
|
386
872
|
}
|
|
873
|
+
function checkoutOrCreateSaveBranch(repoDir, branch) {
|
|
874
|
+
const current = currentBranch(repoDir);
|
|
875
|
+
if (current === branch) {
|
|
876
|
+
return current;
|
|
877
|
+
}
|
|
878
|
+
if (branchExists(repoDir, branch)) {
|
|
879
|
+
checkoutBranch(repoDir, branch);
|
|
880
|
+
return branch;
|
|
881
|
+
}
|
|
882
|
+
if (remoteBranchExists(repoDir, branch)) {
|
|
883
|
+
run("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: repoDir });
|
|
884
|
+
return branch;
|
|
885
|
+
}
|
|
886
|
+
run("git", ["checkout", "-b", branch], { cwd: repoDir });
|
|
887
|
+
return branch;
|
|
888
|
+
}
|
|
889
|
+
function runPackageVerifyLocal(pkgDir) {
|
|
890
|
+
run("npm", ["run", "verify:local"], { cwd: pkgDir });
|
|
891
|
+
}
|
|
892
|
+
function branchNeedsSync(repoDir, branch) {
|
|
893
|
+
if (!remoteBranchExists(repoDir, branch)) {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
const localHead = run("git", ["rev-parse", "HEAD"], { cwd: repoDir, capture: true }).trim();
|
|
897
|
+
const remoteHead = run("git", ["rev-parse", `origin/${branch}`], { cwd: repoDir, capture: true }).trim();
|
|
898
|
+
return localHead !== remoteHead;
|
|
899
|
+
}
|
|
900
|
+
function savePackageRepo(report, message, branch, shouldVerify) {
|
|
901
|
+
checkoutOrCreateSaveBranch(report.path, branch);
|
|
902
|
+
report.branch = currentBranch(report.path);
|
|
903
|
+
report.dirty = hasMeaningfulChanges(report.path);
|
|
904
|
+
const needsSync = branchNeedsSync(report.path, branch);
|
|
905
|
+
if (!report.dirty && !needsSync) {
|
|
906
|
+
report.skippedReason = "clean";
|
|
907
|
+
report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
908
|
+
return report;
|
|
909
|
+
}
|
|
910
|
+
if (shouldVerify && report.dirty) {
|
|
911
|
+
runPackageVerifyLocal(report.path);
|
|
912
|
+
report.verified = true;
|
|
913
|
+
}
|
|
914
|
+
if (report.dirty) {
|
|
915
|
+
run("git", ["add", "-A"], { cwd: report.path });
|
|
916
|
+
run("git", ["commit", "-m", message], { cwd: report.path });
|
|
917
|
+
report.committed = true;
|
|
918
|
+
}
|
|
919
|
+
report.commitSha = run("git", ["rev-parse", "HEAD"], { cwd: report.path, capture: true }).trim();
|
|
920
|
+
const branchSync = syncCurrentBranchToOrigin("save", report.path, branch);
|
|
921
|
+
report.pushed = branchSync.pushed === true;
|
|
922
|
+
if (!report.dirty && needsSync) {
|
|
923
|
+
report.skippedReason = "sync-only";
|
|
924
|
+
}
|
|
925
|
+
return report;
|
|
926
|
+
}
|
|
927
|
+
function createSaveFailure(message, repos, rootRepo, failingRepo, error) {
|
|
928
|
+
const rendered = error instanceof Error ? error.message : String(error);
|
|
929
|
+
const code = error instanceof TreeseedWorkflowError ? error.code : "unsupported_state";
|
|
930
|
+
const exitCode = error instanceof TreeseedWorkflowError ? error.exitCode : void 0;
|
|
931
|
+
throw new TreeseedWorkflowError("save", code, `${message}
|
|
932
|
+
${rendered}`, {
|
|
933
|
+
details: {
|
|
934
|
+
partialFailure: {
|
|
935
|
+
message,
|
|
936
|
+
failingRepo: failingRepo?.name ?? null,
|
|
937
|
+
repos,
|
|
938
|
+
rootRepo,
|
|
939
|
+
error: rendered
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
exitCode
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function ensureLocalTaskBranch(repoDir, branchName) {
|
|
946
|
+
if (!branchExists(repoDir, branchName) && !remoteBranchExists(repoDir, branchName)) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
if (!branchExists(repoDir, branchName) && remoteBranchExists(repoDir, branchName)) {
|
|
950
|
+
ensureLocalBranchTracking(repoDir, branchName);
|
|
951
|
+
}
|
|
952
|
+
if (currentBranch(repoDir) !== branchName) {
|
|
953
|
+
checkoutBranch(repoDir, branchName);
|
|
954
|
+
}
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
function cleanupTaskBranchReport(report, branchName, message, { deleteBranch = true, targetBranch = STAGING_BRANCH } = {}) {
|
|
958
|
+
if (!ensureLocalTaskBranch(report.path, branchName)) {
|
|
959
|
+
report.skippedReason = "branch-missing";
|
|
960
|
+
return report;
|
|
961
|
+
}
|
|
962
|
+
const deprecatedTag = createDeprecatedTaskTag(report.path, branchName, message);
|
|
963
|
+
report.tagName = deprecatedTag.tagName;
|
|
964
|
+
report.commitSha = deprecatedTag.head;
|
|
965
|
+
report.deletedRemote = deleteBranch ? deleteRemoteBranch(report.path, branchName) : false;
|
|
966
|
+
syncBranchWithOrigin(report.path, targetBranch);
|
|
967
|
+
if (deleteBranch) {
|
|
968
|
+
deleteLocalBranch(report.path, branchName);
|
|
969
|
+
report.deletedLocal = true;
|
|
970
|
+
}
|
|
971
|
+
report.branch = currentBranch(report.path) || targetBranch;
|
|
972
|
+
report.dirty = hasMeaningfulChanges(report.path);
|
|
973
|
+
return report;
|
|
974
|
+
}
|
|
975
|
+
function syncAllCheckedOutPackageRepos(root, branchName) {
|
|
976
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
977
|
+
syncBranchWithOrigin(pkg.dir, branchName);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function collectReleasePackageSelection(root) {
|
|
981
|
+
const publishable = sortWorkspacePackages(
|
|
982
|
+
publishableWorkspacePackages(root).filter((pkg) => pkg.name?.startsWith("@treeseed/"))
|
|
983
|
+
);
|
|
984
|
+
const changed = changedWorkspacePackages({
|
|
985
|
+
root,
|
|
986
|
+
baseRef: PRODUCTION_BRANCH,
|
|
987
|
+
includeDependents: false,
|
|
988
|
+
packages: publishable
|
|
989
|
+
});
|
|
990
|
+
const selected = changedWorkspacePackages({
|
|
991
|
+
root,
|
|
992
|
+
baseRef: PRODUCTION_BRANCH,
|
|
993
|
+
includeDependents: true,
|
|
994
|
+
packages: publishable
|
|
995
|
+
});
|
|
996
|
+
const changedNames = changed.map((pkg) => pkg.name);
|
|
997
|
+
const selectedNames = selected.map((pkg) => pkg.name);
|
|
998
|
+
const dependents = selected.filter((pkg) => !changedNames.includes(pkg.name)).map((pkg) => pkg.name);
|
|
999
|
+
return {
|
|
1000
|
+
changed: changedNames,
|
|
1001
|
+
dependents,
|
|
1002
|
+
selected: selectedNames,
|
|
1003
|
+
publishable
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function hasStagedChanges(repoDir) {
|
|
1007
|
+
return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
|
|
1008
|
+
}
|
|
387
1009
|
async function workflowStatus(helpers) {
|
|
388
1010
|
return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
|
|
389
1011
|
}
|
|
@@ -400,13 +1022,27 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
400
1022
|
const revealSecrets = input.showSecrets === true;
|
|
401
1023
|
const printEnvOnly = input.printEnvOnly === true;
|
|
402
1024
|
const rotateMachineKeyFlag = input.rotateMachineKey === true;
|
|
1025
|
+
const connectMarketFlag = input.connectMarket === true;
|
|
1026
|
+
const nonInteractive = input.nonInteractive === true;
|
|
403
1027
|
const repairs = input.repair === false ? [] : resolveTreeseedWorkflowState(tenantRoot).deployConfigPresent ? applyTreeseedSafeRepairs(tenantRoot) : [];
|
|
404
1028
|
const toolHealth = ensureTreeseedActVerificationTooling({
|
|
405
1029
|
tenantRoot,
|
|
406
|
-
installIfMissing: true,
|
|
1030
|
+
installIfMissing: input.installMissingTooling === true,
|
|
407
1031
|
env: helpers.context.env,
|
|
408
1032
|
write: (line) => maybePrint(helpers.write, line)
|
|
409
1033
|
});
|
|
1034
|
+
const secretSession = printEnvOnly && !revealSecrets ? {
|
|
1035
|
+
status: inspectTreeseedKeyAgentStatus(tenantRoot),
|
|
1036
|
+
createdWrappedKey: false,
|
|
1037
|
+
migratedWrappedKey: false,
|
|
1038
|
+
unlockSource: "existing-session"
|
|
1039
|
+
} : await ensureTreeseedSecretSessionForConfig({
|
|
1040
|
+
tenantRoot,
|
|
1041
|
+
interactive: false,
|
|
1042
|
+
env: helpers.context.env,
|
|
1043
|
+
createIfMissing: true,
|
|
1044
|
+
allowMigration: true
|
|
1045
|
+
});
|
|
410
1046
|
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
411
1047
|
const preflight = collectCliPreflight({ cwd: tenantRoot, requireAuth: false });
|
|
412
1048
|
const contextSnapshot = collectTreeseedConfigContext({
|
|
@@ -425,10 +1061,10 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
425
1061
|
}),
|
|
426
1062
|
provider: checkTreeseedProviderConnections({ tenantRoot, scope, env: helpers.context.env })
|
|
427
1063
|
}));
|
|
428
|
-
return
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1064
|
+
return buildWorkflowResult(
|
|
1065
|
+
"config",
|
|
1066
|
+
tenantRoot,
|
|
1067
|
+
{
|
|
432
1068
|
mode: "print-env-only",
|
|
433
1069
|
scopes,
|
|
434
1070
|
sync,
|
|
@@ -436,31 +1072,48 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
436
1072
|
reports: reports2,
|
|
437
1073
|
repairs,
|
|
438
1074
|
preflight,
|
|
439
|
-
toolHealth
|
|
1075
|
+
toolHealth,
|
|
1076
|
+
context: contextSnapshot,
|
|
1077
|
+
secretSession
|
|
440
1078
|
},
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1079
|
+
{
|
|
1080
|
+
nextSteps: createNextSteps([
|
|
1081
|
+
{ operation: "config", reason: "Initialize the selected environment after reviewing the generated values.", input: { environment: scopes } }
|
|
1082
|
+
])
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
445
1085
|
}
|
|
446
1086
|
if (rotateMachineKeyFlag) {
|
|
447
1087
|
const result = rotateTreeseedMachineKey(tenantRoot);
|
|
448
|
-
return
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1088
|
+
return buildWorkflowResult(
|
|
1089
|
+
"config",
|
|
1090
|
+
tenantRoot,
|
|
1091
|
+
{
|
|
452
1092
|
mode: "rotate-machine-key",
|
|
453
1093
|
scopes,
|
|
454
1094
|
sync,
|
|
455
1095
|
keyPath: result.keyPath,
|
|
456
1096
|
repairs,
|
|
457
1097
|
preflight,
|
|
458
|
-
toolHealth
|
|
1098
|
+
toolHealth,
|
|
1099
|
+
context: contextSnapshot,
|
|
1100
|
+
secretSession
|
|
459
1101
|
},
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1102
|
+
{
|
|
1103
|
+
nextSteps: createNextSteps([
|
|
1104
|
+
{ operation: "config", reason: "Inspect the regenerated local environment after the machine key rotation.", input: { environment: ["local"], printEnvOnly: true } }
|
|
1105
|
+
])
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
if (connectMarketFlag) {
|
|
1110
|
+
return connectTreeseedMarketProject(helpers, tenantRoot, input, {
|
|
1111
|
+
scopes,
|
|
1112
|
+
sync,
|
|
1113
|
+
repairs,
|
|
1114
|
+
preflight,
|
|
1115
|
+
toolHealth
|
|
1116
|
+
});
|
|
464
1117
|
}
|
|
465
1118
|
const explicitUpdates = Array.isArray(input.updates) ? input.updates.map((update) => ({
|
|
466
1119
|
scope: update.scope,
|
|
@@ -468,6 +1121,13 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
468
1121
|
value: typeof update.value === "string" ? update.value : "",
|
|
469
1122
|
reused: update.reused === true
|
|
470
1123
|
})) : null;
|
|
1124
|
+
if (!explicitUpdates && !nonInteractive) {
|
|
1125
|
+
workflowError(
|
|
1126
|
+
"config",
|
|
1127
|
+
"validation_failed",
|
|
1128
|
+
"Treeseed config requires interactive input or explicit updates. Re-run in a TTY, or use --non-interactive/--json from the CLI when you want resolved values applied automatically."
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
471
1131
|
const autoUpdates = scopes.flatMap(
|
|
472
1132
|
(scope) => contextSnapshot.entriesByScope[scope].map((entry) => ({
|
|
473
1133
|
scope,
|
|
@@ -476,6 +1136,7 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
476
1136
|
reused: entry.currentValue.length > 0 || entry.suggestedValue.length > 0
|
|
477
1137
|
}))
|
|
478
1138
|
);
|
|
1139
|
+
maybePrint(helpers.write, "Saving resolved configuration values to machine config...");
|
|
479
1140
|
const applyResult = applyTreeseedConfigValues({
|
|
480
1141
|
tenantRoot,
|
|
481
1142
|
updates: explicitUpdates ?? autoUpdates
|
|
@@ -484,6 +1145,12 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
484
1145
|
tenantRoot,
|
|
485
1146
|
scopes,
|
|
486
1147
|
sync,
|
|
1148
|
+
env: helpers.context.env,
|
|
1149
|
+
onProgress: (line) => maybePrint(helpers.write, line)
|
|
1150
|
+
});
|
|
1151
|
+
const refreshedContext = collectTreeseedConfigContext({
|
|
1152
|
+
tenantRoot,
|
|
1153
|
+
scopes,
|
|
487
1154
|
env: helpers.context.env
|
|
488
1155
|
});
|
|
489
1156
|
const reports = printEnv ? scopes.map((scope) => ({
|
|
@@ -510,7 +1177,8 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
510
1177
|
repairs,
|
|
511
1178
|
preflight,
|
|
512
1179
|
toolHealth,
|
|
513
|
-
|
|
1180
|
+
secretSession,
|
|
1181
|
+
context: refreshedContext,
|
|
514
1182
|
result: {
|
|
515
1183
|
...applyResult,
|
|
516
1184
|
...finalizeResult
|
|
@@ -539,46 +1207,129 @@ async function workflowExport(helpers, input = {}) {
|
|
|
539
1207
|
}
|
|
540
1208
|
async function workflowSwitch(helpers, input) {
|
|
541
1209
|
try {
|
|
542
|
-
return withContextEnv(helpers.context.env, () => {
|
|
1210
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
543
1211
|
const tenantRoot = resolveProjectRootOrThrow("switch", helpers.cwd());
|
|
1212
|
+
const root = workspaceRoot(tenantRoot);
|
|
1213
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
544
1214
|
const branchName = String(input.branch ?? input.branchName ?? "").trim();
|
|
545
1215
|
if (!branchName) {
|
|
546
1216
|
workflowError("switch", "validation_failed", "Treeseed switch requires a branch name.");
|
|
547
1217
|
}
|
|
548
1218
|
const preview = input.preview === true;
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
1219
|
+
const executionMode = normalizeExecutionMode(input);
|
|
1220
|
+
const mode = session.mode;
|
|
1221
|
+
const repoDir = session.gitRoot;
|
|
1222
|
+
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
1223
|
+
const packageReports = createWorkspacePackageReports(root);
|
|
553
1224
|
let previewResult = null;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1225
|
+
const dirtyRepos = [rootRepo, ...packageReports].filter((repo) => repo.dirty).map((repo) => repo.name);
|
|
1226
|
+
if (executionMode === "plan") {
|
|
1227
|
+
for (const report of [rootRepo, ...packageReports]) {
|
|
1228
|
+
const local = branchExists(report.path, branchName);
|
|
1229
|
+
const remote = remoteBranchExists(report.path, branchName);
|
|
1230
|
+
report.created = !local && !remote;
|
|
1231
|
+
report.resumed = local || remote;
|
|
559
1232
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1233
|
+
return buildWorkflowResult(
|
|
1234
|
+
"switch",
|
|
1235
|
+
root,
|
|
1236
|
+
{
|
|
1237
|
+
mode,
|
|
1238
|
+
branchName,
|
|
1239
|
+
rootRepo,
|
|
1240
|
+
repos: packageReports,
|
|
1241
|
+
previewRequested: preview,
|
|
1242
|
+
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
1243
|
+
plannedSteps: [
|
|
1244
|
+
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
1245
|
+
...packageReports.map((report) => ({ id: `switch-${report.name}`, description: `Mirror ${branchName} into ${report.name}` })),
|
|
1246
|
+
...preview ? [{ id: "preview", description: `Provision or refresh preview for ${branchName}` }] : []
|
|
1247
|
+
]
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
executionMode,
|
|
1251
|
+
nextSteps: createNextSteps([
|
|
1252
|
+
{ operation: "switch", reason: "Run without --plan to create or resume the task branch.", input: { branch: branchName, preview } }
|
|
1253
|
+
])
|
|
1254
|
+
}
|
|
1255
|
+
);
|
|
569
1256
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1257
|
+
if (mode === "recursive-workspace") {
|
|
1258
|
+
assertWorkspaceClean(root);
|
|
1259
|
+
assertSessionBranchSafety("switch", session);
|
|
1260
|
+
} else {
|
|
1261
|
+
assertCleanWorktree(root);
|
|
573
1262
|
}
|
|
574
|
-
const
|
|
575
|
-
return buildWorkflowResult(
|
|
1263
|
+
const workflowRun = acquireWorkflowRun(
|
|
576
1264
|
"switch",
|
|
577
|
-
|
|
578
|
-
{
|
|
1265
|
+
session,
|
|
1266
|
+
{ branch: branchName, preview },
|
|
1267
|
+
[
|
|
1268
|
+
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
1269
|
+
...packageReports.map((report) => ({
|
|
1270
|
+
id: `switch-${report.name}`,
|
|
1271
|
+
description: `Mirror ${branchName} into ${report.name}`,
|
|
1272
|
+
repoName: report.name,
|
|
1273
|
+
repoPath: report.path,
|
|
1274
|
+
branch: branchName,
|
|
1275
|
+
resumable: true
|
|
1276
|
+
})),
|
|
1277
|
+
...preview ? [{ id: "preview", description: `Provision or refresh preview ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true }] : []
|
|
1278
|
+
],
|
|
1279
|
+
helpers.context
|
|
1280
|
+
);
|
|
1281
|
+
try {
|
|
1282
|
+
const rootSwitch = await executeJournalStep(
|
|
1283
|
+
root,
|
|
1284
|
+
workflowRun.runId,
|
|
1285
|
+
"switch-root",
|
|
1286
|
+
() => checkoutTaskBranchFromStaging(repoDir, branchName, {
|
|
1287
|
+
createIfMissing: input.createIfMissing !== false,
|
|
1288
|
+
pushIfCreated: true
|
|
1289
|
+
})
|
|
1290
|
+
);
|
|
1291
|
+
rootRepo.branch = currentBranch(repoDir) || branchName;
|
|
1292
|
+
rootRepo.created = rootSwitch.created;
|
|
1293
|
+
rootRepo.resumed = rootSwitch.resumed;
|
|
1294
|
+
rootRepo.commitSha = headCommit(repoDir);
|
|
1295
|
+
rootRepo.pushed = rootSwitch.created;
|
|
1296
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1297
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
1298
|
+
if (!report) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const packageSwitch = await executeJournalStep(
|
|
1302
|
+
root,
|
|
1303
|
+
workflowRun.runId,
|
|
1304
|
+
`switch-${report.name}`,
|
|
1305
|
+
() => checkoutTaskBranchFromStaging(pkg.dir, branchName, {
|
|
1306
|
+
createIfMissing: input.createIfMissing !== false,
|
|
1307
|
+
pushIfCreated: false
|
|
1308
|
+
})
|
|
1309
|
+
);
|
|
1310
|
+
report.branch = currentBranch(pkg.dir) || branchName;
|
|
1311
|
+
report.created = packageSwitch.created;
|
|
1312
|
+
report.resumed = packageSwitch.resumed;
|
|
1313
|
+
report.commitSha = headCommit(pkg.dir);
|
|
1314
|
+
report.dirty = hasMeaningfulChanges(pkg.dir);
|
|
1315
|
+
}
|
|
1316
|
+
const stateAfterSwitch = resolveTreeseedWorkflowState(root);
|
|
1317
|
+
if (preview) {
|
|
1318
|
+
previewResult = await executeJournalStep(
|
|
1319
|
+
root,
|
|
1320
|
+
workflowRun.runId,
|
|
1321
|
+
"preview",
|
|
1322
|
+
() => deployBranchPreview(root, branchName, helpers.context, { initialize: !stateAfterSwitch.preview.enabled })
|
|
1323
|
+
) ?? null;
|
|
1324
|
+
}
|
|
1325
|
+
const state = resolveTreeseedWorkflowState(root);
|
|
1326
|
+
const payload = {
|
|
1327
|
+
mode,
|
|
579
1328
|
branchName,
|
|
580
|
-
created,
|
|
581
|
-
resumed,
|
|
1329
|
+
created: rootRepo.created,
|
|
1330
|
+
resumed: rootRepo.resumed,
|
|
1331
|
+
repos: packageReports,
|
|
1332
|
+
rootRepo,
|
|
582
1333
|
previewRequested: preview,
|
|
583
1334
|
preview: {
|
|
584
1335
|
enabled: state.preview.enabled,
|
|
@@ -590,12 +1341,31 @@ async function workflowSwitch(helpers, input) {
|
|
|
590
1341
|
cleanWorktreeRequired: true,
|
|
591
1342
|
baseBranch: STAGING_BRANCH
|
|
592
1343
|
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1344
|
+
};
|
|
1345
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
1346
|
+
return buildWorkflowResult(
|
|
1347
|
+
"switch",
|
|
1348
|
+
root,
|
|
1349
|
+
payload,
|
|
1350
|
+
{
|
|
1351
|
+
runId: workflowRun.runId,
|
|
1352
|
+
nextSteps: createNextSteps([
|
|
1353
|
+
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." },
|
|
1354
|
+
{ operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
|
|
1355
|
+
])
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
1360
|
+
resumable: true,
|
|
1361
|
+
runId: workflowRun.runId,
|
|
1362
|
+
command: "switch",
|
|
1363
|
+
message: `Resume the interrupted switch for ${branchName}.`,
|
|
1364
|
+
recoverCommand: "treeseed recover",
|
|
1365
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
1366
|
+
});
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
599
1369
|
});
|
|
600
1370
|
} catch (error) {
|
|
601
1371
|
toError("switch", error);
|
|
@@ -618,7 +1388,11 @@ async function workflowDev(helpers, input = {}) {
|
|
|
618
1388
|
if (input.port !== void 0) {
|
|
619
1389
|
args.push("--port", String(input.port));
|
|
620
1390
|
}
|
|
621
|
-
const env = {
|
|
1391
|
+
const env = resolveTreeseedLaunchEnvironment({
|
|
1392
|
+
tenantRoot,
|
|
1393
|
+
scope: "local",
|
|
1394
|
+
baseEnv: { ...process.env, ...helpers.context.env ?? {} }
|
|
1395
|
+
});
|
|
622
1396
|
if (input.background) {
|
|
623
1397
|
const child = spawn(process.execPath, args, {
|
|
624
1398
|
cwd: tenantRoot,
|
|
@@ -669,15 +1443,19 @@ async function workflowDev(helpers, input = {}) {
|
|
|
669
1443
|
}
|
|
670
1444
|
async function workflowSave(helpers, input) {
|
|
671
1445
|
try {
|
|
672
|
-
return withContextEnv(helpers.context.env, () => {
|
|
1446
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
673
1447
|
const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
|
|
674
1448
|
const message = ensureMessage("save", input.message, "a commit message");
|
|
675
1449
|
const optionsHotfix = input.hotfix === true;
|
|
676
1450
|
const root = workspaceRoot(tenantRoot);
|
|
677
|
-
const
|
|
678
|
-
const
|
|
1451
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
1452
|
+
const gitRoot = session.gitRoot;
|
|
1453
|
+
const branch = session.branchName;
|
|
679
1454
|
const scope = branch === STAGING_BRANCH ? "staging" : branch === PRODUCTION_BRANCH ? "prod" : "local";
|
|
680
1455
|
const beforeState = resolveTreeseedWorkflowState(root);
|
|
1456
|
+
const recursiveWorkspace = session.mode === "recursive-workspace";
|
|
1457
|
+
const mode = session.mode;
|
|
1458
|
+
const executionMode = normalizeExecutionMode(input);
|
|
681
1459
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
682
1460
|
if (!branch) {
|
|
683
1461
|
workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
|
|
@@ -685,42 +1463,218 @@ async function workflowSave(helpers, input) {
|
|
|
685
1463
|
if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
|
|
686
1464
|
workflowError("save", "unsupported_state", "Treeseed save is blocked on main unless --hotfix is explicitly set.");
|
|
687
1465
|
}
|
|
1466
|
+
const packageReports = createWorkspacePackageReports(root);
|
|
1467
|
+
const rootRepo = createRepoReport("@treeseed/market", gitRoot, branch, hasMeaningfulChanges(gitRoot));
|
|
1468
|
+
const blockers = [];
|
|
1469
|
+
if (executionMode === "plan") {
|
|
1470
|
+
if (!session.rootRepo.hasOriginRemote) {
|
|
1471
|
+
blockers.push("Market repo is missing origin remote.");
|
|
1472
|
+
}
|
|
1473
|
+
if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
|
|
1474
|
+
blockers.push("Main saves require --hotfix.");
|
|
1475
|
+
}
|
|
1476
|
+
if (recursiveWorkspace) {
|
|
1477
|
+
try {
|
|
1478
|
+
assertWorkspaceVersionConsistency(root);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return buildWorkflowResult(
|
|
1484
|
+
"save",
|
|
1485
|
+
root,
|
|
1486
|
+
{
|
|
1487
|
+
mode,
|
|
1488
|
+
branch,
|
|
1489
|
+
scope,
|
|
1490
|
+
hotfix: optionsHotfix,
|
|
1491
|
+
message,
|
|
1492
|
+
repos: packageReports,
|
|
1493
|
+
rootRepo,
|
|
1494
|
+
blockers,
|
|
1495
|
+
plannedSteps: [
|
|
1496
|
+
...packageReports.map((report) => ({ id: `save-${report.name}`, description: `Verify, commit, and push ${report.name}` })),
|
|
1497
|
+
...input.verify !== false ? [{ id: "verify-root", description: "Run market workspace verification" }] : [],
|
|
1498
|
+
{ id: "commit-root", description: "Commit market repo changes if present" },
|
|
1499
|
+
{ id: "sync-root", description: `Push ${branch} to origin` },
|
|
1500
|
+
...beforeState.branchRole === "feature" && (input.preview === true || beforeState.preview.enabled) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
1501
|
+
]
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
executionMode,
|
|
1505
|
+
nextSteps: createNextSteps([
|
|
1506
|
+
{ operation: "save", reason: "Run without --plan to persist the workspace checkpoint.", input: { message, hotfix: optionsHotfix, preview: input.preview === true } }
|
|
1507
|
+
])
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
assertSessionBranchSafety("save", session, {
|
|
1512
|
+
allowPackageReposWithoutOrigin: true
|
|
1513
|
+
});
|
|
688
1514
|
try {
|
|
689
1515
|
originRemoteUrl(gitRoot);
|
|
690
1516
|
} catch {
|
|
691
1517
|
workflowError("save", "validation_failed", "Treeseed save requires an origin remote.");
|
|
692
1518
|
}
|
|
693
|
-
|
|
694
|
-
runWorkspaceSavePreflight({ cwd: root });
|
|
695
|
-
}
|
|
696
|
-
const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
|
|
697
|
-
let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
698
|
-
let commitCreated = false;
|
|
699
|
-
if (hadMeaningfulChanges) {
|
|
700
|
-
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
701
|
-
run("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
702
|
-
head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
703
|
-
commitCreated = true;
|
|
704
|
-
}
|
|
705
|
-
const branchSync = syncCurrentBranchToOrigin("save", gitRoot, branch);
|
|
706
|
-
let previewAction = { status: "skipped" };
|
|
707
|
-
if (beforeState.branchRole === "feature" && branch) {
|
|
708
|
-
if (input.preview === true) {
|
|
709
|
-
previewAction = {
|
|
710
|
-
status: beforeState.preview.enabled ? "refreshed" : "created",
|
|
711
|
-
details: deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled })
|
|
712
|
-
};
|
|
713
|
-
} else if (input.refreshPreview !== false && beforeState.preview.enabled) {
|
|
714
|
-
previewAction = {
|
|
715
|
-
status: "refreshed",
|
|
716
|
-
details: deployBranchPreview(root, branch, helpers.context, { initialize: false })
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return buildWorkflowResult(
|
|
1519
|
+
const workflowRun = acquireWorkflowRun(
|
|
721
1520
|
"save",
|
|
722
|
-
|
|
1521
|
+
session,
|
|
723
1522
|
{
|
|
1523
|
+
message,
|
|
1524
|
+
hotfix: optionsHotfix,
|
|
1525
|
+
preview: input.preview === true,
|
|
1526
|
+
refreshPreview: input.refreshPreview !== false,
|
|
1527
|
+
verify: input.verify !== false
|
|
1528
|
+
},
|
|
1529
|
+
[
|
|
1530
|
+
...packageReports.map((report) => ({
|
|
1531
|
+
id: `save-${report.name}`,
|
|
1532
|
+
description: `Save ${report.name}`,
|
|
1533
|
+
repoName: report.name,
|
|
1534
|
+
repoPath: report.path,
|
|
1535
|
+
branch,
|
|
1536
|
+
resumable: true
|
|
1537
|
+
})),
|
|
1538
|
+
...input.verify !== false ? [{
|
|
1539
|
+
id: "verify-root",
|
|
1540
|
+
description: "Verify market workspace",
|
|
1541
|
+
repoName: rootRepo.name,
|
|
1542
|
+
repoPath: rootRepo.path,
|
|
1543
|
+
branch,
|
|
1544
|
+
resumable: true
|
|
1545
|
+
}] : [],
|
|
1546
|
+
{
|
|
1547
|
+
id: "commit-root",
|
|
1548
|
+
description: "Commit market workspace changes",
|
|
1549
|
+
repoName: rootRepo.name,
|
|
1550
|
+
repoPath: rootRepo.path,
|
|
1551
|
+
branch,
|
|
1552
|
+
resumable: true
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
id: "sync-root",
|
|
1556
|
+
description: `Push ${branch} to origin`,
|
|
1557
|
+
repoName: rootRepo.name,
|
|
1558
|
+
repoPath: rootRepo.path,
|
|
1559
|
+
branch,
|
|
1560
|
+
resumable: true
|
|
1561
|
+
},
|
|
1562
|
+
...beforeState.branchRole === "feature" && (input.preview === true || input.refreshPreview !== false && beforeState.preview.enabled) ? [{
|
|
1563
|
+
id: "preview",
|
|
1564
|
+
description: `Refresh preview ${branch}`,
|
|
1565
|
+
repoName: rootRepo.name,
|
|
1566
|
+
repoPath: rootRepo.path,
|
|
1567
|
+
branch,
|
|
1568
|
+
resumable: true
|
|
1569
|
+
}] : []
|
|
1570
|
+
],
|
|
1571
|
+
helpers.context
|
|
1572
|
+
);
|
|
1573
|
+
try {
|
|
1574
|
+
if (recursiveWorkspace) {
|
|
1575
|
+
assertWorkspaceVersionConsistency(root);
|
|
1576
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1577
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
1578
|
+
if (!report) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
const step = readWorkflowRunJournal(root, workflowRun.runId)?.steps.find((entry) => entry.id === `save-${report.name}`) ?? null;
|
|
1583
|
+
const resumePendingSync = workflowRun.resumed && step?.status === "pending" && branchNeedsSync(report.path, branch);
|
|
1584
|
+
if (!report.dirty && !resumePendingSync) {
|
|
1585
|
+
report.skippedReason = "clean";
|
|
1586
|
+
skipJournalStep(root, workflowRun.runId, `save-${report.name}`, {
|
|
1587
|
+
skippedReason: "clean"
|
|
1588
|
+
});
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const savedReport = await executeJournalStep(root, workflowRun.runId, `save-${report.name}`, () => savePackageRepo(report, message, branch, input.verify !== false));
|
|
1592
|
+
Object.assign(report, savedReport);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
createSaveFailure(
|
|
1595
|
+
`Treeseed save stopped while saving workspace package ${pkg.name}.`,
|
|
1596
|
+
packageReports,
|
|
1597
|
+
rootRepo,
|
|
1598
|
+
report,
|
|
1599
|
+
error
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (input.verify !== false) {
|
|
1605
|
+
try {
|
|
1606
|
+
await executeJournalStep(root, workflowRun.runId, "verify-root", () => {
|
|
1607
|
+
runWorkspaceSavePreflight({ cwd: root });
|
|
1608
|
+
rootRepo.verified = true;
|
|
1609
|
+
return {
|
|
1610
|
+
verified: true
|
|
1611
|
+
};
|
|
1612
|
+
});
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
createSaveFailure(
|
|
1615
|
+
"Treeseed save stopped while verifying the market workspace.",
|
|
1616
|
+
packageReports,
|
|
1617
|
+
rootRepo,
|
|
1618
|
+
null,
|
|
1619
|
+
error
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
|
|
1624
|
+
let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
1625
|
+
let commitCreated = false;
|
|
1626
|
+
if (hadMeaningfulChanges) {
|
|
1627
|
+
const commitResult = await executeJournalStep(root, workflowRun.runId, "commit-root", () => {
|
|
1628
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
1629
|
+
run("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
1630
|
+
return {
|
|
1631
|
+
commitSha: run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim()
|
|
1632
|
+
};
|
|
1633
|
+
});
|
|
1634
|
+
head = String(commitResult?.commitSha ?? head);
|
|
1635
|
+
commitCreated = true;
|
|
1636
|
+
rootRepo.committed = true;
|
|
1637
|
+
} else {
|
|
1638
|
+
skipJournalStep(root, workflowRun.runId, "commit-root", {
|
|
1639
|
+
skippedReason: "clean"
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
rootRepo.commitSha = head;
|
|
1643
|
+
let branchSync;
|
|
1644
|
+
try {
|
|
1645
|
+
branchSync = await executeJournalStep(root, workflowRun.runId, "sync-root", () => syncCurrentBranchToOrigin("save", gitRoot, branch));
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
createSaveFailure(
|
|
1648
|
+
"Treeseed save stopped while syncing the market repository.",
|
|
1649
|
+
packageReports,
|
|
1650
|
+
rootRepo,
|
|
1651
|
+
rootRepo,
|
|
1652
|
+
error
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
rootRepo.pushed = branchSync.pushed === true;
|
|
1656
|
+
if (input.verify !== false) {
|
|
1657
|
+
rootRepo.verified = true;
|
|
1658
|
+
}
|
|
1659
|
+
if (!hadMeaningfulChanges) {
|
|
1660
|
+
rootRepo.skippedReason = "clean";
|
|
1661
|
+
}
|
|
1662
|
+
let previewAction = { status: "skipped" };
|
|
1663
|
+
if (beforeState.branchRole === "feature" && branch) {
|
|
1664
|
+
if (input.preview === true) {
|
|
1665
|
+
previewAction = {
|
|
1666
|
+
status: beforeState.preview.enabled ? "refreshed" : "created",
|
|
1667
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled }))
|
|
1668
|
+
};
|
|
1669
|
+
} else if (input.refreshPreview !== false && beforeState.preview.enabled) {
|
|
1670
|
+
previewAction = {
|
|
1671
|
+
status: "refreshed",
|
|
1672
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const payload = {
|
|
1677
|
+
mode,
|
|
724
1678
|
branch,
|
|
725
1679
|
scope,
|
|
726
1680
|
hotfix: optionsHotfix,
|
|
@@ -729,13 +1683,54 @@ async function workflowSave(helpers, input) {
|
|
|
729
1683
|
commitCreated,
|
|
730
1684
|
noChanges: !hadMeaningfulChanges,
|
|
731
1685
|
branchSync,
|
|
1686
|
+
repos: packageReports,
|
|
1687
|
+
rootRepo,
|
|
1688
|
+
partialFailure: null,
|
|
732
1689
|
previewAction,
|
|
733
1690
|
mergeConflict: null
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1691
|
+
};
|
|
1692
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
1693
|
+
return buildWorkflowResult(
|
|
1694
|
+
"save",
|
|
1695
|
+
root,
|
|
1696
|
+
payload,
|
|
1697
|
+
{
|
|
1698
|
+
runId: workflowRun.runId,
|
|
1699
|
+
nextSteps: createNextSteps([
|
|
1700
|
+
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" } }
|
|
1701
|
+
])
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
const failingRepo = packageReports.find((report) => report.dirty && report.pushed !== true) ?? rootRepo;
|
|
1706
|
+
const wrappedError = error instanceof TreeseedWorkflowError && error.details?.partialFailure != null ? error : new TreeseedWorkflowError(
|
|
1707
|
+
"save",
|
|
1708
|
+
error instanceof TreeseedWorkflowError ? error.code : "unsupported_state",
|
|
1709
|
+
error instanceof Error ? error.message : String(error),
|
|
1710
|
+
{
|
|
1711
|
+
details: {
|
|
1712
|
+
...error instanceof TreeseedWorkflowError ? error.details ?? {} : {},
|
|
1713
|
+
partialFailure: {
|
|
1714
|
+
message: "Treeseed save stopped before the workspace could finish syncing.",
|
|
1715
|
+
failingRepo: failingRepo.name,
|
|
1716
|
+
repos: packageReports,
|
|
1717
|
+
rootRepo,
|
|
1718
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
exitCode: error instanceof TreeseedWorkflowError ? error.exitCode : void 0
|
|
1722
|
+
}
|
|
1723
|
+
);
|
|
1724
|
+
failWorkflowRun(root, workflowRun.runId, wrappedError, {
|
|
1725
|
+
resumable: true,
|
|
1726
|
+
runId: workflowRun.runId,
|
|
1727
|
+
command: "save",
|
|
1728
|
+
message: `Resume the interrupted save on ${branch}.`,
|
|
1729
|
+
recoverCommand: "treeseed recover",
|
|
1730
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
1731
|
+
});
|
|
1732
|
+
throw wrappedError;
|
|
1733
|
+
}
|
|
739
1734
|
});
|
|
740
1735
|
} catch (error) {
|
|
741
1736
|
toError("save", error);
|
|
@@ -743,41 +1738,148 @@ async function workflowSave(helpers, input) {
|
|
|
743
1738
|
}
|
|
744
1739
|
async function workflowClose(helpers, input) {
|
|
745
1740
|
try {
|
|
746
|
-
return withContextEnv(helpers.context.env, async () => {
|
|
1741
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
747
1742
|
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
1743
|
+
const root = workspaceRoot(tenantRoot);
|
|
748
1744
|
const message = ensureMessage("close", input.message, "a close reason");
|
|
1745
|
+
const executionMode = normalizeExecutionMode(input);
|
|
1746
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
1747
|
+
if (executionMode === "plan") {
|
|
1748
|
+
const branchName = session.branchName;
|
|
1749
|
+
const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
|
|
1750
|
+
return buildWorkflowResult(
|
|
1751
|
+
"close",
|
|
1752
|
+
root,
|
|
1753
|
+
{
|
|
1754
|
+
mode: session.mode,
|
|
1755
|
+
branchName,
|
|
1756
|
+
message,
|
|
1757
|
+
autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
|
|
1758
|
+
repos: createWorkspacePackageReports(root),
|
|
1759
|
+
rootRepo: createWorkspaceRootRepoReport(root),
|
|
1760
|
+
blockers,
|
|
1761
|
+
plannedSteps: [
|
|
1762
|
+
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
1763
|
+
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
1764
|
+
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
1765
|
+
id: `cleanup-${pkg.name}`,
|
|
1766
|
+
description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
|
|
1767
|
+
}))
|
|
1768
|
+
]
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
executionMode,
|
|
1772
|
+
nextSteps: createNextSteps([
|
|
1773
|
+
{ operation: "close", reason: "Run without --plan to archive and delete the task branch.", input: { message } }
|
|
1774
|
+
])
|
|
1775
|
+
}
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
749
1778
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
750
1779
|
message,
|
|
751
1780
|
autoSave: input.autoSave
|
|
752
1781
|
});
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1782
|
+
const activeSession = resolveTreeseedWorkflowSession(root);
|
|
1783
|
+
const featureBranch = assertFeatureBranch(root);
|
|
1784
|
+
const mode = activeSession.mode;
|
|
1785
|
+
const repoDir = activeSession.gitRoot;
|
|
1786
|
+
assertSessionBranchSafety("close", activeSession);
|
|
1787
|
+
if (mode === "recursive-workspace") {
|
|
1788
|
+
assertWorkspaceClean(root);
|
|
1789
|
+
} else {
|
|
1790
|
+
assertCleanWorktree(root);
|
|
762
1791
|
}
|
|
763
|
-
|
|
1792
|
+
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
1793
|
+
const packageReports = createWorkspacePackageReports(root);
|
|
1794
|
+
const workflowRun = acquireWorkflowRun(
|
|
764
1795
|
"close",
|
|
765
|
-
|
|
766
|
-
{
|
|
1796
|
+
activeSession,
|
|
1797
|
+
{ message, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
|
|
1798
|
+
[
|
|
1799
|
+
{ id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
1800
|
+
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
1801
|
+
...packageReports.map((report) => ({
|
|
1802
|
+
id: `cleanup-${report.name}`,
|
|
1803
|
+
description: `Archive ${featureBranch} in ${report.name}`,
|
|
1804
|
+
repoName: report.name,
|
|
1805
|
+
repoPath: report.path,
|
|
1806
|
+
branch: featureBranch,
|
|
1807
|
+
resumable: true
|
|
1808
|
+
}))
|
|
1809
|
+
],
|
|
1810
|
+
helpers.context
|
|
1811
|
+
);
|
|
1812
|
+
try {
|
|
1813
|
+
const previewCleanup = input.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
|
|
1814
|
+
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
1815
|
+
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
1816
|
+
const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
1817
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
1818
|
+
if (input.deleteBranch !== false) {
|
|
1819
|
+
deleteLocalBranch(repoDir, featureBranch);
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
deprecatedTag,
|
|
1823
|
+
deletedRemote,
|
|
1824
|
+
deletedLocal: input.deleteBranch !== false,
|
|
1825
|
+
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
1826
|
+
dirty: hasMeaningfulChanges(repoDir)
|
|
1827
|
+
};
|
|
1828
|
+
});
|
|
1829
|
+
rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? null);
|
|
1830
|
+
rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
|
|
1831
|
+
rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
|
|
1832
|
+
rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
|
|
1833
|
+
rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
1834
|
+
rootRepo.dirty = rootCleanup?.dirty === true;
|
|
1835
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1836
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
1837
|
+
if (!report) {
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
|
|
1841
|
+
deleteBranch: input.deleteBranch !== false,
|
|
1842
|
+
targetBranch: STAGING_BRANCH
|
|
1843
|
+
}));
|
|
1844
|
+
Object.assign(report, cleanup);
|
|
1845
|
+
}
|
|
1846
|
+
const payload = {
|
|
1847
|
+
mode,
|
|
767
1848
|
branchName: featureBranch,
|
|
768
1849
|
message,
|
|
769
1850
|
autoSaved: autoSave.performed,
|
|
770
1851
|
autoSaveResult: autoSave.save,
|
|
771
|
-
deprecatedTag,
|
|
1852
|
+
deprecatedTag: rootCleanup?.deprecatedTag ?? null,
|
|
1853
|
+
repos: packageReports,
|
|
1854
|
+
rootRepo,
|
|
772
1855
|
previewCleanup,
|
|
773
|
-
remoteDeleted,
|
|
774
|
-
localDeleted:
|
|
1856
|
+
remoteDeleted: rootRepo.deletedRemote,
|
|
1857
|
+
localDeleted: rootRepo.deletedLocal,
|
|
775
1858
|
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1859
|
+
};
|
|
1860
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
1861
|
+
return buildWorkflowResult(
|
|
1862
|
+
"close",
|
|
1863
|
+
root,
|
|
1864
|
+
payload,
|
|
1865
|
+
{
|
|
1866
|
+
runId: workflowRun.runId,
|
|
1867
|
+
nextSteps: createNextSteps([
|
|
1868
|
+
{ operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
|
|
1869
|
+
])
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
1874
|
+
resumable: true,
|
|
1875
|
+
runId: workflowRun.runId,
|
|
1876
|
+
command: "close",
|
|
1877
|
+
message: `Resume the interrupted close for ${featureBranch}.`,
|
|
1878
|
+
recoverCommand: "treeseed recover",
|
|
1879
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
1880
|
+
});
|
|
1881
|
+
throw error;
|
|
1882
|
+
}
|
|
781
1883
|
});
|
|
782
1884
|
} catch (error) {
|
|
783
1885
|
toError("close", error);
|
|
@@ -785,53 +1887,232 @@ async function workflowClose(helpers, input) {
|
|
|
785
1887
|
}
|
|
786
1888
|
async function workflowStage(helpers, input) {
|
|
787
1889
|
try {
|
|
788
|
-
return withContextEnv(helpers.context.env, async () => {
|
|
1890
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
789
1891
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
1892
|
+
const root = workspaceRoot(tenantRoot);
|
|
790
1893
|
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
1894
|
+
const executionMode = normalizeExecutionMode(input);
|
|
1895
|
+
const initialSession = resolveTreeseedWorkflowSession(root);
|
|
1896
|
+
if (executionMode === "plan") {
|
|
1897
|
+
const blockers = [];
|
|
1898
|
+
try {
|
|
1899
|
+
validateStagingWorkflowContracts(root);
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
1902
|
+
}
|
|
1903
|
+
return buildWorkflowResult(
|
|
1904
|
+
"stage",
|
|
1905
|
+
root,
|
|
1906
|
+
{
|
|
1907
|
+
mode: initialSession.mode,
|
|
1908
|
+
branchName: initialSession.branchName,
|
|
1909
|
+
mergeTarget: STAGING_BRANCH,
|
|
1910
|
+
mergeStrategy: "squash",
|
|
1911
|
+
message,
|
|
1912
|
+
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
1913
|
+
blockers,
|
|
1914
|
+
rootRepo: createWorkspaceRootRepoReport(root),
|
|
1915
|
+
repos: createWorkspacePackageReports(root),
|
|
1916
|
+
plannedSteps: [
|
|
1917
|
+
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
1918
|
+
id: `merge-${pkg.name}`,
|
|
1919
|
+
description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into ${pkg.name} staging`
|
|
1920
|
+
})),
|
|
1921
|
+
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
1922
|
+
{ id: "wait-staging", description: "Wait for staging automation" },
|
|
1923
|
+
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
1924
|
+
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
1925
|
+
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
1926
|
+
id: `cleanup-${pkg.name}`,
|
|
1927
|
+
description: `Archive and delete the task branch from ${pkg.name}`
|
|
1928
|
+
}))
|
|
1929
|
+
]
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
executionMode,
|
|
1933
|
+
nextSteps: createNextSteps([
|
|
1934
|
+
{ operation: "stage", reason: "Run without --plan to promote the task branch into staging.", input: { message } }
|
|
1935
|
+
])
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
791
1939
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
792
1940
|
message,
|
|
793
1941
|
autoSave: input.autoSave
|
|
794
1942
|
});
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
804
|
-
exitCode: 12
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
const stagingWait = input.waitForStaging === false ? { status: "skipped", reason: "disabled" } : waitForStagingAutomation(repoDir);
|
|
808
|
-
const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
|
|
809
|
-
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
810
|
-
const remoteDeleted = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
811
|
-
if (input.deleteBranch !== false) {
|
|
812
|
-
deleteLocalBranch(repoDir, featureBranch);
|
|
1943
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
1944
|
+
const featureBranch = assertFeatureBranch(root);
|
|
1945
|
+
const mode = session.mode;
|
|
1946
|
+
assertSessionBranchSafety("stage", session);
|
|
1947
|
+
if (mode === "recursive-workspace") {
|
|
1948
|
+
assertWorkspaceClean(root);
|
|
1949
|
+
} else {
|
|
1950
|
+
assertCleanWorktree(root);
|
|
813
1951
|
}
|
|
814
|
-
|
|
1952
|
+
validateStagingWorkflowContracts(root);
|
|
1953
|
+
runWorkspaceSavePreflight({ cwd: root });
|
|
1954
|
+
const repoDir = session.gitRoot;
|
|
1955
|
+
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
1956
|
+
const packageReports = createWorkspacePackageReports(root);
|
|
1957
|
+
const workflowRun = acquireWorkflowRun(
|
|
815
1958
|
"stage",
|
|
816
|
-
|
|
817
|
-
{
|
|
1959
|
+
session,
|
|
1960
|
+
{ message, waitForStaging: input.waitForStaging !== false, deletePreview: input.deletePreview !== false, deleteBranch: input.deleteBranch !== false },
|
|
1961
|
+
[
|
|
1962
|
+
...packageReports.map((report) => ({
|
|
1963
|
+
id: `merge-${report.name}`,
|
|
1964
|
+
description: `Merge ${featureBranch} into ${report.name} staging`,
|
|
1965
|
+
repoName: report.name,
|
|
1966
|
+
repoPath: report.path,
|
|
1967
|
+
branch: featureBranch,
|
|
1968
|
+
resumable: true
|
|
1969
|
+
})),
|
|
1970
|
+
{ id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
1971
|
+
{ id: "wait-staging", description: "Wait for staging automation", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
1972
|
+
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
1973
|
+
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
1974
|
+
...packageReports.map((report) => ({
|
|
1975
|
+
id: `cleanup-${report.name}`,
|
|
1976
|
+
description: `Archive ${featureBranch} in ${report.name}`,
|
|
1977
|
+
repoName: report.name,
|
|
1978
|
+
repoPath: report.path,
|
|
1979
|
+
branch: featureBranch,
|
|
1980
|
+
resumable: true
|
|
1981
|
+
}))
|
|
1982
|
+
],
|
|
1983
|
+
helpers.context
|
|
1984
|
+
);
|
|
1985
|
+
try {
|
|
1986
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1987
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
1988
|
+
if (!report) {
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (!ensureLocalTaskBranch(pkg.dir, featureBranch)) {
|
|
1992
|
+
report.skippedReason = "branch-missing";
|
|
1993
|
+
skipJournalStep(root, workflowRun.runId, `merge-${report.name}`, { skippedReason: "branch-missing" });
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
const mergeResult = await executeJournalStep(root, workflowRun.runId, `merge-${report.name}`, () => squashMergeBranchIntoStaging(pkg.dir, featureBranch, message, { pushTarget: true }));
|
|
1998
|
+
report.merged = mergeResult.committed;
|
|
1999
|
+
report.committed = mergeResult.committed;
|
|
2000
|
+
report.pushed = mergeResult.pushed;
|
|
2001
|
+
report.commitSha = mergeResult.commitSha;
|
|
2002
|
+
report.branch = STAGING_BRANCH;
|
|
2003
|
+
checkoutBranch(pkg.dir, featureBranch);
|
|
2004
|
+
report.branch = featureBranch;
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
const reportData = collectMergeConflictReport(pkg.dir);
|
|
2007
|
+
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(reportData, pkg.dir, STAGING_BRANCH), {
|
|
2008
|
+
details: { branch: featureBranch, packageName: pkg.name, report: reportData, originalError: error instanceof Error ? error.message : String(error) },
|
|
2009
|
+
exitCode: 12
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
const rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", () => {
|
|
2015
|
+
assertCleanWorktree(root);
|
|
2016
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2017
|
+
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
2018
|
+
if (mode === "recursive-workspace") {
|
|
2019
|
+
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
2020
|
+
}
|
|
2021
|
+
if (hasStagedChanges(repoDir) || hasMeaningfulChanges(repoDir)) {
|
|
2022
|
+
run("git", ["add", "-A"], { cwd: repoDir });
|
|
2023
|
+
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
2024
|
+
}
|
|
2025
|
+
pushBranch(repoDir, STAGING_BRANCH);
|
|
2026
|
+
return {
|
|
2027
|
+
commitSha: headCommit(repoDir),
|
|
2028
|
+
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
2029
|
+
committed: hasMeaningfulChanges(repoDir) ? false : true
|
|
2030
|
+
};
|
|
2031
|
+
});
|
|
2032
|
+
rootRepo.merged = true;
|
|
2033
|
+
rootRepo.committed = true;
|
|
2034
|
+
rootRepo.commitSha = String(rootMerge?.commitSha ?? headCommit(repoDir));
|
|
2035
|
+
rootRepo.pushed = true;
|
|
2036
|
+
rootRepo.branch = typeof rootMerge?.branch === "string" ? rootMerge.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
const report = collectMergeConflictReport(repoDir);
|
|
2039
|
+
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
2040
|
+
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
2041
|
+
exitCode: 12
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
const stagingWait = input.waitForStaging === false ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForStagingAutomation(repoDir));
|
|
2045
|
+
const previewCleanup = input.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
|
|
2046
|
+
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2047
|
+
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
2048
|
+
const deletedRemote = input.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2049
|
+
if (input.deleteBranch !== false) {
|
|
2050
|
+
deleteLocalBranch(repoDir, featureBranch);
|
|
2051
|
+
}
|
|
2052
|
+
return {
|
|
2053
|
+
deprecatedTag,
|
|
2054
|
+
deletedRemote,
|
|
2055
|
+
deletedLocal: input.deleteBranch !== false,
|
|
2056
|
+
branch: currentBranch(repoDir) || STAGING_BRANCH
|
|
2057
|
+
};
|
|
2058
|
+
});
|
|
2059
|
+
rootRepo.tagName = String(rootCleanup?.deprecatedTag?.tagName ?? rootRepo.tagName ?? "");
|
|
2060
|
+
rootRepo.commitSha = String(rootCleanup?.deprecatedTag?.head ?? rootRepo.commitSha ?? "");
|
|
2061
|
+
rootRepo.deletedRemote = rootCleanup?.deletedRemote === true;
|
|
2062
|
+
rootRepo.deletedLocal = rootCleanup?.deletedLocal === true;
|
|
2063
|
+
rootRepo.branch = typeof rootCleanup?.branch === "string" ? rootCleanup.branch : currentBranch(repoDir) || STAGING_BRANCH;
|
|
2064
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2065
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
2066
|
+
if (!report) {
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
2070
|
+
deleteBranch: input.deleteBranch !== false,
|
|
2071
|
+
targetBranch: STAGING_BRANCH
|
|
2072
|
+
}));
|
|
2073
|
+
Object.assign(report, cleanup);
|
|
2074
|
+
}
|
|
2075
|
+
const payload = {
|
|
2076
|
+
mode,
|
|
818
2077
|
branchName: featureBranch,
|
|
819
2078
|
mergeTarget: STAGING_BRANCH,
|
|
2079
|
+
mergeStrategy: "squash",
|
|
820
2080
|
message,
|
|
821
2081
|
autoSaved: autoSave.performed,
|
|
822
2082
|
autoSaveResult: autoSave.save,
|
|
823
|
-
deprecatedTag,
|
|
2083
|
+
deprecatedTag: rootCleanup?.deprecatedTag ?? null,
|
|
2084
|
+
repos: packageReports,
|
|
2085
|
+
rootRepo,
|
|
824
2086
|
stagingWait,
|
|
825
2087
|
previewCleanup,
|
|
826
|
-
remoteDeleted,
|
|
827
|
-
localDeleted:
|
|
2088
|
+
remoteDeleted: rootRepo.deletedRemote,
|
|
2089
|
+
localDeleted: rootRepo.deletedLocal,
|
|
828
2090
|
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
2091
|
+
};
|
|
2092
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2093
|
+
return buildWorkflowResult(
|
|
2094
|
+
"stage",
|
|
2095
|
+
root,
|
|
2096
|
+
payload,
|
|
2097
|
+
{
|
|
2098
|
+
runId: workflowRun.runId,
|
|
2099
|
+
nextSteps: createNextSteps([
|
|
2100
|
+
{ operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
|
|
2101
|
+
{ operation: "status", reason: "Inspect staging readiness after the task branch merge." }
|
|
2102
|
+
])
|
|
2103
|
+
}
|
|
2104
|
+
);
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2107
|
+
resumable: true,
|
|
2108
|
+
runId: workflowRun.runId,
|
|
2109
|
+
command: "stage",
|
|
2110
|
+
message: `Resume the interrupted stage for ${featureBranch}.`,
|
|
2111
|
+
recoverCommand: "treeseed recover",
|
|
2112
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
2113
|
+
});
|
|
2114
|
+
throw error;
|
|
2115
|
+
}
|
|
835
2116
|
});
|
|
836
2117
|
} catch (error) {
|
|
837
2118
|
toError("stage", error);
|
|
@@ -839,101 +2120,507 @@ async function workflowStage(helpers, input) {
|
|
|
839
2120
|
}
|
|
840
2121
|
async function workflowRelease(helpers, input) {
|
|
841
2122
|
try {
|
|
842
|
-
return withContextEnv(helpers.context.env, () => {
|
|
2123
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
843
2124
|
const level = input.bump ?? "patch";
|
|
844
2125
|
const root = resolveProjectRootOrThrow("release", helpers.cwd());
|
|
845
|
-
const
|
|
2126
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
2127
|
+
const gitRoot = session.gitRoot;
|
|
2128
|
+
const mode = session.mode;
|
|
2129
|
+
const executionMode = normalizeExecutionMode(input);
|
|
2130
|
+
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
2131
|
+
const packageReports = createWorkspacePackageReports(root);
|
|
2132
|
+
const packageSelection = session.packageSelection;
|
|
2133
|
+
const selectedPackageNames = new Set(packageSelection.selected);
|
|
2134
|
+
const blockers = [];
|
|
2135
|
+
if (session.branchName !== STAGING_BRANCH) {
|
|
2136
|
+
blockers.push("Release must start from staging.");
|
|
2137
|
+
}
|
|
2138
|
+
if (mode === "recursive-workspace") {
|
|
2139
|
+
try {
|
|
2140
|
+
assertWorkspaceVersionConsistency(root);
|
|
2141
|
+
validatePackageReleaseWorkflows(root, packageSelection.selected);
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const versionPlan = planWorkspaceReleaseBump(level, root, mode === "recursive-workspace" ? { selectedPackageNames } : {});
|
|
2147
|
+
const plannedVersions = Object.fromEntries(versionPlan.versions.entries());
|
|
2148
|
+
if (executionMode === "plan") {
|
|
2149
|
+
return buildWorkflowResult(
|
|
2150
|
+
"release",
|
|
2151
|
+
root,
|
|
2152
|
+
{
|
|
2153
|
+
mode,
|
|
2154
|
+
mergeStrategy: "merge-commit",
|
|
2155
|
+
level,
|
|
2156
|
+
stagingBranch: STAGING_BRANCH,
|
|
2157
|
+
productionBranch: PRODUCTION_BRANCH,
|
|
2158
|
+
packageSelection,
|
|
2159
|
+
plannedVersions,
|
|
2160
|
+
repos: packageReports,
|
|
2161
|
+
rootRepo,
|
|
2162
|
+
plannedSteps: [
|
|
2163
|
+
...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
|
|
2164
|
+
id: `release-${report.name}`,
|
|
2165
|
+
description: `Release ${report.name} from staging to main and tag ${plannedVersions[report.name] ?? "(planned)"}`
|
|
2166
|
+
})),
|
|
2167
|
+
{ id: "release-root", description: `Release market ${plannedVersions["@treeseed/market"] ?? "(planned)"}` }
|
|
2168
|
+
],
|
|
2169
|
+
blockers
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
executionMode,
|
|
2173
|
+
nextSteps: createNextSteps([
|
|
2174
|
+
{ operation: "release", reason: "Run without --plan to promote staging into production.", input: { bump: level } }
|
|
2175
|
+
])
|
|
2176
|
+
}
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
if (blockers.length > 0) {
|
|
2180
|
+
workflowError("release", "validation_failed", blockers.join("\n"), {
|
|
2181
|
+
details: { blockers }
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
assertSessionBranchSafety("release", session);
|
|
846
2185
|
prepareReleaseBranches(root);
|
|
847
2186
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
848
2187
|
runWorkspaceSavePreflight({ cwd: root });
|
|
849
|
-
const
|
|
850
|
-
applyWorkspaceVersionChanges(plan);
|
|
851
|
-
const rootVersion = bumpRootPackageJson(root, level);
|
|
852
|
-
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
853
|
-
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
854
|
-
run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
|
|
855
|
-
pushBranch(gitRoot, STAGING_BRANCH);
|
|
856
|
-
mergeStagingIntoMain(root);
|
|
857
|
-
const releasedCommit = run("git", ["rev-parse", PRODUCTION_BRANCH], { cwd: gitRoot, capture: true }).trim();
|
|
858
|
-
run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
|
|
859
|
-
run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
|
|
860
|
-
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
861
|
-
return buildWorkflowResult(
|
|
2188
|
+
const workflowRun = acquireWorkflowRun(
|
|
862
2189
|
"release",
|
|
863
|
-
|
|
864
|
-
{
|
|
2190
|
+
session,
|
|
2191
|
+
{ bump: level },
|
|
2192
|
+
[
|
|
2193
|
+
...packageReports.filter((report) => selectedPackageNames.has(report.name)).map((report) => ({
|
|
2194
|
+
id: `release-${report.name}`,
|
|
2195
|
+
description: `Release ${report.name}`,
|
|
2196
|
+
repoName: report.name,
|
|
2197
|
+
repoPath: report.path,
|
|
2198
|
+
branch: STAGING_BRANCH,
|
|
2199
|
+
resumable: true
|
|
2200
|
+
})),
|
|
2201
|
+
{ id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }
|
|
2202
|
+
],
|
|
2203
|
+
helpers.context
|
|
2204
|
+
);
|
|
2205
|
+
try {
|
|
2206
|
+
if (mode === "root-only") {
|
|
2207
|
+
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
2208
|
+
applyWorkspaceVersionChanges(versionPlan);
|
|
2209
|
+
const rootVersion = bumpRootPackageJson(root, level);
|
|
2210
|
+
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2211
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
2212
|
+
run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
|
|
2213
|
+
pushBranch(gitRoot, STAGING_BRANCH);
|
|
2214
|
+
const released = mergeStagingIntoMain(root);
|
|
2215
|
+
run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
|
|
2216
|
+
run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
|
|
2217
|
+
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2218
|
+
return {
|
|
2219
|
+
rootVersion,
|
|
2220
|
+
releasedCommit: released.commitSha
|
|
2221
|
+
};
|
|
2222
|
+
});
|
|
2223
|
+
rootRepo.committed = true;
|
|
2224
|
+
rootRepo.pushed = true;
|
|
2225
|
+
rootRepo.merged = true;
|
|
2226
|
+
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2227
|
+
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
2228
|
+
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
2229
|
+
const payload2 = {
|
|
2230
|
+
mode,
|
|
2231
|
+
mergeStrategy: "merge-commit",
|
|
2232
|
+
level,
|
|
2233
|
+
rootVersion: String(rootRelease2?.rootVersion ?? ""),
|
|
2234
|
+
releaseTag: String(rootRelease2?.rootVersion ?? ""),
|
|
2235
|
+
releasedCommit: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? ""),
|
|
2236
|
+
stagingBranch: STAGING_BRANCH,
|
|
2237
|
+
productionBranch: PRODUCTION_BRANCH,
|
|
2238
|
+
touchedPackages: [...versionPlan.touched],
|
|
2239
|
+
packageSelection: { changed: [], dependents: [], selected: [] },
|
|
2240
|
+
publishWait: [],
|
|
2241
|
+
repos: [],
|
|
2242
|
+
rootRepo,
|
|
2243
|
+
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
2244
|
+
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true }
|
|
2245
|
+
};
|
|
2246
|
+
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
2247
|
+
return buildWorkflowResult("release", root, payload2, {
|
|
2248
|
+
runId: workflowRun.runId,
|
|
2249
|
+
nextSteps: createNextSteps([
|
|
2250
|
+
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
2251
|
+
])
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
assertWorkspaceVersionConsistency(root);
|
|
2255
|
+
validatePackageReleaseWorkflows(root, packageSelection.selected);
|
|
2256
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2257
|
+
if (selectedPackageNames.has(pkg.name)) {
|
|
2258
|
+
prepareReleaseBranches(pkg.dir);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
applyWorkspaceVersionChanges(versionPlan);
|
|
2262
|
+
const publishWait = [];
|
|
2263
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2264
|
+
const report = findReportByName(packageReports, pkg.name);
|
|
2265
|
+
if (!report || !selectedPackageNames.has(pkg.name)) {
|
|
2266
|
+
if (report) {
|
|
2267
|
+
report.skippedReason = "unchanged";
|
|
2268
|
+
}
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, () => {
|
|
2272
|
+
checkoutBranch(pkg.dir, STAGING_BRANCH);
|
|
2273
|
+
if (hasMeaningfulChanges(pkg.dir)) {
|
|
2274
|
+
run("git", ["add", "-A"], { cwd: pkg.dir });
|
|
2275
|
+
run("git", ["commit", "-m", `release: ${versionPlan.versions.get(pkg.name)}`], { cwd: pkg.dir });
|
|
2276
|
+
}
|
|
2277
|
+
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
2278
|
+
const mergeResult = mergeBranchIntoTarget(pkg.dir, {
|
|
2279
|
+
sourceBranch: STAGING_BRANCH,
|
|
2280
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
2281
|
+
message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
2282
|
+
pushTarget: true
|
|
2283
|
+
});
|
|
2284
|
+
const tagName = String(versionPlan.versions.get(pkg.name));
|
|
2285
|
+
run("git", ["tag", "-a", tagName, "-m", `release: ${tagName}`], { cwd: pkg.dir });
|
|
2286
|
+
run("git", ["push", "origin", tagName], { cwd: pkg.dir });
|
|
2287
|
+
const publish = waitForGitHubWorkflowCompletion(pkg.dir, {
|
|
2288
|
+
workflow: "publish.yml",
|
|
2289
|
+
headSha: mergeResult.commitSha,
|
|
2290
|
+
branch: PRODUCTION_BRANCH
|
|
2291
|
+
});
|
|
2292
|
+
syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
|
|
2293
|
+
return {
|
|
2294
|
+
commitSha: mergeResult.commitSha,
|
|
2295
|
+
tagName,
|
|
2296
|
+
publish
|
|
2297
|
+
};
|
|
2298
|
+
});
|
|
2299
|
+
report.committed = true;
|
|
2300
|
+
report.pushed = true;
|
|
2301
|
+
report.merged = true;
|
|
2302
|
+
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
2303
|
+
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
2304
|
+
report.publishWait = releasedPackage?.publish ?? null;
|
|
2305
|
+
report.branch = STAGING_BRANCH;
|
|
2306
|
+
publishWait.push({
|
|
2307
|
+
name: report.name,
|
|
2308
|
+
...releasedPackage?.publish ?? {}
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
2312
|
+
const rootVersion = bumpRootPackageJson(root, level);
|
|
2313
|
+
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2314
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
2315
|
+
run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
|
|
2316
|
+
pushBranch(gitRoot, STAGING_BRANCH);
|
|
2317
|
+
const released = mergeBranchIntoTarget(root, {
|
|
2318
|
+
sourceBranch: STAGING_BRANCH,
|
|
2319
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
2320
|
+
message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
2321
|
+
pushTarget: false
|
|
2322
|
+
});
|
|
2323
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2324
|
+
if (selectedPackageNames.has(pkg.name)) {
|
|
2325
|
+
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
2329
|
+
if (hasMeaningfulChanges(gitRoot)) {
|
|
2330
|
+
run("git", ["commit", "-m", "release: sync package main heads"], { cwd: gitRoot });
|
|
2331
|
+
}
|
|
2332
|
+
run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
|
|
2333
|
+
run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
|
|
2334
|
+
run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
|
|
2335
|
+
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
2336
|
+
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2337
|
+
return {
|
|
2338
|
+
rootVersion,
|
|
2339
|
+
releasedCommit: headCommit(gitRoot)
|
|
2340
|
+
};
|
|
2341
|
+
});
|
|
2342
|
+
rootRepo.committed = true;
|
|
2343
|
+
rootRepo.pushed = true;
|
|
2344
|
+
rootRepo.merged = true;
|
|
2345
|
+
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2346
|
+
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
2347
|
+
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
2348
|
+
const payload = {
|
|
2349
|
+
mode,
|
|
2350
|
+
mergeStrategy: "merge-commit",
|
|
865
2351
|
level,
|
|
866
|
-
rootVersion,
|
|
867
|
-
releaseTag: rootVersion,
|
|
868
|
-
releasedCommit,
|
|
2352
|
+
rootVersion: String(rootRelease?.rootVersion ?? ""),
|
|
2353
|
+
releaseTag: String(rootRelease?.rootVersion ?? ""),
|
|
2354
|
+
releasedCommit: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? ""),
|
|
869
2355
|
stagingBranch: STAGING_BRANCH,
|
|
870
2356
|
productionBranch: PRODUCTION_BRANCH,
|
|
871
|
-
touchedPackages:
|
|
2357
|
+
touchedPackages: packageSelection.selected,
|
|
2358
|
+
packageSelection,
|
|
2359
|
+
publishWait,
|
|
2360
|
+
repos: packageReports,
|
|
2361
|
+
rootRepo,
|
|
872
2362
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
873
2363
|
pushStatus: {
|
|
874
2364
|
stagingPushed: true,
|
|
875
2365
|
productionPushed: true,
|
|
876
2366
|
tagPushed: true
|
|
877
2367
|
}
|
|
2368
|
+
};
|
|
2369
|
+
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2370
|
+
return buildWorkflowResult(
|
|
2371
|
+
"release",
|
|
2372
|
+
root,
|
|
2373
|
+
payload,
|
|
2374
|
+
{
|
|
2375
|
+
runId: workflowRun.runId,
|
|
2376
|
+
nextSteps: createNextSteps([
|
|
2377
|
+
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
2378
|
+
])
|
|
2379
|
+
}
|
|
2380
|
+
);
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2383
|
+
resumable: true,
|
|
2384
|
+
runId: workflowRun.runId,
|
|
2385
|
+
command: "release",
|
|
2386
|
+
message: `Resume the interrupted release on ${STAGING_BRANCH}.`,
|
|
2387
|
+
recoverCommand: "treeseed recover",
|
|
2388
|
+
resumeCommand: `treeseed resume ${workflowRun.runId}`
|
|
2389
|
+
});
|
|
2390
|
+
throw error;
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
} catch (error) {
|
|
2394
|
+
toError("release", error);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
async function workflowResume(helpers, input) {
|
|
2398
|
+
try {
|
|
2399
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
2400
|
+
const root = resolveProjectRootOrThrow("resume", helpers.cwd());
|
|
2401
|
+
const runId = String(input.runId ?? "").trim();
|
|
2402
|
+
if (!runId) {
|
|
2403
|
+
workflowError("resume", "validation_failed", "Treeseed resume requires a run id.");
|
|
2404
|
+
}
|
|
2405
|
+
const journal = readWorkflowRunJournal(root, runId);
|
|
2406
|
+
if (!journal) {
|
|
2407
|
+
workflowError("resume", "resume_unavailable", `Treeseed resume could not find run ${runId}.`, {
|
|
2408
|
+
details: { runId }
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
if (journal.status === "completed") {
|
|
2412
|
+
workflowError("resume", "resume_unavailable", `Run ${runId} is already completed.`, {
|
|
2413
|
+
details: { runId, status: journal.status }
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
if (!journal.resumable) {
|
|
2417
|
+
workflowError("resume", "resume_unavailable", `Run ${runId} is not resumable.`, {
|
|
2418
|
+
details: { runId, status: journal.status }
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
const resumedHelpers = {
|
|
2422
|
+
...helpers,
|
|
2423
|
+
context: {
|
|
2424
|
+
...helpers.context,
|
|
2425
|
+
workflow: {
|
|
2426
|
+
...helpers.context.workflow ?? {},
|
|
2427
|
+
resumeRunId: runId
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
switch (journal.command) {
|
|
2432
|
+
case "switch":
|
|
2433
|
+
return workflowSwitch(resumedHelpers, journal.input);
|
|
2434
|
+
case "save":
|
|
2435
|
+
return workflowSave(resumedHelpers, journal.input);
|
|
2436
|
+
case "close":
|
|
2437
|
+
return workflowClose(resumedHelpers, journal.input);
|
|
2438
|
+
case "stage":
|
|
2439
|
+
return workflowStage(resumedHelpers, journal.input);
|
|
2440
|
+
case "release":
|
|
2441
|
+
return workflowRelease(resumedHelpers, journal.input);
|
|
2442
|
+
case "destroy":
|
|
2443
|
+
return workflowDestroy(resumedHelpers, journal.input);
|
|
2444
|
+
default:
|
|
2445
|
+
workflowError("resume", "resume_unavailable", `Run ${runId} uses unsupported command ${journal.command}.`, {
|
|
2446
|
+
details: { runId, command: journal.command }
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
toError("resume", error);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
async function workflowRecover(helpers, input = {}) {
|
|
2455
|
+
try {
|
|
2456
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
2457
|
+
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
2458
|
+
const lock = inspectWorkflowLock(root);
|
|
2459
|
+
const journals = listWorkflowRunJournals(root);
|
|
2460
|
+
const interruptedRuns = listInterruptedWorkflowRuns(root).map((journal) => ({
|
|
2461
|
+
runId: journal.runId,
|
|
2462
|
+
command: journal.command,
|
|
2463
|
+
status: journal.status,
|
|
2464
|
+
createdAt: journal.createdAt,
|
|
2465
|
+
updatedAt: journal.updatedAt,
|
|
2466
|
+
nextStep: nextPendingJournalStep(journal)?.description ?? null,
|
|
2467
|
+
failure: journal.failure,
|
|
2468
|
+
resumeCommand: `treeseed resume ${journal.runId}`
|
|
2469
|
+
}));
|
|
2470
|
+
const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
|
|
2471
|
+
return buildWorkflowResult(
|
|
2472
|
+
"recover",
|
|
2473
|
+
root,
|
|
2474
|
+
{
|
|
2475
|
+
lock,
|
|
2476
|
+
interruptedRuns,
|
|
2477
|
+
selectedRun,
|
|
2478
|
+
runCount: journals.length
|
|
878
2479
|
},
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
2480
|
+
{
|
|
2481
|
+
includeFinalState: false,
|
|
2482
|
+
nextSteps: createNextSteps([
|
|
2483
|
+
...interruptedRuns.length > 0 ? [{ operation: "resume", reason: "Resume the most recent interrupted workflow run.", input: { runId: interruptedRuns[0].runId } }] : [{ operation: "status", reason: "No interrupted runs were found; inspect current workflow state instead." }]
|
|
2484
|
+
])
|
|
2485
|
+
}
|
|
882
2486
|
);
|
|
883
2487
|
});
|
|
884
2488
|
} catch (error) {
|
|
885
|
-
toError("
|
|
2489
|
+
toError("recover", error);
|
|
886
2490
|
}
|
|
887
2491
|
}
|
|
888
2492
|
async function workflowDestroy(helpers, input) {
|
|
889
2493
|
try {
|
|
890
2494
|
return withContextEnv(helpers.context.env, async () => {
|
|
891
|
-
const tenantRoot = helpers.cwd();
|
|
2495
|
+
const tenantRoot = resolveProjectRootOrThrow("destroy", helpers.cwd());
|
|
2496
|
+
const root = workspaceRoot(tenantRoot);
|
|
2497
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
892
2498
|
const scope = String(input.environment ?? input.target ?? "");
|
|
893
2499
|
if (!scope) {
|
|
894
2500
|
workflowError("destroy", "validation_failed", "Treeseed destroy requires an environment target.");
|
|
895
2501
|
}
|
|
2502
|
+
const executionMode = normalizeExecutionMode(input);
|
|
896
2503
|
const target = createPersistentDeployTarget(scope);
|
|
897
|
-
const dryRun =
|
|
2504
|
+
const dryRun = executionMode === "plan";
|
|
898
2505
|
const force = input.force === true;
|
|
899
2506
|
const destroyRemote = input.destroyRemote !== false;
|
|
900
2507
|
const destroyLocal = input.destroyLocal !== false;
|
|
901
2508
|
const removeBuildArtifacts = input.removeBuildArtifacts === true;
|
|
902
2509
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
903
2510
|
assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose: "destroy" });
|
|
904
|
-
const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote:
|
|
2511
|
+
const deployConfig = validateDestroyPrerequisites(tenantRoot, { requireRemote: executionMode === "execute" && destroyRemote });
|
|
905
2512
|
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
906
2513
|
const expectedConfirmation = deployConfig.slug;
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
2514
|
+
const payload = {
|
|
2515
|
+
scope,
|
|
2516
|
+
dryRun,
|
|
2517
|
+
force,
|
|
2518
|
+
destroyRemote,
|
|
2519
|
+
destroyLocal,
|
|
2520
|
+
removeBuildArtifacts,
|
|
2521
|
+
expectedConfirmation,
|
|
2522
|
+
stateSummary: {
|
|
2523
|
+
workerName: state.workerName,
|
|
2524
|
+
lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null
|
|
2525
|
+
},
|
|
2526
|
+
plannedSteps: [
|
|
2527
|
+
...destroyRemote ? [{ id: "destroy-remote", description: `Destroy remote ${scope} resources` }] : [],
|
|
2528
|
+
...destroyLocal ? [{ id: "cleanup-local", description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}` }] : []
|
|
2529
|
+
],
|
|
2530
|
+
remoteResult: null
|
|
2531
|
+
};
|
|
2532
|
+
if (executionMode === "plan") {
|
|
2533
|
+
return buildWorkflowResult(
|
|
2534
|
+
"destroy",
|
|
2535
|
+
tenantRoot,
|
|
2536
|
+
payload,
|
|
2537
|
+
{
|
|
2538
|
+
executionMode,
|
|
2539
|
+
nextSteps: createNextSteps([
|
|
2540
|
+
{ operation: "destroy", reason: "Run without --plan to destroy the selected environment.", input: { environment: scope, force, removeBuildArtifacts } },
|
|
2541
|
+
{ operation: "status", reason: "Confirm the current environment state before making destructive changes." }
|
|
2542
|
+
])
|
|
2543
|
+
}
|
|
2544
|
+
);
|
|
914
2545
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
scope,
|
|
920
|
-
dryRun,
|
|
2546
|
+
const workflowRun = acquireWorkflowRun(
|
|
2547
|
+
"destroy",
|
|
2548
|
+
session,
|
|
2549
|
+
{
|
|
2550
|
+
environment: scope,
|
|
921
2551
|
force,
|
|
922
2552
|
destroyRemote,
|
|
923
2553
|
destroyLocal,
|
|
924
|
-
removeBuildArtifacts
|
|
925
|
-
expectedConfirmation,
|
|
926
|
-
stateSummary: {
|
|
927
|
-
workerName: state.workerName,
|
|
928
|
-
lastDeploymentTimestamp: state.lastDeploymentTimestamp ?? null
|
|
929
|
-
},
|
|
930
|
-
remoteResult: result
|
|
2554
|
+
removeBuildArtifacts
|
|
931
2555
|
},
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
2556
|
+
[
|
|
2557
|
+
...destroyRemote ? [{
|
|
2558
|
+
id: "destroy-remote",
|
|
2559
|
+
description: `Destroy remote ${scope} resources`,
|
|
2560
|
+
repoName: session.rootRepo.name,
|
|
2561
|
+
repoPath: session.rootRepo.path,
|
|
2562
|
+
branch: session.branchName,
|
|
2563
|
+
resumable: false
|
|
2564
|
+
}] : [],
|
|
2565
|
+
...destroyLocal ? [{
|
|
2566
|
+
id: "cleanup-local",
|
|
2567
|
+
description: `Clean local ${scope} state${removeBuildArtifacts ? " and build artifacts" : ""}`,
|
|
2568
|
+
repoName: session.rootRepo.name,
|
|
2569
|
+
repoPath: session.rootRepo.path,
|
|
2570
|
+
branch: session.branchName,
|
|
2571
|
+
resumable: false
|
|
2572
|
+
}] : []
|
|
2573
|
+
],
|
|
2574
|
+
helpers.context
|
|
2575
|
+
);
|
|
2576
|
+
try {
|
|
2577
|
+
const confirmed = await Promise.resolve(resolveDestroyConfirmation(helpers.context, expectedConfirmation, input));
|
|
2578
|
+
if (!confirmed) {
|
|
2579
|
+
workflowError("destroy", "confirmation_required", `Destroy confirmation required. Re-run with confirm="${expectedConfirmation}".`);
|
|
2580
|
+
}
|
|
2581
|
+
const remoteResult = destroyRemote ? await executeJournalStep(root, workflowRun.runId, "destroy-remote", () => destroyCloudflareResources(tenantRoot, { dryRun: false, force, target })) : null;
|
|
2582
|
+
if (!destroyRemote) {
|
|
2583
|
+
skipJournalStep(root, workflowRun.runId, "destroy-remote", { skippedReason: "destroyRemote=false" });
|
|
2584
|
+
}
|
|
2585
|
+
if (destroyLocal) {
|
|
2586
|
+
await executeJournalStep(root, workflowRun.runId, "cleanup-local", () => {
|
|
2587
|
+
cleanupDestroyedState(tenantRoot, { target, removeBuildArtifacts });
|
|
2588
|
+
return {
|
|
2589
|
+
cleaned: true,
|
|
2590
|
+
removeBuildArtifacts
|
|
2591
|
+
};
|
|
2592
|
+
});
|
|
2593
|
+
} else {
|
|
2594
|
+
skipJournalStep(root, workflowRun.runId, "cleanup-local", { skippedReason: "destroyLocal=false" });
|
|
2595
|
+
}
|
|
2596
|
+
const resultPayload = {
|
|
2597
|
+
...payload,
|
|
2598
|
+
dryRun: false,
|
|
2599
|
+
remoteResult
|
|
2600
|
+
};
|
|
2601
|
+
completeWorkflowRun(root, workflowRun.runId, resultPayload);
|
|
2602
|
+
return buildWorkflowResult(
|
|
2603
|
+
"destroy",
|
|
2604
|
+
tenantRoot,
|
|
2605
|
+
resultPayload,
|
|
2606
|
+
{
|
|
2607
|
+
runId: workflowRun.runId,
|
|
2608
|
+
nextSteps: createNextSteps([
|
|
2609
|
+
{ operation: "config", reason: "Recreate the destroyed environment before using it again.", input: { environment: [scope] } },
|
|
2610
|
+
{ operation: "status", reason: "Confirm the environment teardown state and any remaining local runtime setup." }
|
|
2611
|
+
])
|
|
2612
|
+
}
|
|
2613
|
+
);
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2616
|
+
resumable: false,
|
|
2617
|
+
runId: workflowRun.runId,
|
|
2618
|
+
command: "destroy",
|
|
2619
|
+
message: `Inspect the failed destroy run for ${scope} before retrying manually.`,
|
|
2620
|
+
recoverCommand: "treeseed recover"
|
|
2621
|
+
});
|
|
2622
|
+
throw error;
|
|
2623
|
+
}
|
|
937
2624
|
});
|
|
938
2625
|
} catch (error) {
|
|
939
2626
|
toError("destroy", error);
|
|
@@ -946,7 +2633,9 @@ export {
|
|
|
946
2633
|
workflowDestroy,
|
|
947
2634
|
workflowDev,
|
|
948
2635
|
workflowExport,
|
|
2636
|
+
workflowRecover,
|
|
949
2637
|
workflowRelease,
|
|
2638
|
+
workflowResume,
|
|
950
2639
|
workflowSave,
|
|
951
2640
|
workflowStage,
|
|
952
2641
|
workflowStatus,
|