Files
tessera-ctl/.gitea/scripts/publish-release.sh
T
schalli 75a8e40587 feat(18-02): Task 2 — Release-Dateien idempotent an den Gitea-Release haengen
- upload_asset() nach dem bestehenden GET-then-PATCH/POST-Idempotenzmuster:
  GET .../assets -> vorhandene Datei gleichen Namens per DELETE entfernen ->
  frisch per POST multipart (Feld attachment) hochladen
- Zweite Header-Datei HDR_AUTH (nur Authorization, kein JSON-Content-Type) fuer
  den multipart-Upload; Token bleibt in Header-Dateien, nie als Argument (T-18-03)
- Nach Release-Anlage/-Aktualisierung wird jede Datei aus desktop-dist/manifest.json
  hochgeladen; fehlt das Manifest bei einem Freigabe-Tag, bricht das Skript hart ab
- --dry-run listet zusaetzlich die geplanten Uploads aus dem Manifest, ohne Netzaufruf

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 16:21:17 +02:00

229 lines
8.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# publish-release.sh -- Gitea-Release je Freigabe-Tag aus CHANGELOG.md anlegen
# (quick-260916-dcz).
#
# Entscheidung wie publish-images.sh allein anhand GITHUB_REF:
# refs/tags/vX.Y.Z -> Abschnitt "## X.Y.Z" aus CHANGELOG.md schneiden und als
# Release "Tessera X.Y.Z" anlegen (bzw. aktualisieren, wenn
# der Release zum Tag schon existiert -- idempotent)
# alles andere -> nichts zu tun (Exit 0)
#
# Aufrufformen:
# sh .gitea/scripts/publish-release.sh # im CI, Tag aus GITHUB_REF
# sh .gitea/scripts/publish-release.sh --tag v1.0.0 # lokal, expliziter Tag
# sh .gitea/scripts/publish-release.sh --dry-run --tag v1.0.0 # nur JSON und Ziel zeigen
#
# Umgebung:
# GITEA_TOKEN Zugriffstoken (Pflicht im echten Lauf; im CI aus secrets.REGISTRY_TOKEN
# ueber `env`). Wird nie ausgegeben und nie als Argument uebergeben --
# der Authorization-Header kommt aus einer temporaeren Datei.
# GITEA_API API-Basis; sonst GITHUB_API_URL, sonst GITHUB_SERVER_URL/api/v1,
# sonst http://localhost:3002/api/v1 (nur lokal erreichbar).
# GITEA_REPO owner/repo; sonst GITHUB_REPOSITORY, sonst schalli/tessera-ctl.
# CHANGELOG_FILE Pfad zur Aenderungsliste; Vorgabe CHANGELOG.md.
# DESKTOP_DIST Ordner mit den Desktop-Paketen und manifest.json (Phase 18,
# 18-02); Vorgabe desktop-dist. Fehlt manifest.json bei Tags,
# bricht das Skript ab -- der Release-Text ist dann schon
# angelegt/aktualisiert, der Job wird sichtbar rot.
#
# Fehlt der Abschnitt fuer die Version, endet das Skript mit Exit 1 -- es entsteht
# nie ein leerer Release. JSON wird ausschliesslich mit jq gebaut.
#
# Release-Dateien (Phase 18, 18-02): jede Datei aus manifest.json wird idempotent
# angehaengt -- GET .../releases/{id}/assets, vorhandene Datei gleichen Namens per
# DELETE .../releases/{id}/assets/{asset_id} entfernen, dann frisch per
# POST .../releases/{id}/assets?name=... (multipart-Feld attachment) hochladen.
# Der multipart-Upload braucht eine zweite Header-Datei ($HDR_AUTH) OHNE
# Content-Type: application/json -- curl setzt den multipart-Content-Type sonst
# nicht korrekt, wenn der JSON-Header schon gesetzt ist.
set -eu
usage() {
echo "Aufruf: publish-release.sh [--dry-run] [--tag vX.Y.Z]" >&2
}
DRY_RUN=0
TAG=""
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--tag)
[ $# -ge 2 ] || { usage; exit 2; }
TAG="$2"
shift
;;
*) usage; exit 2 ;;
esac
shift
done
if [ -z "$TAG" ]; then
REF="${GITHUB_REF:-}"
case "$REF" in
refs/tags/v*) TAG="${REF#refs/tags/}" ;;
*)
echo "Kein Freigabe-Tag (nur refs/tags/v*): nichts zu tun."
exit 0
;;
esac
fi
if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Ungueltiger Tag '$TAG' (erwartet vX.Y.Z)." >&2
exit 1
fi
VERSION="${TAG#v}"
API="${GITEA_API:-${GITHUB_API_URL:-${GITHUB_SERVER_URL:+${GITHUB_SERVER_URL}/api/v1}}}"
API="${API:-http://localhost:3002/api/v1}"
API="${API%/}"
REPO="${GITEA_REPO:-${GITHUB_REPOSITORY:-schalli/tessera-ctl}}"
echo "Gitea-API: $API Repo: $REPO Tag: $TAG"
CHANGELOG="${CHANGELOG_FILE:-CHANGELOG.md}"
if [ ! -f "$CHANGELOG" ]; then
echo "$CHANGELOG nicht gefunden." >&2
exit 1
fi
command -v jq >/dev/null 2>&1 || { echo "jq fehlt." >&2; exit 1; }
# Abschnitt "## X.Y.Z" bis zur naechsten "## "-Ueberschrift, ohne die eigene
# Ueberschrift; danach fuehrende und abschliessende Leerzeilen entfernen.
BODY=$(awk -v ver="$VERSION" '
BEGIN { esc = ver; gsub(/\./, "\\.", esc); pat = "^## " esc "( |$)" }
$0 ~ pat { f = 1; next }
/^## / { if (f) exit }
f { print }
' "$CHANGELOG" | awk '
{ line[NR] = $0; if ($0 !~ /^[[:space:]]*$/) last = NR }
END { for (i = 1; i <= last; i++) print line[i] }
' | sed '1{/^$/d}')
if [ -z "$BODY" ]; then
echo "$CHANGELOG hat keinen Abschnitt fuer Version $VERSION (erwartet eine Zeile '## $VERSION – <Datum>'). Kein Release ohne Text." >&2
exit 1
fi
NAME="Tessera $VERSION"
CREATE_JSON=$(jq -n --arg tag "$TAG" --arg name "$NAME" --arg body "$BODY" \
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
UPDATE_JSON=$(jq -n --arg name "$NAME" --arg body "$BODY" '{name: $name, body: $body}')
RELEASES_URL="$API/repos/$REPO/releases"
TAG_URL="$API/repos/$REPO/releases/tags/$TAG"
DESKTOP_DIST="${DESKTOP_DIST:-desktop-dist}"
MANIFEST="$DESKTOP_DIST/manifest.json"
if [ "$DRY_RUN" -eq 1 ]; then
echo "Probelauf (kein Netzaufruf):"
echo " POST $RELEASES_URL"
echo " PATCH $RELEASES_URL/<id> (falls GET $TAG_URL bereits 200 liefert)"
printf '%s\n' "$CREATE_JSON"
if [ -f "$MANIFEST" ]; then
for FNAME in $(jq -r '.files[].name' "$MANIFEST"); do
echo " POST $RELEASES_URL/<id>/assets?name=$FNAME"
done
else
echo " ($MANIFEST fehlt -- keine geplanten Uploads im Probelauf)"
fi
exit 0
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "Kein Zugriffstoken in der Umgebung gesetzt (siehe Kopfkommentar)." >&2
exit 1
fi
umask 077
TMPDIR_REL=$(mktemp -d)
trap 'rm -rf "$TMPDIR_REL"' EXIT INT TERM
HDR="$TMPDIR_REL/headers"
HDR_AUTH="$TMPDIR_REL/headers-auth"
RESP="$TMPDIR_REL/response.json"
ASSETS_RESP="$TMPDIR_REL/assets.json"
JSONFILE="$TMPDIR_REL/payload.json"
printf 'Authorization: token %s\nContent-Type: application/json\n' "$GITEA_TOKEN" > "$HDR"
# Zweite Header-Datei ohne Content-Type: application/json -- der multipart-Upload
# (POST .../assets) darf keinen JSON-Content-Type mitbekommen.
printf 'Authorization: token %s\n' "$GITEA_TOKEN" > "$HDR_AUTH"
# upload_asset FILE NAME RELEASE_ID -- idempotent: vorhandene Datei gleichen Namens
# wird zuerst entfernt (GET -> DELETE), dann frisch hochgeladen (POST multipart).
upload_asset() {
ASSET_FILE="$1"
ASSET_NAME="$2"
ASSET_RELEASE_ID="$3"
ASSETS_CODE=$(curl -sS --header @"$HDR" -o "$ASSETS_RESP" -w '%{http_code}' "$RELEASES_URL/$ASSET_RELEASE_ID/assets")
if [ "$ASSETS_CODE" != "200" ]; then
echo "GET $RELEASES_URL/$ASSET_RELEASE_ID/assets antwortete mit $ASSETS_CODE:" >&2
cat "$ASSETS_RESP" >&2
exit 1
fi
EXISTING_ID=$(jq -r --arg n "$ASSET_NAME" '.[] | select(.name == $n) | .id' "$ASSETS_RESP")
if [ -n "$EXISTING_ID" ]; then
DEL_CODE=$(curl -sS --header @"$HDR" -X DELETE -o "$RESP" -w '%{http_code}' "$RELEASES_URL/$ASSET_RELEASE_ID/assets/$EXISTING_ID")
if [ "$DEL_CODE" != "204" ]; then
echo "DELETE $RELEASES_URL/$ASSET_RELEASE_ID/assets/$EXISTING_ID antwortete mit $DEL_CODE:" >&2
cat "$RESP" >&2
exit 1
fi
fi
UPLOAD_CODE=$(curl -sS --header @"$HDR_AUTH" -X POST -F "attachment=@${ASSET_FILE};filename=${ASSET_NAME}" -o "$RESP" -w '%{http_code}' "$RELEASES_URL/$ASSET_RELEASE_ID/assets?name=$ASSET_NAME")
if [ "$UPLOAD_CODE" != "201" ]; then
echo "POST $RELEASES_URL/$ASSET_RELEASE_ID/assets?name=$ASSET_NAME antwortete mit $UPLOAD_CODE:" >&2
cat "$RESP" >&2
exit 1
fi
}
CODE=$(curl -sS --header @"$HDR" -o "$RESP" -w '%{http_code}' "$TAG_URL")
case "$CODE" in
200)
ID=$(jq -r .id "$RESP")
printf '%s' "$UPDATE_JSON" > "$JSONFILE"
CODE=$(curl -sS --header @"$HDR" -X PATCH --data @"$JSONFILE" -o "$RESP" -w '%{http_code}' "$RELEASES_URL/$ID")
if [ "$CODE" = "200" ]; then
echo "Release $TAG aktualisiert (id $ID)"
else
echo "PATCH $RELEASES_URL/$ID antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
fi
;;
404)
printf '%s' "$CREATE_JSON" > "$JSONFILE"
CODE=$(curl -sS --header @"$HDR" -X POST --data @"$JSONFILE" -o "$RESP" -w '%{http_code}' "$RELEASES_URL")
if [ "$CODE" = "201" ]; then
ID=$(jq -r .id "$RESP")
echo "Release $TAG angelegt (id $ID)"
else
echo "POST $RELEASES_URL antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
fi
;;
*)
echo "GET $TAG_URL antwortete mit $CODE:" >&2
cat "$RESP" >&2
exit 1
;;
esac
# Release-Dateien aus dem Manifest anhaengen (Phase 18, 18-02). Der Release-Text
# ist an dieser Stelle bereits angelegt/aktualisiert -- fehlt das Manifest, wird
# das trotzdem hart abgebrochen (kein Release ohne Pakete bei einem Freigabe-Tag).
if [ ! -f "$MANIFEST" ]; then
echo "$MANIFEST fehlt -- kein Release ohne Desktop-Pakete." >&2
exit 1
fi
for FNAME in $(jq -r '.files[].name' "$MANIFEST"); do
upload_asset "$DESKTOP_DIST/$FNAME" "$FNAME" "$ID"
echo "Release-Datei $FNAME hochgeladen"
done