#!/usr/bin/env python3 """Exercise release validation and publishing without contacting GitHub.""" import importlib.util import json import os import subprocess import sys import tempfile import unittest from pathlib import Path SCRIPTS = Path(__file__).resolve().parent sys.dont_write_bytecode = True def load_script(name): spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module metadata = load_script("release-metadata").metadata release_assets = load_script("prepare-release") prepare = release_assets.prepare class ReleaseTests(unittest.TestCase): def test_versions_and_tags(self): for version in ("0.1.0", "1.2.3-alpha.1", "1.2.3-beta.2", "1.2.3-rc.3"): with self.subTest(version=version): result = metadata(version, f"v{version}") self.assertEqual(result["prerelease"], str("-" in version).lower()) self.assertEqual(result["native_version"], version.split("-")[0]) for version, tag in ( ("0.1.0", "v0.2.0"), ("0.1.0-beta.1", "v0.1.0"), ("0.1.0-beta.01", None), ("0.1.0-beta.0", None), ("01.2.3", None), ("1.2.3-nightly", None), ("1.2.3+build", None), ): with self.subTest(version=version, tag=tag), self.assertRaises(ValueError): metadata(version, tag) def test_staging_and_checksums(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) source = root / "assets" source.mkdir() names = ( "linux/appimage/flectar-mail.AppImage", "linux/deb/flectar-mail_0.1.0~beta.1_amd64.deb", "windows/flectar-mail-windows-x64.zip", "windows/flectar-mail-windows-x64-setup.exe", "macos/flectar-mail-macos-arm64.zip", "macos/flectar-mail-macos-arm64.dmg", "android/flectar-mail.apk", ) for name in names: path = source / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(name.encode()) (source / "benchmark.json").write_text("{}") dist = root / "dist" prepare(source, dist, "0.1.0-beta.1") self.assertEqual(len(list(dist.iterdir())), 8) self.assertTrue((dist / "flectar-mail-0.1.0-beta.1-android-arm64-test.apk").is_file()) result = subprocess.run( ["sha256sum", "--check", "SHA256SUMS"], cwd=dist, capture_output=True, text=True, ) self.assertEqual(result.returncode, 0, result.stderr) missing = source / names[-1] missing.unlink() with self.assertRaises(ValueError): prepare(source, root / "missing", "0.1.0-beta.1") self.assertFalse((root / "missing").exists()) missing.write_bytes(b"") with self.assertRaises(ValueError): prepare(source, root / "empty", "0.1.0-beta.1") missing.write_bytes(b"dmg") (source / missing.name).write_bytes(b"duplicate") with self.assertRaises(ValueError): prepare(source, root / "duplicate", "0.1.0-beta.1") # Stable releases keep the six desktop downloads, even if the # input folder happens to contain test APKs from another step. (source / "linux/deb/flectar-mail_0.1.0~beta.1_amd64.deb").rename( source / "linux/deb/flectar-mail_0.1.0_amd64.deb" ) stable_dist = root / "stable" prepare(source, stable_dist, "0.1.0") self.assertEqual(len(list(stable_dist.iterdir())), 7) self.assertFalse(list(stable_dist.glob("*.apk"))) def test_notes_distinguish_android_preview(self): self.assertIn("test-signed", release_assets.release_notes("0.1.0-beta.1")) self.assertIn("prereleases only", release_assets.release_notes("0.1.0")) def test_metadata_rejects_stale_android_lockfile(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "platform/android").mkdir(parents=True) (root / "Cargo.toml").write_text('[package]\nversion = "0.2.0-beta.1"\n') lock = '[[package]]\nname = "flectar-mail"\nversion = "0.2.0-beta.1"\n' (root / "Cargo.lock").write_text(lock) android_lock = root / "platform/android/Cargo.lock" android_lock.write_text(lock.replace("0.2.0-beta.1", "0.1.0")) command = [sys.executable, str(SCRIPTS / "release-metadata.py"), "--tag", "v0.2.0-beta.1"] result = subprocess.run(command, cwd=root, capture_output=True, text=True) self.assertNotEqual(result.returncode, 0) self.assertIn("platform/android/Cargo.lock", result.stderr) android_lock.write_text(lock) result = subprocess.run(command, cwd=root, capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("prerelease=true", result.stdout) def publish(self, *, existing="missing", prerelease="true", fail_upload=False): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "dist").mkdir() (root / "dist" / "package.zip").write_bytes(b"package") (root / "release-notes.md").write_text("Notes") gh = root / "gh" gh.write_text('''#!/usr/bin/env python3 import json, os, sys with open("calls.jsonl", "a") as stream: stream.write(json.dumps(sys.argv[1:]) + "\\n") command = sys.argv[2] if command == "view": existing = os.environ["EXISTING"] if existing == "missing": sys.exit(1) print(existing) if command == "upload" and os.environ["FAIL_UPLOAD"] == "true": sys.exit(1) ''') gh.chmod(0o755) result = subprocess.run( ["bash", str(SCRIPTS / "publish-release.sh")], cwd=root, env={**os.environ, "PATH": f"{root}:{os.environ['PATH']}", "GITHUB_REF_NAME": "v0.1.0-beta.1" if prerelease == "true" else "v0.1.0", "RELEASE_PRERELEASE": prerelease, "EXISTING": existing, "FAIL_UPLOAD": str(fail_upload).lower()}, capture_output=True, text=True, ) calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()] return result, calls def test_preview_and_stable_publication(self): for prerelease in ("true", "false"): with self.subTest(prerelease=prerelease): result, calls = self.publish(prerelease=prerelease) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual([c[1] for c in calls], ["view", "create", "upload", "edit"]) self.assertIn("--draft", calls[1]) self.assertIn(f"--prerelease={prerelease}", calls[-1]) self.assertIn("--latest=" + str(prerelease == "false").lower(), calls[-1]) def test_failed_upload_does_not_publish(self): result, calls = self.publish(fail_upload=True) self.assertNotEqual(result.returncode, 0) self.assertNotIn("edit", [c[1] for c in calls]) def test_resume_draft_and_reject_published_replacement(self): result, calls = self.publish(existing="true") self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual([c[1] for c in calls], ["view", "upload", "edit"]) result, calls = self.publish(existing="false") self.assertNotEqual(result.returncode, 0) self.assertEqual([c[1] for c in calls], ["view"]) if __name__ == "__main__": unittest.main()