github-actionsci-cdmobiletutorial

I Keep Forgetting About My Long-Running Mobile App Builds

January 15, 2026

If you’ve ever built mobile apps with CI, you know the pain. You push a PR, the build kicks off, you context switch, and then forget about it. An hour later you check the Actions tab and discover it failed 45 minutes ago. Or it succeeded and QA has been waiting on it the whole time.

This has been happening too many times for me to count, so I built something to solve it.

The problem

There’s no way to automatically know when your build is done. You just need to keep an eye on the Actions tab or wait for your PR to become mergeable.

What I actually wanted:

  • A push notification on my phone the moment the build finishes
  • A direct link. Tap the notification, land on Firebase App Distribution or TestFlight, install immediately
  • Different messages for success and failure
  • A link to the GitHub Actions run for failed builds so I can see what went wrong
  • No waiting around, no checking every 10 minutes, no post-it notes under the monitor

How it works

I’m using the API Alerts GitHub Action. It runs as a step at the end of your workflow and sends a push notification to your phone.

The key feature for CI/CD is that you can attach a link to the notification. Tap the notification and it takes you exactly where you need to go.

The trick is using if: success() || failure() so the notification fires no matter the outcome but with different messages.

- name: Notify
  if: success() || failure()
  uses: apialerts/notify-action@v2
  with:
    api_key: ${{ secrets.API_ALERTS_KEY }}
    channel: 'developer'
    message: ${{ job.status == 'success' && 'Build deployed' || 'Build failed' }}
    tags: 'deploy,staging'
    link: ${{ job.status == 'success' && 'https://your-test-link' || 'https://github-run-link' }}

My Android setup

I upload to Firebase App Distribution, then notify. If the build worked, the link takes me straight to App Distribution to install. If it failed, the link goes to the Actions run.

- name: Firebase App Distribution
  uses: wzieba/Firebase-Distribution-Github-Action@v1
  with:
    appId: ${{ secrets.ANDROID_FIREBASE_APP_ID }}
    serviceCredentialsFileContent: ${{ secrets.GCP_CREDENTIALS }}
    groups: staging
    file: app/build/outputs/apk/release/app-release.apk

- name: Notify
  if: success() || failure()
  uses: apialerts/notify-action@v2
  with:
    api_key: ${{ secrets.API_ALERTS_KEY }}
    channel: 'developer'
    message: ${{ job.status == 'success' && '🚀 Android staging deployed' || '❌ Android staging failed' }}
    tags: 'deploy,staging,android'
    link: ${{ job.status == 'success' && 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID' || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}

iOS with Fastlane

I use the same approach for the iOS app. My Fastlane workflow tests, builds, deploys to Firebase App Distribution (or TestFlight), then sends the notification:

- name: Test App
  run: fastlane ios tests

- name: Build
  run: fastlane ios build_beta

- name: Deploy
  run: fastlane ios deploy_beta group:staging

- name: Notify
  if: success() || failure()
  uses: apialerts/notify-action@v2
  with:
    api_key: ${{ secrets.API_ALERTS_KEY }}
    channel: 'developer'
    message: ${{ job.status == 'success' && '🚀 iOS staging deployed' || '❌ iOS staging failed' }}
    tags: 'deploy,staging,ios'
    link: ${{ job.status == 'success' && 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID' || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}

If you’re using TestFlight, swap the success link for your App Store Connect URL or the itms-beta:// deep link so testers can install directly.

Keeping staging and production separate

I use separate channels to keep things organised:

  • developer for staging and feature branch builds. Just me.
  • releases for production builds. The messages are different too:
# Staging
channel: 'developer'
message: ${{ job.status == 'success' && '🚀 Android staging deployed' || '❌ Android staging failed' }}

# Production
channel: 'releases'
message: ${{ job.status == 'success' && '🚢 Android production ready to submit' || '❌ Android production failed' }}

For production, I point the success link to the Play Store listing or App Store Connect instead of Firebase.

Full workflow

This is the actual workflow I use to build the API Alerts Android app. It’s a Compose Multiplatform project, but the pattern works for any Android build.

name: Android - Staging

on:
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

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

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

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Create Keystore
        uses: timheuer/base64-to-file@v1
        with:
          fileDir: 'app'
          fileName: 'upload.jks'
          encodedString: ${{ secrets.ANDROID_UPLOAD_KEYSTORE }}

      - name: Create key.properties
        run: 'echo "$KEY" > key.properties'
        shell: bash
        env:
          KEY: ${{ secrets.ANDROID_KEY_PROPERTIES }}

      - name: Create google-services.json
        run: 'echo "$KEY" > app/google-services.json'
        shell: bash
        env:
          KEY: ${{ secrets.ANDROID_GOOGLE_SERVICES }}

      - name: Test
        run: ./gradlew test

      - name: Build
        run: ./gradlew assembleRelease

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: Android Staging
          path: app/build/outputs/apk/release/app-release.apk
          retention-days: 7

      - name: Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.ANDROID_FIREBASE_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.GCP_CREDENTIALS }}
          groups: staging
          file: app/build/outputs/apk/release/app-release.apk

      - name: Notify
        if: success() || failure()
        uses: apialerts/notify-action@v2
        with:
          api_key: ${{ secrets.API_ALERTS_KEY }}
          channel: 'developer'
          message: ${{ job.status == 'success' && '🚀 Android staging deployed' || '❌ Android staging failed' }}
          tags: 'deploy,staging,android'
          link: ${{ job.status == 'success' && 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID' || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}

The workflow now

  1. Push code to a PR
  2. Context switch. Make a coffee, quick power nap, play with the cat, review another PR, whatever
  3. Phone buzzes: ”🚀 Android staging deployed”
  4. Tap → Firebase App Distribution opens → install → test

Or:

  1. Phone buzzes: ”❌ Android staging failed”
  2. Tap → The GitHub Actions run opens → see exactly what broke

The entire process from “push code” to “testing the build on my phone” now requires zero attention. No more forgetting about builds, no more refreshing the Actions tab, and no more QA wondering why the build they asked for at standup still isn’t dev tested.

And yes, I did turn off the Firebase App Distribution emails. All my dev notifications are in one place now, and if I ever want to go back and look at build history, every event is logged in the mobile app and the web dashboard.

It’s not just mobile

I’ve taken this approach and applied it to my backend deployments, web builds, and even SDK releases. Any build or deployment that takes more than a minute and may fail now sends a notification.