ece7c172f8refreshed .hermes-bootstrap-complete only from complete_source_checkout, but two other paths publish a new checkout identity through write_source_stamp without it: the PM updater's finish (update_finish.py, the path an old updater's handoff takes) and boot-time blessed-checkout adoption (post_update.py). The Windows E2E leg installer-script -> hermes-update (v2026.6.19 -> HEAD) takes the handoff, so it still failed "pinnedCommit2bd1977d8f!= installed HEAD ece7c172f8d8". The refresh now lives inside write_source_stamp, so every caller moves the receipt with the identity it publishes. The tests pin that seam (red onece7c172f8with the CI message, green here).
105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
"""Atomically publish identity for a mutable source checkout."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import suppress
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
from hermes_cli.version_info import _git_version_info, _reset_version_info_cache
|
|
|
|
|
|
def write_source_stamp(root: Path) -> dict | None:
|
|
"""Replace ``install-stamp.json`` with identity read from ``root`` itself.
|
|
|
|
A root git cannot identify -- the ZIP update fallback runs precisely because
|
|
git is unusable -- publishes no identity: the old stamp is removed rather
|
|
than left naming the commit that was just replaced. Returns None then.
|
|
"""
|
|
root = Path(root).resolve()
|
|
info = _git_version_info(root, include_untracked=True)
|
|
if info.commit is None:
|
|
with suppress(FileNotFoundError):
|
|
(root / "install-stamp.json").unlink()
|
|
_reset_version_info_cache()
|
|
return None
|
|
stamp = {
|
|
"schemaVersion": 2,
|
|
"commit": info.commit,
|
|
"commitDate": info.commit_date,
|
|
"branch": info.branch,
|
|
"builtAt": datetime.now(timezone.utc).isoformat(),
|
|
"dirty": info.dirty,
|
|
"source": "git",
|
|
"distribution": None,
|
|
"updateMechanism": "self",
|
|
"baseVersion": info.base_version,
|
|
"displayVersion": info.derived_version,
|
|
"distance": info.distance,
|
|
"payload": "bootstrap",
|
|
"tag": None,
|
|
}
|
|
stamp_path = root / "install-stamp.json"
|
|
fd, tmp_name = tempfile.mkstemp(dir=root, prefix=".install-stamp.", suffix=".tmp")
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
handle.write(json.dumps(stamp, indent=2) + "\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(tmp_name, stamp_path)
|
|
with suppress(OSError):
|
|
directory_fd = os.open(root, os.O_RDONLY)
|
|
try:
|
|
os.fsync(directory_fd)
|
|
finally:
|
|
os.close(directory_fd)
|
|
finally:
|
|
with suppress(OSError):
|
|
os.unlink(tmp_name)
|
|
_reset_version_info_cache()
|
|
# Every path that publishes a new checkout identity (completion handoff, the PM
|
|
# updater's finish, boot-time adoption) moves the installers' receipt with it.
|
|
refresh_bootstrap_receipt(root, 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) |