@treeseed/sdk 0.6.9 → 0.6.11
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 +687 -78
- 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,84 @@ 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
|
+
}
|
|
235
|
+
function unresolvedMergePaths(repoDir) {
|
|
236
|
+
return run("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
237
|
+
}
|
|
238
|
+
function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
239
|
+
const gitRoot = repoRoot(root);
|
|
240
|
+
const packages = checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageNames.has(pkg.name)).map((pkg) => ({
|
|
241
|
+
...pkg,
|
|
242
|
+
repoPath: relative(gitRoot, pkg.dir)
|
|
243
|
+
}));
|
|
244
|
+
const packagePaths = new Set(packages.map((pkg) => pkg.repoPath));
|
|
245
|
+
const unresolved = unresolvedMergePaths(gitRoot);
|
|
246
|
+
if (unresolved.length === 0 || unresolved.some((filePath) => !packagePaths.has(filePath))) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
for (const pkg of packages) {
|
|
250
|
+
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
251
|
+
run("git", ["add", pkg.repoPath], { cwd: gitRoot });
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
166
255
|
function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
167
256
|
if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
|
|
168
257
|
return inspectWorkspaceDependencyMode(root, { mode: "off", env: helpers.context.env });
|
|
@@ -173,6 +262,163 @@ function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
|
173
262
|
}
|
|
174
263
|
return report;
|
|
175
264
|
}
|
|
265
|
+
function normalizeCiMode(mode, operation) {
|
|
266
|
+
if (mode === "hosted" || mode === "off") return mode;
|
|
267
|
+
return operation === "save" ? "off" : "hosted";
|
|
268
|
+
}
|
|
269
|
+
function normalizeSaveVerifyMode(mode) {
|
|
270
|
+
switch (mode) {
|
|
271
|
+
case "skip":
|
|
272
|
+
case "fast":
|
|
273
|
+
case void 0:
|
|
274
|
+
return "skip";
|
|
275
|
+
case "local":
|
|
276
|
+
case "local-only":
|
|
277
|
+
return "local-only";
|
|
278
|
+
case "hosted":
|
|
279
|
+
return "skip";
|
|
280
|
+
case "both":
|
|
281
|
+
case "action-first":
|
|
282
|
+
return "action-first";
|
|
283
|
+
default:
|
|
284
|
+
return "skip";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function shouldUseHostedSaveCi(input) {
|
|
288
|
+
return normalizeCiMode(input.ciMode, "save") === "hosted" || input.verifyMode === "hosted" || input.verifyMode === "both";
|
|
289
|
+
}
|
|
290
|
+
function worktreePayload(root, requestedMode) {
|
|
291
|
+
const metadata = managedWorkflowWorktreeMetadata(root);
|
|
292
|
+
return {
|
|
293
|
+
worktreeMode: requestedMode ?? "auto",
|
|
294
|
+
managedWorktree: metadata,
|
|
295
|
+
worktreePath: metadata?.worktreePath ?? null,
|
|
296
|
+
primaryRoot: metadata?.primaryRoot ?? null
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function helpersForCwd(helpers, cwd) {
|
|
300
|
+
return {
|
|
301
|
+
...helpers,
|
|
302
|
+
context: {
|
|
303
|
+
...helpers.context,
|
|
304
|
+
cwd
|
|
305
|
+
},
|
|
306
|
+
cwd: () => cwd
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function shouldDispatchSwitchToManagedWorktree(root, input, env) {
|
|
310
|
+
return !isManagedWorkflowWorktree(root) && effectiveWorkflowWorktreeMode(input.worktreeMode, env) === "on";
|
|
311
|
+
}
|
|
312
|
+
function skippedWorkflowGate(gate, reason) {
|
|
313
|
+
return {
|
|
314
|
+
name: gate.name,
|
|
315
|
+
repository: gate.repository ?? null,
|
|
316
|
+
workflow: gate.workflow,
|
|
317
|
+
branch: gate.branch,
|
|
318
|
+
headSha: gate.headSha,
|
|
319
|
+
status: "skipped",
|
|
320
|
+
reason,
|
|
321
|
+
conclusion: null,
|
|
322
|
+
runId: null,
|
|
323
|
+
url: null
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function workflowGateFailureMessage(gate, result) {
|
|
327
|
+
const repository = String(result.repository ?? gate.repository ?? gate.name);
|
|
328
|
+
const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
|
|
329
|
+
const url = typeof result.url === "string" && result.url ? `
|
|
330
|
+
${result.url}` : "";
|
|
331
|
+
const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => stringRecord(job)?.name).filter((name) => typeof name === "string" && name.length > 0) : [];
|
|
332
|
+
const jobLine = failedJobs.length > 0 ? `
|
|
333
|
+
Failed jobs: ${failedJobs.join(", ")}` : "";
|
|
334
|
+
const command = runId ? `
|
|
335
|
+
Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
|
|
336
|
+
return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
|
|
337
|
+
}
|
|
338
|
+
function assertHostedGitHubWorkflowAuthReady(operation, root) {
|
|
339
|
+
if (getGitHubAutomationMode() === "stub") {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const tools = collectTreeseedToolStatus({
|
|
343
|
+
tenantRoot: root,
|
|
344
|
+
env: process.env
|
|
345
|
+
});
|
|
346
|
+
const github = tools.auth.github;
|
|
347
|
+
if (!github.authenticated) {
|
|
348
|
+
const command = github.command.length > 0 ? github.command.join(" ") : "npx trsd tools --json";
|
|
349
|
+
workflowError(
|
|
350
|
+
operation,
|
|
351
|
+
"github_auth_unavailable",
|
|
352
|
+
[
|
|
353
|
+
"Treeseed hosted GitHub workflow gates require an authenticated managed GitHub CLI.",
|
|
354
|
+
github.detail,
|
|
355
|
+
`Managed gh check: ${command}`,
|
|
356
|
+
"Remediation:",
|
|
357
|
+
...github.remediation.map((item) => `- ${item}`)
|
|
358
|
+
].join("\n"),
|
|
359
|
+
{
|
|
360
|
+
details: {
|
|
361
|
+
toolsHome: tools.toolsHome,
|
|
362
|
+
ghConfigDir: tools.ghConfigDir,
|
|
363
|
+
github,
|
|
364
|
+
tools: tools.tools
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return tools;
|
|
370
|
+
}
|
|
371
|
+
async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
372
|
+
if (ciMode === "off" || process.env.TREESEED_STAGE_WAIT_MODE === "skip") {
|
|
373
|
+
return gates.map((gate) => skippedWorkflowGate(gate, ciMode === "off" ? "disabled" : "stubbed"));
|
|
374
|
+
}
|
|
375
|
+
if (gates.length === 0) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
assertHostedGitHubWorkflowAuthReady(operation, options.root ?? gates[0].repoPath);
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const gate of gates) {
|
|
381
|
+
if (options.root && options.runId) {
|
|
382
|
+
const cached = getCachedSuccessfulWorkflowGate(options.root, options.runId, {
|
|
383
|
+
repository: gate.repository ?? null,
|
|
384
|
+
workflow: gate.workflow,
|
|
385
|
+
headSha: gate.headSha,
|
|
386
|
+
branch: gate.branch
|
|
387
|
+
});
|
|
388
|
+
if (cached) {
|
|
389
|
+
results.push({
|
|
390
|
+
...cached.result,
|
|
391
|
+
name: gate.name,
|
|
392
|
+
cached: true
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const result = await waitForGitHubWorkflowCompletion(gate.repoPath, {
|
|
398
|
+
repository: gate.repository,
|
|
399
|
+
workflow: gate.workflow,
|
|
400
|
+
headSha: gate.headSha,
|
|
401
|
+
branch: gate.branch
|
|
402
|
+
});
|
|
403
|
+
const normalized = {
|
|
404
|
+
name: gate.name,
|
|
405
|
+
...result,
|
|
406
|
+
workflow: String(result.workflow ?? gate.workflow),
|
|
407
|
+
branch: String(result.branch ?? gate.branch),
|
|
408
|
+
headSha: String(result.headSha ?? gate.headSha)
|
|
409
|
+
};
|
|
410
|
+
if (normalized.status === "completed" && normalized.conclusion !== "success") {
|
|
411
|
+
workflowError(operation, "github_workflow_failed", workflowGateFailureMessage(gate, normalized), {
|
|
412
|
+
details: { gate, workflow: normalized }
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (options.root && options.runId && normalized.status === "completed" && normalized.conclusion === "success") {
|
|
416
|
+
cacheWorkflowGateResult(options.root, options.runId, normalized);
|
|
417
|
+
}
|
|
418
|
+
results.push(normalized);
|
|
419
|
+
}
|
|
420
|
+
return results;
|
|
421
|
+
}
|
|
176
422
|
function ensureTreeseedCommandReadiness(root) {
|
|
177
423
|
if (getGitHubAutomationMode() === "stub") {
|
|
178
424
|
return {
|
|
@@ -295,7 +541,8 @@ function createRepoReport(name, path, branch, dirty) {
|
|
|
295
541
|
tagName: null,
|
|
296
542
|
commitSha: branch ? headCommit(path) : null,
|
|
297
543
|
skippedReason: null,
|
|
298
|
-
publishWait: null
|
|
544
|
+
publishWait: null,
|
|
545
|
+
workflowGates: []
|
|
299
546
|
};
|
|
300
547
|
}
|
|
301
548
|
function createWorkspaceRootRepoReport(root) {
|
|
@@ -712,6 +959,10 @@ function findAutoResumableSaveRun(root, branch) {
|
|
|
712
959
|
if (!branch) return null;
|
|
713
960
|
return listInterruptedWorkflowRuns(root).find((journal) => journal.command === "save" && journal.resumable && journal.session.branchName === branch) ?? null;
|
|
714
961
|
}
|
|
962
|
+
function findAutoResumableTaskRun(root, command, branch) {
|
|
963
|
+
if (!branch) return null;
|
|
964
|
+
return listInterruptedWorkflowRuns(root).find((journal) => journal.command === command && journal.resumable && journal.session.branchName === branch) ?? null;
|
|
965
|
+
}
|
|
715
966
|
function stringRecord(value) {
|
|
716
967
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
717
968
|
}
|
|
@@ -1033,7 +1284,7 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
|
|
|
1033
1284
|
if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
|
|
1034
1285
|
return;
|
|
1035
1286
|
}
|
|
1036
|
-
|
|
1287
|
+
assertHostedGitHubWorkflowAuthReady("release", root);
|
|
1037
1288
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1038
1289
|
if (!selectedPackageNames.has(pkg.name)) continue;
|
|
1039
1290
|
resolveGitHubRepositorySlug(pkg.dir);
|
|
@@ -1101,6 +1352,14 @@ function previewStateFor(tenantRoot, branchName) {
|
|
|
1101
1352
|
target: createBranchPreviewDeployTarget(branchName)
|
|
1102
1353
|
});
|
|
1103
1354
|
}
|
|
1355
|
+
function branchPreviewInitialized(tenantRoot, branchName) {
|
|
1356
|
+
if (!branchName) return false;
|
|
1357
|
+
try {
|
|
1358
|
+
return previewStateFor(tenantRoot, branchName).readiness?.initialized === true;
|
|
1359
|
+
} catch {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1104
1363
|
async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
|
|
1105
1364
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
|
|
1106
1365
|
assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
|
|
@@ -1710,7 +1969,10 @@ async function workflowExport(helpers, input = {}) {
|
|
|
1710
1969
|
return await withContextEnv(helpers.context.env, async () => {
|
|
1711
1970
|
const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
|
|
1712
1971
|
const exported = await exportTreeseedCodebase({ directory });
|
|
1713
|
-
return buildWorkflowResult("export", exported.tenantRoot,
|
|
1972
|
+
return buildWorkflowResult("export", exported.tenantRoot, {
|
|
1973
|
+
...exported,
|
|
1974
|
+
...worktreePayload(exported.tenantRoot, input.worktreeMode)
|
|
1975
|
+
});
|
|
1714
1976
|
});
|
|
1715
1977
|
}
|
|
1716
1978
|
async function workflowSwitch(helpers, input) {
|
|
@@ -1725,6 +1987,27 @@ async function workflowSwitch(helpers, input) {
|
|
|
1725
1987
|
}
|
|
1726
1988
|
const preview = input.preview === true;
|
|
1727
1989
|
const executionMode = normalizeExecutionMode(input);
|
|
1990
|
+
if (executionMode !== "plan" && shouldDispatchSwitchToManagedWorktree(root, input, helpers.context.env)) {
|
|
1991
|
+
const managed = ensureManagedWorkflowWorktree({
|
|
1992
|
+
root,
|
|
1993
|
+
branchName,
|
|
1994
|
+
mode: input.worktreeMode,
|
|
1995
|
+
env: helpers.context.env
|
|
1996
|
+
});
|
|
1997
|
+
const result = await workflowSwitch(helpersForCwd(helpers, managed.worktreePath), {
|
|
1998
|
+
...input,
|
|
1999
|
+
worktreeMode: "off"
|
|
2000
|
+
});
|
|
2001
|
+
return {
|
|
2002
|
+
...result,
|
|
2003
|
+
payload: {
|
|
2004
|
+
...result.payload,
|
|
2005
|
+
worktreeMode: input.worktreeMode ?? "auto",
|
|
2006
|
+
worktreePath: managed.worktreePath,
|
|
2007
|
+
managedWorktree: managed
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
1728
2011
|
const mode = session.mode;
|
|
1729
2012
|
const repoDir = session.gitRoot;
|
|
1730
2013
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
@@ -1747,6 +2030,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
1747
2030
|
rootRepo,
|
|
1748
2031
|
repos: packageReports,
|
|
1749
2032
|
previewRequested: preview,
|
|
2033
|
+
worktreeMode: input.worktreeMode ?? "auto",
|
|
2034
|
+
worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
|
|
1750
2035
|
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
1751
2036
|
plannedSteps: [
|
|
1752
2037
|
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
@@ -1772,7 +2057,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1772
2057
|
const workflowRun = acquireWorkflowRun(
|
|
1773
2058
|
"switch",
|
|
1774
2059
|
session,
|
|
1775
|
-
{ branch: branchName, preview },
|
|
2060
|
+
{ branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
|
|
1776
2061
|
[
|
|
1777
2062
|
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
1778
2063
|
...packageReports.map((report) => ({
|
|
@@ -1849,6 +2134,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1849
2134
|
},
|
|
1850
2135
|
previewResult,
|
|
1851
2136
|
workspaceLinks,
|
|
2137
|
+
...worktreePayload(root, input.worktreeMode),
|
|
1852
2138
|
preconditions: {
|
|
1853
2139
|
cleanWorktreeRequired: true,
|
|
1854
2140
|
baseBranch: STAGING_BRANCH
|
|
@@ -1975,6 +2261,7 @@ async function workflowSave(helpers, input) {
|
|
|
1975
2261
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
1976
2262
|
const message = String(effectiveInput.message ?? "").trim();
|
|
1977
2263
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
2264
|
+
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
1978
2265
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
1979
2266
|
if (!branch) {
|
|
1980
2267
|
workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
|
|
@@ -2002,7 +2289,7 @@ async function workflowSave(helpers, input) {
|
|
|
2002
2289
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2003
2290
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2004
2291
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2005
|
-
verifyMode:
|
|
2292
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2006
2293
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
|
|
2007
2294
|
});
|
|
2008
2295
|
const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
|
|
@@ -2024,6 +2311,9 @@ async function workflowSave(helpers, input) {
|
|
|
2024
2311
|
failure: planAutoResumeRun.failure
|
|
2025
2312
|
} : null,
|
|
2026
2313
|
workspaceLinks,
|
|
2314
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2315
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2316
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2027
2317
|
repositoryPlan,
|
|
2028
2318
|
waves: repositoryPlan.waves,
|
|
2029
2319
|
plannedVersions: repositoryPlan.plannedVersions,
|
|
@@ -2032,7 +2322,7 @@ async function workflowSave(helpers, input) {
|
|
|
2032
2322
|
...repositoryPlan.plannedSteps,
|
|
2033
2323
|
{ id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
|
|
2034
2324
|
{ id: "workspace-link", description: "Restore local workspace links after save" },
|
|
2035
|
-
...beforeState.branchRole === "feature" && (effectiveInput.preview === true ||
|
|
2325
|
+
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
2036
2326
|
]
|
|
2037
2327
|
},
|
|
2038
2328
|
{
|
|
@@ -2065,7 +2355,9 @@ async function workflowSave(helpers, input) {
|
|
|
2065
2355
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2066
2356
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2067
2357
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2068
|
-
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "
|
|
2358
|
+
verifyMode: effectiveInput.verifyMode ?? (effectiveInput.verify === false ? "skip" : "fast"),
|
|
2359
|
+
ciMode: effectiveInput.ciMode ?? "auto",
|
|
2360
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2069
2361
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2070
2362
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2071
2363
|
},
|
|
@@ -2078,7 +2370,15 @@ async function workflowSave(helpers, input) {
|
|
|
2078
2370
|
branch,
|
|
2079
2371
|
resumable: true
|
|
2080
2372
|
},
|
|
2081
|
-
...
|
|
2373
|
+
...shouldUseHostedSaveCi(effectiveInput) ? [{
|
|
2374
|
+
id: "hosted-ci",
|
|
2375
|
+
description: `Wait for hosted save workflows on ${branch}`,
|
|
2376
|
+
repoName: rootRepo.name,
|
|
2377
|
+
repoPath: rootRepo.path,
|
|
2378
|
+
branch,
|
|
2379
|
+
resumable: true
|
|
2380
|
+
}] : [],
|
|
2381
|
+
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || effectiveInput.refreshPreview !== false && previewInitialized) ? [{
|
|
2082
2382
|
id: "preview",
|
|
2083
2383
|
description: `Refresh preview ${branch}`,
|
|
2084
2384
|
repoName: rootRepo.name,
|
|
@@ -2112,7 +2412,7 @@ async function workflowSave(helpers, input) {
|
|
|
2112
2412
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2113
2413
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2114
2414
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2115
|
-
verifyMode:
|
|
2415
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2116
2416
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2117
2417
|
workflowRunId: workflowRun.runId,
|
|
2118
2418
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
@@ -2139,14 +2439,30 @@ async function workflowSave(helpers, input) {
|
|
|
2139
2439
|
lockfileValidation: repo.lockfileValidation
|
|
2140
2440
|
}))
|
|
2141
2441
|
};
|
|
2442
|
+
const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", () => waitForWorkflowGates("save", [
|
|
2443
|
+
...savedRootRepo.pushed && savedRootRepo.commitSha && branch ? [{
|
|
2444
|
+
name: savedRootRepo.name,
|
|
2445
|
+
repoPath: savedRootRepo.path,
|
|
2446
|
+
workflow: "verify.yml",
|
|
2447
|
+
branch,
|
|
2448
|
+
headSha: savedRootRepo.commitSha
|
|
2449
|
+
}] : [],
|
|
2450
|
+
...savedPackageReports.filter((repo) => repo.pushed && repo.commitSha && repo.branch).map((repo) => ({
|
|
2451
|
+
name: repo.name,
|
|
2452
|
+
repoPath: repo.path,
|
|
2453
|
+
workflow: "verify.yml",
|
|
2454
|
+
branch: String(repo.branch),
|
|
2455
|
+
headSha: String(repo.commitSha)
|
|
2456
|
+
}))
|
|
2457
|
+
], "hosted", { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates }))) : { workflowGates: [] };
|
|
2142
2458
|
let previewAction = { status: "skipped" };
|
|
2143
2459
|
if (beforeState.branchRole === "feature" && branch) {
|
|
2144
2460
|
if (effectiveInput.preview === true) {
|
|
2145
2461
|
previewAction = {
|
|
2146
|
-
status:
|
|
2147
|
-
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !
|
|
2462
|
+
status: previewInitialized ? "refreshed" : "created",
|
|
2463
|
+
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: !previewInitialized }))
|
|
2148
2464
|
};
|
|
2149
|
-
} else if (effectiveInput.refreshPreview !== false &&
|
|
2465
|
+
} else if (effectiveInput.refreshPreview !== false && previewInitialized) {
|
|
2150
2466
|
previewAction = {
|
|
2151
2467
|
status: "refreshed",
|
|
2152
2468
|
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
@@ -2175,7 +2491,11 @@ async function workflowSave(helpers, input) {
|
|
|
2175
2491
|
mergeConflict: null,
|
|
2176
2492
|
workspaceLinks,
|
|
2177
2493
|
commandReadiness,
|
|
2178
|
-
lockfileValidation
|
|
2494
|
+
lockfileValidation,
|
|
2495
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2496
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2497
|
+
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
2498
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2179
2499
|
};
|
|
2180
2500
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2181
2501
|
return buildWorkflowResult(
|
|
@@ -2232,9 +2552,13 @@ async function workflowClose(helpers, input) {
|
|
|
2232
2552
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2233
2553
|
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
2234
2554
|
const root = workspaceRoot(tenantRoot);
|
|
2235
|
-
const message = ensureMessage("close", input.message, "a close reason");
|
|
2236
2555
|
const executionMode = normalizeExecutionMode(input);
|
|
2237
2556
|
const session = resolveTreeseedWorkflowSession(root);
|
|
2557
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
2558
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
|
|
2559
|
+
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "close", session.branchName) : null;
|
|
2560
|
+
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2561
|
+
const message = ensureMessage("close", effectiveInput.message, "a close reason");
|
|
2238
2562
|
if (executionMode === "plan") {
|
|
2239
2563
|
const branchName = session.branchName;
|
|
2240
2564
|
const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
|
|
@@ -2245,18 +2569,26 @@ async function workflowClose(helpers, input) {
|
|
|
2245
2569
|
mode: session.mode,
|
|
2246
2570
|
branchName,
|
|
2247
2571
|
message,
|
|
2572
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
2573
|
+
runId: planAutoResumeRun.runId,
|
|
2574
|
+
branch: planAutoResumeRun.session.branchName,
|
|
2575
|
+
failure: planAutoResumeRun.failure
|
|
2576
|
+
} : null,
|
|
2577
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2248
2578
|
autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
|
|
2249
2579
|
repos: createWorkspacePackageReports(root),
|
|
2250
2580
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
2251
2581
|
blockers,
|
|
2252
2582
|
plannedSteps: [
|
|
2583
|
+
{ id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
|
|
2253
2584
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
2254
2585
|
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
2255
2586
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
2256
2587
|
id: `cleanup-${pkg.name}`,
|
|
2257
2588
|
description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
|
|
2258
2589
|
})),
|
|
2259
|
-
{ id: "workspace-link", description: "Restore local workspace links on the final branch" }
|
|
2590
|
+
{ id: "workspace-link", description: "Restore local workspace links on the final branch" },
|
|
2591
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree" }] : []
|
|
2260
2592
|
]
|
|
2261
2593
|
},
|
|
2262
2594
|
{
|
|
@@ -2267,12 +2599,10 @@ async function workflowClose(helpers, input) {
|
|
|
2267
2599
|
}
|
|
2268
2600
|
);
|
|
2269
2601
|
}
|
|
2270
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2271
2602
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
2272
2603
|
message,
|
|
2273
|
-
autoSave:
|
|
2604
|
+
autoSave: effectiveInput.autoSave
|
|
2274
2605
|
});
|
|
2275
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2276
2606
|
const activeSession = resolveTreeseedWorkflowSession(root);
|
|
2277
2607
|
const featureBranch = assertFeatureBranch(root);
|
|
2278
2608
|
const mode = activeSession.mode;
|
|
@@ -2288,8 +2618,14 @@ async function workflowClose(helpers, input) {
|
|
|
2288
2618
|
const workflowRun = acquireWorkflowRun(
|
|
2289
2619
|
"close",
|
|
2290
2620
|
activeSession,
|
|
2291
|
-
{
|
|
2621
|
+
{
|
|
2622
|
+
message,
|
|
2623
|
+
deletePreview: effectiveInput.deletePreview !== false,
|
|
2624
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2625
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto"
|
|
2626
|
+
},
|
|
2292
2627
|
[
|
|
2628
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2293
2629
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2294
2630
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2295
2631
|
...packageReports.map((report) => ({
|
|
@@ -2299,23 +2635,35 @@ async function workflowClose(helpers, input) {
|
|
|
2299
2635
|
repoPath: report.path,
|
|
2300
2636
|
branch: featureBranch,
|
|
2301
2637
|
resumable: true
|
|
2302
|
-
}))
|
|
2638
|
+
})),
|
|
2639
|
+
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2640
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
2303
2641
|
],
|
|
2304
|
-
|
|
2642
|
+
autoResumeRun ? {
|
|
2643
|
+
...helpers.context,
|
|
2644
|
+
workflow: {
|
|
2645
|
+
...helpers.context.workflow ?? {},
|
|
2646
|
+
resumeRunId: autoResumeRun.runId
|
|
2647
|
+
}
|
|
2648
|
+
} : helpers.context
|
|
2305
2649
|
);
|
|
2650
|
+
if (autoResumeRun) {
|
|
2651
|
+
helpers.write(`[workflow][resume] Resuming interrupted close ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2652
|
+
}
|
|
2306
2653
|
try {
|
|
2307
|
-
|
|
2654
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2655
|
+
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
2656
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2309
2657
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
2310
|
-
const deletedRemote =
|
|
2658
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2311
2659
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2312
|
-
if (
|
|
2660
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2313
2661
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2314
2662
|
}
|
|
2315
2663
|
return {
|
|
2316
2664
|
deprecatedTag,
|
|
2317
2665
|
deletedRemote,
|
|
2318
|
-
deletedLocal:
|
|
2666
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2319
2667
|
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
2320
2668
|
dirty: hasMeaningfulChanges(repoDir)
|
|
2321
2669
|
};
|
|
@@ -2332,12 +2680,15 @@ async function workflowClose(helpers, input) {
|
|
|
2332
2680
|
continue;
|
|
2333
2681
|
}
|
|
2334
2682
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
|
|
2335
|
-
deleteBranch:
|
|
2683
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2336
2684
|
targetBranch: STAGING_BRANCH
|
|
2337
2685
|
}));
|
|
2338
2686
|
Object.assign(report, cleanup);
|
|
2339
2687
|
}
|
|
2340
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2688
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
2689
|
+
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
2690
|
+
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
2691
|
+
const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
|
|
2341
2692
|
const payload = {
|
|
2342
2693
|
mode,
|
|
2343
2694
|
branchName: featureBranch,
|
|
@@ -2350,8 +2701,13 @@ async function workflowClose(helpers, input) {
|
|
|
2350
2701
|
previewCleanup,
|
|
2351
2702
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2352
2703
|
localDeleted: rootRepo.deletedLocal,
|
|
2353
|
-
finalBranch
|
|
2354
|
-
workspaceLinks
|
|
2704
|
+
finalBranch,
|
|
2705
|
+
workspaceLinks,
|
|
2706
|
+
worktreeCleanup,
|
|
2707
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2708
|
+
managedWorktree,
|
|
2709
|
+
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
2710
|
+
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
2355
2711
|
};
|
|
2356
2712
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2357
2713
|
return buildWorkflowResult(
|
|
@@ -2366,7 +2722,7 @@ async function workflowClose(helpers, input) {
|
|
|
2366
2722
|
}
|
|
2367
2723
|
);
|
|
2368
2724
|
} catch (error) {
|
|
2369
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2725
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2370
2726
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2371
2727
|
resumable: true,
|
|
2372
2728
|
runId: workflowRun.runId,
|
|
@@ -2387,11 +2743,19 @@ async function workflowStage(helpers, input) {
|
|
|
2387
2743
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2388
2744
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
2389
2745
|
const root = workspaceRoot(tenantRoot);
|
|
2390
|
-
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
2391
2746
|
const executionMode = normalizeExecutionMode(input);
|
|
2392
2747
|
const initialSession = resolveTreeseedWorkflowSession(root);
|
|
2748
|
+
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
2749
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
|
|
2750
|
+
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableTaskRun(root, "stage", initialSession.branchName) : null;
|
|
2751
|
+
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2752
|
+
const message = ensureMessage("stage", effectiveInput.message, "a resolution message");
|
|
2753
|
+
const ciMode = normalizeCiMode(effectiveInput.ciMode, "stage");
|
|
2393
2754
|
if (executionMode === "plan") {
|
|
2394
2755
|
const blockers = [];
|
|
2756
|
+
if (initialSession.branchRole !== "feature") {
|
|
2757
|
+
blockers.push("Stage only applies to task branches.");
|
|
2758
|
+
}
|
|
2395
2759
|
try {
|
|
2396
2760
|
validateStagingWorkflowContracts(root);
|
|
2397
2761
|
} catch (error) {
|
|
@@ -2406,6 +2770,13 @@ async function workflowStage(helpers, input) {
|
|
|
2406
2770
|
mergeTarget: STAGING_BRANCH,
|
|
2407
2771
|
mergeStrategy: "squash",
|
|
2408
2772
|
message,
|
|
2773
|
+
ciMode,
|
|
2774
|
+
autoResumeCandidate: planAutoResumeRun ? {
|
|
2775
|
+
runId: planAutoResumeRun.runId,
|
|
2776
|
+
branch: planAutoResumeRun.session.branchName,
|
|
2777
|
+
failure: planAutoResumeRun.failure
|
|
2778
|
+
} : null,
|
|
2779
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2409
2780
|
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
2410
2781
|
blockers,
|
|
2411
2782
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
@@ -2418,7 +2789,7 @@ async function workflowStage(helpers, input) {
|
|
|
2418
2789
|
{ id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
|
|
2419
2790
|
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
2420
2791
|
{ id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
|
|
2421
|
-
{ id: "wait-staging", description: "Wait for staging
|
|
2792
|
+
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
|
|
2422
2793
|
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
2423
2794
|
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
2424
2795
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
@@ -2436,12 +2807,10 @@ async function workflowStage(helpers, input) {
|
|
|
2436
2807
|
}
|
|
2437
2808
|
);
|
|
2438
2809
|
}
|
|
2439
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2440
2810
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
2441
2811
|
message,
|
|
2442
|
-
autoSave:
|
|
2812
|
+
autoSave: effectiveInput.autoSave
|
|
2443
2813
|
});
|
|
2444
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2445
2814
|
const session = resolveTreeseedWorkflowSession(root);
|
|
2446
2815
|
const featureBranch = assertFeatureBranch(root);
|
|
2447
2816
|
const mode = session.mode;
|
|
@@ -2451,6 +2820,8 @@ async function workflowStage(helpers, input) {
|
|
|
2451
2820
|
} else {
|
|
2452
2821
|
assertCleanWorktree(root);
|
|
2453
2822
|
}
|
|
2823
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2824
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
2454
2825
|
validateStagingWorkflowContracts(root);
|
|
2455
2826
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2456
2827
|
const repoDir = session.gitRoot;
|
|
@@ -2459,8 +2830,16 @@ async function workflowStage(helpers, input) {
|
|
|
2459
2830
|
const workflowRun = acquireWorkflowRun(
|
|
2460
2831
|
"stage",
|
|
2461
2832
|
session,
|
|
2462
|
-
{
|
|
2833
|
+
{
|
|
2834
|
+
message,
|
|
2835
|
+
waitForStaging: effectiveInput.waitForStaging !== false,
|
|
2836
|
+
deletePreview: effectiveInput.deletePreview !== false,
|
|
2837
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2838
|
+
ciMode,
|
|
2839
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto"
|
|
2840
|
+
},
|
|
2463
2841
|
[
|
|
2842
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2464
2843
|
...packageReports.map((report) => ({
|
|
2465
2844
|
id: `merge-${report.name}`,
|
|
2466
2845
|
description: `Merge ${featureBranch} into ${report.name} staging`,
|
|
@@ -2470,7 +2849,7 @@ async function workflowStage(helpers, input) {
|
|
|
2470
2849
|
resumable: true
|
|
2471
2850
|
})),
|
|
2472
2851
|
{ 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
|
|
2852
|
+
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2474
2853
|
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2475
2854
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2476
2855
|
...packageReports.map((report) => ({
|
|
@@ -2480,12 +2859,23 @@ async function workflowStage(helpers, input) {
|
|
|
2480
2859
|
repoPath: report.path,
|
|
2481
2860
|
branch: featureBranch,
|
|
2482
2861
|
resumable: true
|
|
2483
|
-
}))
|
|
2862
|
+
})),
|
|
2863
|
+
{ id: "workspace-link", description: "Restore local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
2864
|
+
...isManagedWorkflowWorktree(root) ? [{ id: "worktree-cleanup", description: "Remove managed workflow worktree", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: false }] : []
|
|
2484
2865
|
],
|
|
2485
|
-
|
|
2866
|
+
autoResumeRun ? {
|
|
2867
|
+
...helpers.context,
|
|
2868
|
+
workflow: {
|
|
2869
|
+
...helpers.context.workflow ?? {},
|
|
2870
|
+
resumeRunId: autoResumeRun.runId
|
|
2871
|
+
}
|
|
2872
|
+
} : helpers.context
|
|
2486
2873
|
);
|
|
2874
|
+
if (autoResumeRun) {
|
|
2875
|
+
helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2876
|
+
}
|
|
2487
2877
|
try {
|
|
2488
|
-
unlinkWorkflowWorkspaceLinks(root, helpers,
|
|
2878
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2489
2879
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2490
2880
|
const report = findReportByName(packageReports, pkg.name);
|
|
2491
2881
|
if (!report) {
|
|
@@ -2517,7 +2907,11 @@ async function workflowStage(helpers, input) {
|
|
|
2517
2907
|
try {
|
|
2518
2908
|
rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
|
|
2519
2909
|
assertCleanWorktree(root);
|
|
2520
|
-
|
|
2910
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2911
|
+
checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
|
|
2912
|
+
} else {
|
|
2913
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2914
|
+
}
|
|
2521
2915
|
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
2522
2916
|
if (mode === "recursive-workspace") {
|
|
2523
2917
|
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
@@ -2532,10 +2926,14 @@ async function workflowStage(helpers, input) {
|
|
|
2532
2926
|
run("git", ["add", "-A"], { cwd: repoDir });
|
|
2533
2927
|
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
2534
2928
|
}
|
|
2535
|
-
|
|
2929
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2930
|
+
pushHeadToBranch(repoDir, STAGING_BRANCH);
|
|
2931
|
+
} else {
|
|
2932
|
+
pushBranch(repoDir, STAGING_BRANCH);
|
|
2933
|
+
}
|
|
2536
2934
|
return {
|
|
2537
2935
|
commitSha: headCommit(repoDir),
|
|
2538
|
-
branch:
|
|
2936
|
+
branch: STAGING_BRANCH,
|
|
2539
2937
|
committed: hasMeaningfulChanges(repoDir) ? false : true,
|
|
2540
2938
|
lockfileValidation: lockfileSafety.lockfileValidation,
|
|
2541
2939
|
lockfileInstall: lockfileSafety.install
|
|
@@ -2553,18 +2951,47 @@ async function workflowStage(helpers, input) {
|
|
|
2553
2951
|
exitCode: 12
|
|
2554
2952
|
});
|
|
2555
2953
|
}
|
|
2556
|
-
const
|
|
2557
|
-
|
|
2954
|
+
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", [
|
|
2955
|
+
{
|
|
2956
|
+
name: rootRepo.name,
|
|
2957
|
+
repoPath: rootRepo.path,
|
|
2958
|
+
workflow: "verify.yml",
|
|
2959
|
+
branch: STAGING_BRANCH,
|
|
2960
|
+
headSha: rootRepo.commitSha
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
name: rootRepo.name,
|
|
2964
|
+
repoPath: rootRepo.path,
|
|
2965
|
+
workflow: "deploy.yml",
|
|
2966
|
+
branch: STAGING_BRANCH,
|
|
2967
|
+
headSha: rootRepo.commitSha
|
|
2968
|
+
},
|
|
2969
|
+
...packageReports.filter((report) => report.merged && report.commitSha).map((report) => ({
|
|
2970
|
+
name: report.name,
|
|
2971
|
+
repoPath: report.path,
|
|
2972
|
+
workflow: "verify.yml",
|
|
2973
|
+
branch: STAGING_BRANCH,
|
|
2974
|
+
headSha: String(report.commitSha)
|
|
2975
|
+
}))
|
|
2976
|
+
], ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({
|
|
2977
|
+
status: "completed",
|
|
2978
|
+
workflowGates
|
|
2979
|
+
})));
|
|
2980
|
+
const stagingWait = {
|
|
2981
|
+
status: String(stageWorkflowGateResult?.status ?? "completed"),
|
|
2982
|
+
workflowGates: Array.isArray(stageWorkflowGateResult?.workflowGates) ? stageWorkflowGateResult.workflowGates : []
|
|
2983
|
+
};
|
|
2984
|
+
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
2985
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2559
2986
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
2560
|
-
const deletedRemote =
|
|
2561
|
-
if (
|
|
2987
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2988
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2562
2989
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2563
2990
|
}
|
|
2564
2991
|
return {
|
|
2565
2992
|
deprecatedTag,
|
|
2566
2993
|
deletedRemote,
|
|
2567
|
-
deletedLocal:
|
|
2994
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2568
2995
|
branch: currentBranch(repoDir) || STAGING_BRANCH
|
|
2569
2996
|
};
|
|
2570
2997
|
});
|
|
@@ -2579,12 +3006,15 @@ async function workflowStage(helpers, input) {
|
|
|
2579
3006
|
continue;
|
|
2580
3007
|
}
|
|
2581
3008
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
2582
|
-
deleteBranch:
|
|
3009
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2583
3010
|
targetBranch: STAGING_BRANCH
|
|
2584
3011
|
}));
|
|
2585
3012
|
Object.assign(report, cleanup);
|
|
2586
3013
|
}
|
|
2587
|
-
const workspaceLinks = ensureWorkflowWorkspaceLinks(root, helpers,
|
|
3014
|
+
const workspaceLinks = await executeJournalStep(root, workflowRun.runId, "workspace-link", () => ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
3015
|
+
const finalBranch = currentBranch(repoDir) || STAGING_BRANCH;
|
|
3016
|
+
const managedWorktree = managedWorkflowWorktreeMetadata(root);
|
|
3017
|
+
const worktreeCleanup = isManagedWorkflowWorktree(root) ? await executeJournalStep(root, workflowRun.runId, "worktree-cleanup", () => removeManagedWorkflowWorktree(root)) : { removed: false, reason: "not-managed" };
|
|
2588
3018
|
const payload = {
|
|
2589
3019
|
mode,
|
|
2590
3020
|
branchName: featureBranch,
|
|
@@ -2602,8 +3032,15 @@ async function workflowStage(helpers, input) {
|
|
|
2602
3032
|
lockfileInstall: rootMerge?.lockfileInstall ?? null,
|
|
2603
3033
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2604
3034
|
localDeleted: rootRepo.deletedLocal,
|
|
2605
|
-
finalBranch
|
|
2606
|
-
workspaceLinks
|
|
3035
|
+
finalBranch,
|
|
3036
|
+
workspaceLinks,
|
|
3037
|
+
ciMode,
|
|
3038
|
+
workflowGates: stagingWait.workflowGates,
|
|
3039
|
+
worktreeCleanup,
|
|
3040
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3041
|
+
managedWorktree,
|
|
3042
|
+
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
3043
|
+
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
2607
3044
|
};
|
|
2608
3045
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2609
3046
|
return buildWorkflowResult(
|
|
@@ -2619,7 +3056,7 @@ async function workflowStage(helpers, input) {
|
|
|
2619
3056
|
}
|
|
2620
3057
|
);
|
|
2621
3058
|
} catch (error) {
|
|
2622
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
3059
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2623
3060
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2624
3061
|
resumable: true,
|
|
2625
3062
|
runId: workflowRun.runId,
|
|
@@ -2650,6 +3087,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2650
3087
|
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
2651
3088
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2652
3089
|
const level = effectiveInput.bump ?? "patch";
|
|
3090
|
+
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
2653
3091
|
const isResume = Boolean(explicitResumeRunId || autoResumeRun);
|
|
2654
3092
|
const packageSelection = session.packageSelection;
|
|
2655
3093
|
const selectedPackageNames = new Set(packageSelection.selected);
|
|
@@ -2666,6 +3104,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2666
3104
|
if (executionMode === "plan") {
|
|
2667
3105
|
return buildWorkflowResult("release", root, {
|
|
2668
3106
|
...plannedRelease,
|
|
3107
|
+
ciMode,
|
|
3108
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2669
3109
|
autoResumeCandidate: planAutoResumeRun ? {
|
|
2670
3110
|
runId: planAutoResumeRun.runId,
|
|
2671
3111
|
branch: planAutoResumeRun.session.branchName,
|
|
@@ -2691,6 +3131,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2691
3131
|
devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
|
|
2692
3132
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2693
3133
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3134
|
+
ciMode,
|
|
3135
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2694
3136
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2695
3137
|
},
|
|
2696
3138
|
[
|
|
@@ -2706,6 +3148,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2706
3148
|
resumable: true
|
|
2707
3149
|
})),
|
|
2708
3150
|
{ id: "release-root", description: "Release market repo", repoName: rootRepo.name, repoPath: rootRepo.path, branch: STAGING_BRANCH, resumable: true },
|
|
3151
|
+
{ id: "release-root-gates", description: "Wait for market release GitHub Actions gates", repoName: rootRepo.name, repoPath: rootRepo.path, branch: PRODUCTION_BRANCH, resumable: true },
|
|
2709
3152
|
...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
3153
|
],
|
|
2711
3154
|
autoResumeRun ? {
|
|
@@ -2732,19 +3175,22 @@ async function workflowRelease(helpers, input) {
|
|
|
2732
3175
|
assertCleanWorktree(root);
|
|
2733
3176
|
}
|
|
2734
3177
|
prepareReleaseBranches(root);
|
|
3178
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2735
3179
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2736
|
-
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"));
|
|
3180
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2737
3181
|
if (mode === "root-only") {
|
|
2738
3182
|
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
2739
3183
|
setRootPackageJsonVersion(root, rootVersion);
|
|
2740
3184
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2741
3185
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2742
3186
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3187
|
+
const stagingCommit = headCommit(gitRoot);
|
|
2743
3188
|
const released = mergeStagingIntoMain(root);
|
|
2744
3189
|
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
|
|
2745
3190
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2746
3191
|
return {
|
|
2747
3192
|
rootVersion,
|
|
3193
|
+
stagingCommit,
|
|
2748
3194
|
releasedCommit: released.commitSha,
|
|
2749
3195
|
tag
|
|
2750
3196
|
};
|
|
@@ -2755,6 +3201,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2755
3201
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2756
3202
|
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
2757
3203
|
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
3204
|
+
const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3205
|
+
{
|
|
3206
|
+
name: rootRepo.name,
|
|
3207
|
+
repoPath: rootRepo.path,
|
|
3208
|
+
workflow: "verify.yml",
|
|
3209
|
+
branch: STAGING_BRANCH,
|
|
3210
|
+
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3211
|
+
},
|
|
3212
|
+
{
|
|
3213
|
+
name: rootRepo.name,
|
|
3214
|
+
repoPath: rootRepo.path,
|
|
3215
|
+
workflow: "deploy.yml",
|
|
3216
|
+
branch: STAGING_BRANCH,
|
|
3217
|
+
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
name: rootRepo.name,
|
|
3221
|
+
repoPath: rootRepo.path,
|
|
3222
|
+
workflow: "verify.yml",
|
|
3223
|
+
branch: rootVersion,
|
|
3224
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
name: rootRepo.name,
|
|
3228
|
+
repoPath: rootRepo.path,
|
|
3229
|
+
workflow: "verify.yml",
|
|
3230
|
+
branch: PRODUCTION_BRANCH,
|
|
3231
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
name: rootRepo.name,
|
|
3235
|
+
repoPath: rootRepo.path,
|
|
3236
|
+
workflow: "deploy.yml",
|
|
3237
|
+
branch: PRODUCTION_BRANCH,
|
|
3238
|
+
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3239
|
+
}
|
|
3240
|
+
].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
|
|
2758
3241
|
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2759
3242
|
const payload2 = {
|
|
2760
3243
|
mode,
|
|
@@ -2775,7 +3258,10 @@ async function workflowRelease(helpers, input) {
|
|
|
2775
3258
|
rootRepo,
|
|
2776
3259
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
2777
3260
|
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
2778
|
-
workspaceLinks: workspaceLinks2
|
|
3261
|
+
workspaceLinks: workspaceLinks2,
|
|
3262
|
+
ciMode,
|
|
3263
|
+
workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
|
|
3264
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2779
3265
|
};
|
|
2780
3266
|
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
2781
3267
|
return buildWorkflowResult("release", root, payload2, {
|
|
@@ -2843,18 +3329,38 @@ async function workflowRelease(helpers, input) {
|
|
|
2843
3329
|
});
|
|
2844
3330
|
const tagName = String(effectiveVersions.get(pkg.name));
|
|
2845
3331
|
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3332
|
+
const workflowGates = await waitForWorkflowGates("release", [
|
|
3333
|
+
{
|
|
3334
|
+
name: pkg.name,
|
|
3335
|
+
repoPath: pkg.dir,
|
|
3336
|
+
workflow: "publish.yml",
|
|
3337
|
+
headSha: mergeResult.commitSha,
|
|
3338
|
+
branch: tagName
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
name: pkg.name,
|
|
3342
|
+
repoPath: pkg.dir,
|
|
3343
|
+
workflow: "verify.yml",
|
|
3344
|
+
headSha: mergeResult.commitSha,
|
|
3345
|
+
branch: tagName
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
name: pkg.name,
|
|
3349
|
+
repoPath: pkg.dir,
|
|
3350
|
+
workflow: "verify.yml",
|
|
3351
|
+
headSha: mergeResult.commitSha,
|
|
3352
|
+
branch: PRODUCTION_BRANCH
|
|
3353
|
+
}
|
|
3354
|
+
], ciMode, { root, runId: workflowRun.runId });
|
|
3355
|
+
const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
|
|
2851
3356
|
assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
|
|
2852
3357
|
syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
|
|
2853
3358
|
return {
|
|
2854
3359
|
commitSha: mergeResult.commitSha,
|
|
2855
3360
|
tagName,
|
|
2856
3361
|
tag,
|
|
2857
|
-
publish
|
|
3362
|
+
publish,
|
|
3363
|
+
workflowGates
|
|
2858
3364
|
};
|
|
2859
3365
|
});
|
|
2860
3366
|
report.committed = true;
|
|
@@ -2863,6 +3369,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2863
3369
|
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
2864
3370
|
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
2865
3371
|
report.publishWait = releasedPackage?.publish ?? null;
|
|
3372
|
+
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
2866
3373
|
report.branch = STAGING_BRANCH;
|
|
2867
3374
|
publishWait.push({
|
|
2868
3375
|
name: report.name,
|
|
@@ -2875,12 +3382,22 @@ async function workflowRelease(helpers, input) {
|
|
|
2875
3382
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2876
3383
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2877
3384
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
2878
|
-
const
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3385
|
+
const stagingCommit = headCommit(gitRoot);
|
|
3386
|
+
let released;
|
|
3387
|
+
try {
|
|
3388
|
+
released = mergeBranchIntoTarget(root, {
|
|
3389
|
+
sourceBranch: STAGING_BRANCH,
|
|
3390
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
3391
|
+
message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3392
|
+
pushTarget: false
|
|
3393
|
+
});
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
if (!resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames)) {
|
|
3396
|
+
throw error;
|
|
3397
|
+
}
|
|
3398
|
+
commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
|
|
3399
|
+
released = { commitSha: headCommit(gitRoot) };
|
|
3400
|
+
}
|
|
2884
3401
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2885
3402
|
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
2886
3403
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
@@ -2894,6 +3411,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2894
3411
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2895
3412
|
return {
|
|
2896
3413
|
rootVersion,
|
|
3414
|
+
stagingCommit,
|
|
2897
3415
|
releasedCommit,
|
|
2898
3416
|
mergeCommit: released.commitSha,
|
|
2899
3417
|
tag
|
|
@@ -2905,6 +3423,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2905
3423
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2906
3424
|
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
2907
3425
|
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
3426
|
+
const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3427
|
+
{
|
|
3428
|
+
name: rootRepo.name,
|
|
3429
|
+
repoPath: rootRepo.path,
|
|
3430
|
+
workflow: "verify.yml",
|
|
3431
|
+
branch: STAGING_BRANCH,
|
|
3432
|
+
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3433
|
+
},
|
|
3434
|
+
{
|
|
3435
|
+
name: rootRepo.name,
|
|
3436
|
+
repoPath: rootRepo.path,
|
|
3437
|
+
workflow: "deploy.yml",
|
|
3438
|
+
branch: STAGING_BRANCH,
|
|
3439
|
+
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3440
|
+
},
|
|
3441
|
+
{
|
|
3442
|
+
name: rootRepo.name,
|
|
3443
|
+
repoPath: rootRepo.path,
|
|
3444
|
+
workflow: "verify.yml",
|
|
3445
|
+
branch: rootVersion,
|
|
3446
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
name: rootRepo.name,
|
|
3450
|
+
repoPath: rootRepo.path,
|
|
3451
|
+
workflow: "verify.yml",
|
|
3452
|
+
branch: PRODUCTION_BRANCH,
|
|
3453
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
name: rootRepo.name,
|
|
3457
|
+
repoPath: rootRepo.path,
|
|
3458
|
+
workflow: "deploy.yml",
|
|
3459
|
+
branch: PRODUCTION_BRANCH,
|
|
3460
|
+
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3461
|
+
}
|
|
3462
|
+
].filter((gate) => gate.headSha), ciMode, { root, runId: workflowRun.runId }).then((workflowGates) => ({ workflowGates })));
|
|
2908
3463
|
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
2909
3464
|
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
3465
|
const activeDevTags = collectActiveDevTagReferences(root);
|
|
@@ -2956,7 +3511,13 @@ async function workflowRelease(helpers, input) {
|
|
|
2956
3511
|
productionPushed: true,
|
|
2957
3512
|
tagPushed: true
|
|
2958
3513
|
},
|
|
2959
|
-
workspaceLinks
|
|
3514
|
+
workspaceLinks,
|
|
3515
|
+
ciMode,
|
|
3516
|
+
workflowGates: [
|
|
3517
|
+
...packageReports.flatMap((report) => report.workflowGates),
|
|
3518
|
+
...Array.isArray(rootWorkflowGateResult?.workflowGates) ? rootWorkflowGateResult.workflowGates : []
|
|
3519
|
+
],
|
|
3520
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2960
3521
|
};
|
|
2961
3522
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2962
3523
|
return buildWorkflowResult("release", root, payload, {
|
|
@@ -3006,7 +3567,21 @@ async function workflowResume(helpers, input) {
|
|
|
3006
3567
|
details: { runId, status: journal.status }
|
|
3007
3568
|
});
|
|
3008
3569
|
}
|
|
3009
|
-
const
|
|
3570
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3571
|
+
const currentHeads = Object.fromEntries(
|
|
3572
|
+
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
3573
|
+
);
|
|
3574
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
3575
|
+
currentBranch: session.branchName,
|
|
3576
|
+
currentHeads
|
|
3577
|
+
});
|
|
3578
|
+
if (classification.state !== "resumable") {
|
|
3579
|
+
workflowError("resume", "resume_unavailable", `Run ${runId} is ${classification.state} and is not safe to resume.`, {
|
|
3580
|
+
details: { runId, status: journal.status, classification }
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
const resumeRoot = typeof journal.session?.root === "string" && existsSync(journal.session.root) ? journal.session.root : root;
|
|
3584
|
+
const resumedHelpers = helpersForCwd({
|
|
3010
3585
|
...helpers,
|
|
3011
3586
|
context: {
|
|
3012
3587
|
...helpers.context,
|
|
@@ -3015,7 +3590,7 @@ async function workflowResume(helpers, input) {
|
|
|
3015
3590
|
resumeRunId: runId
|
|
3016
3591
|
}
|
|
3017
3592
|
}
|
|
3018
|
-
};
|
|
3593
|
+
}, resumeRoot);
|
|
3019
3594
|
switch (journal.command) {
|
|
3020
3595
|
case "switch":
|
|
3021
3596
|
return workflowSwitch(resumedHelpers, journal.input);
|
|
@@ -3045,7 +3620,15 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3045
3620
|
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
3046
3621
|
const lock = inspectWorkflowLock(root);
|
|
3047
3622
|
const journals = listWorkflowRunJournals(root);
|
|
3048
|
-
const
|
|
3623
|
+
const session = resolveTreeseedWorkflowSession(root);
|
|
3624
|
+
const currentHeads = Object.fromEntries(
|
|
3625
|
+
[createWorkspaceRootRepoReport(root), ...createWorkspacePackageReports(root)].map((report) => [report.name, report.commitSha ?? null])
|
|
3626
|
+
);
|
|
3627
|
+
const classifiedRuns = classifyWorkflowRunJournals(root, {
|
|
3628
|
+
currentBranch: session.branchName,
|
|
3629
|
+
currentHeads
|
|
3630
|
+
});
|
|
3631
|
+
const interruptedRuns = classifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
3049
3632
|
runId: journal.runId,
|
|
3050
3633
|
command: journal.command,
|
|
3051
3634
|
status: journal.status,
|
|
@@ -3055,6 +3638,29 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3055
3638
|
failure: journal.failure,
|
|
3056
3639
|
resumeCommand: `treeseed resume ${journal.runId}`
|
|
3057
3640
|
}));
|
|
3641
|
+
const staleRuns = classifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
|
|
3642
|
+
runId: journal.runId,
|
|
3643
|
+
command: journal.command,
|
|
3644
|
+
status: journal.status,
|
|
3645
|
+
createdAt: journal.createdAt,
|
|
3646
|
+
updatedAt: journal.updatedAt,
|
|
3647
|
+
nextStep: nextPendingJournalStep(journal)?.description ?? null,
|
|
3648
|
+
failure: journal.failure,
|
|
3649
|
+
classification
|
|
3650
|
+
}));
|
|
3651
|
+
const obsoleteRuns = classifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
|
|
3652
|
+
runId: journal.runId,
|
|
3653
|
+
command: journal.command,
|
|
3654
|
+
status: journal.status,
|
|
3655
|
+
createdAt: journal.createdAt,
|
|
3656
|
+
updatedAt: journal.updatedAt,
|
|
3657
|
+
failure: journal.failure,
|
|
3658
|
+
classification
|
|
3659
|
+
}));
|
|
3660
|
+
const prunedRuns = input.pruneStale === true ? staleRuns.map((run2) => {
|
|
3661
|
+
archiveWorkflowRun(root, run2.runId, run2.classification);
|
|
3662
|
+
return run2;
|
|
3663
|
+
}) : [];
|
|
3058
3664
|
const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
|
|
3059
3665
|
return buildWorkflowResult(
|
|
3060
3666
|
"recover",
|
|
@@ -3062,13 +3668,16 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3062
3668
|
{
|
|
3063
3669
|
lock,
|
|
3064
3670
|
interruptedRuns,
|
|
3671
|
+
staleRuns,
|
|
3672
|
+
obsoleteRuns,
|
|
3673
|
+
prunedRuns,
|
|
3065
3674
|
selectedRun,
|
|
3066
3675
|
runCount: journals.length
|
|
3067
3676
|
},
|
|
3068
3677
|
{
|
|
3069
3678
|
includeFinalState: false,
|
|
3070
3679
|
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." }]
|
|
3680
|
+
...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
3681
|
])
|
|
3073
3682
|
}
|
|
3074
3683
|
);
|