Automating Android Repo Maintenance with Reusable GitHub Actions
How I modernized 8 Android repos in one session — reusable CI workflows, batch build system updates, and a pattern for maintaining multiple projects at scale.
I maintain several Android sample repos that accompany blog posts on this site. Over time, they drifted — different AGP versions, some on Groovy, some on Kotlin DSL, one still using Eclipse project format from 2013. None had CI. If a repo silently broke after a dependency update, I wouldn’t know until someone opened an issue.
I decided to fix all of them in one pass. Here’s how.
The Problem
Eight repos, each in a different state:
| Repo | AGP | Gradle | Language | Build System |
|---|---|---|---|---|
| SharedPreference-Demo | 1.3.0 | 2.4 | Java | Groovy |
| create-alertdialog-android | 8.7.0 | 8.9 | Kotlin | Kotlin DSL |
| custom-alertdialog-programmatically | 8.7.0 | 8.9 | Kotlin | Kotlin DSL |
| ListViewDemo | 4.1.3 | 6.5 | Kotlin | Groovy |
| ChipDemo | 4.2.1 | 6.7.1 | Kotlin | Groovy |
| ClipboardManager | — | — | Java | Eclipse |
| badge-count-navigation-menu | 4.1.0 | 6.3 | Kotlin | Groovy |
| product-flavours | 4.2.0 | 6.7 | Java | Groovy |
The goal: bring every repo to AGP 9.1.0, Gradle 9.3.1, Kotlin DSL, version catalog, minSdk 34, targetSdk 36, Material3 — and add CI to all of them.
Step 1: Create a Reusable CI Workflow
Instead of copying a 40-line workflow file to 8 repos, I created a single reusable workflow in a dedicated public repo: krrishnaaaa/android-ci.
The reusable workflow
.github/workflows/build.yml in the android-ci repo:
name: Android Build
on:
workflow_call:
inputs:
java-version:
required: false
type: string
default: '21'
gradle-task:
required: false
type: string
default: 'assembleDebug'
test-task:
required: false
type: string
default: 'test'
run-tests:
required: false
type: boolean
default: true
upload-artifact:
required: false
type: boolean
default: true
artifact-path:
required: false
type: string
default: 'app/build/outputs/apk/debug/*.apk'
artifact-name:
required: false
type: string
default: 'debug-apk'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Build
run: ./gradlew ${{ inputs.gradle-task }}
- name: Run tests
if: inputs.run-tests
run: ./gradlew ${{ inputs.test-task }}
- name: Upload artifact
if: inputs.upload-artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.artifact-path }}
retention-days: 14
Key design decisions:
workflow_call— makes it callable from other repos- All inputs optional — sensible defaults mean most repos need zero configuration
- Gradle caching —
setup-gradlecaches dependencies automatically,cache-read-onlyprevents PRs from polluting the cache - Configurable — product flavor repos can override
artifact-path, repos without tests can setrun-tests: false
The caller workflow
In each Android repo, .github/workflows/android.yml is just:
name: Android CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
build:
uses: krrishnaaaa/android-ci/.github/workflows/build.yml@main
Eight lines. Same file for every repo. If I need to change the CI logic (upgrade Java version, add lint, add instrumented tests), I change it once in android-ci and all 8 repos pick it up automatically.
Custom configuration
For repos that need different settings, just pass inputs:
jobs:
build:
uses: krrishnaaaa/android-ci/.github/workflows/build.yml@main
with:
artifact-path: 'app/build/outputs/apk/*/debug/*.apk' # product flavors
Step 2: Batch Build System Updates
Updating 8 repos manually would take hours. I wrote a shell script that modernizes any Android repo:
#!/bin/bash
set -e
REPO_DIR="$1"
NAMESPACE="$2"
cd "$REPO_DIR"
# settings.gradle.kts
cat > settings.gradle.kts << 'EOF'
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "App"
include(":app")
EOF
# root build.gradle.kts
cat > build.gradle.kts << 'EOF'
plugins {
alias(libs.plugins.android.application) apply false
}
EOF
# Version catalog
mkdir -p gradle
cat > gradle/libs.versions.toml << 'EOF'
[versions]
agp = "9.1.0"
core-ktx = "1.16.0"
appcompat = "1.7.0"
material = "1.12.0"
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
EOF
# app/build.gradle.kts
cat > app/build.gradle.kts << BEOF
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "$NAMESPACE"
compileSdk = 36
defaultConfig {
applicationId = "$NAMESPACE"
minSdk = 34
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
}
BEOF
# Delete old Groovy files
rm -f build.gradle app/build.gradle settings.gradle
echo "Done: $REPO_DIR"
Run it on each repo:
./modernize.sh ~/projects/ListViewDemo "com.example.listviewdemo"
./modernize.sh ~/projects/ChipDemo "com.pcsalt.example.chipdemo"
# ... and so on
The script handles the 80% case. Repos with extra dependencies (navigation, lifecycle) or special config (product flavors) needed manual additions after the script ran.
Step 3: Handle Edge Cases
AGP 9.0+ removes the Kotlin plugin
With AGP 9.0 and above, Kotlin support is built into the Android Gradle Plugin. The org.jetbrains.kotlin.android plugin is no longer needed — and will actually fail the build if you include it.
// Before (AGP 8.x)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android") // needed
}
// After (AGP 9.x)
plugins {
id("com.android.application") // Kotlin support built in
}
Also remove kotlinOptions — it’s no longer recognized:
// Remove this entire block
kotlinOptions {
jvmTarget = "17"
}
Eclipse projects need a full rebuild
One repo (ClipboardManager) was still in Eclipse format — project.properties, no Gradle at all. There’s no migration path — just rebuild from scratch. Read the source, understand what it does, create a new Gradle project, rewrite in Kotlin.
Material3 theme names
Theme.Material3.DayNight.DarkActionBar doesn’t exist. If you’re migrating from Theme.AppCompat.Light.DarkActionBar, use Theme.Material3.DayNight instead.
Product flavors change artifact paths
The default CI artifact path app/build/outputs/apk/debug/*.apk doesn’t work when you have product flavors. The actual path becomes app/build/outputs/apk/<flavor>/debug/*.apk. Use a glob:
artifact-path: 'app/build/outputs/apk/*/debug/*.apk'
gradlew must be executable
Old repos have gradlew without execute permission. GitHub Actions runs ./gradlew which fails with exit code 126. Either chmod +x gradlew or copy a fresh one from a working repo.
Results
| Before | After |
|---|---|
| 8 repos, 6 different AGP versions | All on AGP 9.1.0 |
| Mix of Groovy and Kotlin DSL | All Kotlin DSL |
| 1 Eclipse project | Modern Gradle |
| 2 Java projects | All Kotlin |
| 0 repos with CI | All 8 with CI |
| No version catalogs | All using libs.versions.toml |
Every push to main now triggers a build. If a repo breaks, I see it immediately in the GitHub Actions badge.
The Pattern
If you maintain multiple Android repos (sample projects, libraries, client apps), here’s the pattern:
- Create a reusable workflow repo — one source of truth for CI logic
- Add a one-liner caller to each repo — 8 lines, identical everywhere
- Script the repetitive parts — build file generation, dependency updates, theme changes
- Handle edge cases manually — product flavors, extra dependencies, source rewrites
- Verify with builds — run
assembleDebuglocally before pushing
The reusable workflow is the real win. One change propagates to all repos. No copy-paste, no drift.
Source
The reusable workflow is open source: krrishnaaaa/android-ci