Skip to content

Commit 2102f86

Browse files
fix: separate error paths for rebase failure vs post-rebase push failure
Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com>
1 parent 0dd2dfe commit 2102f86

3 files changed

Lines changed: 137 additions & 49 deletions

File tree

dist/index.js

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39647,54 +39647,63 @@ async function pushWithFallback(branchName, owner, repo, octokit) {
3964739647
core.info(`Regular push to '${branchName}' failed. Attempting to rebase on remote branch.`);
3964839648
}
3964939649
// Try pull --rebase then push
39650-
let rebaseErrorMsg = null;
39650+
let errorMsg = null;
39651+
let errorLabel = "Rebase error";
3965139652
let abortErrorMsg = null;
39653+
let rebaseFailed = false;
3965239654
const rebaseResult = await exec.getExecOutput("git", ["pull", "--rebase", "origin", branchName], { ignoreReturnCode: true });
3965339655
if (rebaseResult.exitCode === 0) {
3965439656
const pushResult = await exec.getExecOutput("git", ["push", "--verbose", "origin", branchName], { ignoreReturnCode: true });
3965539657
if (pushResult.exitCode === 0) {
3965639658
core.info(`Successfully pushed to '${branchName}' after rebasing on remote changes.`);
3965739659
return true;
3965839660
}
39659-
// Push after rebase failed
39660-
rebaseErrorMsg = [
39661+
// Push after rebase failed — no rebase in progress, don't abort
39662+
errorLabel = "Push error";
39663+
errorMsg = [
3966139664
pushResult.stderr.trim(),
3966239665
pushResult.stdout.trim(),
3966339666
]
3966439667
.filter(Boolean)
3966539668
.join("\n") || `git push failed with exit code ${pushResult.exitCode}`;
39669+
core.info(`Push after rebase failed: ${errorMsg}`);
3966639670
}
3966739671
else {
3966839672
// Rebase itself failed (merge conflicts)
39669-
rebaseErrorMsg = [
39673+
rebaseFailed = true;
39674+
errorLabel = "Rebase error";
39675+
errorMsg = [
3967039676
rebaseResult.stderr.trim(),
3967139677
rebaseResult.stdout.trim(),
3967239678
]
3967339679
.filter(Boolean)
3967439680
.join("\n") || `git pull --rebase failed with exit code ${rebaseResult.exitCode}`;
39675-
}
39676-
core.info(`Rebase failed (likely due to merge conflicts). Aborting rebase.`);
39677-
// Abort the rebase so the working tree is clean
39678-
const abortResult = await exec.getExecOutput("git", ["rebase", "--abort"], { ignoreReturnCode: true });
39679-
if (abortResult.exitCode !== 0) {
39680-
abortErrorMsg = [
39681-
abortResult.stderr.trim(),
39682-
abortResult.stdout.trim(),
39683-
]
39684-
.filter(Boolean)
39685-
.join("\n") || `rebase --abort failed with exit code ${abortResult.exitCode}`;
39686-
core.info(`rebase --abort failed: ${abortErrorMsg}. The working tree may be in an unexpected state.`);
39681+
core.info(`Rebase failed (likely due to merge conflicts). Aborting rebase.`);
39682+
// Abort the rebase so the working tree is clean
39683+
const abortResult = await exec.getExecOutput("git", ["rebase", "--abort"], { ignoreReturnCode: true });
39684+
if (abortResult.exitCode !== 0) {
39685+
abortErrorMsg = [
39686+
abortResult.stderr.trim(),
39687+
abortResult.stdout.trim(),
39688+
]
39689+
.filter(Boolean)
39690+
.join("\n") || `rebase --abort failed with exit code ${abortResult.exitCode}`;
39691+
core.info(`rebase --abort failed: ${abortErrorMsg}. The working tree may be in an unexpected state.`);
39692+
}
3968739693
}
3968839694
// Last resort: leave a comment on the existing PR
3968939695
const existingPRNumber = await prExists(owner, repo, branchName, octokit);
3969039696
if (existingPRNumber) {
39691-
core.info(`Could not push to '${branchName}' due to conflicts. Leaving a comment on PR #${existingPRNumber}.`);
39692-
let commentBody = `⚠️ **Sync failed**: The latest \`fern api update\` detected changes, but they could not be pushed to this branch due to merge conflicts.\n\n` +
39697+
core.info(`Could not push to '${branchName}'. Leaving a comment on PR #${existingPRNumber}.`);
39698+
const failureReason = rebaseFailed
39699+
? "merge conflicts"
39700+
: "a push rejection after successful rebase";
39701+
let commentBody = `⚠️ **Sync failed**: The latest \`fern api update\` detected changes, but they could not be pushed to this branch due to ${failureReason}.\n\n` +
3969339702
`**To resolve**, either:\n` +
3969439703
`- Merge or close this PR so the next run creates a fresh one, or\n` +
3969539704
`- Manually rebase this branch on \`${github.context.ref.replace("refs/heads/", "")}\` and re-run the workflow.`;
39696-
if (rebaseErrorMsg) {
39697-
commentBody += `\n\n**Rebase error:** \`${rebaseErrorMsg}\``;
39705+
if (errorMsg) {
39706+
commentBody += `\n\n**${errorLabel}:** \`${errorMsg}\``;
3969839707
}
3969939708
if (abortErrorMsg) {
3970039709
commentBody += `\n**Rebase abort error:** \`${abortErrorMsg}\` — the working tree may be in an unexpected state.`;
@@ -39705,7 +39714,7 @@ async function pushWithFallback(branchName, owner, repo, octokit) {
3970539714
issue_number: existingPRNumber,
3970639715
body: commentBody,
3970739716
});
39708-
core.setFailed(`Failed to push changes to '${branchName}' due to conflicts. A comment has been left on PR #${existingPRNumber}.`);
39717+
core.setFailed(`Failed to push changes to '${branchName}' due to ${failureReason}. A comment has been left on PR #${existingPRNumber}.`);
3970939718
}
3971039719
else {
3971139720
core.setFailed(`Failed to push changes to '${branchName}' and no existing PR was found to comment on.`);

src/sync.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,73 @@ describe("updateFromSourceSpec", () => {
387387
);
388388
});
389389

390+
it("should label as 'Push error' when rebase succeeds but post-rebase push fails", async () => {
391+
setupMocks({ hasChanges: true, existingPRNumber: 42 });
392+
// First push via exec throws
393+
state.execImpl = async (
394+
cmd: string,
395+
args?: string[],
396+
): Promise<number> => {
397+
if (
398+
cmd === "git" &&
399+
Array.isArray(args) &&
400+
args.includes("push")
401+
) {
402+
throw new Error("rejected (non-fast-forward)");
403+
}
404+
return 0;
405+
};
406+
// Rebase succeeds but post-rebase push fails
407+
state.getExecOutputImpl = async (
408+
cmd: string,
409+
args?: string[],
410+
) => {
411+
if (
412+
cmd === "git" &&
413+
Array.isArray(args) &&
414+
args.includes("--porcelain")
415+
) {
416+
return { stdout: "M openapi/openapi.json\n", stderr: "", exitCode: 0 };
417+
}
418+
// rebase succeeds
419+
if (
420+
cmd === "git" &&
421+
Array.isArray(args) &&
422+
args.includes("pull") &&
423+
args.includes("--rebase")
424+
) {
425+
return { stdout: "", stderr: "", exitCode: 0 };
426+
}
427+
// post-rebase push fails
428+
if (
429+
cmd === "git" &&
430+
Array.isArray(args) &&
431+
args.includes("push")
432+
) {
433+
return { stdout: "", stderr: "remote rejected", exitCode: 1 };
434+
}
435+
return { stdout: "", stderr: "", exitCode: 0 };
436+
};
437+
await importAndRun();
438+
439+
const commentCall = mockIssuesCreateComment.mock.calls[0][0];
440+
// Should say "Push error", NOT "Rebase error"
441+
expect(commentCall.body).toContain("Push error:");
442+
expect(commentCall.body).not.toContain("Rebase error:");
443+
// Should mention push rejection, not merge conflicts
444+
expect(commentCall.body).toContain("push rejection after successful rebase");
445+
expect(commentCall.body).not.toContain("merge conflicts");
446+
// Should NOT have run rebase --abort (no rebase in progress)
447+
const abortCall = state.execCalls.find(
448+
([cmd, args]) =>
449+
cmd === "git" &&
450+
Array.isArray(args) &&
451+
args.includes("rebase") &&
452+
args.includes("--abort"),
453+
);
454+
expect(abortCall).toBeUndefined();
455+
});
456+
390457
it("should call setFailed when push fails and no existing PR to comment on", async () => {
391458
setupMocks({ hasChanges: true, existingPRNumber: null });
392459
// First push via exec throws

src/sync.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,10 @@ async function pushWithFallback(
523523
}
524524

525525
// Try pull --rebase then push
526-
let rebaseErrorMsg: string | null = null;
526+
let errorMsg: string | null = null;
527+
let errorLabel: string = "Rebase error";
527528
let abortErrorMsg: string | null = null;
529+
let rebaseFailed = false;
528530

529531
const rebaseResult = await exec.getExecOutput(
530532
"git",
@@ -546,60 +548,70 @@ async function pushWithFallback(
546548
return true;
547549
}
548550

549-
// Push after rebase failed
550-
rebaseErrorMsg = [
551+
// Push after rebase failed — no rebase in progress, don't abort
552+
errorLabel = "Push error";
553+
errorMsg = [
551554
pushResult.stderr.trim(),
552555
pushResult.stdout.trim(),
553556
]
554557
.filter(Boolean)
555558
.join("\n") || `git push failed with exit code ${pushResult.exitCode}`;
559+
core.info(
560+
`Push after rebase failed: ${errorMsg}`,
561+
);
556562
} else {
557563
// Rebase itself failed (merge conflicts)
558-
rebaseErrorMsg = [
564+
rebaseFailed = true;
565+
errorLabel = "Rebase error";
566+
errorMsg = [
559567
rebaseResult.stderr.trim(),
560568
rebaseResult.stdout.trim(),
561569
]
562570
.filter(Boolean)
563571
.join("\n") || `git pull --rebase failed with exit code ${rebaseResult.exitCode}`;
564-
}
565-
566-
core.info(
567-
`Rebase failed (likely due to merge conflicts). Aborting rebase.`,
568-
);
569572

570-
// Abort the rebase so the working tree is clean
571-
const abortResult = await exec.getExecOutput(
572-
"git",
573-
["rebase", "--abort"],
574-
{ ignoreReturnCode: true },
575-
);
576-
if (abortResult.exitCode !== 0) {
577-
abortErrorMsg = [
578-
abortResult.stderr.trim(),
579-
abortResult.stdout.trim(),
580-
]
581-
.filter(Boolean)
582-
.join("\n") || `rebase --abort failed with exit code ${abortResult.exitCode}`;
583573
core.info(
584-
`rebase --abort failed: ${abortErrorMsg}. The working tree may be in an unexpected state.`,
574+
`Rebase failed (likely due to merge conflicts). Aborting rebase.`,
575+
);
576+
577+
// Abort the rebase so the working tree is clean
578+
const abortResult = await exec.getExecOutput(
579+
"git",
580+
["rebase", "--abort"],
581+
{ ignoreReturnCode: true },
585582
);
583+
if (abortResult.exitCode !== 0) {
584+
abortErrorMsg = [
585+
abortResult.stderr.trim(),
586+
abortResult.stdout.trim(),
587+
]
588+
.filter(Boolean)
589+
.join("\n") || `rebase --abort failed with exit code ${abortResult.exitCode}`;
590+
core.info(
591+
`rebase --abort failed: ${abortErrorMsg}. The working tree may be in an unexpected state.`,
592+
);
593+
}
586594
}
587595

588596
// Last resort: leave a comment on the existing PR
589597
const existingPRNumber = await prExists(owner, repo, branchName, octokit);
590598
if (existingPRNumber) {
591599
core.info(
592-
`Could not push to '${branchName}' due to conflicts. Leaving a comment on PR #${existingPRNumber}.`,
600+
`Could not push to '${branchName}'. Leaving a comment on PR #${existingPRNumber}.`,
593601
);
594602

603+
const failureReason = rebaseFailed
604+
? "merge conflicts"
605+
: "a push rejection after successful rebase";
606+
595607
let commentBody =
596-
`⚠️ **Sync failed**: The latest \`fern api update\` detected changes, but they could not be pushed to this branch due to merge conflicts.\n\n` +
608+
`⚠️ **Sync failed**: The latest \`fern api update\` detected changes, but they could not be pushed to this branch due to ${failureReason}.\n\n` +
597609
`**To resolve**, either:\n` +
598610
`- Merge or close this PR so the next run creates a fresh one, or\n` +
599611
`- Manually rebase this branch on \`${github.context.ref.replace("refs/heads/", "")}\` and re-run the workflow.`;
600612

601-
if (rebaseErrorMsg) {
602-
commentBody += `\n\n**Rebase error:** \`${rebaseErrorMsg}\``;
613+
if (errorMsg) {
614+
commentBody += `\n\n**${errorLabel}:** \`${errorMsg}\``;
603615
}
604616
if (abortErrorMsg) {
605617
commentBody += `\n**Rebase abort error:** \`${abortErrorMsg}\` — the working tree may be in an unexpected state.`;
@@ -612,7 +624,7 @@ async function pushWithFallback(
612624
body: commentBody,
613625
});
614626
core.setFailed(
615-
`Failed to push changes to '${branchName}' due to conflicts. A comment has been left on PR #${existingPRNumber}.`,
627+
`Failed to push changes to '${branchName}' due to ${failureReason}. A comment has been left on PR #${existingPRNumber}.`,
616628
);
617629
} else {
618630
core.setFailed(

0 commit comments

Comments
 (0)