#!/usr/bin/env bash # Cuts a release: version bump, tests, signed build, tag, apk branch, Gitea # release. Every step was done by hand before, fifteen times in one day. # # tools/release.sh 0.11.6 --notes notes.md # tools/release.sh 0.11.6 --dry-run # show what would happen # tools/release.sh 0.11.6 --no-smoke # skip the emulator run # # Release notes are never generated: the commit subjects since the last tag # are only a starting point, written to a file for you to edit. Pass --notes # to supply them directly. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" REMOTE="${CASTARR_REMOTE:-gitea}" REPO="${CASTARR_REPO:-be-nj/castarr}" API="${CASTARR_API:-https://git.beckm4nn.net/api/v1}" SIGNING_ENV="${CASTARR_SIGNING_ENV:-$HOME/.keys/castarr-release.env}" GRADLE="./gradlew --no-daemon -q" VERSION="" NOTES_FILE="" DRY=0 SMOKE=1 while [ $# -gt 0 ]; do case "$1" in --notes) NOTES_FILE="$2"; shift 2 ;; --dry-run) DRY=1; shift ;; --no-smoke) SMOKE=0; shift ;; -*) echo "unbekannte Option: $1" >&2; exit 2 ;; *) VERSION="$1"; shift ;; esac done [ -n "$VERSION" ] || { echo "usage: $0 [--notes ] [--dry-run] [--no-smoke]" >&2; exit 2; } [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Version muss X.Y.Z sein, nicht '$VERSION'" >&2; exit 2; } say() { printf '\n== %s\n' "$1"; } run() { if [ "$DRY" = 1 ]; then printf ' would run: %s\n' "$*"; else "$@"; fi; } die() { printf 'ABBRUCH: %s\n' "$1" >&2; exit 1; } # --- Vorbedingungen --------------------------------------------------------- say "Prüfen" [ -z "$(git status --porcelain)" ] || die "Arbeitsbaum nicht sauber" [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ] || die "nicht auf main" git fetch -q "$REMOTE" main [ "$(git rev-parse HEAD)" = "$(git rev-parse "$REMOTE/main")" ] || die "main weicht von $REMOTE/main ab — erst pushen oder pullen" git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null && die "Tag v$VERSION existiert schon" CURRENT="$(sed -nE 's/.*versionName = "(.*)".*/\1/p' app/build.gradle.kts)" CODE="$(sed -nE 's/.*versionCode = ([0-9]+).*/\1/p' app/build.gradle.kts)" [ -n "$CURRENT" ] && [ -n "$CODE" ] || die "Version aus app/build.gradle.kts nicht lesbar" NEXT_CODE=$((CODE + 1)) echo " $CURRENT (code $CODE) -> $VERSION (code $NEXT_CODE)" # shellcheck source=tests/helpers/jdk.sh . tests/helpers/jdk.sh echo " JDK: $(basename "${JAVA_HOME:-system}")" # --- Notizen ---------------------------------------------------------------- LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)" if [ -z "$NOTES_FILE" ]; then NOTES_FILE="$(mktemp -t castarr-notes-XXXX.md)" { echo "# Notizen für $VERSION — diese Zeilen sind ein Entwurf, keine Release-Notes." echo "#" [ -n "$LAST_TAG" ] && echo "# Commits seit $LAST_TAG:" || echo "# Commits:" if [ -n "$LAST_TAG" ]; then git log --format='# %s' "$LAST_TAG..HEAD" else git log --format='# %s' -10 fi } > "$NOTES_FILE" if [ -n "${EDITOR:-}" ] && [ -t 0 ]; then "$EDITOR" "$NOTES_FILE" else say "Notizen" echo " Entwurf liegt in $NOTES_FILE." echo " Schreib die Notes dort hinein und ruf erneut auf:" echo " $0 $VERSION --notes $NOTES_FILE" exit 0 fi fi [ -f "$NOTES_FILE" ] || die "Notes-Datei fehlt: $NOTES_FILE" NOTES="$(grep -v '^#' "$NOTES_FILE" | sed -e '/./,$!d')" [ -n "${NOTES//[[:space:]]/}" ] || die "Notes sind leer" # --- Tests ------------------------------------------------------------------ say "Unit-Tests" run $GRADLE testDebugUnitTest if [ "$SMOKE" = 1 ]; then say "Smoke-Test auf dem Emulator" run tests/smoke.sh fi # --- Version bumpen --------------------------------------------------------- say "Version setzen" if [ "$DRY" = 0 ]; then sed -i -E "s/versionCode = $CODE/versionCode = $NEXT_CODE/; s/versionName = \"$CURRENT\"/versionName = \"$VERSION\"/" \ app/build.gradle.kts grep -q "versionName = \"$VERSION\"" app/build.gradle.kts || die "Bump hat nicht gegriffen" else echo " would set versionCode=$NEXT_CODE versionName=$VERSION" fi # --- Signierter Build ------------------------------------------------------- say "Release-APK bauen" if [ -f "$SIGNING_ENV" ]; then # Signing secrets stay in the file; only this shell sees them. set -a; . "$SIGNING_ENV"; set +a echo " signiert (Konfiguration aus $SIGNING_ENV)" else echo " WARNUNG: $SIGNING_ENV fehlt — der Build wäre unsigniert" [ "$DRY" = 1 ] || die "ohne Signatur kein Release (Updater lehnt Signaturwechsel ab)" fi run $GRADLE assembleRelease APK="app/build/outputs/apk/release/app-release.apk" [ "$DRY" = 1 ] || [ -f "$APK" ] || die "APK nicht gebaut: $APK" # --- Commit, Tag, Push ------------------------------------------------------ say "Commit und Tag" run git add app/build.gradle.kts run git commit -q -m "Release $VERSION" run git tag "v$VERSION" run git push -q "$REMOTE" main "v$VERSION" # --- APK-Branch ------------------------------------------------------------- say "APK auf den Branch apk" if [ "$DRY" = 0 ]; then git fetch -q "$REMOTE" apk WORKTREE="$(mktemp -d -t castarr-apk-XXXX)" trap 'git worktree remove --force "$WORKTREE" 2>/dev/null || true' EXIT git worktree add -q -B apk "$WORKTREE" "$REMOTE/apk" cp "$APK" "$WORKTREE/castarr.apk" echo "$VERSION" > "$WORKTREE/VERSION" git -C "$WORKTREE" add castarr.apk VERSION git -C "$WORKTREE" commit -q -m "castarr $VERSION" git -C "$WORKTREE" push -q "$REMOTE" apk git worktree remove --force "$WORKTREE" trap - EXIT else echo " would push $APK as castarr.apk" fi # --- Gitea-Release ---------------------------------------------------------- say "Gitea-Release" TOKEN="${GITEA_TOKEN:-${CASTARR_GITEA_TOKEN:-}}" if [ -z "$TOKEN" ]; then echo " Kein GITEA_TOKEN gesetzt — Release bitte in der Weboberfläche anlegen:" echo " ${API%/api/v1}/$REPO/releases/new?tag=v$VERSION" echo " Notes liegen in $NOTES_FILE" elif [ "$DRY" = 1 ]; then echo " would create release v$VERSION" else BODY="$(NOTES="$NOTES" python3 -c 'import json,os; print(json.dumps({ "tag_name": "v" + os.environ["V"], "name": os.environ["V"], "body": os.environ["NOTES"], "target_commitish": "main"}))' V="$VERSION")" curl -sS -X POST "$API/repos/$REPO/releases" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ -d "$BODY" > /dev/null || die "Release-API fehlgeschlagen" echo " angelegt: ${API%/api/v1}/$REPO/releases/tag/v$VERSION" fi say "Fertig: $VERSION" echo " Die App findet das Update über die Release-API, die APK über den Branch apk."