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 pattern I use is two separate notify steps with if: success() and if: failure() so each outcome has its own clean configuration. GitHub Actions skips steps after a failure by default, so the if: guards are what make each branch fire.

- if: success()
  uses: apialerts/notify-action@v2
  with:
    event: ci.build.success
    channel: developer
    message: 'Build deployed'
    tags: deploy,staging
    link: 'https://your-test-link'

- if: failure()
  uses: apialerts/notify-action@v2
  with:
    event: ci.build.failed
    channel: developer
    message: 'Build failed'
    tags: deploy,staging
    link: ${{ format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}

I set APIALERTS_API_KEY once at the job level so I don’t have to repeat the credential on every notify step (more on that in the full workflow below).

Note the event: field. It’s optional but recommended, and it’s what lets you route different build outcomes to different destinations in 2.0 (a Slack channel for failures, a celebratory webhook for successes, etc.) without changing any of this workflow code. Routers match it with Unix glob (fnmatch), so a pattern like ci.* catches every CI event. Dotted keys like ci.build.failed are a clean convention, but free-form keys with spaces (User Signup matched by User *) work too.

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

- if: success()
  uses: apialerts/notify-action@v2
  with:
    event: ci.android.staging.success
    channel: developer
    message: '🚀 Android staging deployed'
    tags: deploy,staging,android
    link: 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID'

- if: failure()
  uses: apialerts/notify-action@v2
  with:
    event: ci.android.staging.failed
    channel: developer
    message: '❌ Android staging failed'
    tags: deploy,staging,android
    link: ${{ 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

- if: success()
  uses: apialerts/notify-action@v2
  with:
    event: ci.ios.staging.success
    channel: developer
    message: '🚀 iOS staging deployed'
    tags: deploy,staging,ios
    link: 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID'

- if: failure()
  uses: apialerts/notify-action@v2
  with:
    event: ci.ios.staging.failed
    channel: developer
    message: '❌ iOS staging failed'
    tags: deploy,staging,ios
    link: ${{ 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
    env:
      APIALERTS_API_KEY: ${{ secrets.API_ALERTS_KEY }}

    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 success
        if: success()
        uses: apialerts/notify-action@v2
        with:
          event: ci.android.staging.success
          channel: developer
          message: '🚀 Android staging deployed'
          tags: deploy,staging,android
          link: 'https://appdistribution.firebase.google.com/testerapps/YOUR_APP_ID'

      - name: Notify failure
        if: failure()
        uses: apialerts/notify-action@v2
        with:
          event: ci.android.staging.failed
          channel: developer
          message: '❌ Android staging failed'
          tags: deploy,staging,android
          link: ${{ 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.