Automating Builds with GitHub Actions
Automating Builds with GitHub Actions
Manually building, signing, and uploading a Flutter app before every release is error-prone and time-consuming. GitHub Actions solves this by running your entire build pipeline automatically whenever you push to a branch or create a tag. In this lesson you will write a production-ready workflow YAML that installs Flutter, executes your test suite, builds a signed Android AAB and an iOS IPA, and uploads both as downloadable artifacts — all without storing sensitive credentials in your repository.
How GitHub Actions Works
A workflow is a YAML file stored under .github/workflows/. GitHub reads it and runs each job on a cloud-hosted runner (Ubuntu, macOS, or Windows). Jobs contain steps: shell commands or reusable actions pulled from the Marketplace. For Flutter CI/CD you typically need two separate jobs — one on ubuntu-latest for Android and one on macos-latest for iOS — because the iOS toolchain only runs on macOS.
Storing Secrets Securely
Your Android keystore and iOS signing certificates must never be committed to the repository. GitHub provides an encrypted Secrets store (Settings → Secrets and variables → Actions). Binary files like keystores are base-64 encoded before storage and decoded inside the workflow:
KEYSTORE_BASE64— base-64 encoded.jks/.keystorefileKEY_ALIAS— alias used when you created the keystoreKEY_PASSWORD— key passwordSTORE_PASSWORD— keystore passwordIOS_P12_BASE64— base-64 encoded Distribution certificate (.p12)IOS_P12_PASSWORD— password for the .p12 filePROVISIONING_PROFILE_BASE64— base-64 encoded .mobileprovision file
Complete Workflow: Android Signed AAB
The following workflow triggers on every push to main and on version tags (v*). The Android job installs Flutter, caches pub packages, runs tests, decodes the keystore, builds a release AAB, and uploads it as an artifact.
.github/workflows/flutter_ci.yml — Android job
name: Flutter CI/CD
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
jobs:
# ── Android ─────────────────────────────────────────────
build_android:
name: Build Android AAB
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Cache pub packages
uses: actions/cache@v4
with:
path: ~/.pub-cache
key: pub-${{ runner.os }}-${{ hashFiles('**/pubspec.lock') }}
restore-keys: pub-${{ runner.os }}-
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
channel: stable
cache: true
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test --coverage
- name: Decode keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode \
> android/app/keystore.jks
- name: Build release AAB
run: flutter build appbundle --release
env:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: android-release-aab
path: build/app/outputs/bundle/release/app-release.aab
retention-days: 14
Complete Workflow: iOS Signed IPA
The iOS job runs on macos-latest. It imports the distribution certificate into a temporary keychain, installs the provisioning profile, builds the app with flutter build ipa, and uploads the resulting IPA.
.github/workflows/flutter_ci.yml — iOS job (append to same file)
# ── iOS ─────────────────────────────────────────────────
build_ios:
name: Build iOS IPA
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
channel: stable
cache: true
- name: Install dependencies
run: flutter pub get
- name: Import signing certificate
env:
P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
security create-keychain -p "" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "" "$KEYCHAIN_PATH"
echo "$P12_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12
security import "$RUNNER_TEMP/cert.p12" \
-k "$KEYCHAIN_PATH" -P "$P12_PASSWORD" \
-A -t cert -f pkcs12
security list-keychain -d user -s "$KEYCHAIN_PATH"
- name: Install provisioning profile
env:
PP_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
PP_PATH=$RUNNER_TEMP/profile.mobileprovision
echo "$PP_BASE64" | base64 --decode > "$PP_PATH"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PP_PATH" \
~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build iOS IPA
run: flutter build ipa --release --no-codesign
- name: Upload IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-release-ipa
path: build/ios/ipa/*.ipa
retention-days: 14
Configuring android/app/build.gradle for CI Signing
The workflow injects signing credentials via environment variables. Your android/app/build.gradle must read them so Gradle picks up the right keystore at build time:
android/app/build.gradle — signingConfigs block
android {
signingConfigs {
release {
keyAlias System.getenv("KEY_ALIAS") ?: "mykey"
keyPassword System.getenv("KEY_PASSWORD") ?: ""
storeFile file("keystore.jks")
storePassword System.getenv("STORE_PASSWORD") ?: ""
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Caching Strategies for Faster Runs
Cold builds can take 8–15 minutes. Effective caching cuts this to 2–4 minutes on subsequent runs:
- Pub cache — keyed on
pubspec.lock; saves ~30 seconds per run - Flutter SDK cache — the
subosito/flutter-actionaction has a built-incache: trueoption - Gradle cache — add a separate
actions/cachestep targeting~/.gradle/cachesand~/.gradle/wrapper - CocoaPods cache — on macOS, cache
ios/Podskeyed onios/Podfile.lock
flutter-version: '3.22.0') rather than using channel: stable alone. This makes builds reproducible: a new Flutter release will not silently break your pipeline until you deliberately bump the version.Summary
A well-structured GitHub Actions workflow gives you automated, reproducible Flutter builds on every push. The key points are: store all credentials as repository secrets and decode them at runtime; run tests before building so failures are caught early; use caching to keep runtimes under five minutes; and upload artifacts so QA and stakeholders can download the binaries directly from the Actions tab without needing a local environment.
.github/workflows/flutter_ci.yml can replace hours of manual release work with a fully automated, auditable pipeline.