What changed, and why it matters
This commit removes a hard-coded version number from the SeedSigner software and instead reads the version from a generated file or git metadata. It also adds a debug print statement that exposes the device's hostname setting to standard output. There is no direct security vulnerability in the diff itself, but the change makes version reporting depend on build-time files and local git state, and it leaks an internal configuration value in logs or console output.
Remove the debug `print(f"{Settings.HOSTNAME=}")` line before release to avoid leaking internal configuration. Verify that the SeedSigner OS build process always generates a valid `version.json` with both `version` and `last_src_edit` fields, because missing or malformed files now cause runtime exceptions. Update or replace the commented-out tests to cover the new exception-raising behavior and the SeedSigner OS path.
Security signals we found
Hard-coded version constant removed, changing fallback behavior from a static value to exceptions when version metadata is unavailable
New runtime dependency on `version.json` generated by the SeedSigner OS build process
Debug print statement exposes `Settings.HOSTNAME` to stdout, which may appear in logs or captured output
Local development path now raises exceptions instead of falling back to a known version when git metadata is missing
Tests for fallback behavior were commented out rather than updated
Evidence from the diff
The patch deletes the Version.VERSION = "0.8.6" constant and rewrites Version.get_version() to derive the version from version.json when running on SeedSigner OS, or from .git/HEAD / tags in local development. It also rewrites Version.get_last_src_edit() to read from version.json on SeedSigner OS, otherwise scanning src/ for newest file mtime. A debug print(f"{Settings.HOSTNAME=}") was added. Tests that relied on the hard-coded fallback were commented out. The change is a build/dev-ops refactor with a minor information-disclosure side effect from the print statement.
Changed components
src/seedsigner/helpers/version.pytests/test_version.pyInspect captured patch +68 / −41
diff --git a/src/seedsigner/helpers/version.py b/src/seedsigner/helpers/version.py
index 86dfeee..76be03a 100644
--- a/src/seedsigner/helpers/version.py
+++ b/src/seedsigner/helpers/version.py
@@ -3,6 +3,8 @@ import logging
import os
from datetime import datetime
+from seedsigner.models.settings import Settings
+
logger = logging.getLogger(__name__)
@@ -17,9 +19,6 @@ class Version:
Internal utility functions are separated out as class methods for easier mocking in tests.
"""
-
- VERSION = "0.8.6"
-
VERSION_FILENAME = "version.json"
@@ -98,6 +97,31 @@ class Version:
hard-coded VERSION constant.
"""
name = None
+
+ def _prefix_version_name(version: str) -> str:
+ """
+ Ensure version strings are prefixed with 'v' if they look like semantic
+ versions. Only checks that the first part is numeric in order to be compatible
+ with minor versions like an "rc1".
+ """
+ if not version.startswith("v") and version.count(".") >= 1 and version.split(".")[0].isnumeric():
+ return f"v{version}"
+ return version
+
+ print(f"{Settings.HOSTNAME=}")
+
+ if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
+ # The SeedSigner OS build process generates the version.json file for the tag,
+ # branch, or commit hash the image is targeting.
+ try:
+ name = cls._get_version_file()["version"]
+ except Exception:
+ # Shouldn't be possible. Raise an exception to alert testers before this
+ # image goes out.
+ raise Exception("Could not read the version from the version.json file.")
+ return _prefix_version_name(name)
+
+ # In local dev we dynamically read from the .git/HEAD file
branch_name, commit_hash = cls._read_HEAD_file()
if branch_name:
name = branch_name
@@ -108,18 +132,11 @@ class Version:
name = matching_tag
else:
name = commit_hash[:7] # short commit hash
-
- if name is None:
- # Try reading from version.json file
- version_file_data = cls._get_version_file()
- if version_file_data:
- name = version_file_data.get("version")
if name is None:
- # Fallback to hard-coded version
- name = f"v{cls.VERSION}"
+ raise Exception("Could not determine version from git info.")
- return name
+ return _prefix_version_name(name)
@classmethod
@@ -127,6 +144,17 @@ class Version:
"""
Recursively scan the src/ directory for the most recent python file edit time.
"""
+ if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
+ # The SeedSigner OS build process generates the version.json file which will
+ # already contain the last edit time.
+ try:
+ last_src_edit_str = cls._get_version_file()["last_src_edit"]
+ return datetime.fromisoformat(last_src_edit_str)
+ except Exception as e:
+ # Shouldn't be possible. Raise an exception to alert testers before this
+ # image goes out.
+ raise Exception("Could not read the last_src_edit from the version.json file.")
+
try:
path = os.path.dirname(os.path.abspath(__file__))
@@ -148,11 +176,8 @@ class Version:
last_modified = max(file_mtime, last_modified)
if num_files == 0:
- # Fallback to reading from the version file
- version_file_data = cls._get_version_file()
- if version_file_data:
- last_src_edit_str = version_file_data.get("last_src_edit")
- return datetime.fromisoformat(last_src_edit_str)
+ # Shouldn't be possible
+ raise Exception("No python source files found in src/ directory")
return datetime.fromtimestamp(last_modified)
@@ -167,6 +192,8 @@ class Version:
if __name__ == "__main__":
"""
CLI to extract the current version and last edit time and write to `src/seedsigner/version.json`.
+
+ Used by the SeedSigner OS build process to generate the version.json file.
"""
version_info = dict(version=Version.get_version())
last_edit_dt = Version.get_last_src_edit()
diff --git a/tests/test_version.py b/tests/test_version.py
index cc99ef3..3819c6a 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -11,33 +11,33 @@ from seedsigner.helpers.version import Version
class TestVersion(BaseTest):
- def test_version_with_no_git_head(self):
- """
- If there is no .git/HEAD file, the hard-coded VERSION constant should be returned.
- """
- # mock out the os.path.exists call to always return False
- with mock.patch("os.path.exists", return_value=False):
- version = Version.get_version()
- assert version == f"v{Version.VERSION}"
+ # def test_version_with_no_git_head(self):
+ # """
+ # If there is no .git/HEAD file, the hard-coded VERSION constant should be returned.
+ # """
+ # # mock out the os.path.exists call to always return False
+ # with mock.patch("os.path.exists", return_value=False):
+ # version = Version.get_version()
+ # assert version == f"v{Version.VERSION}"
- def test_version_with_actual_git_head(self):
- """
- If the .git dir exists on our actual filesystem right now, we should get a version
- based on its contents.
+ # def test_version_with_actual_git_head(self):
+ # """
+ # If the .git dir exists on our actual filesystem right now, we should get a version
+ # based on its contents.
- NOTE: This test is potentially fragile as it depends on the test runner system's actual
- git state. The get_version() call should be able to handle all possible git states, but
- this does create the possibility of external variability.
- """
- fake_hardcoded_version = "fake.version.123"
- with mock.patch.object(Version, 'VERSION', fake_hardcoded_version):
- git_dot_dir = Version._get_dot_git_dir()
- if os.path.exists(git_dot_dir):
- assert Version.get_version() != f"v{fake_hardcoded_version}"
- else:
- # If there's no .git dir, mark this test as skipped
- pytest.skip(f"No .git dir found at {git_dot_dir}, skipping test.")
+ # NOTE: This test is potentially fragile as it depends on the test runner system's actual
+ # git state. The get_version() call should be able to handle all possible git states, but
+ # this does create the possibility of external variability.
+ # """
+ # fake_hardcoded_version = "fake.version.123"
+ # with mock.patch.object(Version, 'VERSION', fake_hardcoded_version):
+ # git_dot_dir = Version._get_dot_git_dir()
+ # if os.path.exists(git_dot_dir):
+ # assert Version.get_version() != f"v{fake_hardcoded_version}"
+ # else:
+ # # If there's no .git dir, mark this test as skipped
+ # pytest.skip(f"No .git dir found at {git_dot_dir}, skipping test.")
def test_version_with_mocked_git_head(self):
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.