GitHub Action CI/CD用于Flutter Fastlane(ios),可能会犯错误
#ios #fastlane #githubactions #ci

前提是:
设置Fastlane和使用GitHub动作进行CI/CD的主题已在许多文章中涵盖。尽管如此,我们在这样做时犯了很多错误。犯了很多错误后,解决这些问题花了我两个完整的工作日。在本文中,我将在CI/CD期间分享我的错误,并希望能帮助他们避免。

作为本文的一部分,我分解了您在这些步骤中可能犯的Fastlane设置和CI/CD的不同步骤和可能的错误。

  1. 开发人员帐户和iOS AppStore Connect
  2. Fastlane:创建和存储证书(MatchFile和AppFile配置)
  3. Fastlane:钥匙扣
  4. Fastlane:安装证书和配置资料
  5. Fastlane:构建应用程序
  6. 代码签名设置
  7. github动作和环境秘密设置
  8. 生成构建并增量版本代码会自动
  9. 该应用的部署 这些是逐步的事情,因此请确保一一跟随它们并仔细阅读文章。

好吧,现在深呼吸ð® ð®

使用FASTLANE和GITHUB动作扑向iOS应用CI CI

开发人员帐户和iOS AppStore Connect
首先,您需要一个开发人员帐户。您需要收集一些细节。导航到会员资格详细信息的链接,您可以在其中记录团队ID名称和团队ID,以后我们将用于应用程序文件。

之后,导航到标识符列表,并为您的应用程序添加标识符。您可以根据您的设备添加详细信息(功能,捆绑ID,功能)(我们可以稍后更新它,因此没有错误的机会)。

标识符
拥有CI/CD的整个想法是使所有内容都具有自动化,而不必为不同的设备和用户分别配置它。尤其是在我们需要管理证书,为不同用户提供证书的iOS上,这总是很痛苦。

相反,我将创建一个具有必要访问权限的开发帐户,并与开发人员共享。从现在开始,我将其称为dev@company.com

用户和访问
之后,主要的事情是获得App Store Connect API键。 WWDC18之后,启用了2FA,我们不想落后。为此,您需要在App Manager访问中生成一个API密钥,并从“钥匙”选项卡中下载并将其存储在安全的地方。另外,请注意关键ID,来自同一选项卡的发行人ID。


到目前

  • 发行人ID
  • 密钥ID
  • API密钥文件/内容
  • 团队ID
  • 团队名称
  • 应用标识符
  • Apple Username(dev@company.com)

Fastlane:创建和存储证书(MatchFile和AppFile配置)

FastLane是一个开源工具套件,可自动使用您的应用程序发布和部署。在迁移到基于云的系统之前,在本地测试构建和部署过程是一个好主意。

本地设置:
虽然可以直接通过Ruby安装Fastlane,但我不建议它,因为它可能会引起与依赖软件包的冲突。邦德勒更适合我。

gem install bundler

在根目录中创建一个./gemfile,您可以粘贴内容。

源“ https://rubygems.org

宝石“ fastlane”
运行捆绑包更新并同时添加./gemfile和./gemfile.lock到版本控制
每次运行Fastlane时,都会使用Bundle Exec Fastlane [Lane]
在您的CI上,添加捆绑式安装作为第一个构建步骤
要更新FastLane,只需运行捆绑包更新Fastlane
导航您的终端到您的项目目录并运行
捆绑exec fastlane init
现在,在IT内部使用AppFile创建FastLane文件夹。

appfile:

AppFile存储了在Apple ID或应用程序捆绑包标识符等所有Fastlane工具中使用的有用信息,以更快地部署车道并根据您的项目需求进行量身定制。 AppFile必须在您的./fastlane目录内部。

matchfile:

这是创建公共开发帐户(dev@company.com)的主要思想,我们将在其中创建证书,供应配置文件一次,将它们存储在(git/s3)中,然后将其存储在(git/s3)中,然后是队友,CD服务器只会获取并使用它们。

无需为每个开发人员创建证书,无需手动安装它们,也不需要更多的代码签名问题,不太好?

匹配将简单地完成您的复杂任务,以创建所有必需的iOS证书并配置配置文件,然后将其存储在单独的存储库中。每个具有访问所选存储空间的团队成员都可以使用这些凭据进行代码签名。 Match还会自动维修破损和过期的凭据,并将其安装在本地钥匙扣中。这是分享团队签署凭据的最简单方法

所以一切都是通过匹配完成的,所以您的任务工作是什么???

只需在Github中创建一个私人存储库即可。就这样。即使您也不需要推动代码。匹配将为您服务。复制您创建的回购URL,它将是您的git_url。
在您的项目内部的FastLane目录内创建匹配项。为此,您可以在Fastlane目录内运行以下命令并提供GIT_URL。
Fastlane> Fastlane Match Init

它将在Fastlane目录内创建MatchFile。您可以转到文档以进行进一步设置的指导。但是,基本的事情是提供我们以前收集的storage_mode,用户名和app_istentifier。

Matchfile

此MatchFile配置将创建用于开发,AppStore和Ad-Hoc的证书和配置配置文件,然后将其存储在Github中,只需运行以下CLI命令即可。现在,当您完成了Fastlane的关键部分时,您应该得到一杯咖啡。因此,在开始下一步之前去喝酒。

fastlane match development
fastlane match appstore
fastlane match development

可能的错误:

MatchFile可能无法访问GITHUB,因此请确保将SSH添加到GitHub,否则可以推动证书。链接
现在我们有了基本设置。现在是使用证书的使用的关键部分以及将证书存储在车道中的何处。

fastlane:钥匙扣
作为Fastlane设置的一部分,您可能会在管理不同摊子的钥匙扣方面存在问题。当涉及到本地时,我们可以使用系统键链来处理所有开销。但是,当涉及到遥控器时,我们必须创建一个钥匙串并在会话结束后将其删除。

到目前为止,您应该在Fastlane目录中有一个快速文件。可以通过将给定代码粘贴到内部。

来创建快速文件。

`平台:iOS做
$ keychain_name = secureerandom.uuid
$ keychain_password = secureerandom.hex(100)
$ is_ci = env ['ci']

after_all do |lane, options|
    remove_keychain
end

error do |lane, exception, options|
    remove_keychain
end

desc "Remove Keychain from CI"
private_lane :remove_keychain do |options|
    if $is_ci
        if File.exist?(File.expand_path("~/Library/Keychains/#{$keychain_name}-db"))
            UI.important "Deleting keychain #{$keychain_name}"
            delete_keychain(name: $keychain_name)
        elsif
            UI.important "No keychain file found to delete"
        end
    end
end

desc "Setup Keychain for match on CI"
private_lane :setup_keychain do |options|
    create_keychain(
        name: $keychain_name,
        password: $keychain_password,
        default_keychain: true,
        unlock: false,
        timeout: 0,
        lock_when_sleeps: true
    )
end

end`

为了创建钥匙串,我创建了一个脚本,该脚本将生成随机密码和钥匙串,我们将使用该脚本来创建键链并之后删除。证书和配置配置文件仅存储在CI期间临时使用。

另外,您可以加起来:

`desc'配置飞镖的味道
private_lane:floation_config做|选项|
如果!选项[:straim]
ui.message“没有提供的风味,默认(开发)
set_flavor(味道:“开发”)
下一个
结束

if options[:flavor] == "develop" || options[:flavor] == "staging" || options[:flavor] == "production"
    set_flavor(flavor: options[:flavor])
else
    UI.abort_with_message! "No supported flavor provided (#{options[:flavor]}). Supported values are 'develop', 'staging', 'production'."
end

结束

def set_flavor(味道:)
ui.message“将风味设置为#{float}”

ENV[$dart_define_env_key] = "flavor=#{flavor}"

end`

根据这些口味,所有快速行动都是执行的。

Fastlane:安装证书和配置资料

这是Fastlane的主要任务,它减少了安装证书和配置配置文件的张力。做一次,忘记它。

提供的源代码也针对CI和本地环境分开。如果CI为真,将创建新的钥匙扣,我们先前已经定义了这一点。如果不是CI,则使用本地钥匙串并自动安装/更新所需的证书,如果您提供了我们已经讨论过的所有必需详细信息。

desc "Install Certificates and Provisioning Profiles"
    lane :match_sync do |options|
        login
        if $is_ci  
            UI.message "Installing Apple Certificates and Provisioning Profiles for CI"
            setup_keychain
            match(
                readonly: true,
                type: "appstore",
                keychain_name: $keychain_name,
                keychain_password: $keychain_password
            )
        elsif
            UI.message "Installing Apple Certificates and Provisioning Profiles for Local Development"
            UI.message "Installing App Store Certificate and Provisioning Profile"
            match(
                readonly: true,
                type: "appstore"

            )

            UI.message "Installing Development Certificate and Provisioning Profile"
            match(
                readonly: true,
                app_identifier: $app_identifier,
                type: "development",
            )
        end 
    end

    desc "Update Provisioning Profiles"
    lane :match_update do |options|
        if $is_ci
            UI.abort_with_message! "Only run match_update locally!"
        elsif
            UI.message "Updating Provisioning Profiles for New Devices"

            UI.message "Updating App Store Provisioning Profile"
            match(
                readonly: false,
                type: "appstore"
            )

            UI.message "Updating Development Provisioning Profile"
            match(
                readonly: false,
                type: "development"
            )
        end
    end

所有这些下载和匹配证书的任务都是通过我们已经设置的匹配项完成的。

Fastlane:构建应用程序

安装了所有证书和供应配置文件后,还保留了构建应用程序的任务。如果到目前为止一切都很好,那么这不应该是一个大问题。


desc "Build iOS App"
    lane :build do |options|
        flavor_config options
        gym(
            workspace: "ios/Runner.xcworkspace",
            xcargs: "-allowProvisioningUpdates",
        )
  end

健身房为您构建和包装iOS应用程序。它需要照顾所有繁重的工作,并使生成签名的IPA或应用程序文件ðª非常容易。您只需要提供工作空间位置。

另外,您可以使用build_app构建应用程序。


build_app(
  scheme: "Release",
  export_method: "app-store",
  export_options: {
    provisioningProfiles: {
      "com.example.bundleid" => "Provisioning Profile Name",
      "com.example.bundleid2" => "Provisioning Profile Name 2"
    }
  }
)

代码签名设置

现在您需要与AppStore Connect进行通信。为此,您需要我要求您从App Store Connect获得的详细信息。

key_id
issuer_id
key_filepath/content
 desc "Get Apple API Token"
    private_lane :login do |options|
        api_key = app_store_connect_api_key(
            key_id: "D83848D23",
            issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
            key_filepath: "D83848D23.p8"
            duration: 1200,
            in_house: false,
            is_key_content_base64: false
        )
        UI.message "this is the api key#{api_key}"

    end

#alternatives:
app_store_connect_api_key(
  key_id: "D83848D23",
  issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
  key_filepath: "D83848D23.p8"
)
app_store_connect_api_key(
  key_id: "D83848D23",
  issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
  key_filepath: "D83848D23.p8",
  duration: 200,
  in_house: true
)
app_store_connect_api_key(
  key_id: "D83848D23",
  issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
  key_content: "-----BEGIN EC PRIVATE KEY-----\nfewfawefawfe\n-----END EC PRIVATE KEY-----"
)

我建议在存储库中保留环境中的价值。我将在下一步中描述这一点。但是,到目前为止,您应该能够在本地运行所有步骤。

bundle exec fastlane ios build
此命令应该能够生成应用程序。

GitHub动作和环境秘密设置

现在是CI部分,我们要生成应用并将其推到AppStore。这是在根据您的意愿将代码推向开发或任何其他分支机构之后。我添加了一个操作,该操作将以任何分支上的推送和拉请请求运行。我相信您熟悉为项目中的GitHub动作创建YAML的想法。我会详细解释这个过程。在YAML文件中,您只需要粘贴给定代码即可完成与上述步骤相同的事情。但是,此过程适用于CI。

name:Application Build

on: 
  push:
  pull_request:
  workflow_dispatch:
jobs:
  build:
    name: Build Flutter Application
    runs-on: macOS-latest

    steps:
      - uses: actions/checkout@v3

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.3'
          channel: 'stable'
      - run: flutter --version

      - name: Setup key
        uses: webfactory/ssh-agent@v0.7.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY}}

      - name: bundle install
        run: bundle install

      - name: Install dependencies
        run: dart pub get

      - name: Analyze project source
        run: dart analyze

      - name: Run tests
        run: flutter test

      - name: 📱 Build iOS Application
        run: bundle exec fastlane ios build
        env:
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
          MATCH_USERNAME: ${{ secrets.MATCH_USERNAME }}
          APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
          APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
          PROVISIONING_PROFILE_SPECIFIER: ${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}
          CI: ${{ secrets.CI }}

在这里,我正在使用一些存储在环境中的秘密。为此,您需要转到设置>秘密>操作>新存储库秘密。

之后,您可以为他们提供动作的环境。

如果您在env中定义了app_store_connect_api_key_key_id,则无需在fastlane中定义它,因为它会自动被Fastlane识别。但是您需要在fastlane中定义秘密文件名


before_all do
Dotenv.overload ".env.secrets"
end

这将认识到环境的秘密。要使用此功能,您可以简单地创建两个文件.env.secrets和.env.secrets.sample和。从来没有.env.secrets to git。

增加版本代码

如果Fastlane正在处理所有内容,那么该版本也是我们在上传应用程序时面临的问题之一。

别担心我的朋友,这也是由Fastlane完成的,但是您需要自己设置一些东西。

第一个任务是添加车道以增加构建号
increment_build_number # automatically increment by one

increment_build_number(
build_number: "75" # set a specific number
)

increment_build_number(
  build_number: 75, # specify specific build number (optional, omitting it increments by one)
  xcodeproj: "./path/to/MyApp.xcodeproj" # (optional, you must specify the path to your main Xcode project if it is not in the project root directory)
)

在运行车道之前,请记住在Xcode中更改一些配置。

目标的信息选项卡
请记住要更改捆绑版本和捆绑版本字符串(短)。

另外,初始当前项目版本应为1,并且版本控制系统应为apple generic。

如果所有内容都是按照描述,那么您都将应用程序设置为AppStore/ testflight。

应用的部署

现在,我们可以使用upload_to_testflight方法上传到测试范围。您只需要提供在构建方法中创建的IPA文件的位置。


desc "Upload for iOS"
    lane :upload do |options|
        upload_testflight options
        upload_firebase options
    end 

    desc "Upload to Firebase"
    private_lane :upload_firebase do |options|

    end 

    desc "Upload to Testflight"
    private_lane :upload_testflight do |options|
        login
        upload_to_testflight(
            ipa: "Runner.ipa"
            )

    end 

确保提供当前的IPA位置。

和fyi,upload_to_testflight在后台使用飞行员,因此您也可以使用飞行员。

现在,您只需在GitHub操作中添加此车道,如果您完美地完成了所有步骤,则文件将在testflight中。到现在为止,您的快速file应该看起来像这样:


fastlane_require "dotenv"

before_all do
   Dotenv.overload ".env.secrets"
end

$dart_define_env_key = "DART_DEFINE"

desc "Configure Flavor for Dart"
private_lane :flavor_config do |options|
    if !options[:flavor]
        UI.message "No Flavor provided, going with default (develop)"
        set_flavor(flavor: "develop")
        next
    end

    if options[:flavor] == "develop" || options[:flavor] == "staging" || options[:flavor] == "production"
        set_flavor(flavor: options[:flavor])
    else
        UI.abort_with_message! "No supported flavor provided (#{options[:flavor]}). Supported values are 'develop', 'staging', 'production'."
    end
end

def set_flavor(flavor:)
    UI.message "Setting flavor to #{flavor}"

    ENV[$dart_define_env_key] = "flavor=#{flavor}"
end

platform :ios do
    $keychain_name = SecureRandom.uuid
    $keychain_password = SecureRandom.hex(100)
    $is_ci = ENV['CI']

    after_all do |lane, options|
        remove_keychain
    end

    error do |lane, exception, options|
        remove_keychain
    end

    desc "Install Certificates and Provisioning Profiles"
    lane :match_sync do |options|
        login
        if $is_ci  
            UI.message "Installing Apple Certificates and Provisioning Profiles for CI"
            setup_keychain
            match(
                readonly: true,
                type: "appstore",
                keychain_name: $keychain_name,
                keychain_password: $keychain_password
            )
        elsif
            UI.message "Installing Apple Certificates and Provisioning Profiles for Local Development"

            UI.message "Installing App Store Certificate and Provisioning Profile"
            match(
                readonly: true,
                type: "appstore"

            )

            UI.message "Installing Development Certificate and Provisioning Profile"
            match(
                readonly: true,
                app_identifier: $app_identifier,
                type: "development",
            )
        end 
    end

    desc "Update Provisioning Profiles"
    lane :match_update do |options|
        if $is_ci
            UI.abort_with_message! "Only run match_update locally!"
        elsif
            UI.message "Updating Provisioning Profiles for New Devices"

            UI.message "Updating App Store Provisioning Profile"
            match(
                readonly: false,
                type: "appstore"
            )

            UI.message "Updating Development Provisioning Profile"
            match(
                readonly: false,
                type: "development"
            )
        end
    end

    desc "Build iOS App"
    lane :build do |options|
        flavor_config options
        gym(
            workspace: "ios/Runner.xcworkspace",
            xcargs: "-allowProvisioningUpdates",
        )
    end

    desc "Test iOS App"
    lane :test do |options|
        UI.abort_with_message! "No iOS tests are currently available or supported"
    end

    desc "Test flutter code"
    lane :test_flutter do |options|
        Dir.chdir("..") do
            sh "flutter test"
        end
    end

    desc "Upload for iOS"
    lane :upload do |options|
        upload_testflight options
        upload_firebase options
    end 

    desc "Upload to Firebase"
    private_lane :upload_firebase do |options|

    end 

    desc "Upload to Testflight"
    private_lane :upload_testflight do |options|
        login
        upload_to_testflight(
            ipa: "Runner.ipa"
            )

    end 

    desc "Get Apple API Token"
    private_lane :login do |options|
        api_key = app_store_connect_api_key(
            duration: 1200,
            in_house: false,
            is_key_content_base64: false
        )
        UI.message "this is the api key#{api_key}"

    end

    desc "Remove Keychain from CI"
    private_lane :remove_keychain do |options|
        if $is_ci
            if File.exist?(File.expand_path("~/Library/Keychains/#{$keychain_name}-db"))
                UI.important "Deleting keychain #{$keychain_name}"
                delete_keychain(name: $keychain_name)
            elsif
                UI.important "No keychain file found to delete"
            end
        end
    end

    desc "Setup Keychain for match on CI"
    private_lane :setup_keychain do |options|
        create_keychain(
            name: $keychain_name,
            password: $keychain_password,
            default_keychain: true,
            unlock: false,
            timeout: 0,
            lock_when_sleeps: true
        )
    end
end

如果您走得太远,您已经为flutter iOS应用程序设计了一个自动化的CI。该工具可能存在一些小问题,以及您需要修复的依赖项。但是,我已经描述了您在此过程中可能遇到的大多数问题。

好吧,这就是本文的全部。我会在下一个见到你。参见ya。