Skip to content
Merged
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
12 changes: 8 additions & 4 deletions tools/create_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 17 additions & 1 deletion tools/create_release_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import MagicMock

from create_release import Config, Releaser
from lib import stage


class TestDashboardRenderer(unittest.TestCase):
Expand Down Expand Up @@ -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"])
Expand Down
25 changes: 14 additions & 11 deletions tools/validate_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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__":
Expand Down
71 changes: 55 additions & 16 deletions tools/validate_pr_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)


Expand All @@ -65,34 +66,72 @@ 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
+++ b/platform/linux/chat.tox.CiTools.appdata.xml
- <release version="1.18.0-rc.3" date="2024-12-29"/>
+ <release version="1.18.0" date="2024-12-29"/>
"""
minus, plus = parse_version_diff(diff)
minus, plus = validate_pr.parse_version_diff(diff)
self.assertEqual(minus, ["1.18.0-rc.3"])
self.assertEqual(plus, ["1.18.0"])

def test_parse_version_diff_no_changes(self) -> None:
diff = """some other changes
- <p>line</p>
+ <p>new line</p>"""
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()
Loading