Skip to content

Commit 9ce10e3

Browse files
committed
build(release): update prompt defaults
1 parent 605985d commit 9ce10e3

File tree

4 files changed

+188
-34
lines changed

4 files changed

+188
-34
lines changed

utils/release/src/prompt.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ export async function askConsent(
108108
description: string,
109109
command: string,
110110
skipPrompt: boolean,
111-
dryRun: boolean
111+
dryRun: boolean,
112+
defaultYes = false
112113
): Promise<boolean> {
113114
console.log(`\nAbout to: ${description}`);
114115
console.log(`Command: ${command}`);
@@ -123,5 +124,5 @@ export async function askConsent(
123124
return true;
124125
}
125126

126-
return askYesNo('Proceed?');
127+
return askYesNo('Proceed?', defaultYes);
127128
}

utils/release/src/release.ts

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import {
1414
pushToOrigin,
1515
rebase,
1616
getLog,
17+
gitSafe,
1718
GitOptions
1819
} from './git';
1920
import { WorktreeContext } from './worktree';
2021
import { askConsent } from './prompt';
2122
import { isValidSemver, normalizeTag, extractVersion } from './version';
23+
import * as fs from 'fs';
24+
import * as path from 'path';
2225

2326
/**
2427
* Runs an npm script in the repository root.
@@ -32,6 +35,32 @@ function runNpmScript(script: string, cwd: string): void {
3235
});
3336
}
3437

38+
/**
39+
* Checks whether all workspaces (including root) already have the target version.
40+
* If any package.json is missing or has a different version, returns false.
41+
* @param cwd - Repo root
42+
* @param targetVersion - Version string without 'v' prefix
43+
*/
44+
function packagesAlreadyOnVersion(cwd: string, targetVersion: string): boolean {
45+
try {
46+
const rootPkgPath = path.join(cwd, 'package.json');
47+
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf-8'));
48+
const workspaces: string[] = rootPkg.workspaces ?? [];
49+
const packageFiles = [rootPkgPath, ...workspaces.map(w => path.join(cwd, w, 'package.json'))];
50+
51+
for (const pkgPath of packageFiles) {
52+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
53+
if (pkg.version !== targetVersion) {
54+
return false;
55+
}
56+
}
57+
return true;
58+
} catch {
59+
// Fall back to running version:set if anything goes wrong
60+
return false;
61+
}
62+
}
63+
3564
/**
3665
* Checks if working tree is clean and prompts user to handle changes.
3766
* @param gitOpts - Git options
@@ -83,9 +112,11 @@ async function executeStep(
83112
description: string,
84113
command: string,
85114
options: ReleaseOptions,
86-
action: () => void
115+
action: () => void,
116+
defaultYes = true,
117+
continueOnFailure = false
87118
): Promise<StepResult> {
88-
const consent = await askConsent(description, command, options.skipPrompts, options.dryRun);
119+
const consent = await askConsent(description, command, options.skipPrompts, options.dryRun, defaultYes);
89120

90121
if (!consent) {
91122
if (options.dryRun) {
@@ -99,6 +130,10 @@ async function executeStep(
99130
return { success: true, message: description };
100131
} catch (err) {
101132
const errorMessage = err instanceof Error ? err.message : String(err);
133+
if (continueOnFailure) {
134+
console.warn(`Non-blocking failure: ${description} - ${errorMessage}`);
135+
return { success: true, message: `Non-blocking failure: ${description}` };
136+
}
102137
return { success: false, message: `Failed: ${description} - ${errorMessage}` };
103138
}
104139
}
@@ -132,18 +167,23 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
132167

133168
// Pre-release Step 1: Update package versions
134169
console.log('\n==== Pre-release: Update package versions ====');
135-
const versionResult = await executeStep(
136-
`Update package.json versions to ${version}`,
137-
`npm run version:set -- ${version}`,
138-
options,
139-
() => runNpmScript(`version:set -- ${version}`, rootDir)
140-
);
141-
if (!versionResult.success && !dryRun) {
142-
console.error(versionResult.message);
143-
return false;
144-
}
145-
if (!dryRun && !requireCleanWorktree(gitOpts, 'Version update')) {
146-
return false;
170+
const versionsAligned = packagesAlreadyOnVersion(rootDir, version);
171+
if (versionsAligned) {
172+
console.log(`All packages already at ${version}; skipping version:set.`);
173+
} else {
174+
const versionResult = await executeStep(
175+
`Update package.json versions to ${version}`,
176+
`npm run version:set -- ${version}`,
177+
options,
178+
() => runNpmScript(`version:set -- ${version}`, rootDir)
179+
);
180+
if (!versionResult.success && !dryRun) {
181+
console.error(versionResult.message);
182+
return false;
183+
}
184+
if (!dryRun && !requireCleanWorktree(gitOpts, 'Version update')) {
185+
return false;
186+
}
147187
}
148188

149189
// Pre-release Step 2: Rebuild all bundles
@@ -152,7 +192,8 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
152192
'Regenerate dist outputs for fresh builds',
153193
'npm run build',
154194
options,
155-
() => runNpmScript('build', rootDir)
195+
() => runNpmScript('build', rootDir),
196+
true // default Yes; rebuilding should generally proceed
156197
);
157198
if (!buildResult.success && !dryRun) {
158199
console.error(buildResult.message);
@@ -168,7 +209,9 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
168209
'Run depcheck to verify dependency health',
169210
'npm run depcheck',
170211
options,
171-
() => runNpmScript('depcheck', rootDir)
212+
() => runNpmScript('depcheck', rootDir),
213+
true, // default Yes
214+
true // continue even if depcheck reports unused deps
172215
);
173216
if (!depcheckResult.success && !dryRun) {
174217
console.error(depcheckResult.message);
@@ -219,15 +262,45 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
219262
const remoteDevelop = getCommitSha('origin/develop', gitOpts);
220263

221264
if (localDevelop !== remoteDevelop) {
222-
console.error('Local develop branch is not up to date with remote.');
223-
console.error(`Local develop: ${localDevelop}`);
224-
console.error(` ${getCommitMessage('refs/heads/develop', gitOpts)}`);
225-
console.error(`Remote develop: ${remoteDevelop}`);
226-
console.error(` ${getCommitMessage('origin/develop', gitOpts)}`);
227-
console.error('\nPlease update your local develop branch first.');
228-
return false;
265+
const remoteIsAncestor = gitSafe(
266+
['merge-base', '--is-ancestor', 'origin/develop', 'refs/heads/develop'],
267+
gitOpts
268+
);
269+
const localIsAncestor = gitSafe(
270+
['merge-base', '--is-ancestor', 'refs/heads/develop', 'origin/develop'],
271+
gitOpts
272+
);
273+
274+
if (remoteIsAncestor && !localIsAncestor) {
275+
console.log('Local develop is ahead of origin/develop.');
276+
console.log(`Local develop: ${localDevelop} ${getCommitMessage('refs/heads/develop', gitOpts)}`);
277+
console.log(`Remote develop: ${remoteDevelop} ${getCommitMessage('origin/develop', gitOpts)}`);
278+
const pushDevelop = await executeStep(
279+
'Push develop to origin',
280+
'git push origin develop',
281+
options,
282+
() => pushToOrigin('develop', gitOpts),
283+
true // default yes
284+
);
285+
if (!pushDevelop.success) {
286+
console.error(pushDevelop.message);
287+
return false;
288+
}
289+
} else if (localIsAncestor && !remoteIsAncestor) {
290+
console.error('Local develop is behind origin/develop.');
291+
console.error(`Local develop: ${localDevelop} ${getCommitMessage('refs/heads/develop', gitOpts)}`);
292+
console.error(`Remote develop: ${remoteDevelop} ${getCommitMessage('origin/develop', gitOpts)}`);
293+
console.error('\nPlease pull or rebase develop before releasing.');
294+
return false;
295+
} else {
296+
console.error('Local and origin develop have diverged. Please reconcile before releasing.');
297+
console.error(`Local develop: ${localDevelop} ${getCommitMessage('refs/heads/develop', gitOpts)}`);
298+
console.error(`Remote develop: ${remoteDevelop} ${getCommitMessage('origin/develop', gitOpts)}`);
299+
return false;
300+
}
301+
} else {
302+
console.log(`\u2705 develop branch is up to date (${localDevelop.substring(0, 8)})`);
229303
}
230-
console.log(`\u2705 develop branch is up to date (${localDevelop.substring(0, 8)})`);
231304
} else {
232305
console.log('[DRY RUN] Would verify develop matches origin/develop');
233306
}
@@ -255,6 +328,10 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
255328
if (!dryRun && worktreeOpts) {
256329
const masterSha = getCommitSha('HEAD', worktreeOpts);
257330
const remoteDevelop = getCommitSha('origin/develop', gitOpts);
331+
const worktreePath = worktreeContext.getPath();
332+
const rebaseCmd = worktreePath
333+
? `git -C ${worktreePath} rebase origin/develop`
334+
: 'git rebase origin/develop';
258335

259336
if (masterSha !== remoteDevelop) {
260337
console.log('master is not up to date with origin/develop');
@@ -268,8 +345,8 @@ export async function executeRelease(options: ReleaseOptions): Promise<boolean>
268345
}
269346

270347
const rebaseResult = await executeStep(
271-
'Rebase master onto origin/develop',
272-
'git rebase origin/develop',
348+
'Rebase master worktree onto origin/develop (does not touch your local develop)',
349+
rebaseCmd,
273350
options,
274351
() => rebase('origin/develop', worktreeOpts!)
275352
);

utils/release/src/version.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44

55
import { execSync } from 'child_process';
6+
import * as fs from 'fs';
7+
import * as path from 'path';
68
import { askChoice, askInput } from './prompt';
79

810
/**
@@ -64,6 +66,43 @@ export function formatSemver(version: SemverVersion, includeV = true): string {
6466
return includeV ? `v${str}` : str;
6567
}
6668

69+
/**
70+
* Compares two semver strings.
71+
* @param a - First version (with or without 'v')
72+
* @param b - Second version (with or without 'v')
73+
* @returns 1 if a>b, -1 if a<b, 0 if equal
74+
*/
75+
export function compareSemver(a: string, b: string): number {
76+
const va = parseSemver(a);
77+
const vb = parseSemver(b);
78+
79+
if (va.major !== vb.major) return Math.sign(va.major - vb.major);
80+
if (va.minor !== vb.minor) return Math.sign(va.minor - vb.minor);
81+
return Math.sign(va.patch - vb.patch);
82+
}
83+
84+
/**
85+
* Reads the root package.json version if available.
86+
* @param cwd - Working directory
87+
* @returns Version with 'v' prefix or null if unavailable/invalid
88+
*/
89+
export function getPackageVersion(cwd: string): string | null {
90+
try {
91+
const pkgPath = path.join(cwd, 'package.json');
92+
const contents = fs.readFileSync(pkgPath, 'utf-8');
93+
const pkg = JSON.parse(contents);
94+
const version = typeof pkg.version === 'string' ? pkg.version : null;
95+
96+
if (version && isValidSemver(version)) {
97+
return normalizeTag(version);
98+
}
99+
} catch {
100+
// Ignore read/parse errors and fall back to git tags
101+
}
102+
103+
return null;
104+
}
105+
67106
/**
68107
* Gets the latest tag from the remote repository.
69108
* @param cwd - Working directory
@@ -134,6 +173,16 @@ export function getFeatureCommitsSince(tag: string, cwd: string): string[] {
134173
*/
135174
export async function determineVersion(cwd: string): Promise<string> {
136175
const latestTag = getLatestTag(cwd);
176+
const packageVersion = getPackageVersion(cwd);
177+
178+
if (packageVersion) {
179+
if (!latestTag || compareSemver(packageVersion, latestTag) >= 1) {
180+
const latestLabel = latestTag ?? 'no remote tag';
181+
console.log(`Detected local version ${packageVersion} (latest tag: ${latestLabel}).`);
182+
console.log('Using package.json version without suggesting an additional bump.');
183+
return packageVersion;
184+
}
185+
}
137186

138187
if (!latestTag) {
139188
console.log('No existing tags found on origin. Defaulting to initial release tag v0.1.0.');

utils/release/src/worktree.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ export function getWorktreePath(branch: string): string {
4040
export function createWorktree(branch: string, worktreePath: string, options: GitOptions): WorktreeInfo {
4141
console.log(`Creating worktree for ${branch} at ${worktreePath}...`);
4242

43+
// Clean up stale entries and conflicting worktrees for this branch
44+
pruneWorktrees(options);
45+
46+
// If the branch is already checked out in another worktree, try to remove a stale temp worktree
47+
const existing = listWorktrees(options).find((wt) => wt.branch === `refs/heads/${branch}`);
48+
if (existing) {
49+
if (existing.path.includes(`cpp-actions-release-${branch}`)) {
50+
console.log(`Found stale ${branch} worktree at ${existing.path}, removing...`);
51+
removeWorktree(existing.path, options, true);
52+
} else {
53+
throw new Error(
54+
`Branch ${branch} is already checked out at ${existing.path}. ` +
55+
'Please remove or switch that worktree before releasing.'
56+
);
57+
}
58+
}
59+
4360
// Ensure the parent directory exists
4461
const parentDir = path.dirname(worktreePath);
4562
if (!fs.existsSync(parentDir)) {
@@ -101,21 +118,31 @@ export function removeWorktree(worktreePath: string, options: GitOptions, force
101118
}
102119

103120
/**
104-
* Lists all worktrees.
121+
* Lists all worktrees with branch information (if present).
105122
* @param options - Git options
106-
* @returns Array of worktree paths
123+
* @returns Array of worktree info objects
107124
*/
108-
export function listWorktrees(options: GitOptions): string[] {
125+
export function listWorktrees(options: GitOptions): Array<{ path: string; branch: string | null }> {
109126
const output = git(['worktree', 'list', '--porcelain'], { ...options, silent: true });
110-
const paths: string[] = [];
127+
const infos: Array<{ path: string; branch: string | null }> = [];
128+
let currentPath: string | null = null;
111129

112130
for (const line of output.split('\n')) {
113131
if (line.startsWith('worktree ')) {
114-
paths.push(line.substring(9));
132+
currentPath = line.substring(9).trim();
133+
infos.push({ path: currentPath, branch: null });
134+
} else if (line.startsWith('branch ')) {
135+
const branch = line.substring(7).trim();
136+
if (currentPath) {
137+
const idx = infos.findIndex((w) => w.path === currentPath);
138+
if (idx !== -1) {
139+
infos[idx] = { ...infos[idx], branch };
140+
}
141+
}
115142
}
116143
}
117144

118-
return paths;
145+
return infos;
119146
}
120147

121148
/**

0 commit comments

Comments
 (0)