@treeseed/sdk 0.6.37 → 0.6.38
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.js +3 -3
- package/dist/operations/services/github-actions-verification.d.ts +4 -0
- package/dist/operations/services/github-actions-verification.js +5 -3
- package/dist/operations/services/release-history.d.ts +59 -0
- package/dist/operations/services/release-history.js +159 -0
- package/dist/scripts/tenant-build.js +31 -2
- package/dist/workflow/operations.d.ts +14 -2
- package/dist/workflow/operations.js +279 -114
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +12 -9
|
@@ -162,7 +162,7 @@ function remoteBranchExists(repoDir, branchName) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
function fetchOrigin(repoDir) {
|
|
165
|
-
runGit(["fetch", "origin"], { cwd: repoDir });
|
|
165
|
+
runGit(["fetch", "origin"], { cwd: repoDir, capture: true });
|
|
166
166
|
}
|
|
167
167
|
function ensureLocalBranchTracking(repoDir, branchName) {
|
|
168
168
|
if (branchExists(repoDir, branchName)) {
|
|
@@ -175,7 +175,7 @@ function ensureLocalBranchTracking(repoDir, branchName) {
|
|
|
175
175
|
runGit(["checkout", "--orphan", branchName], { cwd: repoDir });
|
|
176
176
|
}
|
|
177
177
|
function checkoutBranch(repoDir, branchName) {
|
|
178
|
-
runGit(["checkout", branchName], { cwd: repoDir });
|
|
178
|
+
runGit(["checkout", branchName], { cwd: repoDir, capture: true });
|
|
179
179
|
}
|
|
180
180
|
function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true, pushIfCreated = false } = {}) {
|
|
181
181
|
const repoDir = assertCleanWorktree(cwd);
|
|
@@ -244,7 +244,7 @@ function syncBranchWithOrigin(repoDir, branchName) {
|
|
|
244
244
|
checkoutBranch(repoDir, branchName);
|
|
245
245
|
}
|
|
246
246
|
if (remoteBranchExists(repoDir, branchName)) {
|
|
247
|
-
runGit(["merge", "--ff-only", `origin/${branchName}`], { cwd: repoDir });
|
|
247
|
+
runGit(["merge", "--ff-only", `origin/${branchName}`], { cwd: repoDir, capture: true });
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
function checkoutDetachedOriginBranch(repoDir, branchName) {
|
|
@@ -16,6 +16,8 @@ export type GitHubActionsWorkflowGate = {
|
|
|
16
16
|
workflow: string;
|
|
17
17
|
branch: string;
|
|
18
18
|
headSha: string;
|
|
19
|
+
timeoutSeconds?: number;
|
|
20
|
+
pollSeconds?: number;
|
|
19
21
|
};
|
|
20
22
|
export type GitHubActionsWorkflowJobStep = {
|
|
21
23
|
name: string;
|
|
@@ -116,6 +118,8 @@ export declare function skippedGitHubActionsGate(gate: GitHubActionsWorkflowGate
|
|
|
116
118
|
url: null;
|
|
117
119
|
createdAt: null;
|
|
118
120
|
updatedAt: null;
|
|
121
|
+
timeoutSeconds: number | null;
|
|
122
|
+
cached: boolean;
|
|
119
123
|
};
|
|
120
124
|
export declare function formatGitHubActionsGateFailure(gate: GitHubActionsWorkflowGate, result: Record<string, unknown>): string;
|
|
121
125
|
export declare function createGitHubActionsGateProgressReporter(gate: GitHubActionsWorkflowGate, options?: {
|
|
@@ -408,7 +408,9 @@ function skippedGitHubActionsGate(gate, reason) {
|
|
|
408
408
|
runId: null,
|
|
409
409
|
url: null,
|
|
410
410
|
createdAt: null,
|
|
411
|
-
updatedAt: null
|
|
411
|
+
updatedAt: null,
|
|
412
|
+
timeoutSeconds: gate.timeoutSeconds ?? null,
|
|
413
|
+
cached: false
|
|
412
414
|
};
|
|
413
415
|
}
|
|
414
416
|
function formatGitHubActionsGateFailure(gate, result) {
|
|
@@ -537,8 +539,8 @@ async function waitForGitHubActionsGate(gate, options = {}) {
|
|
|
537
539
|
workflow: gate.workflow,
|
|
538
540
|
headSha: gate.headSha,
|
|
539
541
|
branch: gate.branch,
|
|
540
|
-
timeoutSeconds: options.timeoutSeconds,
|
|
541
|
-
pollSeconds: options.pollSeconds,
|
|
542
|
+
timeoutSeconds: gate.timeoutSeconds ?? options.timeoutSeconds,
|
|
543
|
+
pollSeconds: gate.pollSeconds ?? options.pollSeconds,
|
|
542
544
|
onProgress: reportProgress
|
|
543
545
|
});
|
|
544
546
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type ReleaseHistoryCommit = {
|
|
2
|
+
sha: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
export type ReleaseHistorySection = 'Added' | 'Changed' | 'Fixed' | 'Infrastructure' | 'Tests' | 'Dependencies';
|
|
7
|
+
export type ReleaseHistorySummary = {
|
|
8
|
+
version: string;
|
|
9
|
+
date: string;
|
|
10
|
+
sourceRef: string;
|
|
11
|
+
targetRef: string;
|
|
12
|
+
commitCount: number;
|
|
13
|
+
sections: Record<ReleaseHistorySection, string[]>;
|
|
14
|
+
notableCommits: ReleaseHistoryCommit[];
|
|
15
|
+
changelogPath: string;
|
|
16
|
+
changelogUpdated: boolean;
|
|
17
|
+
entry: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function collectReleaseHistoryCommits(repoDir: string, sourceRef: string, targetRef: string, options?: {
|
|
20
|
+
maxCommits?: number;
|
|
21
|
+
}): ReleaseHistoryCommit[];
|
|
22
|
+
export declare function renderReleaseChangelogEntry(input: {
|
|
23
|
+
version: string;
|
|
24
|
+
date?: string;
|
|
25
|
+
commits: ReleaseHistoryCommit[];
|
|
26
|
+
extraBullets?: Partial<Record<ReleaseHistorySection, string[]>>;
|
|
27
|
+
}): {
|
|
28
|
+
date: string;
|
|
29
|
+
sections: Record<ReleaseHistorySection, string[]>;
|
|
30
|
+
entry: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function upsertReleaseChangelog(repoDir: string, input: {
|
|
33
|
+
version: string;
|
|
34
|
+
sourceRef: string;
|
|
35
|
+
targetRef: string;
|
|
36
|
+
commits: ReleaseHistoryCommit[];
|
|
37
|
+
extraBullets?: Partial<Record<ReleaseHistorySection, string[]>>;
|
|
38
|
+
}): {
|
|
39
|
+
version: string;
|
|
40
|
+
date: string;
|
|
41
|
+
sourceRef: string;
|
|
42
|
+
targetRef: string;
|
|
43
|
+
commitCount: number;
|
|
44
|
+
sections: Record<ReleaseHistorySection, string[]>;
|
|
45
|
+
notableCommits: ReleaseHistoryCommit[];
|
|
46
|
+
changelogPath: string;
|
|
47
|
+
changelogUpdated: boolean;
|
|
48
|
+
entry: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function renderAdministrativeCommitMessage(input: {
|
|
51
|
+
subject: string;
|
|
52
|
+
version?: string | null;
|
|
53
|
+
tagName?: string | null;
|
|
54
|
+
sourceRef: string;
|
|
55
|
+
targetRef: string;
|
|
56
|
+
commits: ReleaseHistoryCommit[];
|
|
57
|
+
changelog?: ReleaseHistorySummary | null;
|
|
58
|
+
extraLines?: string[];
|
|
59
|
+
}): string;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
const SECTION_ORDER = [
|
|
5
|
+
"Added",
|
|
6
|
+
"Changed",
|
|
7
|
+
"Fixed",
|
|
8
|
+
"Infrastructure",
|
|
9
|
+
"Tests",
|
|
10
|
+
"Dependencies"
|
|
11
|
+
];
|
|
12
|
+
function runGit(repoDir, args) {
|
|
13
|
+
const result = spawnSync("git", args, {
|
|
14
|
+
cwd: repoDir,
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
encoding: "utf8"
|
|
17
|
+
});
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new Error(result.stderr?.trim() || result.stdout?.trim() || `git ${args.join(" ")} failed`);
|
|
20
|
+
}
|
|
21
|
+
return result.stdout;
|
|
22
|
+
}
|
|
23
|
+
function shortSha(value) {
|
|
24
|
+
return value.slice(0, 12);
|
|
25
|
+
}
|
|
26
|
+
function cleanLine(value) {
|
|
27
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
28
|
+
}
|
|
29
|
+
function bulletText(commit) {
|
|
30
|
+
const subject = cleanLine(commit.subject);
|
|
31
|
+
return subject ? `${subject} (${shortSha(commit.sha)})` : shortSha(commit.sha);
|
|
32
|
+
}
|
|
33
|
+
function sectionForCommit(commit) {
|
|
34
|
+
const value = `${commit.subject}
|
|
35
|
+
${commit.body}`.toLowerCase();
|
|
36
|
+
if (/^(feat|add)(\(|:)/u.test(value) || /\badded?\b/u.test(value)) return "Added";
|
|
37
|
+
if (/^(fix|hotfix)(\(|:)/u.test(value) || /\bfix(e[ds])?\b|\bbug\b/u.test(value)) return "Fixed";
|
|
38
|
+
if (/^(test)(\(|:)/u.test(value) || /\btest(s|ing)?\b|\bverify\b/u.test(value)) return "Tests";
|
|
39
|
+
if (/^(deps?|build)(\(|:)/u.test(value) || /\bdependenc(y|ies)\b|\blockfile\b|\bpackage pointer\b/u.test(value)) return "Dependencies";
|
|
40
|
+
if (/^(ci|chore|release)(\(|:)/u.test(value) || /\bdeploy\b|\bworkflow\b|\brelease\b|\bsubmodule\b/u.test(value)) return "Infrastructure";
|
|
41
|
+
return "Changed";
|
|
42
|
+
}
|
|
43
|
+
function uniqueSectionBullets(commits) {
|
|
44
|
+
const sections = Object.fromEntries(SECTION_ORDER.map((section) => [section, []]));
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
for (const commit of commits) {
|
|
47
|
+
const bullet = bulletText(commit);
|
|
48
|
+
const key = bullet.toLowerCase();
|
|
49
|
+
if (seen.has(key)) continue;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
sections[sectionForCommit(commit)].push(bullet);
|
|
52
|
+
}
|
|
53
|
+
return sections;
|
|
54
|
+
}
|
|
55
|
+
function collectReleaseHistoryCommits(repoDir, sourceRef, targetRef, options = {}) {
|
|
56
|
+
const maxCommits = options.maxCommits ?? 80;
|
|
57
|
+
const output = runGit(repoDir, [
|
|
58
|
+
"log",
|
|
59
|
+
"--no-merges",
|
|
60
|
+
`--max-count=${maxCommits}`,
|
|
61
|
+
"--format=%H%x1f%s%x1f%b%x1e",
|
|
62
|
+
`${sourceRef}..${targetRef}`
|
|
63
|
+
]);
|
|
64
|
+
return output.split("").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
65
|
+
const [sha = "", subject = "", body = ""] = entry.split("");
|
|
66
|
+
return { sha: sha.trim(), subject: subject.trim(), body: body.trim() };
|
|
67
|
+
}).filter((commit) => commit.sha.length > 0);
|
|
68
|
+
}
|
|
69
|
+
function renderReleaseChangelogEntry(input) {
|
|
70
|
+
const date = input.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
71
|
+
const sections = uniqueSectionBullets(input.commits);
|
|
72
|
+
for (const [section, bullets] of Object.entries(input.extraBullets ?? {})) {
|
|
73
|
+
for (const bullet of bullets ?? []) {
|
|
74
|
+
const normalized = cleanLine(bullet);
|
|
75
|
+
if (normalized) sections[section].push(normalized);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lines = [`## [${input.version}] - ${date}`, ""];
|
|
79
|
+
let wroteSection = false;
|
|
80
|
+
for (const section of SECTION_ORDER) {
|
|
81
|
+
const bullets = sections[section];
|
|
82
|
+
if (bullets.length === 0) continue;
|
|
83
|
+
wroteSection = true;
|
|
84
|
+
lines.push(`### ${section}`, "");
|
|
85
|
+
for (const bullet of bullets.slice(0, 20)) {
|
|
86
|
+
lines.push(`- ${bullet}`);
|
|
87
|
+
}
|
|
88
|
+
if (bullets.length > 20) {
|
|
89
|
+
lines.push(`- ${bullets.length - 20} additional change${bullets.length - 20 === 1 ? "" : "s"} omitted from this summary.`);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
if (!wroteSection) {
|
|
94
|
+
lines.push("### Changed", "", "- Release metadata and deployment history updated.", "");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
date,
|
|
98
|
+
sections,
|
|
99
|
+
entry: lines.join("\n").trimEnd()
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function upsertReleaseChangelog(repoDir, input) {
|
|
103
|
+
const rendered = renderReleaseChangelogEntry(input);
|
|
104
|
+
const changelogPath = resolve(repoDir, "CHANGELOG.md");
|
|
105
|
+
const current = existsSync(changelogPath) ? readFileSync(changelogPath, "utf8") : "";
|
|
106
|
+
const title = "# Changelog";
|
|
107
|
+
const withoutExisting = current.replace(new RegExp(`^# Changelog\\s*\\n+## \\[${input.version.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\][\\s\\S]*?(?=\\n## \\[|$)`, "u"), `${title}
|
|
108
|
+
|
|
109
|
+
`).trim();
|
|
110
|
+
const body = withoutExisting.startsWith(title) ? withoutExisting.slice(title.length).trim() : withoutExisting.trim();
|
|
111
|
+
const next = `${title}
|
|
112
|
+
|
|
113
|
+
${rendered.entry}${body ? `
|
|
114
|
+
|
|
115
|
+
${body}` : ""}
|
|
116
|
+
`;
|
|
117
|
+
const changed = current !== next;
|
|
118
|
+
if (changed) {
|
|
119
|
+
writeFileSync(changelogPath, next, "utf8");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
version: input.version,
|
|
123
|
+
date: rendered.date,
|
|
124
|
+
sourceRef: input.sourceRef,
|
|
125
|
+
targetRef: input.targetRef,
|
|
126
|
+
commitCount: input.commits.length,
|
|
127
|
+
sections: rendered.sections,
|
|
128
|
+
notableCommits: input.commits.slice(0, 12),
|
|
129
|
+
changelogPath,
|
|
130
|
+
changelogUpdated: changed,
|
|
131
|
+
entry: rendered.entry
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function renderAdministrativeCommitMessage(input) {
|
|
135
|
+
const lines = [
|
|
136
|
+
input.subject,
|
|
137
|
+
"",
|
|
138
|
+
"Release summary:",
|
|
139
|
+
input.version ? `- Version: ${input.version}` : null,
|
|
140
|
+
input.tagName ? `- Tag: ${input.tagName}` : null,
|
|
141
|
+
`- Source: ${input.sourceRef}`,
|
|
142
|
+
`- Target: ${input.targetRef}`,
|
|
143
|
+
`- Promoted commits: ${input.commits.length}`,
|
|
144
|
+
...(input.extraLines ?? []).map((line) => `- ${line}`),
|
|
145
|
+
"",
|
|
146
|
+
"Notable changes:",
|
|
147
|
+
...input.commits.length > 0 ? input.commits.slice(0, 12).map((commit) => `- ${bulletText(commit)}`) : ["- Release metadata and package pointers updated."],
|
|
148
|
+
input.commits.length > 12 ? `- ${input.commits.length - 12} additional promoted commit${input.commits.length - 12 === 1 ? "" : "s"} omitted from this summary.` : null,
|
|
149
|
+
input.changelog ? "" : null,
|
|
150
|
+
input.changelog ? "See CHANGELOG.md for the release history entry." : null
|
|
151
|
+
].filter((line) => line !== null);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
collectReleaseHistoryCommits,
|
|
156
|
+
renderAdministrativeCommitMessage,
|
|
157
|
+
renderReleaseChangelogEntry,
|
|
158
|
+
upsertReleaseChangelog
|
|
159
|
+
};
|
|
@@ -1,11 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createBuildWarningSummary, formatAllowedBuildWarnings } from '../operations/services/build-warning-policy.js';
|
|
3
|
+
import { resolveAstroBin, createProductionBuildEnv, packageScriptPath, runNodeScript } from '../operations/services/runtime-tools.js';
|
|
4
|
+
function runFilteredNodeBinary(binPath, args, options) {
|
|
5
|
+
const result = spawnSync(process.execPath, [binPath, ...args], {
|
|
6
|
+
cwd: options.cwd,
|
|
7
|
+
env: { ...process.env, ...options.env },
|
|
8
|
+
stdio: 'pipe',
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
});
|
|
11
|
+
const warningSummary = createBuildWarningSummary();
|
|
12
|
+
const emitFiltered = (text, stream) => {
|
|
13
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
14
|
+
if (!line)
|
|
15
|
+
continue;
|
|
16
|
+
const classified = warningSummary.record(line);
|
|
17
|
+
if (classified.kind === 'allowed')
|
|
18
|
+
continue;
|
|
19
|
+
stream.write(`${line}\n`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
emitFiltered(result.stdout ?? '', process.stdout);
|
|
23
|
+
emitFiltered(result.stderr ?? '', process.stderr);
|
|
24
|
+
for (const line of formatAllowedBuildWarnings(warningSummary.allowedWarnings)) {
|
|
25
|
+
process.stdout.write(`${line}\n`);
|
|
26
|
+
}
|
|
27
|
+
if (result.status !== 0) {
|
|
28
|
+
process.exit(result.status ?? 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
2
31
|
process.env.TREESEED_LOCAL_DEV_MODE = process.env.TREESEED_LOCAL_DEV_MODE ?? 'cloudflare';
|
|
3
32
|
const publishedRuntime = process.env.TREESEED_CONTENT_SERVING_MODE === 'published_runtime';
|
|
4
33
|
runNodeScript(packageScriptPath('patch-starlight-content-path'), [], { cwd: process.cwd() });
|
|
5
34
|
if (!publishedRuntime) {
|
|
6
35
|
runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
|
|
7
36
|
}
|
|
8
|
-
|
|
37
|
+
runFilteredNodeBinary(resolveAstroBin(), ['build'], {
|
|
9
38
|
cwd: process.cwd(),
|
|
10
39
|
env: createProductionBuildEnv({
|
|
11
40
|
TREESEED_LOCAL_DEV_MODE: process.env.TREESEED_LOCAL_DEV_MODE,
|
|
@@ -44,6 +44,8 @@ type WorkflowRepoReport = {
|
|
|
44
44
|
publishWait: Record<string, unknown> | null;
|
|
45
45
|
workflowGates: Array<Record<string, unknown>>;
|
|
46
46
|
backMerge: Record<string, unknown> | null;
|
|
47
|
+
changelog?: Record<string, unknown> | null;
|
|
48
|
+
adminCommitSummary?: Record<string, unknown> | null;
|
|
47
49
|
};
|
|
48
50
|
export declare function workflowStatus(helpers: WorkflowOperationHelpers, input?: TreeseedWorkflowStatusOptions): Promise<TreeseedWorkflowResult<import("../workflow-state.ts").TreeseedWorkflowState>>;
|
|
49
51
|
export declare function workflowCi(helpers: WorkflowOperationHelpers, input?: TreeseedCiInput): Promise<TreeseedWorkflowResult<TreeseedCiResult>>;
|
|
@@ -417,6 +419,8 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
|
|
|
417
419
|
url: null;
|
|
418
420
|
createdAt: null;
|
|
419
421
|
updatedAt: null;
|
|
422
|
+
timeoutSeconds: number | null;
|
|
423
|
+
cached: boolean;
|
|
420
424
|
}[];
|
|
421
425
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
422
426
|
} & {
|
|
@@ -557,6 +561,8 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
|
|
|
557
561
|
url: null;
|
|
558
562
|
createdAt: null;
|
|
559
563
|
updatedAt: null;
|
|
564
|
+
timeoutSeconds: number | null;
|
|
565
|
+
cached: boolean;
|
|
560
566
|
}[];
|
|
561
567
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
562
568
|
} & {
|
|
@@ -738,6 +744,8 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
|
|
|
738
744
|
url: null;
|
|
739
745
|
createdAt: null;
|
|
740
746
|
updatedAt: null;
|
|
747
|
+
timeoutSeconds: number | null;
|
|
748
|
+
cached: boolean;
|
|
741
749
|
}[];
|
|
742
750
|
releaseCandidate: ReleaseCandidateReport | null;
|
|
743
751
|
} & {
|
|
@@ -874,7 +882,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
874
882
|
publishWait: never[];
|
|
875
883
|
repos: never[];
|
|
876
884
|
rootRepo: WorkflowRepoReport;
|
|
877
|
-
releaseCandidate: ReleaseCandidateReport;
|
|
885
|
+
releaseCandidate: ReleaseCandidateReport | null;
|
|
878
886
|
releaseBackMerge: {
|
|
879
887
|
status: string;
|
|
880
888
|
merged: boolean;
|
|
@@ -914,6 +922,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
914
922
|
url: null;
|
|
915
923
|
createdAt: null;
|
|
916
924
|
updatedAt: null;
|
|
925
|
+
timeoutSeconds: number | null;
|
|
926
|
+
cached: boolean;
|
|
917
927
|
}[];
|
|
918
928
|
} & {
|
|
919
929
|
finalState?: WorkflowStatePayload;
|
|
@@ -959,7 +969,7 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
959
969
|
publishWait: Record<string, unknown>[];
|
|
960
970
|
repos: WorkflowRepoReport[];
|
|
961
971
|
rootRepo: WorkflowRepoReport;
|
|
962
|
-
releaseCandidate: ReleaseCandidateReport;
|
|
972
|
+
releaseCandidate: ReleaseCandidateReport | null;
|
|
963
973
|
releaseBackMerge: {
|
|
964
974
|
status: string;
|
|
965
975
|
merged: boolean;
|
|
@@ -999,6 +1009,8 @@ export declare function workflowRelease(helpers: WorkflowOperationHelpers, input
|
|
|
999
1009
|
url: null;
|
|
1000
1010
|
createdAt: null;
|
|
1001
1011
|
updatedAt: null;
|
|
1012
|
+
timeoutSeconds: number | null;
|
|
1013
|
+
cached: boolean;
|
|
1002
1014
|
})[];
|
|
1003
1015
|
} & {
|
|
1004
1016
|
finalState?: WorkflowStatePayload;
|
|
@@ -57,7 +57,6 @@ import {
|
|
|
57
57
|
headCommit,
|
|
58
58
|
listTaskBranches,
|
|
59
59
|
mergeBranchIntoTarget,
|
|
60
|
-
mergeStagingIntoMain,
|
|
61
60
|
prepareReleaseBranches,
|
|
62
61
|
PRODUCTION_BRANCH,
|
|
63
62
|
pushBranch,
|
|
@@ -78,6 +77,11 @@ import {
|
|
|
78
77
|
import {
|
|
79
78
|
runReleaseCandidateGate
|
|
80
79
|
} from "../operations/services/release-candidate.js";
|
|
80
|
+
import {
|
|
81
|
+
collectReleaseHistoryCommits,
|
|
82
|
+
renderAdministrativeCommitMessage,
|
|
83
|
+
upsertReleaseChangelog
|
|
84
|
+
} from "../operations/services/release-history.js";
|
|
81
85
|
import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../operations/services/runtime-tools.js";
|
|
82
86
|
import { runTenantDeployPreflight, runWorkspaceReleasePreflight, runWorkspaceSavePreflight } from "../operations/services/save-deploy-preflight.js";
|
|
83
87
|
import { collectCliPreflight } from "../operations/services/workspace-preflight.js";
|
|
@@ -402,7 +406,9 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
402
406
|
...result,
|
|
403
407
|
workflow: String(result.workflow ?? gate.workflow),
|
|
404
408
|
branch: String(result.branch ?? gate.branch),
|
|
405
|
-
headSha: String(result.headSha ?? gate.headSha)
|
|
409
|
+
headSha: String(result.headSha ?? gate.headSha),
|
|
410
|
+
timeoutSeconds: gate.timeoutSeconds ?? null,
|
|
411
|
+
cached: false
|
|
406
412
|
};
|
|
407
413
|
if (normalized.status === "completed" && normalized.conclusion !== "success") {
|
|
408
414
|
workflowError(operation, "github_workflow_failed", formatGitHubActionsGateFailure(gate, normalized), {
|
|
@@ -416,13 +422,21 @@ async function waitForWorkflowGates(operation, gates, ciMode, options = {}) {
|
|
|
416
422
|
}
|
|
417
423
|
return results;
|
|
418
424
|
}
|
|
425
|
+
const RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS = 45 * 60;
|
|
426
|
+
function releaseDeployGate(gate) {
|
|
427
|
+
return {
|
|
428
|
+
...gate,
|
|
429
|
+
timeoutSeconds: gate.timeoutSeconds ?? RELEASE_DEPLOY_GATE_TIMEOUT_SECONDS
|
|
430
|
+
};
|
|
431
|
+
}
|
|
419
432
|
function recordHostedDeploymentStatesFromRootGates(root, rootRelease, workflowGates) {
|
|
420
433
|
const gates = Array.isArray(workflowGates) ? workflowGates.map((gate) => stringRecord(gate)).filter((gate) => Boolean(gate)) : [];
|
|
421
434
|
const releaseRecord = stringRecord(rootRelease) ?? {};
|
|
422
435
|
const reports = [];
|
|
436
|
+
const releaseTag = typeof releaseRecord.rootVersion === "string" ? releaseRecord.rootVersion : null;
|
|
423
437
|
for (const target of [
|
|
424
438
|
{ scope: "staging", branch: STAGING_BRANCH, commit: releaseRecord.stagingCommit },
|
|
425
|
-
{ scope: "prod", branch: PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
|
|
439
|
+
{ scope: "prod", branch: releaseTag ?? PRODUCTION_BRANCH, commit: releaseRecord.releasedCommit }
|
|
426
440
|
]) {
|
|
427
441
|
const gate = gates.find((candidate) => candidate.workflow === "deploy.yml" && candidate.branch === target.branch && candidate.status === "completed" && candidate.conclusion === "success");
|
|
428
442
|
const timestamp = typeof gate?.updatedAt === "string" && gate.updatedAt.trim() ? gate.updatedAt : null;
|
|
@@ -702,13 +716,13 @@ function remoteTagCommit(repoDir, tagName) {
|
|
|
702
716
|
const direct = output.split("\n").find((line) => line.endsWith(`refs/tags/${tagName}`));
|
|
703
717
|
return (peeled ?? direct)?.split(/\s+/u)[0] ?? null;
|
|
704
718
|
}
|
|
705
|
-
function ensureReleaseTag(repoDir, tagName, commitSha) {
|
|
719
|
+
function ensureReleaseTag(repoDir, tagName, commitSha, message) {
|
|
706
720
|
const localCommit = gitObjectCommit(repoDir, tagName);
|
|
707
721
|
if (localCommit && localCommit !== commitSha) {
|
|
708
722
|
throw new Error(`Release tag ${tagName} already exists locally at ${localCommit}, expected ${commitSha}.`);
|
|
709
723
|
}
|
|
710
724
|
if (!localCommit) {
|
|
711
|
-
run("git", ["tag", "-a", tagName, commitSha, "-m", `release: ${tagName}`], { cwd: repoDir });
|
|
725
|
+
run("git", ["tag", "-a", tagName, commitSha, "-m", message ?? `release: ${tagName}`], { cwd: repoDir });
|
|
712
726
|
}
|
|
713
727
|
const remoteCommit = remoteTagCommit(repoDir, tagName);
|
|
714
728
|
if (remoteCommit && remoteCommit !== commitSha) {
|
|
@@ -731,6 +745,51 @@ function commitAllIfChanged(repoDir, message) {
|
|
|
731
745
|
run("git", ["commit", "-m", message], { cwd: repoDir });
|
|
732
746
|
return { committed: true, commitSha: headCommit(repoDir) };
|
|
733
747
|
}
|
|
748
|
+
function releaseHistoryCommits(repoDir, sourceRef = `origin/${PRODUCTION_BRANCH}`, targetRef = "HEAD") {
|
|
749
|
+
try {
|
|
750
|
+
return collectReleaseHistoryCommits(repoDir, sourceRef, targetRef);
|
|
751
|
+
} catch {
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function versionLines(versions) {
|
|
756
|
+
return [...(versions ?? /* @__PURE__ */ new Map()).entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, version]) => `${name}: ${version}`);
|
|
757
|
+
}
|
|
758
|
+
function updateReleaseChangelog(repoDir, input) {
|
|
759
|
+
const sourceRef = input.sourceRef ?? `origin/${PRODUCTION_BRANCH}`;
|
|
760
|
+
const targetRef = input.targetRef ?? "HEAD";
|
|
761
|
+
const commits = input.commits ?? releaseHistoryCommits(repoDir, sourceRef, targetRef);
|
|
762
|
+
return upsertReleaseChangelog(repoDir, {
|
|
763
|
+
version: input.version,
|
|
764
|
+
sourceRef,
|
|
765
|
+
targetRef,
|
|
766
|
+
commits,
|
|
767
|
+
extraBullets: input.extraDependencyBullets?.length ? { Dependencies: input.extraDependencyBullets } : void 0
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function releaseAdminMessage(input) {
|
|
771
|
+
return renderAdministrativeCommitMessage({
|
|
772
|
+
subject: input.subject,
|
|
773
|
+
version: input.version,
|
|
774
|
+
tagName: input.tagName,
|
|
775
|
+
sourceRef: input.sourceRef ?? STAGING_BRANCH,
|
|
776
|
+
targetRef: input.targetRef ?? PRODUCTION_BRANCH,
|
|
777
|
+
commits: input.commits ?? [],
|
|
778
|
+
changelog: input.changelog ?? null,
|
|
779
|
+
extraLines: input.extraLines
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function completedJournalStepData(root, runId, stepId) {
|
|
783
|
+
const journal = readWorkflowRunJournal(root, runId);
|
|
784
|
+
return stringRecord(journal?.steps.find((step) => step.id === stepId && step.status === "completed")?.data);
|
|
785
|
+
}
|
|
786
|
+
function shouldResumeReleaseAtRootGates(root, runId) {
|
|
787
|
+
const journal = readWorkflowRunJournal(root, runId);
|
|
788
|
+
if (!journal || journal.command !== "release") return false;
|
|
789
|
+
const rootStep = journal.steps.find((step) => step.id === "release-root");
|
|
790
|
+
const gateStep = journal.steps.find((step) => step.id === "release-root-gates");
|
|
791
|
+
return rootStep?.status === "completed" && gateStep?.status !== "completed";
|
|
792
|
+
}
|
|
734
793
|
function createNextSteps(steps) {
|
|
735
794
|
return steps.map(renderWorkflowStep);
|
|
736
795
|
}
|
|
@@ -1397,8 +1456,25 @@ function runReleaseNpmInstall(repoDir, options = {}) {
|
|
|
1397
1456
|
if (shouldSkipReleaseInstall()) {
|
|
1398
1457
|
return { status: "skipped", reason: "disabled" };
|
|
1399
1458
|
}
|
|
1400
|
-
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false"];
|
|
1401
|
-
|
|
1459
|
+
const args = repoDir === options.workspaceRoot ? ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"] : ["install", "--package-lock-only", "--ignore-scripts", "--workspaces=false", "--no-audit", "--no-fund"];
|
|
1460
|
+
const result = spawnSync("npm", args, {
|
|
1461
|
+
cwd: repoDir,
|
|
1462
|
+
env: {
|
|
1463
|
+
...process.env,
|
|
1464
|
+
npm_config_audit: "false",
|
|
1465
|
+
npm_config_fund: "false"
|
|
1466
|
+
},
|
|
1467
|
+
stdio: "pipe",
|
|
1468
|
+
encoding: "utf8"
|
|
1469
|
+
});
|
|
1470
|
+
if (result.status !== 0) {
|
|
1471
|
+
const detail = [
|
|
1472
|
+
result.error?.message,
|
|
1473
|
+
result.stderr?.trim(),
|
|
1474
|
+
result.stdout?.trim()
|
|
1475
|
+
].filter(Boolean).join("\n");
|
|
1476
|
+
throw new Error(detail || `npm ${args.join(" ")} failed`);
|
|
1477
|
+
}
|
|
1402
1478
|
return { status: "completed", reason: null };
|
|
1403
1479
|
}
|
|
1404
1480
|
function pathIsWithin(parent, candidate) {
|
|
@@ -1416,7 +1492,7 @@ function assertNoInternalDevReferencesForRepo(root, repoDir, packageNames) {
|
|
|
1416
1492
|
throw new Error(`Stable release still contains internal Git/dev dependency references.
|
|
1417
1493
|
${rendered}`);
|
|
1418
1494
|
}
|
|
1419
|
-
function backMergeProductionIntoStaging(repoDir, repoName) {
|
|
1495
|
+
function backMergeProductionIntoStaging(repoDir, repoName, message) {
|
|
1420
1496
|
syncBranchWithOrigin(repoDir, PRODUCTION_BRANCH);
|
|
1421
1497
|
syncBranchWithOrigin(repoDir, STAGING_BRANCH);
|
|
1422
1498
|
checkoutBranch(repoDir, STAGING_BRANCH);
|
|
@@ -1433,7 +1509,7 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
|
|
|
1433
1509
|
} catch {
|
|
1434
1510
|
}
|
|
1435
1511
|
try {
|
|
1436
|
-
run("git", ["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
|
|
1512
|
+
run("git", ["merge", "--no-ff", `origin/${PRODUCTION_BRANCH}`, "-m", message ?? `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`], { cwd: repoDir });
|
|
1437
1513
|
} catch (error) {
|
|
1438
1514
|
const report = collectMergeConflictReport(repoDir);
|
|
1439
1515
|
throw new TreeseedWorkflowError("release", "merge_conflict", formatMergeConflictReport(report, repoDir, STAGING_BRANCH), {
|
|
@@ -1451,14 +1527,32 @@ function backMergeProductionIntoStaging(repoDir, repoName) {
|
|
|
1451
1527
|
commitSha: headCommit(repoDir)
|
|
1452
1528
|
};
|
|
1453
1529
|
}
|
|
1454
|
-
function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads) {
|
|
1530
|
+
function backMergeRootProductionIntoStaging(root, syncPackageStagingHeads, options = {}) {
|
|
1455
1531
|
const gitRoot = repoRoot(root);
|
|
1456
|
-
const
|
|
1532
|
+
const commits = releaseHistoryCommits(gitRoot, STAGING_BRANCH, `origin/${PRODUCTION_BRANCH}`);
|
|
1533
|
+
const backMerge = backMergeProductionIntoStaging(gitRoot, "@treeseed/market", releaseAdminMessage({
|
|
1534
|
+
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
1535
|
+
version: options.version,
|
|
1536
|
+
sourceRef: PRODUCTION_BRANCH,
|
|
1537
|
+
targetRef: STAGING_BRANCH,
|
|
1538
|
+
commits,
|
|
1539
|
+
changelog: options.changelog ?? null,
|
|
1540
|
+
extraLines: versionLines(options.selectedVersions).map((line) => `Released package ${line}`)
|
|
1541
|
+
}));
|
|
1457
1542
|
if (!syncPackageStagingHeads) {
|
|
1458
1543
|
return backMerge;
|
|
1459
1544
|
}
|
|
1460
1545
|
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
1461
|
-
const
|
|
1546
|
+
const pointerCommits = releaseHistoryCommits(gitRoot, `origin/${STAGING_BRANCH}`, "HEAD");
|
|
1547
|
+
const pointerSync = commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
1548
|
+
subject: "release: sync package staging heads",
|
|
1549
|
+
version: options.version,
|
|
1550
|
+
sourceRef: "package staging heads",
|
|
1551
|
+
targetRef: STAGING_BRANCH,
|
|
1552
|
+
commits: pointerCommits,
|
|
1553
|
+
changelog: options.changelog ?? null,
|
|
1554
|
+
extraLines: versionLines(options.selectedVersions).map((line) => `Staging package ${line}`)
|
|
1555
|
+
}));
|
|
1462
1556
|
if (pointerSync.committed) {
|
|
1463
1557
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
1464
1558
|
}
|
|
@@ -3573,6 +3667,10 @@ async function workflowRelease(helpers, input) {
|
|
|
3573
3667
|
if (autoResumeRun) {
|
|
3574
3668
|
helpers.write(`[workflow][resume] Resuming interrupted release ${autoResumeRun.runId} on ${STAGING_BRANCH}.`);
|
|
3575
3669
|
}
|
|
3670
|
+
const resumeAtRootGates = workflowRun.resumed && shouldResumeReleaseAtRootGates(root, workflowRun.runId);
|
|
3671
|
+
if (resumeAtRootGates) {
|
|
3672
|
+
helpers.write(`[workflow][resume] Resuming release ${workflowRun.runId} directly at production deploy gates.`);
|
|
3673
|
+
}
|
|
3576
3674
|
let releaseCleanupSnapshot = null;
|
|
3577
3675
|
try {
|
|
3578
3676
|
const releasePlan = await executeJournalStep(root, workflowRun.runId, "release-plan", () => plannedRelease);
|
|
@@ -3582,30 +3680,67 @@ async function workflowRelease(helpers, input) {
|
|
|
3582
3680
|
const rootVersion = String(releasePlan.rootVersion);
|
|
3583
3681
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
3584
3682
|
assertReleaseGitHubAutomationReady(root, effectiveSelectedPackageNames, ciMode);
|
|
3585
|
-
const releaseCandidate = await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
|
|
3586
|
-
if (!isResume) {
|
|
3683
|
+
const releaseCandidate = resumeAtRootGates ? completedJournalStepData(root, workflowRun.runId, "release-candidate") : await executeJournalStep(root, workflowRun.runId, "release-candidate", () => runReleaseCandidateForPlan("release", root, releasePlan, { allowReuse: true }));
|
|
3684
|
+
if (!resumeAtRootGates && !isResume) {
|
|
3587
3685
|
assertSessionBranchSafety("release", session, { requireCleanPackages: true, requireCurrentBranch: true });
|
|
3588
3686
|
assertCleanWorktree(root);
|
|
3589
3687
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3688
|
+
if (!resumeAtRootGates) {
|
|
3689
|
+
prepareReleaseBranches(root);
|
|
3690
|
+
ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
3691
|
+
runWorkspaceReleasePreflight({ cwd: root });
|
|
3692
|
+
await executeJournalStep(root, workflowRun.runId, "workspace-unlink", () => unlinkWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto"), { rerunCompleted: true });
|
|
3693
|
+
}
|
|
3594
3694
|
if (mode === "root-only") {
|
|
3595
3695
|
const rootRelease2 = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
3596
3696
|
setRootPackageJsonVersion(root, rootVersion);
|
|
3597
3697
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
3598
|
-
|
|
3698
|
+
const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
|
|
3699
|
+
const changelog = updateReleaseChangelog(gitRoot, {
|
|
3700
|
+
version: rootVersion,
|
|
3701
|
+
commits: rootCommitsBeforeChangelog,
|
|
3702
|
+
extraDependencyBullets: [`Release @treeseed/market ${rootVersion}.`]
|
|
3703
|
+
});
|
|
3704
|
+
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
3705
|
+
subject: `release: ${level} bump`,
|
|
3706
|
+
version: rootVersion,
|
|
3707
|
+
tagName: rootVersion,
|
|
3708
|
+
commits: rootCommitsBeforeChangelog,
|
|
3709
|
+
changelog
|
|
3710
|
+
}));
|
|
3599
3711
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3600
3712
|
const stagingCommit = headCommit(gitRoot);
|
|
3601
|
-
const
|
|
3602
|
-
const
|
|
3713
|
+
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
3714
|
+
const released = mergeBranchIntoTarget(root, {
|
|
3715
|
+
sourceBranch: STAGING_BRANCH,
|
|
3716
|
+
targetBranch: PRODUCTION_BRANCH,
|
|
3717
|
+
message: releaseAdminMessage({
|
|
3718
|
+
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3719
|
+
version: rootVersion,
|
|
3720
|
+
tagName: rootVersion,
|
|
3721
|
+
commits: rootCommits,
|
|
3722
|
+
changelog
|
|
3723
|
+
}),
|
|
3724
|
+
pushTarget: true
|
|
3725
|
+
});
|
|
3726
|
+
const tag = ensureReleaseTag(gitRoot, rootVersion, released.commitSha, releaseAdminMessage({
|
|
3727
|
+
subject: `release: ${rootVersion}`,
|
|
3728
|
+
version: rootVersion,
|
|
3729
|
+
tagName: rootVersion,
|
|
3730
|
+
commits: rootCommits,
|
|
3731
|
+
changelog
|
|
3732
|
+
}));
|
|
3603
3733
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
3604
3734
|
return {
|
|
3605
3735
|
rootVersion,
|
|
3606
3736
|
stagingCommit,
|
|
3607
3737
|
releasedCommit: released.commitSha,
|
|
3608
|
-
tag
|
|
3738
|
+
tag,
|
|
3739
|
+
changelog,
|
|
3740
|
+
adminCommitSummary: {
|
|
3741
|
+
commitCount: rootCommits.length,
|
|
3742
|
+
notableCommits: rootCommits.slice(0, 12)
|
|
3743
|
+
}
|
|
3609
3744
|
};
|
|
3610
3745
|
});
|
|
3611
3746
|
rootRepo.committed = true;
|
|
@@ -3615,48 +3750,23 @@ async function workflowRelease(helpers, input) {
|
|
|
3615
3750
|
rootRepo.commitSha = String(rootRelease2?.releasedCommit ?? headCommit(gitRoot));
|
|
3616
3751
|
rootRepo.tagName = String(rootRelease2?.rootVersion ?? "");
|
|
3617
3752
|
const rootWorkflowGateResult2 = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3618
|
-
{
|
|
3619
|
-
name: rootRepo.name,
|
|
3620
|
-
repoPath: rootRepo.path,
|
|
3621
|
-
workflow: "verify.yml",
|
|
3622
|
-
branch: STAGING_BRANCH,
|
|
3623
|
-
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3624
|
-
},
|
|
3625
|
-
{
|
|
3753
|
+
releaseDeployGate({
|
|
3626
3754
|
name: rootRepo.name,
|
|
3627
3755
|
repoPath: rootRepo.path,
|
|
3628
3756
|
workflow: "deploy.yml",
|
|
3629
|
-
branch: STAGING_BRANCH,
|
|
3630
|
-
headSha: String(rootRelease2?.stagingCommit ?? "")
|
|
3631
|
-
},
|
|
3632
|
-
{
|
|
3633
|
-
name: rootRepo.name,
|
|
3634
|
-
repoPath: rootRepo.path,
|
|
3635
|
-
workflow: "verify.yml",
|
|
3636
3757
|
branch: rootVersion,
|
|
3637
3758
|
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3638
|
-
}
|
|
3639
|
-
{
|
|
3640
|
-
name: rootRepo.name,
|
|
3641
|
-
repoPath: rootRepo.path,
|
|
3642
|
-
workflow: "verify.yml",
|
|
3643
|
-
branch: PRODUCTION_BRANCH,
|
|
3644
|
-
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3645
|
-
},
|
|
3646
|
-
{
|
|
3647
|
-
name: rootRepo.name,
|
|
3648
|
-
repoPath: rootRepo.path,
|
|
3649
|
-
workflow: "deploy.yml",
|
|
3650
|
-
branch: PRODUCTION_BRANCH,
|
|
3651
|
-
headSha: String(rootRelease2?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3652
|
-
}
|
|
3759
|
+
})
|
|
3653
3760
|
].filter((gate) => gate.headSha), ciMode, {
|
|
3654
3761
|
root,
|
|
3655
3762
|
runId: workflowRun.runId,
|
|
3656
3763
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3657
3764
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3658
3765
|
const hostedDeploymentState2 = recordHostedDeploymentStatesFromRootGates(root, rootRelease2, rootWorkflowGateResult2?.workflowGates);
|
|
3659
|
-
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false
|
|
3766
|
+
const releaseBackMerge2 = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, false, {
|
|
3767
|
+
version: rootVersion,
|
|
3768
|
+
changelog: rootRelease2?.changelog ?? null
|
|
3769
|
+
}));
|
|
3660
3770
|
const workspaceLinks2 = ensureWorkflowWorkspaceLinks(root, helpers, effectiveInput.workspaceLinks ?? "auto");
|
|
3661
3771
|
const payload2 = {
|
|
3662
3772
|
mode,
|
|
@@ -3695,13 +3805,15 @@ async function workflowRelease(helpers, input) {
|
|
|
3695
3805
|
])
|
|
3696
3806
|
});
|
|
3697
3807
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3808
|
+
if (!resumeAtRootGates) {
|
|
3809
|
+
validatePackageReleaseWorkflows(root, effectivePackageSelection.selected);
|
|
3810
|
+
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
3811
|
+
if (effectiveSelectedPackageNames.has(pkg.name)) {
|
|
3812
|
+
prepareReleaseBranches(pkg.dir);
|
|
3813
|
+
}
|
|
3702
3814
|
}
|
|
3703
3815
|
}
|
|
3704
|
-
releaseCleanupSnapshot = collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
3816
|
+
releaseCleanupSnapshot = resumeAtRootGates ? null : collectReleaseCleanupSnapshot(root, effectiveSelectedPackageNames);
|
|
3705
3817
|
const metadata = await executeJournalStep(root, workflowRun.runId, "prepare-release-metadata", () => {
|
|
3706
3818
|
const releasedPackageDevTags2 = Object.fromEntries(
|
|
3707
3819
|
checkedOutWorkspacePackageRepos(root).filter((pkg) => effectiveSelectedPackageNames.has(pkg.name)).map((pkg) => {
|
|
@@ -3721,7 +3833,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3721
3833
|
replacedDevReferences: replacedDevReferences2,
|
|
3722
3834
|
releaseInstalls: releaseInstalls2
|
|
3723
3835
|
};
|
|
3724
|
-
}, { rerunCompleted: workflowRun.resumed });
|
|
3836
|
+
}, { rerunCompleted: workflowRun.resumed && !resumeAtRootGates });
|
|
3725
3837
|
const replacedDevReferences = Array.isArray(metadata?.replacedDevReferences) ? metadata.replacedDevReferences : [];
|
|
3726
3838
|
const releaseInstalls = Array.isArray(metadata?.releaseInstalls) ? metadata.releaseInstalls : [];
|
|
3727
3839
|
const releasedPackageDevTags = new Map(Object.entries(metadata?.releasedPackageDevTags ?? {}).map(([name, version]) => [name, String(version)]));
|
|
@@ -3736,24 +3848,52 @@ async function workflowRelease(helpers, input) {
|
|
|
3736
3848
|
}
|
|
3737
3849
|
const releasedPackage = await executeJournalStep(root, workflowRun.runId, `release-${report.name}`, async () => {
|
|
3738
3850
|
checkoutBranch(pkg.dir, STAGING_BRANCH);
|
|
3851
|
+
const tagName = String(effectiveVersions.get(pkg.name));
|
|
3739
3852
|
releaseInstalls.push({
|
|
3740
3853
|
name: pkg.name,
|
|
3741
3854
|
...runReleaseNpmInstall(pkg.dir, { workspaceRoot: root })
|
|
3742
3855
|
});
|
|
3743
3856
|
assertNoInternalDevReferencesForRepo(root, pkg.dir, effectiveSelectedPackageNames);
|
|
3857
|
+
const packageCommitsBeforeChangelog = releaseHistoryCommits(pkg.dir);
|
|
3858
|
+
const changelog = updateReleaseChangelog(pkg.dir, {
|
|
3859
|
+
version: tagName,
|
|
3860
|
+
commits: packageCommitsBeforeChangelog,
|
|
3861
|
+
extraDependencyBullets: [`Release ${pkg.name} ${tagName}.`]
|
|
3862
|
+
});
|
|
3744
3863
|
if (hasMeaningfulChanges(pkg.dir)) {
|
|
3745
3864
|
run("git", ["add", "-A"], { cwd: pkg.dir });
|
|
3746
|
-
run("git", ["commit", "-m",
|
|
3865
|
+
run("git", ["commit", "-m", releaseAdminMessage({
|
|
3866
|
+
subject: `release: ${tagName}`,
|
|
3867
|
+
version: tagName,
|
|
3868
|
+
tagName,
|
|
3869
|
+
commits: packageCommitsBeforeChangelog,
|
|
3870
|
+
changelog,
|
|
3871
|
+
extraLines: [`Package: ${pkg.name}`]
|
|
3872
|
+
})], { cwd: pkg.dir });
|
|
3747
3873
|
}
|
|
3748
3874
|
pushBranch(pkg.dir, STAGING_BRANCH);
|
|
3875
|
+
const packageCommits = releaseHistoryCommits(pkg.dir);
|
|
3749
3876
|
const mergeResult = mergeBranchIntoTarget(pkg.dir, {
|
|
3750
3877
|
sourceBranch: STAGING_BRANCH,
|
|
3751
3878
|
targetBranch: PRODUCTION_BRANCH,
|
|
3752
|
-
message:
|
|
3879
|
+
message: releaseAdminMessage({
|
|
3880
|
+
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3881
|
+
version: tagName,
|
|
3882
|
+
tagName,
|
|
3883
|
+
commits: packageCommits,
|
|
3884
|
+
changelog,
|
|
3885
|
+
extraLines: [`Package: ${pkg.name}`]
|
|
3886
|
+
}),
|
|
3753
3887
|
pushTarget: true
|
|
3754
3888
|
});
|
|
3755
|
-
const
|
|
3756
|
-
|
|
3889
|
+
const tag = ensureReleaseTag(pkg.dir, tagName, mergeResult.commitSha, releaseAdminMessage({
|
|
3890
|
+
subject: `release: ${tagName}`,
|
|
3891
|
+
version: tagName,
|
|
3892
|
+
tagName,
|
|
3893
|
+
commits: packageCommits,
|
|
3894
|
+
changelog,
|
|
3895
|
+
extraLines: [`Package: ${pkg.name}`]
|
|
3896
|
+
}));
|
|
3757
3897
|
const workflowGates = await waitForWorkflowGates("release", [
|
|
3758
3898
|
{
|
|
3759
3899
|
name: pkg.name,
|
|
@@ -3761,20 +3901,6 @@ async function workflowRelease(helpers, input) {
|
|
|
3761
3901
|
workflow: "publish.yml",
|
|
3762
3902
|
headSha: mergeResult.commitSha,
|
|
3763
3903
|
branch: tagName
|
|
3764
|
-
},
|
|
3765
|
-
{
|
|
3766
|
-
name: pkg.name,
|
|
3767
|
-
repoPath: pkg.dir,
|
|
3768
|
-
workflow: "verify.yml",
|
|
3769
|
-
headSha: mergeResult.commitSha,
|
|
3770
|
-
branch: tagName
|
|
3771
|
-
},
|
|
3772
|
-
{
|
|
3773
|
-
name: pkg.name,
|
|
3774
|
-
repoPath: pkg.dir,
|
|
3775
|
-
workflow: "verify.yml",
|
|
3776
|
-
headSha: mergeResult.commitSha,
|
|
3777
|
-
branch: PRODUCTION_BRANCH
|
|
3778
3904
|
}
|
|
3779
3905
|
], ciMode, {
|
|
3780
3906
|
root,
|
|
@@ -3783,12 +3909,26 @@ async function workflowRelease(helpers, input) {
|
|
|
3783
3909
|
});
|
|
3784
3910
|
const publish = workflowGates.find((gate) => gate.workflow === "publish.yml") ?? workflowGates[0] ?? null;
|
|
3785
3911
|
assertReleaseGitHubWorkflowSucceeded(pkg.name, publish);
|
|
3786
|
-
const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name
|
|
3912
|
+
const backMerge = backMergeProductionIntoStaging(pkg.dir, pkg.name, releaseAdminMessage({
|
|
3913
|
+
subject: `release: back-merge ${PRODUCTION_BRANCH} into ${STAGING_BRANCH}`,
|
|
3914
|
+
version: tagName,
|
|
3915
|
+
tagName,
|
|
3916
|
+
sourceRef: PRODUCTION_BRANCH,
|
|
3917
|
+
targetRef: STAGING_BRANCH,
|
|
3918
|
+
commits: packageCommits,
|
|
3919
|
+
changelog,
|
|
3920
|
+
extraLines: [`Package: ${pkg.name}`]
|
|
3921
|
+
}));
|
|
3787
3922
|
syncBranchWithOrigin(pkg.dir, STAGING_BRANCH);
|
|
3788
3923
|
return {
|
|
3789
3924
|
commitSha: mergeResult.commitSha,
|
|
3790
3925
|
tagName,
|
|
3791
3926
|
tag,
|
|
3927
|
+
changelog,
|
|
3928
|
+
adminCommitSummary: {
|
|
3929
|
+
commitCount: packageCommits.length,
|
|
3930
|
+
notableCommits: packageCommits.slice(0, 12)
|
|
3931
|
+
},
|
|
3792
3932
|
publish,
|
|
3793
3933
|
workflowGates,
|
|
3794
3934
|
backMerge
|
|
@@ -3802,6 +3942,8 @@ async function workflowRelease(helpers, input) {
|
|
|
3802
3942
|
report.publishWait = releasedPackage?.publish ?? null;
|
|
3803
3943
|
report.workflowGates = Array.isArray(releasedPackage?.workflowGates) ? releasedPackage.workflowGates : [];
|
|
3804
3944
|
report.backMerge = releasedPackage?.backMerge ?? null;
|
|
3945
|
+
report.changelog = releasedPackage?.changelog ?? null;
|
|
3946
|
+
report.adminCommitSummary = releasedPackage?.adminCommitSummary ?? null;
|
|
3805
3947
|
report.branch = STAGING_BRANCH;
|
|
3806
3948
|
publishWait.push({
|
|
3807
3949
|
name: report.name,
|
|
@@ -3812,16 +3954,41 @@ async function workflowRelease(helpers, input) {
|
|
|
3812
3954
|
const rootRelease = await executeJournalStep(root, workflowRun.runId, "release-root", () => {
|
|
3813
3955
|
setRootPackageJsonVersion(root, rootVersion);
|
|
3814
3956
|
run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
|
|
3815
|
-
|
|
3957
|
+
const rootCommitsBeforeChangelog = releaseHistoryCommits(gitRoot);
|
|
3958
|
+
const changelog = updateReleaseChangelog(gitRoot, {
|
|
3959
|
+
version: rootVersion,
|
|
3960
|
+
commits: rootCommitsBeforeChangelog,
|
|
3961
|
+
extraDependencyBullets: [
|
|
3962
|
+
`Release @treeseed/market ${rootVersion}.`,
|
|
3963
|
+
...versionLines(effectiveVersions).map((line) => `Release package ${line}.`)
|
|
3964
|
+
]
|
|
3965
|
+
});
|
|
3966
|
+
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
3967
|
+
subject: `release: ${level} bump`,
|
|
3968
|
+
version: rootVersion,
|
|
3969
|
+
tagName: rootVersion,
|
|
3970
|
+
commits: rootCommitsBeforeChangelog,
|
|
3971
|
+
changelog,
|
|
3972
|
+
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
3973
|
+
}));
|
|
3816
3974
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
3817
3975
|
const stagingCommit = headCommit(gitRoot);
|
|
3976
|
+
const rootCommits = releaseHistoryCommits(gitRoot);
|
|
3818
3977
|
let released;
|
|
3819
3978
|
let submoduleReconciliation = null;
|
|
3979
|
+
const mergeMessage = releaseAdminMessage({
|
|
3980
|
+
subject: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
|
|
3981
|
+
version: rootVersion,
|
|
3982
|
+
tagName: rootVersion,
|
|
3983
|
+
commits: rootCommits,
|
|
3984
|
+
changelog,
|
|
3985
|
+
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
3986
|
+
});
|
|
3820
3987
|
try {
|
|
3821
3988
|
released = mergeBranchIntoTarget(root, {
|
|
3822
3989
|
sourceBranch: STAGING_BRANCH,
|
|
3823
3990
|
targetBranch: PRODUCTION_BRANCH,
|
|
3824
|
-
message:
|
|
3991
|
+
message: mergeMessage,
|
|
3825
3992
|
pushTarget: false,
|
|
3826
3993
|
quietMerge: true
|
|
3827
3994
|
});
|
|
@@ -3832,7 +3999,7 @@ async function workflowRelease(helpers, input) {
|
|
|
3832
3999
|
}
|
|
3833
4000
|
helpers.write(`[release][reconcile] Resolving generated package pointer reconciliation for ${reconciliation.entries.map((entry) => String(entry.path)).join(", ")}.`);
|
|
3834
4001
|
submoduleReconciliation = reconciliation;
|
|
3835
|
-
commitAllIfChanged(gitRoot,
|
|
4002
|
+
commitAllIfChanged(gitRoot, mergeMessage);
|
|
3836
4003
|
released = { commitSha: headCommit(gitRoot) };
|
|
3837
4004
|
}
|
|
3838
4005
|
for (const pkg of checkedOutWorkspacePackageRepos(root)) {
|
|
@@ -3840,9 +4007,26 @@ async function workflowRelease(helpers, input) {
|
|
|
3840
4007
|
syncBranchWithOrigin(pkg.dir, PRODUCTION_BRANCH);
|
|
3841
4008
|
}
|
|
3842
4009
|
}
|
|
3843
|
-
|
|
4010
|
+
const mainPointerCommits = releaseHistoryCommits(gitRoot, released.commitSha, "HEAD");
|
|
4011
|
+
commitAllIfChanged(gitRoot, releaseAdminMessage({
|
|
4012
|
+
subject: "release: sync package main heads",
|
|
4013
|
+
version: rootVersion,
|
|
4014
|
+
tagName: rootVersion,
|
|
4015
|
+
sourceRef: "package main heads",
|
|
4016
|
+
targetRef: PRODUCTION_BRANCH,
|
|
4017
|
+
commits: mainPointerCommits,
|
|
4018
|
+
changelog,
|
|
4019
|
+
extraLines: versionLines(effectiveVersions).map((line) => `Main package ${line}`)
|
|
4020
|
+
}));
|
|
3844
4021
|
const releasedCommit = headCommit(gitRoot);
|
|
3845
|
-
const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit
|
|
4022
|
+
const tag = ensureReleaseTag(gitRoot, rootVersion, releasedCommit, releaseAdminMessage({
|
|
4023
|
+
subject: `release: ${rootVersion}`,
|
|
4024
|
+
version: rootVersion,
|
|
4025
|
+
tagName: rootVersion,
|
|
4026
|
+
commits: rootCommits,
|
|
4027
|
+
changelog,
|
|
4028
|
+
extraLines: versionLines(effectiveVersions).map((line) => `Package ${line}`)
|
|
4029
|
+
}));
|
|
3846
4030
|
run("git", ["push", "origin", PRODUCTION_BRANCH], { cwd: gitRoot });
|
|
3847
4031
|
syncAllCheckedOutPackageRepos(root, STAGING_BRANCH);
|
|
3848
4032
|
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
@@ -3852,6 +4036,11 @@ async function workflowRelease(helpers, input) {
|
|
|
3852
4036
|
releasedCommit,
|
|
3853
4037
|
mergeCommit: released.commitSha,
|
|
3854
4038
|
tag,
|
|
4039
|
+
changelog,
|
|
4040
|
+
adminCommitSummary: {
|
|
4041
|
+
commitCount: rootCommits.length,
|
|
4042
|
+
notableCommits: rootCommits.slice(0, 12)
|
|
4043
|
+
},
|
|
3855
4044
|
submoduleReconciliation
|
|
3856
4045
|
};
|
|
3857
4046
|
});
|
|
@@ -3862,48 +4051,24 @@ async function workflowRelease(helpers, input) {
|
|
|
3862
4051
|
rootRepo.commitSha = String(rootRelease?.releasedCommit ?? headCommit(gitRoot));
|
|
3863
4052
|
rootRepo.tagName = String(rootRelease?.rootVersion ?? "");
|
|
3864
4053
|
const rootWorkflowGateResult = await executeJournalStep(root, workflowRun.runId, "release-root-gates", () => waitForWorkflowGates("release", [
|
|
3865
|
-
{
|
|
3866
|
-
name: rootRepo.name,
|
|
3867
|
-
repoPath: rootRepo.path,
|
|
3868
|
-
workflow: "verify.yml",
|
|
3869
|
-
branch: STAGING_BRANCH,
|
|
3870
|
-
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3871
|
-
},
|
|
3872
|
-
{
|
|
4054
|
+
releaseDeployGate({
|
|
3873
4055
|
name: rootRepo.name,
|
|
3874
4056
|
repoPath: rootRepo.path,
|
|
3875
4057
|
workflow: "deploy.yml",
|
|
3876
|
-
branch: STAGING_BRANCH,
|
|
3877
|
-
headSha: String(rootRelease?.stagingCommit ?? "")
|
|
3878
|
-
},
|
|
3879
|
-
{
|
|
3880
|
-
name: rootRepo.name,
|
|
3881
|
-
repoPath: rootRepo.path,
|
|
3882
|
-
workflow: "verify.yml",
|
|
3883
4058
|
branch: rootVersion,
|
|
3884
4059
|
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3885
|
-
}
|
|
3886
|
-
{
|
|
3887
|
-
name: rootRepo.name,
|
|
3888
|
-
repoPath: rootRepo.path,
|
|
3889
|
-
workflow: "verify.yml",
|
|
3890
|
-
branch: PRODUCTION_BRANCH,
|
|
3891
|
-
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3892
|
-
},
|
|
3893
|
-
{
|
|
3894
|
-
name: rootRepo.name,
|
|
3895
|
-
repoPath: rootRepo.path,
|
|
3896
|
-
workflow: "deploy.yml",
|
|
3897
|
-
branch: PRODUCTION_BRANCH,
|
|
3898
|
-
headSha: String(rootRelease?.releasedCommit ?? rootRepo.commitSha ?? "")
|
|
3899
|
-
}
|
|
4060
|
+
})
|
|
3900
4061
|
].filter((gate) => gate.headSha), ciMode, {
|
|
3901
4062
|
root,
|
|
3902
4063
|
runId: workflowRun.runId,
|
|
3903
4064
|
onProgress: (line, stream) => helpers.write(line, stream)
|
|
3904
4065
|
}).then((workflowGates) => ({ workflowGates })));
|
|
3905
4066
|
const hostedDeploymentState = recordHostedDeploymentStatesFromRootGates(root, rootRelease, rootWorkflowGateResult?.workflowGates);
|
|
3906
|
-
const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true
|
|
4067
|
+
const releaseBackMerge = await executeJournalStep(root, workflowRun.runId, "release-back-merge", () => backMergeRootProductionIntoStaging(root, true, {
|
|
4068
|
+
version: rootVersion,
|
|
4069
|
+
changelog: rootRelease?.changelog ?? null,
|
|
4070
|
+
selectedVersions: effectiveVersions
|
|
4071
|
+
}));
|
|
3907
4072
|
const devTagCleanupMode = effectiveInput.devTagCleanup ?? "safe-after-release";
|
|
3908
4073
|
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", () => {
|
|
3909
4074
|
const activeDevTags = collectActiveDevTagReferences(root);
|
package/package.json
CHANGED
|
@@ -4,9 +4,8 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
6
|
- staging
|
|
7
|
-
- main
|
|
8
7
|
tags:
|
|
9
|
-
- '
|
|
8
|
+
- '*.*.*'
|
|
10
9
|
workflow_dispatch:
|
|
11
10
|
inputs:
|
|
12
11
|
environment:
|
|
@@ -75,12 +74,13 @@ jobs:
|
|
|
75
74
|
|
|
76
75
|
if [[ "${ref_type}" == "tag" ]]; then
|
|
77
76
|
scope="prod"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
if [[ "${ref_name}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
78
|
+
release_tag="true"
|
|
79
|
+
compare_ref="$(git rev-list --parents -n 1 "${head_sha}" | awk '{print $2}')"
|
|
80
|
+
else
|
|
81
|
+
release_tag="false"
|
|
82
|
+
compare_ref=""
|
|
83
|
+
fi
|
|
84
84
|
else
|
|
85
85
|
scope="staging"
|
|
86
86
|
release_tag="false"
|
|
@@ -94,7 +94,10 @@ jobs:
|
|
|
94
94
|
code_changed="false"
|
|
95
95
|
content_changed="false"
|
|
96
96
|
|
|
97
|
-
if [[
|
|
97
|
+
if [[ "${ref_type}" == "tag" && "${release_tag}" != "true" ]]; then
|
|
98
|
+
code_changed="false"
|
|
99
|
+
content_changed="false"
|
|
100
|
+
elif [[ -n "${compare_ref}" ]]; then
|
|
98
101
|
while IFS= read -r path; do
|
|
99
102
|
[[ -z "${path}" ]] && continue
|
|
100
103
|
case "${path}" in
|