@treeseed/sdk 0.6.9 → 0.6.10
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/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/managed-dependencies.d.ts +23 -0
- package/dist/managed-dependencies.js +133 -12
- package/dist/operations/providers/default.js +35 -0
- package/dist/operations/services/config-runtime.js +16 -3
- package/dist/operations/services/deploy.js +14 -1
- package/dist/operations/services/export-runtime.js +4 -0
- package/dist/operations/services/git-workflow.d.ts +2 -0
- package/dist/operations/services/git-workflow.js +39 -4
- package/dist/operations/services/github-api.d.ts +10 -0
- package/dist/operations/services/github-api.js +20 -1
- package/dist/operations/services/github-automation.d.ts +3 -0
- package/dist/operations/services/repository-save-orchestrator.js +10 -4
- package/dist/operations/services/workspace-dependency-mode.js +10 -18
- package/dist/operations-registry.js +1 -0
- package/dist/scripts/patch-starlight-content-path.js +2 -1
- package/dist/workflow/operations.d.ts +259 -429
- package/dist/workflow/operations.js +652 -72
- package/dist/workflow/runs.d.ts +38 -0
- package/dist/workflow/runs.js +182 -15
- package/dist/workflow/worktrees.d.ts +39 -0
- package/dist/workflow/worktrees.js +224 -0
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +35 -2
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +2 -0
- package/dist/workflow.d.ts +14 -1
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +100 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import {
|
|
5
5
|
applyTreeseedEnvironmentToProcess,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setTreeseedRemoteSession,
|
|
24
24
|
writeTreeseedMachineConfig
|
|
25
25
|
} from "../operations/services/config-runtime.js";
|
|
26
|
-
import { formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
26
|
+
import { collectTreeseedToolStatus, formatTreeseedDependencyFailureDetails, installTreeseedDependencies } from "../managed-dependencies.js";
|
|
27
27
|
import { ControlPlaneClient } from "../control-plane-client.js";
|
|
28
28
|
import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
|
|
29
29
|
import {
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
assertFeatureBranch,
|
|
47
47
|
branchExists,
|
|
48
48
|
checkoutBranch,
|
|
49
|
+
checkoutDetachedOriginBranch,
|
|
49
50
|
checkoutTaskBranchFromStaging,
|
|
50
51
|
createDeprecatedTaskTag,
|
|
51
52
|
deleteLocalBranch,
|
|
@@ -59,14 +60,13 @@ import {
|
|
|
59
60
|
prepareReleaseBranches,
|
|
60
61
|
PRODUCTION_BRANCH,
|
|
61
62
|
pushBranch,
|
|
63
|
+
pushHeadToBranch,
|
|
62
64
|
remoteBranchExists,
|
|
63
65
|
STAGING_BRANCH,
|
|
64
66
|
squashMergeBranchIntoStaging,
|
|
65
|
-
syncBranchWithOrigin
|
|
66
|
-
waitForStagingAutomation
|
|
67
|
+
syncBranchWithOrigin
|
|
67
68
|
} from "../operations/services/git-workflow.js";
|
|
68
69
|
import { getGitHubAutomationMode, resolveGitHubRepositorySlug, waitForGitHubWorkflowCompletion } from "../operations/services/github-automation.js";
|
|
69
|
-
import { createGitHubApiClient } from "../operations/services/github-api.js";
|
|
70
70
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
|
|
71
71
|
import { runTenantDeployPreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
|
|
72
72
|
import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
|
|
@@ -111,8 +111,13 @@ import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
|
111
111
|
import { createTreeseedReconcileRegistry, deriveTreeseedDesiredUnits, filterTreeseedDesiredUnitsByBootstrapSystems, planTreeseedReconciliation, resolveTreeseedBootstrapSelection, reconcileTreeseedTarget } from "../reconcile/index.js";
|
|
112
112
|
import {
|
|
113
113
|
acquireWorkflowLock,
|
|
114
|
+
archiveWorkflowRun,
|
|
115
|
+
cacheWorkflowGateResult,
|
|
116
|
+
classifyWorkflowRunJournal,
|
|
117
|
+
classifyWorkflowRunJournals,
|
|
114
118
|
createWorkflowRunJournal,
|
|
115
119
|
generateWorkflowRunId,
|
|
120
|
+
getCachedSuccessfulWorkflowGate,
|
|
116
121
|
inspectWorkflowLock,
|
|
117
122
|
listInterruptedWorkflowRuns,
|
|
118
123
|
listWorkflowRunJournals,
|
|
@@ -129,6 +134,14 @@ import {
|
|
|
129
134
|
classifyTreeseedBranchRole,
|
|
130
135
|
resolveTreeseedWorkflowPaths
|
|
131
136
|
} from "./policy.js";
|
|
137
|
+
import {
|
|
138
|
+
effectiveWorkflowWorktreeMode,
|
|
139
|
+
ensureManagedWorkflowWorktree,
|
|
140
|
+
isManagedWorkflowWorktree,
|
|
141
|
+
managedWorkflowWorktreeMetadata,
|
|
142
|
+
plannedManagedWorkflowWorktreePath,
|
|
143
|
+
removeManagedWorkflowWorktree
|
|
144
|
+
} from "./worktrees.js";
|
|
132
145
|
class TreeseedWorkflowError extends Error {
|
|
133
146
|
code;
|
|
134
147
|
operation;
|
|
@@ -161,8 +174,64 @@ function ensureWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
|
161
174
|
if (report.created.length > 0) {
|
|
162
175
|
helpers.write(`[workspace][link] Linked ${report.created.length} local workspace package paths.`);
|
|
163
176
|
}
|
|
177
|
+
ensureWorkflowWorkspacePackageArtifacts(root, helpers);
|
|
178
|
+
ensureWorkflowCommandBins(root, helpers);
|
|
164
179
|
return report;
|
|
165
180
|
}
|
|
181
|
+
function readPackageScript(root, packageDir, scriptName) {
|
|
182
|
+
try {
|
|
183
|
+
const packageJson = JSON.parse(readFileSync(resolve(root, packageDir, "package.json"), "utf8"));
|
|
184
|
+
const scripts = packageJson.scripts && typeof packageJson.scripts === "object" && !Array.isArray(packageJson.scripts) ? packageJson.scripts : null;
|
|
185
|
+
const script = scripts?.[scriptName];
|
|
186
|
+
return typeof script === "string" && script.trim() ? script : null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function ensureWorkflowWorkspacePackageArtifacts(root, helpers) {
|
|
192
|
+
const packages = [
|
|
193
|
+
{ name: "@treeseed/sdk", dir: "packages/sdk", artifacts: ["dist/workflow-support.js", "dist/plugin-default.js", "dist/platform/env.yaml"] },
|
|
194
|
+
{ name: "@treeseed/core", dir: "packages/core", artifacts: ["dist/api.js", "dist/plugin-default.js"] },
|
|
195
|
+
{ name: "@treeseed/cli", dir: "packages/cli", artifacts: ["dist/cli/main.js"] }
|
|
196
|
+
];
|
|
197
|
+
for (const entry of packages) {
|
|
198
|
+
const packageDir = resolve(root, entry.dir);
|
|
199
|
+
if (!existsSync(resolve(packageDir, "package.json"))) continue;
|
|
200
|
+
if (!readPackageScript(root, entry.dir, "build:dist")) continue;
|
|
201
|
+
const missing = entry.artifacts.filter((artifact) => !existsSync(resolve(packageDir, artifact)));
|
|
202
|
+
if (missing.length === 0) continue;
|
|
203
|
+
helpers.write(`[workspace][build] Building ${entry.name} artifacts for local workspace links.`);
|
|
204
|
+
run("npm", ["--prefix", packageDir, "run", "build:dist"], { cwd: root });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function ensureWorkflowCommandBins(root, helpers) {
|
|
208
|
+
const cliBin = resolve(root, "node_modules/@treeseed/cli/dist/cli/main.js");
|
|
209
|
+
if (!existsSync(cliBin)) return;
|
|
210
|
+
const binDir = resolve(root, "node_modules/.bin");
|
|
211
|
+
mkdirSync(binDir, { recursive: true });
|
|
212
|
+
for (const name of ["trsd", "treeseed"]) {
|
|
213
|
+
const linkPath = resolve(binDir, name);
|
|
214
|
+
const target = relative(dirname(linkPath), cliBin) || cliBin;
|
|
215
|
+
try {
|
|
216
|
+
const stat = lstatSync(linkPath);
|
|
217
|
+
if (stat.isSymbolicLink()) {
|
|
218
|
+
const currentTarget = readlinkSync(linkPath);
|
|
219
|
+
if (currentTarget === target || resolve(dirname(linkPath), currentTarget) === cliBin) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
rmSync(linkPath, { force: true });
|
|
223
|
+
} else {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error.code !== "ENOENT") {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
symlinkSync(target, linkPath);
|
|
232
|
+
helpers.write(`[workspace][link] Linked ${name} command shim.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
166
235
|
function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
167
236
|
if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
|
|
168
237
|
return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
|
|
@@ -173,6 +242,163 @@ function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
|
173
242
|
}
|
|
174
243
|
return report;
|
|
175
244
|
}
|
|
245
|
+
function normalizeCiMode(mode, operation) {
|
|
246
|
+
if (mode === "hosted" || mode === "off") return mode;
|
|
247
|
+
return operation === "save" ? "off" : "hosted";
|
|
248
|
+
}
|
|
249
|
+
function normalizeSaveVerifyMode(mode) {
|
|
250
|
+
switch (mode) {
|
|
251
|
+
case "skip":
|
|
252
|
+
case "fast":
|
|
253
|
+
case void 0:
|
|
254
|
+
return "skip";
|
|
255
|
+
case "local":
|
|
256
|
+
case "local-only":
|
|
257
|
+
return "local-only";
|
|
258
|
+
case "hosted":
|
|
259
|
+
return "skip";
|
|
260
|
+
case "both":
|
|
261
|
+
case "action-first":
|
|
262
|
+
return "action-first";
|
|
263
|
+
default:
|
|
264
|
+
return "skip";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function shouldUseHostedSaveCi(input) {
|
|
268
|
+
return normalizeCiMode(input.ciMode, "save") === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both";
|
|
269
|
+
}
|
|
270
|
+
function worktreePayload(root, requestedMode) {
|
|
271
|
+
const metadata = managedWorkflowWorktreeMetadata(root);
|
|
272
|
+
return {
|
|
273
|
+
worktreeMode: requestedMode ?? "auto",
|
|
274
|
+
managedWorktree: metadata,
|
|
275
|
+
worktreePath: metadata?.worktreePath ?? null,
|
|
276
|
+
primaryRoot: metadata?.primaryRoot ?? null
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function helpersForCwd(helpers, cwd) {
|
|
280
|
+
return {
|
|
281
|
+
...helpers,
|
|
282
|
+
context: {
|
|
283
|
+
...helpers.context,
|
|
284
|
+
cwd
|
|
285
|
+
},
|
|
286
|
+
cwd: () => cwd
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function shouldDispatchSwitchToManagedWorktree(root, input, env) {
|
|
290
|
+
return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
|
|
291
|
+
}
|
|
292
|
+
function skippedWorkflowGate(gate, reason) {
|
|
293
|
+
return {
|
|
294
|
+
name: gate.name,
|
|
295
|
+
repository: gate.repository ?? null,
|
|
296
|
+
workflow: gate.workflow,
|
|
297
|
+
branch: gate.branch,
|
|
298
|
+
headSha: gate.headSha,
|
|
299
|
+
status: "skipped",
|
|
300
|
+
reason,
|
|
301
|
+
conclusion: null,
|
|
302
|
+
runId: null,
|
|
303
|
+
url: null
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function workflowGateFailureMessage(gate, result) {
|
|
307
|
+
const repository = String(result.repository ?? gate.repository ?? gate.name);
|
|
308
|
+
const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
|
|
309
|
+
const url = typeof result.url === "string" && result.url ? `
|
|
310
|
+
${result.url}` : "";
|
|
311
|
+
const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => stringRecord(job)?.name).filter((name) => typeof name === "string" && name.length > 0) : [];
|
|
312
|
+
const jobLine = failedJobs.length > 0 ? `
|
|
313
|
+
Failed jobs: ${failedJobs.join(", ")}` : "";
|
|
314
|
+
const command = runId ? `
|
|
315
|
+
Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
|
|
316
|
+
return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
|
|
317
|
+
}
|
|
318
|
+
function assertHostedGitHubWorkflowAuthReady(operation, root) {
|
|
319
|
+
if (getGitHubAutomationMode() === "stub") {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const tools = collectTreeseedToolStatus({
|
|
323
|
+
tenantRoot: root,
|
|
324
|
+
env: process.env
|
|
325
|
+
});
|
|
326
|
+
const github = tools.auth.github;
|
|
327
|
+
if (!github.authenticated) {
|
|
328
|
+
const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
|
|
329
|
+
workflowError(
|
|
330
|
+
operation,
|
|
331
|
+
"github_auth_unavailable",
|
|
332
|
+
[
|
|
333
|
+
"Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
|
|
334
|
+
github.detail,
|
|
335
|
+
`Managed gh check: ${command}`,
|
|
336
|
+
"Remediation:",
|
|
337
|
+
...github.remediation.map((item) => `- ${item}`)
|
|
338
|
+
].join("\n"),
|
|
339
|
+
{
|
|
340
|
+
details: {
|
|
341
|
+
toolsHome: tools.toolsHome,
|
|
342
|
+
ghConfigDir: tools.ghConfigDir,
|
|
343
|
+
github,
|
|
344
|
+
tools: tools.tools
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return tools;
|
|
350
|
+
}
|
|
351
|
+
async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
352
|
+
if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
353
|
+
return gates.map((gate) => skippedWorkflowGate(gate, ciMode === "off" ? "disabled" : "stubbed"));
|
|
354
|
+
}
|
|
355
|
+
if (gates.length === 0) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
assertHostedGitHubWorkflowAuthReady(operation, options.root ?? gates[0].repoPath);
|
|
359
|
+
const results = [];
|
|
360
|
+
for (const gate of gates) {
|
|
361
|
+
if (options.root && options.runId) {
|
|
362
|
+
const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
|
|
363
|
+
repository: gate.repository ?? null,
|
|
364
|
+
workflow: gate.workflow,
|
|
365
|
+
headSha: gate.headSha,
|
|
366
|
+
branch: gate.branch
|
|
367
|
+
});
|
|
368
|
+
if (cached) {
|
|
369
|
+
results.push({
|
|
370
|
+
...cached.result,
|
|
371
|
+
name: gate.name,
|
|
372
|
+
cached: true
|
|
373
|
+
});
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const result = await waitForGitHubWorkflowCompletion(gate.repoPath, {
|
|
378
|
+
repository: gate.repository,
|
|
379
|
+
workflow: gate.workflow,
|
|
380
|
+
headSha: gate.headSha,
|
|
381
|
+
branch: gate.branch
|
|
382
|
+
});
|
|
383
|
+
const normalized = {
|
|
384
|
+
name: gate.name,
|
|
385
|
+
...result,
|
|
386
|
+
workflow: String(result.workflow ?? gate.workflow),
|
|
387
|
+
branch: String(result.branch ?? gate.branch),
|
|
388
|
+
headSha: String(result.headSha ?? gate.headSha)
|
|
389
|
+
};
|
|
390
|
+
if (normalized.status === "completed" && normalized.conclusion !== "success") {
|
|
391
|
+
workflowError(operation, "github_workflow_failed", workflowGateFailureMessage(gate, normalized), {
|
|
392
|
+
details: { gate, workflow: normalized }
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
|
|
396
|
+
cacheWorkflowGateResult(options.root, options.runId, normalized);
|
|
397
|
+
}
|
|
398
|
+
results.push(normalized);
|
|
399
|
+
}
|
|
400
|
+
return results;
|
|
401
|
+
}
|
|
176
402
|
function ensureTreeseedCommandReadiness(root) {
|
|
177
403
|
if (getGitHubAutomationMode() === "stub") {
|
|
178
404
|
return {
|
|
@@ -295,7 +521,8 @@ function createRepoReport(name, path, branch, dirty) {
|
|
|
295
521
|
tagName: null,
|
|
296
522
|
commitSha: branch ? headCommit(path) : null,
|
|
297
523
|
skippedReason: null,
|
|
298
|
-
publishWait: null
|
|
524
|
+
publishWait: null,
|
|
525
|
+
workflowGates: []
|
|
299
526
|
};
|
|
300
527
|
}
|
|
301
528
|
function createWorkspaceRootRepoReport(root) {
|
|
@@ -712,6 +939,10 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
712
939
|
if (!branch) return null;
|
|
713
940
|
return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
|
|
714
941
|
}
|
|
942
|
+
function findAutoResumableTaskRun(root, command, branch) {
|
|
943
|
+
if (!branch) return null;
|
|
944
|
+
return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
|
|
945
|
+
}
|
|
715
946
|
function stringRecord(value) {
|
|
716
947
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
717
948
|
}
|
|
@@ -1033,7 +1264,7 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
|
|
|
1033
1264
|
if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
|
|
1034
1265
|
return;
|
|
1035
1266
|
}
|
|
1036
|
-
|
|
1267
|
+
assertHostedGitHubWorkflowAuthReady("release", root);
|
|
1037
1268
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1038
1269
|
if (!selectedPackageNames.has(pkg.name)) continue;
|
|
1039
1270
|
resolveGitHubRepositorySlug(pkg.dir);
|
|
@@ -1101,6 +1332,14 @@ function previewStateFor(tenantRoot, branchName) {
|
|
|
1101
1332
|
target: createBranchPreviewDeployTarget(branchName)
|
|
1102
1333
|
});
|
|
1103
1334
|
}
|
|
1335
|
+
function branchPreviewInitialized(tenantRoot, branchName) {
|
|
1336
|
+
if (!branchName) return false;
|
|
1337
|
+
try {
|
|
1338
|
+
return previewStateFor(tenantRoot, branchName).readiness?.initialized === true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1104
1343
|
async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
|
|
1105
1344
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
|
|
1106
1345
|
assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
|
|
@@ -1710,7 +1949,10 @@ async function workflowExport(helpers, input = {}) {
|
|
|
1710
1949
|
return await withContextEnv(helpers.context.env, async () => {
|
|
1711
1950
|
const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
|
|
1712
1951
|
const exported = await exportTreeseedCodebase({ directory });
|
|
1713
|
-
return buildWorkflowResult("export", exported.tenantRoot,
|
|
1952
|
+
return buildWorkflowResult("export", exported.tenantRoot, {
|
|
1953
|
+
...exported,
|
|
1954
|
+
...worktreePayload(exported.tenantRoot, input.worktreeMode)
|
|
1955
|
+
});
|
|
1714
1956
|
});
|
|
1715
1957
|
}
|
|
1716
1958
|
async function workflowSwitch(helpers, input) {
|
|
@@ -1725,6 +1967,27 @@ async function workflowSwitch(helpers, input) {
|
|
|
1725
1967
|
}
|
|
1726
1968
|
const preview = input.preview === true;
|
|
1727
1969
|
const executionMode = normalizeExecutionMode(input);
|
|
1970
|
+
if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
1971
|
+
const managed = ensureManagedWorkflowWorktree({
|
|
1972
|
+
root,
|
|
1973
|
+
branchName,
|
|
1974
|
+
mode: input.worktreeMode,
|
|
1975
|
+
env: helpers.context.env
|
|
1976
|
+
});
|
|
1977
|
+
const result = await workflowSwitch(helpersForCwd(helpers, managed.worktreePath), {
|
|
1978
|
+
...input,
|
|
1979
|
+
worktreeMode: "off"
|
|
1980
|
+
});
|
|
1981
|
+
return {
|
|
1982
|
+
...result,
|
|
1983
|
+
payload: {
|
|
1984
|
+
...result.payload,
|
|
1985
|
+
worktreeMode: input.worktreeMode ?? "auto",
|
|
1986
|
+
worktreePath: managed.worktreePath,
|
|
1987
|
+
managedWorktree: managed
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1728
1991
|
const mode = session.mode;
|
|
1729
1992
|
const repoDir = session.gitRoot;
|
|
1730
1993
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
@@ -1747,6 +2010,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
1747
2010
|
rootRepo,
|
|
1748
2011
|
repos: packageReports,
|
|
1749
2012
|
previewRequested: preview,
|
|
2013
|
+
worktreeMode: input.worktreeMode ?? "auto",
|
|
2014
|
+
worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
|
|
1750
2015
|
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
1751
2016
|
plannedSteps: [
|
|
1752
2017
|
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
@@ -1772,7 +2037,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1772
2037
|
const workflowRun = acquireWorkflowRun(
|
|
1773
2038
|
"switch",
|
|
1774
2039
|
session,
|
|
1775
|
-
{ branch: branchName, preview },
|
|
2040
|
+
{ branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
|
|
1776
2041
|
[
|
|
1777
2042
|
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
1778
2043
|
...packageReports.map((report) => ({
|
|
@@ -1849,6 +2114,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1849
2114
|
},
|
|
1850
2115
|
previewResult,
|
|
1851
2116
|
workspaceLinks,
|
|
2117
|
+
...worktreePayload(root, input.worktreeMode),
|
|
1852
2118
|
preconditions: {
|
|
1853
2119
|
cleanWorktreeRequired: true,
|
|
1854
2120
|
baseBranch: STAGING_BRANCH
|
|
@@ -1975,6 +2241,7 @@ async function workflowSave(helpers, input) {
|
|
|
1975
2241
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
1976
2242
|
const message = String(effectiveInput.message ?? "").trim();
|
|
1977
2243
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
2244
|
+
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
1978
2245
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
1979
2246
|
if (!branch) {
|
|
1980
2247
|
workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
|
|
@@ -2002,7 +2269,7 @@ async function workflowSave(helpers, input) {
|
|
|
2002
2269
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2003
2270
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2004
2271
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2005
|
-
verifyMode:
|
|
2272
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2006
2273
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
|
|
2007
2274
|
});
|
|
2008
2275
|
const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
|
|
@@ -2024,6 +2291,9 @@ async function workflowSave(helpers, input) {
|
|
|
2024
2291
|
failure: planAutoResumeRun.failure
|
|
2025
2292
|
} : null,
|
|
2026
2293
|
workspaceLinks,
|
|
2294
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2295
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2296
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2027
2297
|
repositoryPlan,
|
|
2028
2298
|
waves: repositoryPlan.waves,
|
|
2029
2299
|
plannedVersions: repositoryPlan.plannedVersions,
|
|
@@ -2032,7 +2302,7 @@ async function workflowSave(helpers, input) {
|
|
|
2032
2302
|
...repositoryPlan.plannedSteps,
|
|
2033
2303
|
{ id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
|
|
2034
2304
|
{ id: "workspace-link", description: "Restore local workspace links after save" },
|
|
2035
|
-
...beforeState.branchRole === "feature" && (effectiveInput.preview === true ||
|
|
2305
|
+
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
2036
2306
|
]
|
|
2037
2307
|
},
|
|
2038
2308
|
{
|
|
@@ -2065,7 +2335,9 @@ async function workflowSave(helpers, input) {
|
|
|
2065
2335
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2066
2336
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2067
2337
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2068
|
-
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "
|
|
2338
|
+
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
|
|
2339
|
+
ciMode: effectiveInput.ciMode ?? "auto",
|
|
2340
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2069
2341
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2070
2342
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2071
2343
|
},
|
|
@@ -2078,7 +2350,15 @@ async function workflowSave(helpers, input) {
|
|
|
2078
2350
|
branch,
|
|
2079
2351
|
resumable: true
|
|
2080
2352
|
},
|
|
2081
|
-
...
|
|
2353
|
+
...shouldUseHostedSaveCi(effectiveInput) ? [{
|
|
2354
|
+
id: "hosted-ci",
|
|
2355
|
+
description: `Wait for hosted save workflows on ${branch}`,
|
|
2356
|
+
repoName: rootRepo.name,
|
|
2357
|
+
repoPath: rootRepo.path,
|
|
2358
|
+
branch,
|
|
2359
|
+
resumable: true
|
|
2360
|
+
}] : [],
|
|
2361
|
+
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && previewInitialized) ? [{
|
|
2082
2362
|
id: "preview",
|
|
2083
2363
|
description: `Refresh preview ${branch}`,
|
|
2084
2364
|
repoName: rootRepo.name,
|
|
@@ -2112,7 +2392,7 @@ async function workflowSave(helpers, input) {
|
|
|
2112
2392
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2113
2393
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2114
2394
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2115
|
-
verifyMode:
|
|
2395
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2116
2396
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2117
2397
|
workflowRunId: workflowRun.runId,
|
|
2118
2398
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
@@ -2139,14 +2419,30 @@ async function workflowSave(helpers, input) {
|
|
|
2139
2419
|
lockfileValidation: repo.lockfileValidation
|
|
2140
2420
|
}))
|
|
2141
2421
|
};
|
|
2422
|
+
const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates("save", [
|
|
2423
|
+
...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
|
|
2424
|
+
name: savedRootRepo.name,
|
|
2425
|
+
repoPath: savedRootRepo.path,
|
|
2426
|
+
workflow: "verify.yml",
|
|
2427
|
+
branch,
|
|
2428
|
+
headSha: savedRootRepo.commitSha
|
|
2429
|
+
}] : [],
|
|
2430
|
+
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
|
|
2431
|
+
name: repo.name,
|
|
2432
|
+
repoPath: repo.path,
|
|
2433
|
+
workflow: "verify.yml",
|
|
2434
|
+
branch: String(repo.branch),
|
|
2435
|
+
headSha: String(repo.commitSha)
|
|
2436
|
+
}))
|
|
2437
|
+
], "hosted", { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates }))) : { workflowGates: [] };
|
|
2142
2438
|
let previewAction = { status: "skipped" };
|
|
2143
2439
|
if (beforeState.branchRole === "feature" && branch) {
|
|
2144
2440
|
if (effectiveInput.preview === true) {
|
|
2145
2441
|
previewAction = {
|
|
2146
|
-
status:
|
|
2147
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !
|
|
2442
|
+
status: previewInitialized ? "refreshed" : "created",
|
|
2443
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
|
|
2148
2444
|
};
|
|
2149
|
-
} else if (effectiveInput.refreshPreview !== false &&
|
|
2445
|
+
} else if (effectiveInput.refreshPreview !== false && previewInitialized) {
|
|
2150
2446
|
previewAction = {
|
|
2151
2447
|
status: "refreshed",
|
|
2152
2448
|
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
@@ -2175,7 +2471,11 @@ async function workflowSave(helpers, input) {
|
|
|
2175
2471
|
mergeConflict: null,
|
|
2176
2472
|
workspaceLinks,
|
|
2177
2473
|
commandReadiness,
|
|
2178
|
-
lockfileValidation
|
|
2474
|
+
lockfileValidation,
|
|
2475
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2476
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2477
|
+
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
2478
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2179
2479
|
};
|
|
2180
2480
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2181
2481
|
return buildWorkflowResult(
|
|
@@ -2232,9 +2532,13 @@ async function workflowClose(helpers, input) {
|
|
|
2232
2532
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2233
2533
|
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
2234
2534
|
const root = workspaceRoot(tenantRoot);
|
|
2235
|
-
const message = ensureMessage("close", input.message, "a close reason");
|
|
2236
2535
|
const executionMode = normalizeExecutionMode(input);
|
|
2237
2536
|
const session = resolveTreeseedWorkflowSession(root);
|
|
2537
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
2538
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
|
|
2539
|
+
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
|
|
2540
|
+
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2541
|
+
const message = ensureMessage("close", effectiveInput.message, "a close reason");
|
|
2238
2542
|
if (executionMode === "plan") {
|
|
2239
2543
|
const branchName = session.branchName;
|
|
2240
2544
|
const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
|
|
@@ -2245,18 +2549,26 @@ async function workflowClose(helpers, input) {
|
|
|
2245
2549
|
mode: session.mode,
|
|
2246
2550
|
branchName,
|
|
2247
2551
|
message,
|
|
2552
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
2553
|
+
runId: planAutoResumeRun.runId,
|
|
2554
|
+
branch: planAutoResumeRun.session.branchName,
|
|
2555
|
+
failure: planAutoResumeRun.failure
|
|
2556
|
+
} : null,
|
|
2557
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2248
2558
|
autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
|
|
2249
2559
|
repos: createWorkspacePackageReports(root),
|
|
2250
2560
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
2251
2561
|
blockers,
|
|
2252
2562
|
plannedSteps: [
|
|
2563
|
+
{ id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
|
|
2253
2564
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
2254
2565
|
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
2255
2566
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
2256
2567
|
id: `cleanup-${pkg.name}`,
|
|
2257
2568
|
description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
|
|
2258
2569
|
})),
|
|
2259
|
-
{ id: "workspace-link", description: "Restore local workspace links on the final branch" }
|
|
2570
|
+
{ id: "workspace-link", description: "Restore local workspace links on the final branch" },
|
|
2571
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
|
|
2260
2572
|
]
|
|
2261
2573
|
},
|
|
2262
2574
|
{
|
|
@@ -2267,12 +2579,10 @@ async function workflowClose(helpers, input) {
|
|
|
2267
2579
|
}
|
|
2268
2580
|
);
|
|
2269
2581
|
}
|
|
2270
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2271
2582
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
2272
2583
|
message,
|
|
2273
|
-
autoSave:
|
|
2584
|
+
autoSave: effectiveInput.autoSave
|
|
2274
2585
|
});
|
|
2275
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2276
2586
|
const activeSession = resolveTreeseedWorkflowSession(root);
|
|
2277
2587
|
const featureBranch = assertFeatureBranch(root);
|
|
2278
2588
|
const mode = activeSession.mode;
|
|
@@ -2288,8 +2598,14 @@ async function workflowClose(helpers, input) {
|
|
|
2288
2598
|
const workflowRun = acquireWorkflowRun(
|
|
2289
2599
|
"close",
|
|
2290
2600
|
activeSession,
|
|
2291
|
-
{
|
|
2601
|
+
{
|
|
2602
|
+
message,
|
|
2603
|
+
deletePreview: effectiveInput.deletePreview !== false,
|
|
2604
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2605
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto"
|
|
2606
|
+
},
|
|
2292
2607
|
[
|
|
2608
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2293
2609
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2294
2610
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2295
2611
|
...packageReports.map((report) => ({
|
|
@@ -2299,23 +2615,35 @@ async function workflowClose(helpers, input) {
|
|
|
2299
2615
|
repoPath: report.path,
|
|
2300
2616
|
branch: featureBranch,
|
|
2301
2617
|
resumable: true
|
|
2302
|
-
}))
|
|
2618
|
+
})),
|
|
2619
|
+
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2620
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
2303
2621
|
],
|
|
2304
|
-
|
|
2622
|
+
autoResumeRun ? {
|
|
2623
|
+
...helpers.context,
|
|
2624
|
+
workflow: {
|
|
2625
|
+
...helpers.context.workflow ?? {},
|
|
2626
|
+
resumeRunId: autoResumeRun.runId
|
|
2627
|
+
}
|
|
2628
|
+
} : helpers.context
|
|
2305
2629
|
);
|
|
2630
|
+
if (autoResumeRun) {
|
|
2631
|
+
helpers.write(`[workflow][resume] Resuming interrupted close ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2632
|
+
}
|
|
2306
2633
|
try {
|
|
2307
|
-
|
|
2634
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2635
|
+
const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
|
|
2308
2636
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2309
2637
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
2310
|
-
const deletedRemote =
|
|
2638
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2311
2639
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2312
|
-
if (
|
|
2640
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2313
2641
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2314
2642
|
}
|
|
2315
2643
|
return {
|
|
2316
2644
|
deprecatedTag,
|
|
2317
2645
|
deletedRemote,
|
|
2318
|
-
deletedLocal:
|
|
2646
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2319
2647
|
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
2320
2648
|
dirty: hasMeaningfulChanges(repoDir)
|
|
2321
2649
|
};
|
|
@@ -2332,12 +2660,15 @@ async function workflowClose(helpers, input) {
|
|
|
2332
2660
|
continue;
|
|
2333
2661
|
}
|
|
2334
2662
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
|
|
2335
|
-
deleteBranch:
|
|
2663
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2336
2664
|
targetBranch: STAGING_BRANCH
|
|
2337
2665
|
}));
|
|
2338
2666
|
Object.assign(report, cleanup);
|
|
2339
2667
|
}
|
|
2340
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2668
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
2669
|
+
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
2670
|
+
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
2671
|
+
const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
|
|
2341
2672
|
const payload = {
|
|
2342
2673
|
mode,
|
|
2343
2674
|
branchName: featureBranch,
|
|
@@ -2350,8 +2681,13 @@ async function workflowClose(helpers, input) {
|
|
|
2350
2681
|
previewCleanup,
|
|
2351
2682
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2352
2683
|
localDeleted: rootRepo.deletedLocal,
|
|
2353
|
-
finalBranch
|
|
2354
|
-
workspaceLinks
|
|
2684
|
+
finalBranch,
|
|
2685
|
+
workspaceLinks,
|
|
2686
|
+
worktreeCleanup,
|
|
2687
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2688
|
+
managedWorktree,
|
|
2689
|
+
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
2690
|
+
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
2355
2691
|
};
|
|
2356
2692
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2357
2693
|
return buildWorkflowResult(
|
|
@@ -2366,7 +2702,7 @@ async function workflowClose(helpers, input) {
|
|
|
2366
2702
|
}
|
|
2367
2703
|
);
|
|
2368
2704
|
} catch (error) {
|
|
2369
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2705
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2370
2706
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2371
2707
|
resumable: true,
|
|
2372
2708
|
runId: workflowRun.runId,
|
|
@@ -2387,11 +2723,19 @@ async function workflowStage(helpers, input) {
|
|
|
2387
2723
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2388
2724
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
2389
2725
|
const root = workspaceRoot(tenantRoot);
|
|
2390
|
-
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
2391
2726
|
const executionMode = normalizeExecutionMode(input);
|
|
2392
2727
|
const initialSession = resolveTreeseedWorkflowSession(root);
|
|
2728
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
2729
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
|
|
2730
|
+
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
|
|
2731
|
+
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2732
|
+
const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
|
|
2733
|
+
const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
|
|
2393
2734
|
if (executionMode === "plan") {
|
|
2394
2735
|
const blockers = [];
|
|
2736
|
+
if (initialSession.branchRole !== "feature") {
|
|
2737
|
+
blockers.push("Stage only applies to task branches.");
|
|
2738
|
+
}
|
|
2395
2739
|
try {
|
|
2396
2740
|
validateStagingWorkflowContracts(root);
|
|
2397
2741
|
} catch (error) {
|
|
@@ -2406,6 +2750,13 @@ async function workflowStage(helpers, input) {
|
|
|
2406
2750
|
mergeTarget: STAGING_BRANCH,
|
|
2407
2751
|
mergeStrategy: "squash",
|
|
2408
2752
|
message,
|
|
2753
|
+
ciMode,
|
|
2754
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
2755
|
+
runId: planAutoResumeRun.runId,
|
|
2756
|
+
branch: planAutoResumeRun.session.branchName,
|
|
2757
|
+
failure: planAutoResumeRun.failure
|
|
2758
|
+
} : null,
|
|
2759
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2409
2760
|
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
2410
2761
|
blockers,
|
|
2411
2762
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
@@ -2418,7 +2769,7 @@ async function workflowStage(helpers, input) {
|
|
|
2418
2769
|
{ id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
|
|
2419
2770
|
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
2420
2771
|
{ id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
|
|
2421
|
-
{ id: "wait-staging", description: "Wait for staging
|
|
2772
|
+
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
|
|
2422
2773
|
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
2423
2774
|
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
2424
2775
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
@@ -2436,12 +2787,10 @@ async function workflowStage(helpers, input) {
|
|
|
2436
2787
|
}
|
|
2437
2788
|
);
|
|
2438
2789
|
}
|
|
2439
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2440
2790
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
2441
2791
|
message,
|
|
2442
|
-
autoSave:
|
|
2792
|
+
autoSave: effectiveInput.autoSave
|
|
2443
2793
|
});
|
|
2444
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2445
2794
|
const session = resolveTreeseedWorkflowSession(root);
|
|
2446
2795
|
const featureBranch = assertFeatureBranch(root);
|
|
2447
2796
|
const mode = session.mode;
|
|
@@ -2451,6 +2800,8 @@ async function workflowStage(helpers, input) {
|
|
|
2451
2800
|
} else {
|
|
2452
2801
|
assertCleanWorktree(root);
|
|
2453
2802
|
}
|
|
2803
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2804
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
2454
2805
|
validateStagingWorkflowContracts(root);
|
|
2455
2806
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2456
2807
|
const repoDir = session.gitRoot;
|
|
@@ -2459,8 +2810,16 @@ async function workflowStage(helpers, input) {
|
|
|
2459
2810
|
const workflowRun = acquireWorkflowRun(
|
|
2460
2811
|
"stage",
|
|
2461
2812
|
session,
|
|
2462
|
-
{
|
|
2813
|
+
{
|
|
2814
|
+
message,
|
|
2815
|
+
waitForStaging: effectiveInput.waitForStaging !== false,
|
|
2816
|
+
deletePreview: effectiveInput.deletePreview !== false,
|
|
2817
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2818
|
+
ciMode,
|
|
2819
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto"
|
|
2820
|
+
},
|
|
2463
2821
|
[
|
|
2822
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2464
2823
|
...packageReports.map((report) => ({
|
|
2465
2824
|
id: `merge-${report.name}`,
|
|
2466
2825
|
description: `Merge ${featureBranch} into ${report.name} staging`,
|
|
@@ -2470,7 +2829,7 @@ async function workflowStage(helpers, input) {
|
|
|
2470
2829
|
resumable: true
|
|
2471
2830
|
})),
|
|
2472
2831
|
{ id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2473
|
-
{ id: "wait-staging", description: "Wait for staging
|
|
2832
|
+
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2474
2833
|
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2475
2834
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2476
2835
|
...packageReports.map((report) => ({
|
|
@@ -2480,12 +2839,23 @@ async function workflowStage(helpers, input) {
|
|
|
2480
2839
|
repoPath: report.path,
|
|
2481
2840
|
branch: featureBranch,
|
|
2482
2841
|
resumable: true
|
|
2483
|
-
}))
|
|
2842
|
+
})),
|
|
2843
|
+
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2844
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
2484
2845
|
],
|
|
2485
|
-
|
|
2846
|
+
autoResumeRun ? {
|
|
2847
|
+
...helpers.context,
|
|
2848
|
+
workflow: {
|
|
2849
|
+
...helpers.context.workflow ?? {},
|
|
2850
|
+
resumeRunId: autoResumeRun.runId
|
|
2851
|
+
}
|
|
2852
|
+
} : helpers.context
|
|
2486
2853
|
);
|
|
2854
|
+
if (autoResumeRun) {
|
|
2855
|
+
helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2856
|
+
}
|
|
2487
2857
|
try {
|
|
2488
|
-
unlinkWorkflowWorkspaceLinks(root, helpers,
|
|
2858
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2489
2859
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2490
2860
|
const report = findReportByName(packageReports, pkg.name);
|
|
2491
2861
|
if (!report) {
|
|
@@ -2517,7 +2887,11 @@ async function workflowStage(helpers, input) {
|
|
|
2517
2887
|
try {
|
|
2518
2888
|
rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
|
|
2519
2889
|
assertCleanWorktree(root);
|
|
2520
|
-
|
|
2890
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2891
|
+
checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
|
|
2892
|
+
} else {
|
|
2893
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2894
|
+
}
|
|
2521
2895
|
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
2522
2896
|
if (mode === "recursive-workspace") {
|
|
2523
2897
|
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
@@ -2532,10 +2906,14 @@ async function workflowStage(helpers, input) {
|
|
|
2532
2906
|
run("git", ["add", "-A"], { cwd: repoDir });
|
|
2533
2907
|
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
2534
2908
|
}
|
|
2535
|
-
|
|
2909
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2910
|
+
pushHeadToBranch(repoDir, STAGING_BRANCH);
|
|
2911
|
+
} else {
|
|
2912
|
+
pushBranch(repoDir, STAGING_BRANCH);
|
|
2913
|
+
}
|
|
2536
2914
|
return {
|
|
2537
2915
|
commitSha: headCommit(repoDir),
|
|
2538
|
-
branch:
|
|
2916
|
+
branch: STAGING_BRANCH,
|
|
2539
2917
|
committed: hasMeaningfulChanges(repoDir) ? false : true,
|
|
2540
2918
|
lockfileValidation: lockfileSafety.lockfileValidation,
|
|
2541
2919
|
lockfileInstall: lockfileSafety.install
|
|
@@ -2553,18 +2931,47 @@ async function workflowStage(helpers, input) {
|
|
|
2553
2931
|
exitCode: 12
|
|
2554
2932
|
});
|
|
2555
2933
|
}
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2934
|
+
const stageWorkflowGateResult = effectiveInput.waitForStaging === false ? (skipJournalStep(root, workflowRun.runId, "wait-staging", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "wait-staging", () => waitForWorkflowGates("stage", [
|
|
2935
|
+
{
|
|
2936
|
+
name: rootRepo.name,
|
|
2937
|
+
repoPath: rootRepo.path,
|
|
2938
|
+
workflow: "verify.yml",
|
|
2939
|
+
branch: STAGING_BRANCH,
|
|
2940
|
+
headSha: rootRepo.commitSha
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
name: rootRepo.name,
|
|
2944
|
+
repoPath: rootRepo.path,
|
|
2945
|
+
workflow: "deploy.yml",
|
|
2946
|
+
branch: STAGING_BRANCH,
|
|
2947
|
+
headSha: rootRepo.commitSha
|
|
2948
|
+
},
|
|
2949
|
+
...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
|
|
2950
|
+
name: report.name,
|
|
2951
|
+
repoPath: report.path,
|
|
2952
|
+
workflow: "verify.yml",
|
|
2953
|
+
branch: STAGING_BRANCH,
|
|
2954
|
+
headSha: String(report.commitSha)
|
|
2955
|
+
}))
|
|
2956
|
+
], ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({
|
|
2957
|
+
status: "completed",
|
|
2958
|
+
workflowGates
|
|
2959
|
+
})));
|
|
2960
|
+
const stagingWait = {
|
|
2961
|
+
status: String(stageWorkflowGateResult?.status ?? "completed"),
|
|
2962
|
+
workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
|
|
2963
|
+
};
|
|
2964
|
+
const previewCleanup = effectiveInput.deletePreview === false ? (skipJournalStep(root, workflowRun.runId, "preview-cleanup", { performed: false }), { performed: false }) : await executeJournalStep(root, workflowRun.runId, "preview-cleanup", () => destroyPreviewIfPresent(root, featureBranch));
|
|
2558
2965
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2559
2966
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
2560
|
-
const deletedRemote =
|
|
2561
|
-
if (
|
|
2967
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2968
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2562
2969
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2563
2970
|
}
|
|
2564
2971
|
return {
|
|
2565
2972
|
deprecatedTag,
|
|
2566
2973
|
deletedRemote,
|
|
2567
|
-
deletedLocal:
|
|
2974
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2568
2975
|
branch: currentBranch(repoDir) || STAGING_BRANCH
|
|
2569
2976
|
};
|
|
2570
2977
|
});
|
|
@@ -2579,12 +2986,15 @@ async function workflowStage(helpers, input) {
|
|
|
2579
2986
|
continue;
|
|
2580
2987
|
}
|
|
2581
2988
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
2582
|
-
deleteBranch:
|
|
2989
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2583
2990
|
targetBranch: STAGING_BRANCH
|
|
2584
2991
|
}));
|
|
2585
2992
|
Object.assign(report, cleanup);
|
|
2586
2993
|
}
|
|
2587
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2994
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
2995
|
+
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
2996
|
+
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
2997
|
+
const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
|
|
2588
2998
|
const payload = {
|
|
2589
2999
|
mode,
|
|
2590
3000
|
branchName: featureBranch,
|
|
@@ -2602,8 +3012,15 @@ async function workflowStage(helpers, input) {
|
|
|
2602
3012
|
lockfileInstall: rootMerge?.lockfileInstall ?? null,
|
|
2603
3013
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2604
3014
|
localDeleted: rootRepo.deletedLocal,
|
|
2605
|
-
finalBranch
|
|
2606
|
-
workspaceLinks
|
|
3015
|
+
finalBranch,
|
|
3016
|
+
workspaceLinks,
|
|
3017
|
+
ciMode,
|
|
3018
|
+
workflowGates: stagingWait.workflowGates,
|
|
3019
|
+
worktreeCleanup,
|
|
3020
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3021
|
+
managedWorktree,
|
|
3022
|
+
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
3023
|
+
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
2607
3024
|
};
|
|
2608
3025
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2609
3026
|
return buildWorkflowResult(
|
|
@@ -2619,7 +3036,7 @@ async function workflowStage(helpers, input) {
|
|
|
2619
3036
|
}
|
|
2620
3037
|
);
|
|
2621
3038
|
} catch (error) {
|
|
2622
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
3039
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2623
3040
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2624
3041
|
resumable: true,
|
|
2625
3042
|
runId: workflowRun.runId,
|
|
@@ -2650,6 +3067,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2650
3067
|
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
2651
3068
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2652
3069
|
const level = effectiveInput.bump ?? "patch";
|
|
3070
|
+
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
2653
3071
|
const isResume = Boolean(explicitResumeRunId || autoResumeRun);
|
|
2654
3072
|
const packageSelection = session.packageSelection;
|
|
2655
3073
|
const selectedPackageNames = new Set(packageSelection.selected);
|
|
@@ -2666,6 +3084,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2666
3084
|
if (executionMode === "plan") {
|
|
2667
3085
|
return buildWorkflowResult("release", root, {
|
|
2668
3086
|
...plannedRelease,
|
|
3087
|
+
ciMode,
|
|
3088
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2669
3089
|
autoResumeCandidate: planAutoResumeRun ? {
|
|
2670
3090
|
runId: planAutoResumeRun.runId,
|
|
2671
3091
|
branch: planAutoResumeRun.session.branchName,
|
|
@@ -2691,6 +3111,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2691
3111
|
devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
|
|
2692
3112
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2693
3113
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3114
|
+
ciMode,
|
|
3115
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2694
3116
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2695
3117
|
},
|
|
2696
3118
|
[
|
|
@@ -2706,6 +3128,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2706
3128
|
resumable: true
|
|
2707
3129
|
})),
|
|
2708
3130
|
{ id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3131
|
+
{ id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
2709
3132
|
...mode === "recursive-workspace" ? [{ id: "cleanup-dev-tags", description: "Clean replaced dev package tags", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true }] : []
|
|
2710
3133
|
],
|
|
2711
3134
|
autoResumeRun ? {
|
|
@@ -2732,19 +3155,22 @@ async function workflowRelease(helpers, input) {
|
|
|
2732
3155
|
assertCleanWorktree(root);
|
|
2733
3156
|
}
|
|
2734
3157
|
prepareReleaseBranches(root);
|
|
3158
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2735
3159
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2736
|
-
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
3160
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2737
3161
|
if (mode === "root-only") {
|
|
2738
3162
|
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
2739
3163
|
setRootPackageJsonVersion(root, rootVersion);
|
|
2740
3164
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2741
3165
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2742
3166
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3167
|
+
const stagingCommit = headCommit(gitRoot);
|
|
2743
3168
|
const released = mergeStagingIntoMain(root);
|
|
2744
3169
|
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
|
|
2745
3170
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2746
3171
|
return {
|
|
2747
3172
|
rootVersion,
|
|
3173
|
+
stagingCommit,
|
|
2748
3174
|
releasedCommit: released.commitSha,
|
|
2749
3175
|
tag
|
|
2750
3176
|
};
|
|
@@ -2755,6 +3181,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2755
3181
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2756
3182
|
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
2757
3183
|
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
3184
|
+
const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3185
|
+
{
|
|
3186
|
+
name: rootRepo.name,
|
|
3187
|
+
repoPath: rootRepo.path,
|
|
3188
|
+
workflow: "verify.yml",
|
|
3189
|
+
branch: STAGING_BRANCH,
|
|
3190
|
+
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
name: rootRepo.name,
|
|
3194
|
+
repoPath: rootRepo.path,
|
|
3195
|
+
workflow: "deploy.yml",
|
|
3196
|
+
branch: STAGING_BRANCH,
|
|
3197
|
+
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
name: rootRepo.name,
|
|
3201
|
+
repoPath: rootRepo.path,
|
|
3202
|
+
workflow: "verify.yml",
|
|
3203
|
+
branch: rootVersion,
|
|
3204
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
name: rootRepo.name,
|
|
3208
|
+
repoPath: rootRepo.path,
|
|
3209
|
+
workflow: "verify.yml",
|
|
3210
|
+
branch: PRODUCTION_BRANCH,
|
|
3211
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
name: rootRepo.name,
|
|
3215
|
+
repoPath: rootRepo.path,
|
|
3216
|
+
workflow: "deploy.yml",
|
|
3217
|
+
branch: PRODUCTION_BRANCH,
|
|
3218
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3219
|
+
}
|
|
3220
|
+
].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
|
|
2758
3221
|
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2759
3222
|
const payload2 = {
|
|
2760
3223
|
mode,
|
|
@@ -2775,7 +3238,10 @@ async function workflowRelease(helpers, input) {
|
|
|
2775
3238
|
rootRepo,
|
|
2776
3239
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
2777
3240
|
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
2778
|
-
workspaceLinks: workspaceLinks2
|
|
3241
|
+
workspaceLinks: workspaceLinks2,
|
|
3242
|
+
ciMode,
|
|
3243
|
+
workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
|
|
3244
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2779
3245
|
};
|
|
2780
3246
|
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
2781
3247
|
return buildWorkflowResult("release", root, payload2, {
|
|
@@ -2843,18 +3309,38 @@ async function workflowRelease(helpers, input) {
|
|
|
2843
3309
|
});
|
|
2844
3310
|
const tagName = String(effectiveVersions.get(pkg.name));
|
|
2845
3311
|
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3312
|
+
const workflowGates = await waitForWorkflowGates("release", [
|
|
3313
|
+
{
|
|
3314
|
+
name: pkg.name,
|
|
3315
|
+
repoPath: pkg.dir,
|
|
3316
|
+
workflow: "publish.yml",
|
|
3317
|
+
headSha: mergeResult.commitSha,
|
|
3318
|
+
branch: tagName
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
name: pkg.name,
|
|
3322
|
+
repoPath: pkg.dir,
|
|
3323
|
+
workflow: "verify.yml",
|
|
3324
|
+
headSha: mergeResult.commitSha,
|
|
3325
|
+
branch: tagName
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
name: pkg.name,
|
|
3329
|
+
repoPath: pkg.dir,
|
|
3330
|
+
workflow: "verify.yml",
|
|
3331
|
+
headSha: mergeResult.commitSha,
|
|
3332
|
+
branch: PRODUCTION_BRANCH
|
|
3333
|
+
}
|
|
3334
|
+
], ciMode, { root, runId: workflowRun.runId });
|
|
3335
|
+
const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
|
|
2851
3336
|
assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
|
|
2852
3337
|
syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
|
|
2853
3338
|
return {
|
|
2854
3339
|
commitSha: mergeResult.commitSha,
|
|
2855
3340
|
tagName,
|
|
2856
3341
|
tag,
|
|
2857
|
-
publish
|
|
3342
|
+
publish,
|
|
3343
|
+
workflowGates
|
|
2858
3344
|
};
|
|
2859
3345
|
});
|
|
2860
3346
|
report.committed = true;
|
|
@@ -2863,6 +3349,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2863
3349
|
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
2864
3350
|
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
2865
3351
|
report.publishWait = releasedPackage?.publish ?? null;
|
|
3352
|
+
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
2866
3353
|
report.branch = STAGING_BRANCH;
|
|
2867
3354
|
publishWait.push({
|
|
2868
3355
|
name: report.name,
|
|
@@ -2875,6 +3362,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2875
3362
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2876
3363
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2877
3364
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3365
|
+
const stagingCommit = headCommit(gitRoot);
|
|
2878
3366
|
const released = mergeBranchIntoTarget(root, {
|
|
2879
3367
|
sourceBranch: STAGING_BRANCH,
|
|
2880
3368
|
targetBranch: PRODUCTION_BRANCH,
|
|
@@ -2894,6 +3382,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2894
3382
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2895
3383
|
return {
|
|
2896
3384
|
rootVersion,
|
|
3385
|
+
stagingCommit,
|
|
2897
3386
|
releasedCommit,
|
|
2898
3387
|
mergeCommit: released.commitSha,
|
|
2899
3388
|
tag
|
|
@@ -2905,6 +3394,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2905
3394
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2906
3395
|
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
2907
3396
|
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
3397
|
+
const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3398
|
+
{
|
|
3399
|
+
name: rootRepo.name,
|
|
3400
|
+
repoPath: rootRepo.path,
|
|
3401
|
+
workflow: "verify.yml",
|
|
3402
|
+
branch: STAGING_BRANCH,
|
|
3403
|
+
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3404
|
+
},
|
|
3405
|
+
{
|
|
3406
|
+
name: rootRepo.name,
|
|
3407
|
+
repoPath: rootRepo.path,
|
|
3408
|
+
workflow: "deploy.yml",
|
|
3409
|
+
branch: STAGING_BRANCH,
|
|
3410
|
+
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3411
|
+
},
|
|
3412
|
+
{
|
|
3413
|
+
name: rootRepo.name,
|
|
3414
|
+
repoPath: rootRepo.path,
|
|
3415
|
+
workflow: "verify.yml",
|
|
3416
|
+
branch: rootVersion,
|
|
3417
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
name: rootRepo.name,
|
|
3421
|
+
repoPath: rootRepo.path,
|
|
3422
|
+
workflow: "verify.yml",
|
|
3423
|
+
branch: PRODUCTION_BRANCH,
|
|
3424
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
name: rootRepo.name,
|
|
3428
|
+
repoPath: rootRepo.path,
|
|
3429
|
+
workflow: "deploy.yml",
|
|
3430
|
+
branch: PRODUCTION_BRANCH,
|
|
3431
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3432
|
+
}
|
|
3433
|
+
].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
|
|
2908
3434
|
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
2909
3435
|
const devTagCleanup = devTagCleanupMode === "off" ? (skipJournalStep(root, workflowRun.runId, "cleanup-dev-tags", { status: "skipped", reason: "disabled" }), { status: "skipped", reason: "disabled" }) : await executeJournalStep(root, workflowRun.runId, "cleanup-dev-tags", () => {
|
|
2910
3436
|
const activeDevTags = collectActiveDevTagReferences(root);
|
|
@@ -2956,7 +3482,13 @@ async function workflowRelease(helpers, input) {
|
|
|
2956
3482
|
productionPushed: true,
|
|
2957
3483
|
tagPushed: true
|
|
2958
3484
|
},
|
|
2959
|
-
workspaceLinks
|
|
3485
|
+
workspaceLinks,
|
|
3486
|
+
ciMode,
|
|
3487
|
+
workflowGates: [
|
|
3488
|
+
...packageReports.flatMap((report) => report.workflowGates),
|
|
3489
|
+
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
3490
|
+
],
|
|
3491
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2960
3492
|
};
|
|
2961
3493
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2962
3494
|
return buildWorkflowResult("release", root, payload, {
|
|
@@ -3006,7 +3538,21 @@ async function workflowResume(helpers, input) {
|
|
|
3006
3538
|
details: { runId, status: journal.status }
|
|
3007
3539
|
});
|
|
3008
3540
|
}
|
|
3009
|
-
const
|
|
3541
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3542
|
+
const currentHeads = Object.fromEntries(
|
|
3543
|
+
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
3544
|
+
);
|
|
3545
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
3546
|
+
currentBranch: session.branchName,
|
|
3547
|
+
currentHeads
|
|
3548
|
+
});
|
|
3549
|
+
if (classification.state !== "resumable") {
|
|
3550
|
+
workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
|
|
3551
|
+
details: { runId, status: journal.status, classification }
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
const resumeRoot = typeof journal.session?.root === "string" && existsSync(journal.session.root) ? journal.session.root : root;
|
|
3555
|
+
const resumedHelpers = helpersForCwd({
|
|
3010
3556
|
...helpers,
|
|
3011
3557
|
context: {
|
|
3012
3558
|
...helpers.context,
|
|
@@ -3015,7 +3561,7 @@ async function workflowResume(helpers, input) {
|
|
|
3015
3561
|
resumeRunId: runId
|
|
3016
3562
|
}
|
|
3017
3563
|
}
|
|
3018
|
-
};
|
|
3564
|
+
}, resumeRoot);
|
|
3019
3565
|
switch (journal.command) {
|
|
3020
3566
|
case "switch":
|
|
3021
3567
|
return workflowSwitch(resumedHelpers, journal.input);
|
|
@@ -3045,7 +3591,15 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3045
3591
|
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
3046
3592
|
const lock = inspectWorkflowLock(root);
|
|
3047
3593
|
const journals = listWorkflowRunJournals(root);
|
|
3048
|
-
const
|
|
3594
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3595
|
+
const currentHeads = Object.fromEntries(
|
|
3596
|
+
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
3597
|
+
);
|
|
3598
|
+
const classifiedRuns = classifyWorkflowRunJournals(root, {
|
|
3599
|
+
currentBranch: session.branchName,
|
|
3600
|
+
currentHeads
|
|
3601
|
+
});
|
|
3602
|
+
const interruptedRuns = classifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
3049
3603
|
runId: journal.runId,
|
|
3050
3604
|
command: journal.command,
|
|
3051
3605
|
status: journal.status,
|
|
@@ -3055,6 +3609,29 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3055
3609
|
failure: journal.failure,
|
|
3056
3610
|
resumeCommand: `treeseed resume ${journal.runId}`
|
|
3057
3611
|
}));
|
|
3612
|
+
const staleRuns = classifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
|
|
3613
|
+
runId: journal.runId,
|
|
3614
|
+
command: journal.command,
|
|
3615
|
+
status: journal.status,
|
|
3616
|
+
createdAt: journal.createdAt,
|
|
3617
|
+
updatedAt: journal.updatedAt,
|
|
3618
|
+
nextStep: nextPendingJournalStep(journal)?.description ?? null,
|
|
3619
|
+
failure: journal.failure,
|
|
3620
|
+
classification
|
|
3621
|
+
}));
|
|
3622
|
+
const obsoleteRuns = classifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
|
|
3623
|
+
runId: journal.runId,
|
|
3624
|
+
command: journal.command,
|
|
3625
|
+
status: journal.status,
|
|
3626
|
+
createdAt: journal.createdAt,
|
|
3627
|
+
updatedAt: journal.updatedAt,
|
|
3628
|
+
failure: journal.failure,
|
|
3629
|
+
classification
|
|
3630
|
+
}));
|
|
3631
|
+
const prunedRuns = input.pruneStale === true ? staleRuns.map((run2) => {
|
|
3632
|
+
archiveWorkflowRun(root, run2.runId, run2.classification);
|
|
3633
|
+
return run2;
|
|
3634
|
+
}) : [];
|
|
3058
3635
|
const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
|
|
3059
3636
|
return buildWorkflowResult(
|
|
3060
3637
|
"recover",
|
|
@@ -3062,13 +3639,16 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3062
3639
|
{
|
|
3063
3640
|
lock,
|
|
3064
3641
|
interruptedRuns,
|
|
3642
|
+
staleRuns,
|
|
3643
|
+
obsoleteRuns,
|
|
3644
|
+
prunedRuns,
|
|
3065
3645
|
selectedRun,
|
|
3066
3646
|
runCount: journals.length
|
|
3067
3647
|
},
|
|
3068
3648
|
{
|
|
3069
3649
|
includeFinalState: false,
|
|
3070
3650
|
nextSteps: createNextSteps([
|
|
3071
|
-
...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." }]
|
|
3651
|
+
...interruptedRuns.length > 0 ? [{ operation: "resume", reason: "Resume the most recent interrupted workflow run.", input: { runId: interruptedRuns[0].runId } }] : staleRuns.length > 0 && input.pruneStale !== true ? [{ operation: "recover", reason: "Archive stale interrupted runs that no longer match current heads.", input: { pruneStale: true } }] : [{ operation: "status", reason: "No interrupted runs were found; inspect current workflow state instead." }]
|
|
3072
3652
|
])
|
|
3073
3653
|
}
|
|
3074
3654
|
);
|