"""release/manifests.py against releases made up here, without a network: python3 release/manifests_test.py - every platform: the cask names each disk image with its SHA-256 and says the app updates itself; the Scoop manifest names the archive, its SHA-256 and its folder; the update manifest carries each update's own signature under the names the updater asks for, and an update with no signature is left out; - no update key: no update manifest, and a cask that does not say the app updates itself; - a notarised build: no caveat; - nothing attached: a failure. Exit 0 iff every check passes. """ import json import os import pathlib import shutil import subprocess import sys import tempfile here = pathlib.Path(__file__).resolve().parent fails = 0 def check(ok, what): global fails print(f"{'PASS' if ok else 'FAIL'} {what}") if not ok: fails += 1 V = "0.3.0" TAG = f"wai-quantum-desktop-v{V}" PUBLIC = "https://forge.example/Org/repo" def release(files): """A folder of assets: each name with its bytes, its .sha256 and, when asked, its .sig.""" d = pathlib.Path(tempfile.mkdtemp(prefix="wqd-assets-")) for name, signed in files: (d / name).write_text(f"bytes of {name}") (d / f"{name}.sha256").write_text(f"{('%064x' % abs(hash(name)))[:64]} {name}\n") if signed: (d / f"{name}.sig").write_text(f"c2lnIG9m{name}\n") return d def run(assets, env=None): out = pathlib.Path(tempfile.mkdtemp(prefix="wqd-out-")) e = {k: v for k, v in os.environ.items() if k not in ("FORGE_TOKEN", "MACOS_NOTARISED")} e.update({"ASSETS_DIR": str(assets)}, **(env or {})) r = subprocess.run([sys.executable, "-I", str(here / "manifests.py"), V, TAG, "http://forge.invalid/api/v1", "Org/repo", PUBLIC, str(out)], env=e, capture_output=True, text=True) return r, out def sha(assets, name): return (assets / f"{name}.sha256").read_text().split()[0] P = f"wai-quantum-desktop-{V}-" every = [ (P + "aarch64-apple-darwin.dmg", False), (P + "aarch64-apple-darwin.app.tar.gz", True), (P + "x86_64-apple-darwin.dmg", False), (P + "x86_64-apple-darwin.app.tar.gz", False), (P + "x86_64-unknown-linux-gnu.AppImage", True), (P + "x86_64-unknown-linux-gnu.deb", True), (P + "x86_64-unknown-linux-gnu.rpm", True), (P + "x86_64-pc-windows-msvc-setup.exe", True), (P + "x86_64-pc-windows-msvc.zip", False), ] # Every platform. a = release(every) r, out = run(a) check(r.returncode == 0, f"every platform: it runs ({r.stdout.strip()} {r.stderr.strip()})") cask = (out / "wai-quantum-desktop.rb").read_text() for cpu, triple in (("arm", "aarch64-apple-darwin"), ("intel", "x86_64-apple-darwin")): name = P + triple + ".dmg" check(f'on_{cpu} do\n sha256 "{sha(a, name)}"\n\n url "{PUBLIC}/releases/download/{TAG}/{name}",' in cask, f"the cask names the {triple} disk image with its SHA-256") check(f'version "{V}"' in cask and 'app "Quantum Studio.app"' in cask and "auto_updates true\n depends_on :macos\n" in cask and "xattr -dr com.apple.quarantine" in cask, "the cask: its version, the app, that it updates itself, that it is for macOS, and the caveat for an app signed ad hoc") check('verified: "forge.example/Org/repo/"' in cask, "the cask says where its downloads are verified from") check("\n\n\n" not in cask and cask.endswith(" EOS\nend\n"), "the cask has no run of blank lines, and ends with its caveat") if shutil.which("ruby"): p = out / "wai-quantum-desktop.rb" rc = subprocess.run(["ruby", "-c", str(p)], capture_output=True, text=True) check(rc.returncode == 0, f"the cask is Ruby ({rc.stdout.strip()}{rc.stderr.strip()})") else: print("note: no ruby here to parse the cask") scoop = json.loads((out / "wai-quantum-desktop.json").read_text()) win = P + "x86_64-pc-windows-msvc" check(scoop["version"] == V and scoop["architecture"]["64bit"] == {"url": f"{PUBLIC}/releases/download/{TAG}/{win}.zip", "hash": sha(a, win + ".zip"), "extract_dir": win} and scoop["shortcuts"] == [["wai-quantum-desktop.exe", "Quantum Studio"]], f"the Scoop manifest names the archive, its SHA-256 and its folder ({scoop['architecture']})") latest = json.loads((out / "latest.json").read_text()) want = {"darwin-aarch64": P + "aarch64-apple-darwin.app.tar.gz", "linux-x86_64": P + "x86_64-unknown-linux-gnu.AppImage", "linux-x86_64-appimage": P + "x86_64-unknown-linux-gnu.AppImage", "linux-x86_64-deb": P + "x86_64-unknown-linux-gnu.deb", "linux-x86_64-rpm": P + "x86_64-unknown-linux-gnu.rpm", "windows-x86_64": P + "x86_64-pc-windows-msvc-setup.exe", "windows-x86_64-nsis": P + "x86_64-pc-windows-msvc-setup.exe"} check(sorted(latest["platforms"]) == sorted(want), f"the update manifest has an update for each signed platform and no other ({sorted(latest['platforms'])})") check(all(latest["platforms"][k] == {"signature": (a / f"{n}.sig").read_text().strip(), "url": f"{PUBLIC}/releases/download/{TAG}/{n}"} for k, n in want.items()), "each update carries its own signature and its public address") check(latest["version"] == V and "darwin-x86_64" not in latest["platforms"] and "has no update signature" in r.stderr, "an update with no signature is left out, and the log says so") # No update key: nothing signed. b = release([(n, False) for n, _ in every]) r, out = run(b) cask = (out / "wai-quantum-desktop.rb").read_text() check(r.returncode == 0 and not (out / "latest.json").exists() and "auto_updates" not in cask and "\n\n\n" not in cask and "no update is signed" in r.stderr, "with no update key: no update manifest, and the cask does not say the app updates itself") # A notarised build: no caveat. r, out = run(a, {"MACOS_NOTARISED": "true"}) cask = (out / "wai-quantum-desktop.rb").read_text() check("caveats" not in cask and cask.endswith(" ]\nend\n"), "a notarised build's cask has no caveat") # Only Windows attached: the Scoop manifest alone, and the missing ones named. c = release([(P + "x86_64-pc-windows-msvc.zip", False)]) r, out = run(c) check(r.returncode == 0 and (out / "wai-quantum-desktop.json").exists() and not (out / "wai-quantum-desktop.rb").exists() and "aarch64-apple-darwin.dmg is not attached" in r.stderr, "a platform that is not attached is left out, and the log names it") # Nothing attached. d = release([]) r, out = run(d) check(r.returncode != 0 and "no installer is attached" in r.stderr, "with nothing attached, it fails") for x in (a, b, c, d): shutil.rmtree(x, ignore_errors=True) print(f"{fails} check(s) failed" if fails else "every check passed") sys.exit(1 if fails else 0)