From f506ac04102228c43f983beaeb8eda15dae0c564 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:22:34 +0000 Subject: [PATCH] release: publish signed builds on v* tags Set applicationId to com.subtracks and add a GitHub Actions workflow that, on a v* tag, builds and signs the release APK and AAB, creates the GitHub release with the APK, and uploads the AAB to Google Play. Release notes come from the matching metadata changelog. Signing reads the keystore from the environment (RELEASE_KEYSTORE, RELEASE_KEYSTORE_PASSWORD, RELEASE_KEY_ALIAS, RELEASE_KEY_PASSWORD) and falls back to the debug key locally. --- .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ AGENTS.md | 3 +- app/build.gradle.kts | 18 ++++++++-- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..36638494 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - name: Set up Android SDK + uses: android-actions/setup-android@v4 + - name: Install platform and build tools + run: sdkmanager "platforms;android-37" "build-tools;37.0.0" + - name: Decode signing material + id: signing + env: + KEYSTORE_BASE64: ${{ secrets.SUBTRACKS_KEYSTORE_BASE64 }} + KEYPROPERTIES_BASE64: ${{ secrets.SUBTRACKS_KEYPROPERTIES_BASE64 }} + run: | + echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore" + alias=$(printf '%s' "$KEYPROPERTIES_BASE64" | base64 -d | sed -n 's/^keyAlias=//p' | tr -d '\r') + test -n "$alias" || { echo "no keyAlias found in SUBTRACKS_KEYPROPERTIES_BASE64" >&2; exit 1; } + echo "alias=$alias" >> "$GITHUB_OUTPUT" + - name: Build APK and bundle + env: + RELEASE_KEYSTORE: ${{ runner.temp }}/release.keystore + RELEASE_KEYSTORE_PASSWORD: ${{ secrets.SUBTRACKS_UPLOAD_STORE_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ steps.signing.outputs.alias }} + RELEASE_KEY_PASSWORD: ${{ secrets.SUBTRACKS_UPLOAD_KEY_PASSWORD }} + run: | + chmod +x gradlew + ./gradlew :app:assembleRelease :app:bundleRelease --no-daemon + - name: Read version code + id: version + run: echo "code=$(grep -oP 'versionCode = \K\d+' app/build.gradle.kts)" >> "$GITHUB_OUTPUT" + - name: Stage Play release notes + run: | + mkdir -p whatsnew + cp "metadata/en-US/changelogs/${{ steps.version.outputs.code }}.txt" whatsnew/whatsnew-en-US + - name: Upload to Google Play + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + packageName: com.subtracks + releaseFiles: app/build/outputs/bundle/release/app-release.aab + tracks: ${{ vars.PLAY_TRACK || 'internal' }} + status: completed + whatsNewDirectory: whatsnew + mappingFile: app/build/outputs/mapping/release/mapping.txt + - name: Collect release APK + run: cp app/build/outputs/apk/release/app-release.apk subtracks.apk + - name: Create GitHub release + uses: softprops/action-gh-release@v3 + with: + body_path: metadata/en-US/changelogs/${{ steps.version.outputs.code }}.txt + files: subtracks.apk diff --git a/AGENTS.md b/AGENTS.md index 593f6ae9..ce39a03a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Guidance for agents (and humans) working in this repository. ## What this is -A native Android rewrite of Subtracks, a client for Subsonic-compatible servers (Navidrome, gonic, Airsonic, ...), in Kotlin and Jetpack Compose. The `main` history still contains the earlier Flutter app; the native app is being built fresh under `app/` and ships as `com.subtracks.next` for side-by-side beta. The aim is a clean, F-Droid-friendly client: free/libre dependencies only, no telemetry. +A native Android rewrite of Subtracks, a client for Subsonic-compatible servers (Navidrome, gonic, Airsonic, ...), in Kotlin and Jetpack Compose. The `main` history still contains the earlier Flutter app; the native app is being built fresh under `app/` and ships as `com.subtracks`. The aim is a clean, F-Droid-friendly client: free/libre dependencies only, no telemetry. ## Environment @@ -59,7 +59,6 @@ Everything comes from the Nix flake devshell; do not install toolchains by hand. - Room 3 does not map `PagingSource` automatically: a DAO that returns it needs `@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)`. - Material icons are not pulled in by `material3`; use the BOM-managed `material-icons-core`/`material-icons-extended` (frozen at 1.7.8). Extended is large in debug builds but R8 strips unused icons from release. - Robolectric creates a fresh `Application` per test in one JVM, so `SubtracksApp.onCreate` stops any running Koin before `startKoin`. -- `applicationId` is `com.subtracks.next` for the beta; change it to `com.subtracks` before any store release. - The CI image provides `nix-ld`, `jq`, `sqlite`, `node`, `zstd` and a `runner` user that `cache-nix-action` expects. None of that is needed locally beyond the devshell. - Integration servers: navidrome on 4533 (`admin`/`password`), gonic on 4747 (`admin`/`admin`), lms on 5082 (`admin`/`subtracks-lms`), Nextcloud Music on 8090 (`admin`/`subtracks-nextcloud`, base URL `http://localhost:8090/index.php/apps/music/subsonic/`). The test music is cached in `.integration/music` (gitignored). - lms and Nextcloud Music authenticate the Subsonic API with per-user **API keys** instead of passwords. The harness seeds a key and most tests use `useTokenAuth = false`; `SubsonicAuthIntegrationTest` drives `useTokenAuth = true` and asserts the fallback to plaintext (lms answers code 42, Nextcloud 41). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76e5acac..2c7b100e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { buildToolsVersion = "37.0.0" defaultConfig { - applicationId = "com.subtracks.next" + applicationId = "com.subtracks" minSdk = 24 targetSdk = 37 versionCode = 13 @@ -79,13 +79,25 @@ android { .kotlin.directories .add("src/integrationTest/kotlin") + signingConfigs { + val keystorePath = providers.environmentVariable("RELEASE_KEYSTORE").orNull + if (keystorePath != null) { + create("release") { + storeFile = file(keystorePath) + storePassword = providers.environmentVariable("RELEASE_KEYSTORE_PASSWORD").orNull + keyAlias = providers.environmentVariable("RELEASE_KEY_ALIAS").orNull + keyPassword = providers.environmentVariable("RELEASE_KEY_PASSWORD").orNull + } + } + } + buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) - // Debug key so `installRelease` works locally; store and F-Droid builds re-sign. - signingConfig = signingConfigs.getByName("debug") + // Release signing when the keystore is supplied (CI); debug elsewhere so local build/install works. + signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") } } }