fix(update): a finished update moves the installer's bootstrap receipt to its HEAD

install.sh / install.ps1 write .hermes-bootstrap-complete once, at the commit
they installed. The shared completion tail republished install-stamp.json but
never the receipt, so after `hermes update` it kept naming the replaced release
(Windows E2E, installer-script -> hermes-update v2026.6.19 -> HEAD: "pinnedCommit
2bd1977d8f != installed HEAD"). The desktop's own repair bootstrap already
stamps the live HEAD; the CLI update now does the same through
complete_source_checkout, which both the completion handoff and a pre-handoff
updater's --finish-update startup reach.

Only an existing receipt is refreshed: its presence marks a script install, so
a manual clone never grows one. The test uses the E2E's own verifier.
This commit is contained in:
ethernet
2026-09-23 20:41:16 -04:00
parent 9a8cdf96bf
commit ece7c172f8
3 changed files with 80 additions and 3 deletions

View File

@@ -60,9 +60,11 @@ def complete_source_checkout(
completion_message=completion_message,
)
if complete:
from hermes_cli.source_stamp import write_source_stamp
from hermes_cli.source_stamp import refresh_bootstrap_receipt, write_source_stamp
write_source_stamp(root)
stamp = write_source_stamp(root)
if stamp is not None:
refresh_bootstrap_receipt(root, stamp)
return complete

View File

@@ -60,4 +60,43 @@ def write_source_stamp(root: Path) -> dict | None:
with suppress(OSError):
os.unlink(tmp_name)
_reset_version_info_cache()
return stamp
return stamp
BOOTSTRAP_RECEIPT = ".hermes-bootstrap-complete"
def refresh_bootstrap_receipt(root: Path, stamp: dict) -> None:
"""Move the installers' bootstrap receipt to the commit ``stamp`` just published.
install.sh / install.ps1 write the receipt once, at the commit they installed;
an update that moves the checkout has to move it too, or it keeps naming the
replaced release. Only an existing receipt is refreshed: its presence is what
marks a script install, so a manual clone never grows one.
"""
receipt_path = Path(root) / BOOTSTRAP_RECEIPT
try:
# install.ps1 writes it with Windows PowerShell's UTF-8 BOM.
previous = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
except FileNotFoundError:
return
except (OSError, ValueError):
previous = {}
if not isinstance(previous, dict):
previous = {}
receipt = {
**previous,
"schemaVersion": 1,
"pinnedCommit": stamp["commit"],
# A detached checkout keeps the branch it was installed from.
"pinnedBranch": stamp["branch"] or previous.get("pinnedBranch"),
"completedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
}
fd, tmp_name = tempfile.mkstemp(dir=root, prefix=".hermes-bootstrap-complete.", suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(json.dumps(receipt, indent=2) + "\n")
os.replace(tmp_name, receipt_path)
finally:
with suppress(OSError):
os.unlink(tmp_name)

View File

@@ -1,8 +1,10 @@
"""A source checkout publishes identity only after successful completion."""
import json
import os
from pathlib import Path
import subprocess
import sys
from hermes_cli.source_completion import complete_source_checkout
@@ -50,3 +52,37 @@ def test_failed_source_completion_does_not_publish_identity(tmp_path, monkeypatc
assert not complete_source_checkout(root, desktop=False, assume_yes=True)
assert not (root / "install-stamp.json").exists()
def _verify_bootstrap_receipt(root: Path) -> subprocess.CompletedProcess:
"""The same verifier the Windows install/update E2E runs after an update."""
script = Path(__file__).resolve().parents[2] / "scripts" / "verify-bootstrap-version-stamp.py"
return subprocess.run([sys.executable, "-B", str(script), "--stamp", str(root / ".hermes-bootstrap-complete"),
"--repo", str(root)], capture_output=True, text=True, encoding="utf-8")
def test_update_completion_moves_an_installer_receipt_to_the_updated_head(tmp_path, monkeypatch):
root = _repo(tmp_path)
release = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root, capture_output=True, text=True, check=True).stdout.strip()
branch = subprocess.run(["git", "branch", "--show-current"], cwd=root, capture_output=True, text=True, check=True).stdout.strip()
# What install.sh / install.ps1's complete stage leaves behind at the installed release.
(root / ".hermes-bootstrap-complete").write_text(json.dumps({
"schemaVersion": 1, "pinnedCommit": release, "pinnedBranch": branch, "completedAt": "2026-06-19T00:00:00.000Z",
}), encoding="utf-8")
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "update"], cwd=root, check=True, capture_output=True,
env={**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.invalid",
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.invalid"})
_completion_dependencies(monkeypatch, lambda **_kwargs: True)
assert complete_source_checkout(root, desktop=False, assume_yes=True)
result = _verify_bootstrap_receipt(root)
assert result.returncode == 0, result.stdout + result.stderr
def test_completion_never_invents_an_installer_receipt(tmp_path, monkeypatch):
# The receipt's presence is what marks a script install; a manual clone stays one.
root = _repo(tmp_path)
_completion_dependencies(monkeypatch, lambda **_kwargs: True)
assert complete_source_checkout(root, desktop=False, assume_yes=True)
assert not (root / ".hermes-bootstrap-complete").exists()