Simpl-Resultat/.claude/skills/release/scripts/verify-release.sh
le king fu da7129d529
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m54s
fix: give rpm installs their own key in latest.json
latest.json only had linux-x86_64, built from the .deb. An .rpm install
looks up linux-x86_64-rpm first, then fell back on linux-x86_64,
downloaded the .deb and refused it (InvalidUpdaterFormat).

- scripts/build-latest-json.sh writes linux-x86_64-deb,
  linux-x86_64-rpm, linux-x86_64 (the .deb, as a fallback) and
  windows-x86_64 from the release assets. It fails unless it finds
  exactly one .deb, one .rpm and one -setup.exe, each with a non-empty
  .sig. The JSON comes from jq alone, notes read with --rawfile, file
  names encoded with @uri like the upload step.
- scripts/build-latest-json.test.mjs (vitest) runs it under
  bash -e -o pipefail on a temporary assets dir; check-frontend
  installs jq.
- release.yml calls the script, writes the notes output under a random
  delimiter, checks the manifest with jq before the registry DELETE,
  and names the .rpm instead of an AppImage in the release notes.
- verify-release.sh requires the four keys, the extension of each URL
  and the minisign file: of each signature, and exits 1 on any failure.
- Docs: release skill, qa-update-cycle section 3 (rpm procedure, keys,
  reason and removal condition of the fallback), architecture, README,
  CHANGELOG, STATE.

Resolves #320

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:49:12 -04:00

109 lines
4.8 KiB
Bash
Executable file

#!/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"