@treeseed/sdk 0.6.32 → 0.6.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/operations/services/deploy.d.ts +1 -0
- package/dist/operations/services/deploy.js +37 -0
- package/dist/operations/services/git-workflow.d.ts +20 -2
- package/dist/operations/services/git-workflow.js +44 -10
- package/dist/operations/services/github-actions-verification.d.ts +2 -0
- package/dist/operations/services/github-actions-verification.js +20 -3
- package/dist/operations/services/github-api.d.ts +14 -0
- package/dist/operations/services/github-api.js +37 -13
- package/dist/operations/services/github-automation.d.ts +2 -0
- package/dist/scripts/check-build-warnings.js +32 -3
- package/dist/workflow/operations.d.ts +29 -0
- package/dist/workflow/operations.js +232 -13
- package/dist/workflow/runs.js +26 -0
- package/dist/workflow-state.d.ts +1 -0
- package/dist/workflow-state.js +16 -4
- package/dist/workflow.d.ts +4 -0
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +2 -4
|
@@ -998,6 +998,7 @@ export declare function runRemoteD1Migrations(tenantRoot: any, options?: {}): {
|
|
|
998
998
|
};
|
|
999
999
|
export declare function markDeploymentInitialized(tenantRoot: any, options?: {}): any;
|
|
1000
1000
|
export declare function markManagedServicesInitialized(tenantRoot: any, options?: {}): any;
|
|
1001
|
+
export declare function recordHostedDeploymentState(tenantRoot: any, options?: {}): any;
|
|
1001
1002
|
export declare function assertDeploymentInitialized(tenantRoot: any, options?: {}): any;
|
|
1002
1003
|
export declare function finalizeDeploymentState(tenantRoot: any, options?: {}): any;
|
|
1003
1004
|
export declare function printDeploySummary(summary: any): void;
|
|
@@ -2096,6 +2096,42 @@ function markManagedServicesInitialized(tenantRoot, options = {}) {
|
|
|
2096
2096
|
writeDeployState(tenantRoot, state, { target });
|
|
2097
2097
|
return state;
|
|
2098
2098
|
}
|
|
2099
|
+
function recordHostedDeploymentState(tenantRoot, options = {}) {
|
|
2100
|
+
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
2101
|
+
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
2102
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
2103
|
+
const timestamp = typeof options.timestamp === "string" && options.timestamp.trim() ? options.timestamp.trim() : (/* @__PURE__ */ new Date()).toISOString();
|
|
2104
|
+
const deployedUrl = typeof options.url === "string" && options.url.trim() ? options.url.trim() : state.lastDeployedUrl ?? resolveConfiguredSurfaceBaseUrl(deployConfig, target, "web");
|
|
2105
|
+
const commit = typeof options.commit === "string" && options.commit.trim() ? options.commit.trim() : null;
|
|
2106
|
+
state.lastDeployedUrl = deployedUrl;
|
|
2107
|
+
state.lastDeploymentTimestamp = timestamp;
|
|
2108
|
+
state.lastDeployedCommit = commit;
|
|
2109
|
+
state.readiness = {
|
|
2110
|
+
...state.readiness ?? {},
|
|
2111
|
+
initialized: true,
|
|
2112
|
+
configured: true,
|
|
2113
|
+
provisioned: true,
|
|
2114
|
+
deployable: true,
|
|
2115
|
+
phase: "provisioned",
|
|
2116
|
+
initializedAt: state.readiness?.initializedAt ?? timestamp,
|
|
2117
|
+
lastValidatedAt: timestamp,
|
|
2118
|
+
blockers: [],
|
|
2119
|
+
warnings: state.readiness?.warnings ?? []
|
|
2120
|
+
};
|
|
2121
|
+
const nextHistoryEntry = {
|
|
2122
|
+
commit,
|
|
2123
|
+
timestamp,
|
|
2124
|
+
url: deployedUrl,
|
|
2125
|
+
target: deployTargetLabel(target),
|
|
2126
|
+
source: options.source ?? "hosted-github-workflow",
|
|
2127
|
+
workflow: options.workflow ?? null,
|
|
2128
|
+
runId: options.runId ?? null
|
|
2129
|
+
};
|
|
2130
|
+
const history = Array.isArray(state.deploymentHistory) ? state.deploymentHistory : [];
|
|
2131
|
+
state.deploymentHistory = [...history, nextHistoryEntry].slice(-20);
|
|
2132
|
+
writeDeployState(tenantRoot, state, { target });
|
|
2133
|
+
return state;
|
|
2134
|
+
}
|
|
2099
2135
|
function assertDeploymentInitialized(tenantRoot, options = {}) {
|
|
2100
2136
|
const target = normalizeTarget(options.scope ?? options.target ?? "prod");
|
|
2101
2137
|
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
@@ -2225,6 +2261,7 @@ export {
|
|
|
2225
2261
|
queueId,
|
|
2226
2262
|
queueName,
|
|
2227
2263
|
reconcileCloudflareWebCacheRules,
|
|
2264
|
+
recordHostedDeploymentState,
|
|
2228
2265
|
resolveCloudflareZoneIdForHost,
|
|
2229
2266
|
resolveConfiguredCloudflareAccountId,
|
|
2230
2267
|
resolveConfiguredSurfaceBaseUrl,
|
|
@@ -119,15 +119,32 @@ export declare function mergeCurrentBranchIntoStaging(cwd: any, featureBranch: a
|
|
|
119
119
|
committed: boolean;
|
|
120
120
|
commitSha: string;
|
|
121
121
|
pushed: boolean;
|
|
122
|
+
generatedMetadataReconciliation: {
|
|
123
|
+
commitSha: null;
|
|
124
|
+
resolved: boolean;
|
|
125
|
+
repoDir: any;
|
|
126
|
+
targetBranch: string;
|
|
127
|
+
reconciledFiles: string[];
|
|
128
|
+
allConflictsWereGeneratedMetadata: boolean;
|
|
129
|
+
} | null;
|
|
122
130
|
};
|
|
123
|
-
export declare function squashMergeBranchIntoStaging(cwd: any, featureBranch: any, message: any, { pushTarget }?: {
|
|
131
|
+
export declare function squashMergeBranchIntoStaging(cwd: any, featureBranch: any, message: any, { pushTarget, reportGeneratedMetadataReconciliation }?: {
|
|
124
132
|
pushTarget?: boolean | undefined;
|
|
133
|
+
reportGeneratedMetadataReconciliation?: boolean | undefined;
|
|
125
134
|
}): {
|
|
126
135
|
repoDir: string;
|
|
127
136
|
targetBranch: string;
|
|
128
137
|
committed: boolean;
|
|
129
138
|
commitSha: string;
|
|
130
139
|
pushed: boolean;
|
|
140
|
+
generatedMetadataReconciliation: {
|
|
141
|
+
commitSha: null;
|
|
142
|
+
resolved: boolean;
|
|
143
|
+
repoDir: any;
|
|
144
|
+
targetBranch: string;
|
|
145
|
+
reconciledFiles: string[];
|
|
146
|
+
allConflictsWereGeneratedMetadata: boolean;
|
|
147
|
+
} | null;
|
|
131
148
|
};
|
|
132
149
|
export declare function currentManagedBranch(cwd?: any): string;
|
|
133
150
|
export declare function isTaskBranch(branchName: any): boolean;
|
|
@@ -162,8 +179,9 @@ export declare function mergeStagingIntoMain(cwd?: any): {
|
|
|
162
179
|
commitSha: string;
|
|
163
180
|
pushed: boolean;
|
|
164
181
|
};
|
|
165
|
-
export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget }?: {
|
|
182
|
+
export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget, quietMerge }?: {
|
|
166
183
|
pushTarget?: boolean | undefined;
|
|
184
|
+
quietMerge?: boolean | undefined;
|
|
167
185
|
}): {
|
|
168
186
|
repoDir: string;
|
|
169
187
|
targetBranch: any;
|
|
@@ -28,14 +28,34 @@ function conflictedFiles(repoDir) {
|
|
|
28
28
|
}
|
|
29
29
|
function resolveGeneratedPackageMetadataConflicts(repoDir) {
|
|
30
30
|
const files = conflictedFiles(repoDir);
|
|
31
|
-
if (files.length === 0)
|
|
31
|
+
if (files.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
resolved: false,
|
|
34
|
+
repoDir,
|
|
35
|
+
targetBranch: STAGING_BRANCH,
|
|
36
|
+
reconciledFiles: [],
|
|
37
|
+
allConflictsWereGeneratedMetadata: false
|
|
38
|
+
};
|
|
39
|
+
}
|
|
32
40
|
const generatedMetadataFiles = /* @__PURE__ */ new Set(["package.json", "package-lock.json"]);
|
|
33
41
|
if (files.some((file) => !generatedMetadataFiles.has(file))) {
|
|
34
|
-
return
|
|
42
|
+
return {
|
|
43
|
+
resolved: false,
|
|
44
|
+
repoDir,
|
|
45
|
+
targetBranch: STAGING_BRANCH,
|
|
46
|
+
reconciledFiles: files,
|
|
47
|
+
allConflictsWereGeneratedMetadata: false
|
|
48
|
+
};
|
|
35
49
|
}
|
|
36
50
|
runGit(["checkout", "--theirs", "--", ...files], { cwd: repoDir });
|
|
37
51
|
runGit(["add", "--", ...files], { cwd: repoDir });
|
|
38
|
-
return
|
|
52
|
+
return {
|
|
53
|
+
resolved: true,
|
|
54
|
+
repoDir,
|
|
55
|
+
targetBranch: STAGING_BRANCH,
|
|
56
|
+
reconciledFiles: files,
|
|
57
|
+
allConflictsWereGeneratedMetadata: true
|
|
58
|
+
};
|
|
39
59
|
}
|
|
40
60
|
function headCommit(repoDir, ref = "HEAD") {
|
|
41
61
|
return runGit(["rev-parse", ref], { cwd: repoDir, capture: true }).trim();
|
|
@@ -301,22 +321,35 @@ function deleteRemoteBranch(repoDir, branchName) {
|
|
|
301
321
|
function mergeCurrentBranchIntoStaging(cwd, featureBranch) {
|
|
302
322
|
return squashMergeBranchIntoStaging(cwd, featureBranch, `stage: ${featureBranch}`);
|
|
303
323
|
}
|
|
304
|
-
function squashMergeBranchIntoStaging(cwd, featureBranch, message, { pushTarget = true } = {}) {
|
|
324
|
+
function squashMergeBranchIntoStaging(cwd, featureBranch, message, { pushTarget = true, reportGeneratedMetadataReconciliation = true } = {}) {
|
|
305
325
|
const repoDir = assertCleanWorktree(cwd);
|
|
306
326
|
fetchOrigin(repoDir);
|
|
307
327
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
328
|
+
let generatedMetadataReconciliation = null;
|
|
308
329
|
try {
|
|
309
|
-
runGit(["merge", "--squash", featureBranch], { cwd: repoDir });
|
|
330
|
+
runGit(["merge", "--squash", featureBranch], { cwd: repoDir, capture: true });
|
|
310
331
|
} catch (error) {
|
|
311
|
-
|
|
332
|
+
const reconciliation = resolveGeneratedPackageMetadataConflicts(repoDir);
|
|
333
|
+
if (!reconciliation.resolved) {
|
|
312
334
|
throw error;
|
|
313
335
|
}
|
|
336
|
+
if (reportGeneratedMetadataReconciliation) {
|
|
337
|
+
console.log(`Resolving generated package metadata reconciliation for ${reconciliation.reconciledFiles.join(", ")}.`);
|
|
338
|
+
}
|
|
339
|
+
generatedMetadataReconciliation = {
|
|
340
|
+
...reconciliation,
|
|
341
|
+
commitSha: null
|
|
342
|
+
};
|
|
314
343
|
}
|
|
315
344
|
let committed = false;
|
|
316
345
|
if (repoHasStagedChanges(repoDir)) {
|
|
317
346
|
runGit(["commit", "-m", message], { cwd: repoDir });
|
|
318
347
|
committed = true;
|
|
319
348
|
}
|
|
349
|
+
const commitSha = headCommit(repoDir);
|
|
350
|
+
if (generatedMetadataReconciliation) {
|
|
351
|
+
generatedMetadataReconciliation.commitSha = commitSha;
|
|
352
|
+
}
|
|
320
353
|
if (pushTarget) {
|
|
321
354
|
pushBranch(repoDir, STAGING_BRANCH);
|
|
322
355
|
}
|
|
@@ -324,8 +357,9 @@ function squashMergeBranchIntoStaging(cwd, featureBranch, message, { pushTarget
|
|
|
324
357
|
repoDir,
|
|
325
358
|
targetBranch: STAGING_BRANCH,
|
|
326
359
|
committed,
|
|
327
|
-
commitSha
|
|
328
|
-
pushed: pushTarget
|
|
360
|
+
commitSha,
|
|
361
|
+
pushed: pushTarget,
|
|
362
|
+
generatedMetadataReconciliation
|
|
329
363
|
};
|
|
330
364
|
}
|
|
331
365
|
function currentManagedBranch(cwd = workspaceRoot()) {
|
|
@@ -425,13 +459,13 @@ function mergeStagingIntoMain(cwd = workspaceRoot()) {
|
|
|
425
459
|
pushTarget: true
|
|
426
460
|
});
|
|
427
461
|
}
|
|
428
|
-
function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true } = {}) {
|
|
462
|
+
function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true, quietMerge = false } = {}) {
|
|
429
463
|
const repoDir = prepareReleaseBranches(cwd);
|
|
430
464
|
checkoutBranch(repoDir, targetBranch);
|
|
431
465
|
if (remoteBranchExists(repoDir, targetBranch)) {
|
|
432
466
|
runGit(["merge", "--ff-only", `origin/${targetBranch}`], { cwd: repoDir });
|
|
433
467
|
}
|
|
434
|
-
runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir });
|
|
468
|
+
runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir, capture: quietMerge });
|
|
435
469
|
pushBranch(repoDir, STAGING_BRANCH);
|
|
436
470
|
if (pushTarget) {
|
|
437
471
|
pushBranch(repoDir, targetBranch);
|
|
@@ -114,6 +114,8 @@ export declare function skippedGitHubActionsGate(gate: GitHubActionsWorkflowGate
|
|
|
114
114
|
conclusion: null;
|
|
115
115
|
runId: null;
|
|
116
116
|
url: null;
|
|
117
|
+
createdAt: null;
|
|
118
|
+
updatedAt: null;
|
|
117
119
|
};
|
|
118
120
|
export declare function formatGitHubActionsGateFailure(gate: GitHubActionsWorkflowGate, result: Record<string, unknown>): string;
|
|
119
121
|
export declare function waitForGitHubActionsGate(gate: GitHubActionsWorkflowGate, options?: {
|
|
@@ -406,7 +406,9 @@ function skippedGitHubActionsGate(gate, reason) {
|
|
|
406
406
|
reason,
|
|
407
407
|
conclusion: null,
|
|
408
408
|
runId: null,
|
|
409
|
-
url: null
|
|
409
|
+
url: null,
|
|
410
|
+
createdAt: null,
|
|
411
|
+
updatedAt: null
|
|
410
412
|
};
|
|
411
413
|
}
|
|
412
414
|
function formatGitHubActionsGateFailure(gate, result) {
|
|
@@ -431,6 +433,21 @@ function formatElapsed(seconds) {
|
|
|
431
433
|
function shortSha(value) {
|
|
432
434
|
return value ? value.slice(0, 12) : "(unknown)";
|
|
433
435
|
}
|
|
436
|
+
function activeJobSummary(event) {
|
|
437
|
+
const activeJobs = event.activeJobs ?? [];
|
|
438
|
+
if (activeJobs.length === 0) return "";
|
|
439
|
+
const summaries = activeJobs.slice(0, 2).map((job) => {
|
|
440
|
+
const activeStep = (job.steps ?? []).find((step) => step.status && step.status !== "completed");
|
|
441
|
+
return activeStep?.name ? `${job.name} > ${activeStep.name}` : job.name;
|
|
442
|
+
}).filter(Boolean);
|
|
443
|
+
return summaries.length > 0 ? `; active: ${summaries.join(", ")}` : "";
|
|
444
|
+
}
|
|
445
|
+
function failedJobSummary(event) {
|
|
446
|
+
const failedJobs = event.failedJobs ?? [];
|
|
447
|
+
if (failedJobs.length === 0) return "";
|
|
448
|
+
const names = failedJobs.slice(0, 3).map((job) => job.name).filter(Boolean);
|
|
449
|
+
return names.length > 0 ? `; failed: ${names.join(", ")}` : "";
|
|
450
|
+
}
|
|
434
451
|
function formatGitHubActionsGateProgress(gate, event, operation) {
|
|
435
452
|
const prefix = `[${operation}][gate][${gate.name}] ${event.workflow}`;
|
|
436
453
|
if (event.type === "waiting") {
|
|
@@ -439,12 +456,12 @@ function formatGitHubActionsGateProgress(gate, event, operation) {
|
|
|
439
456
|
if (event.type === "completed") {
|
|
440
457
|
const conclusion = event.conclusion === "success" ? "successfully" : `with conclusion ${event.conclusion ?? "unknown"}`;
|
|
441
458
|
const url2 = event.url ? `: ${event.url}` : "";
|
|
442
|
-
return `${prefix} completed ${conclusion} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
|
|
459
|
+
return `${prefix} completed ${conclusion}${failedJobSummary(event)} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
|
|
443
460
|
}
|
|
444
461
|
const status = event.status ?? "waiting";
|
|
445
462
|
const url = event.url ? `: ${event.url}` : "";
|
|
446
463
|
const run = event.runId ? ` run ${event.runId}` : "";
|
|
447
|
-
return `${prefix}${run} ${status}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
|
|
464
|
+
return `${prefix}${run} ${status}${activeJobSummary(event)}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
|
|
448
465
|
}
|
|
449
466
|
async function waitForGitHubActionsGate(gate, options = {}) {
|
|
450
467
|
const { waitForGitHubWorkflowCompletion } = await import("./github-automation.js");
|
|
@@ -26,6 +26,8 @@ export interface GitHubWorkflowRunSummary {
|
|
|
26
26
|
url: string | null;
|
|
27
27
|
headSha: string | null;
|
|
28
28
|
headBranch: string | null;
|
|
29
|
+
createdAt: string | null;
|
|
30
|
+
updatedAt: string | null;
|
|
29
31
|
}
|
|
30
32
|
export interface GitHubWorkflowJobSummary {
|
|
31
33
|
id: number;
|
|
@@ -33,6 +35,12 @@ export interface GitHubWorkflowJobSummary {
|
|
|
33
35
|
status: string | null;
|
|
34
36
|
conclusion: string | null;
|
|
35
37
|
url: string | null;
|
|
38
|
+
steps?: GitHubWorkflowJobStepSummary[];
|
|
39
|
+
}
|
|
40
|
+
export interface GitHubWorkflowJobStepSummary {
|
|
41
|
+
name: string;
|
|
42
|
+
status: string | null;
|
|
43
|
+
conclusion: string | null;
|
|
36
44
|
}
|
|
37
45
|
export type GitHubWorkflowProgressEvent = {
|
|
38
46
|
type: 'waiting' | 'running' | 'completed';
|
|
@@ -45,6 +53,10 @@ export type GitHubWorkflowProgressEvent = {
|
|
|
45
53
|
url: string | null;
|
|
46
54
|
status: string | null;
|
|
47
55
|
conclusion: string | null;
|
|
56
|
+
jobs?: GitHubWorkflowJobSummary[];
|
|
57
|
+
activeJobs?: GitHubWorkflowJobSummary[];
|
|
58
|
+
completedJobs?: GitHubWorkflowJobSummary[];
|
|
59
|
+
failedJobs?: GitHubWorkflowJobSummary[];
|
|
48
60
|
};
|
|
49
61
|
export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
|
|
50
62
|
export declare function parseGitHubRepositorySlug(value: string): {
|
|
@@ -152,6 +164,8 @@ export declare function waitForGitHubWorkflowRunCompletion(repository: string |
|
|
|
152
164
|
runId: number;
|
|
153
165
|
headSha: string | null;
|
|
154
166
|
branch: string | null;
|
|
167
|
+
createdAt: string | null;
|
|
168
|
+
updatedAt: string | null;
|
|
155
169
|
conclusion: string | null;
|
|
156
170
|
url: string | null;
|
|
157
171
|
jobs: GitHubWorkflowJobSummary[];
|
|
@@ -520,7 +520,9 @@ function normalizeWorkflowRun(run) {
|
|
|
520
520
|
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
521
521
|
url: typeof run.html_url === "string" ? run.html_url : null,
|
|
522
522
|
headSha: typeof run.head_sha === "string" ? run.head_sha : null,
|
|
523
|
-
headBranch: typeof run.head_branch === "string" ? run.head_branch : null
|
|
523
|
+
headBranch: typeof run.head_branch === "string" ? run.head_branch : null,
|
|
524
|
+
createdAt: typeof run.created_at === "string" ? run.created_at : null,
|
|
525
|
+
updatedAt: typeof run.updated_at === "string" ? run.updated_at : null
|
|
524
526
|
};
|
|
525
527
|
}
|
|
526
528
|
function normalizeWorkflowJob(job) {
|
|
@@ -529,9 +531,27 @@ function normalizeWorkflowJob(job) {
|
|
|
529
531
|
name: String(job.name ?? ""),
|
|
530
532
|
status: typeof job.status === "string" ? job.status : null,
|
|
531
533
|
conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
|
|
532
|
-
url: typeof job.html_url === "string" ? job.html_url : null
|
|
534
|
+
url: typeof job.html_url === "string" ? job.html_url : null,
|
|
535
|
+
steps: Array.isArray(job.steps) ? job.steps.map((step) => ({
|
|
536
|
+
name: String(step.name ?? ""),
|
|
537
|
+
status: typeof step.status === "string" ? step.status : null,
|
|
538
|
+
conclusion: typeof step.conclusion === "string" ? step.conclusion : null
|
|
539
|
+
})) : []
|
|
533
540
|
};
|
|
534
541
|
}
|
|
542
|
+
async function listWorkflowJobsForProgress(client, owner, repo, runId) {
|
|
543
|
+
try {
|
|
544
|
+
const jobs = await client.rest.actions.listJobsForWorkflowRun({
|
|
545
|
+
owner,
|
|
546
|
+
repo,
|
|
547
|
+
run_id: runId,
|
|
548
|
+
per_page: 100
|
|
549
|
+
});
|
|
550
|
+
return jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
|
|
551
|
+
} catch {
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
535
555
|
function sleep(ms) {
|
|
536
556
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
537
557
|
}
|
|
@@ -547,7 +567,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
547
567
|
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
548
568
|
const startedAt = Date.now();
|
|
549
569
|
let lastProgress = null;
|
|
550
|
-
const emitProgress = (type, run = null) => {
|
|
570
|
+
const emitProgress = (type, run = null, jobs = []) => {
|
|
571
|
+
const completedJobs = jobs.filter((job) => job.status === "completed");
|
|
572
|
+
const failedJobs = jobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped");
|
|
573
|
+
const activeJobs = jobs.filter((job) => job.status && job.status !== "completed");
|
|
551
574
|
const event = {
|
|
552
575
|
type,
|
|
553
576
|
repository: `${owner}/${name}`,
|
|
@@ -558,7 +581,11 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
558
581
|
runId: run?.id ?? null,
|
|
559
582
|
url: run?.url ?? null,
|
|
560
583
|
status: run?.status ?? null,
|
|
561
|
-
conclusion: run?.conclusion ?? null
|
|
584
|
+
conclusion: run?.conclusion ?? null,
|
|
585
|
+
jobs,
|
|
586
|
+
activeJobs,
|
|
587
|
+
completedJobs,
|
|
588
|
+
failedJobs
|
|
562
589
|
};
|
|
563
590
|
lastProgress = event;
|
|
564
591
|
onProgress?.(event);
|
|
@@ -587,15 +614,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
587
614
|
run_id: match.id
|
|
588
615
|
});
|
|
589
616
|
const normalized = normalizeWorkflowRun(current.data);
|
|
617
|
+
const progressJobs = await listWorkflowJobsForProgress(client, owner, name, match.id);
|
|
590
618
|
if (normalized.status === "completed") {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
owner,
|
|
594
|
-
repo: name,
|
|
595
|
-
run_id: match.id,
|
|
596
|
-
per_page: 100
|
|
597
|
-
});
|
|
598
|
-
const normalizedJobs = jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
|
|
619
|
+
const normalizedJobs = progressJobs;
|
|
620
|
+
emitProgress("completed", normalized, normalizedJobs);
|
|
599
621
|
return {
|
|
600
622
|
status: "completed",
|
|
601
623
|
repository: `${owner}/${name}`,
|
|
@@ -603,13 +625,15 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
603
625
|
runId: normalized.id,
|
|
604
626
|
headSha: normalized.headSha,
|
|
605
627
|
branch: normalized.headBranch,
|
|
628
|
+
createdAt: normalized.createdAt,
|
|
629
|
+
updatedAt: normalized.updatedAt,
|
|
606
630
|
conclusion: normalized.conclusion,
|
|
607
631
|
url: normalized.url,
|
|
608
632
|
jobs: normalizedJobs,
|
|
609
633
|
failedJobs: normalizedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
|
|
610
634
|
};
|
|
611
635
|
}
|
|
612
|
-
emitProgress("running", normalized);
|
|
636
|
+
emitProgress("running", normalized, progressJobs);
|
|
613
637
|
await sleep(pollSeconds * 1e3);
|
|
614
638
|
}
|
|
615
639
|
} catch (error) {
|
|
@@ -284,6 +284,8 @@ export declare function waitForGitHubWorkflowCompletion(tenantRoot: any, { repos
|
|
|
284
284
|
runId: number;
|
|
285
285
|
headSha: string | null;
|
|
286
286
|
branch: string | null;
|
|
287
|
+
createdAt: string | null;
|
|
288
|
+
updatedAt: string | null;
|
|
287
289
|
conclusion: string | null;
|
|
288
290
|
url: string | null;
|
|
289
291
|
jobs: import("./github-api.ts").GitHubWorkflowJobSummary[];
|
|
@@ -4,17 +4,31 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
|
+
const defaultAllowlisted = [
|
|
8
|
+
{
|
|
9
|
+
label: 'vite-browser-external-libsodium-url',
|
|
10
|
+
pattern: /Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"/u,
|
|
11
|
+
},
|
|
12
|
+
];
|
|
7
13
|
const allowlisted = [];
|
|
8
14
|
const files = [];
|
|
15
|
+
let useDefaultPolicy = true;
|
|
9
16
|
|
|
10
17
|
for (let index = 0; index < args.length; index += 1) {
|
|
11
18
|
const arg = args[index];
|
|
19
|
+
if (arg === '--no-default-policy') {
|
|
20
|
+
useDefaultPolicy = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
12
23
|
if (arg === '--allow') {
|
|
13
24
|
const pattern = args[index + 1];
|
|
14
25
|
if (!pattern) {
|
|
15
26
|
throw new Error('Missing value for --allow.');
|
|
16
27
|
}
|
|
17
|
-
allowlisted.push(
|
|
28
|
+
allowlisted.push({
|
|
29
|
+
label: `custom:${pattern}`,
|
|
30
|
+
pattern: new RegExp(pattern),
|
|
31
|
+
});
|
|
18
32
|
index += 1;
|
|
19
33
|
continue;
|
|
20
34
|
}
|
|
@@ -22,17 +36,25 @@ for (let index = 0; index < args.length; index += 1) {
|
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
if (files.length === 0) {
|
|
25
|
-
throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>]');
|
|
39
|
+
throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>] [--no-default-policy]');
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
const warningLines = [];
|
|
43
|
+
const allowedWarnings = new Map();
|
|
44
|
+
const effectiveAllowlisted = [
|
|
45
|
+
...(useDefaultPolicy ? defaultAllowlisted : []),
|
|
46
|
+
...allowlisted,
|
|
47
|
+
];
|
|
29
48
|
for (const file of files) {
|
|
30
49
|
const contents = readFileSync(resolve(process.cwd(), file), 'utf8');
|
|
31
50
|
for (const line of contents.split(/\r?\n/u)) {
|
|
32
51
|
if (!line.includes('[WARN]')) {
|
|
33
52
|
continue;
|
|
34
53
|
}
|
|
35
|
-
|
|
54
|
+
const allowed = effectiveAllowlisted.find((rule) => rule.pattern.test(line));
|
|
55
|
+
if (allowed) {
|
|
56
|
+
const current = allowedWarnings.get(allowed.label) ?? 0;
|
|
57
|
+
allowedWarnings.set(allowed.label, current + 1);
|
|
36
58
|
continue;
|
|
37
59
|
}
|
|
38
60
|
warningLines.push(line);
|
|
@@ -47,4 +69,11 @@ if (warningLines.length > 0) {
|
|
|
47
69
|
process.exit(1);
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
const allowedTotal = [...allowedWarnings.values()].reduce((sum, count) => sum + count, 0);
|
|
73
|
+
if (allowedTotal > 0) {
|
|
74
|
+
console.log(`Allowed build warnings: ${allowedTotal}`);
|
|
75
|
+
for (const [label, count] of [...allowedWarnings.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
76
|
+
console.log(`- ${label}: ${count}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
50
79
|
console.log('No unexpected build warnings detected.');
|
|
@@ -415,6 +415,8 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
415
415
|
conclusion: null;
|
|
416
416
|
runId: null;
|
|
417
417
|
url: null;
|
|
418
|
+
createdAt: null;
|
|
419
|
+
updatedAt: null;
|
|
418
420
|
}[];
|
|
419
421
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
420
422
|
} & {
|
|
@@ -553,6 +555,8 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
553
555
|
conclusion: null;
|
|
554
556
|
runId: null;
|
|
555
557
|
url: null;
|
|
558
|
+
createdAt: null;
|
|
559
|
+
updatedAt: null;
|
|
556
560
|
}[];
|
|
557
561
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
558
562
|
} & {
|
|
@@ -732,6 +736,8 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
|
|
|
732
736
|
conclusion: null;
|
|
733
737
|
runId: null;
|
|
734
738
|
url: null;
|
|
739
|
+
createdAt: null;
|
|
740
|
+
updatedAt: null;
|
|
735
741
|
}[];
|
|
736
742
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
737
743
|
} & {
|
|
@@ -798,6 +804,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
798
804
|
worktreePath: string | null;
|
|
799
805
|
primaryRoot: string | null;
|
|
800
806
|
ciMode: "hosted" | "off";
|
|
807
|
+
fresh: boolean;
|
|
808
|
+
freshArchivedRuns: never[];
|
|
801
809
|
mode: TreeseedWorkflowMode;
|
|
802
810
|
mergeStrategy: string;
|
|
803
811
|
level: string;
|
|
@@ -844,6 +852,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
844
852
|
mode: "root-only";
|
|
845
853
|
mergeStrategy: string;
|
|
846
854
|
level: "patch" | "major" | "minor";
|
|
855
|
+
fresh: boolean;
|
|
856
|
+
freshArchivedRuns: {
|
|
857
|
+
runId: string;
|
|
858
|
+
reasons: string[];
|
|
859
|
+
}[];
|
|
847
860
|
resumed: boolean;
|
|
848
861
|
resumedRunId: string | null;
|
|
849
862
|
autoResumed: boolean;
|
|
@@ -879,6 +892,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
879
892
|
targetBranch: string;
|
|
880
893
|
commitSha: string;
|
|
881
894
|
};
|
|
895
|
+
hostedDeploymentState: Record<string, unknown>[];
|
|
882
896
|
finalBranch: string;
|
|
883
897
|
pushStatus: {
|
|
884
898
|
stagingPushed: boolean;
|
|
@@ -898,6 +912,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
898
912
|
conclusion: null;
|
|
899
913
|
runId: null;
|
|
900
914
|
url: null;
|
|
915
|
+
createdAt: null;
|
|
916
|
+
updatedAt: null;
|
|
901
917
|
}[];
|
|
902
918
|
} & {
|
|
903
919
|
finalState?: WorkflowStatePayload;
|
|
@@ -909,6 +925,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
909
925
|
mode: "recursive-workspace";
|
|
910
926
|
mergeStrategy: string;
|
|
911
927
|
level: "patch" | "major" | "minor";
|
|
928
|
+
fresh: boolean;
|
|
929
|
+
freshArchivedRuns: {
|
|
930
|
+
runId: string;
|
|
931
|
+
reasons: string[];
|
|
932
|
+
}[];
|
|
912
933
|
resumed: boolean;
|
|
913
934
|
resumedRunId: string | null;
|
|
914
935
|
autoResumed: boolean;
|
|
@@ -956,6 +977,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
956
977
|
targetBranch: string;
|
|
957
978
|
commitSha: string;
|
|
958
979
|
};
|
|
980
|
+
hostedDeploymentState: Record<string, unknown>[];
|
|
959
981
|
finalBranch: string;
|
|
960
982
|
pushStatus: {
|
|
961
983
|
stagingPushed: boolean;
|
|
@@ -975,6 +997,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
975
997
|
conclusion: null;
|
|
976
998
|
runId: null;
|
|
977
999
|
url: null;
|
|
1000
|
+
createdAt: null;
|
|
1001
|
+
updatedAt: null;
|
|
978
1002
|
})[];
|
|
979
1003
|
} & {
|
|
980
1004
|
finalState?: WorkflowStatePayload;
|
|
@@ -1041,6 +1065,11 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
|
|
|
1041
1065
|
} | null;
|
|
1042
1066
|
classification: import("./runs.ts").TreeseedWorkflowRunClassification;
|
|
1043
1067
|
}[];
|
|
1068
|
+
markedObsoleteRun: {
|
|
1069
|
+
runId: string;
|
|
1070
|
+
command: TreeseedWorkflowRunCommand;
|
|
1071
|
+
reason: string;
|
|
1072
|
+
} | null;
|
|
1044
1073
|
selectedRun: TreeseedWorkflowRunJournal | null;
|
|
1045
1074
|
runCount: number;
|
|
1046
1075
|
} & {
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
ensureGeneratedWranglerConfig,
|
|
37
37
|
finalizeDeploymentState,
|
|
38
38
|
loadDeployState,
|
|
39
|
+
recordHostedDeploymentState,
|
|
39
40
|
runRemoteD1Migrations,
|
|
40
41
|
validateDeployPrerequisites,
|
|
41
42
|
validateDestroyPrerequisites
|
|
@@ -254,13 +255,30 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
|
254
255
|
const packagePaths = new Set(packages.map((pkg) => pkg.repoPath));
|
|
255
256
|
const unresolved = unresolvedMergePaths(gitRoot);
|
|
256
257
|
if (unresolved.length === 0 || unresolved.some((filePath) => !packagePaths.has(filePath))) {
|
|
257
|
-
return
|
|
258
|
+
return {
|
|
259
|
+
resolved: false,
|
|
260
|
+
allUnresolvedPathsWerePackagePointers: unresolved.length > 0 && unresolved.every((filePath) => packagePaths.has(filePath)),
|
|
261
|
+
unresolvedPaths: unresolved,
|
|
262
|
+
entries: []
|
|
263
|
+
};
|
|
258
264
|
}
|
|
265
|
+
const entries = [];
|
|
259
266
|
for (const pkg of packages) {
|
|
260
267
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
261
268
|
run("git", ["add", pkg.repoPath], { cwd: gitRoot });
|
|
269
|
+
entries.push({
|
|
270
|
+
packageName: pkg.name,
|
|
271
|
+
path: pkg.repoPath,
|
|
272
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
273
|
+
resolvedCommit: headCommit(pkg.dir)
|
|
274
|
+
});
|
|
262
275
|
}
|
|
263
|
-
return
|
|
276
|
+
return {
|
|
277
|
+
resolved: true,
|
|
278
|
+
allUnresolvedPathsWerePackagePointers: true,
|
|
279
|
+
unresolvedPaths: unresolved,
|
|
280
|
+
entries
|
|
281
|
+
};
|
|
264
282
|
}
|
|
265
283
|
function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
266
284
|
if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
|
|
@@ -401,6 +419,38 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
401
419
|
}
|
|
402
420
|
return results;
|
|
403
421
|
}
|
|
422
|
+
function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
|
|
423
|
+
const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
|
|
424
|
+
const releaseRecord = stringRecord(rootRelease) ?? {};
|
|
425
|
+
const reports = [];
|
|
426
|
+
for (const target of [
|
|
427
|
+
{ scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
|
|
428
|
+
{ scope: "prod", branch: PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
|
|
429
|
+
]) {
|
|
430
|
+
const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
|
|
431
|
+
const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
|
|
432
|
+
if (!gate || !timestamp) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const state = recordHostedDeploymentState(root, {
|
|
436
|
+
scope: target.scope,
|
|
437
|
+
commit: typeof target.commit === "string" ? target.commit : null,
|
|
438
|
+
timestamp,
|
|
439
|
+
workflow: gate.workflow,
|
|
440
|
+
runId: gate.runId ?? null
|
|
441
|
+
});
|
|
442
|
+
reports.push({
|
|
443
|
+
scope: target.scope,
|
|
444
|
+
branch: target.branch,
|
|
445
|
+
commit: typeof target.commit === "string" ? target.commit : null,
|
|
446
|
+
timestamp: state.lastDeploymentTimestamp ?? timestamp,
|
|
447
|
+
url: state.lastDeployedUrl ?? null,
|
|
448
|
+
workflow: gate.workflow,
|
|
449
|
+
runId: gate.runId ?? null
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return reports;
|
|
453
|
+
}
|
|
404
454
|
function ensureTreeseedCommandReadiness(root) {
|
|
405
455
|
if (getGitHubAutomationMode() === "stub") {
|
|
406
456
|
return {
|
|
@@ -1060,6 +1110,123 @@ function releasePlanMatchesCurrentHeads(plan, rootRepo, packageReports) {
|
|
|
1060
1110
|
function releaseRunHasCompletedMutation(journal) {
|
|
1061
1111
|
return journal.steps.some((step) => step.status === "completed" && step.id !== "release-plan" && step.id !== "workspace-unlink");
|
|
1062
1112
|
}
|
|
1113
|
+
function generatedReleaseMetadataFiles(repoDir) {
|
|
1114
|
+
return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
|
|
1115
|
+
if (existsSync(resolve(repoDir, filePath))) return true;
|
|
1116
|
+
try {
|
|
1117
|
+
run("git", ["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
|
|
1118
|
+
return true;
|
|
1119
|
+
} catch {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function collectReleaseCleanupSnapshot(root, selectedPackageNames) {
|
|
1125
|
+
return {
|
|
1126
|
+
repos: [
|
|
1127
|
+
{
|
|
1128
|
+
name: "@treeseed/market",
|
|
1129
|
+
path: repoRoot(root),
|
|
1130
|
+
branch: currentBranch(repoRoot(root)) || null,
|
|
1131
|
+
files: generatedReleaseMetadataFiles(repoRoot(root))
|
|
1132
|
+
},
|
|
1133
|
+
...checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageNames.has(pkg.name)).map((pkg) => ({
|
|
1134
|
+
name: pkg.name,
|
|
1135
|
+
path: pkg.dir,
|
|
1136
|
+
branch: currentBranch(pkg.dir) || null,
|
|
1137
|
+
files: generatedReleaseMetadataFiles(pkg.dir)
|
|
1138
|
+
}))
|
|
1139
|
+
]
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function restoreReleaseGeneratedMetadata(repo) {
|
|
1143
|
+
const restored = [];
|
|
1144
|
+
const skipped = [];
|
|
1145
|
+
for (const filePath of repo.files) {
|
|
1146
|
+
const status = run("git", ["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
|
|
1147
|
+
if (!status.trim()) {
|
|
1148
|
+
skipped.push(filePath);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
run("git", ["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
|
|
1152
|
+
restored.push(filePath);
|
|
1153
|
+
}
|
|
1154
|
+
return { restored, skipped };
|
|
1155
|
+
}
|
|
1156
|
+
function cleanupFailedReleaseLocalState(root, helpers, snapshot, workspaceLinksMode) {
|
|
1157
|
+
const report = { restored: [], skipped: [], manualReview: [] };
|
|
1158
|
+
try {
|
|
1159
|
+
ensureWorkflowWorkspaceLinks(root, helpers, workspaceLinksMode ?? "auto");
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
report.manualReview.push({
|
|
1162
|
+
scope: "workspace-links",
|
|
1163
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (!snapshot) {
|
|
1167
|
+
report.skipped.push({ scope: "release-metadata", reason: "cleanup snapshot was not recorded before failure" });
|
|
1168
|
+
return report;
|
|
1169
|
+
}
|
|
1170
|
+
for (const repo of snapshot.repos) {
|
|
1171
|
+
try {
|
|
1172
|
+
const restored = restoreReleaseGeneratedMetadata(repo);
|
|
1173
|
+
if (repo.branch && currentBranch(repo.path) !== repo.branch) {
|
|
1174
|
+
checkoutBranch(repo.path, repo.branch);
|
|
1175
|
+
}
|
|
1176
|
+
if (restored.restored.length > 0) {
|
|
1177
|
+
report.restored.push({ repo: repo.name, path: repo.path, files: restored.restored });
|
|
1178
|
+
}
|
|
1179
|
+
if (restored.skipped.length > 0) {
|
|
1180
|
+
report.skipped.push({ repo: repo.name, path: repo.path, files: restored.skipped, reason: "unchanged" });
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
report.manualReview.push({
|
|
1184
|
+
repo: repo.name,
|
|
1185
|
+
path: repo.path,
|
|
1186
|
+
branch: repo.branch,
|
|
1187
|
+
files: repo.files,
|
|
1188
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1189
|
+
nextCommand: repo.branch ? `git -C ${repo.path} restore --staged --worktree -- ${repo.files.join(" ")} && git -C ${repo.path} checkout ${repo.branch}` : null
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return report;
|
|
1194
|
+
}
|
|
1195
|
+
function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
|
|
1196
|
+
if (branch !== STAGING_BRANCH) return { archived: [], blockers: [] };
|
|
1197
|
+
const currentHeads = Object.fromEntries([
|
|
1198
|
+
[rootRepo.name, rootRepo.commitSha ?? null],
|
|
1199
|
+
...packageReports.map((report) => [report.name, report.commitSha ?? null])
|
|
1200
|
+
]);
|
|
1201
|
+
const archived = [];
|
|
1202
|
+
const blockers = [];
|
|
1203
|
+
for (const journal of listInterruptedWorkflowRuns(root).filter((entry) => entry.command === "release")) {
|
|
1204
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1205
|
+
currentBranch: branch,
|
|
1206
|
+
currentHeads
|
|
1207
|
+
});
|
|
1208
|
+
if (classification.state === "stale") {
|
|
1209
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1210
|
+
...classification,
|
|
1211
|
+
reasons: ["fresh release superseded stale failed release", ...classification.reasons]
|
|
1212
|
+
});
|
|
1213
|
+
archived.push({ runId: journal.runId, reasons: classification.reasons });
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (classification.state === "resumable" && releaseRunHasCompletedMutation(journal)) {
|
|
1217
|
+
blockers.push(`${journal.runId}: completed release mutations and is still safe to resume. Mark it obsolete with \`npx trsd recover --obsolete ${journal.runId} --reason "superseded by fresh release"\` before using --fresh.`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (blockers.length > 0) {
|
|
1221
|
+
workflowError("release", "validation_failed", [
|
|
1222
|
+
"Treeseed release --fresh will not bypass a resumable partial release that already completed release mutations.",
|
|
1223
|
+
...blockers
|
|
1224
|
+
].join("\n"), {
|
|
1225
|
+
details: { archived, blockers }
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
return { archived, blockers };
|
|
1229
|
+
}
|
|
1063
1230
|
function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
|
|
1064
1231
|
if (branch !== STAGING_BRANCH) return null;
|
|
1065
1232
|
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
@@ -3323,8 +3490,10 @@ async function workflowRelease(helpers, input) {
|
|
|
3323
3490
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3324
3491
|
const packageReports = createWorkspacePackageReports(root);
|
|
3325
3492
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
3493
|
+
const freshRelease = input.fresh === true && !explicitResumeRunId;
|
|
3494
|
+
const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
3495
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3496
|
+
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3328
3497
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
3329
3498
|
const level = effectiveInput.bump ?? "patch";
|
|
3330
3499
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
@@ -3345,6 +3514,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3345
3514
|
return buildWorkflowResult("release", root, {
|
|
3346
3515
|
...plannedRelease,
|
|
3347
3516
|
ciMode,
|
|
3517
|
+
fresh: input.fresh === true,
|
|
3518
|
+
freshArchivedRuns: [],
|
|
3348
3519
|
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3349
3520
|
autoResumeCandidate: planAutoResumeRun ? {
|
|
3350
3521
|
runId: planAutoResumeRun.runId,
|
|
@@ -3372,6 +3543,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3372
3543
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3373
3544
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3374
3545
|
ciMode,
|
|
3546
|
+
fresh: input.fresh === true,
|
|
3375
3547
|
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3376
3548
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
3377
3549
|
},
|
|
@@ -3404,6 +3576,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3404
3576
|
if (autoResumeRun) {
|
|
3405
3577
|
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
|
|
3406
3578
|
}
|
|
3579
|
+
let releaseCleanupSnapshot = null;
|
|
3407
3580
|
try {
|
|
3408
3581
|
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
|
|
3409
3582
|
const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
|
|
@@ -3485,12 +3658,15 @@ async function workflowRelease(helpers, input) {
|
|
|
3485
3658
|
runId: workflowRun.runId,
|
|
3486
3659
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3487
3660
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3661
|
+
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
3488
3662
|
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false));
|
|
3489
3663
|
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
3490
3664
|
const payload2 = {
|
|
3491
3665
|
mode,
|
|
3492
3666
|
mergeStrategy: "merge-commit",
|
|
3493
3667
|
level,
|
|
3668
|
+
fresh: input.fresh === true,
|
|
3669
|
+
freshArchivedRuns: freshPreparation.archived,
|
|
3494
3670
|
resumed: workflowRun.resumed,
|
|
3495
3671
|
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
3496
3672
|
autoResumed: autoResumeRun != null,
|
|
@@ -3506,6 +3682,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3506
3682
|
rootRepo,
|
|
3507
3683
|
releaseCandidate,
|
|
3508
3684
|
releaseBackMerge: releaseBackMerge2,
|
|
3685
|
+
hostedDeploymentState: hostedDeploymentState2,
|
|
3509
3686
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
3510
3687
|
pushStatus: { stagingPushed: true, productionPushed: true, tagPushed: true },
|
|
3511
3688
|
workspaceLinks: workspaceLinks2,
|
|
@@ -3527,6 +3704,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3527
3704
|
prepareReleaseBranches(pkg.dir);
|
|
3528
3705
|
}
|
|
3529
3706
|
}
|
|
3707
|
+
releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
3530
3708
|
const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
3531
3709
|
const releasedPackageDevTags2 = Object.fromEntries(
|
|
3532
3710
|
checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
|
|
@@ -3641,17 +3819,22 @@ async function workflowRelease(helpers, input) {
|
|
|
3641
3819
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3642
3820
|
const stagingCommit = headCommit(gitRoot);
|
|
3643
3821
|
let released;
|
|
3822
|
+
let submoduleReconciliation = null;
|
|
3644
3823
|
try {
|
|
3645
3824
|
released = mergeBranchIntoTarget(root, {
|
|
3646
3825
|
sourceBranch: STAGING_BRANCH,
|
|
3647
3826
|
targetBranch: PRODUCTION_BRANCH,
|
|
3648
3827
|
message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3649
|
-
pushTarget: false
|
|
3828
|
+
pushTarget: false,
|
|
3829
|
+
quietMerge: true
|
|
3650
3830
|
});
|
|
3651
3831
|
} catch (error) {
|
|
3652
|
-
|
|
3832
|
+
const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
|
|
3833
|
+
if (!reconciliation.resolved) {
|
|
3653
3834
|
throw error;
|
|
3654
3835
|
}
|
|
3836
|
+
helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
|
|
3837
|
+
submoduleReconciliation = reconciliation;
|
|
3655
3838
|
commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
|
|
3656
3839
|
released = { commitSha: headCommit(gitRoot) };
|
|
3657
3840
|
}
|
|
@@ -3671,7 +3854,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3671
3854
|
stagingCommit,
|
|
3672
3855
|
releasedCommit,
|
|
3673
3856
|
mergeCommit: released.commitSha,
|
|
3674
|
-
tag
|
|
3857
|
+
tag,
|
|
3858
|
+
submoduleReconciliation
|
|
3675
3859
|
};
|
|
3676
3860
|
});
|
|
3677
3861
|
rootRepo.committed = true;
|
|
@@ -3721,6 +3905,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3721
3905
|
runId: workflowRun.runId,
|
|
3722
3906
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3723
3907
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3908
|
+
const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
|
|
3724
3909
|
const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true));
|
|
3725
3910
|
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
3726
3911
|
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", () => {
|
|
@@ -3752,6 +3937,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3752
3937
|
mode,
|
|
3753
3938
|
mergeStrategy: "merge-commit",
|
|
3754
3939
|
level,
|
|
3940
|
+
fresh: input.fresh === true,
|
|
3941
|
+
freshArchivedRuns: freshPreparation.archived,
|
|
3755
3942
|
resumed: workflowRun.resumed,
|
|
3756
3943
|
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
3757
3944
|
autoResumed: autoResumeRun != null,
|
|
@@ -3770,6 +3957,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3770
3957
|
rootRepo,
|
|
3771
3958
|
releaseCandidate,
|
|
3772
3959
|
releaseBackMerge,
|
|
3960
|
+
hostedDeploymentState,
|
|
3773
3961
|
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
3774
3962
|
pushStatus: {
|
|
3775
3963
|
stagingPushed: true,
|
|
@@ -3792,7 +3980,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3792
3980
|
])
|
|
3793
3981
|
});
|
|
3794
3982
|
} catch (error) {
|
|
3795
|
-
|
|
3983
|
+
const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
|
|
3796
3984
|
const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
|
|
3797
3985
|
const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
|
|
3798
3986
|
const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
|
|
@@ -3808,14 +3996,22 @@ ${repair.blockers.join("\n")}`, "stderr");
|
|
|
3808
3996
|
} catch (repairError) {
|
|
3809
3997
|
helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
|
|
3810
3998
|
}
|
|
3811
|
-
|
|
3999
|
+
if (localCleanup.restored.length > 0) {
|
|
4000
|
+
helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
|
|
4001
|
+
}
|
|
4002
|
+
if (localCleanup.manualReview.length > 0) {
|
|
4003
|
+
helpers.write(`[release][recovery] Local cleanup needs manual review:
|
|
4004
|
+
${localCleanup.manualReview.map((entry) => `- ${String(entry.repo ?? entry.scope ?? "repo")}: ${String(entry.reason ?? "unknown")}`).join("\n")}`, "stderr");
|
|
4005
|
+
}
|
|
4006
|
+
helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
|
|
3812
4007
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
3813
4008
|
resumable: true,
|
|
3814
4009
|
runId: workflowRun.runId,
|
|
3815
4010
|
command: "release",
|
|
3816
4011
|
message: `Resume the interrupted release on ${STAGING_BRANCH}. Last phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`,
|
|
3817
4012
|
recoverCommand: "npx trsd recover --json",
|
|
3818
|
-
resumeCommand: `npx trsd release --${level} --json
|
|
4013
|
+
resumeCommand: `npx trsd release --${level} --json`,
|
|
4014
|
+
localCleanup
|
|
3819
4015
|
});
|
|
3820
4016
|
throw error;
|
|
3821
4017
|
}
|
|
@@ -3909,7 +4105,29 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3909
4105
|
currentBranch: session.branchName,
|
|
3910
4106
|
currentHeads
|
|
3911
4107
|
});
|
|
3912
|
-
const
|
|
4108
|
+
const markedObsoleteRun = input.obsoleteRunId ? (() => {
|
|
4109
|
+
const entry = classifiedRuns.find((candidate) => candidate.journal.runId === input.obsoleteRunId);
|
|
4110
|
+
if (!entry) {
|
|
4111
|
+
workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
|
|
4112
|
+
}
|
|
4113
|
+
const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
|
|
4114
|
+
const classification = {
|
|
4115
|
+
state: "obsolete",
|
|
4116
|
+
reasons: [reason],
|
|
4117
|
+
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4118
|
+
};
|
|
4119
|
+
archiveWorkflowRun(root, entry.journal.runId, classification);
|
|
4120
|
+
return {
|
|
4121
|
+
runId: entry.journal.runId,
|
|
4122
|
+
command: entry.journal.command,
|
|
4123
|
+
reason
|
|
4124
|
+
};
|
|
4125
|
+
})() : null;
|
|
4126
|
+
const effectiveClassifiedRuns = markedObsoleteRun ? classifyWorkflowRunJournals(root, {
|
|
4127
|
+
currentBranch: session.branchName,
|
|
4128
|
+
currentHeads
|
|
4129
|
+
}) : classifiedRuns;
|
|
4130
|
+
const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
3913
4131
|
runId: journal.runId,
|
|
3914
4132
|
command: journal.command,
|
|
3915
4133
|
status: journal.status,
|
|
@@ -3919,7 +4137,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3919
4137
|
failure: journal.failure,
|
|
3920
4138
|
resumeCommand: `treeseed resume ${journal.runId}`
|
|
3921
4139
|
}));
|
|
3922
|
-
const staleRuns =
|
|
4140
|
+
const staleRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
|
|
3923
4141
|
runId: journal.runId,
|
|
3924
4142
|
command: journal.command,
|
|
3925
4143
|
status: journal.status,
|
|
@@ -3929,7 +4147,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3929
4147
|
failure: journal.failure,
|
|
3930
4148
|
classification
|
|
3931
4149
|
}));
|
|
3932
|
-
const obsoleteRuns =
|
|
4150
|
+
const obsoleteRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
|
|
3933
4151
|
runId: journal.runId,
|
|
3934
4152
|
command: journal.command,
|
|
3935
4153
|
status: journal.status,
|
|
@@ -3952,6 +4170,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3952
4170
|
staleRuns,
|
|
3953
4171
|
obsoleteRuns,
|
|
3954
4172
|
prunedRuns,
|
|
4173
|
+
markedObsoleteRun,
|
|
3955
4174
|
selectedRun,
|
|
3956
4175
|
runCount: journals.length
|
|
3957
4176
|
},
|
package/dist/workflow/runs.js
CHANGED
|
@@ -247,6 +247,16 @@ function isReleaseGateOnlyCompletion(journal) {
|
|
|
247
247
|
const pendingStep = journal.steps.find((step) => step.status === "pending");
|
|
248
248
|
return pendingStep?.id === "release-root-gates" || pendingStep?.id === "release-back-merge" || pendingStep?.id === "cleanup-dev-tags";
|
|
249
249
|
}
|
|
250
|
+
function releaseStepData(journal, stepId) {
|
|
251
|
+
return stringRecord(journal.steps.find((step) => step.id === stepId)?.data);
|
|
252
|
+
}
|
|
253
|
+
function expectedPackageHeadAfterReleaseGate(journal, packageName) {
|
|
254
|
+
const data = releaseStepData(journal, `release-${packageName}`);
|
|
255
|
+
const backMerge = stringRecord(data?.backMerge);
|
|
256
|
+
if (typeof backMerge?.commitSha === "string") return backMerge.commitSha;
|
|
257
|
+
if (typeof data?.commitSha === "string") return data.commitSha;
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
250
260
|
function classifyWorkflowRunJournal(journal, options = {}) {
|
|
251
261
|
const reasons = [];
|
|
252
262
|
const now = options.now ?? nowIso();
|
|
@@ -283,6 +293,22 @@ function classifyWorkflowRunJournal(journal, options = {}) {
|
|
|
283
293
|
reasons.push(`current branch ${options.currentBranch} does not match journal branch ${journal.session.branchName}`);
|
|
284
294
|
}
|
|
285
295
|
const releaseGateOnlyCompletion = isReleaseGateOnlyCompletion(journal);
|
|
296
|
+
if (journal.command === "release" && options.currentHeads && releaseGateOnlyCompletion) {
|
|
297
|
+
const rootRelease = releaseStepData(journal, "release-root");
|
|
298
|
+
const expectedRootHead = typeof rootRelease?.stagingCommit === "string" ? rootRelease.stagingCommit : null;
|
|
299
|
+
const rootHead = options.currentHeads["@treeseed/market"];
|
|
300
|
+
if (rootHead && expectedRootHead && rootHead !== expectedRootHead) {
|
|
301
|
+
reasons.push(`market staging head changed from ${expectedRootHead} to ${rootHead}`);
|
|
302
|
+
}
|
|
303
|
+
const releasePlan = releaseStepData(journal, "release-plan");
|
|
304
|
+
for (const name of selectedReleasePackageNames(releasePlan)) {
|
|
305
|
+
const currentHead = options.currentHeads[name];
|
|
306
|
+
const expectedHead = expectedPackageHeadAfterReleaseGate(journal, name);
|
|
307
|
+
if (currentHead && expectedHead && currentHead !== expectedHead) {
|
|
308
|
+
reasons.push(`${name} staging head changed from ${expectedHead} to ${currentHead}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
286
312
|
if (journal.command === "release" && options.currentHeads && !releaseGateOnlyCompletion) {
|
|
287
313
|
const releasePlan = stringRecord(journal.steps.find((step) => step.id === "release-plan")?.data);
|
|
288
314
|
if (releasePlan) {
|
package/dist/workflow-state.d.ts
CHANGED
package/dist/workflow-state.js
CHANGED
|
@@ -261,6 +261,7 @@ function safeReleaseHistory(repoDir) {
|
|
|
261
261
|
return {
|
|
262
262
|
stagingAheadMain: null,
|
|
263
263
|
stagingBehindMain: null,
|
|
264
|
+
unreleasedStagingCommits: null,
|
|
264
265
|
backMerged: null,
|
|
265
266
|
detail: "Repository root is unavailable."
|
|
266
267
|
};
|
|
@@ -273,16 +274,20 @@ function safeReleaseHistory(repoDir) {
|
|
|
273
274
|
if (!Number.isFinite(stagingAheadMain) || !Number.isFinite(stagingBehindMain)) {
|
|
274
275
|
throw new Error("invalid rev-list output");
|
|
275
276
|
}
|
|
277
|
+
const stagingOnlySubjects = run("git", ["log", "--format=%s", "main..staging"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
278
|
+
const unreleasedStagingCommits = stagingOnlySubjects.filter((subject) => subject !== "release: sync package staging heads" && subject !== "release: back-merge main into staging" && !subject.startsWith("release: back-merge main into staging ")).length;
|
|
276
279
|
return {
|
|
277
280
|
stagingAheadMain,
|
|
278
281
|
stagingBehindMain,
|
|
282
|
+
unreleasedStagingCommits,
|
|
279
283
|
backMerged: stagingBehindMain === 0,
|
|
280
|
-
detail: stagingBehindMain === 0 ? "Staging contains current main release history." : `Staging is missing ${stagingBehindMain} main commit${stagingBehindMain === 1 ? "" : "s"}.`
|
|
284
|
+
detail: stagingBehindMain === 0 && unreleasedStagingCommits === 0 ? stagingAheadMain > 0 ? "Staging contains current main release history and is only ahead by release sync commits." : "Staging contains current main release history." : stagingBehindMain === 0 ? `Staging has ${unreleasedStagingCommits} unreleased commit${unreleasedStagingCommits === 1 ? "" : "s"} and contains current main release history.` : `Staging is missing ${stagingBehindMain} main commit${stagingBehindMain === 1 ? "" : "s"}.`
|
|
281
285
|
};
|
|
282
286
|
} catch {
|
|
283
287
|
return {
|
|
284
288
|
stagingAheadMain: null,
|
|
285
289
|
stagingBehindMain: null,
|
|
290
|
+
unreleasedStagingCommits: null,
|
|
286
291
|
backMerged: null,
|
|
287
292
|
detail: "Could not compare staging and main release history."
|
|
288
293
|
};
|
|
@@ -414,6 +419,8 @@ function resolveTreeseedWorkflowState(cwd, options = {}) {
|
|
|
414
419
|
if (interruptedRuns.length > 0) {
|
|
415
420
|
workflowBlockers.push(`Interrupted workflow runs detected: ${interruptedRuns.map((run2) => run2.runId).join(", ")}.`);
|
|
416
421
|
}
|
|
422
|
+
const releaseHistory = safeReleaseHistory(root);
|
|
423
|
+
const releaseReady = branchRole === "staging" && !dirtyWorktree && (releaseHistory.unreleasedStagingCommits ?? 0) > 0;
|
|
417
424
|
const state = {
|
|
418
425
|
cwd: effectiveCwd,
|
|
419
426
|
workspaceRoot,
|
|
@@ -527,8 +534,8 @@ function resolveTreeseedWorkflowState(cwd, options = {}) {
|
|
|
527
534
|
idleRemainingMs: keyStatus.idleRemainingMs,
|
|
528
535
|
startupPassphraseConfigured: Boolean(process.env.TREESEED_KEY_PASSPHRASE?.trim())
|
|
529
536
|
},
|
|
530
|
-
releaseReady
|
|
531
|
-
releaseHistory
|
|
537
|
+
releaseReady,
|
|
538
|
+
releaseHistory,
|
|
532
539
|
readiness: {
|
|
533
540
|
local: { ready: false, blockers: [], warnings: [] },
|
|
534
541
|
staging: { ready: false, blockers: [], warnings: [] },
|
|
@@ -747,8 +754,13 @@ function recommendTreeseedNextSteps(state) {
|
|
|
747
754
|
}
|
|
748
755
|
if (!state.persistentEnvironments.staging.initialized) {
|
|
749
756
|
recommendations.push({ operation: "config", reason: "Initialize the staging environment before releasing.", input: { environment: ["staging"] } });
|
|
757
|
+
} else if ((state.releaseHistory.unreleasedStagingCommits ?? 0) > 0) {
|
|
758
|
+
recommendations.push({ operation: "release", reason: "Promote unreleased staging commits into production.", input: { bump: "patch" } });
|
|
759
|
+
if (state.managedServices.api.enabled) {
|
|
760
|
+
recommendations.push({ operation: "auth:login", reason: "Keep the local runtime authenticated to the remote API used by managed services." });
|
|
761
|
+
}
|
|
750
762
|
} else {
|
|
751
|
-
recommendations.push({ operation: "
|
|
763
|
+
recommendations.push({ operation: "status", reason: "Inspect staging and production state; no unreleased staging commits are pending." });
|
|
752
764
|
if (state.managedServices.api.enabled) {
|
|
753
765
|
recommendations.push({ operation: "auth:login", reason: "Keep the local runtime authenticated to the remote API used by managed services." });
|
|
754
766
|
}
|
package/dist/workflow.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export type TreeseedWorkflowRecovery = {
|
|
|
25
25
|
recoverCommand?: string | null;
|
|
26
26
|
resumeCommand?: string | null;
|
|
27
27
|
lock?: Record<string, unknown> | null;
|
|
28
|
+
localCleanup?: Record<string, unknown> | null;
|
|
28
29
|
};
|
|
29
30
|
export type TreeseedWorkflowExecutionMode = 'execute' | 'plan';
|
|
30
31
|
export type TreeseedWorkflowWorktreeMode = 'auto' | 'on' | 'off';
|
|
@@ -214,6 +215,7 @@ export type TreeseedReleaseInput = {
|
|
|
214
215
|
ciMode?: TreeseedWorkflowCiMode;
|
|
215
216
|
worktreeMode?: TreeseedWorkflowWorktreeMode;
|
|
216
217
|
workspaceLinks?: 'auto' | 'off';
|
|
218
|
+
fresh?: boolean;
|
|
217
219
|
plan?: boolean;
|
|
218
220
|
dryRun?: boolean;
|
|
219
221
|
};
|
|
@@ -223,6 +225,8 @@ export type TreeseedResumeInput = {
|
|
|
223
225
|
export type TreeseedRecoverInput = {
|
|
224
226
|
runId?: string;
|
|
225
227
|
pruneStale?: boolean;
|
|
228
|
+
obsoleteRunId?: string;
|
|
229
|
+
obsoleteReason?: string;
|
|
226
230
|
};
|
|
227
231
|
export type TreeseedDestroyInput = {
|
|
228
232
|
target?: 'local' | 'staging' | 'prod';
|
package/package.json
CHANGED
|
@@ -184,11 +184,9 @@ jobs:
|
|
|
184
184
|
set -euo pipefail
|
|
185
185
|
npm run verify:local 2>&1 | tee verify.log
|
|
186
186
|
if test -f ./packages/sdk/scripts/check-build-warnings.mjs; then
|
|
187
|
-
node ./packages/sdk/scripts/check-build-warnings.mjs verify.log
|
|
188
|
-
--allow 'Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"'
|
|
187
|
+
node ./packages/sdk/scripts/check-build-warnings.mjs verify.log
|
|
189
188
|
elif test -f ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js; then
|
|
190
|
-
node ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js verify.log
|
|
191
|
-
--allow 'Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"'
|
|
189
|
+
node ./node_modules/@treeseed/sdk/dist/scripts/check-build-warnings.js verify.log
|
|
192
190
|
else
|
|
193
191
|
echo "Unable to resolve @treeseed/sdk warning scanner entrypoint."
|
|
194
192
|
exit 1
|