diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 717d44f..cdae170 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -1,9 +1,9 @@ -name: Build Android APK +name: Release APK on: push: tags: - - 'v*' + - 'v*.*.*' workflow_dispatch: concurrency: @@ -12,7 +12,9 @@ concurrency: jobs: build: + if: github.server_url == 'https://github.com' runs-on: ubuntu-latest + timeout-minutes: 60 permissions: contents: write @@ -24,10 +26,14 @@ jobs: sudo rm -rf /usr/share/swift /usr/local/julia* sudo apt-get autoremove -y > /dev/null 2>&1 sudo apt-get clean - df -h - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: actions/setup-node@v4 with: node-version: '20' @@ -36,10 +42,15 @@ jobs: - name: Install dependencies run: npm ci - - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Accept Android SDK licenses + run: yes | sdkmanager --licenses || true + + - name: Install SDK components + run: | + sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" - name: Cache Gradle uses: actions/cache@v4 @@ -47,31 +58,65 @@ jobs: path: | ~/.gradle/caches ~/.gradle/wrapper - key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: gradle- + key: ${{ runner.os }}-gradle-${{ hashFiles('package.json') }} + restore-keys: | + ${{ runner.os }}-gradle- - - name: Generate Android native project - run: npx expo prebuild --platform android --clean - - - name: Build debug APK - working-directory: android + - name: Decode keystore run: | - chmod +x gradlew - ./gradlew assembleDebug --no-daemon + mkdir -p ~/.config/sheethappens + echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > ~/.config/sheethappens/sheethappens.jks + cat > ~/.config/sheethappens/signing.env <> "$GITHUB_OUTPUT" + + - name: Generate changelog + id: changelog + run: | + PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${{ github.ref_name }}$" | head -1) + echo "Previous tag: $PREV_TAG" + + FEATS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \ + | grep -E "^feat(\([^)]+\))?: " \ + | sed -E 's/^feat(\([^)]+\))?: //' \ + | sed 's/^/- /') + + FIXES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"%s" --no-merges \ + | grep -E "^fix(\([^)]+\))?: " \ + | sed -E 's/^fix(\([^)]+\))?: //' \ + | sed 's/^/- /') + + { + echo "changelog<> "$GITHUB_OUTPUT" - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release create ${{ github.ref_name }} \ - android/app/build/outputs/apk/debug/app-debug.apk#SheetHappens-${{ github.ref_name }}.apk \ - --title "SheetHappens ${{ github.ref_name }}" \ - --generate-notes + uses: softprops/action-gh-release@v2 + with: + name: "SheetHappens ${{ github.ref_name }}" + body: | + ## SheetHappens ${{ github.ref_name }} + + ${{ steps.changelog.outputs.changelog }} + ### Installation + 1. Enable "Unknown sources" on the device + 2. Transfer the APK to the device and open it to install + files: ${{ steps.apk.outputs.path }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/build-apk.sh b/build-apk.sh new file mode 100755 index 0000000..5544ea5 --- /dev/null +++ b/build-apk.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# build-apk.sh — Build a signed release APK locally without expo.dev / EAS. +# +# Prerequisites (first time only): +# 1. Generate a keystore: +# keytool -genkey -v -keystore ~/.config/sheethappens/sheethappens.jks \ +# -alias sheethappens -keyalg RSA -keysize 2048 -validity 10000 +# 2. Fill in signing credentials: +# mkdir -p ~/.config/sheethappens +# cat > ~/.config/sheethappens/signing.env <&2; exit 1; } + +# ─── 1. Signing credentials ──────────────────────────────────────────────── +if [ ! -f "$SIGNING_ENV" ]; then + abort "Missing signing.env — see comments at top of this script." +fi +# shellcheck source=/dev/null +source "$SIGNING_ENV" + +[ -z "${KEYSTORE_PATH:-}" ] && abort "KEYSTORE_PATH not set in signing.env" +[ -z "${KEYSTORE_ALIAS:-}" ] && abort "KEYSTORE_ALIAS not set in signing.env" +[ -z "${KEYSTORE_STORE_PASSWORD:-}" ] && abort "KEYSTORE_STORE_PASSWORD not set in signing.env" +[ -z "${KEYSTORE_KEY_PASSWORD:-}" ] && abort "KEYSTORE_KEY_PASSWORD not set in signing.env" + +KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH/#\$HOME/$HOME}" +KEYSTORE_PATH_EXPANDED="${KEYSTORE_PATH_EXPANDED/#~/$HOME}" + +[ ! -f "$KEYSTORE_PATH_EXPANDED" ] && \ + abort "Keystore not found: $KEYSTORE_PATH_EXPANDED" + +# ─── 2. Java check ───────────────────────────────────────────────────────── +if [ -z "${JAVA_HOME:-}" ] || "$JAVA_HOME/bin/java" -version 2>&1 | grep -qE '"(2[5-9]|[3-9][0-9])\.'; then + if [ -d "$HOME/jdk21" ]; then + export JAVA_HOME="$HOME/jdk21" + export PATH="$JAVA_HOME/bin:$PATH" + info "Using local JDK 21: $JAVA_HOME" + else + abort "Java 25+ detected but Gradle only supports ≤ Java 24.\nInstall JDK 21 and set JAVA_HOME." + fi +fi +info "Java: $("$JAVA_HOME/bin/java" -version 2>&1 | head -1)" + +# ─── 3. Android SDK ──────────────────────────────────────────────────────── +if [ -z "${ANDROID_HOME:-}" ]; then + for candidate in "$HOME/android-sdk" "$HOME/Android/Sdk" "/opt/android-sdk"; do + if [ -d "$candidate/platform-tools" ]; then + export ANDROID_HOME="$candidate" + break + fi + done +fi +[ -z "${ANDROID_HOME:-}" ] && abort "Android SDK not found. Set ANDROID_HOME." +export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin" +info "Android SDK: $ANDROID_HOME" + +# ─── 4. expo prebuild ────────────────────────────────────────────────────── +info "Running expo prebuild (Android)…" +cd "$SCRIPT_DIR" +npx expo prebuild --platform android --clean --no-install + +# ─── 5. Patch build.gradle — inject release signingConfig ────────────────── +info "Patching android/app/build.gradle for release signing…" + +python3 - "$SCRIPT_DIR/android/app/build.gradle" << 'PYEOF' +import sys, re + +path = sys.argv[1] +with open(path, 'r') as f: + content = f.read() + +if 'MYAPP_UPLOAD_STORE_FILE' in content: + print('[patch] build.gradle already patched, skipping.') + sys.exit(0) + +release_signing = """\n release { + if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) { + storeFile file(MYAPP_UPLOAD_STORE_FILE) + storePassword MYAPP_UPLOAD_STORE_PASSWORD + keyAlias MYAPP_UPLOAD_KEY_ALIAS + keyPassword MYAPP_UPLOAD_KEY_PASSWORD + } + }""" + +content = re.sub( + r'(signingConfigs\s*\{[\s\S]*?debug\s*\{[\s\S]*?\})', + r'\1' + release_signing, + content, + count=1 +) + +def replace_release_signing(m): + return m.group(0).replace('signingConfig signingConfigs.debug', + 'signingConfig signingConfigs.release', 1) + +content = re.sub( + r'release\s*\{[^}]*signingConfig\s+signingConfigs\.debug', + replace_release_signing, + content, + count=1, + flags=re.DOTALL +) + +with open(path, 'w') as f: + f.write(content) + +print('[patch] build.gradle patched for release signing.') +PYEOF + +# ─── 6. Inject signing props into gradle.properties ──────────────────────── +info "Injecting signing credentials into gradle.properties…" +GRADLE_PROPS="$SCRIPT_DIR/android/gradle.properties" + +sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS" 2>/dev/null || true + +cat >> "$GRADLE_PROPS" << EOF + +# --- sheethappens release signing (injected by build-apk.sh — wiped after build) +MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH_EXPANDED +MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD +MYAPP_UPLOAD_KEY_ALIAS=$KEYSTORE_ALIAS +MYAPP_UPLOAD_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD +# --- end signing +EOF + +# ─── 7. Gradle build ─────────────────────────────────────────────────────── +cd "$SCRIPT_DIR/android" +chmod +x gradlew +info "Building APK (release)…" +./gradlew assembleRelease --no-daemon + +# ─── 8. Wipe credentials from gradle.properties ─────────────────────────── +sed -i '/^# --- sheethappens release signing/,/^# --- end signing/d' "$GRADLE_PROPS" +info "Signing credentials wiped from gradle.properties." + +# ─── 9. Copy to dist/ ───────────────────────────────────────────────────── +mkdir -p "$DIST_DIR" +TIMESTAMP="$(date +%Y%m%d-%H%M)" +OUTPUT="$DIST_DIR/sheethappens-$TIMESTAMP.apk" +cp "app/build/outputs/apk/release/app-release.apk" "$OUTPUT" + +echo "" +info "Build complete!" +echo -e " ${GREEN}→ $OUTPUT${NC}" +echo ""