Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .bumpy/surface-bump-file-parse-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': patch
---

Surface bump file parse errors to users instead of silently ignoring them
8 changes: 7 additions & 1 deletion packages/bumpy/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
}

// Filter to only bump files added/modified on this branch
const allBumpFiles = await readBumpFiles(rootDir);
const { bumpFiles: allBumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);
if (parseErrors.length > 0) {
for (const err of parseErrors) {
log.error(err);
}
process.exit(1);
}
const { branchBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir);

// If an empty bump file exists on this branch, the check passes
Expand Down
111 changes: 97 additions & 14 deletions packages/bumpy/src/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
const config = await loadConfig(rootDir);
const { packages } = await discoverWorkspace(rootDir, config);
const depGraph = new DependencyGraph(packages);
const allBumpFiles = await readBumpFiles(rootDir);
const { bumpFiles: allBumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);

// Skip on the version PR branch — it has no bump files by design
const prBranchName = detectPrBranch(rootDir);
Expand All @@ -110,27 +110,48 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi

// Filter to only bump files added/modified in this PR
const changedFiles = getChangedFiles(rootDir, config.baseBranch);
const { branchBumpFiles: prBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir);
const { branchBumpFiles: prBumpFiles, hasEmptyBumpFile } = filterBranchBumpFiles(
allBumpFiles,
changedFiles,
rootDir,
parseErrors,
);

// An empty bump file signals intentionally no releases needed
if (hasEmptyBumpFile) {
log.success('Empty bump file found — no releases needed.');
if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
await postOrUpdatePrComment(prNumber, formatNoBumpFilesComment(prBranch, pm), rootDir, opts.patComments);
// Surface any parse errors
if (parseErrors.length > 0) {
for (const err of parseErrors) {
log.error(err);
}
return;
}

if (prBumpFiles.length === 0) {
const willFail = !opts.noFail;
const msg = 'No bump files found in this PR.';
// An empty bump file signals intentionally no releases needed
if (hasEmptyBumpFile && parseErrors.length === 0) {
log.success('Empty bump file found — no releases needed.');
if (shouldComment && prNumber) {
await postOrUpdatePrComment(prNumber, formatEmptyBumpFileComment(), rootDir, opts.patComments);
}
return;
}

const willFail = !opts.noFail || parseErrors.length > 0;
const msg =
parseErrors.length > 0
? 'Bump file(s) found but failed to parse — see errors above.'
: 'No bump files found in this PR.';
if (willFail) log.error(msg);
else log.warn(msg);

if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
await postOrUpdatePrComment(prNumber, formatNoBumpFilesComment(prBranch, pm), rootDir, opts.patComments);
await postOrUpdatePrComment(
prNumber,
parseErrors.length > 0
? formatBumpFileErrorsComment(parseErrors, prBranch, pm)
: formatNoBumpFilesComment(prBranch, pm),
rootDir,
opts.patComments,
);
}

if (willFail) process.exit(1);
Expand All @@ -154,10 +175,15 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
// Comment on PR
if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
const comment = formatReleasePlanComment(plan, prBumpFiles, prNumber, prBranch, pm, plan.warnings);
const comment = formatReleasePlanComment(plan, prBumpFiles, prNumber, prBranch, pm, plan.warnings, parseErrors);
await postOrUpdatePrComment(prNumber, comment, rootDir, opts.patComments);
}

// Fail if there were parse errors (even if some files parsed successfully)
if (parseErrors.length > 0 && !opts.noFail) {
process.exit(1);
}

// Check for uncovered packages
const coveredPackages = new Set(plan.releases.map((r) => r.name));
const changedPackages = await findChangedPackages(changedFiles, packages, rootDir, config);
Expand Down Expand Up @@ -188,7 +214,14 @@ export async function ciReleaseCommand(rootDir: string, opts: ReleaseOptions): P
ensureGitIdentity(rootDir, config);
const { packages } = await discoverWorkspace(rootDir, config);
const depGraph = new DependencyGraph(packages);
const bumpFiles = await readBumpFiles(rootDir);
const { bumpFiles, errors: releaseParseErrors } = await readBumpFiles(rootDir);

if (releaseParseErrors.length > 0) {
for (const err of releaseParseErrors) {
log.error(err);
}
throw new Error('Bump file parse errors must be fixed before releasing.');
}

if (bumpFiles.length === 0) {
// No bump files — check if there are unpublished packages to publish
Expand Down Expand Up @@ -440,6 +473,7 @@ function formatReleasePlanComment(
prBranch: string | null,
pm: PackageManager,
warnings: string[] = [],
parseErrors: string[] = [],
): string {
const repo = process.env.GITHUB_REPOSITORY;
const lines: string[] = [];
Expand Down Expand Up @@ -488,6 +522,15 @@ function formatReleasePlanComment(
}
lines.push('');

if (parseErrors.length > 0) {
lines.push('#### Errors');
lines.push('');
for (const e of parseErrors) {
lines.push(`> :x: ${e}`);
}
lines.push('');
}

if (warnings.length > 0) {
lines.push('#### Warnings');
lines.push('');
Expand All @@ -509,6 +552,46 @@ function formatReleasePlanComment(
return lines.join('\n');
}

function formatBumpFileErrorsComment(errors: string[], prBranch: string | null, pm: PackageManager): string {
const runCmd = pmRunCommand(pm);
const lines = [
`<a href="https://bumpy.varlock.dev"><img src="${FROG_IMG_BASE}/frog-neutral.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
'',
'**This PR has bump file(s) with errors that need to be fixed.**',
'<br clear="left" />\n',
'#### Errors',
'',
...errors.map((e) => `> :x: ${e}`),
'',
'Please fix the errors above or recreate the bump file:\n',
'```bash',
`${runCmd} add`,
'```',
];

const addLink = buildAddBumpFileLink(prBranch);
if (addLink) {
lines.push('');
lines.push(`Or [click here to add a bump file](${addLink}) directly on GitHub.`);
}

lines.push('\n---');
lines.push(`_This comment is maintained by [bumpy](https://bumpy.varlock.dev)._`);
return lines.join('\n');
}

function formatEmptyBumpFileComment(): string {
const lines = [
`<a href="https://bumpy.varlock.dev"><img src="${FROG_IMG_BASE}/frog-neutral.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
'',
'**This PR includes an empty bump file — no version bump is needed.** :white_check_mark:',
'<br clear="left" />',
'\n---',
`_This comment is maintained by [bumpy](https://bumpy.varlock.dev)._`,
];
return lines.join('\n');
}

function formatNoBumpFilesComment(prBranch: string | null, pm: PackageManager): string {
const runCmd = pmRunCommand(pm);
const lines = [
Expand Down
8 changes: 7 additions & 1 deletion packages/bumpy/src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ export async function statusCommand(rootDir: string, opts: StatusOptions): Promi
const config = await loadConfig(rootDir);
const packages = await discoverPackages(rootDir, config);
const depGraph = new DependencyGraph(packages);
const bumpFiles = await readBumpFiles(rootDir);
const { bumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);

if (parseErrors.length > 0) {
for (const err of parseErrors) {
log.error(err);
}
}

if (bumpFiles.length === 0) {
if (opts.json) {
Expand Down
9 changes: 8 additions & 1 deletion packages/bumpy/src/commands/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ export async function versionCommand(rootDir: string, opts: VersionOptions = {})
const config = await loadConfig(rootDir);
const packages = await discoverPackages(rootDir, config);
const depGraph = new DependencyGraph(packages);
const bumpFiles = await readBumpFiles(rootDir);
const { bumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);

if (parseErrors.length > 0) {
for (const err of parseErrors) {
log.error(err);
}
throw new Error('Bump file parse errors must be fixed before versioning.');
}

if (bumpFiles.length === 0) {
log.info('No pending bump files.');
Expand Down
Loading