Flutter iOS通过Fastlane+GitHub Action打包归档Run Script阶段失败
问题场景
使用Fastlane结合GitHub Action将Flutter iOS应用部署至TestFlight,所有密钥与配置信息均匹配,但构建始终在Run Script阶段失败,报错信息如下:
PhaseScriptExecution Run\ Script /Users/runner/Library/Developer/Xcode/DerivedData/Runner-avobjsyvghznfxgtyjuolmxrrfls/Build/Intermediates.noindex/ArchiveIntermediates/prod/IntermediateBuildFilesPath/Runner.build/Release-prod-iphoneos/Runner.build/Script-9740EEB61CF901F6004384FC.sh (in target 'Runner' from project 'Runner')
引发问题的是Xcode Run Script阶段的脚本:
/bin/sh "${FLUTTER_ROOT}/packages/flutter_tools/bin/xcode_backend.sh" build
本地配置相同环境变量后Fastfile可正常运行,但GitHub Action环境下执行失败,已尝试给FLUTTER_ROOT添加{}包裹、配置Project Runner Info,问题仍未解决。
相关配置代码
GitHub Workflow yaml
name: Flutter CI on: push: branches: xxx jobs: deploy: name: Deploying to Testflight runs-on: macOS-latest steps: - name: Setup XCode on Machine uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '13.4' - name: Setup Flutter uses: subosito/flutter-action@v2.8.0 with: channel: 'stable' flutter-version: '3.3.9' - name: Give permission to private repo uses: shaunco/ssh-agent@git-repo-mapping with: ssh-private-key: | ${{ secrets.SECRET_REPO_DEPLOY_KEY }} repo-mappings: | github.com/anfin21/package_library - name: Checkout repository uses: actions/checkout@v1 - name: Install packages run: flutter pub get - name: Deploy iOS Beta to TestFlight via Fastlane uses: maierj/fastlane-action@v1.4.0 with: lane: closed_beta subdirectory: ios env: APP_STORE_CONNECT_TEAM_ID: '${{ secrets.ITC_TEAM_ID }}' DEVELOPER_APP_ID: '${{ secrets.APPLICATON_ID }}' DEVELOPER_APP_IDENTIFIER: '${{ secrets.BUNDLE_IDENTIFIER }}' DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}' FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_EMAIL_ID }}' FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}' MATCH_PASSWORD: '${{ secrets.MATCH_PASSWORD }}' GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}' PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}' TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}' TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}' APPLE_KEY_ID: '${{ secrets.APPLE_KEY_ID }}' APPLE_ISSUER_ID: '${{ secrets.APPLE_ISSUER_ID }}' APPLE_KEY_CONTENT: '${{ secrets.APPLE_KEY_CONTENT }}'
iOS Fastfile代码
default_platform(:ios) DEVELOPER_APP_ID = ENV["DEVELOPER_APP_ID"] DEVELOPER_APP_IDENTIFIER = ENV["DEVELOPER_APP_IDENTIFIER"] PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"] TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"] TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"] APPLE_ISSUER_ID = ENV["APPLE_ISSUER_ID"] APPLE_KEY_ID = ENV["APPLE_KEY_ID"] APPLE_KEY_CONTENT = ENV["APPLE_KEY_CONTENT"] GIT_AUTHORIZATION = ENV["GIT_AUTHORIZATION"] def delete_temp_keychain(name) delete_keychain( name: name ) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db") end def create_temp_keychain(name, password) create_keychain( name: name, password: password, unlock: false, timeout: 0 ) end def ensure_temp_keychain(name, password) delete_temp_keychain(name) create_temp_keychain(name, password) end platform :ios do lane :closed_beta do keychain_name = TEMP_KEYCHAIN_USER keychain_password = TEMP_KEYCHAIN_PASSWORD ensure_temp_keychain(keychain_name, keychain_password) api_key = app_store_connect_api_key( key_id: APPLE_KEY_ID, issuer_id: APPLE_ISSUER_ID, key_content: APPLE_KEY_CONTENT, duration: 1200, in_house: false ) cocoapods( clean_install: true, ) match( type: 'appstore', app_identifier: "#{DEVELOPER_APP_IDENTIFIER}", git_basic_authorization: Base64.strict_encode64(GIT_AUTHORIZATION), readonly: false, keychain_name: keychain_name, keychain_password: keychain_password, api_key: api_key ) gym( configuration: "Release", sdk: "iphoneos16.1", workspace: "Runner.xcworkspace", scheme: "prod", export_method: "app-store", export_options: { provisioningProfiles: { DEVELOPER_APP_ID => PROVISIONING_PROFILE_SPECIFIER } } ) pilot( apple_id: "#{DEVELOPER_APP_ID}", app_identifier: "#{DEVELOPER_APP_IDENTIFIER}", skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, notify_external_testers: false, ipa: "./Runner.ipa" ) delete_temp_keychain(keychain_name) end end
解决步骤
1. 修复FLUTTER_ROOT环境变量传递问题
GitHub Action中通过subosito/flutter-action安装Flutter后,FLUTTER_ROOT会被自动设置,但Fastlane执行gym时可能未继承该变量。可以在Workflow的Fastlane步骤中显式传递:
在Deploy iOS Beta to TestFlight via Fastlane的env中添加:
FLUTTER_ROOT: ${{ env.FLUTTER_ROOT }}
或者在Fastfile的closed_beta lane开头手动设置:
ENV['FLUTTER_ROOT'] = `which flutter`.chomp.split('/bin/flutter')[0]
2. 匹配Xcode与SDK版本
当前使用Xcode 13.4,其最高支持的iOS SDK版本为iphoneos15.5,但Fastfile中gym指定的sdk: "iphoneos16.1"属于Xcode 14.1+的SDK,这会导致构建时找不到对应SDK。修改gym的sdk配置:
sdk: "iphoneos15.5"
或升级Xcode版本至14.1+,同时更新Workflow中的Xcode版本配置。
3. 修正Fastfile中export_options的语法错误
Fastfile中gym的export_options里,provisioningProfiles的键应使用Bundle ID(DEVELOPER_APP_IDENTIFIER)而非App Store应用ID(DEVELOPER_APP_ID),同时修正哈希语法:
export_options: { provisioningProfiles: { DEVELOPER_APP_IDENTIFIER => PROVISIONING_PROFILE_SPECIFIER } }
4. 验证Flutter环境初始化
在flutter pub get步骤前添加环境检查,确认Flutter与Xcode配置正常:
- name: Check Flutter environment run: flutter doctor -v
5. 确保脚本执行权限
GitHub Action Runner环境中可能存在脚本权限不足的问题,添加步骤给xcode_backend.sh赋予执行权限:
- name: Grant execute permission to xcode_backend.sh run: chmod +x "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh"
内容的提问来源于stack exchange,提问作者Lam Kiet Vuong

