Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hermes",
"productName": "Hermes",
"private": true,
"version": "0.17.0",
"version": "0.19.0",
"description": "Native desktop shell for Hermes Agent.",
"author": "Nous Research",
"type": "module",
Expand Down
24 changes: 19 additions & 5 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -2165,8 +2165,15 @@ def bump_version(current: str, part: str) -> str:
return f"{major}.{minor}.{patch}"


def update_version_files(semver: str, calver_date: str):
"""Update version strings in source files."""
def update_version_files(semver: str, calver_date: str) -> list[Path]:
"""Update version strings in source files.

Returns the exact set of files that were written so the caller can stage
precisely what changed. A newly-bumped file that is written but not
returned here would be silently dropped from the release commit (#68783).
"""
modified: list[Path] = []

# Update __init__.py
content = VERSION_FILE.read_text(encoding="utf-8")
content = re.sub(
Expand All @@ -2180,6 +2187,7 @@ def update_version_files(semver: str, calver_date: str):
content,
)
VERSION_FILE.write_text(content, encoding="utf-8")
modified.append(VERSION_FILE)

# Update pyproject.toml
pyproject = PYPROJECT_FILE.read_text(encoding="utf-8")
Expand All @@ -2190,6 +2198,7 @@ def update_version_files(semver: str, calver_date: str):
flags=re.MULTILINE,
)
PYPROJECT_FILE.write_text(pyproject, encoding="utf-8")
modified.append(PYPROJECT_FILE)

# Keep the desktop Electron app's package.json version in lockstep with the
# Python package version. The desktop About panel reads the live Hermes
Expand All @@ -2205,6 +2214,9 @@ def update_version_files(semver: str, calver_date: str):
count=1,
)
desktop_pkg.write_text(pkg_text, encoding="utf-8")
modified.append(desktop_pkg)

return modified


def resolve_author(name: str, email: str) -> str:
Expand Down Expand Up @@ -2540,11 +2552,13 @@ def main():

# Update version files
if args.bump:
update_version_files(new_version, calver_date)
written = update_version_files(new_version, calver_date)
print(f" ✓ Updated version files to v{new_version} ({calver_date})")

# Commit version bump
add_files = [str(VERSION_FILE), str(PYPROJECT_FILE)]
# Commit version bump. Stage exactly the files update_version_files
# wrote, so a newly-bumped file (e.g. the desktop package.json) can't
# drift out of the release commit (#68783).
add_files = [str(p) for p in written]
add_result = git_result("add", *add_files)
if add_result.returncode != 0:
print(f" ✗ Failed to stage version files: {add_result.stderr.strip()}")
Expand Down
72 changes: 72 additions & 0 deletions tests/scripts/test_release_version_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Tests for ``release.update_version_files`` write/return contract.

The release commit stages exactly the files ``update_version_files`` reports
back. A file that gets written but not returned is silently dropped from the
commit — which is how the desktop ``package.json`` bump went stale for several
releases (#68783). These tests pin the contract that every file the function
writes is also returned.
"""

import json
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPTS_DIR = REPO_ROOT / "scripts"

sys.path.insert(0, str(SCRIPTS_DIR))

import release # noqa: E402


def _prepare_tree(monkeypatch, root: Path, *, with_desktop: bool) -> None:
"""Pin release's module-level path constants at a throwaway temp tree so a
bump never scribbles on the real repo files."""
(root / "pyproject.toml").write_text(
'[project]\nname = "hermes-agent"\nversion = "0.13.0"\n', encoding="utf-8"
)
version_dir = root / "hermes_cli"
version_dir.mkdir()
(version_dir / "__init__.py").write_text(
'__version__ = "0.13.0"\n__release_date__ = "2026-05-14"\n',
encoding="utf-8",
)
monkeypatch.setattr(release, "VERSION_FILE", version_dir / "__init__.py")
monkeypatch.setattr(release, "PYPROJECT_FILE", root / "pyproject.toml")
monkeypatch.setattr(release, "REPO_ROOT", root)

if with_desktop:
pkg_dir = root / "apps" / "desktop"
pkg_dir.mkdir(parents=True)
(pkg_dir / "package.json").write_text(
json.dumps({"name": "hermes", "version": "0.13.0"}, indent=2) + "\n",
encoding="utf-8",
)


def test_returns_desktop_package_when_present(monkeypatch, tmp_path):
"""When the desktop package.json exists it must be both bumped AND returned
so the release commit can stage it (#68783)."""
_prepare_tree(monkeypatch, tmp_path, with_desktop=True)

modified = release.update_version_files("0.14.0", "2026-05-21")

desktop_pkg = tmp_path / "apps" / "desktop" / "package.json"
assert desktop_pkg in modified, "desktop package.json written but not returned"
# The bump actually landed on disk.
assert json.loads(desktop_pkg.read_text())["version"] == "0.14.0"
# Core version files are always reported.
assert release.VERSION_FILE in modified
assert release.PYPROJECT_FILE in modified


def test_omits_desktop_package_when_absent(monkeypatch, tmp_path):
"""Older release branches predate the desktop app — no package.json means
it must not be reported (staging a missing path would abort the release)."""
_prepare_tree(monkeypatch, tmp_path, with_desktop=False)

modified = release.update_version_files("0.14.0", "2026-05-21")

assert all("package.json" not in str(p) for p in modified)
assert release.VERSION_FILE in modified
assert release.PYPROJECT_FILE in modified
Loading