Installer: fetch the project when run on its own, refuse inside the ZIP #12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: android | |
| on: | |
| # No `paths` filter on push: a paths filter also applies to tag pushes, so a tag on a commit | |
| # that did not touch android/ would never build or release. The workflow is cheap enough to | |
| # run on every push to main; pull requests keep the filter. | |
| push: | |
| branches: [main, master] | |
| tags: | |
| - "android-v*" | |
| pull_request: | |
| paths: | |
| - "android/**" | |
| - ".github/workflows/android.yml" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write # needed only for the release step on tags | |
| jobs: | |
| build: | |
| name: assembleRelease + assembleDebug + lint + unit tests | |
| runs-on: ubuntu-latest # ships with the Android SDK; the build downloads platform 35 itself | |
| defaults: | |
| run: | |
| working-directory: android | |
| env: | |
| # Release signing. The permanent key is the ANDROID_KEYSTORE_B64 secret (base64 of the | |
| # .jks); it is decoded into the runner's temp dir below, never into the checkout. When the | |
| # secrets are absent (a fork's pull request) the build still runs and produces an unsigned | |
| # release APK; the signature check below then fails loudly instead of releasing it. | |
| # ANDROID_KEYSTORE_FILE is set by the decode step (the runner context is not | |
| # available in job-level env, only in steps). | |
| ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-java@v6 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| - uses: gradle/actions/setup-gradle@v6 | |
| - name: Make the wrapper executable | |
| run: chmod +x gradlew | |
| - name: Decode the release keystore | |
| env: | |
| ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} | |
| run: | | |
| if [ -z "$ANDROID_KEYSTORE_B64" ]; then | |
| echo "::warning::ANDROID_KEYSTORE_B64 secret not available; the release APK will be unsigned." | |
| exit 0 | |
| fi | |
| KS="$RUNNER_TEMP/localflow.jks" | |
| # Strip any whitespace/CR the secret may carry (a Windows pipe adds CRLF); base64 -d | |
| # rejects stray bytes with "invalid input". | |
| printf '%s' "$ANDROID_KEYSTORE_B64" | tr -d '\r\n\t ' | base64 -d > "$KS" | |
| if [ ! -s "$KS" ]; then echo "::error::keystore decoded to an empty file"; exit 1; fi | |
| chmod 600 "$KS" | |
| echo "ANDROID_KEYSTORE_FILE=$KS" >> "$GITHUB_ENV" | |
| echo "Keystore decoded to $KS ($(stat -c %s "$KS") bytes)" | |
| - name: Unit tests (WAV builder, text splice) | |
| run: ./gradlew testDebugUnitTest --no-daemon --stacktrace | |
| - name: Build debug APK | |
| run: ./gradlew assembleDebug --no-daemon --stacktrace | |
| - name: Build release APK (signed with the permanent key) | |
| run: ./gradlew assembleRelease --no-daemon --stacktrace | |
| - name: Lint | |
| run: ./gradlew lintDebug --no-daemon | |
| - name: Verify the release signature | |
| # Fails if the APK is unsigned (secrets absent) or signed with anything but a valid key, | |
| # and prints the certificate digest so every run shows which key signed it. The | |
| # permanent LocalFlow key is documented in docs/ANDROID.md ("Verify the download"). | |
| run: | | |
| if [ ! -f "$ANDROID_KEYSTORE_FILE" ]; then | |
| if [[ "$GITHUB_REF" == refs/tags/android-v* ]]; then | |
| echo "::error::No keystore on a release tag build; refusing to publish an unsigned APK." | |
| exit 1 | |
| fi | |
| echo "::warning::No keystore (secrets unavailable); skipping signature check. The release APK from this run is unsigned." | |
| exit 0 | |
| fi | |
| APKSIGNER=$(ls -1 "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -n 1) | |
| echo "Using $APKSIGNER" | |
| "$APKSIGNER" verify --print-certs app/build/outputs/apk/release/app-release.apk | tee apksigner.txt | |
| DIGEST=$(grep -i 'SHA-256 digest' apksigner.txt | head -n 1 | awk '{print $NF}') | |
| echo "Release APK signed by certificate SHA-256: $DIGEST" | |
| echo "### Release APK signing certificate" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| cat apksigner.txt >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| - name: Rename release APK | |
| # Tags are named android-vX.Y.Z; drop that prefix so the asset is LocalFlow-android-vX.Y.Z.apk | |
| run: | | |
| REF="${GITHUB_REF_NAME//\//-}"; REF="${REF#android-}" | |
| echo "ASSET_NAME=LocalFlow-android-${REF}.apk" >> "$GITHUB_ENV" | |
| cp app/build/outputs/apk/release/app-release.apk "LocalFlow-android-${REF}.apk" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: app-release.apk | |
| path: android/app/build/outputs/apk/release/app-release.apk | |
| if-no-files-found: error | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| # Debug-key build for PR reviewers only; it is never attached to a release. | |
| name: app-debug.apk | |
| path: android/app/build/outputs/apk/debug/app-debug.apk | |
| if-no-files-found: error | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: lint-report | |
| path: android/app/build/reports/lint-results-debug.html | |
| if-no-files-found: ignore | |
| - name: Release on tag | |
| if: startsWith(github.ref, 'refs/tags/android-v') | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: ${{ github.ref_name }} | |
| files: android/${{ env.ASSET_NAME }} | |
| generate_release_notes: true |