The 0.1.1 cask had three findings in the tap: no blank line between each disk image's sha256 and its url, and no depends_on :macos. The writer now puts both in, and manifests_test.py checks them. Written from the 0.1.1 release's own assets, the cask has no finding, and the Scoop manifest and latest.json are byte for byte the ones published. (A minimum version is not given: the Intel build states macOS 10.13, below what Homebrew supports, so brew style calls it redundant.) Co-Authored-By: Claude Opus 5.5 <[email protected]>
130 lines
6.5 KiB
Python
130 lines
6.5 KiB
Python
"""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)
|