from __future__ import annotations from pathlib import Path import tempfile import unittest from scripts.stable_release import ( STABLE_SHA256_PLACEHOLDER, StableReleaseError, expected_asset_names, render_notes, validate_assets, validate_changelog, validate_notes, verify_release, ) from scripts.preview_release import sha256 from scripts.preview_release import MIN_ASSET_SIZE VERSION = "1.6.0" def write_fake_asset(path: Path) -> None: if path.suffix == ".exe": header = b"MZ" + b"\0" * 6 elif path.suffix == ".zip": header = b"PK\x03\x04" + b"\0" * 4 elif path.name.endswith(".AppImage"): header = b"\x7fELF\x02\x01\x01\0" elif path.suffix == ".deb": header = b"!\n" elif path.name.endswith(".tar.gz"): header = b"\x1f\x8b\x08\0\0\0\0\0" elif path.suffix == ".dmg": header = b"\0" * 8 else: raise AssertionError(path) path.write_bytes(header + b"\0" * (MIN_ASSET_SIZE - len(header))) if path.suffix == ".dmg": with path.open("r+b") as stream: stream.seek(-512, 2) stream.write(b"koly") def notes(checksum_placeholder: bool = True) -> str: checksum = STABLE_SHA256_PLACEHOLDER if checksum_placeholder else "0" * 64 return f"""# Pebrel {VERSION} ## English ### Added - A stable package set. ## 中文 ### 新增 - 稳定版安装包集合。 ## Contributors \"@Kuddev ## SHA256 {checksum} """ class StableReleaseTests(unittest.TestCase): def test_stable_workflow_runs_gpui_interaction_tests_on_every_platform(self) -> None: root = Path(__file__).resolve().parents[2] workflow = (root / ".github/workflows/release.yml").read_text(encoding="utf-8") command = "cargo test --locked -p nebula --bin pebrel --features gpui-test-support gpui_shell::" self.assertEqual(workflow.count(command), 3) windows_step = workflow.split(" - name: Test workspace and native harness\n", 1)[1] windows_step = windows_step.split(" - name:", 1)[0] self.assertIn("shell: pwsh", windows_step) lines = [line.strip() for line in windows_step.splitlines()] commands = 0 for index, line in enumerate(lines): if line.startswith(("cargo test ", "python -m unittest ")): commands += 1 self.assertEqual(lines[index + 1], "if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }") self.assertEqual(commands, 5) def test_native_packagers_expose_stable_channel_without_preview_id(self) -> None: root = Path(__file__).resolve().parents[2] linux = (root / "scripts/package-linux.sh").read_text(encoding="utf-8") macos = (root / "scripts/package-macos.sh").read_text(encoding="utf-8") for source in (linux, macos): self.assertIn('--channel', source) self.assertIn('channel" == "stable"', source) self.assertIn('stable packages must not include --preview-id', source) self.assertIn('asset_architecture="x64"', linux) self.assertIn('asset_architecture="x64"', macos) self.assertIn('asset_architecture="arm64"', macos) def test_expected_assets_use_stable_public_names(self) -> None: names = expected_asset_names(VERSION) self.assertIn("Pebrel-v1.6.0-linux-x64-preview.AppImage", names) self.assertIn("Pebrel-v1.6.0-macos-arm64-preview.dmg", names) self.assertIn("Pebrel-v1.6.0-macos-x64-preview.dmg", names) self.assertIn("Pebrel-v1.6.0-windows-x64-setup.exe", names) self.assertIn("NebulaTerminal-1.6.0-windows-x64-setup.exe", names) self.assertEqual(len(names), 8) def test_stable_manifest_rejects_prerelease_versions(self) -> None: with self.assertRaisesRegex(StableReleaseError, "invalid stable version"): expected_asset_names("1.6.0-rc.1") def test_17_and_later_assets_use_only_pebrel_installer(self) -> None: for version in ("1.7.0", "1.7.1", "1.10.0", "2.0.0"): with self.subTest(version=version): names = expected_asset_names(version) self.assertEqual(len(names), 7) self.assertIn(f"Pebrel-v{version}-windows-x64-setup.exe", names) self.assertNotIn(f"NebulaTerminal-{version}-windows-x64-setup.exe", names) def test_17_assets_reject_retired_alias_and_missing_installer(self) -> None: version = "1.7.0" with tempfile.TemporaryDirectory() as directory: root = Path(directory) for name in expected_asset_names(version): if name.startswith("Pebrel-"): write_fake_asset(root / name) self.assertEqual(len(validate_assets(root, version)), 7) legacy = root / "NebulaTerminal-1.7.0-windows-x64-setup.exe" write_fake_asset(legacy) with self.assertRaisesRegex(StableReleaseError, "unexpected: NebulaTerminal"): validate_assets(root, version) legacy.unlink() (root / "Pebrel-v1.7.0-windows-x64-setup.exe").unlink() with self.assertRaisesRegex(StableReleaseError, "missing: Pebrel-v1.7.0-windows-x64-setup.exe"): validate_assets(root, version) def test_17_notes_require_only_current_asset_names(self) -> None: version = "1.7.0" checksums = "\n".join( f"- `{name}`: `PENDING FINAL BUILD`" for name in expected_asset_names(version) if name.startswith("Pebrel-") ) body = notes().replace(VERSION, version).replace(STABLE_SHA256_PLACEHOLDER, checksums) with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "v1.7.0.md" source.write_text(body, encoding="utf-8") self.assertEqual(validate_notes(source, version), body) source.write_text( body + "- `NebulaTerminal-1.7.0-windows-x64-setup.exe`: `PENDING FINAL BUILD`\n", encoding="utf-8", ) with self.assertRaisesRegex(StableReleaseError, "unexpected: NebulaTerminal"): validate_notes(source, version) def test_complete_assets_require_identical_legacy_installer(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) for name in expected_asset_names(VERSION): write_fake_asset(root / name) self.assertEqual(len(validate_assets(root, VERSION)), 8) legacy = root / "NebulaTerminal-1.6.0-windows-x64-setup.exe" legacy.write_bytes(legacy.read_bytes()[:-1] + b"x") with self.assertRaisesRegex(StableReleaseError, "byte-identical"): validate_assets(root, VERSION) def test_asset_magic_is_checked(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) for name in expected_asset_names(VERSION): write_fake_asset(root / name) (root / "Pebrel-v1.6.0-linux-x64-preview.AppImage").write_bytes(b"bad" + b"\0" * MIN_ASSET_SIZE) with self.assertRaisesRegex(StableReleaseError, "ELF"): validate_assets(root, VERSION) def test_notes_require_bilingual_categories_and_contributor_link(self) -> None: with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "v1.6.0.md" source.write_text(notes(), encoding="utf-8") self.assertEqual(validate_notes(source, VERSION), notes()) source.write_text(notes().replace("https://github.com/Kuddev", "https://example.invalid"), encoding="utf-8") with self.assertRaisesRegex(StableReleaseError, "GitHub links"): validate_notes(source, VERSION) def test_changelog_must_list_the_same_stable_asset_names(self) -> None: with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "CHANGELOG.md" source.write_text( "## 1.6.0 - 2026-09-07\n\n" + "\n".join(f"- `{name}`: `PENDING FINAL BUILD`" for name in expected_asset_names(VERSION)) + "\n", encoding="utf-8", ) self.assertTrue(validate_changelog(source, VERSION)) source.write_text(source.read_text(encoding="utf-8").replace("Pebrel-v1.6.0", "Pebrel-1.6.0"), encoding="utf-8") with self.assertRaisesRegex(StableReleaseError, "missing stable asset"): validate_changelog(source, VERSION) def test_render_notes_replaces_placeholder_with_all_hashes(self) -> None: rendered = render_notes(notes(), "a" * 64 + " Pebrel-v1.6.0-windows-x64.zip\n") self.assertNotIn(STABLE_SHA256_PLACEHOLDER, rendered) self.assertIn("a" * 64 + " Pebrel-v1.6.0-windows-x64.zip", rendered) def test_render_notes_replaces_pending_named_entries(self) -> None: source = notes().replace(STABLE_SHA256_PLACEHOLDER, "- `Pebrel-v1.6.0-windows-x64.zip`: `PENDING FINAL BUILD`") rendered = render_notes(source, "a" * 64 + " Pebrel-v1.6.0-windows-x64.zip\n") self.assertIn("`Pebrel-v1.6.0-windows-x64.zip`: `" + "a" * 64, rendered) def test_render_notes_rejects_stale_explicit_hashes(self) -> None: with self.assertRaisesRegex(StableReleaseError, "stale or incomplete"): render_notes(notes(False), "a" * 64 + " Pebrel-v1.6.0-windows-x64.zip\n") def test_remote_release_verification_checks_all_assets_and_notes(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) for name in expected_asset_names(VERSION): write_fake_asset(root / name) (root / "SHA256SUMS").write_text("checksums\n", encoding="utf-8") release_notes = notes().replace(STABLE_SHA256_PLACEHOLDER, "rendered checksums") (root / "RELEASE_NOTES.md").write_text(release_notes, encoding="utf-8") names = [*expected_asset_names(VERSION), "SHA256SUMS"] metadata = { "name": "Pebrel 1.6.0", "tag_name": "v1.6.0", "draft": False, "prerelease": False, "body": release_notes, "assets": [ { "name": name, "label": "", "state": "uploaded", "size": (root / name).stat().st_size, "digest": f"sha256:{sha256(root / name)}", } for name in names ], } verify_release(metadata, root, VERSION, "a" * 40, "a" * 40) metadata["body"] = release_notes.replace("\n", "\r\n") verify_release(metadata, root, VERSION, "a" * 40, "a" * 40) metadata["assets"][0]["label"] = "wrong" with self.assertRaisesRegex(StableReleaseError, "label"): verify_release(metadata, root, VERSION, "a" * 40, "a" * 40) if __name__ == "__main__": unittest.main()