跳转至正文
CI/CD

使用 GitHub actions 和证书自动进行 Capacitor iOS build

如何在 5 分钟内使用 fastlane 和 GitHub Actions 为 iOS Ionic 应用搭建 CI/CD 流水线(2024)

使用 GitHub actions 和证书 Capgo 自动构建 Capacitor iOS 的博客插图

为 Capacitor 应用配置 CI/CD 可能既复杂又耗时。你需要了解这些:

我们现在推荐使用 配合 Capgo CLI 使用 Capgo Build 用于原生 Capacitor 构建。 此 Fastlane 指南仅供维护现有 GitHub Actions 流水线的团队参考,但新的 iOS 构建应使用 Capgo CLI,这样您就无需自行维护 Fastlane、Xcode 运行器、证书和上传脚本。

Capgo 通过 Capgo 进行 CI/CD 构建

免去维护 Fastlane、Xcode 运行器、证书、描述文件和上传脚本的麻烦。 Capgo 构建 通过现有的 CI/CD 流水线运行已签名的原生 iOS 构建:

  • 适配你的流水线:在 Web 构建完成后,通过 GitHub Actions、GitLab CI、Jenkins 或本地脚本触发 Capgo Build,并 npx cap sync.
  • 通过 CI secrets 签名:将 App Store Connect 密钥、证书、provisioning profile、密码和 Team ID 保存在你自己的 CI secrets 中。
  • 无需维护原生 runner: Capgo Build 提供维护良好的 Apple 构建环境,因此您无需自行管理 macOS runner、Xcode 镜像或 Fastlane lanes。
  • 产物与提交:下载已签名的产物用于 QA,或通过 Capgo CLI 提交发布构建。

定价

  • Capgo 套餐 $12/月起
  • 包含 OTA 更新和每月约 15 次 Native Build
  • 额外的构建分钟数通过 credits 按分钟计费

在 CI/CD 中配置 Capgo Build

手动配置指南

你需要做的是:

使用 Fastlane 和 GitHub Actions 及证书实现 iOS 持续交付

前提条件

继续本教程之前:

  • 请确保你已安装 Fastlane 已安装 在你的开发机上。
  • 请确保你已加入 iOS 开发者计划。

关于价格的重要信息

GitHub Action 价格

https://github.com/features/actions

该服务是“免费”, 最高可达上限,具体取决于所选机器。
我们将使用一台 macOS 机器,你可以在截图中看到其价格和限制(价格以教程编写时为准,未来可能会有变动)

了解了要求和价格后,我们继续。

注意:本文假设你已在 App Store Connect 中创建了 App。Fastlane 会复制关键信息!

你将在本教程中学到什么

本文的操作步骤

  1. 配合 Fastlane 使用 App Store Connect API
    • 前提条件:
      • 创建 App Store Connect API 密钥
      • 使用 App Store Connect API 密钥
  2. 复制 Fastlane 文件
  3. 配置 GitHub Actions

1. 配合 Fastlane 使用 App Store Connect API

自 2021 年 2 月起,所有用户都必须使用双重认证或两步验证登录 App Store Connect。为 Apple ID 增加这层额外安全保护,有助于确保只有您本人能访问自己的账户。
来自 Apple 支持

要求

为了让 Fastlane 能够使用 App Store Connect API 来上传您的 App,您需要提供以下 三 项内容:

  1. 发行者 ID
  2. 密钥 ID
  3. Key 文件或 Key 内容

获取 App Store Connect API 密钥

要生成 Key,您必须拥有 App Store Connect 的 Admin 权限。如果您没有该权限,可以将这篇文章发给相关人员。

  1. 登录 App Store Connect.

  2. 选择 用户与访问权限.

App Store Connect 用户访问权限

3 — 选择“集成”选项卡。

App Store Connect API 集成

  1. 点击“生成 API 密钥”或“添加(+)”按钮。

在 App Store Connect 创建 API 密钥

  1. 为密钥输入一个名称。该名称仅供您参考,不属于密钥本身的一部分。

App Store Connect API 密钥创建:名称

6 — 在“访问权限”下,选择该密钥的角色。密钥适用的角色与团队中用户适用的角色相同。请参阅 角色权限。我们建议选择 App 管理员.

  1. 点击“生成”。

无法将 API 密钥的访问权限限定到特定 App。

新密钥的名称、密钥 ID、下载链接及其他信息将显示在页面上。

在 App Store Connect 下载密钥

您可以在此处获取全部三项必要信息。
<1> Issue ID。(APPLE_ISSUER_ID secret)
&lt;2&gt; Key ID。(APPLE_KEY_ID secret)
&lt;3&gt; 点击 “Download API Key” 下载您的 API 私钥。下载链接仅在私钥尚未下载时显示。Apple 不会保留私钥副本,因此您只能下载一次。

🔴 请妥善保管私钥。切勿分享密钥,不要将密钥存储在 code 仓库中,也不要在客户端 code 中包含密钥。

使用 App Store Connect API 密钥

为了创建用于授权的 JWT token,您需要 API Key 文件(下载的 p8 文件)、Key ID 和 Issuer ID。有多种方式可以将这些信息传递给 Fastlane。我选择使用 Fastlane 的新 action app_store_connect_api_key。您可以在 Fastlane 文档中了解其他方式。我介绍这种方法,因为我认为对于大多数支持设置环境变量的 CI 来说,这是最简单的方式。

请将下载的 p8 文件转换为 Base64,并作为 secret 存储 (APPLE_KEY_CONTENT).

base64 -i APPLE_KEY_CONTENT.p8 | pbcopy

现在我们可以使用 API 密钥通过 Fastlane 来管理 App Store Connect 了,太棒了!

2. 证书

打开 Xcode 并前往 设置 > 账户 > Apple ID > 团队 并选择你的团队。

Code 签名身份

点击 管理证书.

如果还没有创建证书,可以创建一个新证书。

点击 + 并选择 Apple Distribution

Apple Distribution

然后,你需要前往 Keychain 将证书下载为 .p12 文件。

为此,你需要打开 Keychain 并切换到 登录 钥匙串,然后点击 我的证书.

我的证书

然后选择要下载的证书。(按证书日期查找)

然后右键点击证书上的私钥,并选择 导出.

选择文件格式 个人信息交换 (.p12).

这会将证书下载为 .p12 文件。

请在终端中打开该文件,并使用以下命令将其转换为 Base64:

base64 -i BUILD_CERTIFICATE.p12 | pbcopy

这将成为你的 BUILD_CERTIFICATE_BASE64 secret。此外,系统提示时请输入证书密码。该密码将成为你的 P12_PASSWORD secret。

3. 描述文件

打开 Apple Developer 并选择正确的团队。

然后,点击 +

创建新配置文件

并选择 App Store Connect.

选择 App Store Connect

然后,你需要选择正确的应用。请注意,不能使用通配符,否则签名会失败。

选择正确的应用

选择你之前创建的正确证书(查看过期日期,它应该与今天的日期和月份相同),然后点击 继续.

选择正确的证书

最后输入 profile 名称并点击 生成.

该名称将用于在 Fastlane 中标识描述文件,对应值为 APPLE_PROFILE_NAME.

生成描述文件

你可以将描述文件下载为 .mobileprovision 文件。

下载描述文件

请将描述文件转换为 Base64,并将其作为 secret 存储 (BUILD_PROVISION_PROFILE_BASE64).

base64 -i BUILD_PROVISION_PROFILE.mobileprovision | pbcopy

4. 复制 Fastlane 文件

Fastlane 是一个 Ruby 库,用于自动化常见的移动开发任务。使用 Fastlane,你可以配置自定义的 “lanes”,这些 lanes 会打包一系列 “actions”,用来执行你通常会在 Android Studio 中完成的任务。Fastlane 能做很多事,但本教程只会用到少数几个核心 actions。

在 Capacitor/Ionic 项目根目录下创建 Fastlane 文件夹,并在其中添加 Fastfile:

  • 文件夹: <project-root>/fastlane/
  • 文件: <project-root>/fastlane/Fastfile

这与 ... 同级 package.json, capacitor.config.*,以及 ios/ 文件夹。不要将其创建在 ios/App/.

platform :ios do
  desc 'Export ipa and submit to TestFlight'
  lane :beta do
    keychain_info = { keychain_name: "ios-build-#{Time.now.to_i}.keychain", keychain_password: SecureRandom.uuid }
    
    begin
      setup_signing(keychain_info)
      bump_build_number
      build_app_with_signing(keychain_info)
      submit_to_testflight
    ensure
      cleanup_keychain(keychain_info)
    end
  end

  private_lane :setup_signing do |options|
    create_keychain(
      name: options[:keychain_name],
      password: options[:keychain_password],
      unlock: true,
      timeout: 0,
      lock_when_sleeps: false, 
      add_to_search_list: true
    )
    import_cert(options)
    install_profile
    update_project_settings
  end

  lane :bump_build_number do
		file = File.read('../package.json')
		data_hash = JSON.parse(file)
		api_key = app_store_connect_api_key(
      key_id: ENV['APPLE_KEY_ID'],
      issuer_id: ENV['APPLE_ISSUER_ID'],
      key_content: ENV['APPLE_KEY_CONTENT'],
      is_key_content_base64: true,
      duration: 1200,
      in_house: false
    )
		build_num = app_store_build_number(
      api_key: api_key,
			app_identifier: ENV['BUNDLE_IDENTIFIER'],
			live: false
    )
		build_num = build_num + 1
		UI.message("Bumped build number to #{build_num}")
		increment_build_number(
			build_number: build_num,
			xcodeproj: "./ios/App/App.xcodeproj",
			skip_info_plist: true
		)
	end

  private_lane :import_cert do |options|
    cert_path = "#{Dir.tmpdir}/build_certificate.p12"
    File.write(cert_path, Base64.decode64(ENV['BUILD_CERTIFICATE_BASE64']))
    import_certificate(
      certificate_path: cert_path,
      certificate_password: ENV['P12_PASSWORD'] || "",
      keychain_name: options[:keychain_name],
      keychain_password: options[:keychain_password],
      log_output: true
    )
    File.delete(cert_path)
  end  
  
  private_lane :cleanup_keychain do |options|
    delete_keychain(
      name: options[:keychain_name]
    )
  end  

  private_lane :install_profile do
    profile_path = "#{Dir.tmpdir}/build_pp.mobileprovision"
    File.write(profile_path, Base64.decode64(ENV['BUILD_PROVISION_PROFILE_BASE64']))
    UI.user_error!("Failed to create provisioning profile at #{profile_path}") unless File.exist?(profile_path)
    ENV['PROVISIONING_PROFILE_PATH'] = profile_path
    install_provisioning_profile(path: profile_path)
    File.delete(profile_path)
  end

  private_lane :update_project_settings do
    update_code_signing_settings(
      use_automatic_signing: false,
      path: "./ios/App/App.xcodeproj",
      code_sign_identity: "iPhone Distribution",
      profile_name: ENV['APPLE_PROFILE_NAME'],
      bundle_identifier: ENV['BUNDLE_IDENTIFIER'],
      team_id: ENV['APP_STORE_CONNECT_TEAM_ID']
    )
    update_project_team(
      path: "./ios/App/App.xcodeproj",
      teamid: ENV['APP_STORE_CONNECT_TEAM_ID']
    )
  end

  private_lane :build_app_with_signing do |options|
    unlock_keychain(
      path: options[:keychain_name],
      password: options[:keychain_password],
      set_default: false
    )
    build_app(
      workspace: "./ios/App/App.xcworkspace",
      scheme: "App",
      configuration: "Release",
      export_method: "app-store",
      output_name: "App.ipa",
      export_options: {
        provisioningProfiles: {
          ENV['BUNDLE_IDENTIFIER'] => ENV['APPLE_PROFILE_NAME']
        }
      },
      xcargs: "-verbose",
      buildlog_path: "./build_logs",
      export_xcargs: "-allowProvisioningUpdates",
    )
  end   

  private_lane :submit_to_testflight do
    api_key = app_store_connect_api_key(
      key_id: ENV['APPLE_KEY_ID'],
      issuer_id: ENV['APPLE_ISSUER_ID'],
      key_content: ENV['APPLE_KEY_CONTENT'],
      is_key_content_base64: true,
      duration: 1200,
      in_house: false
    )
    pilot(
      api_key: api_key,
      skip_waiting_for_build_processing: true,
      skip_submission: true,
      distribute_external: false,
      notify_external_testers: false,
      ipa: "./App.ipa"
    )
  end
end

5. 配置 Secrets

GitHub Actions 会使用你在下一步配置的仓库 Secrets。你只需要一个本地 .env 文件(如果你想在自己的机器上运行或测试 Fastlane)。

如需本地测试,请创建 <project-root>/fastlane/.env 在 ... 旁边 Fastfile。不要提交此文件。请将 fastlane/.env 其添加到你的 .gitignore 中(或确认它已被忽略)。示例如下:

APP_STORE_CONNECT_TEAM_ID=UVTJ336J2D
BUNDLE_IDENTIFIER=ee.forgr.testfastlane
# See previous section for these secrets
BUILD_CERTIFICATE_BASE64=
BUILD_PROVISION_PROFILE_BASE64=
APPLE_KEY_ID=
APPLE_ISSUER_ID=
APPLE_KEY_CONTENT=
P12_PASSWORD=
APPLE_PROFILE_NAME=

获取 APP_STORE_CONNECT_TEAM_ID

前往 开发者中心 并向下滚动到 Membership details 部分。 Team ID 是您需要设置的值 APP_STORE_CONNECT_TEAM_ID secret。

app-store-connect-team-id

获取 BUNDLE_IDENTIFIER

  1. 打开 Xcode
  2. 双击 App 在项目导航栏中
  3. 然后点击该标签页 Signing and Capabilities
  4. 复制 Bundle identifier的值。这是你需要在 BUNDLE_IDENTIFIER secret。
bundle-identifier-xcode

6. 构建处理

在 GitHub Actions 中, 费用按你运行 CI/CD 工作流所用的 分钟数计算。根据我的经验,构建在 App Store Connect 中处理完大约需要 10–15 分钟。

对于私有项目,每次构建的预估成本可能高达 $0.08/分钟 x 15 分钟 = $1.2,甚至更高,具体取决于你项目的配置和依赖项。

如果你担心私有项目的成本,可以设置 skip_waiting_for_build_processing 改为 true。这样无需等待 App Store Connect 处理完构建,就能节省构建分钟数。

不过,这也有取舍——在把构建分发给用户之前,你需要手动在 App Store Connect 中更新应用的合规信息。

这个优化主要适用于构建分钟数计费的私有项目。对于公开/免费项目,构建分钟数免费,因此无需启用此设置。请参阅 GitHub 的 定价页面 以获取更多详情。

7. 设置 GitHub Actions

配置 GitHub secrets

请从以下位置复制 secrets: .env 文件,并将其粘贴到 GitHub 仓库的 secrets 中。

前往 设置 > Secrets 和变量 > 操作 > 新建仓库 secret

github-secrets

2. BUILD_CERTIFICATE_BASE64 - Base64 编码的证书。

3. BUILD_PROVISION_PROFILE_BASE64 - Base64 编码的 provisioning profile。

4. BUNDLE_IDENTIFIER - 您应用的 bundle identifier。

5. APPLE_KEY_ID — App Store Connect API Key 🔺Key ID。

6. APPLE_ISSUER_ID — App Store Connect API Key 🔺Issuer ID。

7. APPLE_KEY_CONTENT — App Store Connect API Key 🔺 Key 内容 .p8, 检查一下

8. 配置 GitHub workflow 文件

创建一个 GitHub workflow 目录。

cd .github/workflows

在 workflow 文件夹中,创建一个名为 build-upload-ios.yml的文件并添加以下内容。

name: Build source code on ios

on:
  push:
    tags:
      - '*'

jobs:
  build_ios:
    runs-on: macOS-latest
    steps:
      - uses: actions/checkout@v6
      - name: Set Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: npm
      - name: Install dependencies
        id: install_code
        run: npm ci
      - name: Build
        id: build_code
        run: npm run build
      - uses: actions/cache@v5
        with:
          path: ios/App/Pods
          key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-pods-
      - name: Sync
        id: sync_code
        run: npx cap sync
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true
      - uses: maierj/fastlane-action@v3.1.0
        env:
          APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
          BUNDLE_IDENTIFIER: ${{ secrets.BUNDLE_IDENTIFIER }}
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
          APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
          APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
          APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          APPLE_PROFILE_NAME: ${{ secrets.APPLE_PROFILE_NAME }}
        with:
          lane: ios beta
      - name: Upload release bundle
        uses: actions/upload-artifact@v6
        with:
          name: ios-release
          path: ./App.ipa
          retention-days: 10

此 workflow 应在每次 GitHub 后触发 标签,如果需要自动打 tag,请参考 使用 GitHub actions 自动构建与发布 首先。

接着,这个 workflow 会拉取你的 NodeJS 依赖、安装它们并构建你的 JavaScript 应用。

每次你提交新的 commit,都会在 TestFlight 中构建一个 release。

你的 App 不需要使用 Ionic,只需 Capacitor 基础即可。它可以包含旧的 Cordova 模块,但应优先使用 Capacitor JS 插件。

8. 触发 workflow

创建一个 commit

提交一个 提交,你应该能在仓库中看到正在运行的 workflow。

触发 workflow

把新的 commit 推送到分支 main 或 development 来触发工作流。

由 commit 启动

几分钟后,build 应该就会出现在你的 App Store Connect dashboard 中。

TestFlight Dashboard

9. 我能从本地机器部署吗?

可以,而且毫不费力。

你可以像往常一样使用 Xcode 来构建并签名你的 App。

CI/CD 配置指南

其他 CI/CD 平台

Live Updates 与部署

感谢

本文参考了以下文章:

继续阅读:使用 GitHub actions 和证书自动 build Capacitor iOS 应用

如果你正在使用 使用 GitHub actions 和证书自动进行 Capacitor iOS build 规划 CI/CD 自动化,并将其与 Capgo CI/CD 集成 用于 Capgo CI/CD 中的产品工作流, Capgo Native Build 用于 Capgo Native Builds 中的产品工作流, Capgo 集成 用于 Capgo 集成中的产品工作流, CI/CD 集成 了解 CI/CD 集成的实现细节,以及 GitHub Actions 集成 有关 GitHub Actions 集成的实现细节。

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。