Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 11 additions & 2 deletions scripts/ci/package_windows_portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
WINDOWS_LEGACY_INSTALLER_RE = re.compile(
r"(?P<name>.+?)_(?P<version>.+?)_(?P<arch>x64|amd64|arm64|aarch64)-setup\.exe$"
)
LEGACY_NIGHTLY_VERSION_RE = re.compile(
rf"^(?P<version>.+?)-nightly[._-](?P<date>[0-9]{{8}})[._-](?P<sha>{SHORT_SHA_PATTERN})$"
)

PORTABLE_README_NAME = "README-portable.txt"
PORTABLE_README_TEXT = """AstrBot Windows portable package
Expand Down Expand Up @@ -119,7 +122,12 @@ def installer_to_portable_name(installer_name: str) -> str:
name = legacy_match.group("name")
version = legacy_match.group("version")
arch = normalize_arch(legacy_match.group("arch"))
return f"{name}_{version}_windows_{arch}_portable.zip"
nightly_suffix = ""
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
nightly_match = LEGACY_NIGHTLY_VERSION_RE.fullmatch(version)
if nightly_match:
version = nightly_match.group("version")
nightly_suffix = f"_nightly_{nightly_match.group('sha')}"
return f"{name}_{version}_windows_{arch}_portable{nightly_suffix}.zip"

raise ValueError(
"Unexpected Windows installer name: "
Expand Down Expand Up @@ -203,9 +211,10 @@ def populate_portable_root(
) -> None:
release_dir = resolve_release_dir(bundle_dir)
main_executable_path = resolve_main_executable_path(bundle_dir, project_config)
portable_executable_name = f"{project_config.product_name}.exe"

destination_root.mkdir(parents=True, exist_ok=True)
shutil.copy2(main_executable_path, destination_root / main_executable_path.name)
shutil.copy2(main_executable_path, destination_root / portable_executable_name)

webview_loader = release_dir / "WebView2Loader.dll"
if webview_loader.is_file():
Expand Down
11 changes: 10 additions & 1 deletion scripts/ci/test_package_windows_portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def test_installer_to_portable_name_accepts_canonical_nightly_windows_name(self)
"AstrBot_4.29.0_windows_amd64_portable_nightly_deadbeef.zip",
)

def test_installer_to_portable_name_normalizes_legacy_nightly_windows_name(self):
self.assertEqual(
MODULE.installer_to_portable_name(
"AstrBot_4.29.0-nightly.20260401.deadbeef_aarch64-setup.exe"
),
"AstrBot_4.29.0_windows_arm64_portable_nightly_deadbeef.zip",
)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

def test_installer_to_portable_name_rejects_noncanonical_nightly_suffix_length(
self,
):
Comment on lines +60 to 118
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add coverage for malformed legacy nightly versions that should not be treated as nightly builds

To better exercise LEGACY_NIGHTLY_VERSION_RE, please add cases where the version segment looks nightly-like but doesn’t actually match the regex (e.g. wrong SHA length, invalid date, or missing components). These should follow non-nightly behavior and yield ..._portable.zip without the _nightly_<sha> suffix. You can add these as extra subTests here or as a sibling test asserting the suffix is omitted when the pattern doesn’t fully match.

Suggested change
def test_installer_to_portable_name_normalizes_legacy_nightly_windows_name(self):
arch_cases = [
("x64", "amd64"),
("amd64", "amd64"),
("arm64", "arm64"),
("aarch64", "arm64"),
]
separators = [".", "_", "-"]
for arch_input, arch_output in arch_cases:
for separator in separators:
with self.subTest(arch=arch_input, separator=separator):
installer_name = f"AstrBot_4.29.0-nightly{separator}20260401{separator}deadbeef_{arch_input}-setup.exe"
expected_name = f"AstrBot_4.29.0_windows_{arch_output}_portable_nightly_deadbeef.zip"
self.assertEqual(
MODULE.installer_to_portable_name(installer_name),
expected_name,
)
def test_installer_to_portable_name_rejects_noncanonical_nightly_suffix_length(
self,
):
def test_installer_to_portable_name_normalizes_legacy_nightly_windows_name(self):
arch_cases = [
("x64", "amd64"),
("amd64", "amd64"),
("arm64", "arm64"),
("aarch64", "arm64"),
]
separators = [".", "_", "-"]
for arch_input, arch_output in arch_cases:
for separator in separators:
with self.subTest(arch=arch_input, separator=separator):
installer_name = f"AstrBot_4.29.0-nightly{separator}20260401{separator}deadbeef_{arch_input}-setup.exe"
expected_name = f"AstrBot_4.29.0_windows_{arch_output}_portable_nightly_deadbeef.zip"
self.assertEqual(
MODULE.installer_to_portable_name(installer_name),
expected_name,
)
def test_installer_to_portable_name_treats_malformed_legacy_nightly_as_non_nightly(
self,
):
arch_cases = [
("x64", "amd64"),
("amd64", "amd64"),
("arm64", "arm64"),
("aarch64", "arm64"),
]
separators = [".", "_", "-"]
# Cases that look "nightly-like" but should not match LEGACY_NIGHTLY_VERSION_RE
malformed_fragments = [
# Wrong SHA length (too short)
("nightly{sep}20260401{sep}deadbee", "short_sha"),
# Invalid date (13th month)
("nightly{sep}20261301{sep}deadbeef", "invalid_date"),
# Missing SHA component entirely
("nightly{sep}20260401", "missing_sha"),
]
for arch_input, arch_output in arch_cases:
for separator in separators:
for fragment_template, case_name in malformed_fragments:
fragment = fragment_template.format(sep=separator)
with self.subTest(
arch=arch_input, separator=separator, malformed_case=case_name
):
installer_name = (
f"AstrBot_4.29.0-{fragment}_{arch_input}-setup.exe"
)
# Malformed legacy nightly patterns should be treated as non-nightly
expected_name = (
f"AstrBot_4.29.0_windows_{arch_output}_portable.zip"
)
self.assertEqual(
MODULE.installer_to_portable_name(installer_name),
expected_name,
)
def test_installer_to_portable_name_rejects_noncanonical_nightly_suffix_length(
self,
):
self.assertEqual(project_config.binary_name, "astrbot-desktop-tauri")
self.assertEqual(project_config.portable_marker_name, "portable.flag")

Expand Down Expand Up @@ -299,7 +307,8 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
project_config=MODULE.load_project_config_from(script_path),
)

self.assertTrue((destination_root / "astrbot-desktop-tauri.exe").is_file())
self.assertTrue((destination_root / "AstrBot.exe").is_file())
self.assertFalse((destination_root / "astrbot-desktop-tauri.exe").exists())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider asserting the copied executable name via project_config.product_name to guard against future renames

The current checks are correct, but to make this test resilient to future product renames, derive the expected .exe name from project_config.product_name (e.g. f"{project_config.product_name}.exe") instead of hardcoding AstrBot.exe.

Suggested implementation:

            )

            executable_name = f"{project_config.product_name}.exe"

            self.assertTrue((destination_root / executable_name).is_file())
            self.assertFalse((destination_root / "astrbot-desktop-tauri.exe").exists())
            self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
            self.assertTrue(

This edit assumes that project_config is already defined in the scope of this test method (which appears likely from project_config=MODULE.load_project_config_from(script_path), above). If it is instead only passed into another function and not bound as a local variable, you will need to:

  1. Assign the result of MODULE.load_project_config_from(script_path) to a local project_config variable before this block, and
  2. Pass that project_config into any existing calls that currently construct it inline.

self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
self.assertTrue(
(
Expand Down