@treeseed/sdk 0.6.31 → 0.6.33
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/git-workflow.d.ts +2 -1
- package/dist/operations/services/git-workflow.js +2 -2
- package/dist/operations/services/github-actions-verification.js +17 -2
- package/dist/operations/services/github-api.d.ts +10 -0
- package/dist/operations/services/github-api.js +32 -12
- package/dist/scripts/check-build-warnings.js +14 -2
- package/dist/workflow/operations.d.ts +17 -0
- package/dist/workflow/operations.js +195 -13
- package/dist/workflow/runs.js +26 -0
- package/dist/workflow.d.ts +4 -0
- package/package.json +1 -1
|
@@ -162,8 +162,9 @@ export declare function mergeStagingIntoMain(cwd?: any): {
|
|
|
162
162
|
commitSha: string;
|
|
163
163
|
pushed: boolean;
|
|
164
164
|
};
|
|
165
|
-
export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget }?: {
|
|
165
|
+
export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget, quietMerge }?: {
|
|
166
166
|
pushTarget?: boolean | undefined;
|
|
167
|
+
quietMerge?: boolean | undefined;
|
|
167
168
|
}): {
|
|
168
169
|
repoDir: string;
|
|
169
170
|
targetBranch: any;
|
|
@@ -425,13 +425,13 @@ function mergeStagingIntoMain(cwd = workspaceRoot()) {
|
|
|
425
425
|
pushTarget: true
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
|
-
function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true } = {}) {
|
|
428
|
+
function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true, quietMerge = false } = {}) {
|
|
429
429
|
const repoDir = prepareReleaseBranches(cwd);
|
|
430
430
|
checkoutBranch(repoDir, targetBranch);
|
|
431
431
|
if (remoteBranchExists(repoDir, targetBranch)) {
|
|
432
432
|
runGit(["merge", "--ff-only", `origin/${targetBranch}`], { cwd: repoDir });
|
|
433
433
|
}
|
|
434
|
-
runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir });
|
|
434
|
+
runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir, capture: quietMerge });
|
|
435
435
|
pushBranch(repoDir, STAGING_BRANCH);
|
|
436
436
|
if (pushTarget) {
|
|
437
437
|
pushBranch(repoDir, targetBranch);
|
|
@@ -431,6 +431,21 @@ function formatElapsed(seconds) {
|
|
|
431
431
|
function shortSha(value) {
|
|
432
432
|
return value ? value.slice(0, 12) : "(unknown)";
|
|
433
433
|
}
|
|
434
|
+
function activeJobSummary(event) {
|
|
435
|
+
const activeJobs = event.activeJobs ?? [];
|
|
436
|
+
if (activeJobs.length === 0) return "";
|
|
437
|
+
const summaries = activeJobs.slice(0, 2).map((job) => {
|
|
438
|
+
const activeStep = (job.steps ?? []).find((step) => step.status && step.status !== "completed");
|
|
439
|
+
return activeStep?.name ? `${job.name} > ${activeStep.name}` : job.name;
|
|
440
|
+
}).filter(Boolean);
|
|
441
|
+
return summaries.length > 0 ? `; active: ${summaries.join(", ")}` : "";
|
|
442
|
+
}
|
|
443
|
+
function failedJobSummary(event) {
|
|
444
|
+
const failedJobs = event.failedJobs ?? [];
|
|
445
|
+
if (failedJobs.length === 0) return "";
|
|
446
|
+
const names = failedJobs.slice(0, 3).map((job) => job.name).filter(Boolean);
|
|
447
|
+
return names.length > 0 ? `; failed: ${names.join(", ")}` : "";
|
|
448
|
+
}
|
|
434
449
|
function formatGitHubActionsGateProgress(gate, event, operation) {
|
|
435
450
|
const prefix = `[${operation}][gate][${gate.name}] ${event.workflow}`;
|
|
436
451
|
if (event.type === "waiting") {
|
|
@@ -439,12 +454,12 @@ function formatGitHubActionsGateProgress(gate, event, operation) {
|
|
|
439
454
|
if (event.type === "completed") {
|
|
440
455
|
const conclusion = event.conclusion === "success" ? "successfully" : `with conclusion ${event.conclusion ?? "unknown"}`;
|
|
441
456
|
const url2 = event.url ? `: ${event.url}` : "";
|
|
442
|
-
return `${prefix} completed ${conclusion} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
|
|
457
|
+
return `${prefix} completed ${conclusion}${failedJobSummary(event)} in ${formatElapsed(event.elapsedSeconds)}${url2}`;
|
|
443
458
|
}
|
|
444
459
|
const status = event.status ?? "waiting";
|
|
445
460
|
const url = event.url ? `: ${event.url}` : "";
|
|
446
461
|
const run = event.runId ? ` run ${event.runId}` : "";
|
|
447
|
-
return `${prefix}${run} ${status}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
|
|
462
|
+
return `${prefix}${run} ${status}${activeJobSummary(event)}${url} (${formatElapsed(event.elapsedSeconds)} elapsed)`;
|
|
448
463
|
}
|
|
449
464
|
async function waitForGitHubActionsGate(gate, options = {}) {
|
|
450
465
|
const { waitForGitHubWorkflowCompletion } = await import("./github-automation.js");
|
|
@@ -33,6 +33,12 @@ export interface GitHubWorkflowJobSummary {
|
|
|
33
33
|
status: string | null;
|
|
34
34
|
conclusion: string | null;
|
|
35
35
|
url: string | null;
|
|
36
|
+
steps?: GitHubWorkflowJobStepSummary[];
|
|
37
|
+
}
|
|
38
|
+
export interface GitHubWorkflowJobStepSummary {
|
|
39
|
+
name: string;
|
|
40
|
+
status: string | null;
|
|
41
|
+
conclusion: string | null;
|
|
36
42
|
}
|
|
37
43
|
export type GitHubWorkflowProgressEvent = {
|
|
38
44
|
type: 'waiting' | 'running' | 'completed';
|
|
@@ -45,6 +51,10 @@ export type GitHubWorkflowProgressEvent = {
|
|
|
45
51
|
url: string | null;
|
|
46
52
|
status: string | null;
|
|
47
53
|
conclusion: string | null;
|
|
54
|
+
jobs?: GitHubWorkflowJobSummary[];
|
|
55
|
+
activeJobs?: GitHubWorkflowJobSummary[];
|
|
56
|
+
completedJobs?: GitHubWorkflowJobSummary[];
|
|
57
|
+
failedJobs?: GitHubWorkflowJobSummary[];
|
|
48
58
|
};
|
|
49
59
|
export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
|
|
50
60
|
export declare function parseGitHubRepositorySlug(value: string): {
|
|
@@ -529,9 +529,27 @@ function normalizeWorkflowJob(job) {
|
|
|
529
529
|
name: String(job.name ?? ""),
|
|
530
530
|
status: typeof job.status === "string" ? job.status : null,
|
|
531
531
|
conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
|
|
532
|
-
url: typeof job.html_url === "string" ? job.html_url : null
|
|
532
|
+
url: typeof job.html_url === "string" ? job.html_url : null,
|
|
533
|
+
steps: Array.isArray(job.steps) ? job.steps.map((step) => ({
|
|
534
|
+
name: String(step.name ?? ""),
|
|
535
|
+
status: typeof step.status === "string" ? step.status : null,
|
|
536
|
+
conclusion: typeof step.conclusion === "string" ? step.conclusion : null
|
|
537
|
+
})) : []
|
|
533
538
|
};
|
|
534
539
|
}
|
|
540
|
+
async function listWorkflowJobsForProgress(client, owner, repo, runId) {
|
|
541
|
+
try {
|
|
542
|
+
const jobs = await client.rest.actions.listJobsForWorkflowRun({
|
|
543
|
+
owner,
|
|
544
|
+
repo,
|
|
545
|
+
run_id: runId,
|
|
546
|
+
per_page: 100
|
|
547
|
+
});
|
|
548
|
+
return jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
|
|
549
|
+
} catch {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
535
553
|
function sleep(ms) {
|
|
536
554
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
537
555
|
}
|
|
@@ -547,7 +565,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
547
565
|
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
548
566
|
const startedAt = Date.now();
|
|
549
567
|
let lastProgress = null;
|
|
550
|
-
const emitProgress = (type, run = null) => {
|
|
568
|
+
const emitProgress = (type, run = null, jobs = []) => {
|
|
569
|
+
const completedJobs = jobs.filter((job) => job.status === "completed");
|
|
570
|
+
const failedJobs = jobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped");
|
|
571
|
+
const activeJobs = jobs.filter((job) => job.status && job.status !== "completed");
|
|
551
572
|
const event = {
|
|
552
573
|
type,
|
|
553
574
|
repository: `${owner}/${name}`,
|
|
@@ -558,7 +579,11 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
558
579
|
runId: run?.id ?? null,
|
|
559
580
|
url: run?.url ?? null,
|
|
560
581
|
status: run?.status ?? null,
|
|
561
|
-
conclusion: run?.conclusion ?? null
|
|
582
|
+
conclusion: run?.conclusion ?? null,
|
|
583
|
+
jobs,
|
|
584
|
+
activeJobs,
|
|
585
|
+
completedJobs,
|
|
586
|
+
failedJobs
|
|
562
587
|
};
|
|
563
588
|
lastProgress = event;
|
|
564
589
|
onProgress?.(event);
|
|
@@ -587,15 +612,10 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
587
612
|
run_id: match.id
|
|
588
613
|
});
|
|
589
614
|
const normalized = normalizeWorkflowRun(current.data);
|
|
615
|
+
const progressJobs = await listWorkflowJobsForProgress(client, owner, name, match.id);
|
|
590
616
|
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));
|
|
617
|
+
const normalizedJobs = progressJobs;
|
|
618
|
+
emitProgress("completed", normalized, normalizedJobs);
|
|
599
619
|
return {
|
|
600
620
|
status: "completed",
|
|
601
621
|
repository: `${owner}/${name}`,
|
|
@@ -609,7 +629,7 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
|
|
|
609
629
|
failedJobs: normalizedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
|
|
610
630
|
};
|
|
611
631
|
}
|
|
612
|
-
emitProgress("running", normalized);
|
|
632
|
+
emitProgress("running", normalized, progressJobs);
|
|
613
633
|
await sleep(pollSeconds * 1e3);
|
|
614
634
|
}
|
|
615
635
|
} catch (error) {
|
|
@@ -4,11 +4,19 @@ 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
|
+
/Module "url" has been externalized for browser compatibility, imported by ".*libsodium-sumo.*"/u,
|
|
9
|
+
];
|
|
7
10
|
const allowlisted = [];
|
|
8
11
|
const files = [];
|
|
12
|
+
let useDefaultPolicy = true;
|
|
9
13
|
|
|
10
14
|
for (let index = 0; index < args.length; index += 1) {
|
|
11
15
|
const arg = args[index];
|
|
16
|
+
if (arg === '--no-default-policy') {
|
|
17
|
+
useDefaultPolicy = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
12
20
|
if (arg === '--allow') {
|
|
13
21
|
const pattern = args[index + 1];
|
|
14
22
|
if (!pattern) {
|
|
@@ -22,17 +30,21 @@ for (let index = 0; index < args.length; index += 1) {
|
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
if (files.length === 0) {
|
|
25
|
-
throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>]');
|
|
33
|
+
throw new Error('Usage: node check-build-warnings.mjs <log-file> [<log-file> ...] [--allow <regex>] [--no-default-policy]');
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
const warningLines = [];
|
|
37
|
+
const effectiveAllowlisted = [
|
|
38
|
+
...(useDefaultPolicy ? defaultAllowlisted : []),
|
|
39
|
+
...allowlisted,
|
|
40
|
+
];
|
|
29
41
|
for (const file of files) {
|
|
30
42
|
const contents = readFileSync(resolve(process.cwd(), file), 'utf8');
|
|
31
43
|
for (const line of contents.split(/\r?\n/u)) {
|
|
32
44
|
if (!line.includes('[WARN]')) {
|
|
33
45
|
continue;
|
|
34
46
|
}
|
|
35
|
-
if (
|
|
47
|
+
if (effectiveAllowlisted.some((pattern) => pattern.test(line))) {
|
|
36
48
|
continue;
|
|
37
49
|
}
|
|
38
50
|
warningLines.push(line);
|
|
@@ -798,6 +798,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
798
798
|
worktreePath: string | null;
|
|
799
799
|
primaryRoot: string | null;
|
|
800
800
|
ciMode: "hosted" | "off";
|
|
801
|
+
fresh: boolean;
|
|
802
|
+
freshArchivedRuns: never[];
|
|
801
803
|
mode: TreeseedWorkflowMode;
|
|
802
804
|
mergeStrategy: string;
|
|
803
805
|
level: string;
|
|
@@ -844,6 +846,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
844
846
|
mode: "root-only";
|
|
845
847
|
mergeStrategy: string;
|
|
846
848
|
level: "patch" | "major" | "minor";
|
|
849
|
+
fresh: boolean;
|
|
850
|
+
freshArchivedRuns: {
|
|
851
|
+
runId: string;
|
|
852
|
+
reasons: string[];
|
|
853
|
+
}[];
|
|
847
854
|
resumed: boolean;
|
|
848
855
|
resumedRunId: string | null;
|
|
849
856
|
autoResumed: boolean;
|
|
@@ -909,6 +916,11 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
909
916
|
mode: "recursive-workspace";
|
|
910
917
|
mergeStrategy: string;
|
|
911
918
|
level: "patch" | "major" | "minor";
|
|
919
|
+
fresh: boolean;
|
|
920
|
+
freshArchivedRuns: {
|
|
921
|
+
runId: string;
|
|
922
|
+
reasons: string[];
|
|
923
|
+
}[];
|
|
912
924
|
resumed: boolean;
|
|
913
925
|
resumedRunId: string | null;
|
|
914
926
|
autoResumed: boolean;
|
|
@@ -1041,6 +1053,11 @@ export declare function workflowRecover(helpers: WorkflowOperationHelpers, input
|
|
|
1041
1053
|
} | null;
|
|
1042
1054
|
classification: import("./runs.ts").TreeseedWorkflowRunClassification;
|
|
1043
1055
|
}[];
|
|
1056
|
+
markedObsoleteRun: {
|
|
1057
|
+
runId: string;
|
|
1058
|
+
command: TreeseedWorkflowRunCommand;
|
|
1059
|
+
reason: string;
|
|
1060
|
+
} | null;
|
|
1044
1061
|
selectedRun: TreeseedWorkflowRunJournal | null;
|
|
1045
1062
|
runCount: number;
|
|
1046
1063
|
} & {
|
|
@@ -254,13 +254,30 @@ function resolveRootReleaseSubmoduleConflicts(root, selectedPackageNames) {
|
|
|
254
254
|
const packagePaths = new Set(packages.map((pkg) => pkg.repoPath));
|
|
255
255
|
const unresolved = unresolvedMergePaths(gitRoot);
|
|
256
256
|
if (unresolved.length === 0 || unresolved.some((filePath) => !packagePaths.has(filePath))) {
|
|
257
|
-
return
|
|
257
|
+
return {
|
|
258
|
+
resolved: false,
|
|
259
|
+
allUnresolvedPathsWerePackagePointers: unresolved.length > 0 && unresolved.every((filePath) => packagePaths.has(filePath)),
|
|
260
|
+
unresolvedPaths: unresolved,
|
|
261
|
+
entries: []
|
|
262
|
+
};
|
|
258
263
|
}
|
|
264
|
+
const entries = [];
|
|
259
265
|
for (const pkg of packages) {
|
|
260
266
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
261
267
|
run("git", ["add", pkg.repoPath], { cwd: gitRoot });
|
|
268
|
+
entries.push({
|
|
269
|
+
packageName: pkg.name,
|
|
270
|
+
path: pkg.repoPath,
|
|
271
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
272
|
+
resolvedCommit: headCommit(pkg.dir)
|
|
273
|
+
});
|
|
262
274
|
}
|
|
263
|
-
return
|
|
275
|
+
return {
|
|
276
|
+
resolved: true,
|
|
277
|
+
allUnresolvedPathsWerePackagePointers: true,
|
|
278
|
+
unresolvedPaths: unresolved,
|
|
279
|
+
entries
|
|
280
|
+
};
|
|
264
281
|
}
|
|
265
282
|
function unlinkWorkflowWorkspaceLinks(root, helpers, mode = "auto") {
|
|
266
283
|
if (!shouldManageWorkspaceLinks(mode, helpers.context.env)) {
|
|
@@ -1060,6 +1077,123 @@ function releasePlanMatchesCurrentHeads(plan, rootRepo, packageReports) {
|
|
|
1060
1077
|
function releaseRunHasCompletedMutation(journal) {
|
|
1061
1078
|
return journal.steps.some((step) => step.status === "completed" && step.id !== "release-plan" && step.id !== "workspace-unlink");
|
|
1062
1079
|
}
|
|
1080
|
+
function generatedReleaseMetadataFiles(repoDir) {
|
|
1081
|
+
return ["package.json", "package-lock.json", "npm-shrinkwrap.json"].filter((filePath) => {
|
|
1082
|
+
if (existsSync(resolve(repoDir, filePath))) return true;
|
|
1083
|
+
try {
|
|
1084
|
+
run("git", ["ls-files", "--error-unmatch", filePath], { cwd: repoDir, capture: true });
|
|
1085
|
+
return true;
|
|
1086
|
+
} catch {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
function collectReleaseCleanupSnapshot(root, selectedPackageNames) {
|
|
1092
|
+
return {
|
|
1093
|
+
repos: [
|
|
1094
|
+
{
|
|
1095
|
+
name: "@treeseed/market",
|
|
1096
|
+
path: repoRoot(root),
|
|
1097
|
+
branch: currentBranch(repoRoot(root)) || null,
|
|
1098
|
+
files: generatedReleaseMetadataFiles(repoRoot(root))
|
|
1099
|
+
},
|
|
1100
|
+
...checkedOutWorkspacePackageRepos(root).filter((pkg) => selectedPackageNames.has(pkg.name)).map((pkg) => ({
|
|
1101
|
+
name: pkg.name,
|
|
1102
|
+
path: pkg.dir,
|
|
1103
|
+
branch: currentBranch(pkg.dir) || null,
|
|
1104
|
+
files: generatedReleaseMetadataFiles(pkg.dir)
|
|
1105
|
+
}))
|
|
1106
|
+
]
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function restoreReleaseGeneratedMetadata(repo) {
|
|
1110
|
+
const restored = [];
|
|
1111
|
+
const skipped = [];
|
|
1112
|
+
for (const filePath of repo.files) {
|
|
1113
|
+
const status = run("git", ["status", "--porcelain", "--", filePath], { cwd: repo.path, capture: true });
|
|
1114
|
+
if (!status.trim()) {
|
|
1115
|
+
skipped.push(filePath);
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
run("git", ["restore", "--staged", "--worktree", "--", filePath], { cwd: repo.path, capture: true });
|
|
1119
|
+
restored.push(filePath);
|
|
1120
|
+
}
|
|
1121
|
+
return { restored, skipped };
|
|
1122
|
+
}
|
|
1123
|
+
function cleanupFailedReleaseLocalState(root, helpers, snapshot, workspaceLinksMode) {
|
|
1124
|
+
const report = { restored: [], skipped: [], manualReview: [] };
|
|
1125
|
+
try {
|
|
1126
|
+
ensureWorkflowWorkspaceLinks(root, helpers, workspaceLinksMode ?? "auto");
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
report.manualReview.push({
|
|
1129
|
+
scope: "workspace-links",
|
|
1130
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
if (!snapshot) {
|
|
1134
|
+
report.skipped.push({ scope: "release-metadata", reason: "cleanup snapshot was not recorded before failure" });
|
|
1135
|
+
return report;
|
|
1136
|
+
}
|
|
1137
|
+
for (const repo of snapshot.repos) {
|
|
1138
|
+
try {
|
|
1139
|
+
const restored = restoreReleaseGeneratedMetadata(repo);
|
|
1140
|
+
if (repo.branch && currentBranch(repo.path) !== repo.branch) {
|
|
1141
|
+
checkoutBranch(repo.path, repo.branch);
|
|
1142
|
+
}
|
|
1143
|
+
if (restored.restored.length > 0) {
|
|
1144
|
+
report.restored.push({ repo: repo.name, path: repo.path, files: restored.restored });
|
|
1145
|
+
}
|
|
1146
|
+
if (restored.skipped.length > 0) {
|
|
1147
|
+
report.skipped.push({ repo: repo.name, path: repo.path, files: restored.skipped, reason: "unchanged" });
|
|
1148
|
+
}
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
report.manualReview.push({
|
|
1151
|
+
repo: repo.name,
|
|
1152
|
+
path: repo.path,
|
|
1153
|
+
branch: repo.branch,
|
|
1154
|
+
files: repo.files,
|
|
1155
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1156
|
+
nextCommand: repo.branch ? `git -C ${repo.path} restore --staged --worktree -- ${repo.files.join(" ")} && git -C ${repo.path} checkout ${repo.branch}` : null
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return report;
|
|
1161
|
+
}
|
|
1162
|
+
function prepareFreshReleaseRun(root, branch, rootRepo, packageReports) {
|
|
1163
|
+
if (branch !== STAGING_BRANCH) return { archived: [], blockers: [] };
|
|
1164
|
+
const currentHeads = Object.fromEntries([
|
|
1165
|
+
[rootRepo.name, rootRepo.commitSha ?? null],
|
|
1166
|
+
...packageReports.map((report) => [report.name, report.commitSha ?? null])
|
|
1167
|
+
]);
|
|
1168
|
+
const archived = [];
|
|
1169
|
+
const blockers = [];
|
|
1170
|
+
for (const journal of listInterruptedWorkflowRuns(root).filter((entry) => entry.command === "release")) {
|
|
1171
|
+
const classification = classifyWorkflowRunJournal(journal, {
|
|
1172
|
+
currentBranch: branch,
|
|
1173
|
+
currentHeads
|
|
1174
|
+
});
|
|
1175
|
+
if (classification.state === "stale") {
|
|
1176
|
+
archiveWorkflowRun(root, journal.runId, {
|
|
1177
|
+
...classification,
|
|
1178
|
+
reasons: ["fresh release superseded stale failed release", ...classification.reasons]
|
|
1179
|
+
});
|
|
1180
|
+
archived.push({ runId: journal.runId, reasons: classification.reasons });
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (classification.state === "resumable" && releaseRunHasCompletedMutation(journal)) {
|
|
1184
|
+
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.`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (blockers.length > 0) {
|
|
1188
|
+
workflowError("release", "validation_failed", [
|
|
1189
|
+
"Treeseed release --fresh will not bypass a resumable partial release that already completed release mutations.",
|
|
1190
|
+
...blockers
|
|
1191
|
+
].join("\n"), {
|
|
1192
|
+
details: { archived, blockers }
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
return { archived, blockers };
|
|
1196
|
+
}
|
|
1063
1197
|
function findAutoResumableReleaseRun(root, branch, rootRepo, packageReports) {
|
|
1064
1198
|
if (branch !== STAGING_BRANCH) return null;
|
|
1065
1199
|
return listInterruptedWorkflowRuns(root).find((journal) => {
|
|
@@ -3323,8 +3457,10 @@ async function workflowRelease(helpers, input) {
|
|
|
3323
3457
|
const rootRepo = createWorkspaceRootRepoReport(root);
|
|
3324
3458
|
const packageReports = createWorkspacePackageReports(root);
|
|
3325
3459
|
const explicitResumeRunId = helpers.context.workflow?.resumeRunId ?? null;
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
3460
|
+
const freshRelease = input.fresh === true && !explicitResumeRunId;
|
|
3461
|
+
const freshPreparation = freshRelease && executionMode === "execute" ? prepareFreshReleaseRun(root, session.branchName, rootRepo, packageReports) : { archived: [], blockers: [] };
|
|
3462
|
+
const autoResumeRun = executionMode === "execute" && !explicitResumeRunId && !freshRelease ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3463
|
+
const planAutoResumeRun = executionMode === "plan" && input.fresh !== true ? findAutoResumableReleaseRun(root, session.branchName, rootRepo, packageReports) : null;
|
|
3328
3464
|
const effectiveInput = autoResumeRun ? autoResumeRun.input : input;
|
|
3329
3465
|
const level = effectiveInput.bump ?? "patch";
|
|
3330
3466
|
const ciMode = normalizeCiMode(effectiveInput.ciMode, "release");
|
|
@@ -3345,6 +3481,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3345
3481
|
return buildWorkflowResult("release", root, {
|
|
3346
3482
|
...plannedRelease,
|
|
3347
3483
|
ciMode,
|
|
3484
|
+
fresh: input.fresh === true,
|
|
3485
|
+
freshArchivedRuns: [],
|
|
3348
3486
|
...worktreePayload(root, effectiveInput.worktreeMode),
|
|
3349
3487
|
autoResumeCandidate: planAutoResumeRun ? {
|
|
3350
3488
|
runId: planAutoResumeRun.runId,
|
|
@@ -3372,6 +3510,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3372
3510
|
gitDependencyProtocol: effectiveInput.gitDependencyProtocol ?? "preserve-origin",
|
|
3373
3511
|
gitRemoteWriteMode: effectiveInput.gitRemoteWriteMode ?? "ssh-pushurl",
|
|
3374
3512
|
ciMode,
|
|
3513
|
+
fresh: input.fresh === true,
|
|
3375
3514
|
worktreeMode: effectiveInput.worktreeMode ?? "auto",
|
|
3376
3515
|
workspaceLinks: effectiveInput.workspaceLinks ?? "auto"
|
|
3377
3516
|
},
|
|
@@ -3404,6 +3543,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3404
3543
|
if (autoResumeRun) {
|
|
3405
3544
|
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
|
|
3406
3545
|
}
|
|
3546
|
+
let releaseCleanupSnapshot = null;
|
|
3407
3547
|
try {
|
|
3408
3548
|
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
|
|
3409
3549
|
const effectivePackageSelection = releasePlanPackageSelection(releasePlan.packageSelection);
|
|
@@ -3491,6 +3631,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3491
3631
|
mode,
|
|
3492
3632
|
mergeStrategy: "merge-commit",
|
|
3493
3633
|
level,
|
|
3634
|
+
fresh: input.fresh === true,
|
|
3635
|
+
freshArchivedRuns: freshPreparation.archived,
|
|
3494
3636
|
resumed: workflowRun.resumed,
|
|
3495
3637
|
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
3496
3638
|
autoResumed: autoResumeRun != null,
|
|
@@ -3527,6 +3669,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3527
3669
|
prepareReleaseBranches(pkg.dir);
|
|
3528
3670
|
}
|
|
3529
3671
|
}
|
|
3672
|
+
releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
3530
3673
|
const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
3531
3674
|
const releasedPackageDevTags2 = Object.fromEntries(
|
|
3532
3675
|
checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
|
|
@@ -3641,17 +3784,22 @@ async function workflowRelease(helpers, input) {
|
|
|
3641
3784
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3642
3785
|
const stagingCommit = headCommit(gitRoot);
|
|
3643
3786
|
let released;
|
|
3787
|
+
let submoduleReconciliation = null;
|
|
3644
3788
|
try {
|
|
3645
3789
|
released = mergeBranchIntoTarget(root, {
|
|
3646
3790
|
sourceBranch: STAGING_BRANCH,
|
|
3647
3791
|
targetBranch: PRODUCTION_BRANCH,
|
|
3648
3792
|
message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3649
|
-
pushTarget: false
|
|
3793
|
+
pushTarget: false,
|
|
3794
|
+
quietMerge: true
|
|
3650
3795
|
});
|
|
3651
3796
|
} catch (error) {
|
|
3652
|
-
|
|
3797
|
+
const reconciliation = resolveRootReleaseSubmoduleConflicts(root, effectiveSelectedPackageNames);
|
|
3798
|
+
if (!reconciliation.resolved) {
|
|
3653
3799
|
throw error;
|
|
3654
3800
|
}
|
|
3801
|
+
helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
|
|
3802
|
+
submoduleReconciliation = reconciliation;
|
|
3655
3803
|
commitAllIfChanged(gitRoot, `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`);
|
|
3656
3804
|
released = { commitSha: headCommit(gitRoot) };
|
|
3657
3805
|
}
|
|
@@ -3671,7 +3819,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3671
3819
|
stagingCommit,
|
|
3672
3820
|
releasedCommit,
|
|
3673
3821
|
mergeCommit: released.commitSha,
|
|
3674
|
-
tag
|
|
3822
|
+
tag,
|
|
3823
|
+
submoduleReconciliation
|
|
3675
3824
|
};
|
|
3676
3825
|
});
|
|
3677
3826
|
rootRepo.committed = true;
|
|
@@ -3752,6 +3901,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3752
3901
|
mode,
|
|
3753
3902
|
mergeStrategy: "merge-commit",
|
|
3754
3903
|
level,
|
|
3904
|
+
fresh: input.fresh === true,
|
|
3905
|
+
freshArchivedRuns: freshPreparation.archived,
|
|
3755
3906
|
resumed: workflowRun.resumed,
|
|
3756
3907
|
resumedRunId: workflowRun.resumed ? workflowRun.runId : null,
|
|
3757
3908
|
autoResumed: autoResumeRun != null,
|
|
@@ -3792,7 +3943,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3792
3943
|
])
|
|
3793
3944
|
});
|
|
3794
3945
|
} catch (error) {
|
|
3795
|
-
|
|
3946
|
+
const localCleanup = cleanupFailedReleaseLocalState(root, helpers, releaseCleanupSnapshot, effectiveInput.workspaceLinks ?? "auto");
|
|
3796
3947
|
const latestJournal = readWorkflowRunJournal(root, workflowRun.runId);
|
|
3797
3948
|
const lastCompleted = [...latestJournal?.steps ?? []].reverse().find((step) => step.status === "completed") ?? null;
|
|
3798
3949
|
const nextPending = latestJournal?.steps.find((step) => step.status === "pending") ?? null;
|
|
@@ -3808,14 +3959,22 @@ ${repair.blockers.join("\n")}`, "stderr");
|
|
|
3808
3959
|
} catch (repairError) {
|
|
3809
3960
|
helpers.write(`[release][recovery] Package repo repair failed: ${repairError instanceof Error ? repairError.message : String(repairError)}`, "stderr");
|
|
3810
3961
|
}
|
|
3811
|
-
|
|
3962
|
+
if (localCleanup.restored.length > 0) {
|
|
3963
|
+
helpers.write(`[release][recovery] Restored generated release metadata in ${localCleanup.restored.length} repo(s).`, "stderr");
|
|
3964
|
+
}
|
|
3965
|
+
if (localCleanup.manualReview.length > 0) {
|
|
3966
|
+
helpers.write(`[release][recovery] Local cleanup needs manual review:
|
|
3967
|
+
${localCleanup.manualReview.map((entry) => `- ${String(entry.repo ?? entry.scope ?? "repo")}: ${String(entry.reason ?? "unknown")}`).join("\n")}`, "stderr");
|
|
3968
|
+
}
|
|
3969
|
+
helpers.write(`Safe recovery: npx trsd release --${level} --json, npx trsd release --${level} --fresh --json, or inspect with npx trsd recover --json.`, "stderr");
|
|
3812
3970
|
failWorkflowRun(root, workflowRun.runId, error, {
|
|
3813
3971
|
resumable: true,
|
|
3814
3972
|
runId: workflowRun.runId,
|
|
3815
3973
|
command: "release",
|
|
3816
3974
|
message: `Resume the interrupted release on ${STAGING_BRANCH}. Last phase: ${lastCompleted?.id ?? "not-started"}; next phase: ${nextPending?.id ?? "none"}.`,
|
|
3817
3975
|
recoverCommand: "npx trsd recover --json",
|
|
3818
|
-
resumeCommand: `npx trsd release --${level} --json
|
|
3976
|
+
resumeCommand: `npx trsd release --${level} --json`,
|
|
3977
|
+
localCleanup
|
|
3819
3978
|
});
|
|
3820
3979
|
throw error;
|
|
3821
3980
|
}
|
|
@@ -3909,7 +4068,29 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3909
4068
|
currentBranch: session.branchName,
|
|
3910
4069
|
currentHeads
|
|
3911
4070
|
});
|
|
3912
|
-
const
|
|
4071
|
+
const markedObsoleteRun = input.obsoleteRunId ? (() => {
|
|
4072
|
+
const entry = classifiedRuns.find((candidate) => candidate.journal.runId === input.obsoleteRunId);
|
|
4073
|
+
if (!entry) {
|
|
4074
|
+
workflowError("recover", "validation_failed", `Treeseed recover could not find workflow run ${input.obsoleteRunId}.`);
|
|
4075
|
+
}
|
|
4076
|
+
const reason = input.obsoleteReason?.trim() || "marked obsolete by operator";
|
|
4077
|
+
const classification = {
|
|
4078
|
+
state: "obsolete",
|
|
4079
|
+
reasons: [reason],
|
|
4080
|
+
classifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4081
|
+
};
|
|
4082
|
+
archiveWorkflowRun(root, entry.journal.runId, classification);
|
|
4083
|
+
return {
|
|
4084
|
+
runId: entry.journal.runId,
|
|
4085
|
+
command: entry.journal.command,
|
|
4086
|
+
reason
|
|
4087
|
+
};
|
|
4088
|
+
})() : null;
|
|
4089
|
+
const effectiveClassifiedRuns = markedObsoleteRun ? classifyWorkflowRunJournals(root, {
|
|
4090
|
+
currentBranch: session.branchName,
|
|
4091
|
+
currentHeads
|
|
4092
|
+
}) : classifiedRuns;
|
|
4093
|
+
const interruptedRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "resumable").map(({ journal }) => ({
|
|
3913
4094
|
runId: journal.runId,
|
|
3914
4095
|
command: journal.command,
|
|
3915
4096
|
status: journal.status,
|
|
@@ -3919,7 +4100,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3919
4100
|
failure: journal.failure,
|
|
3920
4101
|
resumeCommand: `treeseed resume ${journal.runId}`
|
|
3921
4102
|
}));
|
|
3922
|
-
const staleRuns =
|
|
4103
|
+
const staleRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "stale").map(({ journal, classification }) => ({
|
|
3923
4104
|
runId: journal.runId,
|
|
3924
4105
|
command: journal.command,
|
|
3925
4106
|
status: journal.status,
|
|
@@ -3929,7 +4110,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3929
4110
|
failure: journal.failure,
|
|
3930
4111
|
classification
|
|
3931
4112
|
}));
|
|
3932
|
-
const obsoleteRuns =
|
|
4113
|
+
const obsoleteRuns = effectiveClassifiedRuns.filter((entry) => entry.classification.state === "obsolete").map(({ journal, classification }) => ({
|
|
3933
4114
|
runId: journal.runId,
|
|
3934
4115
|
command: journal.command,
|
|
3935
4116
|
status: journal.status,
|
|
@@ -3952,6 +4133,7 @@ async function workflowRecover(helpers, input = {}) {
|
|
|
3952
4133
|
staleRuns,
|
|
3953
4134
|
obsoleteRuns,
|
|
3954
4135
|
prunedRuns,
|
|
4136
|
+
markedObsoleteRun,
|
|
3955
4137
|
selectedRun,
|
|
3956
4138
|
runCount: journals.length
|
|
3957
4139
|
},
|
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.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';
|