diff --git a/tools/create_release.py b/tools/create_release.py
index d3f07e8..12a1949 100755
--- a/tools/create_release.py
+++ b/tools/create_release.py
@@ -146,14 +146,16 @@ def assign_to_user(
s.ok(f"Assigned to {self.github.actor()}")
return stage.UserAbort(f"Returning to the user to {action}")
- def report_failure(self, version: str, exception: Exception) -> None:
+ def report_failure(
+ self, version: str, exception: Exception | BaseException
+ ) -> None:
"""Report a failure to the release tracking issue."""
if not self.config.issue:
return
print(f"Reporting failure to tracking issue #{self.config.issue}...")
instruction = f"❌ **Failure:** {exception}"
- self.assign_to_user(
+ raise self.assign_to_user(
None,
version,
action="fix the failure",
@@ -166,7 +168,7 @@ def run(self) -> None:
self.run_stages()
except stage.UserAbort as e:
print(e.message)
- except Exception as e:
+ except (Exception, BaseException) as e:
self.report_failure(self.version, e)
raise e
@@ -440,7 +442,8 @@ def stage_validate(self) -> None:
validate_pr.Config(
commit=not self.config.verify,
release=self.config.production,
- )
+ ),
+ failures=[],
)
def extract_issue_release_notes(self, body: str) -> str:
@@ -989,6 +992,7 @@ def run_stages(self, version: str | None = None) -> None:
self.stage_branch(version)
self.stage_gitignore()
self.stage_validate()
+ self.update_dashboard(version)
self.stage_release_notes(version)
self.stage_commit(version)
self.stage_push()
diff --git a/tools/create_release_test.py b/tools/create_release_test.py
index b2577c6..7078d05 100644
--- a/tools/create_release_test.py
+++ b/tools/create_release_test.py
@@ -5,6 +5,7 @@
from unittest.mock import MagicMock
from create_release import Config, Releaser
+from lib import stage
class TestDashboardRenderer(unittest.TestCase):
@@ -70,13 +71,28 @@ def setUp(self) -> None:
self.git = MagicMock()
self.releaser = Releaser(self.config, self.git, self.github)
+ def test_run_catches_base_exception(self) -> None:
+ # Mock run_stages to raise SystemExit (a BaseException)
+ with unittest.mock.patch.object(
+ self.releaser, "run_stages", side_effect=SystemExit(1)
+ ), unittest.mock.patch.object(
+ self.releaser, "report_failure", side_effect=stage.UserAbort("failed")
+ ) as mock_report:
+ with self.assertRaises(stage.UserAbort):
+ self.releaser.run()
+
+ mock_report.assert_called()
+ args, kwargs = mock_report.call_args
+ self.assertIsInstance(args[1], SystemExit)
+
def test_report_failure(self) -> None:
self.github.actor.return_value = "human"
self.github.get_issue.return_value = MagicMock(
body="### Release progress\n[ ] ..."
)
- self.releaser.report_failure("v1.0.0", Exception("Something went wrong"))
+ with self.assertRaises(stage.UserAbort):
+ self.releaser.report_failure("v1.0.0", Exception("Something went wrong"))
# Check that issue was reassigned
self.github.issue_unassign.assert_called_with(1, ["toktok-releaser"])
diff --git a/tools/validate_pr.py b/tools/validate_pr.py
index 0bf0e69..ec5d010 100755
--- a/tools/validate_pr.py
+++ b/tools/validate_pr.py
@@ -310,7 +310,7 @@ def check_changelog(failures: list[str], config: Config) -> None:
check.ok("The changelog is up-to-date")
-def main(config: Config) -> None:
+def main(config: Config, failures: list[str] | None = None) -> None:
"""Main entry point."""
actor = github.actor()
if config.debug:
@@ -326,29 +326,32 @@ def main(config: Config) -> None:
print("\nRunning checks...\n")
- failures: list[str] = []
+ failed_checks: list[str] = failures if failures is not None else []
# If the PR branch looks like a version number, do checks for a release PR.
if config.release or re.match(git.RELEASE_BRANCH_REGEX, github.head_ref()):
print("This is a release PR.\n")
- check_github_weblate_prs(failures)
- check_flathub_descriptor_dependencies(failures, config)
- check_toxcore_version(failures)
- check_package_versions(failures, config)
+ check_github_weblate_prs(failed_checks)
+ check_flathub_descriptor_dependencies(failed_checks, config)
+ check_toxcore_version(failed_checks)
+ check_package_versions(failed_checks, config)
else:
print(f"This is not a release PR ({git.RELEASE_BRANCH_REGEX.pattern}).\n")
- check_no_version_changes(failures)
+ check_no_version_changes(failed_checks)
- check_changelog(failures, config)
+ check_changelog(failed_checks, config)
if config.debug:
print(f"\nDebug: {len(github.api_requests)} GitHub API requests made")
- if failures:
+ if failed_checks:
print("\nSome checks failed:")
- for failure in failures:
+ for failure in failed_checks:
print(f" - {failure}")
- exit(1)
+
+ if failures is None:
+ exit(1)
+ raise stage.InvalidState(f"{len(failed_checks)} checks failed")
if __name__ == "__main__":
diff --git a/tools/validate_pr_test.py b/tools/validate_pr_test.py
index dad8b41..4777f50 100644
--- a/tools/validate_pr_test.py
+++ b/tools/validate_pr_test.py
@@ -2,15 +2,16 @@
# Copyright © 2026 The TokTok team
import unittest
import unittest.mock
+from typing import Any
-from validate_pr import (Config, check_changelog, parse_toxcore_version,
- parse_version_diff, parse_weblate_prs)
+import validate_pr
+from lib import stage
class TestCheckChangelog(unittest.TestCase):
- @unittest.mock.patch("update_changelog.main")
- @unittest.mock.patch("update_changelog.read_clog_toml", return_value={})
- @unittest.mock.patch("update_changelog.parse_config")
+ @unittest.mock.patch("validate_pr.update_changelog.main")
+ @unittest.mock.patch("validate_pr.update_changelog.read_clog_toml", return_value={})
+ @unittest.mock.patch("validate_pr.update_changelog.parse_config")
@unittest.mock.patch("validate_pr.github.head_ref")
@unittest.mock.patch("validate_pr.has_diff", return_value=False)
@unittest.mock.patch("validate_pr.stage.Stage")
@@ -29,23 +30,23 @@ def test_check_changelog_production(
# 1. Release config set to True
mock_head_ref.return_value = "some-branch"
- config = Config(commit=False, release=True)
- check_changelog([], config)
+ config = validate_pr.Config(commit=False, release=True)
+ validate_pr.check_changelog([], config)
self.assertTrue(clog_config.production)
mock_clog_main.assert_called_with(clog_config)
# 2. Release config False, but branch is a production release branch
clog_config.production = False
mock_head_ref.return_value = "release/v1.0.0"
- config = Config(commit=False, release=False)
- check_changelog([], config)
+ config = validate_pr.Config(commit=False, release=False)
+ validate_pr.check_changelog([], config)
self.assertTrue(clog_config.production)
# 3. Release config False, branch is an RC release branch
clog_config.production = False
mock_head_ref.return_value = "release/v1.0.0-rc.1"
- config = Config(commit=False, release=False)
- check_changelog([], config)
+ config = validate_pr.Config(commit=False, release=False)
+ validate_pr.check_changelog([], config)
self.assertFalse(clog_config.production)
@@ -65,15 +66,15 @@ def test_parse_weblate_prs(self) -> None:
},
]
expected = [("Translation 1", "url1"), ("Translation 2", "url3")]
- self.assertEqual(parse_weblate_prs(prs_data), expected)
+ self.assertEqual(validate_pr.parse_weblate_prs(prs_data), expected)
def test_parse_toxcore_version(self) -> None:
content = """#!/bin/bash
TOXCORE_VERSION=0.2.20
SOME_OTHER_VAR=val
"""
- self.assertEqual(parse_toxcore_version(content), "0.2.20")
- self.assertIsNone(parse_toxcore_version("no version here"))
+ self.assertEqual(validate_pr.parse_toxcore_version(content), "0.2.20")
+ self.assertIsNone(validate_pr.parse_toxcore_version("no version here"))
def test_parse_version_diff(self) -> None:
diff = """--- a/platform/linux/chat.tox.CiTools.appdata.xml
@@ -81,7 +82,7 @@ def test_parse_version_diff(self) -> None:
-
line
+new line
""" - minus, plus = parse_version_diff(diff) + minus, plus = validate_pr.parse_version_diff(diff) self.assertEqual(minus, []) self.assertEqual(plus, []) +class TestMain(unittest.TestCase): + @unittest.mock.patch("validate_pr.github.actor", return_value="human") + @unittest.mock.patch("validate_pr.github.head_ref", return_value="master") + @unittest.mock.patch("validate_pr.github.api_requests", new_callable=list) + @unittest.mock.patch("validate_pr.check_github_weblate_prs") + @unittest.mock.patch("validate_pr.check_flathub_descriptor_dependencies") + @unittest.mock.patch("validate_pr.check_toxcore_version") + @unittest.mock.patch("validate_pr.check_package_versions") + @unittest.mock.patch("validate_pr.check_no_version_changes") + @unittest.mock.patch("validate_pr.check_changelog") + def test_main_with_failures_list( + self, + mock_check_changelog: unittest.mock.MagicMock, + mock_check_no_version_changes: unittest.mock.MagicMock, + mock_check_package_versions: unittest.mock.MagicMock, + mock_check_toxcore_version: unittest.mock.MagicMock, + mock_check_flathub_descriptor_dependencies: unittest.mock.MagicMock, + mock_check_github_weblate_prs: unittest.mock.MagicMock, + mock_api_requests: list[Any], + mock_head_ref: unittest.mock.MagicMock, + mock_actor: unittest.mock.MagicMock, + ) -> None: + # Setup: check_changelog appends a failure + mock_check_changelog.side_effect = lambda f, c: f.append("Changelog failure") + + config = validate_pr.Config(commit=False, release=False) + + # 1. Test with failures=[] -> should raise stage.InvalidState + with self.assertRaises(stage.InvalidState) as cm: + validate_pr.main(config, failures=[]) + self.assertIn("1 checks failed", str(cm.exception)) + + # 2. Test with failures=None -> should call exit(1) + with self.assertRaises(SystemExit) as cm_exit: + validate_pr.main(config, failures=None) + self.assertEqual(cm_exit.exception.code, 1) + + if __name__ == "__main__": unittest.main()