name: android # Native Android (Kotlin/Compose/Media3) — M8 rewrite, now the only client. # At cutover (M8 phase 14.4) the asset name flipped from # minstrel-android-.apk to minstrel-.apk so the server's bundled # in-app-update channel (release.yml polls this name) resolves to the # native APK with no further changes. # # Tag format: vYYYY.MM.DD (per-day CalVer, mutable within the day). If a # tag is force-moved within the day, this workflow re-runs and overwrites # the existing release asset — same name, new content. on: push: branches: [main, dev] tags: ['v*'] paths: - 'android/**' - '.gitea/workflows/android.yml' # pull_request trigger intentionally omitted — see test-web.yml for # the rationale (single-author repo, push covers PR-merge equivalent). concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: # Silences the JDK 22+ "restricted method in java.lang.System has been # called" warning that Gradle 9.1's bundled native-platform jar trips # at launch (System.load for native primitives). Affects the LAUNCHER # JVM, not the daemon — that's why org.gradle.jvmargs in # gradle.properties isn't enough. Future-compat: required opt-in once # JDK 25 promotes the warning to an error. JAVA_TOOL_OPTIONS: "--enable-native-access=ALL-UNNAMED" jobs: build: name: Build + lint + test # Using flutter-ci runner label because it's the only proven-working # label with docker that can pull our container.image. Switch to # android-ci once the operator registers that runner label. runs-on: flutter-ci container: image: git.fabledsword.com/bvandeusen/ci-android:36 defaults: run: working-directory: android steps: - name: Checkout uses: actions/checkout@v4 - name: Cache Gradle dirs # Resolved deps + Gradle distribution + Kotlin daemon caches. # Saves ~3 min per CI run after the first warm-up. uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper ~/.kotlin key: gradle-${{ runner.os }}-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/gradle/libs.versions.toml', 'android/**/*.gradle.kts') }} restore-keys: | gradle-${{ runner.os }}- - name: Make gradlew executable run: chmod +x ./gradlew - name: Gradle wrapper validation run: ./gradlew --version - name: ktlint run: ./gradlew ktlintCheck - name: detekt run: ./gradlew detekt - name: Unit tests run: ./gradlew testDebugUnitTest - name: Assemble debug if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: ./gradlew assembleDebug - name: Upload debug APK if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: minstrel-android-debug-${{ github.sha }} path: android/app/build/outputs/apk/debug/app-debug.apk release: name: Build signed release APK needs: build if: startsWith(github.ref, 'refs/tags/v') runs-on: flutter-ci container: image: git.fabledsword.com/bvandeusen/ci-android:36 defaults: run: working-directory: android env: # PKCS12 keystores collapse store + key password into a single # value, so both env vars map to one secret. build.gradle still # reads them separately to stay format-agnostic. ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} steps: - name: Checkout uses: actions/checkout@v4 - name: Make gradlew executable run: chmod +x ./gradlew - name: Decode signing keystore # Reuses the same keystore env-var contract as flutter.yml so the # native APK is signed with the same key — Android will accept # the upgrade in place at cutover without uninstall. env: ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} shell: bash run: | if [ -z "${ANDROID_KEYSTORE_B64}" ]; then echo "::error::ANDROID_KEYSTORE_B64 missing"; exit 1 fi KEYSTORE_PATH="${RUNNER_TEMP}/minstrel-release.keystore" echo "${ANDROID_KEYSTORE_B64}" | base64 -d > "${KEYSTORE_PATH}" echo "ANDROID_KEYSTORE_PATH=${KEYSTORE_PATH}" >> "${GITHUB_ENV}" - name: Build release APK run: ./gradlew assembleRelease - name: Attach APK to release # M8 phase 14.4 cutover: the asset is now minstrel-.apk so # release.yml's bundled in-app-update fetcher (which polls this # exact filename) resolves to the native APK with no further # workflow plumbing. shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" REPO="${GITHUB_REPOSITORY}" RELEASE_ID=$(curl -fsSL \ -H "Authorization: token ${CI_TOKEN}" \ "https://git.fabledsword.com/api/v1/repos/${REPO}/releases/tags/${TAG}" \ | grep -oP '"id":\s*\K[0-9]+' | head -1) if [ -z "${RELEASE_ID}" ]; then echo "::error::release for ${TAG} not found"; exit 1 fi curl -fsSL \ -H "Authorization: token ${CI_TOKEN}" \ -F "attachment=@app/build/outputs/apk/release/app-release.apk" \ "https://git.fabledsword.com/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=minstrel-${TAG}.apk"