Skip to content

fix(dev-lead): force-with-lease retry when a rebase rewrites branch history (#647) - #649

Merged
don-petry merged 1 commit into
mainfrom
claude/github-actions-failure-vtnmt0
Jun 13, 2026
Merged

don-petry merged 1 commit into
mainfrom
claude/github-actions-failure-vtnmt0

Conversation

@don-petry

Copy link
Copy Markdown
Collaborator

Closes #647

Problem

push_with_merge_guard (scripts/lib/auto-merge.sh) ran a plain git push. When an engine resolved a rebase under a non-rebase intent (on-mention, review-changes), the rewritten branch history could never be published — the push was rejected non-fast-forward and the whole run failed red, even though the rebase and conflict resolution succeeded.

Observed in run #27471503333 (@dev-lead - resolve the rebase main conflict on PR #624), which left PR #624 stuck dirty for a day. Only the dedicated rebase intent worked, because prompts/dev-lead/rebase.md has the engine force-push itself.

Fix

When the plain push is rejected non-fast-forward and HEAD has diverged from its upstream (i.e. history was rewritten), retry once with --force-with-lease:

  • The lease (our remote-tracking ref) makes the retry abort if the remote advanced under us, so a concurrent push by someone else is never clobbered.
  • A normal non-fast-forward where the branch is not diverged (remote legitimately advanced) still fails exactly as before — no force.
  • All callers invoke push_with_merge_guard with no args, so the behavior change is scoped to the rejected-push path only.

This mirrors the established force-push in the dedicated rebase intent.

Tests

tests/dev-lead/unit/test_auto_merge.bats — added 2 cases (force-with-lease retry succeeds on rewritten history; no force-push when branch isn't diverged). Full suite: 25/25 pass, shellcheck --severity=warning -x clean.


Note: the original symptom (PR #624 stuck) was already resolved manually by rebasing + force-pushing it; #624 has since merged and closed #573. This PR fixes the underlying push path so it can't recur.


Generated by Claude Code

…istory

push_with_merge_guard did a plain `git push`, so when an engine resolved a
rebase under a non-rebase intent (on-mention, review-changes) the rewritten
branch could never be published — the push was rejected non-fast-forward and
the run failed red, even though the rebase + conflict resolution succeeded
(e.g. PR #624 stuck dirty for a day).

Detect that case — a non-fast-forward rejection while HEAD has diverged from
its upstream (history rewritten) — and retry once with --force-with-lease. The
lease (our remote-tracking ref) makes the retry abort if the remote advanced
under us, so a concurrent push is never clobbered. Normal non-fast-forwards
(remote legitimately advanced, branch not diverged) still fail as before. This
mirrors the dedicated rebase intent's own force-push (prompts/dev-lead/rebase.md).

Closes #647

https://claude.ai/code/session_01BTcE5pAvrmNqkatFUTAetF
@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@don-petry, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 9 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20fadeee-cfaa-4046-af30-73e1260f289c

📥 Commits

Reviewing files that changed from the base of the PR and between 6042fcd and 3d374b3.

📒 Files selected for processing (2)
  • scripts/lib/auto-merge.sh
  • tests/dev-lead/unit/test_auto_merge.bats
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/github-actions-failure-vtnmt0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud

Copy link
Copy Markdown

@don-petry

Copy link
Copy Markdown
Collaborator Author

Dev-Lead — review-changes (no-changes)

No changes were needed for this PR.

@don-petry
don-petry marked this pull request as ready for review June 13, 2026 19:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a retry mechanism using --force-with-lease in scripts/lib/auto-merge.sh when a push is rejected due to a non-fast-forward error and the local branch has diverged from its upstream (indicating a history rewrite). It also adds corresponding unit tests in tests/dev-lead/unit/test_auto_merge.bats. The review feedback suggests improving readability in the test stub helper by extracting the outer function's positional parameter into a named local variable to avoid confusion with the inner script's escaped positional parameters inside the heredoc.

Comment on lines +194 to +217
_install_git_stub_force() {
cat > "$STUB_BIN_DIR/git" <<EOF
#!/usr/bin/env bash
case "\$1" in
push)
shift
for a in "\$@"; do
[ "\$a" = "--force-with-lease" ] && exit 0
done
echo " ! [rejected] feature -> feature (non-fast-forward)" >&2
exit 1
;;
rev-parse)
case "\$*" in
*"@{u}"*) echo "origin/feature" ;;
*) echo "deadbeefcafe" ;;
esac
exit 0
;;
merge-base)
# --is-ancestor <upstream> HEAD: 0 = upstream reachable (not diverged),
# 1 = diverged (history rewritten by a rebase).
[ "${1:-}" = "diverged" ] && exit 1 || exit 0
;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Mixing escaped and unescaped positional parameters (like \$1 and ${1:-}) inside a double-quoted heredoc (<<EOF) can be confusing and hard to maintain. It is not immediately obvious that ${1:-} refers to the argument of the outer function _install_git_stub_force rather than the generated script's own $1 (which is matched as \$1 on line 197).

Using a clearly named local variable (e.g., local mode="${1:-}") and referencing it as "$mode" makes the distinction between generation-time expansion and runtime evaluation much clearer.

_install_git_stub_force() {
  local mode="${1:-}"
  cat > "$STUB_BIN_DIR/git" <<EOF
#!/usr/bin/env bash
case "\$1" in
  push)
    shift
    for a in "\$@"; do
      [ "\$a" = "--force-with-lease" ] && exit 0
    done
    echo "  ! [rejected]        feature -> feature (non-fast-forward)" >&2
    exit 1
    ;;
  rev-parse)
    case "\$*" in
      *"@{u}"*) echo "origin/feature" ;;
      *)        echo "deadbeefcafe" ;;
    esac
    exit 0
    ;;
  merge-base)
    # --is-ancestor <upstream> HEAD: 0 = upstream reachable (not diverged),
    # 1 = diverged (history rewritten by a rebase).
    [ "$mode" = "diverged" ] && exit 1 || exit 0
    ;;

@don-petry
don-petry requested a review from a team as a code owner June 13, 2026 19:57
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@don-petry
don-petry merged commit 0ecbb10 into main Jun 13, 2026
39 of 43 checks passed
@don-petry
don-petry deleted the claude/github-actions-failure-vtnmt0 branch June 13, 2026 19:57
don-petry added a commit that referenced this pull request Jun 14, 2026
…istory (#649)

push_with_merge_guard did a plain `git push`, so when an engine resolved a
rebase under a non-rebase intent (on-mention, review-changes) the rewritten
branch could never be published — the push was rejected non-fast-forward and
the run failed red, even though the rebase + conflict resolution succeeded
(e.g. PR #624 stuck dirty for a day).

Detect that case — a non-fast-forward rejection while HEAD has diverged from
its upstream (history rewritten) — and retry once with --force-with-lease. The
lease (our remote-tracking ref) makes the retry abort if the remote advanced
under us, so a concurrent push is never clobbered. Normal non-fast-forwards
(remote legitimately advanced, branch not diverged) still fail as before. This
mirrors the dedicated rebase intent's own force-push (prompts/dev-lead/rebase.md).

Closes #647

https://claude.ai/code/session_01BTcE5pAvrmNqkatFUTAetF

Co-authored-by: Claude <noreply@anthropic.com>
don-petry added a commit that referenced this pull request Jun 18, 2026
…istory (#649)

push_with_merge_guard did a plain `git push`, so when an engine resolved a
rebase under a non-rebase intent (on-mention, review-changes) the rewritten
branch could never be published — the push was rejected non-fast-forward and
the run failed red, even though the rebase + conflict resolution succeeded
(e.g. PR #624 stuck dirty for a day).

Detect that case — a non-fast-forward rejection while HEAD has diverged from
its upstream (history rewritten) — and retry once with --force-with-lease. The
lease (our remote-tracking ref) makes the retry abort if the remote advanced
under us, so a concurrent push is never clobbered. Normal non-fast-forwards
(remote legitimately advanced, branch not diverged) still fail as before. This
mirrors the dedicated rebase intent's own force-push (prompts/dev-lead/rebase.md).

Closes #647

https://claude.ai/code/session_01BTcE5pAvrmNqkatFUTAetF

Co-authored-by: Claude <noreply@anthropic.com>
don-petry added a commit that referenced this pull request Jun 25, 2026
…istory (#649)

push_with_merge_guard did a plain `git push`, so when an engine resolved a
rebase under a non-rebase intent (on-mention, review-changes) the rewritten
branch could never be published — the push was rejected non-fast-forward and
the run failed red, even though the rebase + conflict resolution succeeded
(e.g. PR #624 stuck dirty for a day).

Detect that case — a non-fast-forward rejection while HEAD has diverged from
its upstream (history rewritten) — and retry once with --force-with-lease. The
lease (our remote-tracking ref) makes the retry abort if the remote advanced
under us, so a concurrent push is never clobbered. Normal non-fast-forwards
(remote legitimately advanced, branch not diverged) still fail as before. This
mirrors the dedicated rebase intent's own force-push (prompts/dev-lead/rebase.md).

Closes #647

https://claude.ai/code/session_01BTcE5pAvrmNqkatFUTAetF

Co-authored-by: Claude <noreply@anthropic.com>
don-petry added a commit that referenced this pull request Jun 25, 2026
…istory (#649)

push_with_merge_guard did a plain `git push`, so when an engine resolved a
rebase under a non-rebase intent (on-mention, review-changes) the rewritten
branch could never be published — the push was rejected non-fast-forward and
the run failed red, even though the rebase + conflict resolution succeeded
(e.g. PR #624 stuck dirty for a day).

Detect that case — a non-fast-forward rejection while HEAD has diverged from
its upstream (history rewritten) — and retry once with --force-with-lease. The
lease (our remote-tracking ref) makes the retry abort if the remote advanced
under us, so a concurrent push is never clobbered. Normal non-fast-forwards
(remote legitimately advanced, branch not diverged) still fail as before. This
mirrors the dedicated rebase intent's own force-push (prompts/dev-lead/rebase.md).

Closes #647

https://claude.ai/code/session_01BTcE5pAvrmNqkatFUTAetF

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants