mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(install): stamp the bootstrap-complete marker from install.sh too
install.ps1 wrote the marker on Windows and the Rust installer now writes it, but install.sh -- the path every Mac and Linux CLI install takes -- never did. A machine set up with install.sh therefore looked uninstalled to the desktop app, which re-ran first-run bootstrap on every launch. Stamp the same schema-v1 payload install.ps1 writes, from both the staged `complete` stage and monolithic main(). An unresolvable HEAD skips the marker rather than writing one the desktop validator rejects: absent reads as a clean "bootstrap needed", malformed reads as a confusing half-state.
This commit is contained in:
parent
ea3be4191e
commit
2b0b5e4c53
2 changed files with 154 additions and 0 deletions
|
|
@ -2391,6 +2391,48 @@ maybe_start_gateway() {
|
|||
fi
|
||||
}
|
||||
|
||||
write_bootstrap_marker() {
|
||||
# Writes $INSTALL_DIR/.hermes-bootstrap-complete, which tells the Hermes
|
||||
# desktop app (apps/desktop/electron/main.ts) and the macOS launcher fast
|
||||
# path (apps/bootstrap-installer) "a real install finished here -- don't
|
||||
# re-run first-run bootstrap."
|
||||
#
|
||||
# Schema mirrors install.ps1's Write-BootstrapMarker and main.ts's
|
||||
# writeBootstrapMarker(). Keep the three in lockstep:
|
||||
# schemaVersion 1 + pinnedCommit (length >= 7) are what the desktop
|
||||
# validator requires; desktopVersion is omitted because only the desktop
|
||||
# app knows its own version.
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
log_warn "Skipping bootstrap marker: $INSTALL_DIR doesn't exist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Explicit --commit wins; otherwise read HEAD from the checkout we just
|
||||
# installed. If neither resolves, skip the marker entirely rather than
|
||||
# write one the desktop will reject -- an absent marker is a clean
|
||||
# "bootstrap needed", a malformed one is a confusing half-state.
|
||||
local pinned_commit="$INSTALL_COMMIT"
|
||||
if [ -z "$pinned_commit" ]; then
|
||||
pinned_commit=$(git -C "$INSTALL_DIR" rev-parse HEAD 2>/dev/null) || pinned_commit=""
|
||||
fi
|
||||
|
||||
if [ -z "$pinned_commit" ]; then
|
||||
log_warn "Skipping bootstrap marker: could not resolve HEAD in $INSTALL_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local marker_path="$INSTALL_DIR/.hermes-bootstrap-complete"
|
||||
local tmp_path="$marker_path.tmp"
|
||||
|
||||
# Atomic publish: the macOS launcher predicate only checks existence, so a
|
||||
# torn write would arm the fast path against a half-written marker.
|
||||
printf '{\n "schemaVersion": 1,\n "pinnedCommit": "%s",\n "pinnedBranch": "%s",\n "completedAt": "%s"\n}\n' \
|
||||
"$pinned_commit" \
|
||||
"$BRANCH" \
|
||||
"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" > "$tmp_path"
|
||||
mv -f "$tmp_path" "$marker_path"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo ""
|
||||
echo -e "${GREEN}${BOLD}"
|
||||
|
|
@ -3024,6 +3066,7 @@ run_stage_body() {
|
|||
detect_os
|
||||
resolve_install_layout
|
||||
print_success
|
||||
write_bootstrap_marker
|
||||
# Code-scoped stamp: write next to the install tree, not into
|
||||
# $HERMES_HOME. $HERMES_HOME is a shared data dir (it can be
|
||||
# bind-mounted into a Docker gateway too), so a stamp there gets
|
||||
|
|
@ -3108,6 +3151,8 @@ main() {
|
|||
|
||||
print_success
|
||||
|
||||
write_bootstrap_marker
|
||||
|
||||
# Code-scoped stamp: write next to the install tree, not into $HERMES_HOME.
|
||||
# $HERMES_HOME is a shared data dir (it can be bind-mounted into a Docker
|
||||
# gateway too), so a stamp there gets clobbered by the container's 'docker'
|
||||
|
|
|
|||
109
tests/test_install_sh_bootstrap_marker.py
Normal file
109
tests/test_install_sh_bootstrap_marker.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""install.sh must stamp the desktop bootstrap-complete marker.
|
||||
|
||||
The marker at ``$INSTALL_DIR/.hermes-bootstrap-complete`` is what the desktop
|
||||
app (apps/desktop/electron/main.ts) and the macOS launcher fast path
|
||||
(apps/bootstrap-installer) use to decide "a real install finished here."
|
||||
install.sh never wrote it, so a CLI-installed Mac/Linux box re-ran first-run
|
||||
bootstrap on every desktop launch (#60721).
|
||||
|
||||
These exercise the real shell function against a temp checkout rather than
|
||||
asserting on the text of install.sh.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
|
||||
|
||||
|
||||
def run_write_marker(install_dir, *, commit="", branch="main"):
|
||||
"""Source install.sh and invoke write_bootstrap_marker in isolation.
|
||||
|
||||
install.sh guards its own entrypoint behind MANIFEST_MODE/STAGE_NAME/main,
|
||||
so sourcing it with --help-less argv defines the functions without running
|
||||
an install.
|
||||
"""
|
||||
script = f"""
|
||||
set -e
|
||||
INSTALL_DIR={install_dir!s}
|
||||
INSTALL_COMMIT={commit!r}
|
||||
BRANCH={branch!r}
|
||||
# Pull in the function definitions without triggering an install.
|
||||
eval "$(sed -n '/^write_bootstrap_marker()/,/^}}/p' {INSTALL_SH!s})"
|
||||
log_warn() {{ echo "WARN: $*" >&2; }}
|
||||
write_bootstrap_marker
|
||||
"""
|
||||
return subprocess.run(
|
||||
["bash", "-c", script], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
|
||||
def make_checkout(tmp_path):
|
||||
install_dir = tmp_path / "hermes-agent"
|
||||
install_dir.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=install_dir, check=True)
|
||||
subprocess.run(
|
||||
["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q",
|
||||
"--allow-empty", "-m", "init"],
|
||||
cwd=install_dir,
|
||||
check=True,
|
||||
)
|
||||
return install_dir
|
||||
|
||||
|
||||
def test_marker_matches_the_schema_the_desktop_validates(tmp_path):
|
||||
"""Desktop's isBootstrapComplete() needs schemaVersion 1 + a >=7 char commit."""
|
||||
install_dir = make_checkout(tmp_path)
|
||||
|
||||
result = run_write_marker(install_dir)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
marker = install_dir / ".hermes-bootstrap-complete"
|
||||
assert marker.is_file(), "install.sh must stamp the bootstrap marker"
|
||||
|
||||
payload = json.loads(marker.read_text())
|
||||
assert payload["schemaVersion"] == 1
|
||||
assert len(payload["pinnedCommit"]) >= 7
|
||||
assert payload["pinnedBranch"] == "main"
|
||||
assert payload["completedAt"].endswith("Z")
|
||||
|
||||
|
||||
def test_marker_publish_leaves_no_temp_sibling(tmp_path):
|
||||
"""The launcher predicate is existence-only, so the write must be atomic."""
|
||||
install_dir = make_checkout(tmp_path)
|
||||
|
||||
run_write_marker(install_dir)
|
||||
|
||||
assert (install_dir / ".hermes-bootstrap-complete").is_file()
|
||||
assert not (install_dir / ".hermes-bootstrap-complete.tmp").exists()
|
||||
|
||||
|
||||
def test_explicit_commit_pin_wins_over_head(tmp_path):
|
||||
install_dir = make_checkout(tmp_path)
|
||||
pinned = "abcdef1234567890abcdef1234567890abcdef12"
|
||||
|
||||
run_write_marker(install_dir, commit=pinned)
|
||||
|
||||
payload = json.loads((install_dir / ".hermes-bootstrap-complete").read_text())
|
||||
assert payload["pinnedCommit"] == pinned
|
||||
|
||||
|
||||
def test_no_marker_written_when_head_cannot_be_resolved(tmp_path):
|
||||
"""A malformed marker is worse than none: absent means a clean re-bootstrap."""
|
||||
install_dir = tmp_path / "not-a-checkout"
|
||||
install_dir.mkdir()
|
||||
|
||||
result = run_write_marker(install_dir)
|
||||
|
||||
assert result.returncode == 0, "an unresolvable HEAD must not fail the install"
|
||||
assert not (install_dir / ".hermes-bootstrap-complete").exists()
|
||||
|
||||
|
||||
def test_missing_install_dir_is_not_fatal(tmp_path):
|
||||
result = run_write_marker(tmp_path / "does-not-exist")
|
||||
|
||||
assert result.returncode == 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue