diff --git a/apps/bootstrap-installer/package.json b/apps/bootstrap-installer/package.json index c385f9b108..ff55c62f54 100644 --- a/apps/bootstrap-installer/package.json +++ b/apps/bootstrap-installer/package.json @@ -1,7 +1,7 @@ { "name": "@hermes/bootstrap-installer", "private": true, - "version": "0.0.1", + "version": "0.21.1", "description": "Hermes Setup — signed installer that drives scripts/install.ps1 with a polished native UI.", "type": "module", "scripts": { diff --git a/apps/bootstrap-installer/src-tauri/Cargo.toml b/apps/bootstrap-installer/src-tauri/Cargo.toml index 78fc71e56c..2057af89bb 100644 --- a/apps/bootstrap-installer/src-tauri/Cargo.toml +++ b/apps/bootstrap-installer/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hermes-bootstrap" -version = "0.0.1" +version = "0.21.1" description = "Hermes Setup — signed installer that drives scripts/install.ps1" authors = ["Nous Research "] edition = "2021" diff --git a/apps/bootstrap-installer/src-tauri/tauri.conf.json b/apps/bootstrap-installer/src-tauri/tauri.conf.json index a74bd105c3..c789e9524c 100644 --- a/apps/bootstrap-installer/src-tauri/tauri.conf.json +++ b/apps/bootstrap-installer/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Hermes", - "version": "0.0.1", + "version": "0.21.1", "identifier": "com.nousresearch.hermes.setup", "build": { "beforeDevCommand": "npm run dev", diff --git a/scripts/release.py b/scripts/release.py index 10eb846a63..5a07abf88a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -2230,6 +2230,60 @@ def update_version_files(semver: str, calver_date: str): ) desktop_pkg.write_text(pkg_text, encoding="utf-8") + # Keep the bootstrap installer (Hermes-Setup.dmg CFBundleShortVersionString) + # in lockstep with the Python package version. Tauri reads `version` from + # package.json + tauri.conf.json; a hardcoded 0.0.1 ships in the DMG. + installer_pkg = REPO_ROOT / "apps" / "bootstrap-installer" / "package.json" + if installer_pkg.exists(): + pkg_text = installer_pkg.read_text(encoding="utf-8") + pkg_text = re.sub( + r'("version"\s*:\s*)"[^"]+"', + rf'\g<1>"{semver}"', + pkg_text, + count=1, + ) + installer_pkg.write_text(pkg_text, encoding="utf-8") + + installer_tauri = ( + REPO_ROOT / "apps" / "bootstrap-installer" / "src-tauri" / "tauri.conf.json" + ) + if installer_tauri.exists(): + pkg_text = installer_tauri.read_text(encoding="utf-8") + pkg_text = re.sub( + r'("version"\s*:\s*)"[^"]+"', + rf'\g<1>"{semver}"', + pkg_text, + count=1, + ) + installer_tauri.write_text(pkg_text, encoding="utf-8") + + installer_cargo = ( + REPO_ROOT / "apps" / "bootstrap-installer" / "src-tauri" / "Cargo.toml" + ) + if installer_cargo.exists(): + cargo_text = installer_cargo.read_text(encoding="utf-8") + cargo_text = re.sub( + r'^version\s*=\s*"[^"]+"', + f'version = "{semver}"', + cargo_text, + count=1, + flags=re.MULTILINE, + ) + installer_cargo.write_text(cargo_text, encoding="utf-8") + + +def version_files_to_stage() -> list[str]: + """Return version-bearing files that exist and should be `git add`ed after a bump.""" + candidates = [ + VERSION_FILE, + PYPROJECT_FILE, + REPO_ROOT / "apps" / "desktop" / "package.json", + REPO_ROOT / "apps" / "bootstrap-installer" / "package.json", + REPO_ROOT / "apps" / "bootstrap-installer" / "src-tauri" / "tauri.conf.json", + REPO_ROOT / "apps" / "bootstrap-installer" / "src-tauri" / "Cargo.toml", + ] + return [str(path) for path in candidates if path.exists()] + def resolve_author(name: str, email: str) -> str: """Resolve a git author to a GitHub @mention.""" @@ -2568,7 +2622,7 @@ def main(): print(f" ✓ Updated version files to v{new_version} ({calver_date})") # Commit version bump - add_files = [str(VERSION_FILE), str(PYPROJECT_FILE)] + add_files = version_files_to_stage() add_result = git_result("add", *add_files) if add_result.returncode != 0: print(f" ✗ Failed to stage version files: {add_result.stderr.strip()}") diff --git a/tests/scripts/test_release_bootstrap_installer_version.py b/tests/scripts/test_release_bootstrap_installer_version.py new file mode 100644 index 0000000000..a6e4ab25e5 --- /dev/null +++ b/tests/scripts/test_release_bootstrap_installer_version.py @@ -0,0 +1,144 @@ +"""release.py must stamp bootstrap-installer versions with the release semver. + +Tauri CFBundleShortVersionString is read from +apps/bootstrap-installer/src-tauri/tauri.conf.json (and the sibling +package.json). Those files were hardcoded 0.0.1 and omitted from +update_version_files / the --publish --bump git add list, so Hermes-Setup.dmg +always shipped 0.0.1. Same class as the desktop stamp (#68783 / PR #68796). +""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "release.py" + + +def _load(): + spec = importlib.util.spec_from_file_location( + "release_bootstrap_installer_version", SCRIPT + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +release = _load() + + +def _json_version(path: Path) -> str: + return json.loads(path.read_text(encoding="utf-8"))["version"] + + +def _patch_repo(tmp_path, monkeypatch, *, with_installer: bool = True): + repo = tmp_path + init_py = repo / "hermes_cli" / "__init__.py" + init_py.parent.mkdir(parents=True) + init_py.write_text( + '__version__ = "0.0.1"\n__release_date__ = "2026.1.1"\n', + encoding="utf-8", + ) + pyproject = repo / "pyproject.toml" + pyproject.write_text('version = "0.0.1"\n', encoding="utf-8") + + desktop_pkg = repo / "apps" / "desktop" / "package.json" + desktop_pkg.parent.mkdir(parents=True) + desktop_pkg.write_text('{"version":"0.0.1"}\n', encoding="utf-8") + + installer_pkg = repo / "apps" / "bootstrap-installer" / "package.json" + tauri_conf = ( + repo / "apps" / "bootstrap-installer" / "src-tauri" / "tauri.conf.json" + ) + cargo_toml = repo / "apps" / "bootstrap-installer" / "src-tauri" / "Cargo.toml" + if with_installer: + tauri_conf.parent.mkdir(parents=True) + installer_pkg.write_text( + '{"name":"x","version":"0.0.1"}\n', encoding="utf-8" + ) + tauri_conf.write_text( + '{"productName":"Hermes","version":"0.0.1"}\n', encoding="utf-8" + ) + cargo_toml.write_text('[package]\nversion = "0.0.1"\n', encoding="utf-8") + + monkeypatch.setattr(release, "REPO_ROOT", repo) + monkeypatch.setattr(release, "VERSION_FILE", init_py) + monkeypatch.setattr(release, "PYPROJECT_FILE", pyproject) + return { + "repo": repo, + "init_py": init_py, + "pyproject": pyproject, + "desktop_pkg": desktop_pkg, + "installer_pkg": installer_pkg, + "tauri_conf": tauri_conf, + "cargo_toml": cargo_toml, + } + + +def test_update_version_files_stamps_bootstrap_installer(tmp_path, monkeypatch): + paths = _patch_repo(tmp_path, monkeypatch) + + release.update_version_files("0.21.1", "2026.9.10") + + assert _json_version(paths["installer_pkg"]) == "0.21.1" + assert _json_version(paths["tauri_conf"]) == "0.21.1" + assert 'version = "0.21.1"' in paths["cargo_toml"].read_text(encoding="utf-8") + + # CONTROL: existing desktop / Python stamps still happen. + assert _json_version(paths["desktop_pkg"]) == "0.21.1" + assert 'version = "0.21.1"' in paths["pyproject"].read_text(encoding="utf-8") + init_text = paths["init_py"].read_text(encoding="utf-8") + assert '__version__ = "0.21.1"' in init_text + assert '__release_date__ = "2026.9.10"' in init_text + + +def test_update_version_files_skips_missing_installer_dir(tmp_path, monkeypatch): + paths = _patch_repo(tmp_path, monkeypatch, with_installer=False) + + release.update_version_files("0.21.1", "2026.9.10") + + assert not paths["installer_pkg"].exists() + assert not paths["tauri_conf"].exists() + assert _json_version(paths["desktop_pkg"]) == "0.21.1" + assert 'version = "0.21.1"' in paths["pyproject"].read_text(encoding="utf-8") + assert '__version__ = "0.21.1"' in paths["init_py"].read_text(encoding="utf-8") + + +def test_update_version_files_does_not_invent_version_keys(tmp_path, monkeypatch): + paths = _patch_repo(tmp_path, monkeypatch) + original = '{"name":"x","productName":"Hermes"}\n' + paths["installer_pkg"].write_text(original, encoding="utf-8") + paths["tauri_conf"].write_text(original, encoding="utf-8") + + release.update_version_files("0.21.1", "2026.9.10") + + assert paths["installer_pkg"].read_text(encoding="utf-8") == original + assert paths["tauri_conf"].read_text(encoding="utf-8") == original + assert _json_version(paths["desktop_pkg"]) == "0.21.1" + + +def test_version_files_to_stage_includes_installer_when_present(tmp_path, monkeypatch): + paths = _patch_repo(tmp_path, monkeypatch) + + staged = release.version_files_to_stage() + + assert str(paths["init_py"]) in staged + assert str(paths["pyproject"]) in staged + assert str(paths["desktop_pkg"]) in staged + assert str(paths["installer_pkg"]) in staged + assert str(paths["tauri_conf"]) in staged + assert str(paths["cargo_toml"]) in staged + + +def test_version_files_to_stage_omits_missing_installer(tmp_path, monkeypatch): + paths = _patch_repo(tmp_path, monkeypatch, with_installer=False) + + staged = release.version_files_to_stage() + + assert str(paths["installer_pkg"]) not in staged + assert str(paths["tauri_conf"]) not in staged + assert str(paths["cargo_toml"]) not in staged + assert str(paths["desktop_pkg"]) in staged + assert str(paths["init_py"]) in staged + assert str(paths["pyproject"]) in staged