| name | github-action-editor |
| description | Creates and maintains GitHub Actions CI/CD workflows for Android projects. Use when user asks to "set up CI", "create a GitHub Action", "add a CI pipeline", "configure GitHub Actions for Android", or "optimize build times". |
GitHub Action Editor
Overview
Best practices for GitHub Actions in Android projects: workflow structure, job dependencies, Gradle caching, secrets management, matrix builds, and CI optimization.
Workflow Anatomy
name: CI
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build
run: ./gradlew assembleDebug
Reusable Workflow (Callable)
Split large workflows into reusable units:
name: Build (Reusable)
on:
workflow_call:
inputs:
build_type:
type: string
required: true
default: debug
secrets:
KEYSTORE_FILE:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-android
- name: Build
run: ./gradlew assemble${{ inputs.build_type }}
name: Pull Request
on:
pull_request:
types: [opened, reopened, synchronize]
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build:
uses: ./.github/workflows/workflow_build.yml
with:
build_type: Debug
test:
uses: ./.github/workflows/workflow_test.yml
lint:
uses: ./.github/workflows/workflow_lint.yml
Composite Action (Setup Android)
Extract repeated setup steps:
name: Setup Android
description: Setup Java, Android SDK, and Gradle cache
inputs:
java-version:
description: Java version
default: '17'
runs:
using: composite
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-home-cache-cleanup: true
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.toml', '**/*.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
Gradle Caching Strategy
- name: Cache Gradle and Android SDK
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/build-cache
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.versions.toml', '**/*.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
Gradle Build Cache for CI
- name: Build with cache
run: ./gradlew assembleDebug
env:
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
Common Workflow Jobs
Lint
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-android
- name: Run Lint
run: ./gradlew lint
- name: Upload lint reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: '**/build/reports/lint-results*.html'
Unit Tests
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-android
- name: Run Tests
run: ./gradlew test
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: '**/build/reports/tests/'
- name: Publish test results
uses: dorny/test-reporter@v1
if: always()
with:
name: Unit Test Results
path: '**/build/test-results/**/*.xml'
reporter: java-junit
Snapshot Tests
snapshot:
name: Snapshot Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-android
- name: Verify Snapshots
run: ./gradlew verifyPaparazziDebug
- name: Upload snapshot diffs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: snapshot-diffs
path: '**/build/paparazzi/failures/'
Detekt (Static Analysis)
detekt:
name: Detekt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-android
- name: Run Detekt
run: ./gradlew detekt
- name: Upload SARIF to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: '**/build/reports/detekt/detekt.sarif'
Matrix Builds
test:
strategy:
matrix:
api-level: [26, 31, 34]
include:
- api-level: 34
target: google_apis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Instrumented Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target || 'default' }}
arch: x86_64
script: ./gradlew connectedAndroidTest
Secrets and Environment Variables
- name: Build release APK
run: ./gradlew assembleRelease
env:
KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > keystore.jks
Conditional Steps
- name: Deploy to Play Store
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./gradlew publishRelease
on:
push:
paths:
- '**/*.kt'
- '**/*.gradle.kts'
- 'gradle/**'
- '!**/*.md'
Job Dependencies and Artifacts
jobs:
build:
runs-on: ubuntu-latest
outputs:
apk_path: ${{ steps.build.outputs.apk_path }}
steps:
- id: build
run: |
./gradlew assembleDebug
echo "apk_path=app/build/outputs/apk/debug/app-debug.apk" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: debug-apk
path: app/build/outputs/apk/debug/
distribute:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: debug-apk
path: apk/
Build Time Optimization
runs-on: ubuntu-latest-4-cores
- name: Build with config cache
run: ./gradlew assembleDebug --configuration-cache
- name: Run tests in parallel
run: ./gradlew test --parallel --max-workers=4
- name: Detect affected modules
run: ./gradlew :tools:affected-module-detector:run
Workflow Best Practices
| ✅ Do | ❌ Don't |
|---|
Use concurrency to cancel stale runs | Let stale PRs queue up indefinitely |
| Cache Gradle dependencies with hash key | Use static cache key (stale cache) |
| Use composite actions for repeated setup | Copy-paste setup steps across workflows |
| Upload artifacts on failure | Lose test/lint output after failure |
Use secrets.* for sensitive values | Hardcode credentials in workflow files |
Pin action versions (@v4) | Use @main or @latest (unpinned) |
Fail fast with fail-fast: true in matrix | Let all matrix jobs run when one fails |
Checklist: New Workflow
References