diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 4dbe5ef..50fc3da 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -14,38 +14,64 @@ concurrency: group: android-build-${{ github.ref }} cancel-in-progress: true +env: + FLUTTER_VERSION: '3.44.6' + JAVA_VERSION: '17' + ANDROID_COMPILE_SDK: '37' + GRADLE_VERSION: '9.1.0' + jobs: analyze-and-build: - name: Analyze and build Android + name: Analyze, test and debug build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 1 - name: Set up Java uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: ${{ env.JAVA_VERSION }} cache: gradle - name: Set up Flutter uses: subosito/flutter-action@v2 with: + flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable cache: true - - name: Cache Pub packages - uses: actions/cache@v4 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} - restore-keys: | - ${{ runner.os }}-pub- + - name: Provision Android SDK + shell: bash + run: | + set -euo pipefail + SDKMANAGER="$(find "${ANDROID_SDK_ROOT:-$ANDROID_HOME}/cmdline-tools" -type f -name sdkmanager 2>/dev/null | head -n 1)" + test -n "$SDKMANAGER" || { echo "Android sdkmanager not found"; exit 1; } + SDKROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}" + yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true + "$SDKMANAGER" "platforms;android-${ANDROID_COMPILE_SDK}" + echo "ANDROID_SDK_ROOT=$SDKROOT" >> "$GITHUB_ENV" - - name: Flutter version - run: flutter --version + - name: Provision Gradle wrapper + shell: bash + run: | + set -euo pipefail + if [ ! -f android/gradlew ]; then + gradle --version + (cd android && gradle wrapper --gradle-version "$GRADLE_VERSION" --distribution-type all) + fi + chmod +x android/gradlew + ./android/gradlew --version + + - name: Flutter and Android environment + run: | + flutter --version + flutter doctor -v + java -version - name: Get dependencies run: flutter pub get @@ -59,43 +85,70 @@ jobs: - name: Build debug APK run: flutter build apk --debug + - name: Verify debug APK + shell: bash + run: | + set -euo pipefail + APK="build/app/outputs/flutter-apk/app-debug.apk" + test -s "$APK" + file "$APK" + sha256sum "$APK" | tee build/app/outputs/flutter-apk/app-debug.apk.sha256 + - name: Upload debug APK uses: actions/upload-artifact@v5 with: name: motorsignal-debug-apk - path: build/app/outputs/flutter-apk/app-debug.apk + path: | + build/app/outputs/flutter-apk/app-debug.apk + build/app/outputs/flutter-apk/app-debug.apk.sha256 if-no-files-found: error retention-days: 14 release: - name: Build signed release APK + name: Build signed release APK and AAB runs-on: ubuntu-latest + needs: analyze-and-build if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 1 - name: Set up Java uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: ${{ env.JAVA_VERSION }} cache: gradle - name: Set up Flutter uses: subosito/flutter-action@v2 with: + flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable cache: true - - name: Cache Pub packages - uses: actions/cache@v4 - with: - path: ~/.pub-cache - key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }} - restore-keys: | - ${{ runner.os }}-pub- + - name: Provision Android SDK + shell: bash + run: | + set -euo pipefail + SDKMANAGER="$(find "${ANDROID_SDK_ROOT:-$ANDROID_HOME}/cmdline-tools" -type f -name sdkmanager 2>/dev/null | head -n 1)" + test -n "$SDKMANAGER" || { echo "Android sdkmanager not found"; exit 1; } + yes | "$SDKMANAGER" --licenses >/dev/null 2>&1 || true + "$SDKMANAGER" "platforms;android-${ANDROID_COMPILE_SDK}" + + - name: Provision Gradle wrapper + shell: bash + run: | + set -euo pipefail + if [ ! -f android/gradlew ]; then + gradle --version + (cd android && gradle wrapper --gradle-version "$GRADLE_VERSION" --distribution-type all) + fi + chmod +x android/gradlew + ./android/gradlew --version - name: Get dependencies run: flutter pub get @@ -103,16 +156,22 @@ jobs: - name: Restore release keystore env: KEYSTORE_BASE64: ${{ secrets.MOTORSIGNAL_KEYSTORE_BASE64 }} + shell: bash run: | + set -euo pipefail test -n "$KEYSTORE_BASE64" || { echo "Missing MOTORSIGNAL_KEYSTORE_BASE64 repository secret"; exit 1; } - echo "$KEYSTORE_BASE64" | base64 --decode > android/app/motorsignal-upload-keystore.jks + printf '%s' "$KEYSTORE_BASE64" | base64 --decode > android/app/motorsignal-upload-keystore.jks + chmod 600 android/app/motorsignal-upload-keystore.jks + test -s android/app/motorsignal-upload-keystore.jks - name: Create signing properties env: STORE_PASSWORD: ${{ secrets.MOTORSIGNAL_STORE_PASSWORD }} KEY_PASSWORD: ${{ secrets.MOTORSIGNAL_KEY_PASSWORD }} KEY_ALIAS: ${{ secrets.MOTORSIGNAL_KEY_ALIAS }} + shell: bash run: | + set -euo pipefail test -n "$STORE_PASSWORD" || { echo "Missing MOTORSIGNAL_STORE_PASSWORD repository secret"; exit 1; } test -n "$KEY_PASSWORD" || { echo "Missing MOTORSIGNAL_KEY_PASSWORD repository secret"; exit 1; } test -n "$KEY_ALIAS" || { echo "Missing MOTORSIGNAL_KEY_ALIAS repository secret"; exit 1; } @@ -122,6 +181,19 @@ jobs: keyAlias=$KEY_ALIAS storeFile=app/motorsignal-upload-keystore.jks EOF + chmod 600 android/key.properties + + - name: Verify release signing key + env: + STORE_PASSWORD: ${{ secrets.MOTORSIGNAL_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.MOTORSIGNAL_KEY_ALIAS }} + shell: bash + run: | + set -euo pipefail + keytool -list -keystore android/app/motorsignal-upload-keystore.jks \ + -storetype PKCS12 \ + -storepass "$STORE_PASSWORD" \ + -alias "$KEY_ALIAS" >/dev/null - name: Build release APK run: flutter build apk --release @@ -129,11 +201,27 @@ jobs: - name: Build release App Bundle run: flutter build appbundle --release + - name: Verify release artifacts and signatures + env: + STORE_PASSWORD: ${{ secrets.MOTORSIGNAL_STORE_PASSWORD }} + shell: bash + run: | + set -euo pipefail + APK="build/app/outputs/flutter-apk/app-release.apk" + AAB="build/app/outputs/bundle/release/app-release.aab" + test -s "$APK" + test -s "$AAB" + file "$APK" "$AAB" + "$ANDROID_HOME/build-tools/$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -n 1)/apksigner" verify --verbose "$APK" + sha256sum "$APK" "$AAB" | tee build/release-artifacts.sha256 + - name: Upload release APK uses: actions/upload-artifact@v5 with: name: motorsignal-release-apk - path: build/app/outputs/flutter-apk/app-release.apk + path: | + build/app/outputs/flutter-apk/app-release.apk + build/release-artifacts.sha256 if-no-files-found: error retention-days: 30 @@ -141,6 +229,14 @@ jobs: uses: actions/upload-artifact@v5 with: name: motorsignal-release-aab - path: build/app/outputs/bundle/release/app-release.aab + path: | + build/app/outputs/bundle/release/app-release.aab + build/release-artifacts.sha256 if-no-files-found: error retention-days: 30 + + - name: Remove signing credentials + if: always() + shell: bash + run: | + rm -f android/key.properties android/app/motorsignal-upload-keystore.jks