@treeseed/sdk 0.6.8 → 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 +25 -6
- 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/package-reference-policy.js +19 -3
- 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/scripts/prepare.js +14 -0
- package/dist/verification.js +22 -1
- package/dist/workflow/operations.d.ts +259 -429
- package/dist/workflow/operations.js +662 -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 +2 -2
- package/templates/github/deploy.workflow.yml +135 -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
|
}
|
|
@@ -752,20 +983,24 @@ function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
|
|
|
752
983
|
if (journal.command !== "release" || !journal.resumable || journal.session.branchName !== STAGING_BRANCH) {
|
|
753
984
|
return false;
|
|
754
985
|
}
|
|
986
|
+
const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
|
|
987
|
+
const nextStep = nextPendingJournalStep(journal);
|
|
755
988
|
if (releaseRunHasCompletedMutation(journal)) {
|
|
989
|
+
if (nextStep?.id === "release-root" && releasePlanHead(releasePlan ?? {}, rootRepo.name) !== rootRepo.commitSha) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
756
992
|
return true;
|
|
757
993
|
}
|
|
758
|
-
const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
|
|
759
994
|
return releasePlan ? releasePlanMatchesCurrentHeads(releasePlan, rootRepo, packageReports) : true;
|
|
760
995
|
}) ?? null;
|
|
761
996
|
}
|
|
762
|
-
async function executeJournalStep(root, runId, stepId, action) {
|
|
997
|
+
async function executeJournalStep(root, runId, stepId, action, options = {}) {
|
|
763
998
|
const current = readWorkflowRunJournal(root, runId);
|
|
764
999
|
const step = current?.steps.find((entry) => entry.id === stepId) ?? null;
|
|
765
1000
|
if (!current || !step) {
|
|
766
1001
|
throw new Error(`Unknown workflow step "${stepId}" for run ${runId}.`);
|
|
767
1002
|
}
|
|
768
|
-
if (step.status === "completed") {
|
|
1003
|
+
if (step.status === "completed" && !options.rerunCompleted) {
|
|
769
1004
|
return step.data ?? null;
|
|
770
1005
|
}
|
|
771
1006
|
const data = await Promise.resolve(action());
|
|
@@ -918,7 +1153,7 @@ function runReleaseNpmInstall(repoDir, options = {}) {
|
|
|
918
1153
|
if (shouldSkipReleaseInstall()) {
|
|
919
1154
|
return { status: "skipped", reason: "stubbed" };
|
|
920
1155
|
}
|
|
921
|
-
const args = repoDir === options.workspaceRoot ? ["install"] : ["install", "--workspaces=false"];
|
|
1156
|
+
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
|
|
922
1157
|
run("npm", args, { cwd: repoDir });
|
|
923
1158
|
return { status: "completed", reason: null };
|
|
924
1159
|
}
|
|
@@ -976,7 +1211,7 @@ function buildReleasePlanSnapshot(input) {
|
|
|
976
1211
|
plannedPublishWaits: input.packageSelection.selected.map((name) => ({
|
|
977
1212
|
name,
|
|
978
1213
|
workflow: "publish.yml",
|
|
979
|
-
branch: PRODUCTION_BRANCH,
|
|
1214
|
+
branch: String(plannedVersions[name] ?? PRODUCTION_BRANCH),
|
|
980
1215
|
status: "planned"
|
|
981
1216
|
})),
|
|
982
1217
|
touchedPackages: input.packageSelection.selected,
|
|
@@ -1029,7 +1264,7 @@ function assertReleaseGitHubAutomationReady(root, selectedPackageNames) {
|
|
|
1029
1264
|
if (process.env.TREESEED_GITHUB_AUTOMATION_MODE === "stub") {
|
|
1030
1265
|
return;
|
|
1031
1266
|
}
|
|
1032
|
-
|
|
1267
|
+
assertHostedGitHubWorkflowAuthReady("release", root);
|
|
1033
1268
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
1034
1269
|
if (!selectedPackageNames.has(pkg.name)) continue;
|
|
1035
1270
|
resolveGitHubRepositorySlug(pkg.dir);
|
|
@@ -1097,6 +1332,14 @@ function previewStateFor(tenantRoot, branchName) {
|
|
|
1097
1332
|
target: createBranchPreviewDeployTarget(branchName)
|
|
1098
1333
|
});
|
|
1099
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
|
+
}
|
|
1100
1343
|
async function deployBranchPreview(tenantRoot, branchName, context, { initialize }) {
|
|
1101
1344
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging", override: true });
|
|
1102
1345
|
assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
|
|
@@ -1706,7 +1949,10 @@ async function workflowExport(helpers, input = {}) {
|
|
|
1706
1949
|
return await withContextEnv(helpers.context.env, async () => {
|
|
1707
1950
|
const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
|
|
1708
1951
|
const exported = await exportTreeseedCodebase({ directory });
|
|
1709
|
-
return buildWorkflowResult("export", exported.tenantRoot,
|
|
1952
|
+
return buildWorkflowResult("export", exported.tenantRoot, {
|
|
1953
|
+
...exported,
|
|
1954
|
+
...worktreePayload(exported.tenantRoot, input.worktreeMode)
|
|
1955
|
+
});
|
|
1710
1956
|
});
|
|
1711
1957
|
}
|
|
1712
1958
|
async function workflowSwitch(helpers, input) {
|
|
@@ -1721,6 +1967,27 @@ async function workflowSwitch(helpers, input) {
|
|
|
1721
1967
|
}
|
|
1722
1968
|
const preview = input.preview === true;
|
|
1723
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
|
+
}
|
|
1724
1991
|
const mode = session.mode;
|
|
1725
1992
|
const repoDir = session.gitRoot;
|
|
1726
1993
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
@@ -1743,6 +2010,8 @@ async function workflowSwitch(helpers, input) {
|
|
|
1743
2010
|
rootRepo,
|
|
1744
2011
|
repos: packageReports,
|
|
1745
2012
|
previewRequested: preview,
|
|
2013
|
+
worktreeMode: input.worktreeMode ?? "auto",
|
|
2014
|
+
worktreePath: effectiveWorkflowWorktreeMode(input.worktreeMode, helpers.context.env) === "on" ? plannedManagedWorkflowWorktreePath(root, branchName) : null,
|
|
1746
2015
|
blockers: dirtyRepos.length > 0 ? [`Clean worktrees required: ${dirtyRepos.join(", ")}`] : [],
|
|
1747
2016
|
plannedSteps: [
|
|
1748
2017
|
{ id: "switch-root", description: `Switch market repo to ${branchName}` },
|
|
@@ -1768,7 +2037,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1768
2037
|
const workflowRun = acquireWorkflowRun(
|
|
1769
2038
|
"switch",
|
|
1770
2039
|
session,
|
|
1771
|
-
{ branch: branchName, preview },
|
|
2040
|
+
{ branch: branchName, preview, worktreeMode: input.worktreeMode ?? "auto" },
|
|
1772
2041
|
[
|
|
1773
2042
|
{ id: "switch-root", description: `Switch market repo to ${branchName}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: branchName, resumable: true },
|
|
1774
2043
|
...packageReports.map((report) => ({
|
|
@@ -1845,6 +2114,7 @@ async function workflowSwitch(helpers, input) {
|
|
|
1845
2114
|
},
|
|
1846
2115
|
previewResult,
|
|
1847
2116
|
workspaceLinks,
|
|
2117
|
+
...worktreePayload(root, input.worktreeMode),
|
|
1848
2118
|
preconditions: {
|
|
1849
2119
|
cleanWorktreeRequired: true,
|
|
1850
2120
|
baseBranch: STAGING_BRANCH
|
|
@@ -1971,6 +2241,7 @@ async function workflowSave(helpers, input) {
|
|
|
1971
2241
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
1972
2242
|
const message = String(effectiveInput.message ?? "").trim();
|
|
1973
2243
|
const optionsHotfix = effectiveInput.hotfix === true;
|
|
2244
|
+
const previewInitialized = branchPreviewInitialized(root, branch);
|
|
1974
2245
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope, override: true });
|
|
1975
2246
|
if (!branch) {
|
|
1976
2247
|
workflowError("save", "validation_failed", "Treeseed save requires an active git branch.");
|
|
@@ -1998,7 +2269,7 @@ async function workflowSave(helpers, input) {
|
|
|
1998
2269
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
1999
2270
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2000
2271
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2001
|
-
verifyMode:
|
|
2272
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2002
2273
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto"
|
|
2003
2274
|
});
|
|
2004
2275
|
const workspaceLinks = inspectWorkspaceDependencyMode(root, { mode: effectiveInput.workspaceLinks ?? "auto", env: helpers.context.env });
|
|
@@ -2020,6 +2291,9 @@ async function workflowSave(helpers, input) {
|
|
|
2020
2291
|
failure: planAutoResumeRun.failure
|
|
2021
2292
|
} : null,
|
|
2022
2293
|
workspaceLinks,
|
|
2294
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2295
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2296
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2023
2297
|
repositoryPlan,
|
|
2024
2298
|
waves: repositoryPlan.waves,
|
|
2025
2299
|
plannedVersions: repositoryPlan.plannedVersions,
|
|
@@ -2028,7 +2302,7 @@ async function workflowSave(helpers, input) {
|
|
|
2028
2302
|
...repositoryPlan.plannedSteps,
|
|
2029
2303
|
{ id: "lockfile-validation", description: "Validate refreshed package-lock.json files before any save commit is pushed" },
|
|
2030
2304
|
{ id: "workspace-link", description: "Restore local workspace links after save" },
|
|
2031
|
-
...beforeState.branchRole === "feature" && (effectiveInput.preview === true ||
|
|
2305
|
+
...beforeState.branchRole === "feature" && (effectiveInput.preview === true || previewInitialized) ? [{ id: "preview", description: `Refresh preview deployment for ${branch}` }] : []
|
|
2032
2306
|
]
|
|
2033
2307
|
},
|
|
2034
2308
|
{
|
|
@@ -2061,7 +2335,9 @@ async function workflowSave(helpers, input) {
|
|
|
2061
2335
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2062
2336
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2063
2337
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2064
|
-
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",
|
|
2065
2341
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2066
2342
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2067
2343
|
},
|
|
@@ -2074,7 +2350,15 @@ async function workflowSave(helpers, input) {
|
|
|
2074
2350
|
branch,
|
|
2075
2351
|
resumable: true
|
|
2076
2352
|
},
|
|
2077
|
-
...
|
|
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) ? [{
|
|
2078
2362
|
id: "preview",
|
|
2079
2363
|
description: `Refresh preview ${branch}`,
|
|
2080
2364
|
repoName: rootRepo.name,
|
|
@@ -2108,7 +2392,7 @@ async function workflowSave(helpers, input) {
|
|
|
2108
2392
|
devDependencyReferenceMode: effectiveInput.devDependencyReferenceMode ?? "git-tag",
|
|
2109
2393
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2110
2394
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
2111
|
-
verifyMode:
|
|
2395
|
+
verifyMode: normalizeSaveVerifyMode(effectiveInput.verify === false ? "skip" : effectiveInput.verifyMode),
|
|
2112
2396
|
commitMessageMode: effectiveInput.commitMessageMode ?? "auto",
|
|
2113
2397
|
workflowRunId: workflowRun.runId,
|
|
2114
2398
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
@@ -2135,14 +2419,30 @@ async function workflowSave(helpers, input) {
|
|
|
2135
2419
|
lockfileValidation: repo.lockfileValidation
|
|
2136
2420
|
}))
|
|
2137
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: [] };
|
|
2138
2438
|
let previewAction = { status: "skipped" };
|
|
2139
2439
|
if (beforeState.branchRole === "feature" && branch) {
|
|
2140
2440
|
if (effectiveInput.preview === true) {
|
|
2141
2441
|
previewAction = {
|
|
2142
|
-
status:
|
|
2143
|
-
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 }))
|
|
2144
2444
|
};
|
|
2145
|
-
} else if (effectiveInput.refreshPreview !== false &&
|
|
2445
|
+
} else if (effectiveInput.refreshPreview !== false && previewInitialized) {
|
|
2146
2446
|
previewAction = {
|
|
2147
2447
|
status: "refreshed",
|
|
2148
2448
|
details: await executeJournalStep(root, workflowRun.runId, "preview", () => deployBranchPreview(root, branch, helpers.context, { initialize: false }))
|
|
@@ -2171,7 +2471,11 @@ async function workflowSave(helpers, input) {
|
|
|
2171
2471
|
mergeConflict: null,
|
|
2172
2472
|
workspaceLinks,
|
|
2173
2473
|
commandReadiness,
|
|
2174
|
-
lockfileValidation
|
|
2474
|
+
lockfileValidation,
|
|
2475
|
+
ciMode: normalizeCiMode(effectiveInput.ciMode, "save"),
|
|
2476
|
+
verifyMode: effectiveInput.verifyMode ?? "fast",
|
|
2477
|
+
workflowGates: saveWorkflowGates?.workflowGates ?? [],
|
|
2478
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2175
2479
|
};
|
|
2176
2480
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2177
2481
|
return buildWorkflowResult(
|
|
@@ -2228,9 +2532,13 @@ async function workflowClose(helpers, input) {
|
|
|
2228
2532
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2229
2533
|
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
2230
2534
|
const root = workspaceRoot(tenantRoot);
|
|
2231
|
-
const message = ensureMessage("close", input.message, "a close reason");
|
|
2232
2535
|
const executionMode = normalizeExecutionMode(input);
|
|
2233
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");
|
|
2234
2542
|
if (executionMode === "plan") {
|
|
2235
2543
|
const branchName = session.branchName;
|
|
2236
2544
|
const blockers = session.branchRole !== "feature" ? ["Close only applies to task branches."] : [];
|
|
@@ -2241,18 +2549,26 @@ async function workflowClose(helpers, input) {
|
|
|
2241
2549
|
mode: session.mode,
|
|
2242
2550
|
branchName,
|
|
2243
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),
|
|
2244
2558
|
autoSaveRequired: session.rootRepo.dirty || session.packageRepos.some((repo) => repo.dirty),
|
|
2245
2559
|
repos: createWorkspacePackageReports(root),
|
|
2246
2560
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
2247
2561
|
blockers,
|
|
2248
2562
|
plannedSteps: [
|
|
2563
|
+
{ id: "workspace-unlink", description: "Remove local workspace links before task cleanup" },
|
|
2249
2564
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${branchName ?? "(current task)"}` },
|
|
2250
2565
|
{ id: "cleanup-root", description: `Archive and delete ${branchName ?? "(current task)"} in market` },
|
|
2251
2566
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
2252
2567
|
id: `cleanup-${pkg.name}`,
|
|
2253
2568
|
description: `Archive and delete ${branchName ?? "(current task)"} in ${pkg.name}`
|
|
2254
2569
|
})),
|
|
2255
|
-
{ 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" }] : []
|
|
2256
2572
|
]
|
|
2257
2573
|
},
|
|
2258
2574
|
{
|
|
@@ -2263,12 +2579,10 @@ async function workflowClose(helpers, input) {
|
|
|
2263
2579
|
}
|
|
2264
2580
|
);
|
|
2265
2581
|
}
|
|
2266
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2267
2582
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
2268
2583
|
message,
|
|
2269
|
-
autoSave:
|
|
2584
|
+
autoSave: effectiveInput.autoSave
|
|
2270
2585
|
});
|
|
2271
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2272
2586
|
const activeSession = resolveTreeseedWorkflowSession(root);
|
|
2273
2587
|
const featureBranch = assertFeatureBranch(root);
|
|
2274
2588
|
const mode = activeSession.mode;
|
|
@@ -2284,8 +2598,14 @@ async function workflowClose(helpers, input) {
|
|
|
2284
2598
|
const workflowRun = acquireWorkflowRun(
|
|
2285
2599
|
"close",
|
|
2286
2600
|
activeSession,
|
|
2287
|
-
{
|
|
2601
|
+
{
|
|
2602
|
+
message,
|
|
2603
|
+
deletePreview: effectiveInput.deletePreview !== false,
|
|
2604
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2605
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto"
|
|
2606
|
+
},
|
|
2288
2607
|
[
|
|
2608
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2289
2609
|
{ id: "preview-cleanup", description: `Destroy preview resources for ${featureBranch}`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2290
2610
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2291
2611
|
...packageReports.map((report) => ({
|
|
@@ -2295,23 +2615,35 @@ async function workflowClose(helpers, input) {
|
|
|
2295
2615
|
repoPath: report.path,
|
|
2296
2616
|
branch: featureBranch,
|
|
2297
2617
|
resumable: true
|
|
2298
|
-
}))
|
|
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 }] : []
|
|
2299
2621
|
],
|
|
2300
|
-
|
|
2622
|
+
autoResumeRun ? {
|
|
2623
|
+
...helpers.context,
|
|
2624
|
+
workflow: {
|
|
2625
|
+
...helpers.context.workflow ?? {},
|
|
2626
|
+
resumeRunId: autoResumeRun.runId
|
|
2627
|
+
}
|
|
2628
|
+
} : helpers.context
|
|
2301
2629
|
);
|
|
2630
|
+
if (autoResumeRun) {
|
|
2631
|
+
helpers.write(`[workflow][resume] Resuming interrupted close ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2632
|
+
}
|
|
2302
2633
|
try {
|
|
2303
|
-
|
|
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));
|
|
2304
2636
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2305
2637
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `close: ${message}`);
|
|
2306
|
-
const deletedRemote =
|
|
2638
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2307
2639
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2308
|
-
if (
|
|
2640
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2309
2641
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2310
2642
|
}
|
|
2311
2643
|
return {
|
|
2312
2644
|
deprecatedTag,
|
|
2313
2645
|
deletedRemote,
|
|
2314
|
-
deletedLocal:
|
|
2646
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2315
2647
|
branch: currentBranch(repoDir) || STAGING_BRANCH,
|
|
2316
2648
|
dirty: hasMeaningfulChanges(repoDir)
|
|
2317
2649
|
};
|
|
@@ -2328,12 +2660,15 @@ async function workflowClose(helpers, input) {
|
|
|
2328
2660
|
continue;
|
|
2329
2661
|
}
|
|
2330
2662
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `close: ${message}`, {
|
|
2331
|
-
deleteBranch:
|
|
2663
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2332
2664
|
targetBranch: STAGING_BRANCH
|
|
2333
2665
|
}));
|
|
2334
2666
|
Object.assign(report, cleanup);
|
|
2335
2667
|
}
|
|
2336
|
-
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" };
|
|
2337
2672
|
const payload = {
|
|
2338
2673
|
mode,
|
|
2339
2674
|
branchName: featureBranch,
|
|
@@ -2346,8 +2681,13 @@ async function workflowClose(helpers, input) {
|
|
|
2346
2681
|
previewCleanup,
|
|
2347
2682
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2348
2683
|
localDeleted: rootRepo.deletedLocal,
|
|
2349
|
-
finalBranch
|
|
2350
|
-
workspaceLinks
|
|
2684
|
+
finalBranch,
|
|
2685
|
+
workspaceLinks,
|
|
2686
|
+
worktreeCleanup,
|
|
2687
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2688
|
+
managedWorktree,
|
|
2689
|
+
worktreePath: managedWorktree?.worktreePath ?? null,
|
|
2690
|
+
primaryRoot: managedWorktree?.primaryRoot ?? null
|
|
2351
2691
|
};
|
|
2352
2692
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2353
2693
|
return buildWorkflowResult(
|
|
@@ -2362,7 +2702,7 @@ async function workflowClose(helpers, input) {
|
|
|
2362
2702
|
}
|
|
2363
2703
|
);
|
|
2364
2704
|
} catch (error) {
|
|
2365
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
2705
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2366
2706
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2367
2707
|
resumable: true,
|
|
2368
2708
|
runId: workflowRun.runId,
|
|
@@ -2383,11 +2723,19 @@ async function workflowStage(helpers, input) {
|
|
|
2383
2723
|
return await withContextEnv(helpers.context.env, async () => {
|
|
2384
2724
|
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
2385
2725
|
const root = workspaceRoot(tenantRoot);
|
|
2386
|
-
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
2387
2726
|
const executionMode = normalizeExecutionMode(input);
|
|
2388
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");
|
|
2389
2734
|
if (executionMode === "plan") {
|
|
2390
2735
|
const blockers = [];
|
|
2736
|
+
if (initialSession.branchRole !== "feature") {
|
|
2737
|
+
blockers.push("Stage only applies to task branches.");
|
|
2738
|
+
}
|
|
2391
2739
|
try {
|
|
2392
2740
|
validateStagingWorkflowContracts(root);
|
|
2393
2741
|
} catch (error) {
|
|
@@ -2402,6 +2750,13 @@ async function workflowStage(helpers, input) {
|
|
|
2402
2750
|
mergeTarget: STAGING_BRANCH,
|
|
2403
2751
|
mergeStrategy: "squash",
|
|
2404
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),
|
|
2405
2760
|
autoSaveRequired: initialSession.rootRepo.dirty || initialSession.packageRepos.some((repo) => repo.dirty),
|
|
2406
2761
|
blockers,
|
|
2407
2762
|
rootRepo: createWorkspaceRootRepoReport(root),
|
|
@@ -2414,7 +2769,7 @@ async function workflowStage(helpers, input) {
|
|
|
2414
2769
|
{ id: "workspace-unlink", description: "Remove local workspace links before staging promotion" },
|
|
2415
2770
|
{ id: "merge-root", description: `Squash-merge ${initialSession.branchName ?? "(current task)"} into market staging` },
|
|
2416
2771
|
{ id: "lockfile-validation", description: "Refresh and validate the merged root workspace lockfile before pushing staging" },
|
|
2417
|
-
{ id: "wait-staging", description: "Wait for staging
|
|
2772
|
+
{ id: "wait-staging", description: "Wait for exact-SHA staging GitHub Actions gates" },
|
|
2418
2773
|
{ id: "preview-cleanup", description: "Destroy preview resources" },
|
|
2419
2774
|
{ id: "cleanup-root", description: "Archive and delete the task branch from market" },
|
|
2420
2775
|
...checkedOutWorkspacePackageRepos(root).map((pkg) => ({
|
|
@@ -2432,12 +2787,10 @@ async function workflowStage(helpers, input) {
|
|
|
2432
2787
|
}
|
|
2433
2788
|
);
|
|
2434
2789
|
}
|
|
2435
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2436
2790
|
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
2437
2791
|
message,
|
|
2438
|
-
autoSave:
|
|
2792
|
+
autoSave: effectiveInput.autoSave
|
|
2439
2793
|
});
|
|
2440
|
-
unlinkWorkflowWorkspaceLinks(root, helpers, input.workspaceLinks ?? "auto");
|
|
2441
2794
|
const session = resolveTreeseedWorkflowSession(root);
|
|
2442
2795
|
const featureBranch = assertFeatureBranch(root);
|
|
2443
2796
|
const mode = session.mode;
|
|
@@ -2447,6 +2800,8 @@ async function workflowStage(helpers, input) {
|
|
|
2447
2800
|
} else {
|
|
2448
2801
|
assertCleanWorktree(root);
|
|
2449
2802
|
}
|
|
2803
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2804
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
2450
2805
|
validateStagingWorkflowContracts(root);
|
|
2451
2806
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2452
2807
|
const repoDir = session.gitRoot;
|
|
@@ -2455,8 +2810,16 @@ async function workflowStage(helpers, input) {
|
|
|
2455
2810
|
const workflowRun = acquireWorkflowRun(
|
|
2456
2811
|
"stage",
|
|
2457
2812
|
session,
|
|
2458
|
-
{
|
|
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
|
+
},
|
|
2459
2821
|
[
|
|
2822
|
+
{ id: "workspace-unlink", description: "Remove local workspace links", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2460
2823
|
...packageReports.map((report) => ({
|
|
2461
2824
|
id: `merge-${report.name}`,
|
|
2462
2825
|
description: `Merge ${featureBranch} into ${report.name} staging`,
|
|
@@ -2466,7 +2829,7 @@ async function workflowStage(helpers, input) {
|
|
|
2466
2829
|
resumable: true
|
|
2467
2830
|
})),
|
|
2468
2831
|
{ id: "merge-root", description: `Merge ${featureBranch} into market staging`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2469
|
-
{ 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 },
|
|
2470
2833
|
{ id: "preview-cleanup", description: "Destroy preview resources", repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2471
2834
|
{ id: "cleanup-root", description: `Archive ${featureBranch} in market`, repoName: rootRepo.name, repoPath: rootRepo.path, branch: featureBranch, resumable: true },
|
|
2472
2835
|
...packageReports.map((report) => ({
|
|
@@ -2476,12 +2839,23 @@ async function workflowStage(helpers, input) {
|
|
|
2476
2839
|
repoPath: report.path,
|
|
2477
2840
|
branch: featureBranch,
|
|
2478
2841
|
resumable: true
|
|
2479
|
-
}))
|
|
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 }] : []
|
|
2480
2845
|
],
|
|
2481
|
-
|
|
2846
|
+
autoResumeRun ? {
|
|
2847
|
+
...helpers.context,
|
|
2848
|
+
workflow: {
|
|
2849
|
+
...helpers.context.workflow ?? {},
|
|
2850
|
+
resumeRunId: autoResumeRun.runId
|
|
2851
|
+
}
|
|
2852
|
+
} : helpers.context
|
|
2482
2853
|
);
|
|
2854
|
+
if (autoResumeRun) {
|
|
2855
|
+
helpers.write(`[workflow][resume] Resuming interrupted stage ${autoResumeRun.runId} on ${featureBranch}.`);
|
|
2856
|
+
}
|
|
2483
2857
|
try {
|
|
2484
|
-
unlinkWorkflowWorkspaceLinks(root, helpers,
|
|
2858
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
2485
2859
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
2486
2860
|
const report = findReportByName(packageReports, pkg.name);
|
|
2487
2861
|
if (!report) {
|
|
@@ -2513,7 +2887,11 @@ async function workflowStage(helpers, input) {
|
|
|
2513
2887
|
try {
|
|
2514
2888
|
rootMerge = await executeJournalStep(root, workflowRun.runId, "merge-root", async () => {
|
|
2515
2889
|
assertCleanWorktree(root);
|
|
2516
|
-
|
|
2890
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2891
|
+
checkoutDetachedOriginBranch(repoDir, STAGING_BRANCH);
|
|
2892
|
+
} else {
|
|
2893
|
+
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
2894
|
+
}
|
|
2517
2895
|
run("git", ["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
2518
2896
|
if (mode === "recursive-workspace") {
|
|
2519
2897
|
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
@@ -2528,10 +2906,14 @@ async function workflowStage(helpers, input) {
|
|
|
2528
2906
|
run("git", ["add", "-A"], { cwd: repoDir });
|
|
2529
2907
|
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
2530
2908
|
}
|
|
2531
|
-
|
|
2909
|
+
if (isManagedWorkflowWorktree(root)) {
|
|
2910
|
+
pushHeadToBranch(repoDir, STAGING_BRANCH);
|
|
2911
|
+
} else {
|
|
2912
|
+
pushBranch(repoDir, STAGING_BRANCH);
|
|
2913
|
+
}
|
|
2532
2914
|
return {
|
|
2533
2915
|
commitSha: headCommit(repoDir),
|
|
2534
|
-
branch:
|
|
2916
|
+
branch: STAGING_BRANCH,
|
|
2535
2917
|
committed: hasMeaningfulChanges(repoDir) ? false : true,
|
|
2536
2918
|
lockfileValidation: lockfileSafety.lockfileValidation,
|
|
2537
2919
|
lockfileInstall: lockfileSafety.install
|
|
@@ -2549,18 +2931,47 @@ async function workflowStage(helpers, input) {
|
|
|
2549
2931
|
exitCode: 12
|
|
2550
2932
|
});
|
|
2551
2933
|
}
|
|
2552
|
-
const
|
|
2553
|
-
|
|
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));
|
|
2554
2965
|
const rootCleanup = await executeJournalStep(root, workflowRun.runId, "cleanup-root", () => {
|
|
2555
2966
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
2556
|
-
const deletedRemote =
|
|
2557
|
-
if (
|
|
2967
|
+
const deletedRemote = effectiveInput.deleteBranch === false ? false : deleteRemoteBranch(repoDir, featureBranch);
|
|
2968
|
+
if (effectiveInput.deleteBranch !== false) {
|
|
2558
2969
|
deleteLocalBranch(repoDir, featureBranch);
|
|
2559
2970
|
}
|
|
2560
2971
|
return {
|
|
2561
2972
|
deprecatedTag,
|
|
2562
2973
|
deletedRemote,
|
|
2563
|
-
deletedLocal:
|
|
2974
|
+
deletedLocal: effectiveInput.deleteBranch !== false,
|
|
2564
2975
|
branch: currentBranch(repoDir) || STAGING_BRANCH
|
|
2565
2976
|
};
|
|
2566
2977
|
});
|
|
@@ -2575,12 +2986,15 @@ async function workflowStage(helpers, input) {
|
|
|
2575
2986
|
continue;
|
|
2576
2987
|
}
|
|
2577
2988
|
const cleanup = await executeJournalStep(root, workflowRun.runId, `cleanup-${report.name}`, () => cleanupTaskBranchReport(report, featureBranch, `stage: ${message}`, {
|
|
2578
|
-
deleteBranch:
|
|
2989
|
+
deleteBranch: effectiveInput.deleteBranch !== false,
|
|
2579
2990
|
targetBranch: STAGING_BRANCH
|
|
2580
2991
|
}));
|
|
2581
2992
|
Object.assign(report, cleanup);
|
|
2582
2993
|
}
|
|
2583
|
-
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" };
|
|
2584
2998
|
const payload = {
|
|
2585
2999
|
mode,
|
|
2586
3000
|
branchName: featureBranch,
|
|
@@ -2598,8 +3012,15 @@ async function workflowStage(helpers, input) {
|
|
|
2598
3012
|
lockfileInstall: rootMerge?.lockfileInstall ?? null,
|
|
2599
3013
|
remoteDeleted: rootRepo.deletedRemote,
|
|
2600
3014
|
localDeleted: rootRepo.deletedLocal,
|
|
2601
|
-
finalBranch
|
|
2602
|
-
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
|
|
2603
3024
|
};
|
|
2604
3025
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2605
3026
|
return buildWorkflowResult(
|
|
@@ -2615,7 +3036,7 @@ async function workflowStage(helpers, input) {
|
|
|
2615
3036
|
}
|
|
2616
3037
|
);
|
|
2617
3038
|
} catch (error) {
|
|
2618
|
-
ensureWorkflowWorkspaceLinks(root, helpers,
|
|
3039
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2619
3040
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
2620
3041
|
resumable: true,
|
|
2621
3042
|
runId: workflowRun.runId,
|
|
@@ -2646,6 +3067,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2646
3067
|
const planAutoResumeRun = executionMode === "plan" ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
2647
3068
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
2648
3069
|
const level = effectiveInput.bump ?? "patch";
|
|
3070
|
+
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
2649
3071
|
const isResume = Boolean(explicitResumeRunId || autoResumeRun);
|
|
2650
3072
|
const packageSelection = session.packageSelection;
|
|
2651
3073
|
const selectedPackageNames = new Set(packageSelection.selected);
|
|
@@ -2662,6 +3084,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2662
3084
|
if (executionMode === "plan") {
|
|
2663
3085
|
return buildWorkflowResult("release", root, {
|
|
2664
3086
|
...plannedRelease,
|
|
3087
|
+
ciMode,
|
|
3088
|
+
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
2665
3089
|
autoResumeCandidate: planAutoResumeRun ? {
|
|
2666
3090
|
runId: planAutoResumeRun.runId,
|
|
2667
3091
|
branch: planAutoResumeRun.session.branchName,
|
|
@@ -2687,6 +3111,8 @@ async function workflowRelease(helpers, input) {
|
|
|
2687
3111
|
devTagCleanup: effectiveInput.devTagCleanup ?? "safe-after-release",
|
|
2688
3112
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
2689
3113
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3114
|
+
ciMode,
|
|
3115
|
+
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
2690
3116
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
2691
3117
|
},
|
|
2692
3118
|
[
|
|
@@ -2702,6 +3128,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2702
3128
|
resumable: true
|
|
2703
3129
|
})),
|
|
2704
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 },
|
|
2705
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 }] : []
|
|
2706
3133
|
],
|
|
2707
3134
|
autoResumeRun ? {
|
|
@@ -2728,19 +3155,22 @@ async function workflowRelease(helpers, input) {
|
|
|
2728
3155
|
assertCleanWorktree(root);
|
|
2729
3156
|
}
|
|
2730
3157
|
prepareReleaseBranches(root);
|
|
3158
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2731
3159
|
runWorkspaceSavePreflight({ cwd: root });
|
|
2732
|
-
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 });
|
|
2733
3161
|
if (mode === "root-only") {
|
|
2734
3162
|
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
2735
3163
|
setRootPackageJsonVersion(root, rootVersion);
|
|
2736
3164
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2737
3165
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2738
3166
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3167
|
+
const stagingCommit = headCommit(gitRoot);
|
|
2739
3168
|
const released = mergeStagingIntoMain(root);
|
|
2740
3169
|
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha);
|
|
2741
3170
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2742
3171
|
return {
|
|
2743
3172
|
rootVersion,
|
|
3173
|
+
stagingCommit,
|
|
2744
3174
|
releasedCommit: released.commitSha,
|
|
2745
3175
|
tag
|
|
2746
3176
|
};
|
|
@@ -2751,6 +3181,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2751
3181
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2752
3182
|
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
2753
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 })));
|
|
2754
3221
|
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
2755
3222
|
const payload2 = {
|
|
2756
3223
|
mode,
|
|
@@ -2771,7 +3238,10 @@ async function workflowRelease(helpers, input) {
|
|
|
2771
3238
|
rootRepo,
|
|
2772
3239
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
2773
3240
|
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
2774
|
-
workspaceLinks: workspaceLinks2
|
|
3241
|
+
workspaceLinks: workspaceLinks2,
|
|
3242
|
+
ciMode,
|
|
3243
|
+
workflowGates: rootWorkflowGateResult2?.workflowGates ?? [],
|
|
3244
|
+
...worktreePayload(root, effectiveInput.worktreeMode)
|
|
2775
3245
|
};
|
|
2776
3246
|
completeWorkflowRun(root, workflowRun.runId, payload2);
|
|
2777
3247
|
return buildWorkflowResult("release", root, payload2, {
|
|
@@ -2806,7 +3276,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2806
3276
|
replacedDevReferences: replacedDevReferences2,
|
|
2807
3277
|
releaseInstalls: releaseInstalls2
|
|
2808
3278
|
};
|
|
2809
|
-
});
|
|
3279
|
+
}, { rerunCompleted: workflowRun.resumed });
|
|
2810
3280
|
const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
|
|
2811
3281
|
const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
|
|
2812
3282
|
const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
|
|
@@ -2839,18 +3309,38 @@ async function workflowRelease(helpers, input) {
|
|
|
2839
3309
|
});
|
|
2840
3310
|
const tagName = String(effectiveVersions.get(pkg.name));
|
|
2841
3311
|
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha);
|
|
2842
|
-
const
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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;
|
|
2847
3336
|
assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
|
|
2848
3337
|
syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
|
|
2849
3338
|
return {
|
|
2850
3339
|
commitSha: mergeResult.commitSha,
|
|
2851
3340
|
tagName,
|
|
2852
3341
|
tag,
|
|
2853
|
-
publish
|
|
3342
|
+
publish,
|
|
3343
|
+
workflowGates
|
|
2854
3344
|
};
|
|
2855
3345
|
});
|
|
2856
3346
|
report.committed = true;
|
|
@@ -2859,6 +3349,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2859
3349
|
report.tagName = String(releasedPackage?.tagName ?? "");
|
|
2860
3350
|
report.commitSha = String(releasedPackage?.commitSha ?? report.commitSha ?? "");
|
|
2861
3351
|
report.publishWait = releasedPackage?.publish ?? null;
|
|
3352
|
+
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
2862
3353
|
report.branch = STAGING_BRANCH;
|
|
2863
3354
|
publishWait.push({
|
|
2864
3355
|
name: report.name,
|
|
@@ -2871,6 +3362,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2871
3362
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
2872
3363
|
commitAllIfChanged(gitRoot, `release: ${level} bump`);
|
|
2873
3364
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3365
|
+
const stagingCommit = headCommit(gitRoot);
|
|
2874
3366
|
const released = mergeBranchIntoTarget(root, {
|
|
2875
3367
|
sourceBranch: STAGING_BRANCH,
|
|
2876
3368
|
targetBranch: PRODUCTION_BRANCH,
|
|
@@ -2890,6 +3382,7 @@ async function workflowRelease(helpers, input) {
|
|
|
2890
3382
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
2891
3383
|
return {
|
|
2892
3384
|
rootVersion,
|
|
3385
|
+
stagingCommit,
|
|
2893
3386
|
releasedCommit,
|
|
2894
3387
|
mergeCommit: released.commitSha,
|
|
2895
3388
|
tag
|
|
@@ -2901,6 +3394,43 @@ async function workflowRelease(helpers, input) {
|
|
|
2901
3394
|
rootRepo.branch = PRODUCTION_BRANCH;
|
|
2902
3395
|
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
2903
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 })));
|
|
2904
3434
|
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
2905
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", () => {
|
|
2906
3436
|
const activeDevTags = collectActiveDevTagReferences(root);
|
|
@@ -2952,7 +3482,13 @@ async function workflowRelease(helpers, input) {
|
|
|
2952
3482
|
productionPushed: true,
|
|
2953
3483
|
tagPushed: true
|
|
2954
3484
|
},
|
|
2955
|
-
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)
|
|
2956
3492
|
};
|
|
2957
3493
|
completeWorkflowRun(root, workflowRun.runId, payload);
|
|
2958
3494
|
return buildWorkflowResult("release", root, payload, {
|
|
@@ -3002,7 +3538,21 @@ async function workflowResume(helpers, input) {
|
|
|
3002
3538
|
details: { runId, status: journal.status }
|
|
3003
3539
|
});
|
|
3004
3540
|
}
|
|
3005
|
-
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({
|
|
3006
3556
|
...helpers,
|
|
3007
3557
|
context: {
|
|
3008
3558
|
...helpers.context,
|
|
@@ -3011,7 +3561,7 @@ async function workflowResume(helpers, input) {
|
|
|
3011
3561
|
resumeRunId: runId
|
|
3012
3562
|
}
|
|
3013
3563
|
}
|
|
3014
|
-
};
|
|
3564
|
+
}, resumeRoot);
|
|
3015
3565
|
switch (journal.command) {
|
|
3016
3566
|
case "switch":
|
|
3017
3567
|
return workflowSwitch(resumedHelpers, journal.input);
|
|
@@ -3041,7 +3591,15 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3041
3591
|
const root = resolveProjectRootOrThrow("recover", helpers.cwd());
|
|
3042
3592
|
const lock = inspectWorkflowLock(root);
|
|
3043
3593
|
const journals = listWorkflowRunJournals(root);
|
|
3044
|
-
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 }) => ({
|
|
3045
3603
|
runId: journal.runId,
|
|
3046
3604
|
command: journal.command,
|
|
3047
3605
|
status: journal.status,
|
|
@@ -3051,6 +3609,29 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3051
3609
|
failure: journal.failure,
|
|
3052
3610
|
resumeCommand: `treeseed resume ${journal.runId}`
|
|
3053
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
|
+
}) : [];
|
|
3054
3635
|
const selectedRun = input.runId ? readWorkflowRunJournal(root, input.runId) : null;
|
|
3055
3636
|
return buildWorkflowResult(
|
|
3056
3637
|
"recover",
|
|
@@ -3058,13 +3639,16 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3058
3639
|
{
|
|
3059
3640
|
lock,
|
|
3060
3641
|
interruptedRuns,
|
|
3642
|
+
staleRuns,
|
|
3643
|
+
obsoleteRuns,
|
|
3644
|
+
prunedRuns,
|
|
3061
3645
|
selectedRun,
|
|
3062
3646
|
runCount: journals.length
|
|
3063
3647
|
},
|
|
3064
3648
|
{
|
|
3065
3649
|
includeFinalState: false,
|
|
3066
3650
|
nextSteps: createNextSteps([
|
|
3067
|
-
...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." }]
|
|
3068
3652
|
])
|
|
3069
3653
|
}
|
|
3070
3654
|
);
|