Last Updated: November 24, 2025
Author: Ali Mansour
Source: GitHub Repository
Feedback: Submit Issues
Continuous Integration (CI) is a fundamental practice in modern software development. For an Android app like "My-Tasks," a CI workflow can automatically build and test your code every time you open a pull request, ensuring that new changes don't break existing functionality.
This codelab will guide you through completing a "skeleton" GitHub Actions workflow file. You will implement the TODO items to build a complete, functioning CI pipeline.
The application we are working on named My Tasks


.github/workflows/pull_request.yml).My-Tasks app.pull_request to a specific branch.runs-on property to select a job runner.needs.actions/checkout and actions/setup-java.My-Tasks repository: https://github.com/dev-ali-mansour/My-Tasks.starter branch checked out.Before we begin, let's get your environment ready.
You have two options here:
If you want to contribute to the repository or set up your own version with CI/CD:
git clone https://github.com/YOUR_USERNAME/My-Tasks.git
cd My-Tasks
Setting up GitHub Secrets
To enable the automated workflows (Pull Request checks and Google Play deployment), you need to configure the following secrets in your repository settings:
Required Secrets for Release Workflow:
Secret Name | Description | How to Get It |
| Password for your Android keystore | The password you set when creating your keystore |
| Alias name for your signing key | The alias you used when creating your key |
| Password for your signing key | The password you set for your signing key |
| Base64-encoded keystore file | Run: |
| Google Play service account JSON | Download from Google Play Console → API access |
Creating an Android Keystore (if you don't have one):
keytool -genkey -v -keystore release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias your-key-alias
Encoding the Keystore for GitHub Secrets:
base64 -w 0 release-key.jks > keystore-base64.txt
# Copy the content of keystore-base64.txt and paste it as ANDROID_KEYSTORE secret
Setting up Google Play Service Account:
GOOGLE_PLAY_AUTH_JSON secretNote: The release workflow will only trigger when you push a tag (e.g., v1.0.0). The pull request workflow runs automatically on PRs to main or develop branches.
If you just want to practice this codelab without contributing to the repository:
git clone https://github.com/dev-ali-mansour/My-Tasks.git
cd My-Tasks
On Linux, if you have JDK 21 installed at /usr/lib/jvm/java-21-openjdk:
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export PATH="$JAVA_HOME/bin:$PATH"
java -version
You should see a Java 21 version.
On macOS, If you installed JDK 21 using Homebrew:
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
export PATH="$JAVA_HOME/bin:$PATH"
java -version
If using Azul Zulu or Oracle JDK:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home
or
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home
On macOS, If JDK 21 is installed (e.g., Oracle, Azul, Temurin) To set JAVA_HOME temporarily (PowerShell session only):
$env:JAVA_HOME="C:\Program Files\Java\jdk-21"
$env:Path="$env:JAVA_HOME\bin;$env:Path"
java -version
To set JAVA_HOME permanently:
setx JAVA_HOME "C:\Program Files\Java\jdk-21" /M
Note: ⚠️ Restart PowerShell or your IDE after doing this.
File → Open...My-Tasks folder.From Android Studio:
app configuration.From the command line:
./gradlew :app:assembleDebug
The resulting APK will be in app/build/outputs/apk/debug/.
Unit tests (JVM):
./gradlew :app:testDebugUnitTest
./gradlew :app:testReleaseUnitTest
This runs unit tests for core use cases, repository implementations, and feature ViewModels like:
HomeViewModelTestNewTaskViewModelTestUpdateTaskViewModelTestTaskDetailsViewModelTestTo generate a global JaCoCo coverage report:
./gradlew jacocoTestReport
Open the HTML report:
app/build/reports/jacoco/jacocoTestReport/html/index.htmlFor per-package and per-class coverage, see the HTML tree under:
app/build/reports/jacoco/html/...starter branch..github/workflows/pull_request.yml.You will see the following content, which includes 7 TODO comments. We will now complete them one by one.
name: Run tests
run-name: Run test - ${{ github.head_ref }}
# TODO: [1] Configure the trigger of this action to only run on pull requests to main or develop
jobs:
validate_pr_sources:
# TODO: [2] Configure the runner of this job to the latest Ubuntu version
steps:
- name: Validate source branch
id: source_check
run: |
echo "Source branch is: ${{ github.head_ref }}"
# Only enforce naming rules when PR targets the main branch
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "Validating source branch for PR to main..."
# Allow only branches matching release/<major>.<minor>.<patch> or hotfix/<major>.<minor>.<patch>
if [[ "${{ github.head_ref }}" =~ ^(release|hotfix)/[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "✅ Pull request source is a valid release/hotfix branch."
else
echo "::error::❌ PRs to main are only allowed from release/x.y.z or hotfix/x.y.z branches (e.g., release/1.2.3 or hotfix/1.2.3). Got: ${{ github.head_ref }}"
exit 1
fi
else
echo "Skipping source branch validation because target branch is not main."
fi
run-tests:
# TODO: [3] Configure this job to run after the source branch is validated
runs-on: ubuntu-latest
steps:
# TODO: [4] Checkout the source branch
# TODO: [5] Setup JAVA 21 and install Gradle
- name: Cache Gradle files
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', '**/gradle.lockfile') }}
restore-keys: |
${{ runner.os }}-gradle-
# TODO: [6] Grant execution permission for the Gradle wrapper file
# TODO: [7] Run the unit tests
Our first step is to tell GitHub when this workflow should run. We want it to trigger only when a pull request is opened (or updated) against our main branch or develop branch.
Find TODO [1] and replace it with the on: block.
Replace this:
# TODO: [1] Configure the trigger of this action to only run on pull requests to main or develop
With this:
on:
pull_request:
branches:
- main
- develop
This YAML structure tells GitHub to listen for the pull_request event, but only for pull requests targeting the main branch or develop branch.
Next, we'll configure the validate_pr_sources job. This job has two TODOs.
A "runner" is the virtual machine that will execute your job. We need to specify one. Let's use the latest available Ubuntu runner.
Replace this:
validate_pr_sources:
# TODO: [2] Configure the runner of this job to the latest Ubuntu version
With this:
validate_pr_sources:
runs-on: ubuntu-latest
This is the main job where we'll check out our code and run our unit tests. This section has five TODOs.
We don't want to run our tests unless the branch name is valid. To enforce this, we'll make the run-tests job dependent on validate_pr_sources. This is done using the needs: keyword.
Replace this:
run-tests:
# TODO: [3] Configure this job to run after the source branch is validated
With this:
run-tests:
needs: validate_pr_sources
This creates a dependency graph. The run-tests job will now wait for validate_pr_sources to complete successfully before it starts. If validate_pr_sources fails, run-tests will be skipped.
The runner is a blank machine. It doesn't have your code on it yet. We must use the official actions/checkout action to pull a copy of our repository onto the runner.
Replace this:
steps:
# TODO: [4] Checkout the source branch
With this:
steps:
- name: Checkout
uses: actions/checkout@v4
Our Android app requires Java to build. We'll use the actions/setup-java action to install Java 21 (Temurin distribution), which is a common requirement for modern Android builds.
Replace this:
# TODO: [5] Setup JAVA 21 and install Gradle
With this:
- name: Setup Java 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
Note: The existing "Cache Gradle files" step will automatically handle the Gradle installation and caching, so we only need to set up Java.
The Gradle Wrapper (gradlew) script in our repository needs to have "execute" permissions to run. By default, files checked out from Git might not have this permission.
Replace this:
# TODO: [6] Grant execution permission for the Gradle wrapper file
With this:
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
Finally, it's time to run the tests! We'll use the gradlew script we just made executable to run the standard test task. This will execute all unit tests in the project.
Replace this:
# TODO: [7] Run the unit tests
With this:
- name: Run unit tests
run: ./gradlew test
You've successfully built a complete GitHub Actions CI workflow for your My-Tasks Android app!
Your final pull_request.yml file should look like this:
name: Run tests
run-name: Run test - ${{ github.head_ref }}
on:
pull_request:
branches:
- main
- develop
jobs:
validate_pr_sources:
runs-on: ubuntu-latest
steps:
- name: Validate source branch
id: source_check
run: |
echo "Source branch is: ${{ github.head_ref }}"
# Only enforce naming rules when PR targets the main branch
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "Validating source branch for PR to main..."
# Allow only branches matching release/<major>.<minor>.<patch> or hotfix/<major>.<minor>.<patch>
if [[ "${{ github.head_ref }}" =~ ^(release|hotfix)/[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "✅ Pull request source is a valid release/hotfix branch."
else
echo "::error::❌ PRs to main are only allowed from release/x.y.z or hotfix/x.y.z branches (e.g., release/1.2.3 or hotfix/1.2.3). Got: ${{ github.head_ref }}"
exit 1
fi
else
echo "Skipping source branch validation because target branch is not main."
fi
run-tests:
needs: validate_pr_sources
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
- name: Cache Gradle files
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', '**/gradle.lockfile') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execution permission for gradlew
run: chmod +x gradlew
- name: Run Unit Tests
run: ./gradlew test
pull_request.yml file on the starter branch (or a new feature branch, e.g., feature/ci-workflow).main.validate_pr_sources will run first, followed by run-tests.release.yml workflow file in the project. It has been built to trigger on pushing tag starts with v, then it starts running test, bumping the version, generating signed bundle, and upload it to Google Play.androidTest) on an emulator.