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.
This commit is contained in:
austinried
2026-10-08 18:52:43 +00:00
parent 293561d81f
commit f506ac0410
3 changed files with 84 additions and 5 deletions
+68
View File
@@ -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
+1 -2
View File
@@ -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).
+15 -3
View File
@@ -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")
}
}
}