---
title: GitHub Actions
description: Automate AppFlight uploads on every push using GitHub Actions.
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.page/mhmzdev/appflight-cli-docs/llms.txt
> Use this file to discover all available pages before exploring further.

## Setup

1. **Create a CI key** — run `appflight keys create --name "GitHub Actions"` on your machine. The raw key is printed once; copy it now.

:::warning Do not use your laptop's login key in CI
`appflight login` mints a key named `cli:<hostname>` for the machine you ran it on, and the **next**
`login` on that machine revokes it and mints a fresh one. Paste that key into a pipeline and the
pipeline starts failing with exit `6` the next time you sign in on your laptop — with nothing in the
CI logs to explain why. A key from `keys create` is never rotated, which is the whole reason it
exists.
:::
2. **Add secret** — GitHub repo → Settings → Secrets and variables → Actions → New repository secret → `APPFLIGHT_API_KEY`
3. **Commit `appflight.json`** to your repository (run `appflight init` locally first)

## Workflow

Create `.github/workflows/appflight.yml`:

<Tabs groupId="framework">
  <TabItem label="Flutter" value="flutter">

    ```yaml
    name: Upload to AppFlight

    on:
      push:
        branches: [main]

    jobs:
      build_and_upload:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4

          - name: Setup Java
            uses: actions/setup-java@v4
            with:
              distribution: 'zulu'
              java-version: '17'

          - name: Setup Flutter
            uses: subosito/flutter-action@v2
            with:
              flutter-version: '3.38.2'   # pin your version — see note below
              cache: true

          - name: Setup Dart
            uses: dart-lang/setup-dart@v1

          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build APK
            run: flutter build apk --flavor stage --release

          - name: Upload to AppFlight
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci
    ```

    :::tip Why pin the Flutter version?
    Using `flutter-version: '3.x'` resolves to whatever the latest stable is at build time, which can silently introduce breaking changes. Pin to the same version your team uses locally (e.g. `3.38.2`). Update the pin deliberately when you upgrade.
    :::

  </TabItem>
  <TabItem label="React Native" value="react-native">

    ```yaml
    name: Upload to AppFlight

    on:
      push:
        branches: [main]

    jobs:
      build_and_upload:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4

          - name: Setup Java
            uses: actions/setup-java@v4
            with:
              distribution: 'zulu'
              java-version: '17'

          - name: Setup Node
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm'

          - name: Install dependencies
            run: npm install

          - name: Setup Dart
            uses: dart-lang/setup-dart@v1

          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build APK
            run: cd android && ./gradlew assembleStageRelease

          - name: Upload to AppFlight
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci
    ```

    :::tip Using Yarn?
    Replace `cache: 'npm'` with `cache: 'yarn'` and `npm install` with `yarn install`.
    :::

  </TabItem>
  <TabItem label="Kotlin" value="kotlin">

    ```yaml
    name: Upload to AppFlight

    on:
      push:
        branches: [main]

    jobs:
      build_and_upload:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4

          - name: Setup Java
            uses: actions/setup-java@v4
            with:
              distribution: 'zulu'
              java-version: '17'

          - name: Setup Dart
            uses: dart-lang/setup-dart@v1

          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build APK
            run: ./gradlew assembleStageRelease

          - name: Upload to AppFlight
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci
    ```

  </TabItem>
</Tabs>

:::warning PATH for the installed binary
`dart pub global activate appflight_cli` installs the `appflight` binary into `$HOME/.pub-cache/bin`, which is **not** on the runner's PATH by default. The `echo ... >> $GITHUB_PATH` line above adds it for every subsequent step. Without it, the next step fails with `appflight: command not found`.
:::

:::info Java is required for the Android build
`actions/setup-java@v4` is required for Flutter, React Native, and Kotlin — the Android Gradle Plugin runs on the JDK. Match the JDK your local Android Studio uses (17 is the current default).
:::

## Multi-flavor

Install the CLI once, then repeat the build and upload steps per flavor:

<Tabs groupId="framework">
  <TabItem label="Flutter" value="flutter">

    ```yaml
          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build stage APK
            run: flutter build apk --flavor stage --release

          - name: Upload stage
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci

          - name: Build qa APK
            run: flutter build apk --flavor qa --release

          - name: Upload qa
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor qa --ci
    ```

  </TabItem>
  <TabItem label="React Native" value="react-native">

    ```yaml
          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build stage APK
            run: cd android && ./gradlew assembleStageRelease

          - name: Upload stage
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci

          - name: Build qa APK
            run: cd android && ./gradlew assembleQaRelease

          - name: Upload qa
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor qa --ci
    ```

  </TabItem>
  <TabItem label="Kotlin" value="kotlin">

    ```yaml
          - name: Install AppFlight CLI
            run: |
              dart pub global activate appflight_cli
              echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH

          - name: Build stage APK
            run: ./gradlew assembleStageRelease

          - name: Upload stage
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor stage --ci

          - name: Build qa APK
            run: ./gradlew assembleQaRelease

          - name: Upload qa
            env:
              APPFLIGHT_API_KEY: ${{ secrets.APPFLIGHT_API_KEY }}
            run: appflight upload --flavor qa --ci
    ```

  </TabItem>
</Tabs>

## Faster builds

On ephemeral runners, Gradle caches are cold on every run. This optional step usually cuts ~1–2 minutes:

```yaml
      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: gradle-${{ runner.os }}-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/app/build.gradle.kts') }}
          restore-keys: gradle-${{ runner.os }}-
```

For Flutter projects, also cache pub packages:

```yaml
      - name: Cache pub packages
        uses: actions/cache@v4
        with:
          path: ~/.pub-cache
          key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }}
          restore-keys: pub-${{ runner.os }}-
```

Place both **after** your SDK setup step and **before** `Install AppFlight CLI`.

## Smaller, faster APKs for testing (Flutter)

For CI builds that only need to land on testers' devices (not the Play Store), an `arm64-only` APK is ~40% smaller and builds faster:

```yaml
      - name: Build APK
        run: flutter build apk --flavor stage --release --target-platform android-arm64
```

Skip this for production Play Store builds — Play requires a universal AAB or the full set of ABIs.

## Exit codes

If `appflight upload` exits non-zero the workflow fails. Common codes:

| Code | Meaning | Fix |
|---|---|---|
| 4 | APK not found | Check `apkPath` in `appflight.json` matches your build output |
| 5 | Version already uploaded | Bump version in `pubspec.yaml` (Flutter), `android/app/build.gradle` (React Native), or `app/build.gradle` (Kotlin) |
| 6 | API key invalid or revoked | Check with `appflight keys list`; mint a fresh one with `appflight keys create --name …` |
| 8 | Plan limit reached | Run `appflight upgrade` for the upgrade path |
| 9 | Forbidden — your role may not do this | Only org admins and developers can create apps or upload to org apps |
