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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user