使用 Fastlane 和 GitHub Actions 实现自动化流水线
使用 Fastlane 任务和 Actions 工作流自动构建、签名并发布到应用商店。
使用 Fastlane 和 GitHub Actions 实现自动化流水线 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 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
endAn 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
endSecrets 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_PASSWORDandMATCH_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 usemacos-latestbecause 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 toproduction. - Staged rollout: release to a fraction of users first, then increase. Fastlane's
upload_to_play_storeacceptsrollout: '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
.aabandupload_to_play_store; iOS lanes usematch+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 analyzeandflutter testwithneeds:. - 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 实现自动化流水线」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「使用 Fastlane 和 GitHub Actions 实现自动化流水线」这节课中我会学到什么?
使用 Fastlane 任务和 Actions 工作流自动构建、签名并发布到应用商店。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「使用 Fastlane 和 GitHub Actions 实现自动化流水线」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 构建变体与环境配置
- 使用 Fastlane 和 GitHub Actions 实现自动化流水线
- 崩溃报告与符号化堆栈跟踪
- 远程配置、功能开关与分阶段发布