The rate limit, the version compare and the download check were all wrapped in Android — a Context, a socket, a file — so none of them had a test, and all three shipped broken: the remote thrown out after five reconnects (0.11.1), a truncated APK handed to the installer (0.11.5), and a version compare that would read 0.11.10 as older than 0.11.9 the moment we get there. They are now plain Kotlin in AttemptBudget and UpdateRules, with the clock injected, and 20 tests covering the failures themselves. AttemptBudget also prunes expired entries before evicting: dropping only empty queues let the map grow without bound while every tracked address held one fresh failure. tests/smoke.sh installs the debug APK on the headless emulator, walks the first screen with the D-pad and fails on a crash or ANR — the class of bug unit tests cannot see, and the reason 0.11.0 crashed on startup. Forty seconds end to end. tools/release.sh does the bump, tests, signed build, tag, apk branch and Gitea release in one command. It refuses an unsigned build and never writes the release notes itself; commit subjects are offered as a draft. tests/helpers/jdk.sh picks a JDK Gradle can run on: Ubuntu moved default-java to 25, which fails the build with a bare "IllegalArgumentException: 25.0.4". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 lines
6.7 KiB
Bash
Executable File
172 lines
6.7 KiB
Bash
Executable File
#!/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 <version> [--notes <file>] [--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."
|