#!/usr/bin/env bash # Build a signed release APK (arm64-v8a, R8-minified, resources shrunk), # create + push a semver tag via SSH, then print the Gitea release URL # for manual asset upload. # # First run: auto-generates a release keystore at ~/.config/browser-flow/ # Subsequent runs: reuses it. # # Auto-bumps minor of latest vX.Y.Z tag. Override with VERSION env. # # Usage: # ./scripts/release-apk.sh # VERSION=v0.5.0 ./scripts/release-apk.sh set -euo pipefail cd "$(dirname "$0")/.." export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17}" export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH" # ---- Keystore (auto-generate on first run) ------------------------------- KEY_DIR="$HOME/.config/browser-flow" KEY_FILE="$KEY_DIR/release.jks" ENV_FILE="$KEY_DIR/release.env" KEY_ALIAS="browser-flow-release" if [[ ! -f "$KEY_FILE" ]]; then echo "→ No keystore at ${KEY_FILE}, generating one..." mkdir -p "$KEY_DIR" STORE_PWD=$(openssl rand -base64 24 | tr -d '/=+' | cut -c1-24) KEY_PWD="$STORE_PWD" keytool -genkeypair -v \ -keystore "$KEY_FILE" \ -alias "$KEY_ALIAS" \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass "$STORE_PWD" \ -keypass "$KEY_PWD" \ -dname "CN=Browser Flow, OU=Apps, O=Gitean" \ >/dev/null cat > "$ENV_FILE" <&2 exit 1 fi echo "→ Repo: ${HOST} ${OWNER}/${REPO}" # ---- Determine next tag -------------------------------------------------- git fetch --tags --quiet origin || true if [[ -n "${VERSION:-}" ]]; then TAG="$VERSION" else LATEST=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \ | sort -t. -k1.2,1n -k2,2n -k3,3n \ | tail -1) LATEST="${LATEST:-v0.0.0}" IFS='.' read -r MAJ MIN PAT <<< "${LATEST#v}" TAG="v${MAJ}.$((MIN + 1)).0" echo "→ Latest tag: $LATEST bumping minor to ${TAG}" fi if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then echo "Tag ${TAG} already exists locally. Pass VERSION=... to override." >&2 exit 1 fi SHA="$(git rev-parse --short HEAD)" TS="$(date +%Y-%m-%d\ %H:%M)" VER_NO_V="${TAG#v}" # ---- Sync app version + write build-info --------------------------------- echo "→ Stamping version ${VER_NO_V} and commit ${SHA}..." # Bump app.json's expo.version so Constants.expoConfig.version matches the tag jq --arg v "$VER_NO_V" '.expo.version = $v' app.json > app.json.tmp && mv app.json.tmp app.json # Bump package.json version too (keeps everything aligned) jq --arg v "$VER_NO_V" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json # Generate constants/build-info.ts so the UI can show "v0.5.0 · abc123" cat > constants/build-info.ts </dev/null # Detect logical CPU count for Gradle worker pool CPUS=$(sysctl -n hw.logicalcpu 2>/dev/null || nproc 2>/dev/null || echo 8) echo "→ Gradle assembleRelease (arm64-v8a, signed, minified, ${CPUS} workers)..." ( cd android && ./gradlew assembleRelease \ -x lint -x test \ --parallel \ --build-cache \ --max-workers="${CPUS}" \ -Dorg.gradle.jvmargs="-Xmx6g -XX:+UseParallelGC -Dfile.encoding=UTF-8" \ -Dorg.gradle.workers.max="${CPUS}" \ -Pkotlin.incremental=true \ -Pkotlin.parallel.tasks.in.project=true \ -PreactNativeArchitectures=arm64-v8a \ -PenableProguardInReleaseBuilds=true \ -PenableShrinkResourcesInReleaseBuilds=true \ "-Pandroid.injected.signing.store.file=${BROWSER_FLOW_STORE_FILE}" \ "-Pandroid.injected.signing.store.password=${BROWSER_FLOW_STORE_PWD}" \ "-Pandroid.injected.signing.key.alias=${BROWSER_FLOW_KEY_ALIAS}" \ "-Pandroid.injected.signing.key.password=${BROWSER_FLOW_KEY_PWD}" ) # Locate the produced APK (release output path can vary slightly by AGP) APK_SRC=$(find android/app/build/outputs/apk/release -name '*.apk' -not -name '*unsigned*' | head -1) [[ -f "$APK_SRC" ]] || { echo "Release APK not found"; exit 1; } mkdir -p dist # Obtainium-friendly name: AppName-vX.Y.Z.apk. App name is read once from # app.json (expo.name) so renaming the app is a single-source change. APP_NAME="$(jq -r '.expo.name' app.json)" APK_OUT="dist/${APP_NAME}-${TAG}.apk" cp "$APK_SRC" "${APK_OUT}" SIZE=$(du -h "${APK_OUT}" | cut -f1) echo "→ Built: ${APK_OUT} (${SIZE})" # ---- Commit version bump + tag + push via SSH ---------------------------- echo "→ Committing release bump and tagging ${TAG}..." git add app.json package.json constants/build-info.ts # Only create a commit if there are actually staged changes (idempotent re-runs) if ! git diff --cached --quiet; then git commit -m "release: ${TAG}" >/dev/null fi git tag -a "${TAG}" -m "Release ${TAG} sha=${SHA} built=${TS}" echo "→ Pushing main + tag to origin (SSH)..." git push origin main git push origin "refs/tags/${TAG}" # ---- Create release + upload asset (Gitea API direct) -------------------- # We use curl rather than `tea` because tea sometimes fails silently and we # need explicit error handling / retry logic. TEA_CFG="$HOME/Library/Application Support/tea/config.yml" TOKEN=$(grep -m1 'token:' "$TEA_CFG" | awk '{print $2}') [[ -n "$TOKEN" ]] || { echo "Could not read tea token from $TEA_CFG"; exit 1; } NOTE="Signed release · arm64-v8a · R8 minified · sha=${SHA} · built=${TS}" echo "→ Creating Gitea release ${TAG}..." # Try lookup first (idempotency — tag may already have a release if a previous # run got partway through). REL_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \ "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/releases/tags/${TAG}") REL_ID=$(echo "$REL_JSON" | jq -r 'if type == "object" and .id then .id else empty end') if [[ -z "$REL_ID" ]]; then # Create new release BODY=$(jq -n --arg tag "$TAG" --arg name "Warden ${TAG}" --arg body "$NOTE" \ '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false, target_commitish:"main"}') REL_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \ -d "$BODY" \ "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/releases" \ | jq -r 'if type == "object" and .id then .id else empty end') fi [[ -n "$REL_ID" ]] || { echo "Could not create or resolve release for ${TAG}"; exit 1; } echo " release id: ${REL_ID}" echo "→ Uploading $(basename "${APK_OUT}") to release ${REL_ID}..." UPLOADED=0 for i in 1 2 3 4 5 6 7 8; do HTTP=$(curl -sS -o /tmp/tea_upload.json -w "%{http_code}" --max-time 300 \ -H "Authorization: token ${TOKEN}" \ -F "attachment=@${APK_OUT};type=application/vnd.android.package-archive" \ "https://${HOST}/api/v1/repos/${OWNER}/${REPO}/releases/${REL_ID}/assets?name=$(basename "${APK_OUT}")") if [[ "$HTTP" == "201" ]]; then echo "✓ Uploaded." UPLOADED=1 break fi # Exponential backoff: 4, 8, 12, 16, 20, 24, 28 seconds WAIT=$((i * 4)) echo " attempt $i: HTTP ${HTTP}, retrying in ${WAIT}s..." sleep "${WAIT}" done if [[ "$UPLOADED" != "1" ]]; then echo "⚠️ Upload failed after 8 attempts. Re-run later with:" echo " curl -H \"Authorization: token \$TOKEN\" -F \"attachment=@${APK_OUT}\" \\" echo " \"https://${HOST}/api/v1/repos/${OWNER}/${REPO}/releases/${REL_ID}/assets?name=$(basename "${APK_OUT}")\"" fi DOWNLOAD_URL="https://${HOST}/${OWNER}/${REPO}/releases/download/${TAG}/$(basename "${APK_OUT}")" echo echo "Done." echo echo "Tag: ${TAG}" echo "APK: $(pwd)/${APK_OUT} (${SIZE})" echo "Download: ${DOWNLOAD_URL}"