Skip to content
Latchkey

Build Mobile workflow (gnmyt/Nexterm)

The Build Mobile workflow from gnmyt/Nexterm, explained and optimized by Latchkey.

C

CI health: C - fair

Point runs-on at Latchkey and get caching, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: gnmyt/Nexterm.github/workflows/build-mobile.ymlLicense MITView source

What it does

This is the Build Mobile workflow from the gnmyt/Nexterm repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Build Mobile

on:
  workflow_call:
    inputs:
      version:
        required: true
        type: string
      semver:
        required: true
        type: string
      upload_url:
        required: true
        type: string

jobs:
  build-apk:
    name: "APK (${{ matrix.abi }})"
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - abi: armeabi-v7a
            flutter_target: android-arm
          - abi: arm64-v8a
            flutter_target: android-arm64
          - abi: x86_64
            flutter_target: android-x64

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

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

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"

      - name: Setup Android signing
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > mobile/android/app/release.keystore
          cat > mobile/android/key.properties << EOF
          storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          storeFile=release.keystore
          EOF

      - name: Update pubspec.yaml version
        run: |
          sed -i "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml

      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get

      - name: Build APK
        working-directory: mobile
        run: flutter build apk --release --target-platform ${{ matrix.flutter_target }} --split-per-abi

      - name: Rename APK
        run: |
          mv mobile/build/app/outputs/flutter-apk/app-${{ matrix.abi }}-release.apk \
             mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk

      - name: Upload APK to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk
          asset_name: nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk
          asset_content_type: application/vnd.android.package-archive

  build-apk-universal:
    name: "APK (Universal)"
    runs-on: ubuntu-latest

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

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

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"

      - name: Setup Android signing
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > mobile/android/app/release.keystore
          cat > mobile/android/key.properties << EOF
          storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          storeFile=release.keystore
          EOF

      - name: Update pubspec.yaml version
        run: |
          sed -i "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml

      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get

      - name: Build Universal APK
        working-directory: mobile
        run: flutter build apk --release

      - name: Rename APK
        run: |
          mv mobile/build/app/outputs/flutter-apk/app-release.apk \
             mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-universal.apk

      - name: Upload Universal APK to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-universal.apk
          asset_name: nexterm-${{ inputs.version }}-universal.apk
          asset_content_type: application/vnd.android.package-archive

  build-ios:
    name: "iOS (Unsigned)"
    runs-on: macos-latest

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

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"

      - name: Update pubspec.yaml version
        run: |
          sed -i '' "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i '' "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml

      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get

      - name: Install CocoaPods dependencies
        working-directory: mobile/ios
        run: pod install

      - name: Build iOS
        working-directory: mobile
        run: |
          flutter build ios --release --no-codesign

      - name: Create unsigned IPA
        run: |
          mkdir -p mobile/build/ios/ipa
          APP_PATH=$(find mobile/build/ios -name "Runner.app" -type d | head -1)
          if [ -z "$APP_PATH" ]; then
            echo "Error: Runner.app not found"
            find mobile/build/ios -type d -name "*.app" || true
            exit 1
          fi
          echo "Found app at: $APP_PATH"
          WORK_DIR=$(mktemp -d)
          mkdir -p "$WORK_DIR/Payload"
          cp -r "$APP_PATH" "$WORK_DIR/Payload/"
          cd "$WORK_DIR"
          zip -r "$GITHUB_WORKSPACE/mobile/build/ios/ipa/nexterm-${{ inputs.version }}.ipa" Payload
          rm -rf "$WORK_DIR"

      - name: Upload unsigned IPA to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/ios/ipa/nexterm-${{ inputs.version }}.ipa
          asset_name: nexterm-${{ inputs.version }}.ipa
          asset_content_type: application/octet-stream

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

name: Build Mobile
 
on:
  workflow_call:
    inputs:
      version:
        required: true
        type: string
      semver:
        required: true
        type: string
      upload_url:
        required: true
        type: string
 
jobs:
  build-apk:
    timeout-minutes: 30
    name: "APK (${{ matrix.abi }})"
    runs-on: latchkey-small
    strategy:
      fail-fast: false
      matrix:
        include:
          - abi: armeabi-v7a
            flutter_target: android-arm
          - abi: arm64-v8a
            flutter_target: android-arm64
          - abi: x86_64
            flutter_target: android-x64
 
    steps:
      - name: Checkout project
        uses: actions/checkout@v4
 
      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          cache: 'maven'
          distribution: "temurin"
          java-version: "17"
 
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"
 
      - name: Setup Android signing
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > mobile/android/app/release.keystore
          cat > mobile/android/key.properties << EOF
          storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          storeFile=release.keystore
          EOF
 
      - name: Update pubspec.yaml version
        run: |
          sed -i "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml
 
      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get
 
      - name: Build APK
        working-directory: mobile
        run: flutter build apk --release --target-platform ${{ matrix.flutter_target }} --split-per-abi
 
      - name: Rename APK
        run: |
          mv mobile/build/app/outputs/flutter-apk/app-${{ matrix.abi }}-release.apk \
             mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk
 
      - name: Upload APK to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk
          asset_name: nexterm-${{ inputs.version }}-${{ matrix.abi }}.apk
          asset_content_type: application/vnd.android.package-archive
 
  build-apk-universal:
    timeout-minutes: 30
    name: "APK (Universal)"
    runs-on: latchkey-small
 
    steps:
      - name: Checkout project
        uses: actions/checkout@v4
 
      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          cache: 'maven'
          distribution: "temurin"
          java-version: "17"
 
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"
 
      - name: Setup Android signing
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > mobile/android/app/release.keystore
          cat > mobile/android/key.properties << EOF
          storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          storeFile=release.keystore
          EOF
 
      - name: Update pubspec.yaml version
        run: |
          sed -i "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml
 
      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get
 
      - name: Build Universal APK
        working-directory: mobile
        run: flutter build apk --release
 
      - name: Rename APK
        run: |
          mv mobile/build/app/outputs/flutter-apk/app-release.apk \
             mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-universal.apk
 
      - name: Upload Universal APK to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/app/outputs/flutter-apk/nexterm-${{ inputs.version }}-universal.apk
          asset_name: nexterm-${{ inputs.version }}-universal.apk
          asset_content_type: application/vnd.android.package-archive
 
  build-ios:
    timeout-minutes: 30
    name: "iOS (Unsigned)"
    runs-on: macos-latest
 
    steps:
      - name: Checkout project
        uses: actions/checkout@v4
 
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.44.0"
          channel: "stable"
 
      - name: Update pubspec.yaml version
        run: |
          sed -i '' "s/^version: .*/version: ${{ inputs.semver }}+${{ github.run_number }}/" mobile/pubspec.yaml
          sed -i '' "s/sdk: \^3.9.2/sdk: ^3.8.0/" mobile/pubspec.yaml
 
      - name: Install dependencies
        working-directory: mobile
        run: flutter pub get
 
      - name: Install CocoaPods dependencies
        working-directory: mobile/ios
        run: pod install
 
      - name: Build iOS
        working-directory: mobile
        run: |
          flutter build ios --release --no-codesign
 
      - name: Create unsigned IPA
        run: |
          mkdir -p mobile/build/ios/ipa
          APP_PATH=$(find mobile/build/ios -name "Runner.app" -type d | head -1)
          if [ -z "$APP_PATH" ]; then
            echo "Error: Runner.app not found"
            find mobile/build/ios -type d -name "*.app" || true
            exit 1
          fi
          echo "Found app at: $APP_PATH"
          WORK_DIR=$(mktemp -d)
          mkdir -p "$WORK_DIR/Payload"
          cp -r "$APP_PATH" "$WORK_DIR/Payload/"
          cd "$WORK_DIR"
          zip -r "$GITHUB_WORKSPACE/mobile/build/ios/ipa/nexterm-${{ inputs.version }}.ipa" Payload
          rm -rf "$WORK_DIR"
 
      - name: Upload unsigned IPA to Release
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ inputs.upload_url }}
          asset_path: mobile/build/ios/ipa/nexterm-${{ inputs.version }}.ipa
          asset_name: nexterm-${{ inputs.version }}.ipa
          asset_content_type: application/octet-stream
 

What changed

1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.

This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow