From a40f22798ec6b5877958f6dbe5aeda8468e7d628 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Tue, 30 Jun 2026 20:11:01 +0700 Subject: [PATCH 1/2] fix(installer): reset managed clone when ff-only pull fails Bootstrap and desktop updates run install.ps1/install.sh, which aborted with exit 128 when the managed checkout had diverged from origin/main. Mirror the hermes update recovery path: reset to origin/$BRANCH instead of failing the repository stage. --- scripts/install.ps1 | 10 +++++++++- scripts/install.sh | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index fde9d4363..15f98f4b4 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1362,8 +1362,16 @@ function Install-Repository { } else { git -c windows.appendAtomically=false checkout $Branch if ($LASTEXITCODE -ne 0) { throw "git checkout $Branch failed (exit $LASTEXITCODE)" } + # Managed installs should follow origin/$Branch exactly. If + # the checkout has diverged (or has local-only commits), + # ff-only pull cannot succeed — mirror ``hermes update`` and + # reset to the fetched remote so bootstrap/install can recover. git -c windows.appendAtomically=false pull --ff-only origin $Branch - if ($LASTEXITCODE -ne 0) { throw "git pull failed (exit $LASTEXITCODE)" } + if ($LASTEXITCODE -ne 0) { + Write-Warn "Fast-forward not possible; resetting managed install to origin/$Branch..." + git -c windows.appendAtomically=false reset --hard "origin/$Branch" + if ($LASTEXITCODE -ne 0) { throw "git reset --hard origin/$Branch failed (exit $LASTEXITCODE)" } + } } if ($autostashRef) { diff --git a/scripts/install.sh b/scripts/install.sh index 864f9e075..4171ba3d7 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1222,7 +1222,14 @@ clone_repo() { git remote set-branches origin "$BRANCH" 2>/dev/null || true git fetch origin "$BRANCH" git checkout "$BRANCH" - git pull --ff-only origin "$BRANCH" + # Managed installs should follow origin/$BRANCH exactly. If the + # checkout has diverged (or has local-only commits), ff-only pull + # cannot succeed — mirror ``hermes update`` and reset to the + # fetched remote so bootstrap/install can recover. + if ! git pull --ff-only origin "$BRANCH"; then + log_warn "Fast-forward not possible; resetting managed install to origin/$BRANCH..." + git reset --hard "origin/$BRANCH" + fi if [ -n "$autostash_ref" ]; then local restore_now="yes" From 665d0f0789ee0571e8c4284342c842cc57ebf2dc Mon Sep 17 00:00:00 2001 From: xxxigm Date: Tue, 30 Jun 2026 20:11:01 +0700 Subject: [PATCH 2/2] test(installer): cover diverged managed-clone recovery in install scripts Pin the ff-only-then-reset fallback in install.sh and install.ps1 so bootstrap cannot regress to hard-failing on diverged git history. --- tests/test_install_diverged_update.py | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_install_diverged_update.py diff --git a/tests/test_install_diverged_update.py b/tests/test_install_diverged_update.py new file mode 100644 index 000000000..a042d8c48 --- /dev/null +++ b/tests/test_install_diverged_update.py @@ -0,0 +1,67 @@ +"""Regression: installer/bootstrap must recover from diverged managed clones. + +When ``~/.hermes/hermes-agent`` has local-only commits (or diverged history), +``git pull --ff-only`` fails with exit 128 and bootstrap aborts at the +repository stage. ``hermes update`` already resets to ``origin/$BRANCH`` in +that case; both installer scripts must do the same. + +Fixes the bootstrap failure seen in #53257 and desktop update paths that run +``install.ps1`` / ``install.sh`` non-interactively. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = REPO_ROOT / "scripts" / "install.sh" +INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1" + + +def _extract_install_sh_update_block() -> str: + text = INSTALL_SH.read_text() + match = re.search( + r"(?Pgit checkout \"\$BRANCH\".*?fi\n\n if \[ -n \"\$autostash_ref\" \])", + text, + re.DOTALL, + ) + assert match is not None, "managed-install update block not found in install.sh" + return match["block"] + + +def _extract_install_ps1_branch_update_block() -> str: + text = INSTALL_PS1.read_text() + match = re.search( + r"(?Pgit -c windows\.appendAtomically=false checkout \$Branch.*?elseif \(\$Tag\))", + text, + re.DOTALL, + ) + assert match is not None, "branch update block not found in install.ps1" + return match["block"] + + +def test_install_sh_resets_when_ff_only_pull_fails() -> None: + block = _extract_install_sh_update_block() + + assert 'git pull --ff-only origin "$BRANCH"' in block + assert 'git reset --hard "origin/$BRANCH"' in block + assert "Fast-forward not possible" in block + + pull_idx = block.find('git pull --ff-only origin "$BRANCH"') + reset_idx = block.find('git reset --hard "origin/$BRANCH"') + assert pull_idx != -1 and reset_idx != -1 + assert pull_idx < reset_idx, "ff-only pull must be attempted before reset fallback" + + +def test_install_ps1_resets_when_ff_only_pull_fails() -> None: + block = _extract_install_ps1_branch_update_block() + + assert "pull --ff-only origin $Branch" in block + assert 'reset --hard "origin/$Branch"' in block + assert "Fast-forward not possible" in block + + pull_idx = block.find("pull --ff-only origin $Branch") + reset_idx = block.find('reset --hard "origin/$Branch"') + assert pull_idx != -1 and reset_idx != -1 + assert pull_idx < reset_idx, "ff-only pull must be attempted before reset fallback"