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") } } }