0Pricing
Flutter Mobile Development · 강의

Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인

Fastlane 레인과 Actions 작업 흐름을 사용해 자동으로 빌드하고 서명한 뒤 스토어에 배포합니다.

Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Automate Flutter Releases

Shipping a Flutter app to both stores by hand is slow and error-prone: bumping versions, building IPA/AAB, signing, uploading, and writing release notes for every platform.

A CI/CD pipeline turns this into a single trigger. We combine two tools:

  • Fastlane — Ruby-based automation for the iOS and Android store steps (signing, building, uploading to TestFlight / Play Console).
  • GitHub Actions — the orchestrator that checks out code, sets up Flutter, and invokes Fastlane on a hosted runner.

Think of GitHub Actions as the conductor and Fastlane lanes as the per-platform musicians.

Version and Build Number Strategy

Stores reject uploads that reuse a build number. Your pipeline must compute a fresh, monotonically increasing number on every run.

In Flutter, pubspec.yaml holds version: 1.4.0+57 where 1.4.0 is the user-facing name and 57 is the build number. A common CI pattern is to feed the GitHub Actions run number into the build.

Here is a small Dart helper that derives the full version string from a base name and a CI counter.

void main() {
  String buildVersion(String name, int ciRunNumber) {
    if (ciRunNumber < 1) {
      throw ArgumentError('Build number must be >= 1');
    }
    return '$name+$ciRunNumber';
  }

  print(buildVersion('1.4.0', 57)); // 1.4.0+57
  print(buildVersion('2.0.0', 120)); // 2.0.0+120
}

Anatomy of a Fastlane Lane

Fastlane configuration lives in a Fastfile (Ruby). A lane is a named sequence of actions for one task on one platform.

For Android, a release lane typically builds the App Bundle with Flutter, then uploads it to a Play Console track:

  • sh 'flutter build appbundle --release' — produces the signed .aab.
  • upload_to_play_store — pushes it to the internal or production track.

The track parameter is the key decision: start on internal for QA, promote to production later.

An Android Fastlane Lane

This is a typical android/fastlane/Fastfile. Fastlane runs from the android/ directory, so we call Flutter from the project root with ...

Note how the lane uploads to the internal track first and skips the metadata/images upload, which avoids accidental store-listing changes from CI.

default_platform(:android)

platform :android do
  desc 'Build and upload AAB to the internal track'
  lane :beta do
    sh 'flutter build appbundle --release'
    upload_to_play_store(
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab',
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

An iOS Fastlane Lane

The iOS lane builds an IPA and ships it to TestFlight. Code signing on CI uses match, which stores certificates and provisioning profiles in a private Git repo and installs them on the runner.

  • setup_ci — creates a temporary keychain so signing works on an ephemeral runner.
  • match(type: 'appstore', readonly: true) — fetches signing assets without regenerating them.
  • upload_to_testflight — distributes the build to internal testers.
default_platform(:ios)

platform :ios do
  desc 'Build and upload to TestFlight'
  lane :beta do
    setup_ci
    match(type: 'appstore', readonly: true)
    sh 'flutter build ipa --release --export-options-plist=ExportOptions.plist'
    upload_to_testflight(
      ipa: '../build/ios/ipa/Runner.ipa',
      skip_waiting_for_build_processing: true
    )
  end
end

Secrets Belong in the Vault

Never commit signing keys, the Play service-account JSON, or App Store Connect API keys. Store them as encrypted GitHub Actions secrets and inject them as environment variables at runtime.

Common secrets for a Flutter pipeline:

  • PLAY_STORE_JSON_KEY — Google Play service account credentials.
  • APP_STORE_CONNECT_API_KEY — ASC key for TestFlight uploads.
  • MATCH_PASSWORD and MATCH_GIT_BASIC_AUTH — to decrypt the match repo.
  • ANDROID_KEYSTORE_BASE64 — your upload keystore, base64-encoded.

A small Dart validator can fail fast if a required variable is missing before any expensive build step runs.

void main() {
  List<String> missingSecrets(Map<String, String?> env, List<String> required) {
    return required.where((k) {
      final v = env[k];
      return v == null || v.trim().isEmpty;
    }).toList();
  }

  final fakeEnv = {
    'PLAY_STORE_JSON_KEY': '{...}',
    'MATCH_PASSWORD': '',
  };
  final required = ['PLAY_STORE_JSON_KEY', 'MATCH_PASSWORD', 'ASC_KEY'];

  final missing = missingSecrets(fakeEnv, required);
  if (missing.isNotEmpty) {
    print('Missing secrets: ${missing.join(', ')}');
  } else {
    print('All secrets present');
  }
}

The GitHub Actions Workflow

The workflow YAML lives in .github/workflows/release.yml. It defines when the pipeline runs and which runner executes it.

Key choices:

  • Trigger: run on a pushed tag like v* so only intentional releases fire.
  • Runner: Android jobs can use ubuntu-latest; iOS must use macos-latest because Xcode is required.
  • Matrix or separate jobs let both platforms build in parallel.

Each job checks out code, runs subosito/flutter-action to install the SDK, then calls the matching Fastlane lane.

A Release Workflow YAML

This workflow fires on a version tag and builds both platforms in parallel jobs. Notice the iOS job runs on macOS and the Android job on Ubuntu.

Secrets flow in through the env block, so Fastlane and match read them without any value being written to disk in plaintext.

name: Release
on:
  push:
    tags: ['v*']

jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { channel: stable }
      - run: flutter pub get
      - run: bundle exec fastlane beta
        working-directory: android
        env:
          PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}

  ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { channel: stable }
      - run: flutter pub get
      - run: bundle exec fastlane beta
        working-directory: ios
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

Gate the Pipeline with Tests

A release pipeline should never ship a red build. Run flutter analyze and flutter test in an early job and make the build jobs depend on it via needs:.

If any step exits non-zero, GitHub Actions stops the pipeline. You can mirror this gating logic in Dart: a release is allowed only when analysis is clean and all tests pass.

void main() {
  bool canRelease({
    required bool analyzeClean,
    required int testsPassed,
    required int testsTotal,
  }) {
    return analyzeClean && testsTotal > 0 && testsPassed == testsTotal;
  }

  print(canRelease(analyzeClean: true, testsPassed: 42, testsTotal: 42));
  print(canRelease(analyzeClean: true, testsPassed: 41, testsTotal: 42));
  print(canRelease(analyzeClean: false, testsPassed: 42, testsTotal: 42));
}

Observability: Crash and Build Reporting

Shipping is only half the story — you need to know what happens after release. Wire observability in two places:

  • App side: integrate Firebase Crashlytics or Sentry so production crashes are reported with stack traces and the exact build number.
  • Pipeline side: upload dSYM/ProGuard symbol files during the Fastlane lane (e.g. upload_symbols_to_crashlytics) so crash reports are de-obfuscated.

Always tag reports with the same build number your pipeline generated, so a crash maps back to a specific commit and CI run.

import 'dart:async';

void main() {
  runZonedGuarded(() {
    // Simulated app start that throws in production code.
    throw StateError('Null user session at startup');
  }, (error, stack) {
    // In a real app this would call Crashlytics.recordError.
    final report = {
      'build': 57,
      'error': error.toString(),
      'firstFrame': stack.toString().split('\n').first,
    };
    print('Reported crash: $report');
  });
}

Promotion and Staged Rollout

Mature pipelines do not push straight to 100% of users. Two safety patterns:

  • Track promotion: CI uploads to internal; a separate manually-triggered job promotes the same artifact to production.
  • Staged rollout: release to a fraction of users first, then increase. Fastlane's upload_to_play_store accepts rollout: '0.1' for a 10% start.

This Dart snippet models a rollout schedule that doubles exposure each day, capped at 100%.

void main() {
  List<double> rolloutSchedule(double start, int days) {
    final stages = <double>[];
    var pct = start;
    for (var i = 0; i < days; i++) {
      stages.add(double.parse(pct.clamp(0.0, 1.0).toStringAsFixed(2)));
      pct *= 2;
    }
    return stages;
  }

  print(rolloutSchedule(0.1, 5)); // [0.1, 0.2, 0.4, 0.8, 1.0]
}

Quick Check: Runner Choice

Test your understanding of the platform constraints in a Flutter release pipeline.

Recap

You built a mental model of a production Flutter release pipeline:

  • GitHub Actions orchestrates; Fastlane lanes handle per-platform store work.
  • Derive a unique build number per run from the CI counter and pubspec.yaml.
  • Android lanes build an .aab and upload_to_play_store; iOS lanes use match + upload_to_testflight.
  • Keep keys in encrypted secrets; validate they exist before building.
  • Run on the right runner — macos-latest for iOS, ubuntu for Android.
  • Gate releases behind flutter analyze and flutter test with needs:.
  • Close the loop with crash reporting, symbol upload, and staged rollout.

Tag a commit with v1.4.0 and your whole release runs itself.

자주 묻는 질문

“Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인” 강의는 무료인가요?

네 — “Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.

“Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인”에서 뭘 배우나요?

Fastlane 레인과 Actions 작업 흐름을 사용해 자동으로 빌드하고 서명한 뒤 스토어에 배포합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 빌드 플레이버 및 환경 구성
  2. Fastlane 및 GitHub Actions를 활용한 자동화 파이프라인
  3. 충돌 보고 및 기호화된 스택 추적
  4. 원격 구성, 기능 플래그 및 단계적 출시
← Flutter Mobile Development(으)로 돌아가기