#!/usr/bin/env bash # Step 9 of /release: verify the published Forgejo release for a tag — # the 7 expected assets and the content of latest.json (drives auto-update). # Exits 1 when any check fails (#320), with a VERDICT as last line. # # Usage: verify-release.sh vX.Y.Z [latest.json] # The optional second argument checks that local file instead of the # release's latest.json asset, to prove the checks on crafted files. set -uo pipefail TAG="${1:?usage: verify-release.sh vX.Y.Z [latest.json]}" LOCAL_LATEST="${2:-}" VERSION="${TAG#v}" TOKEN=$(cat ~/.forgejo-token) API="https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-resultat" WORK="${TMPDIR:-/tmp}/verify-release-${TAG}"; mkdir -p "$WORK" OUT="$WORK/latest.json" STATUS=0 echo "=== release ${TAG} ===" timeout 20 curl -s -H "Authorization: token $TOKEN" "${API}/releases/tags/${TAG}" > "$WORK/release.json" python3 - "$WORK/release.json" "$VERSION" <<'EOF' || STATUS=1 import sys, json r = json.load(open(sys.argv[1])); version = sys.argv[2] if "id" not in r: print("PAS DE RELEASE :", r); sys.exit(1) print(f"name={r.get('name')!r} tag={r.get('tag_name')} draft={r.get('draft')} prerelease={r.get('prerelease')} assets={len(r.get('assets', []))}") names = {} for a in r.get("assets", []): n = a["name"]; s = a["size"]; names[n] = a["browser_download_url"] print(f" {n} {s/1_000_000:.1f} MB" if s > 100_000 else f" {n} {s} B") # Naming conventions vary by Tauri version — report by suffix rather than failing on exact names. kinds = {"exe": 0, "exe.sig": 0, "deb": 0, "deb.sig": 0, "rpm": 0, "rpm.sig": 0, "latest.json": 0} for n in names: for k in ("exe.sig", "deb.sig", "rpm.sig"): if n.endswith("." + k): kinds[k] += 1; break else: for k in ("exe", "deb", "rpm"): if n.endswith("." + k): kinds[k] += 1; break else: if n == "latest.json": kinds["latest.json"] += 1 missing = [k for k, v in kinds.items() if v == 0] print("couverture:", kinds, "| MANQUANT:" if missing else "| complet", missing if missing else "") open(sys.argv[1] + ".latest-url", "w").write(names.get("latest.json") or "") sys.exit(1 if missing else 0) EOF if [ -n "$LOCAL_LATEST" ]; then OUT="$LOCAL_LATEST" echo; echo "=== latest.json (fichier local $OUT, pas l'asset de la release) ===" else URL=$(cat "$WORK/release.json.latest-url" 2>/dev/null) if [ -z "$URL" ]; then echo "latest.json absent — arrêt"; echo "VERDICT : ÉCHEC"; exit 1; fi echo; echo "=== latest.json ($URL) ===" timeout 20 curl -sL "$URL" > "$OUT" fi python3 - "$OUT" "$VERSION" <<'EOF' || STATUS=1 import sys, json, base64, binascii from urllib.parse import unquote, urlsplit j = json.load(open(sys.argv[1])); version = sys.argv[2] ok = True v = j.get("version"); pub = j.get("pub_date"); notes = j.get("notes", "") print(f"version={v} {'OK' if v == version else 'MAUVAISE VERSION'} | pub_date={pub}") ok &= v == version print(f"notes: {len(notes)} caractères — début: {notes[:120]!r}") # The four keys (#320): tauri-plugin-updater looks up "{os}-{arch}-{installer}", # then "{os}-{arch}". Each with the extension its URL must end with. EXPECTED = {"linux-x86_64": ".deb", "linux-x86_64-deb": ".deb", "linux-x86_64-rpm": ".rpm", "windows-x86_64": ".exe"} def signed_file(sig): """The file: field of the trusted comment, in the base64 minisign signature.""" try: text = base64.b64decode(sig, validate=True).decode("utf-8") except (binascii.Error, ValueError): return None for line in text.splitlines(): if line.startswith("trusted comment: ") and "\tfile:" in line: return line.split("\tfile:", 1)[1] return None platforms = j.get("platforms", {}) for plat in sorted(set(platforms) | set(EXPECTED)): if plat not in platforms: print(f" {plat}: MANQUANT"); ok = False; continue if plat not in EXPECTED: print(f" {plat}: clé INATTENDUE"); ok = False; continue p = platforms[plat] sig = p.get("signature") or ""; url = p.get("url") or "" name = unquote(urlsplit(url).path.rsplit("/", 1)[-1]) # file name, decoded ext = EXPECTED[plat]; signed = signed_file(sig) if sig else None checks = { "extension": name.endswith(ext), "signature": bool(sig), "file:": signed == name, "version": version in url, } ok &= all(checks.values()) print(f" {plat}: url={name}" f" | extension {ext} {'OK' if checks['extension'] else 'MAUVAISE EXTENSION'}" f" | signature {'OK (' + str(len(sig)) + ' car.)' if sig else 'VIDE'}" f" | file: {'OK' if checks['file:'] else 'DIFFÉRENT (' + repr(signed) + ')'}" f" | url contient version={'oui' if checks['version'] else 'NON'}") sys.exit(0 if ok else 1) EOF echo if [ "$STATUS" -eq 0 ]; then echo "VERDICT : OK"; else echo "VERDICT : ÉCHEC"; fi exit "$STATUS"