What changed, and why it matters
This commit refactors how SeedSigner determines and records its software version. It removes an environment-variable-based build path and instead reads version info directly from git. The change is mostly cleanup and testing improvements. There is no obvious security vulnerability, but the code still uses shell commands (os.popen) and parses git output, which can be risky if inputs are ever attacker-controlled. Here, the inputs come from the local git state or trusted environment variables, so the practical risk is low.
No immediate action required. As a defensive measure, reviewers may want to confirm that all git-derived strings are treated as display data only and never executed or used in security-critical decisions. Consider replacing os.popen with subprocess.run(shell=False) in future hardening work.
Security signals we found
Use of os.popen with shell command strings built from local git state
Parsing of .git/HEAD, .git/refs, and .git/config files
Environment-variable-driven code path selection
Version metadata used in UI and potentially reproducible builds
Evidence from the diff
The patch simplifies version detection by eliminating the SEEDSIGNER_VERSION_NAME environment variable as the builder identifier and replacing it with SEEDSIGNER_OS_BUILDER. Version metadata (name, fork, short commit hash, timestamp) is now gathered via git shell calls and .git/ file parsing, with fallbacks for local dev and GitHub Actions CI. The version.json schema changes the commit_hash key to short_commit_hash. Tests are updated accordingly, and the screenshot generator’s version override now uses keyword arguments. The code still invokes os.popen with unsanitized git commands and parses remote URLs/config files, but these are local/trusted sources in this context.
Changed components
src/seedsigner/helpers/version.pytools/write_versionfile.pytests/test_version.pytests/screenshot_generator/generator.pyInspect captured patch +370 / −292
diff --git a/src/seedsigner/helpers/version.py b/src/seedsigner/helpers/version.py
index 7590f9d..b228dad 100644
--- a/src/seedsigner/helpers/version.py
+++ b/src/seedsigner/helpers/version.py
@@ -40,52 +40,70 @@ class Version(Singleton):
around in the main code) and so that the version data is only determined once per
runtime.
- On SeedSigner OS:
- * Version data is written to `src/seedsigner/version.json` during the build
- process via tools/write_versionfile.py.
- * version_name: copied from the command used to build the SeedSigner OS image:
- --app-branch (the target git branch OR tag) or
- --app-commit-id (target commit hash)
- * version_fork: the git repo owner/fork name (e.g. "SeedSigner") targeted by the
- build process.
- * version_timestamp: the last git commit time for the target branch, tag, or
- commit hash.
- * commit_hash: the short commit hash for the specified build target.
+ SeedSigner OS lifecycle:
+ * The SeedSigner OS build process runs the tools/write_versionfile.py script to
+ generate version.json.
+ * The version.json file is included in the SeedSigner OS image.
+
+ The version data is fetched differently depending on the environment:
+
+ In SeedSigner OS:
+ * Version data is read exclusively from version.json at runtime.
+
+ In the SeedSigner OS builder:
+ - Note: the build process relies on `git` being installed so we leverage it here.
+ * version_name:
+ * read dynamically from `git` shell calls to retrieve, in order:
+ * Current git branch name
+ * Current git tag name
+ * Current git short commit hash
+ * version_fork:
+ * the git repo owner parsed out of the remote url.
+ * https://github.com/SeedSigner/seedsigner.git -> "SeedSigner"
+ * Read dynamically from shell `git` call to check the remote "origin" URL.
+ * short_commit_hash:
+ * the commit hash for the current branch / tag / detached HEAD.
+ * Read dynamically from shell `git` call.
+ * version_timestamp:
+ * the last git commit time for the current branch / tag / detached HEAD.
+ * Read dynamically from shell `git` call.
In local dev:
- * version_name: read dynamically from a few possible sources. In order:
- * SEEDSIGNER_VERSION_NAME env var, if available.
- * Shell `git` calls (e.g. `git branch --show-current`).
- * Directly parsing the .git/HEAD file and possibly .git/refs/tags.
- * Note: we avoid reading from the version.json file as it may be out of date
- and could lead to confusion.
- * version_fork: read dynamically from:
- * Shell `git` call to check the remote "origin" URL.
- * Parse the .git/config for the remote "origin" URL.
+ - Similar process as for the SeedSigner OS builder, but with additional fallbacks and
+ a different method for determining version_timestamp.
+ - If a local version.json is present, it will be ignored; it may be out of date and
+ could lead to confusion.
+ * version_name:
+ * `git` shell calls + directly parsing the .git/HEAD file and .git/refs/tags
+ when necessary.
+ * version_fork:
+ * `git` shell call + parse the .git/config for the remote "origin" URL.
+ * commit_hash:
+ * Shell `git` call + parse the .git/HEAD file and
+ .git/refs/heads/<branch_name> when necessary.
* version_timestamp: determined by scanning the src/ directory for the most
recently modified python file.
- * commit_hash: read dynamically from:
- * Shell `git` call.
- * Parse the .git/HEAD file and possibly .git/refs/heads/<branch_name>.
In Github Actions CI:
- * version_name: read from GITHUB_REF_NAME or GITHUB_SHA env vars.
- * version_fork: TODO
- * version_timestamp: TODO
- * commit_hash: TODO
+ * version_name: read from GITHUB_REF_NAME env var.
+ * version_fork: read from GITHUB_REPOSITORY_OWNER env var.
+ * version_timestamp: Shell `git` call to get the last commit time for the current
+ branch/tag/commit.
+ * commit_hash: read from GITHUB_SHA env var.
This class defines the limited methods that are meant to be publicly accessible
across the SeedSigner codebase.
- The misc utility functions in `VersionUtils` were explicitly isolated because they
- should NOT be used elsewhere in the codebase.
+ The utility functions in `VersionUtils` were explicitly isolated because they should
+ NOT be used elsewhere in the codebase.
TODO: Should `VersionUtils` be an internal class within `Version` to further signal
that it is not to be used externally?
+ TODO: Also pull git log history and display it in its own View?
"""
_version_name: str = None
_version_fork: str = None
- _version_timestamp: datetime = None
_short_commit_hash: str = None
+ _version_timestamp: datetime = None
@classmethod
@@ -115,26 +133,30 @@ class Version(Singleton):
@classmethod
- def get_version_timestamp(cls) -> datetime | None:
- return cls.get_instance()._version_timestamp
+ def get_short_commit_hash(cls) -> str | None:
+ return cls.get_instance()._short_commit_hash
@classmethod
- def get_short_commit_hash(cls) -> str | None:
- return cls.get_instance()._short_commit_hash
+ def get_version_timestamp(cls) -> datetime | None:
+ return cls.get_instance()._version_timestamp
@classmethod
@not_allowed_in_seedsigner_os
- def override_data(cls, version_name, version_fork, version_timestamp, short_commit_hash):
+ def override_data(cls, **kwargs):
"""
Only used by the screenshot generator.
"""
instance = cls.get_instance()
- instance._version_name = version_name
- instance._version_fork = version_fork
- instance._version_timestamp = version_timestamp
- instance._short_commit_hash = short_commit_hash
+ if VersionUtils.VERSIONFILE_ATTR__NAME in kwargs:
+ instance._version_name = kwargs[VersionUtils.VERSIONFILE_ATTR__NAME]
+ if VersionUtils.VERSIONFILE_ATTR__FORK in kwargs:
+ instance._version_fork = kwargs[VersionUtils.VERSIONFILE_ATTR__FORK]
+ if VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH in kwargs:
+ instance._short_commit_hash = kwargs[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
+ if VersionUtils.VERSIONFILE_ATTR__TIMESTAMP in kwargs:
+ instance._version_timestamp = kwargs[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP]
@classmethod
@@ -143,11 +165,12 @@ class Version(Singleton):
return {
VersionUtils.VERSIONFILE_ATTR__NAME: instance._version_name,
VersionUtils.VERSIONFILE_ATTR__FORK: instance._version_fork,
+ VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: instance._short_commit_hash,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: instance._version_timestamp.isoformat() if instance._version_timestamp else None,
- VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH: instance._short_commit_hash,
}
+
class VersionUtils:
""" *********************************************************************************
Not meant to be used elsewhere in the SeedSigner codebase (aside from
@@ -168,7 +191,7 @@ class VersionUtils:
* One external http GET to github to get the most recent release tag.
********************************************************************************* """
- ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME = "SEEDSIGNER_VERSION_NAME"
+ ENV_VAR__IS_SEEDSIGNER_OS_BUILDER = "SEEDSIGNER_OS_BUILDER"
ENV_VAR__GITHUB_ACTIONS__IS_CI = "CI"
ENV_VAR__GITHUB_ACTIONS__REF_NAME = "GITHUB_REF_NAME"
ENV_VAR__GITHUB_ACTIONS__SHA = "GITHUB_SHA"
@@ -177,8 +200,8 @@ class VersionUtils:
VERSIONFILE__FILENAME = "version.json"
VERSIONFILE_ATTR__NAME = "name"
VERSIONFILE_ATTR__FORK = "fork"
+ VERSIONFILE_ATTR__SHORT_COMMIT_HASH = "short_commit_hash"
VERSIONFILE_ATTR__TIMESTAMP = "timestamp"
- VERSIONFILE_ATTR__COMMIT_HASH = "commit_hash"
""" *************************************************************************************
@@ -211,18 +234,10 @@ class VersionUtils:
else:
raise Exception("Could not determine version from Github Actions env vars.")
- elif VersionUtils._is_seedsigner_os_builder_env():
- # In the SeedSigner OS build environment, get the version name from env var.
- # Note: This get_version_name call will never fail because the env var that
- # provides the version name is what defines whether we're in the SeedSigner OS
- # builder in the first place.
- version_name = VersionUtils._get_version_name_from_seedsigner_os_builder_env_var()
- return VersionUtils._prefix_version_name(version_name)
-
else:
- # In local dev, we try the following methods in order:
+ # In the SeedSigner OS builder we know that we'll have the `git` shell
+ # commands available. We add extra fallbacks for local dev.
for get_version_name_method in [
- VersionUtils._get_version_name_from_seedsigner_os_builder_env_var,
VersionUtils._get_version_name_from_git_shell,
VersionUtils._get_version_name_from_git_HEAD,
]:
@@ -252,12 +267,9 @@ class VersionUtils:
# In Github Actions CI, try to get the version name from env vars
return VersionUtils._get_version_fork_from_github_actions_env_vars()
- elif VersionUtils._is_seedsigner_os_builder_env():
- # In the SeedSigner OS build environment `git` shell call should be available
- return VersionUtils._get_version_fork_from_git_shell()
-
else:
- # In local dev we try to access the current git state via:
+ # In the SeedSigner OS builder we know that we'll have the `git` shell
+ # commands available. We add extra fallbacks for local dev.
for get_version_fork_method in [
VersionUtils._get_version_fork_from_git_shell,
VersionUtils._get_version_fork_from_git_config,
@@ -268,37 +280,6 @@ class VersionUtils:
return None
- @classmethod
- def get_version_timestamp(cls) -> datetime:
- """
- Returns a datetime object representing the last edit time of the source code via
- the most recent git commit time (SeedSigner OS, as written in version.json) or
- the most recently modified python source file (local dev).
- """
- if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
- # The SeedSigner OS build process generates the version.json file which will
- # already contain the last edit time.
- version_timestamp = VersionUtils._get_version_timestamp_from_version_file()
- if version_timestamp is None:
- # Shouldn't be possible. Raise an exception to alert testers before this
- # image goes out.
- raise Exception("Could not read the version timestamp from the version.json file.")
- return datetime.fromisoformat(version_timestamp)
-
- elif VersionUtils.is_github_actions_ci():
- # In Github Actions CI `git` shell call should be available
- return VersionUtils._get_version_timestamp_from_git_shell()
-
- elif VersionUtils._is_seedsigner_os_builder_env():
- # In the SeedSigner OS build environment `git` shell call should be available
- return VersionUtils._get_version_timestamp_from_git_shell()
-
- else:
- # In local dev we change our approach and instead use the last modified time
- # of the source python files.
- return VersionUtils._get_last_modified_timestamp_from_src_files()
-
-
@classmethod
def get_short_commit_hash(cls) -> str | None:
"""
@@ -315,12 +296,9 @@ class VersionUtils:
# In Github Actions CI the "SHA" env var should always be available
full_commit_hash = VersionUtils._get_full_commit_hash_from_github_actions_env_vars()
- elif VersionUtils._is_seedsigner_os_builder_env():
- # In the SeedSigner OS build environment `git` shell call should be available
- full_commit_hash = VersionUtils._get_full_commit_hash_from_git_shell()
-
else:
- # In local dev we try to access the current git state via:
+ # In the SeedSigner OS builder we know that we'll have the `git` shell
+ # commands available. We add extra fallbacks for local dev.
for get_full_commit_hash_method in [
VersionUtils._get_full_commit_hash_from_git_shell,
VersionUtils._get_full_commit_hash_from_git_HEAD,
@@ -341,6 +319,44 @@ class VersionUtils:
return full_commit_hash[:7]
+ @classmethod
+ def get_version_timestamp(cls) -> datetime:
+ """
+ Returns a datetime object representing the last edit time of the source code via
+ the most recent git commit time (SeedSigner OS, as written in version.json) or
+ the most recently modified python source file (local dev).
+ """
+ if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
+ # The SeedSigner OS build process generates the version.json file which will
+ # already contain the last edit time.
+ version_timestamp = VersionUtils._get_version_timestamp_from_version_file()
+ if version_timestamp is None:
+ # Shouldn't be possible. Raise an exception to alert testers before this
+ # image goes out.
+ raise Exception("Could not read the version timestamp from the version.json file.")
+ return datetime.fromisoformat(version_timestamp)
+
+ elif VersionUtils.is_github_actions_ci():
+ # In Github Actions CI `git` shell call should be available
+ return VersionUtils._get_version_timestamp_from_git_shell()
+
+ elif VersionUtils._is_seedsigner_os_builder_env():
+ # In the SeedSigner OS builder we know that we'll have the `git` shell
+ # commands available.
+ # Note: This is the only piece of version data that behaves differently
+ # between the SeedSigner OS builder and local dev.
+ return VersionUtils._get_version_timestamp_from_git_shell()
+
+ else:
+ # In local dev we change our approach and instead use the last modified time
+ # of the source python files.
+ return VersionUtils._get_last_modified_timestamp_from_src_files()
+
+
+
+ """ *************************************************************************************
+ Misc internal utility functions.
+ ************************************************************************************* """
@classmethod
def _prefix_version_name(cls, version_name: str) -> str:
"""
@@ -353,6 +369,7 @@ class VersionUtils:
return version_name
+
""" *************************************************************************************
Reading data from the version.json file.
************************************************************************************* """
@@ -399,23 +416,23 @@ class VersionUtils:
@classmethod
- def _get_version_timestamp_from_version_file(cls) -> str | None:
+ def _get_short_commit_hash_from_version_file(cls) -> str | None:
"""
- Attempts to read the version.json and return the version timestamp.
+ Attempts to read the version.json and return the version commit hash.
"""
version_data = cls._read_version_file()
if version_data:
- return version_data.get(cls.VERSIONFILE_ATTR__TIMESTAMP)
+ return version_data.get(cls.VERSIONFILE_ATTR__SHORT_COMMIT_HASH)
@classmethod
- def _get_short_commit_hash_from_version_file(cls) -> str | None:
+ def _get_version_timestamp_from_version_file(cls) -> str | None:
"""
- Attempts to read the version.json and return the version commit hash.
+ Attempts to read the version.json and return the version timestamp.
"""
version_data = cls._read_version_file()
if version_data:
- return version_data.get(cls.VERSIONFILE_ATTR__COMMIT_HASH)
+ return version_data.get(cls.VERSIONFILE_ATTR__TIMESTAMP)
@@ -425,20 +442,10 @@ class VersionUtils:
@classmethod
def _is_seedsigner_os_builder_env(cls) -> bool:
"""
- Simple check to see if we're running in the SeedSigner OS build environment.
+ The `ENV_VAR__IS_SEEDSIGNER_OS_BUILDER` env var is set during the SeedSigner OS
+ build process.
"""
- return os.getenv(cls.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME) is not None
-
-
- @classmethod
- def _get_version_name_from_seedsigner_os_builder_env_var(cls) -> str | None:
- """
- Primarily used during the SeedSigner OS build process to set the version name via env var.
-
- This env var can also be set manually in local dev when needed.
- e.g. SEEDSIGNER_VERSION_NAME=some_name python main.py
- """
- return os.getenv(cls.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME)
+ return os.getenv(cls.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER) is not None
@@ -478,14 +485,14 @@ class VersionUtils:
************************************************************************************* """
@classmethod
@not_allowed_in_seedsigner_os
- def _get_version_name_from_git_shell_branch(cls) -> str | None:
+ def _get_branch_name_from_git_shell(cls) -> str | None:
branch_name = os.popen("git branch --show-current 2> /dev/null").read()
return branch_name.strip() if branch_name else None
@classmethod
@not_allowed_in_seedsigner_os
- def _get_version_name_from_git_shell_tag(cls) -> str | None:
+ def _get_tag_name_from_git_shell(cls) -> str | None:
# Only return a value if the current commit exactly corresponds with a tag.
# (`--points-at` defaults to the current HEAD)
tag_name = os.popen(f"git tag --points-at 2> /dev/null").read()
@@ -496,22 +503,34 @@ class VersionUtils:
@not_allowed_in_seedsigner_os
def _get_full_commit_hash_from_git_shell(cls) -> str | None:
"""
- Attempts to get the current git commit hash via shell `git` commands.
+ Attempts to get the current full commit hash via shell `git` commands.
"""
commit_hash = os.popen("git rev-parse HEAD").read()
return commit_hash.strip() if commit_hash else None
+ @classmethod
+ @not_allowed_in_seedsigner_os
+ def _get_short_commit_hash_from_git_shell(cls) -> str | None:
+ """
+ Attempts to get the current short commit hash via shell `git` commands.
+ """
+ commit_hash = cls._get_full_commit_hash_from_git_shell()
+ return commit_hash[:7] if commit_hash else None
+
+
@classmethod
@not_allowed_in_seedsigner_os
def _get_version_name_from_git_shell(cls) -> str | None:
"""
Attempts to get the version name via shell `git` commands.
+
+ Note: If we have to fall back to the commit hash, we use the short version.
"""
return (
- cls._get_version_name_from_git_shell_branch() or
- cls._get_version_name_from_git_shell_tag() or
- cls._get_full_commit_hash_from_git_shell()
+ cls._get_branch_name_from_git_shell() or
+ cls._get_tag_name_from_git_shell() or
+ cls._get_short_commit_hash_from_git_shell()
)
@@ -747,7 +766,7 @@ class VersionUtils:
# Shouldn't be possible
raise Exception("No python source files found in src/ directory")
- return datetime.fromtimestamp(last_modified)
+ return datetime.fromtimestamp(last_modified).astimezone(timezone.utc).replace(tzinfo=None)
except Exception as e:
# Catch and log any unexpected errors but this isn't a mission-critical
diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py
index bd088eb..28dca7b 100644
--- a/tests/screenshot_generator/generator.py
+++ b/tests/screenshot_generator/generator.py
@@ -197,16 +197,19 @@ def generate_screenshots(locale):
value=SettingsConstants.OPTION__ENABLED
)
- # Initialize the Version data to the most recent release
- (version_name, version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
- if not version_name or not version_timestamp:
- raise Exception("Could not fetch latest release version from GitHub")
- Version.override_data(
- version_name=version_name,
- version_fork="SeedSigner", # main repo; screenshot should hide fork and commit hash
- version_timestamp=version_timestamp,
- short_commit_hash="abcd1234" # dummy value should be ignored
- )
+ def reset_version_most_recent_release():
+ # Initialize the Version data to the most recent release
+ (version_name, version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
+ if not version_name or not version_timestamp:
+ raise Exception("Could not fetch latest release version from GitHub")
+ new_values = {
+ VersionUtils.VERSIONFILE_ATTR__NAME: version_name,
+ VersionUtils.VERSIONFILE_ATTR__FORK: "SeedSigner", # main repo; screenshot should hide fork and commit hash
+ VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: version_timestamp,
+ VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: "abcd1234" # dummy value should be ignored
+ }
+ Version.override_data(**new_values)
+ reset_version_most_recent_release()
# Automatically populate all Settings options Views
settings_views_list = []
@@ -314,14 +317,19 @@ def generate_screenshots(locale):
def reset_version_to_local_git_state_cb():
# Normally, directly manipulating the singleton's internal instance is not
- # allowed, but the screnshot generator is an atypical use case.
+ # allowed, but the screnshot generator is a special exception.
Version._instance = None
+ def reset_version_to_most_recent_release_cb():
+ reset_version_most_recent_release()
+
+
screenshot_sections = {
"Main Menu Views": [
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True)),
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"),
+ ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), run_before=reset_version_to_local_git_state_cb, screenshot_name="OpeningSplashView_current_git_state"),
ScreenshotConfig(MainMenuView),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
@@ -447,7 +455,7 @@ def generate_screenshots(locale):
"Settings Views": settings_views_list + [
ScreenshotConfig(settings_views.IOTestView),
ScreenshotConfig(settings_views.DonateView),
- ScreenshotConfig(settings_views.VersionView),
+ ScreenshotConfig(settings_views.VersionView, run_before=reset_version_to_most_recent_release_cb),
ScreenshotConfig(settings_views.VersionView, run_before=reset_version_to_local_git_state_cb, screenshot_name="VersionView_current_git_state"),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
diff --git a/tests/test_version.py b/tests/test_version.py
index 354a571..3411942 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -29,8 +29,8 @@ TEST__FULL_COMMIT_HASH = "c5efda306c60877191013a6093d92cd0bfcccec8"
TEST__VERSION_DICT = {
VersionUtils.VERSIONFILE_ATTR__NAME: VersionUtils._prefix_version_name(TEST__VERSION_NAME),
VersionUtils.VERSIONFILE_ATTR__FORK: TEST__VERSION_FORK,
+ VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: TEST__VERSION_TIMESTAMP.isoformat(),
- VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
}
# Mimic result of reading from version.json
@@ -68,7 +68,7 @@ class VersionBaseTest(BaseTest):
assumptions when running locally and when actually running in CI. Any test that
needs to simulate being in CI can override this env var as needed.
"""
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
yield
@@ -193,7 +193,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return the fallback warning note as the version name.
- with mock.patch("builtins.open", side_effect=FileNotFoundError):
+ with patch("builtins.open", side_effect=FileNotFoundError):
result = VersionUtils.get_version_name()
assert "not detected" in result
@@ -206,7 +206,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
- with mock.patch("builtins.open", side_effect=FileNotFoundError):
+ with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_version_fork() is None
@@ -218,7 +218,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
- with mock.patch("builtins.open", side_effect=FileNotFoundError):
+ with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_short_commit_hash() is None
@@ -238,21 +238,20 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
- assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Expect errors if keys are missing from version.json
- with mock.patch("builtins.open", mock.mock_open(read_data="{'some_key':'some_value'}")):
+ with patch("builtins.open", mock.mock_open(read_data="{'some_key':'some_value'}")):
with pytest.raises(Exception):
VersionUtils.get_version_name()
# This time it's the timestamp that's missing
- with mock.patch("builtins.open", mock.mock_open(read_data=str({VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME}).replace("'", '"'))):
+ with patch("builtins.open", mock.mock_open(read_data=str({VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME}).replace("'", '"'))):
with pytest.raises(Exception):
VersionUtils.get_version_timestamp()
-
def test__read_version_file(self):
"""
Low-level test for reading the version.json file.
@@ -265,8 +264,8 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
assert version_data is not None
assert version_data[VersionUtils.VERSIONFILE_ATTR__NAME] == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert version_data[VersionUtils.VERSIONFILE_ATTR__FORK] == TEST__VERSION_FORK
+ assert version_data[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == TEST__SHORT_COMMIT_HASH
assert version_data[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == TEST__VERSION_TIMESTAMP.isoformat()
- assert version_data[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH] == TEST__SHORT_COMMIT_HASH
def test__read_version_file__missing(self):
@@ -277,23 +276,14 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
# The upstream dependent calls should also return None
assert VersionUtils._get_version_name_from_version_file() is None
assert VersionUtils._get_version_fork_from_version_file() is None
- assert VersionUtils._get_version_timestamp_from_version_file() is None
assert VersionUtils._get_short_commit_hash_from_version_file() is None
+ assert VersionUtils._get_version_timestamp_from_version_file() is None
# Gracefully handle other unexpected exceptions
with patch("builtins.open", side_effect=Exception("Unexpected error")):
assert VersionUtils._read_version_file() is None
- def test__get_version_name_from_seedsigner_os_builder_env_var(self):
- assert os.environ.get(VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME) is None
- assert VersionUtils._get_version_name_from_seedsigner_os_builder_env_var() is None
-
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
- result = VersionUtils._get_version_name_from_seedsigner_os_builder_env_var()
- assert result == TEST__VERSION_NAME
-
-
class TestVersionUtils_GithubActions(VersionBaseTest):
def test_github_actions_env_vars(self):
@@ -305,9 +295,9 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
pulled from the appropriate env vars.
"""
# CI uses some limited `git` shell calls; mock out the associated calls.
- with mock.patch("seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell", return_value=TEST__VERSION_TIMESTAMP):
+ with patch("seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell", return_value=TEST__VERSION_TIMESTAMP):
# When running CI on a branch
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_BRANCH,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
@@ -319,7 +309,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# When running CI on a semantic tag
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__SEMANTIC_TAG,
}):
@@ -327,7 +317,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils.get_version_name() == f"v{TEST__SEMANTIC_TAG}"
# When running CI on a generic tag
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_TAG,
}):
@@ -337,7 +327,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
# When running CI on a commit (detached HEAD) with no REF_NAME, the
# version_name should be the short commit hash.
# TODO: I don't think this scenario ever happens.
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
@@ -348,7 +338,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
# raise error.
# Note: This scenario definitely would never happen. Just trying to get to
# 100% test coverage.
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
@@ -365,13 +355,13 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
Note: Have to mock a value for ALL scenario variations here because this test will
actually run in Github Actions so the env var will already be present!
"""
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
assert VersionUtils.is_github_actions_ci() is True
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
assert VersionUtils.is_github_actions_ci() is False
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
assert VersionUtils.is_github_actions_ci() is False
@@ -381,7 +371,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
env vars when set.
"""
# Need to signal that we're in a GitHub Actions CI environment
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
@@ -389,12 +379,12 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils._get_version_name_from_github_actions_env_vars() is None
# REF_NAME should be passed straight through
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_NAME}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_NAME}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == TEST__VERSION_NAME
# Unlikely scenario: no REF_NAME but SHA is set; should return short commit hash
- with mock.patch.dict(os.environ, {
+ with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
}):
@@ -410,30 +400,40 @@ class TestVersionUtils_SeedSignerOSBuilder(VersionBaseTest):
since all the downstream helper methods are tested in detail elsewhere.
"""
# Simulate running in the SeedSigner OS build environment
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
# SeedSigner OS builder uses some limited `git` shell calls; mock out the
# associated calls.
- with mock.patch.multiple(
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
- _get_version_timestamp_from_git_shell=Mock(return_value=TEST__VERSION_TIMESTAMP),
+ _get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_NAME),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
+ _get_version_timestamp_from_git_shell=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
- assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
- assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
- assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
- assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+ # Meanwhile, mock out the timestamp from source files and ensure it's NOT used
+ with patch.object(
+ VersionUtils,
+ "_get_last_modified_timestamp_from_src_files",
+ Mock(return_value=datetime.now())
+ ):
+ assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
+ assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
+ assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+
+ # Verify that the source file timestamp method was NOT called
+ VersionUtils._get_last_modified_timestamp_from_src_files.assert_not_called()
def test_is_seedsigner_os_builder_env(self):
"""
is_seedsigner_os_builder_env should return True only when the
- ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME env var is set.
+ ENV_VAR__IS_SEEDSIGNER_OS_BUILDER env var is set.
"""
- with mock.patch.dict(os.environ, {}, clear=True):
+ with patch.dict(os.environ, {}, clear=True):
assert VersionUtils._is_seedsigner_os_builder_env() is False
- with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
+ with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
assert VersionUtils._is_seedsigner_os_builder_env() is True
@@ -450,17 +450,29 @@ class TestVersionUtils_GitShell(VersionBaseTest):
Note that in local dev we use the last modified timestamp from the source files
rather than from git.
"""
- with mock.patch.multiple(
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
- _get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__SHORT_COMMIT_HASH),
+ _get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
- assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
- assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
- assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
- assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ # Mock out the git shell call that retrieves the timestamp.
+ with patch(
+ "seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
+ return_value=None
+ ) as mock_timestamp_from_git_shell:
+ # Verify that we're using the local dev code path
+ assert VersionUtils.is_github_actions_ci() is False
+ assert VersionUtils._is_seedsigner_os_builder_env() is False
+
+ assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
+ assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
+ assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+
+ # Verify that the git shell timestamp method was NOT called
+ mock_timestamp_from_git_shell.assert_not_called()
def test__get_version_name_from_git_shell(self):
@@ -474,34 +486,36 @@ class TestVersionUtils_GitShell(VersionBaseTest):
assert result is None
# If we're on a branch, should return the branch name
- with mock.patch.multiple(
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
- _get_version_name_from_git_shell_branch=Mock(return_value=TEST__VERSION_BRANCH),
- _get_version_name_from_git_shell_tag=Mock(return_value=TEST__VERSION_TAG),
+ _get_branch_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
+ _get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_BRANCH
# If we're on a tag, the detached HEAD state wipes out the branch name
- with mock.patch.multiple(
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
- _get_version_name_from_git_shell_branch=Mock(return_value=None),
- _get_version_name_from_git_shell_tag=Mock(return_value=TEST__VERSION_TAG),
+ _get_branch_name_from_git_shell=Mock(return_value=None),
+ _get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_TAG
- # Similarly, if we're detached at a specific commit hash
- with mock.patch.multiple(
+ # Similarly, if we're detached at a specific commit hash.
+ # Note: when falling back to the commit hash, we're expecting the SHORT commit
+ # hash.
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
- _get_version_name_from_git_shell_branch=Mock(return_value=None),
- _get_version_name_from_git_shell_tag=Mock(return_value=None),
+ _get_branch_name_from_git_shell=Mock(return_value=None),
+ _get_tag_name_from_git_shell=Mock(return_value=None),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
- assert result == TEST__FULL_COMMIT_HASH
+ assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_fork_from_git_shell(self, mock_popen: Mock):
@@ -509,12 +523,32 @@ class TestVersionUtils_GitShell(VersionBaseTest):
Test that _get_version_fork_from_git_shell returns the expected repo owner from
the remote url.
"""
- remote_url = "https://github.com/SeedSigner/seedsigner.git"
- expected_fork = "SeedSigner"
+ for owner in ["SeedSigner", "seedsigner", "some-user"]:
+ remote_url = f"https://github.com/{owner}/seedsigner.git"
+
+ mock_popen.return_value.read.return_value = remote_url
+ result = VersionUtils._get_version_fork_from_git_shell()
+ assert result == owner
- mock_popen.return_value.read.return_value = remote_url
- result = VersionUtils._get_version_fork_from_git_shell()
- assert result == expected_fork
+
+ def test__get_full_commit_hash_from_git_shell(self, mock_popen: Mock):
+ """
+ Test that _get_full_commit_hash_from_git_shell returns the expected full commit hash.
+ """
+ mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
+
+ result = VersionUtils._get_full_commit_hash_from_git_shell()
+ assert result == TEST__FULL_COMMIT_HASH
+
+
+ def test__get_short_commit_hash_from_git_shell(self, mock_popen: Mock):
+ """
+ Test that _get_short_commit_hash_from_git_shell returns the expected short commit hash.
+ """
+ mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
+
+ result = VersionUtils._get_short_commit_hash_from_git_shell()
+ assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_timestamp_from_git_shell(self, mock_popen: Mock):
@@ -540,16 +574,6 @@ class TestVersionUtils_GitShell(VersionBaseTest):
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
- def test__get_full_commit_hash_from_git_shell(self, mock_popen: Mock):
- """
- Test that _get_full_commit_hash_from_git_shell returns the expected short commit hash.
- """
- mock_popen.return_value.read.return_value = TEST__SHORT_COMMIT_HASH
-
- result = VersionUtils._get_full_commit_hash_from_git_shell()
- assert result == TEST__SHORT_COMMIT_HASH
-
-
class TestVersionUtils_DotGitFiles(VersionBaseTest):
def test_local_dev_with_dot_git_dir_parsing(self, mock_popen: Mock):
@@ -567,37 +591,50 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
"""
# If we're on a branch, getting the commit hash requires the
# .git/refs/heads/<branch> file.
- with mock.patch.multiple(
+ with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(TEST__VERSION_BRANCH, None)),
_get_full_commit_hash_from_git_refs_heads=Mock(return_value=TEST__FULL_COMMIT_HASH),
_get_version_fork_from_git_config=Mock(return_value=TEST__VERSION_FORK),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
- assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
- assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
- assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
- assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ # Mock out the git shell call that retrieves the timestamp.
+ with patch(
+ "seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
+ return_value=None
+ ) as mock_timestamp_from_git_shell:
+ # Verify that we're using the local dev code path
+ assert VersionUtils.is_github_actions_ci() is False
+ assert VersionUtils._is_seedsigner_os_builder_env() is False
- # If we're on a tag (detached HEAD), getting the commit hash requires
- # checking the refs/tags for a matching tag.
- with mock.patch.multiple(
- "seedsigner.helpers.version.VersionUtils",
- _read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
- _get_matching_tag_from_git_refs_tags=Mock(return_value=TEST__VERSION_TAG),
- ):
- assert VersionUtils.get_version_name() == TEST__VERSION_TAG
- assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
+ assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
+ assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+ assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
- # If we're on a commit hash (detached HEAD) with no matching tag, the
- # version name should be the short commit hash.
- with mock.patch.multiple(
- "seedsigner.helpers.version.VersionUtils",
- _read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
- _get_matching_tag_from_git_refs_tags=Mock(return_value=None),
- ):
- assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
- assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+ # If we're on a tag (detached HEAD), getting the commit hash requires
+ # checking the refs/tags for a matching tag.
+ with patch.multiple(
+ "seedsigner.helpers.version.VersionUtils",
+ _read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
+ _get_matching_tag_from_git_refs_tags=Mock(return_value=TEST__VERSION_TAG),
+ ):
+ assert VersionUtils.get_version_name() == TEST__VERSION_TAG
+ assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+
+ # If we're on a commit hash (detached HEAD) with no matching tag, the
+ # version name should be the short commit hash.
+ with patch.multiple(
+ "seedsigner.helpers.version.VersionUtils",
+ _read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
+ _get_matching_tag_from_git_refs_tags=Mock(return_value=None),
+ ):
+ assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
+ assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
+
+ # Verify that through all of this that the git shell timestamp method was
+ # NOT called.
+ mock_timestamp_from_git_shell.assert_not_called()
def test__get_dot_git_dir(self):
@@ -677,17 +714,17 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
# We already tested _read_git_HEAD_file() above, so just mock scenarios here.
# On a branch:
- with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(TEST__VERSION_BRANCH, None)):
+ with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(TEST__VERSION_BRANCH, None)):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_BRANCH
# Detached HEAD at commit hash, with matching tag
- with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
- with mock.patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=TEST__VERSION_NAME):
+ with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
+ with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=TEST__VERSION_NAME):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_NAME
# Detached HEAD at commit hash, no matching tag; returns the short commit hash
- with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
- with mock.patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=None):
+ with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
+ with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=None):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
@@ -695,7 +732,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
# _get_commit_hash_from_git_HEAD is a trivial convenience function that relies on
# _read_git_HEAD_file which we've already tested. Just verify the expected outputs
# here.
- with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__SHORT_COMMIT_HASH)):
+ with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__SHORT_COMMIT_HASH)):
assert VersionUtils._get_full_commit_hash_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
@@ -759,7 +796,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
- with mock.patch("builtins.open", mock.mock_open(read_data=git_config)):
+ with patch("builtins.open", mock.mock_open(read_data=git_config)):
assert VersionUtils._get_version_fork_from_git_config() == expected_fork
# Missing origin section should return None
@@ -769,7 +806,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
- with mock.patch("builtins.open", mock.mock_open(read_data=git_config_no_origin)):
+ with patch("builtins.open", mock.mock_open(read_data=git_config_no_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# Handle malformed origin section (can't find url) should return None
@@ -779,11 +816,11 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
- with mock.patch("builtins.open", mock.mock_open(read_data=git_config_malformed_origin)):
+ with patch("builtins.open", mock.mock_open(read_data=git_config_malformed_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# If the git config file is missing, should return None
- with mock.patch("builtins.open", side_effect=FileNotFoundError):
+ with patch("builtins.open", side_effect=FileNotFoundError):
assert VersionUtils._get_version_fork_from_git_config() is None
@@ -802,12 +839,13 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
assert timestamp < datetime.now() + timedelta(days=30)
# Now mock out os.path.getmtime to force all files to have a known timestamp
- expected_timestamp = datetime(2025, 12, 23, 0, 0, 0)
- with mock.patch("os.path.getmtime", return_value=expected_timestamp.timestamp()):
- assert VersionUtils._get_last_modified_timestamp_from_src_files() == expected_timestamp
+ local_timestamp = datetime(2025, 12, 23, 0, 0, 0)
+ with patch("os.path.getmtime", return_value=local_timestamp.timestamp()):
+ # Note that the result will be converted to UTC with no tzinfo
+ assert VersionUtils._get_last_modified_timestamp_from_src_files() == local_timestamp.astimezone(timezone.utc).replace(tzinfo=None)
# Mock out os.walk() to simulate no .py files found
- with mock.patch("os.walk", return_value=[]):
+ with patch("os.walk", return_value=[]):
assert VersionUtils._get_last_modified_timestamp_from_src_files() is None
@@ -833,7 +871,7 @@ class TestVersionUtils_Misc(VersionBaseTest):
fake_response.status = 200
fake_response.read.return_value = json.dumps(latest_releases_response_dict).encode('utf-8')
- with mock.patch("urllib.request.urlopen", return_value=fake_response):
+ with patch("urllib.request.urlopen", return_value=fake_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
# mock_popen returns empty string, which mimics not having local git data to
@@ -859,13 +897,13 @@ class TestVersionUtils_Misc(VersionBaseTest):
# Should gracefully handle HTTP errors
fake_error_response = Mock()
fake_error_response.status = 404
- with mock.patch("urllib.request.urlopen", return_value=fake_error_response):
+ with patch("urllib.request.urlopen", return_value=fake_error_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
# And other exceptions
- with mock.patch("urllib.request.urlopen", side_effect=Exception("Network error")):
+ with patch("urllib.request.urlopen", side_effect=Exception("Network error")):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
@@ -884,7 +922,7 @@ class TestVersion(VersionBaseTest):
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
- Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH]
+ Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
@@ -893,17 +931,12 @@ class TestVersion(VersionBaseTest):
Test that we can override the version data via the Version.override_version_data()
method.
"""
- override_name = "v9.9.9-test"
- override_fork = "TestFork"
- override_commit_hash = "abcd123"
- override_timestamp = datetime(2030, 1, 1, 0, 0, 0)
-
# Initially the version data is pulled from the usual sources
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
assert Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
- assert Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH]
+ assert Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# While we're in the mocked SeedSigner OS environment, verify that the
@@ -911,28 +944,59 @@ class TestVersion(VersionBaseTest):
with pytest.raises(NotAllowedInSeedSignerOS):
Version.override_data()
+ override_name = "v9.9.9-test"
+ override_fork = "TestFork"
+ override_commit_hash = "abcd123"
+ override_timestamp = datetime(2030, 1, 1, 0, 0, 0)
+ override_dict = {
+ VersionUtils.VERSIONFILE_ATTR__NAME: override_name,
+ VersionUtils.VERSIONFILE_ATTR__FORK: override_fork,
+ VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: override_commit_hash,
+ VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: override_timestamp,
+ }
+
# No longer in the mocked SeedSigner OS environment; should be allowed now.
- Version.override_data(
- version_name=override_name,
- version_fork=override_fork,
- short_commit_hash=override_commit_hash,
- version_timestamp=override_timestamp,
- )
+ Version.override_data(**override_dict)
+
+ assert Version.get_version_name() == override_name
+ assert Version.get_version_fork() == override_fork
+ assert Version.get_short_commit_hash() == override_commit_hash
+ assert Version.get_version_timestamp() == override_timestamp
+ # No overrides specified should result in no values changed.
+ Version.override_data()
assert Version.get_version_name() == override_name
assert Version.get_version_fork() == override_fork
assert Version.get_short_commit_hash() == override_commit_hash
assert Version.get_version_timestamp() == override_timestamp
+
def test_to_dict(self):
"""
Test that Version.to_dict() returns the expected dictionary.
"""
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
+ # In SeedSigner OS, the version data comes from the version file.
assert Version.to_dict() == TEST__VERSION_DICT
+ extra_attr = "extra"
+ different_dict = {
+ VersionUtils.VERSIONFILE_ATTR__NAME: "v1.2.3-different",
+ VersionUtils.VERSIONFILE_ATTR__FORK: "DifferentFork",
+ VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: "differenthash",
+ VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: datetime(2026, 1, 1, 0, 0, 0), # stored internally as a datetime
+ extra_attr: "should be ignored",
+ }
+ Version.override_data(**different_dict)
+ output_dict = Version.to_dict()
+ assert output_dict[VersionUtils.VERSIONFILE_ATTR__NAME] == different_dict[VersionUtils.VERSIONFILE_ATTR__NAME]
+ assert output_dict[VersionUtils.VERSIONFILE_ATTR__FORK] == different_dict[VersionUtils.VERSIONFILE_ATTR__FORK]
+ assert output_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == different_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
+ assert output_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == different_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP].isoformat() # returned as a string
+ assert extra_attr not in output_dict
+
class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
@@ -949,14 +1013,14 @@ class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
if we run a decorated function while in SeedSigner OS.
"""
# Patch over the Settings.HOSTNAME value to simulate running in SeedSigner OS
- with mock.patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
+ with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
with pytest.raises(NotAllowedInSeedSignerOS):
self.dummy_function()
def test_allowed_outside_seedsigner_os(self):
# Now try with any other HOSTNAME
- with mock.patch("seedsigner.models.settings.Settings.HOSTNAME", "my_dev_machine"):
+ with patch("seedsigner.models.settings.Settings.HOSTNAME", "my_dev_machine"):
assert self.dummy_function() == self.SUCCESS
diff --git a/tools/write_versionfile.py b/tools/write_versionfile.py
index 14323b5..4bf4cf1 100644
--- a/tools/write_versionfile.py
+++ b/tools/write_versionfile.py
@@ -8,53 +8,40 @@ from seedsigner.helpers.version import Version, VersionUtils
CLI utility to extract the current version data and write to
`src/seedsigner/version.json`. Primarily used by the SeedSigner OS build process.
-SeedSigner OS lifecycle:
- * Build process runs this script to generate version.json.
- * version.json is included in the SeedSigner OS image.
- * SeedSigner OS reads version.json at runtime.
-
Notes:
* The SeedSigner OS build environment already relies on `git` being installed.
- * This script can also be run in local dev but `git` shell commands are required.
+ * This script can also be run in local dev but note slight difference in how
+ version_timestamp is gathered.
+ * Tries to pull current version status via `git` shell commands, but has fallbacks
+ to directly parse .git/ files.
Version data:
- * version_name:
- * Check for the SEEDSIGNER_VERSION_NAME env var (provided in SeedSigner OS build
- env).
- * Will be the branch, tag, or commit hash being built.
- * If running in local dev instead, this script will try to populate that env var
- using `git` shell commands:
- * Current git branch name
- * Current git tag name
- * Current git commit hash
+ * version_name: Retrieves the current git status by checking, in order:
+ * Current git branch name
+ * Current git tag name
+ * Current git commit hash
* version_fork:
- * Pulls the current repo owner from the `git remote` shell command.
+ * The current repo owner.
+
+ * short_commit_hash:
+ * Current git short commit hash.
* version_timestamp:
- * Pulls last git commit time from `git log`.
+ * SeedSigner OS builder: Pulls last git commit time for the checked out
+ branch/tag/commit.
+ * Local dev: Scans the source python files for the most recent modified time.
- * short_commit_hash:
- * Pulls current git commit hash from `git` shell command.
"""
if __name__ == "__main__":
- is_seedsigner_os_builder = VersionUtils._is_seedsigner_os_builder_env()
-
- if not is_seedsigner_os_builder:
- # Pull version_name from the current git state via `git` shell commands
- version_name = VersionUtils._get_version_name_from_git_shell()
-
- # Temporarily set the env var
- os.environ[VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME] = version_name
-
+ # As soon as `Version` is instantiated, it will gather all the version data
+ # according to the logic in `VersionUtils`.
version_info = Version.get_instance().to_dict()
+
+ # Write the version.json file.
version_file_path = VersionUtils._get_version_file_path()
with open(version_file_path, "w") as f:
json.dump(version_info, f, indent=4)
print(f"Wrote version info to: {version_file_path}")
print(json.dumps(version_info, indent=4))
-
- # Clean up the temp env var if needed
- if not is_seedsigner_os_builder:
- del os.environ[VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME]
Why this scored 20/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.