Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,74 @@ jobs:
path: app/build/reports/tests/
if-no-files-found: ignore
retention-days: 14

lint:
name: Android Lint
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

# Pre-existing findings live in app/lint-baseline.xml; only new
# findings fail this step.
- name: Run Android Lint
run: ./gradlew :app:lintDebug --stacktrace

- name: Upload lint report
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-report
path: |
app/build/reports/lint-results-debug.html
app/build/reports/lint-results-debug.txt
app/build/reports/lint-results-debug.xml
if-no-files-found: ignore
retention-days: 14

release-build:
name: Unsigned release build (R8/ProGuard)
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

# No signing secrets in CI. -PallowUnsignedRelease builds an unsigned,
# uninstallable APK purely to exercise R8/ProGuard, which debug builds
# never touch; a plain assembleRelease still refuses to run unsigned.
- name: Build unsigned release APK
run: ./gradlew :app:assembleRelease -PallowUnsignedRelease --stacktrace

# If R8 ever does strip something reflection needs, the minified APK is
# what gets inspected (apkanalyzer/aapt) to see which class went missing.
- name: Upload unsigned release APK
uses: actions/upload-artifact@v4
with:
name: app-release-unsigned
path: app/build/outputs/apk/release/*.apk
if-no-files-found: error
retention-days: 14
33 changes: 31 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ android {
project.hasProperty('RELEASE_KEY_PASSWORD') &&
file("${rootProject.projectDir}/opendroid-release.keystore").exists()

// Explicit opt-in for building release WITHOUT signing, so CI can exercise
// R8/ProGuard (where reflection-dependent code breaks) without holding any
// signing secrets. The resulting APK is unsigned and not installable - the
// guard below still fails a plain `assembleRelease` when signing is missing.
def allowUnsignedRelease = project.hasProperty('allowUnsignedRelease')

if (!hasSigningConfig) {
logger.warn("WARNING: Release signing not configured. Set credentials in ~/.gradle/gradle.properties (see gradle.properties.example).")
}
Expand Down Expand Up @@ -68,7 +74,7 @@ android {
def releaseBuildRequested = graph.allTasks.any { t ->
t.project == project && t.name ==~ /(assemble|bundle|package)Release.*/
}
if (releaseBuildRequested && !hasSigningConfig) {
if (releaseBuildRequested && !hasSigningConfig && !allowUnsignedRelease) {
throw new GradleException(
"Release signing is not configured. Set RELEASE_STORE_PASSWORD, RELEASE_KEY_ALIAS " +
"and RELEASE_KEY_PASSWORD in ~/.gradle/gradle.properties (or as ORG_GRADLE_PROJECT_* " +
Expand All @@ -94,17 +100,36 @@ android {
buildFeatures {
compose true
}
testOptions {
unitTests {
// Robolectric needs the app's resources and manifest on the JVM.
includeAndroidResources = true
}
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
lint {
abortOnError false
// Pre-existing findings are frozen in the baseline so they do not
// block merges; anything NEW fails the build. Shrink the baseline
// over time (delete entries, fix, repeat) - never regenerate it to
// absorb new findings.
baseline file('lint-baseline.xml')
abortOnError true
checkReleaseBuilds true
}
}

kapt {
arguments {
// Export Room schema JSONs (app/schemas/, committed to Git) so future
// migrations can be tested against the real historical schemas.
arg("room.schemaLocation", "$projectDir/schemas")
}
}

dependencies {
// AndroidX Core & Lifecycle
implementation 'androidx.core:core-ktx:1.12.0'
Expand Down Expand Up @@ -167,6 +192,10 @@ dependencies {

// Testing
testImplementation 'junit:junit:4.13.2'
// Robolectric runs the Room migration tests on the JVM - no emulator, so
// they live in src/test and run under the existing testDebugUnitTest.
testImplementation 'org.robolectric:robolectric:4.16.1'
testImplementation 'androidx.test:core:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2026.06.01')
Expand Down
Loading
Loading