Skip to content

Commit 07eb1c8

Browse files
authored
Improve release script (#2278)
2 parents fb76514 + f7b67ad commit 07eb1c8

2 files changed

Lines changed: 176 additions & 2 deletions

File tree

automation/utils/bin/rui-prepare-release.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { bumpPackageJson, bumpXml, getNextVersion } from "../src/bump-version";
55
import { exec } from "../src/shell";
66
import { gh } from "../src/github";
77
import { printGithubAuthHelp } from "../src/cli-utils";
8-
import { printPkgInformation, selectPackageV2 } from "../src/prepare-release-helpers";
8+
import { printPkgInformation, selectPackageV2, ensureMainBranch } from "../src/prepare-release-helpers";
99

1010
async function main(): Promise<void> {
1111
try {
@@ -22,6 +22,9 @@ async function main(): Promise<void> {
2222
process.exit(1);
2323
}
2424

25+
// Check git branch: must be on main and in sync with origin/main
26+
await ensureMainBranch();
27+
2528
// Step 1: Initialize Jira client
2629
let jira: Jira | undefined;
2730
try {

automation/utils/src/prepare-release-helpers.ts

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
} from "./changelog-parser";
88
import { listPackages, PackageListing } from "./monorepo";
99
import chalk from "chalk";
10-
1110
import { prompt } from "enquirer";
11+
import { exec } from "./shell";
1212

1313
type WidgetPkg = {
1414
type: "widget";
@@ -123,6 +123,107 @@ function createPackagesTree(map: PackagesFullInfoMap, list: PackagesFullInfoList
123123
return tree;
124124
}
125125

126+
async function getCurrentBranch(): Promise<string> {
127+
const { stdout } = await exec("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" });
128+
return stdout.trim();
129+
}
130+
131+
async function getRemoteSyncCounts(): Promise<{ behind: number; ahead: number }> {
132+
const [{ stdout: behindStr }, { stdout: aheadStr }] = await Promise.all([
133+
exec("git rev-list HEAD..origin/main --count", { stdio: "pipe" }),
134+
exec("git rev-list origin/main..HEAD --count", { stdio: "pipe" })
135+
]);
136+
return {
137+
behind: parseInt(behindStr.trim(), 10),
138+
ahead: parseInt(aheadStr.trim(), 10)
139+
};
140+
}
141+
142+
async function switchToMain(): Promise<void> {
143+
const { confirmSwitch } = await prompt<{ confirmSwitch: boolean }>({
144+
type: "confirm",
145+
name: "confirmSwitch",
146+
message: `❓ Switch to ${chalk.blue("main")} branch?`,
147+
initial: true
148+
});
149+
150+
if (!confirmSwitch) {
151+
console.log(chalk.red("❌ Release preparation must start from the main branch"));
152+
process.exit(1);
153+
}
154+
155+
await exec("git checkout main", { stdio: "pipe" });
156+
console.log(chalk.green("✅ Switched to main"));
157+
}
158+
159+
async function fastForwardMain(): Promise<void> {
160+
const { confirmFastForward } = await prompt<{ confirmFastForward: boolean }>({
161+
type: "confirm",
162+
name: "confirmFastForward",
163+
message: `❓ Fast-forward ${chalk.blue("main")} to ${chalk.blue("origin/main")}?`,
164+
initial: true
165+
});
166+
167+
if (!confirmFastForward) {
168+
console.log(chalk.yellow("⚠️ Continuing with an outdated main branch"));
169+
return;
170+
}
171+
172+
await exec("git merge --ff-only origin/main", { stdio: "pipe" });
173+
console.log(chalk.green("✅ main fast-forwarded to origin/main"));
174+
}
175+
176+
export async function ensureMainBranch(): Promise<void> {
177+
const branch = await getCurrentBranch();
178+
179+
if (branch !== "main") {
180+
console.log(chalk.yellow(`⚠️ Current branch is ${chalk.blue(branch)}, expected ${chalk.blue("main")}`));
181+
await switchToMain();
182+
}
183+
184+
console.log(chalk.blue("🔄 Fetching origin/main..."));
185+
await exec("git fetch origin main", { stdio: "pipe" });
186+
187+
const { behind, ahead } = await getRemoteSyncCounts();
188+
189+
if (behind === 0 && ahead === 0) {
190+
console.log(chalk.green("✅ main is up to date with origin/main"));
191+
return;
192+
}
193+
194+
if (behind > 0 && ahead === 0) {
195+
console.log(chalk.yellow(`⚠️ main is ${behind} commit(s) behind origin/main`));
196+
await fastForwardMain();
197+
return;
198+
}
199+
200+
if (ahead > 0 && behind === 0) {
201+
console.log(
202+
chalk.yellow(`⚠️ main is ${ahead} commit(s) ahead of origin/main (unpushed local commits detected)`)
203+
);
204+
console.log(chalk.yellow(" Proceeding, but consider pushing or resetting before releasing."));
205+
return;
206+
}
207+
208+
// Truly diverged: both sides have unique commits
209+
console.log(chalk.red(`❌ main has diverged from origin/main: ${ahead} ahead, ${behind} behind`));
210+
console.log(chalk.red(" You may need to reset or rebase before creating a release."));
211+
212+
const { continueAnyway } = await prompt<{ continueAnyway: boolean }>({
213+
type: "confirm",
214+
name: "continueAnyway",
215+
message: "❓ Continue anyway? (not recommended)",
216+
initial: false
217+
});
218+
219+
if (!continueAnyway) {
220+
console.log(chalk.red("❌ Release preparation canceled"));
221+
process.exit(1);
222+
}
223+
224+
console.log(chalk.yellow("⚠️ Continuing with a diverged main branch"));
225+
}
226+
126227
export async function selectPackageV2(): Promise<WidgetPkg | ModulePkg> {
127228
const pkgs = await listPackages(['"*"', '"!web-widgets"']);
128229
const pkgsList = await loadPackagesFullInfo(pkgs);
@@ -165,16 +266,86 @@ export async function selectPackageV2(): Promise<WidgetPkg | ModulePkg> {
165266
}
166267

167268
const PADDING = 60;
269+
// eslint-disable-next-line no-control-regex
270+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
271+
const visibleLen = (s: string): number => s.replace(ANSI_RE, "").length;
272+
273+
function wrapLine(line: string, maxLen: number): string[] {
274+
if (maxLen <= 0) return [line];
275+
const result: string[] = [];
276+
let remaining = line;
277+
while (remaining.length > maxLen) {
278+
result.push(remaining.slice(0, maxLen));
279+
remaining = remaining.slice(maxLen);
280+
}
281+
result.push(remaining);
282+
return result;
283+
}
284+
285+
function printSectionBox(type: string, logs: string[], treePrefix: string, boxWidth: number): void {
286+
const headerInner = `─ ${type} `;
287+
const topDashes = "─".repeat(Math.max(0, boxWidth - 2 - headerInner.length));
288+
console.log(`${treePrefix}${chalk.dim(`┌${headerInner}${topDashes}┐`)}`);
289+
const contentWidth = boxWidth - 4; // subtract "│ " left and " │" right
290+
for (const log of logs) {
291+
for (const wrappedLine of wrapLine(log, contentWidth)) {
292+
console.log(`${treePrefix}${chalk.dim("│")} ${wrappedLine.padEnd(contentWidth)} ${chalk.dim("│")}`);
293+
}
294+
}
295+
console.log(`${treePrefix}${chalk.dim(`└${"─".repeat(Math.max(0, boxWidth - 2))}┘`)}`);
296+
}
297+
298+
function printUnreleasedChangelog(
299+
changelog: WidgetChangelogFileWrapper | ModuleChangelogFileWrapper,
300+
treePrefix: string
301+
): void {
302+
const unreleased = changelog.changelog.content[0];
303+
const subcomponents = "subcomponents" in unreleased ? unreleased.subcomponents : [];
304+
305+
const termWidth = process.stdout.columns || 100;
306+
const boxWidth = Math.max(20, termWidth - visibleLen(treePrefix));
307+
308+
for (const section of unreleased.sections) {
309+
if (section.logs.length === 0) continue;
310+
printSectionBox(
311+
section.type,
312+
section.logs.map(l => `- ${l}`),
313+
treePrefix,
314+
boxWidth
315+
);
316+
}
317+
318+
for (const sub of subcomponents) {
319+
const label = "version" in sub ? `${sub.name} [${sub.version.format()}]` : sub.name;
320+
console.log(`${treePrefix}${chalk.yellow(label)}`);
321+
for (const section of sub.sections) {
322+
if (section.logs.length === 0) continue;
323+
printSectionBox(
324+
section.type,
325+
section.logs.map(l => `- ${l}`),
326+
`${treePrefix} `,
327+
Math.max(20, boxWidth - 2)
328+
);
329+
}
330+
}
331+
}
332+
168333
export function printPkgInformation(pkg: WidgetPkg | ModulePkg): void {
169334
console.log(
170335
`${shortName(pkg.info.name).padEnd(PADDING + 3, " ")} ${chalk.bold(pkg.info.version.format())} ${pkg.changelog.hasUnreleasedLogs() ? "🆕" : " "}`
171336
);
337+
if (pkg.changelog.hasUnreleasedLogs()) {
338+
printUnreleasedChangelog(pkg.changelog, " ");
339+
}
172340
if (pkg.widgets.length) {
173341
pkg.widgets.forEach((widget, i) => {
174342
const isLast = i === pkg.widgets.length - 1;
175343
console.log(
176344
`${isLast ? "└" : "├"}${shortName(widget.info.name).padEnd(PADDING, " ")} ${chalk.dim(widget.info.version.format())} ${widget.changelog.hasUnreleasedLogs() ? "🆕" : ""}`
177345
);
346+
if (widget.changelog.hasUnreleasedLogs()) {
347+
printUnreleasedChangelog(widget.changelog, isLast ? " " : "│ ");
348+
}
178349
});
179350
}
180351
}

0 commit comments

Comments
 (0)