跳到主要内容

Gitea Actions & Flutter CI/CD 完整指南

· 阅读需 24 分钟
Sébastien Lorber
Docusaurus maintainer
Yangshun Tay
Ex-Meta Staff Engineer, Co-founder GreatFrontEnd

Gitea Actions 与 Flutter 双端自动化构建完整指南

本指南涵盖了从 Gitea Actions 环境搭建、macOS Runner 配置、Fastlane Match 证书管理、Fastlane firebase_app_distribution Android 自动分发,到 Flutter 工程并行构建工作流配置的全流程。


一、Gitea Actions 安装与配置​

1.1 版本要求​

Gitea 1.19.0 及以上版本支持 Actions 功能,1.21.0 及以上版本默认启用。

如果你使用的是 1.21.0 之前的版本,需在 Gitea 配置文件 app.ini 中手动启用:

[actions]
ENABLED = true

修改后重启 Gitea 服务。

1.2 获取注册 Token​

Runner 注册到 Gitea 实例需要 Token,Token 有三种级别:

级别获取路径适用范围
实例级<Gitea地址>/admin/actions/runners所有仓库可用
组织级<Gitea地址>/<org>/settings/actions/runners该组织下仓库可用
仓库级<Gitea地址>/<owner>/<repo>/settings/actions/runners仅该仓库可用

注意: Token 只能使用一次,注册后即失效。

1.3 安装 Act Runner(macOS 二进制方式,推荐)​

由于 iOS 构建强制依赖 macOS 宿主机环境(Xcode、钥匙串、签名证书等),必须使用二进制方式安装 Runner 并配置为 host 模式,使 Job 直接在宿主机上执行而非 Docker 容器中。

Step 1:下载 Runner 二进制文件​

在 macOS 机器上根据芯片架构下载对应版本:

# Apple Silicon Mac (M1/M2/M3)
curl -L https://dl.gitea.com/act_runner/latest/act_runner-latest-darwin-arm64 -o act_runner

# Intel Mac
curl -L https://dl.gitea.com/act_runner/latest/act_runner-latest-darwin-amd64 -o act_runner

chmod +x act_runner
./act_runner --version

Step 2:生成并修改配置文件​

./act_runner generate-config > config.yaml

编辑 config.yaml,将 labels 部分修改为 host 模式:

# 关键:将默认的 docker 模式改为 host 模式
labels:
- "macos-latest:host"
- "macos-13:host"

默认标签类似 ubuntu-latest:docker://node:16-bullseye,表示用 Docker 容器运行。改为 :host 后缀后,Job 将直接在 macOS 宿主机上执行,可以访问完整的 Xcode、Android SDK、钥匙串等环境。

Step 3:注册 Runner​

./act_runner register \
--no-interactive \
--instance http://<你的Gitea服务器IP>:3000 \
--token <你的Token> \
--name flutter-macos-runner \
--labels "macos-latest:host" \
--config config.yaml

关键参数说明:

  • --instance:必须使用局域网 IP 或域名,禁止使用 127.0.0.1 或 localhost
  • --labels:设置 Runner 标签,工作流中通过 runs-on 匹配该标签

注册信息会存储在 .runner 文件中,不要手动编辑。

Step 4:启动 Runner​

./act_runner daemon --config config.yaml

建议配合 launchd 或 pm2 实现开机自启和进程守护。

1.4 在仓库中启用 Actions​

即使 Gitea 实例启用了 Actions,每个仓库默认仍是禁用状态。需要手动开启:

  1. 进入仓库设置页面:<Gitea地址>/<owner>/<repo>/settings
  2. 勾选 "Enable Repository Actions"

1.5 验证 Runner 注册成功​

访问 Gitea 管理后台的 Runners 页面,确认新注册的 Runner 状态为 在线,且标签显示为 macos-latest:host。


二、macOS 机器配置要求​

由于 iOS 构建强制依赖 macOS 环境,而 macOS 同时也能构建 Android,因此推荐使用一台 macOS 机器作为 Runner 来同时承担双端构建任务。

2.1 硬件配置要求​

配置项最低要求推荐配置
CPU4 核8 核(Apple Silicon M1/M2/M3 更佳)
内存8 GB16 GB 及以上
磁盘空间44 GB70 GB 及以上(SSD)
芯片架构Intel x64 或 Apple Silicon arm64Apple Silicon(M系列构建 Android 速度提升约 50%)

重要提示: 双端同时构建对内存和磁盘 I/O 要求较高,8GB 内存容易因内存不足触发页面交换,导致构建失败。

2.2 软件环境要求​

软件版本要求说明
macOS12 (Monterey) 或更高Flutter 3.47+ 最低要求 macOS 12
Xcode15.0 或更高从 App Store 下载,安装后执行 sudo xcodebuild -runFirstLaunch
Android Studio最新版通过 SDK Manager 安装 Android SDK、Build-Tools、Platform-Tools
Flutter SDKstable 频道根据芯片选择 arm64(Apple Silicon)或 x64(Intel)版本
CocoaPods最新版iOS 原生依赖管理,brew install cocoapods
Java JDK17Android 构建必需,brew install --cask temurin
Ruby3.0+Fastlane 运行环境,brew install ruby
Fastlane最新版自动化构建和上传工具,brew install fastlane
Docker最新版部分 Action 内部可能依赖 Docker 功能
Firebase CLI最新版Fastlane firebase_app_distribution 插件依赖,brew install firebase-cli

2.3 环境配置步骤​

Step 1:安装 Homebrew

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Step 2:安装 Flutter SDK

# Apple Silicon Mac 下载 arm64 版本
brew install --cask flutter

Step 3:安装 Xcode 并配置

# 从 App Store 安装 Xcode 后执行
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

Step 4:安装 CocoaPods

brew install cocoapods
pod setup

Step 5:安装 Android Studio 和 SDK

从官网下载 macOS 版本的 Android Studio,打开后通过 SDK Manager 安装:

  • Android SDK Build-Tools
  • Android SDK Platform-Tools
  • Android SDK Platforms(目标 API 版本)
  • Android SDK Command-line Tools

Step 6:安装 Java 17

brew install --cask temurin

Step 7:安装 Ruby 和 Fastlane

brew install ruby
brew install fastlane

验证 Fastlane 安装:

fastlane --version

Step 8:安装 Firebase CLI(Fastlane firebase_app_distribution 插件依赖)

brew install firebase-cli
firebase login:ci

执行 firebase login:ci 后会输出一个长字符串(Token),请妥善保存,后续需配置到 Gitea Secrets 中。Fastlane firebase_app_distribution 插件在 CI 环境中依赖 Firebase CLI 进行身份验证。

Step 9:接受 Android 许可证

flutter doctor --android-licenses

Step 10:配置环境变量

编辑 ~/.zshrc(macOS 默认 shell),添加以下内容:

export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$HOME/flutter/bin

# 国内网络加速(可选)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

使配置生效:

source ~/.zshrc

Step 11:验证环境

flutter doctor -v

确认输出中 Android toolchain 和 iOS toolchain 均显示绿色对勾。


三、Fastlane Match 证书管理​

Fastlane Match 的核心思想是将所有 iOS 签名证书和描述文件加密后存储在一个私有 Git 仓库中,团队成员和 CI 服务器通过 Match 同步,实现:

优势说明
团队共享所有人共用同一套证书,避免重复生成
自动修复证书过期或缺失时自动重新生成
版本控制所有变更记录可追溯,支持回滚
CI 友好无需手动导入证书,CI 环境一键同步

3.1 创建私有证书仓库​

在 GitHub / GitLab / Gitea 上创建一个私有仓库,用于存储加密后的证书和描述文件:

# 例如在 GitHub 上创建
# https://github.com/yourteam/ios-certificates

务必设为私有仓库,证书文件即使加密也不应公开。

3.2 初始化 Match​

进入项目 iOS 目录并初始化:

cd /path/to/your/flutter-project/ios
fastlane match init

执行后会提示选择存储方式,选择 Git,然后输入证书仓库 URL:

? Please choose the storage method for your certificates and profiles:
> git
> google_cloud
> s3

? Please enter the URL of the Git repository:
git@github.com:yourteam/ios-certificates.git

3.3 配置 Matchfile​

初始化后会在 ios/fastlane/ 目录下生成 Matchfile,编辑如下:

git_url("git@github.com:yourteam/ios-certificates.git")
storage_mode("git")
type("appstore") # 默认类型
app_identifier(["com.yourcompany.yourapp"]) # 替换为你的 Bundle ID
username("your_apple_id@example.com") # Apple Developer 账号

如果项目有多个 App(如主应用、扩展、Watch 等),在 app_identifier 数组中添加:

app_identifier([
"com.yourcompany.yourapp",
"com.yourcompany.yourapp.NotificationService"
])

3.4 生成证书和描述文件​

首次使用 Match 需要生成证书并上传到仓库:

# 生成 App Store 发布证书
fastlane match appstore

# 生成开发证书(可选)
fastlane match development

# 生成 Ad-hoc 证书(可选,用于内部测试分发)
fastlane match adhoc

执行过程:

  1. 首次会要求输入 加密密码(用于加密仓库中的证书文件),务必妥善保管
  2. Match 会自动登录 Apple Developer 后台,创建证书和描述文件
  3. 证书和描述文件加密后推送到 Git 仓库
  4. 本地自动安装到钥匙串

提示: 如果已有证书,Match 会检测并复用;如果证书过期或缺失,会自动重新生成。

3.5 配置 Appfile​

在 ios/fastlane/Appfile 中配置 App 基本信息:

app_identifier("com.yourcompany.yourapp")
apple_id("your_apple_id@example.com")
team_id("YOUR_TEAM_ID") # Apple Developer Team ID
itc_team_id("YOUR_ITC_TEAM_ID") # iTunes Connect Team ID(如果与 team_id 不同)

Team ID 可在 Apple Developer 后台查看。

3.6 生成 App Store Connect API Key(推荐)​

相比 Apple ID + 密码,API Key 更安全且支持自动化:

  1. 登录 App Store Connect
  2. 进入 Users and Access > Keys
  3. 点击 Generate API Key
  4. 设置角色为 App Manager(最小权限原则)
  5. 下载 .p8 文件,记录 Key ID 和 Issuer ID

将 .p8 文件转为 Base64 编码(避免文件传输问题):

cat AuthKey_XXXXX.p8 | base64

四、Fastfile 配置​

在 ios/fastlane/Fastfile 中定义构建和上传流程:

default_platform(:ios)

# 全局前置:创建临时钥匙链并同步证书
before_all do
# CI 环境下创建临时钥匙链(避免污染系统钥匙串)
if ENV["CI"]
create_keychain(
name: "app_signing_keychain",
password: "temporary_password",
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end

# 从 Git 仓库同步证书和描述文件
match(
type: "appstore",
readonly: true, # CI 上设为 true,避免意外创建新证书
keychain_name: "app_signing_keychain",
keychain_password: "temporary_password",
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
end

# 全局后置:清理临时钥匙链
after_all do
if ENV["CI"]
delete_keychain(name: "app_signing_keychain")
end
end

# 发生错误时也清理
error do |lane, exception|
if ENV["CI"]
delete_keychain(name: "app_signing_keychain")
end
end

platform :ios do
# ==================== 构建并上传到 TestFlight ====================
desc "Build and upload to TestFlight"
lane :beta do
# 1. 自动递增 build 号
increment_build_number(xcodeproj: "Runner.xcodeproj")

# 2. 构建 IPA
build_app(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store", # App Store / TestFlight
clean: true,
# 显式指定签名配置(关键!避免 gym 自动检测失败)
export_options: {
signingStyle: "manual",
provisioningProfiles: Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
}
)

# 3. 生成 App Store Connect API Key
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_BASE64"],
duration: 1200,
in_house: false
)

# 4. 上传到 TestFlight
pilot(
api_key: api_key,
skip_waiting_for_build_processing: true, # 不等待 Apple 处理完成
skip_submission: true # 不自动提交审核
)
end

# ==================== 构建并上传到 App Store ====================
desc "Build and upload to App Store"
lane :release do
increment_build_number(xcodeproj: "Runner.xcodeproj")

build_app(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
clean: true,
export_options: {
signingStyle: "manual",
provisioningProfiles: Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
}
)

api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_BASE64"],
duration: 1200,
in_house: false
)

upload_to_app_store(
api_key: api_key,
skip_metadata: true,
skip_screenshots: true,
force: true
)
end

# ==================== 仅同步证书(不构建)====================
desc "Sync certificates from match repo"
lane :sync_certs do
match(
type: "appstore",
readonly: true,
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
end
end

==================== Android 平台配置 ====================

platform :android do desc "Build Android APK/AAB and distribute to Firebase App Distribution" lane :android_beta do

1. 构建 Release APK

gradle( task: "app:assembleRelease", build_type: "Release" )

2. 构建 App Bundle (AAB)

gradle( task: "app:bundleRelease", build_type: "Release" )

3. 分发 APK 到 Firebase App Distribution

firebase_app_distribution( app: ENV["FIREBASE_ANDROID_APP_ID"], firebase_cli_token: ENV["FIREBASE_TOKEN"], android_artifact_path: "app/build/outputs/apk/release/app-release.apk", release_notes: ENV["RELEASE_NOTES"] || "Auto build from CI", testers: ENV["FIREBASE_TESTERS"] || "tester1@example.com,tester2@example.com" )

4. 分发 AAB 到 Firebase App Distribution(推荐用于生产环境)

firebase_app_distribution(

app: ENV["FIREBASE_ANDROID_APP_ID"],

firebase_cli_token: ENV["FIREBASE_TOKEN"],

android_app_bundle: "app/build/outputs/bundle/release/app-release.aab",

release_notes: ENV["RELEASE_NOTES"] || "Auto build from CI",

testers: ENV["FIREBASE_TESTERS"] || "tester1@example.com,tester2@example.com"

)

end

desc "Sync certificates and prepare Android release" lane :android_release do

构建 Release AAB

gradle( task: "app:bundleRelease", build_type: "Release" )

可选:上传到 Google Play Store

play_deploy(

track: "internal",

aab: "app/build/outputs/bundle/release/app-release.aab"

)

end end

4.1 关键配置说明​

配置项说明
readonly: trueCI 环境必须设为 true,避免意外创建新证书
signingStyle: "manual"禁用 Xcode 自动签名,使用 Match 提供的描述文件
MATCH_PROVISIONING_PROFILE_MAPPINGMatch 自动生成的描述文件映射,直接传给 export_options
skip_waiting_for_build_processingTestFlight 上传后不等待 Apple 处理,节省 CI 时间

4.2 Xcode 项目配置​

使用 Match 后,需要在 Xcode 中禁用自动签名:

  1. 打开 ios/Runner.xcworkspace
  2. 选择 Runner Target > Signing & Capabilities
  3. 取消勾选 Automatically manage signing
  4. 将 Code Signing Style 设为 Manual

或者在 Fastfile 中通过 update_code_signing_settings 自动修改(推荐,避免手动操作):

# 在 match 之后、build_app 之前添加
update_code_signing_settings(
use_automatic_signing: false,
path: "Runner.xcodeproj",
code_sign_identity: "iPhone Distribution",
profile_name: Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING].values.first,
bundle_identifier: ENV["APP_IDENTIFIER"] || "com.yourcompany.yourapp"
)

五、Flutter 工程并行构建配置​

5.1 工作流文件结构​

在 Flutter 项目根目录创建 .gitea/workflows/flutter-dual-build.yml 文件。Gitea Actions 完全兼容 GitHub Actions 语法。

5.2 并行构建工作流(含 Fastlane 集成 + Firebase App Distribution)​

以下配置将 Android 和 iOS 构建拆分为两个独立的 Job,在同一台 macOS Runner 上并行执行。iOS 端集成 Fastlane Match 自动管理证书并上传 TestFlight;Android 端集成 Fastlane firebase_app_distribution 插件实现 Firebase App Distribution 自动分发:

name: Flutter Dual Platform Build

on:
push:
branches: [main, develop]
pull_request:
branches: [main]
release:
types: [published]

# 在仓库 Settings > Secrets 中配置以下变量:
# MATCH_GIT_BASIC_AUTHORIZATION
# MATCH_PASSWORD
# APP_STORE_CONNECT_API_KEY_ID
# APP_STORE_CONNECT_API_ISSUER_ID
# APP_STORE_CONNECT_API_KEY_BASE64
# FIREBASE_TOKEN

jobs:
# ==================== Android 构建任务 ====================
build-android:
name: Build Android APK/AAB & Distribute via Fastlane
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 'stable'
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Code analysis
run: flutter analyze

- name: Run tests
run: flutter test

- name: Build release APK
run: flutter build apk --release

- name: Build App Bundle (for Google Play)
run: flutter build appbundle --release

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: android-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 7

- 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: 7

- name: Set up Ruby and Fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android

- name: Add firebase_app_distribution plugin
run: |
cd android
fastlane add_plugin firebase_app_distribution

- name: Distribute to Firebase App Distribution
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
FIREBASE_ANDROID_APP_ID: ${{ secrets.FIREBASE_ANDROID_APP_ID }}
FIREBASE_TESTERS: ${{ secrets.FIREBASE_TESTERS }}
RELEASE_NOTES: "Auto build from $(git rev-parse --short HEAD)"
run: |
cd android
bundle exec fastlane android_beta

# ==================== iOS 构建任务 ====================
build-ios:
name: Build iOS & Upload to TestFlight
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 'stable'
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Install CocoaPods dependencies
run: |
cd ios
pod install

- name: Set up Ruby and Fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ios

- name: Configure Git for Match repo
run: |
git config --global user.email "ci@example.com"
git config --global user.name "CI Bot"

- name: Build and Upload to TestFlight
env:
CI: true
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
run: |
cd ios
bundle exec fastlane beta

- name: Upload IPA artifact (optional)
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-release-ipa
path: ios/build/Runner.ipa
retention-days: 7

5.3 工作流说明​

配置项说明
runs-on: macos-latest两个 Job 都指定 macOS Runner,Gitea 会自动并行调度
cache: true启用 Flutter 依赖缓存,显著加速后续构建
--no-codesigniOS 构建时不签名(Fastlane 中通过 Match 处理签名)
retention-days: 7构建产物保留 7 天,可根据需要调整
FIREBASE_TOKENFirebase CLI 认证 Token(Fastlane 插件用于身份验证)

5.4 触发条件​

工作流支持以下触发方式:

  • 推送代码到 main 或 develop 分支时自动构建
  • 提交 Pull Request 到 main 分支时自动构建
  • 发布 Release 时自动构建(适合正式打包场景)

六、Fastlane firebase_app_distribution 插件配置​

Fastlane 的 firebase_app_distribution 插件提供了在 Fastlane 流水线中直接调用 Firebase App Distribution 分发的能力。相比直接在 CI 工作流中执行 Firebase CLI 命令,使用 Fastlane 插件的优势在于:

优势说明
统一管理iOS (TestFlight) 和 Android (Firebase) 的分发逻辑都在 Fastfile 中定义
链式调用可以在同一个 lane 中完成构建、签名、分发全流程
环境隔离CI 环境下通过环境变量传递凭据,无需手动登录
错误处理Fastlane 内置的错误处理和日志输出更友好

6.1 安装插件​

在 Android 项目根目录(即 android/ 目录)执行以下命令安装插件:

cd android
fastlane add_plugin firebase_app_distribution

执行后会提示选择安装源,选择 Option 3: RubyGems.org 即可。

该命令会自动完成以下操作:

  1. 在 fastlane/Pluginfile 中添加 gem 'fastlane-plugin-firebase_app_distribution'
  2. 更新 Gemfile 和 Gemfile.lock
  3. 安装插件 gem 包

注意: 确保 android/fastlane/ 目录已初始化(运行过 fastlane init)。如果尚未初始化,先执行 fastlane init。

6.2 配置 Pluginfile​

插件安装后,android/fastlane/Pluginfile 应包含以下内容:

gem 'fastlane-plugin-firebase_app_distribution'

如果文件不存在,请手动创建并添加上述内容。

6.3 Firebase 身份验证配置​

Fastlane firebase_app_distribution 插件支持两种身份验证方式:

方式一:使用 Firebase CLI 登录(推荐用于 CI)​

在 macOS Runner 上执行以下命令生成认证 Token:

firebase login:ci

执行后会输出一个长字符串(Refresh Token),请妥善保存并配置到 Gitea Secrets 中,命名为 FIREBASE_TOKEN。

重要: Fastlane 插件底层仍然依赖 Firebase CLI。因此 Runner 上必须安装 Firebase CLI(brew install firebase-cli),但具体的登录和分发操作由 Fastlane 插件封装,无需在工作流 YAML 中直接调用 firebase 命令。

方式二:使用 Service Account 凭据(推荐用于生产环境)​

Service Account 方式无需 Token 过期问题,更适合生产环境:

Step 1:创建 Service Account

  1. 进入 Firebase 控制台 > 项目设置 > 服务账户
  2. 点击 生成新的私钥
  3. 下载 JSON 文件,保存为 firebase-service-account.json

Step 2:将私钥配置为 Gitea Secret

# 读取 JSON 文件内容并转为 Base64
cat firebase-service-account.json | base64

将 Base64 编码后的内容添加为 Gitea Secret,命名为 FIREBASE_SERVICE_ACCOUNT_JSON。

Step 3:在 Fastfile 中使用 Service Account

在 Fastfile 的 firebase_app_distribution 调用中,使用 service_credentials_file 参数替代 firebase_cli_token:

firebase_app_distribution(
app: ENV["FIREBASE_ANDROID_APP_ID"],
service_credentials_file: "/path/to/service-account.json",
android_artifact_path: "app/build/outputs/apk/release/app-release.apk",
release_notes: "Auto build from CI",
testers: "tester1@example.com"
)

6.4 Fastfile 完整配置(含 Android)​

以下是包含 iOS (TestFlight) 和 Android (Firebase App Distribution) 的完整 Fastfile 配置:

default_platform(:ios)

# 全局前置:创建临时钥匙链并同步证书
before_all do
# CI 环境下创建临时钥匙链(避免污染系统钥匙串)
if ENV["CI"]
create_keychain(
name: "app_signing_keychain",
password: "temporary_password",
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end

# 从 Git 仓库同步证书和描述文件
match(
type: "appstore",
readonly: true,
keychain_name: "app_signing_keychain",
keychain_password: "temporary_password",
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
end

# 全局后置:清理临时钥匙链
after_all do
if ENV["CI"]
delete_keychain(name: "app_signing_keychain")
end
end

# 发生错误时也清理
error do |lane, exception|
if ENV["CI"]
delete_keychain(name: "app_signing_keychain")
end
end

platform :ios do
# ==================== 构建并上传到 TestFlight ====================
desc "Build and upload to TestFlight"
lane :beta do
# 1. 自动递增 build 号
increment_build_number(xcodeproj: "Runner.xcodeproj")

# 2. 构建 IPA
build_app(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
clean: true,
export_options: {
signingStyle: "manual",
provisioningProfiles: Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
}
)

# 3. 生成 App Store Connect API Key
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_BASE64"],
duration: 1200,
in_house: false
)

# 4. 上传到 TestFlight
pilot(
api_key: api_key,
skip_waiting_for_build_processing: true,
skip_submission: true
)
end

# ==================== 构建并上传到 App Store ====================
desc "Build and upload to App Store"
lane :release do
increment_build_number(xcodeproj: "Runner.xcodeproj")

build_app(
scheme: "Runner",
workspace: "Runner.xcworkspace",
export_method: "app-store",
clean: true,
export_options: {
signingStyle: "manual",
provisioningProfiles: Actions.lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]
}
)

api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_BASE64"],
duration: 1200,
in_house: false
)

upload_to_app_store(
api_key: api_key,
skip_metadata: true,
skip_screenshots: true,
force: true
)
end

# ==================== 仅同步证书(不构建)====================
desc "Sync certificates from match repo"
lane :sync_certs do
match(
type: "appstore",
readonly: true,
git_basic_authorization: ENV["MATCH_GIT_BASIC_AUTHORIZATION"]
)
end
end

# ==================== Android 平台配置 ====================
platform :android do
desc "Build Android APK/AAB and distribute to Firebase App Distribution"
lane :android_beta do
# 1. 构建 Release APK
gradle(
task: "app:assembleRelease",
build_type: "Release"
)

# 2. 构建 App Bundle (AAB)
gradle(
task: "app:bundleRelease",
build_type: "Release"
)

# 3. 分发 APK 到 Firebase App Distribution
firebase_app_distribution(
app: ENV["FIREBASE_ANDROID_APP_ID"],
firebase_cli_token: ENV["FIREBASE_TOKEN"],
android_artifact_path: "app/build/outputs/apk/release/app-release.apk",
release_notes: ENV["RELEASE_NOTES"] || "Auto build from CI",
testers: ENV["FIREBASE_TESTERS"] || "tester1@example.com,tester2@example.com"
)

# 4. 分发 AAB 到 Firebase App Distribution(推荐用于生产环境)
# firebase_app_distribution(
# app: ENV["FIREBASE_ANDROID_APP_ID"],
# firebase_cli_token: ENV["FIREBASE_TOKEN"],
# android_app_bundle: "app/build/outputs/bundle/release/app-release.aab",
# release_notes: ENV["RELEASE_NOTES"] || "Auto build from CI",
# testers: ENV["FIREBASE_TESTERS"] || "tester1@example.com,tester2@example.com"
# )
end

desc "Sync certificates and prepare Android release"
lane :android_release do
# 构建 Release AAB
gradle(
task: "app:bundleRelease",
build_type: "Release"
)

# 可选:上传到 Google Play Store
# play_deploy(
# track: "internal",
# aab: "app/build/outputs/bundle/release/app-release.aab"
# )
end
end

6.5 firebase_app_distribution 参数说明​

参数说明必填
appFirebase 应用 ID(格式:1:project-id:android:app-id),可在 Firebase 控制台 > 项目设置 > 常规中获取是
firebase_cli_tokenFirebase CLI 登录时输出的 Refresh Token(CI 环境认证用)二选一
service_credentials_fileGoogle Service Account JSON 文件路径(生产环境推荐)二选一
android_artifact_pathAPK 文件路径否
android_app_bundleAAB 文件路径(推荐用于生产环境)否
release_notes版本说明文本否
release_notes_file版本说明文件路径否
testers测试人员邮箱列表(逗号分隔)否
testers_file测试人员邮箱文件路径否
groups测试组名称(逗号分隔,需先在 Firebase 控制台创建)否
groups_file测试组名称文件路径否

6.6 配置 Gitea Secrets​

进入仓库设置 > Secrets,添加以下变量:

Secret 名称值说明
FIREBASE_TOKENfirebase login:ci 输出的 TokenFirebase CLI 认证(Fastlane 插件使用)
FIREBASE_ANDROID_APP_IDAndroid 应用的 Firebase App ID格式:1:project-id:android:app-id
FIREBASE_TESTERS测试人员邮箱(逗号分隔)可选,也可在 Fastfile 中配置
MATCH_GIT_BASIC_AUTHORIZATIONBase64 编码的 Git 认证访问证书仓库
MATCH_PASSWORDMatch 加密密码解密证书文件
APP_STORE_CONNECT_API_KEY_IDKey IDApp Store Connect API
APP_STORE_CONNECT_API_ISSUER_IDIssuer IDApp Store Connect API
APP_STORE_CONNECT_API_KEY_BASE64Base64 编码的 .p8 文件API 私钥

6.7 首次本地验证​

在本地开发机器上首次配置 Fastlane firebase_app_distribution 插件:

# 1. 进入 Android 目录
cd android

# 2. 安装插件(如果尚未安装)
fastlane add_plugin firebase_app_distribution

# 3. 登录 Firebase(本地开发用)
firebase login

# 4. 测试构建和分发
fastlane android_beta

# 5. 指定测试人员和版本说明
fastlane android_beta env:{
"FIREBASE_ANDROID_APP_ID":"your-app-id",
"FIREBASE_TESTERS":"your-email@example.com",
"RELEASE_NOTES":"Local test build"
}

6.8 进阶:Service Account 认证(生产环境推荐)​

对于生产环境的 CI/CD,推荐使用 Service Account 而非 firebase login:ci Token,因为 Token 会过期而 Service Account 不会:

Step 1:创建 Service Account

  1. 进入 Firebase 控制台 > 项目设置 > 服务账户
  2. 点击 生成新的私钥
  3. 下载 JSON 文件,保存为 firebase-service-account.json

Step 2:将私钥配置为 Gitea Secret

# 读取 JSON 文件内容并转为 Base64
cat firebase-service-account.json | base64

将 Base64 编码后的内容添加为 Gitea Secret,命名为 FIREBASE_SERVICE_ACCOUNT_JSON。

Step 3:在工作流中写入 Service Account 文件并配置 Fastlane

- name: Write Service Account file
env:
FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
run: |
echo "$FIREBASE_SERVICE_ACCOUNT_JSON" | base64 -d > /tmp/firebase-service-account.json
echo "Service Account file created: $(ls -la /tmp/firebase-service-account.json)"

- name: Build and Distribute via Fastlane
env:
FIREBASE_SERVICE_ACCOUNT_PATH: /tmp/firebase-service-account.json
run: |
cd android
# 设置环境变量供 Fastlane 插件使用
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/firebase-service-account.json
bundle exec fastlane android_beta

Step 4:更新 Fastfile 使用 Service Account

在 android/fastlane/Fastfile 的 android_beta lane 中,将 firebase_cli_token 替换为 service_credentials_file:

firebase_app_distribution(
app: ENV["FIREBASE_ANDROID_APP_ID"],
service_credentials_file: ENV["FIREBASE_SERVICE_ACCOUNT_PATH"],
android_artifact_path: "app/build/outputs/apk/release/app-release.apk",
release_notes: ENV["RELEASE_NOTES"] || "Auto build from CI",
testers: ENV["FIREBASE_TESTERS"] || "tester1@example.com,tester2@example.com"
)

注意: 使用 Service Account 方式时,无需安装 Firebase CLI(brew install firebase-cli 可省略),因为插件直接通过 Google API 进行认证,不依赖 CLI 工具。


七、常见问题与优化建议​

7.1 磁盘空间清理​

双端构建会占用大量磁盘空间,建议定期清理缓存:

# 清理 Gradle 缓存
rm -rf ~/.gradle/caches

# 清理 Xcode DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData

# 清理 Flutter 构建缓存
flutter clean

7.2 iOS 签名配置​

上述工作流中 iOS 端通过 Fastlane Match 自动管理签名。如需手动配置:

  1. 在 Apple Developer 后台配置 Bundle ID、证书和描述文件
  2. 将证书和描述文件导入 macOS Runner 的钥匙串
  3. 在 Fastfile 中确保 signingStyle 设为 manual

7.3 构建产物分发​

平台分发方式说明
AndroidFastlane firebase_app_distribution 插件通过 Fastlane 统一管理,支持 APK/AAB
iOSTestFlight(通过 Fastlane)Apple 官方测试分发
两者Gitea Artifacts构建产物保存在 Gitea 中供下载

7.4 Runner 性能优化​

  • 并行度控制:在 Runner 配置中设置 capacity 参数控制并发 Job 数量,双端并行构建建议设为 2
  • 缓存优势:host 模式下 Flutter 的 cache: true 直接缓存到宿主机 ~/.pub-cache,比 Docker 模式更持久,构建速度更快
  • Apple Silicon 优势:M 系列芯片 Mac 在 Android Gradle 构建上比 Intel Mac 快约 50%,推荐作为构建机

7.5 安全性注意事项​

host 模式没有容器隔离保护,Job 可以直接访问宿主机文件系统。建议:

  • 仅对可信仓库启用 Actions
  • 避免 fork PR 自动触发构建
  • 定期审计工作流文件
  • Firebase Service Account 密钥不要提交到代码仓库

7.6 网络加速​

如果 Runner 位于中国大陆网络环境,建议在 macOS 机器上配置 Flutter 国内镜像(见 2.3 节 Step 10),否则 flutter pub get 可能因网络问题超时失败。


八、总结​

步骤关键操作
1. 启用 ActionsGitea 1.21.0+ 默认启用,旧版本需在 app.ini 中配置
2. 注册 Runner使用二进制方式安装,配置为 host 模式,使用局域网 IP
3. 配置 macOS 环境安装 Xcode、Android Studio、Flutter SDK、Java 17、CocoaPods、Fastlane、Firebase CLI(Fastlane 插件依赖)
4. 配置 Fastlane Match创建私有证书仓库,初始化 Match,生成证书和描述文件
5. 配置 Fastfile定义 TestFlight 构建和上传 lane
6. 配置 Fastlane firebase_app_distribution安装插件,配置 Firebase 项目,生成认证 Token 或 Service Account
7. 编写工作流创建 .gitea/workflows/flutter-dual-build.yml,拆分为 Android + iOS 两个并行 Job
8. 配置 Secrets在 Gitea 中添加 MATCH、API Key、Firebase 等 Secret
9. 启用仓库 Actions在仓库设置中勾选 "Enable Repository Actions"
10. 推送代码触发构建推送代码后在 Actions 页面查看构建状态

按照以上步骤配置完成后,每次推送代码即可在 Gitea Actions 中自动并行构建:

  • Android:生成 APK/AAB 并通过 Fastlane firebase_app_distribution 插件自动分发到 Firebase App Distribution
  • iOS:通过 Fastlane Match 管理签名证书,构建 IPA 并上传到 TestFlight

大幅提升交付效率。