1#!/usr/bin/env python323from __future__ import annotations45import hashlib6import importlib.util7import json8import pathlib9import tempfile10import unittest111213REPO_DIR = pathlib.Path(__file__).resolve().parents[2]14MODULE_PATH = REPO_DIR / "scripts" / "verify-portal-release.py"15SPEC = importlib.util.spec_from_file_location("portal_release_verifier", MODULE_PATH)16assert SPEC is not None and SPEC.loader is not None17VERIFIER = importlib.util.module_from_spec(SPEC)18SPEC.loader.exec_module(VERIFIER)192021class FakeGit:22 def __init__(self, *, dirty: bool = False, tag_commit: str = "a" * 40) -> None:23 self.dirty = dirty24 self.tag_commit = tag_commit25 self.commands: list[tuple[str, ...]] = []2627 def __call__(self, command: list[str], _cwd: pathlib.Path) -> str:28 self.commands.append(tuple(command))29 if command[1] == "status":30 return " M tracked" if self.dirty else ""31 if command[1] == "rev-parse":32 return "a" * 4033 if command[1] == "rev-list":34 return self.tag_commit35 raise AssertionError(command)363738class PortalReleaseVerifierTests(unittest.TestCase):39 def make_repo(self, root: pathlib.Path, version: str = "0.1.0") -> pathlib.Path:40 repo = root / "repo"41 (repo / ".git").mkdir(parents=True)42 (repo / "src-tauri").mkdir()43 (repo / "crates" / "sigil-host").mkdir(parents=True)44 (repo / "crates" / "sigil-protocol").mkdir(parents=True)45 (repo / "Cargo.toml").write_text(46 '[workspace]\nmembers = ["crates/sigil-host", "crates/sigil-protocol", "src-tauri"]\n',47 encoding="utf-8",48 )49 for path, name in [50 (repo / "crates" / "sigil-host" / "Cargo.toml", "sigil-host"),51 (repo / "crates" / "sigil-protocol" / "Cargo.toml", "sigil-protocol"),52 ]:53 path.write_text(f'[package]\nname = "{name}"\nversion = "{version}"\n', encoding="utf-8")54 (repo / "src-tauri" / "Cargo.toml").write_text(55 f'[package]\nname = "portal"\nversion = "{version}"\n'56 '[features]\ndemo-direct-node = []\n'57 'experimental-non-macos-pointer-capture = []\n',58 encoding="utf-8",59 )60 (repo / "src-tauri" / "tauri.conf.json").write_text(61 json.dumps(62 {63 "productName": "Portal",64 "identifier": "sh.goq.portal",65 "version": version,66 }67 ),68 encoding="utf-8",69 )70 return repo7172 def test_source_binds_clean_tag_head_and_all_versions(self) -> None:73 with tempfile.TemporaryDirectory() as temporary:74 repo = self.make_repo(pathlib.Path(temporary))75 fake_git = FakeGit()76 result = VERIFIER.validate_source(repo, "v0.1.0", fake_git)77 self.assertEqual(result["architecture"], "arm64")78 self.assertFalse(result["demo_direct_node"])79 self.assertEqual(len(result["workspace_packages"]), 3)80 self.assertIn(("git", "status", "--porcelain=v1", "--untracked-files=all"), fake_git.commands)8182 def test_source_rejects_dirty_mismatched_or_demo_default(self) -> None:83 with tempfile.TemporaryDirectory() as temporary:84 root = pathlib.Path(temporary)85 repo = self.make_repo(root)86 with self.assertRaisesRegex(VERIFIER.VerificationError, "clean"):87 VERIFIER.validate_source(repo, "v0.1.0", FakeGit(dirty=True))88 with self.assertRaisesRegex(VERIFIER.VerificationError, "exactly to HEAD"):89 VERIFIER.validate_source(repo, "v0.1.0", FakeGit(tag_commit="b" * 40))90 (repo / "src-tauri" / "Cargo.toml").write_text(91 '[package]\nname = "portal"\nversion = "0.1.0"\n'92 '[features]\ndefault = ["demo-direct-node"]\ndemo-direct-node = []\n',93 encoding="utf-8",94 )95 with self.assertRaisesRegex(VERIFIER.VerificationError, "enabled by default"):96 VERIFIER.validate_source(repo, "v0.1.0", FakeGit())97 (repo / "src-tauri" / "Cargo.toml").write_text(98 '[package]\nname = "portal"\nversion = "0.1.0"\n'99 '[features]\n'100 'default = ["experimental-non-macos-pointer-capture"]\n'101 'demo-direct-node = []\n'102 'experimental-non-macos-pointer-capture = []\n',103 encoding="utf-8",104 )105 with self.assertRaisesRegex(106 VERIFIER.VerificationError,107 "experimental-non-macos-pointer-capture must not be enabled by default",108 ):109 VERIFIER.validate_source(repo, "v0.1.0", FakeGit())110111 def write_assets(self, assets: pathlib.Path) -> None:112 assets.mkdir()113 dmg_name, checksum_name, manifest_name = VERIFIER.expected_asset_names("v0.1.0")114 dmg = assets / dmg_name115 dmg.write_bytes(b"signed-notarized-dmg")116 digest = hashlib.sha256(dmg.read_bytes()).hexdigest()117 (assets / checksum_name).write_text(f"{digest} {dmg_name}\n", encoding="utf-8")118 manifest = VERIFIER.expected_release_manifest("v0.1.0", "a" * 40, digest)119 (assets / manifest_name).write_text(json.dumps(manifest), encoding="utf-8")120121 def test_assets_require_exact_names_digest_and_manifest(self) -> None:122 with tempfile.TemporaryDirectory() as temporary:123 root = pathlib.Path(temporary)124 repo = self.make_repo(root)125 assets = root / "assets"126 self.write_assets(assets)127 result = VERIFIER.validate_assets(repo, "v0.1.0", assets, FakeGit())128 self.assertTrue(result["gatekeeper_verified"])129 _, checksum_name, manifest_name = VERIFIER.expected_asset_names("v0.1.0")130 original_checksum = (assets / checksum_name).read_text(encoding="utf-8")131 (assets / checksum_name).write_text("0" * 64 + " wrong.dmg\n", encoding="utf-8")132 with self.assertRaisesRegex(VERIFIER.VerificationError, "checksum file"):133 VERIFIER.validate_assets(repo, "v0.1.0", assets, FakeGit())134 (assets / checksum_name).write_text(original_checksum, encoding="utf-8")135 manifest = json.loads((assets / manifest_name).read_text(encoding="utf-8"))136 manifest["notarized"] = False137 (assets / manifest_name).write_text(json.dumps(manifest), encoding="utf-8")138 with self.assertRaisesRegex(VERIFIER.VerificationError, "release manifest"):139 VERIFIER.validate_assets(repo, "v0.1.0", assets, FakeGit())140 self.write_assets_replacement(assets)141 (assets / "extra.txt").write_text("unexpected", encoding="utf-8")142 with self.assertRaisesRegex(VERIFIER.VerificationError, "exact Portal asset set"):143 VERIFIER.validate_assets(repo, "v0.1.0", assets, FakeGit())144145 def write_assets_replacement(self, assets: pathlib.Path) -> None:146 dmg_name, checksum_name, manifest_name = VERIFIER.expected_asset_names("v0.1.0")147 digest = hashlib.sha256((assets / dmg_name).read_bytes()).hexdigest()148 (assets / checksum_name).write_text(f"{digest} {dmg_name}\n", encoding="utf-8")149 manifest = VERIFIER.expected_release_manifest("v0.1.0", "a" * 40, digest)150 (assets / manifest_name).write_text(json.dumps(manifest), encoding="utf-8")151152 def test_remote_release_names_are_exact(self) -> None:153 expected = list(VERIFIER.expected_asset_names("v0.1.0"))154 self.assertEqual(155 VERIFIER.validate_release_asset_names("v0.1.0", list(reversed(expected))),156 sorted(expected),157 )158 with self.assertRaisesRegex(VERIFIER.VerificationError, "exact Portal asset set"):159 VERIFIER.validate_release_asset_names("v0.1.0", expected + ["extra"])160161 def test_website_manifest_fails_closed_and_accepts_exact_promoted_release(self) -> None:162 with tempfile.TemporaryDirectory() as temporary:163 path = pathlib.Path(temporary) / "portal-release.json"164 unavailable = {165 "format": 1,166 "product": "portal-client",167 "builds": {168 "macos-arm64": {169 "available": False,170 "reason": "No signed release has been promoted.",171 }172 },173 }174 path.write_text(json.dumps(unavailable), encoding="utf-8")175 VERIFIER.validate_website_manifest(path)176177 tag = "v0.1.0"178 asset, checksum, manifest = VERIFIER.expected_asset_names(tag)179 base = f"https://github.com/FelineStateMachine/goq/releases/download/{tag}"180 promoted = {181 "format": 1,182 "product": "portal-client",183 "builds": {184 "macos-arm64": {185 "architecture": "arm64",186 "asset": asset,187 "available": True,188 "checksum_asset": checksum,189 "checksum_url": f"{base}/{checksum}",190 "download_url": f"{base}/{asset}",191 "manifest_asset": manifest,192 "manifest_url": f"{base}/{manifest}",193 "platform": "macos",194 "release_tag": tag,195 "release_url": f"https://github.com/FelineStateMachine/goq/releases/tag/{tag}",196 "sha256": "1" * 64,197 "verification": VERIFIER.VERIFICATION,198 "version": "0.1.0",199 }200 },201 }202 path.write_text(json.dumps(promoted), encoding="utf-8")203 VERIFIER.validate_website_manifest(path)204 promoted["builds"]["macos-arm64"]["download_url"] = "https://example.invalid/dev.dmg"205 path.write_text(json.dumps(promoted), encoding="utf-8")206 with self.assertRaisesRegex(VERIFIER.VerificationError, "download URL"):207 VERIFIER.validate_website_manifest(path)208209 def test_workflow_keeps_secrets_protected_and_attaches_to_shared_draft(self) -> None:210 workflow = (REPO_DIR / ".github" / "workflows" / "portal-release.yml").read_text(encoding="utf-8")211 self.assertIn("environment:\n name: main", workflow)212 self.assertIn("workflow_dispatch:", workflow)213 self.assertIn("public-release-${{ inputs.tag }}", workflow)214 self.assertIn("release-assets", workflow)215 self.assertIn("Require the exact Sigil candidate draft", workflow)216 self.assertIn("five-asset pre-signature contract", workflow)217 self.assertIn("actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6", workflow)218 self.assertIn('test "$GITHUB_REF" = "refs/tags/$RELEASE_TAG"', workflow)219 self.assertIn("artifact-metadata: write", workflow)220 self.assertIn("attestations: write", workflow)221 self.assertIn("id-token: write", workflow)222 self.assertNotIn("release create", workflow)223 self.assertNotIn("--draft=false --prerelease", workflow)224 self.assertNotIn("demo-direct-node", workflow)225226 sigil_workflow = (REPO_DIR / ".github" / "workflows" / "sigil-release.yml").read_text(227 encoding="utf-8"228 )229 self.assertIn("complete six-asset public-alpha contract", sigil_workflow)230 self.assertIn("verify-portal-release.py assets", sigil_workflow)231 self.assertIn("runs-on: macos-26", sigil_workflow)232 self.assertIn("verify-macos-portal-signature.sh", sigil_workflow)233 self.assertIn("gh attestation verify", sigil_workflow)234 self.assertIn("--signer-workflow", sigil_workflow)235 self.assertIn("--source-ref", sigil_workflow)236 self.assertIn("--source-digest", sigil_workflow)237 self.assertIn("portal-apple-team-id.txt", sigil_workflow)238 self.assertLess(239 sigil_workflow.index("verify-portal-release.py assets"),240 sigil_workflow.index("--draft=false"),241 )242 self.assertLess(243 sigil_workflow.index("verify-macos-portal-signature.sh"),244 sigil_workflow.index("--draft=false"),245 )246 package = (REPO_DIR / "scripts" / "package-macos-client.sh").read_text(encoding="utf-8")247 self.assertIn("--target aarch64-apple-darwin", package)248 self.assertIn('[[ "$architectures" == arm64 ]]', package)249 self.assertNotIn("--expected-arch", package)250 self.assertNotIn("--features", package)251 self.assertIn("portal-apple-team-id.txt", package)252 self.assertIn('[[ "${APPLE_TEAM_ID:-}" == "$expected_team_id" ]]', package)253254 native_verifier = (REPO_DIR / "scripts" / "verify-macos-portal-signature.sh").read_text(255 encoding="utf-8"256 )257 self.assertIn("--expected-team-id", native_verifier)258 self.assertIn('[[ "$team_identifier" == "$expected_team_id" ]]', native_verifier)259260261if __name__ == "__main__":262 unittest.main()