Build Mobile workflow (gnmyt/Nexterm)
The Build Mobile workflow from gnmyt/Nexterm, explained and optimized by Latchkey.
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.
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
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
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cache dependency installs on the setup step so they are served from cache.
- Add a job timeout so a hung step cannot burn hours of runner time.
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.