Skip to content

Commit 1a45922

Browse files
ci(release): add verify-release.sh post-release gate (closes #26)
v4.4.7 stayed an unpublished GitHub draft (two orphaned "untagged" drafts), so the canonical download URL 404'd while the App Store entry (201) pointed at it — exactly the #23 failure class repeating. The manual checklist step 8 (unauth 200 check) existed already but was bypassed/false-checked twice. Add an executable gate that can't be falsely ticked off. verify-release.sh verifies in one run: release is non-draft and unique for the tag, canonical URL returns 200, App Store feed lists the version with matching download URL, the App Store signature verifies byte-exact against the published tarball, data/ is present, and info.xml matches the tag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1835a4d commit 1a45922

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ scripts/*
119119
!scripts/check-forbidden-names.test.sh
120120
!scripts/check-i18n-parity.sh
121121
!scripts/l10n_js_sync.py
122+
!scripts/verify-release.sh
122123
.env
123124
.env.*
124125
.ralph/

scripts/verify-release.sh

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/bin/bash
2+
# verify-release.sh <version> — Post-Release-Gate fuer den Nextcloud App Store.
3+
#
4+
# Maschinell statt manuell: faengt die Fehlerklassen, die App-Store-201 + integrity:check
5+
# NICHT bemerken und die in der Vergangenheit zu nicht-installierbaren Releases gefuehrt haben:
6+
# • Draft-Falle (#23 v4.4.6, #26 v4.4.7): GitHub-Release blieb Draft → kanonische Tag-URL 404,
7+
# obwohl der App-Store-Eintrag (201) darauf zeigt → `occ app:install` schlaegt fuer ALLE User fehl.
8+
# • Signatur-Mismatch: falscher/zweiter Build publiziert → NC verwirft die Signatur beim Install.
9+
# • Packaging-Bug (v4.4.5): `data/` fehlt im Tarball → App laedt, Features ohne Daten.
10+
# • Versions-Drift: info.xml != Release-Tag.
11+
#
12+
# Nach dem App-Store-POST (Checkliste Schritt 9) laufen lassen. Exit 0 = installierbar.
13+
#
14+
# Usage: ./scripts/verify-release.sh 4.4.7
15+
set -eo pipefail
16+
17+
VERSION="${1:?Usage: verify-release.sh <version, z.B. 4.4.7>}"
18+
REPO="${LEARNING_REPO:-andremadstop/learning-nc}"
19+
TAG="v${VERSION}"
20+
URL="https://github.com/${REPO}/releases/download/${TAG}/learning-${VERSION}.tar.gz"
21+
PLATFORM="${NC_PLATFORM:-33.0.0}" # App-Store-Feed-Version, den NC-Clients abfragen
22+
23+
fail() { echo "$1" >&2; exit 1; }
24+
25+
# Zertifikat finden (certs/ ist gitignored; Fallback auf globalen NC-Cert-Store)
26+
CERT="${LEARNING_CERT:-}"
27+
if [ -z "$CERT" ]; then
28+
for c in "$(dirname "$0")/../certs/learning.crt" "$HOME/.nextcloud/certificates/learning.crt"; do
29+
[ -f "$c" ] && CERT="$c" && break
30+
done
31+
fi
32+
33+
TMP="$(mktemp -d)"
34+
trap 'rm -rf "$TMP"' EXIT
35+
36+
echo "=== verify-release v$VERSION ($REPO) ==="
37+
38+
# 1. Genau EIN non-draft Release fuer den Tag (Draft-Leichen aufdecken)
39+
mapfile -t DRAFTS < <(gh api "repos/$REPO/releases" --jq ".[] | select(.tag_name==\"$TAG\") | .draft")
40+
[ "${#DRAFTS[@]}" -ge 1 ] || fail "Kein GitHub-Release mit Tag $TAG gefunden"
41+
[ "${#DRAFTS[@]}" -eq 1 ] || fail "${#DRAFTS[@]} Releases mit Tag $TAG (erwartet 1) — verwaiste Draft(s) loeschen"
42+
[ "${DRAFTS[0]}" = "false" ] || fail "GitHub-Release $TAG ist noch DRAFT → publizieren: gh release edit $TAG --draft=false --latest"
43+
echo "✓ GitHub-Release $TAG: non-draft, genau 1 Eintrag"
44+
45+
# 2. Kanonische Tag-URL liefert nach Redirect HTTP 200 (Draft-Falle gibt hier 404)
46+
CODE="$(curl -sIL -o /dev/null -w '%{http_code}' "$URL")"
47+
[ "$CODE" = "200" ] || fail "Kanonische Download-URL liefert HTTP $CODE (erwartet 200) — Draft-Falle? $URL"
48+
echo "✓ Kanonische URL: HTTP 200"
49+
50+
# 3. Publizierten Tarball laden
51+
curl -sL -o "$TMP/app.tar.gz" "$URL"
52+
[ -s "$TMP/app.tar.gz" ] || fail "Tarball-Download leer"
53+
54+
# 4. App-Store-Feed: Version vorhanden, download == kanonische URL, Signatur extrahieren
55+
curl -s "https://apps.nextcloud.com/api/v1/platform/${PLATFORM}/apps.json" -o "$TMP/feed.json"
56+
python3 - "$VERSION" "$TMP/feed.json" "$TMP/as.b64" "$URL" <<'PY' || fail "App-Store-Feed-Check fehlgeschlagen"
57+
import json, sys
58+
ver, feed, out, url = sys.argv[1:5]
59+
try:
60+
data = json.load(open(feed))
61+
except Exception as e:
62+
sys.exit(f"App-Store-Feed nicht parsebar: {e}")
63+
app = [a for a in data if a.get('id') == 'learning']
64+
if not app:
65+
sys.exit("App 'learning' nicht im App-Store-Feed")
66+
rel = [r for r in app[0].get('releases', []) if r['version'] == ver]
67+
if not rel:
68+
sys.exit(f"v{ver} nicht im App-Store-Feed (POST vergessen?)")
69+
r = rel[0]
70+
if r.get('download') != url:
71+
sys.exit(f"App-Store download-URL != kanonische URL:\n store: {r.get('download')}\n canon: {url}")
72+
sig = r.get('signature') or ''
73+
if not sig:
74+
sys.exit(f"Keine Signatur fuer v{ver} im App-Store-Feed")
75+
open(out, 'w').write(sig)
76+
PY
77+
echo "✓ App-Store-Feed: v$VERSION gelistet, download-URL == kanonische URL"
78+
79+
# 5. App-Store-Signatur verifiziert gegen den PUBLIZIERTEN Tarball (byte-genau)
80+
[ -n "$CERT" ] || fail "Kein Zertifikat gefunden (certs/learning.crt oder ~/.nextcloud/certificates/learning.crt)"
81+
openssl base64 -d -A < "$TMP/as.b64" > "$TMP/as.bin"
82+
openssl x509 -in "$CERT" -pubkey -noout > "$TMP/pub.pem" 2>/dev/null \
83+
|| fail "Public Key konnte nicht aus $CERT extrahiert werden"
84+
openssl dgst -sha512 -verify "$TMP/pub.pem" -signature "$TMP/as.bin" "$TMP/app.tar.gz" >/dev/null 2>&1 \
85+
|| fail "App-Store-Signatur passt NICHT zum publizierten Tarball — falscher Build publiziert? (NC verwirft das beim Install)"
86+
echo "✓ App-Store-Signatur == publizierter Tarball (Verified OK)"
87+
88+
# 6. Packaging-Gates: data/ vorhanden + info.xml-Version == Tag
89+
tar -xzf "$TMP/app.tar.gz" -C "$TMP"
90+
DATA_FILES="$(find "$TMP/learning/data" -type f 2>/dev/null | wc -l)"
91+
[ "$DATA_FILES" -gt 0 ] || fail "Tarball enthaelt KEIN data/ (Packaging-Bug wie v4.4.5)"
92+
INFO_VER="$(grep -oE '<version>[0-9.]+</version>' "$TMP/learning/appinfo/info.xml" | head -1 | grep -oE '[0-9.]+')"
93+
[ "$INFO_VER" = "$VERSION" ] || fail "info.xml-Version ($INFO_VER) != $VERSION"
94+
echo "✓ Packaging: data/ vorhanden ($DATA_FILES files), info.xml == $VERSION"
95+
96+
echo ""
97+
echo "✅ Release v$VERSION ist installierbar — alle Gates gruen."

0 commit comments

Comments
 (0)