如何使用 Fastlane 和 GitHub Actions 自动化Flutter应用的发布流程,以便将其分发到 Firebase App Distribution、Google Play、TestFlight 以及 App Store Connect平台上
想象一下:现在是周五下午4点,你的团队刚刚完成了本次迭代周期的最后一项功能开发。但产品经理要求在当天结束前通过TestFlight发送一个新的版本,这样客户就可以在周末进行测试。 你打开Xcode,等待代码打包完成,处理一个昨天还不存在的签名错误,修复问题后重新打包代码,再次等待上传,然后等待App Store Connect进行处理。接着你需要为Android版本重复这些步骤,不过这次是通过Android Studio来操作的。你需要签署APK文件,登录Firebase App Distribution平台,将文件上传进去,添加测试人员,编写发布说明,最后点击发送按钮。 现在已经是下午6点4
想象一下:现在是周五下午4点,你的团队刚刚完成了本次迭代周期的最后一项功能开发。但产品经理要求在当天结束前通过TestFlight发送一个新的版本,这样客户就可以在周末进行测试。
你打开Xcode,等待代码打包完成,处理一个昨天还不存在的签名错误,修复问题后重新打包代码,再次等待上传,然后等待App Store Connect进行处理。接着你需要为Android版本重复这些步骤,不过这次是通过Android Studio来操作的。你需要签署APK文件,登录Firebase App Distribution平台,将文件上传进去,添加测试人员,编写发布说明,最后点击发送按钮。
现在已经是下午6点45分了。在过去的两个小时里,你一行产品代码都没有编写过。每个版本发布周期都会遇到这种情况。
现在再想象另一种情况:你将代码推送到dev分支上,GitHub的服务器会自动处理后续工作。几分钟内,一个独立的云环境就会下载你的代码,安装Flutter框架,解密加密过的签名凭证,生成APK和IPA文件,然后同时将这些文件发送到Firebase App Distribution平台供Android测试人员使用,以及通过TestFlight发送给iOS测试人员。这样你就可以回家休息了,系统会自动通知所有测试人员。
这本手册所要介绍的正是这样的工作流程。
当你读完这份指南时,你会发现将代码推送到dev分支后,构建好的版本会自动被分发到Firebase App Distribution平台和TestFlight上。而将代码推送到prod分支,则会同时发布到Google Play商店和Apple App Store上。从此以后,你再也不用手动导出IPA文件或上传APK文件了。
实现这一切的工具是GitHub Actions,它提供了用于执行自动化操作的云服务器;而Fastlane则负责处理代码构建、签名以及分发等相关工作。这本手册将这两项工具都视为重要的生产基础设施,认为它们和应用程序本身一样,需要得到同样的重视和详细的文档说明。
目录
先决条件
在开始之前,请确保以下条件都已满足。如果缺少任何一项,都会导致难以诊断的故障。
拥有GitHub仓库的Flutter项目:该项目应该已经能够在本地进行构建了。如果在你的机器上,
flutter build apk --release和flutter build ios --release --no-codesign这两个命令都能成功执行,那么你就准备好了。拥有管理员或账户持有者权限的Apple开发者账户:你需要这个账户来创建App Store Connect API密钥。仅具有开发者权限是不够的。
拥有至少处于“草稿”状态的已发布应用的Google Play Console账户:Google Play API无法将应用推送到从未上传过任何版本的应用上。如果你的应用是全新的,你需要先手动上传一次,才能让自动化流程开始运行。
一个启用了Android和iOS版本发布的Firebase项目:
在开发机器上安装了Ruby:
Fastlane是一个基于Ruby开发的工具。运行ruby -v即可检查是否已安装Ruby。macOS系统本身预装了Ruby,但版本可能比较陈旧。可以通过Homebrew安装最新版本的Ruby:brew install ruby。在本地安装了Fastlane:
使用gem install fastlane命令进行安装。在配置过程中,你需要在终端中使用这个工具,之后CI服务器会接管相关任务。在macOS上安装了Homebrew:
Homebrew用于在本地安装各种开发依赖项。熟悉使用的终端环境:
本指南中的每一步操作都需要执行命令。对于大多数步骤来说,都没有图形化界面可供使用。
什么是CI/CD?为什么你的Flutter应用需要它
概念说明
CI/CD代表持续集成和持续交付。其核心理念就是自动化代码编写与代码发布之间的整个流程。持续集成意味着每次代码修改都会自动被构建并测试;而持续交付则意味着每次成功的构建结果都会自动被准备好进行分发。
对于移动应用开发来说,这一概念的重要性远超其他软件领域。移动应用的构建过程非常复杂:涉及到使用证书对代码进行签名、配置各种配置文件、管理密钥库以及生成API密钥等等,这些步骤必须以正确的方式完成,才能确保构建成功。如果这些操作都是手动完成的,出错的风险就会大大增加;而通过自动化手段来执行这些任务,就可以确保过程的可靠性和可重复性。
为什么手动部署存在问题
当部署过程是手动的时,随着时间的推移,会出现一些问题。首先,这会使得部署成为一项需要专门技能的任务——只有那些曾经做过这项工作的人才了解具体的操作步骤;而当这些人无法参与开发工作时,团队就无法完成应用的发布工作。
其次,这种构建方式存在不一致性。某个人在笔记本电脑上进行的构建过程所使用的环境变量或Xcode设置,可能与另一个人使用相同设备进行构建时所设置的参数有所不同。
第三,这样的构建流程效率很低。构建、生成归档文件以及上传操作都会中断实际的工程开发工作流程。
自动化技术能够解决这三个问题。所有构建步骤都被记录在版本控制系统中,因此每次运行时环境都是相同的——因为这些步骤都是通过这些版本控制文件在云端重新创建的虚拟机器来执行的。而且,在你继续进行其他开发工作的同时,这些构建过程可以在后台自动运行。
架构:各组成部分之间的关联关系
在修改任何配置文件之前,首先需要了解整个系统的运作原理以及各个组件是如何相互配合的。如果不先弄清楚这些关系,直接开始开发工作的话,遇到问题时就会不知道该从哪里入手进行调试。
GitHub Actions提供了基于云端的虚拟机器来执行构建任务。每当你将代码推送到指定的分支时,GitHub会自动启动一台新的虚拟机器(对于Android应用使用Ubuntu系统,对于iOS应用使用macOS系统),然后执行你在工作流文件中定义的构建步骤,完成后就会关闭这台虚拟机器。每次使用时,这台虚拟机器都会被彻底重置为初始状态。
Fastlane是一款开源工具,专门用于自动化移动应用的构建和部署流程。它会在GitHub Actions提供的虚拟机器环境中运行,负责处理与特定平台相关的操作,例如生成应用程序包文件、管理iOS应用的代码签名过程以及将编译后的二进制文件上传到相应的分发平台。你需要编写一系列名为“lane”的脚本序列,GitHub Actions会按照这些脚本来自动执行相应的构建流程。
Fastlane Match是Fastlane系统中专门用于iOS代码签名的子系统。开发iOS应用程序时,需要在用于构建这些应用的机器上安装证书和配置文件。Match会将这些文件存储在加密后的私有GitHub仓库中,并在构建过程开始之前将它们下载到持续集成工具中。这样一来,就彻底避免了在多台机器上手动管理证书所带来的麻烦。Firebase应用分发服务会接收您为开发环境准备的APK和IPA文件,并自动通知您的测试人员。
App Store Connect与Google Play Console则会接收您为生产环境准备的应用版本。
证书存储库是一个独立的私有GitHub仓库,Fastlane Match会从这个仓库中读取和写入数据。其中保存了您的iOS应用签名所需的文件,这些文件都经过了加密处理,而解密密码只有您自己知道。
生成您的凭证和密钥
这一环节需要您访问多个第三方管理平台,以收集CI流程所需的各类凭证。
Firebase凭证
Firebase应用分发服务需要两样信息:您的应用程序ID,以及一个服务账户——该账户需赋予CI服务器上传应用版本的权限。
登录Firebase控制台并打开您的项目。
进入项目设置(左侧导航栏中“项目概览”旁边的齿轮图标)。
向下滚动至您的应用部分。您会看到自己注册的Android和iOS应用列表。找到并复制每项应用的应用ID。Android应用ID的形式为1:1234567890:android:abc123def456,iOS应用ID的形式也为1:1234567890:ios:abc123def456。
点击生成新的私钥,然后确认弹出的对话框。系统会将一个.json文件下载到您的设备上。这个文件就是服务账户的凭证,请妥善保管,切勿将其上传到任何代码仓库中。
Apple App Store Connect API密钥
苹果已经将基于密码的API访问方式替换为API密钥。您需要这个密钥,才能让Fastlane在不需要使用您的Apple ID凭证的情况下与App Store Connect进行通信。
访问App Store Connect网站,然后在顶部导航栏中选择用户与访问权限选项。
在左侧侧边栏中,依次点击秘密和变量,然后再点击Actions选项。
对于下面的每一项密钥,请点击“新建仓库密钥”。密钥的名称必须与实际输入的内容完全一致,因为工作流文件会直接引用这些名称。
请逐一添加以下密钥:
环境与配置:
ENV_FILE_BASE64:对您的.env文件进行编码后得到的Base64字符串。
Firebase与Google Play:
FIREBASE_APP_ID_ANDROID:从Firebase控制台复制的Android应用ID(格式为:1:xxx:android:xxx)。FIREBASE_APP_ID_IOS:从Firebase控制台复制的iOS应用ID。FIREBASE_SERVICE_ACCOUNT_JSON:直接粘贴Firebase服务账户.json文件的原始内容。不要对这部分内容进行编码,因为工作流会直接将其写入文件中。GOOGLE_play_json:直接粘贴Google Play服务账户.json文件的原始内容。
Android签名:
ANDROID_KEYSTORE_BASE64:对.jks密钥库文件进行编码后得到的Base64字符串。ANDROID_KEY_ALIAS:在生成密钥库时所使用的别名(例如:your-app-key)。ANDROID_KEY_PASSWORD:在生成密钥库时设置的密钥密码。ANDROID_STORE_PASSWORD:在生成密钥库时设置的存储密码。
Apple App Store:
APPSTORE_ISSUER_ID:从App Store Connect API密钥页面获取的发行者ID。APPSTORE_API_KEY_ID:从App Store Connect API密钥页面获取的密钥ID。APPSTORE_API_PRIVATE_KEY_BASE64:对.p8文件进行编码后得到的Base64字符串。
Fastlane Match:
MATCH_GIT_BASIC_AUTHORIZATION:格式为username:token的Base64字符串。MATCH_PASSWORD:您自己创建的一个强密码。这个密码用于加密Match仓库中的证书。建议使用密码管理工具来生成这样的强密码,并妥善保管它,因为一旦丢失,就必须重新创建证书仓库。
为Android项目配置Fastlane
Fastlane for Android会被保存在您的Flutter项目的android/目录中。请创建以下文件。
Gemfile文件
# android/Gemfile
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
source "https://rubygems.org"这一行告诉Bundler(Ruby的包管理工具)从哪里获取所需的软件包。gem "fastlane"则表示将Fastlane作为项目的依赖项。
plugins_path这一行用于在Pluginfile文件存在的情况下,从中加载额外的插件配置信息。这种结构使得主Gemfile文件与插件列表可以分开维护,这也是Fastlane项目所遵循的常规做法。
请始终使用Bundler来执行Fastlane命令(即bundle exec fastlane),而不是直接调用fastlane命令。因为Bundler能确保使用Gemfile.lock文件中指定的软件包版本,从而保证在不同机器上构建项目时结果的一致性。
Gradle属性文件
# android/gradle.properties
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs用于配置Gradle构建过程中Java虚拟机的运行参数。-Xmx4G将最大堆内存大小设置为4GB。-XX:MaxMetaspaceSize=1G将元空间(类元数据)的大小限制为1GB。-XX:ReservedCodeCacheSize=512m为编译后的代码缓存预留512MB的内存。-XX:+HeapDumpOnOutOfMemoryError当JVM内存不足时,会生成堆栈转储文件,从而便于进行故障排查。
如果不进行这样的配置,在使用GitHub Actions进行Gradle构建时,运行过程经常会因为JVM内存限制而失败,因为默认的JVM内存设置超过了标准GitHub托管环境允许的最大值(7GB RAM)。
Android Appfile文件
# android/fastlane/Appfile
json_key_file(ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"])
package_name("com.yourcompany.app")
json_key_file(...)这一行告诉Fastlane从哪里获取用于访问Google Play商店的Service Account JSON文件。该文件的路径由GitHub Actions的工作流步骤来确定,具体值存储在FIREBASE_SERVICE ACCOUNT_JSON_PATH环境变量中。package_name(...)用于指定应用程序的包名。请将com.yourcompany.app替换为您在AndroidManifest.xml文件中定义的实际应用包名。
Android插件文件
# android/fastlane/Pluginfile
gem 'fastlane-plugin-firebase_app_distribution'
这一行代码声明将“Firebase App Distribution”插件作为依赖项添加到项目中。Fastlane的核心安装版本并不包含针对特定平台的插件。通过添加fastlane-plugin-firebase_app_distribution这个Gem包,系统才会具备firebase_appdistribution这个动作,而firebase流程也正是利用这个动作来上传构建文件并通知测试人员。如果没有这一行代码,当firebase流程尝试调用firebase_appdistribution时,就会出现“方法未定义”的错误。
Android Fastfile文件
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "将新的测试版本提交到Firebase App Distribution"
lane :firebase do
notes = ENV["RELEASE_NOTES"]
if notes.nil? || notes.strip.empty?
file_path = File.join(Dir.pwd, "..", "release_notes.txt")
if File.exist?(file_path) && !File.read(file_path).strip.empty?
notes = File.read(file_path)
else
notes = "新的构建文件由持续集成系统上传"
end
end
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID_ANDROID"],
apk_path: "../build/app/outputs/flutter-apk/app-release.apk",
groups: "testers",
release_notes: notes,
service_credentials_file: ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"]
)
end
desc "将应用部署到Google Play商店"
lane :prod do
upload_to_play_store(
track: 'production',
aab: "../build/app/outputs/bundle/release/app-release.aab",
json_key: 'play-store-service-account.json',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
end
default_platform(:android)这一行代码指定了默认的操作环境,让Fastlane知道当前正在处理一个Android项目。lane :firebase do定义了一个名为firebase的流程步骤序列。
在文件的开头部分,notes这段代码会优先从三个来源获取发布说明:首先是从环境变量RELEASE_NOTES中获取(当通过GitHub Actions手动触发工作流并且指定了相关参数时,这个变量就会被设置);其次是从项目根目录下的release_notes.txt文件中读取内容;如果前两个来源都没有提供有效信息,就会使用一个默认的备用字符串。而firebase_app_distribution(...)则是该插件提供的核心功能。
app: ENV["FIREBASE_APP_ID_ANDROID"]这一行代码指定了要上传到哪个Firebase应用上,这些信息是从工作流中设置的环境变量中获取的。apk_path指明了Flutter生成的可执行APK文件的位置。groups: "testers"指定了在Firebase App Distribution中要通知的测试人员组别。请根据实际情况替换这个值。对于prod流程来说,upload_to_play_store(...)是Fastlane内置提供的动作。track: 'production'表示将应用上传到生产环境对应的存储区。skip_upload_metadata: true、skip_upload_images: true以及skip_upload_screenshots: true这些选项的作用是防止Fastlane尝试管理与应用商店相关的信息,因为这些功能并不属于这个构建流程的职责范围。
为iOS设置Fastlane
由于代码签名的原因,iOS的配置过程比Android更为复杂。`ios/`目录需要单独的Fastlane配置文件。
iOS的Gemfile文件
# ios/Gemfile
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
其结构与Android的Gemfile文件完全相同。iOS和Android使用不同的Bundler环境,因为它们的目录位置不同,可能也需要不同的gem版本或插件。在`ios/`目录下运行`bundle install`命令时,安装的gem会与`android/`目录中安装的gem相互独立。
iOS的Appfile文件
# ios/fastlane/Appfile
app_identifier("com.yourcompany.app")
`app_identifier(...)`用于指定iOS应用的标识符。这个标识符必须与Xcode中设置的值完全一致(可以在“运行器目标”的“一般”选项卡中查看)。请将`com.yourcompany.app`替换为你的实际应用标识符。Fastlane Match在生成证书和配置文件时,会使用这个标识符。
Matchfile文件
# ios/fastlane/Matchfile
git_url(ENV["MATCH_GIT_URL"] || "https://github.com/YOUR_GITHUB_USERNAME/your-certificates-repo")
storage_mode("git")
type("appstore")
`git_url(...)`用于指定私有证书仓库的位置。在GitHub Actions的工作流程中,`MATCH_GIT_URL`环境变量会包含个人访问令牌,这样Match才能成功访问私有仓库。当在本地运行Match时,如果未设置该环境变量,系统会要求用户手动输入凭据。`storage_mode("git")`表示使用Git作为存储后端,而不是S3或Google Cloud Storage。`type("appstore")`用于指定默认的证书类型,不过每个Fastlane通道都可以自行修改这个设置。
iOS的Pluginfile文件
# ios/fastlane/Pluginfile
gem 'fastlane-plugin-firebase_app_distribution'
在iOS平台上,用于上传ad-hoc IPA文件到Firebase的`firebase`通道也需要使用相同的Firebase App Distribution插件。iOS和Android的Pluginfile文件是分开的,因此都需要包含这个插件的声明。
iOS的Fastfile文件
# ios/fastlane/Fastfile
default_platform(:ios)
before_all do
setup_ci
end
platform :ios do
desc "将新的测试版版本推送到TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
issuer_id:ENV["APPSTORE_CONNECT_API_KEY_ISSUER_ID"],
keyfilepath: ENV["APP STORE_CONNECT_API_KEY_KEY_FILEPATH"],
in_house: false
)
match(
type: "appstore",
readonly: false,
app_identifier: "com.YOUR-APP.app",
api_key: api_key
)
update_code_signing_settings(
path: "Runner.xcodeproj",
use_automatic_signing: false,
team_id: "GL369K3W98",
code_sign_identity: "Apple Distribution",
profile_name: "match AppStore com.YOUR-APP.app",
targets: ["Runner"]
)
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store"
)
notes = ENV["RELEASE_NOTES"]
if notes.nil? || notes.strip.empty?
file_path = File.join(Dir.pwd, "..", "release_notes.txt")
if File.exist?(file_path) && !File.read(file_path).strip.empty?
notes = File.read(file_path)
else
notes = "新的构建版本由持续集成系统上传"
end
end
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: notes
)
end
desc "部署到Apple App Store"
lane :prod do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
issuer_id:ENV["APPSTORE_CONNECT_API_KEY_ISSUER_ID"],
keyfilepath: ENV["APP STORE_CONNECT_API_KEY_KEY_FILEPATH"],
in_house: false
)
match(
type: "appstore",
readonly: false,
app_identifier: "com.YOUR-APP.app",
api_key: api_key
)
update_code_signing_settings(
path: "Runner.xcodeproj",
use_automatic_signing: false,
team_id: "GL369K3W98",
code_sign_identity: "Apple Distribution",
profile_name: "match AppStore com.YOUR-APP.app",
targets: ["Runner"]
)
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store"
)
upload_to_app_store(
force: true, # 跳过HTML报告生成
submit_for_review: false, # 直接上传到App Store Connect,不自动提交审核
automatic_release: false
)
end
desc "将新的测试版版本推送到Firebase App Distribution"
lane :firebase do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
issuer_id:ENV["APPSTORE_CONNECT_API_KEY_ISSUER_ID"],
keyfilepath: ENV["APP STORE_CONNECT_API_KEY_KEY_FILEPATH"],
in_house: false
)
match(
type: "adhoc",
readonly: false,
app_identifier: "com.YOUR-APP.app",
api_key: api_key
)
update_code_signing_settings(
path: "Runner.xcodeproj",
use_automatic Signing: false,
team_id: "GL369K3W98",
code_sign_identity: "Apple Distribution",
profile_name: "match AdHoc com.YOUR-APP.app",
targets: ["Runner"]
)
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "ad-hoc"
)
notes = ENV["RELEASE_NOTES"]
if notes.nil? || notes.strip.empty?
file_path = File.join(Dir.pwd, "..", "release_notes.txt")
if File.exist?(file_path) && !File.read(file_path).strip.empty?
notes = File.read(file_path)
else
notes = "新的构建版本由持续集成系统上传"
end
end
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID_IOS"],
groups: "testers",
release_notes: notes,
service_credentials_file:ENV["FIREBASE_SERVICE_ACCOUNT_JSON_PATH"]
)
end
end
before_all do setup_ci end 这一指令会在所有测试环节之前执行。setup_ci 是 Fastlane 中内置的一个操作,它的作用是配置用于持续集成环境的相关设置:它会创建一个临时密钥链(这样在安装证书时就不会出现 macOS 要求输入密码的提示),禁用代码签名相关的弹窗,并配置其他与持续集成相关的选项。如果没有执行这个步骤,证书的安装过程将会因为用户需要点击一个永远不会出现的确认对话框而陷入停滞。
app_store_connect_api_key(...) 这个指令会读取 App Store Connect 的 API 密钥,并生成一个 API 密钥对象,后续的其他操作会使用这个对象来进行 App Store 验证。key_id、issuer_id 和 key_filepath 这些参数都来自于工作流程中设置的环境变量。in_house: false 表示这是一个普通的开发者账户(而不是 Apple Enterprise Program 账户,后者有着不同的分发规则)。
match(type: "appstore", ...) 这个指令会连接证书仓库,下载 App Store 分发证书和配置文件,然后将它们安装到 macOS 的密钥链中。
readonly: false 如果证书还不存在,这个选项允许 Match 自动生成证书。对于一个新的项目来说,在第一次执行这个指令时,Match 会生成证书并将其上传到仓库中;之后的执行过程只需下载已经存在的证书即可。对于 firebase 测试环节来说,会使用 type: "adhoc" 这个选项,因为 Firebase App Distribution 需要的是临时分配的证书,而不是 App Store 的证书。
update_code_signing_settings(...) 这个指令会修改 Xcode 项目文件,使其使用 Match 刚刚下载到的证书和配置文件。
use_automatic_signing: false 这个选项非常重要:如果启用自动签名功能,Xcode 会尝试自行管理证书,但在没有图形界面的持续集成环境中,这种操作是无法正常进行的。team_id: "YOUR_team_ID" 这里填写的是你的 Apple 开发者团队 ID,这个信息可以在 Apple 开发者门户的“会员信息”板块中找到。profile_name: "match AppStore com.yourcompany.app" 这个参数遵循 Match 在创建配置文件时所使用的命名规则。
build_app workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store") 这个指令会调用 xcodebuild 命令来打包并导出应用程序。Runner.xcworkspace 是 Flutter 生成的 Xcode 工作区文件。当项目中包含了 CocoaPods 依赖项时,就必须使用工作区文件而不是项目文件本身。export_method: "app-store" 这个参数指定了最终生成的 IPA 文件应使用哪种导出格式。对于 Firebase 测试环节来说,应该使用 "ad-hoc" 这种格式。
upload_to_testflight(skip_waiting_for_build_processing: true) 这个指令会将生成的 IPA 文件上传到 App Store Connect 平台。skipwaiting_for_build_processing: true 这个选项告诉 Fastlane 不需要等待 Apple 完成构建过程的处理,因为这个过程可能需要 15 到 30 分钟的时间。一旦上传完成,整个工作流程也就结束了。等到 Apple 完成他们的处理后,该应用程序就会出现在 TestFlight 平台上。upload_to_app_store FORCE: true, submit_for_review: false, automatic_release: false) 该命令会将应用程序上传到 App Store Connect,以便进行正式发布。force: true 会跳过 Fastlane 生成的 HTML 总结报告,因为在持续集成环境中这份报告并无实际用途。submit_for_review: false 表示在上传构建结果后不会自动提交审核流程,这样你就可以先自行检查后再进行提交。automatic_release: false 可以防止应用程序在获得审核通过后自动发布。
编写 GitHub Actions 工作流
工作流其实是存储在仓库根目录下的 .github/workflows/ 文件中的 YAML 格式文件。每个工作流文件都会定义一个工作流的名称、触发该工作流的事件类型,以及需要执行的步骤顺序。
Android 工作流
# .github/workflows/android_distribution.yml
name: Android Firebase 应用程序发布流程
on:
push:
branches:
- dev
- prod
workflow_dispatch:
inputs:
release_notes:
description: '发布说明'
required: false
default: '由 GitHub Actions 手动触发'
jobs:
distribute_android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- run: flutter pub get
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android
- name: 解码密钥库文件
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTOREBASE64 }}
run: |
echo $ANDROID_KEYSTORE_BASE64 | base64 --decode > android/app/upload-keystore.jks
echo "storeFile=upload-keystore.jks" > android/key.properties
echo "storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}" >>> android/key.properties
echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >>> android/key.properties
echo "keyAlias=${{ secrets.ANDROID_KEY_alias }}" >>> android/key.properties
- name: 创建 .env 文件
env:
ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }}
run: echo $ENV_FILE_BASE64 | base64 --decode > .env
- name: 构建 Android 发布版本
run: |
if [ "${{ github.ref_name }}" == "prod" ]; then
flutter build appbundle --release
else
flutter build apk --release
fi
- name: 生成 Firebase 服务账户 JSON 文件
if: ${{ github/ref_name == 'dev' }}
env:
FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICEACCOUNT.JSON }}
run: echo $FIREBASE_SERVICE ACCOUNT_json > android/firebase-service-account.json
- name: 在开发环境中将应用程序发布到 Firebase App Distribution
if: ${{ github.ref_name == 'dev' }}
env:
FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_IDANDROID }}
FIREBASE_SERVICE_ACCOUNT_JSON_PATH: "firebase-service-account.json"
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
run: bundle exec fastlane firebase
working-directory: android
- name: 在正式环境中将应用程序发布到 Google Play Store
if: ${{ github.ref_name == 'prod' }}
env:
GOOGLE_PLAY_JSON: ${{ secrets.GOOGLE_Play.JSON }}
run: |
echo $GOOGLE_play_json > play-store-service-account.json
bundle exec fastlane prod
working-directory: android
name: Android Firebase应用分发 是在您仓库的GitHub Actions选项卡中显示的名称。
on: push: branches: [dev, prod] 用于配置触发条件。每当有提交被推送到dev或prod分支时,这个工作流就会运行。其他分支,包括main和develop,都不会触发此工作流,因为这些分支属于仅用于测试的阶段分支。
workflow_dispatch: inputs: release_notes 可以添加手动触发机制。在GitHub Actions选项卡中,您可以点击“运行工作流”,并根据需要输入发布说明,这些信息会被传递给Fastlane。这对于进行测试或进行临时性发布非常有用。
runs-on: ubuntu-latest 指定了使用的虚拟机。之所以选择Ubuntu,是因为Android构建工具链是在Linux环境下运行的,而且使用Ubuntu作为运行环境比使用macOS更经济实惠。
actions/checkout@v4 会将您的仓库克隆到虚拟机的工作目录中。如果没有这个步骤,其他后续操作就无法访问您的代码。
actions/setup-java@v3 使用Zulu发行版安装Java 17。当前大多数Flutter项目都在使用Gradle 8,而Java 17是确保Gradle 8能够正常运行的必备版本。如果没有正确的Java版本,Gradle会立即出现运行错误。
subosito/flutter-action@v2 用于安装Flutter SDK。channel: 'stable'表示使用稳定版发布渠道,这对于生产环境的构建来说是非常合适的。cache: true则可以在多次运行工作流时重复利用已下载的Flutter SDK文件,从而大大缩短后续运行的准备时间。
ruby/setup-ruby@v1 安装Ruby 3.2,并且当设置bundler-cache: true时,会自动在android/目录中执行bundle install命令。bundler-cache选项还可以在多次运行过程中重复使用已安装的Ruby gem包,这样每次运行工作流时都能节省两到三分钟的时间。
Decode Keystore 这一步骤是Android安全配置的核心。命令echo $ANDROID_KEYSTORE_BASE64 | base64 --decode > android/app/upload-keystore.jks用于将Base64编码格式转换回二进制文件,从而生成位于指定路径的.jks文件。后续的echo命令会生成key.properties文件,Android Gradle在构建过程中会读取这个文件来获取密钥库文件及其密码。这个文件每次运行工作流时都会根据 secrets中的信息重新生成,因此永远不会被永久保存在任何地方。
if [ "${{ github.ref_name }}" == "prod" ] 是一个Bash条件语句。github/ref_name表示触发本次推送操作的分支名称。如果分支是prod,那么工作流会生成App Bundle文件(格式为.aab,适用于Play Store);否则(如果是dev分支),则会生成APK文件(格式为.apk,这种格式更简单、生成速度更快,适合用于Firebase应用分发)。通过这个条件语句,同一个工作流脚本能够同时处理这两种不同的分支情况。if: ${{ github.ref_name == 'dev' }}是一种基于步骤条件的判断机制。只有当触发分支为dev时,包含这一条件的步骤才会被执行。在针对prod分支进行的推送操作中,Firebase相关的分发步骤会被完全跳过;而在针对dev分支进行的推送操作中,Play Store相关的步骤也会被完全忽略。iOS工作流程
# .github/workflows/ios_distribution.yml
name: iOS TestFlight与Firebase应用分发
on:
push:
branches:
- dev
- prod
workflow_dispatch:
inputs:
release_notes:
description: '发布说明'
required: false
default: '通过GitHub Actions手动触发'
jobs:
distribute_ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- run: flutter pub get
- name: 创建.env文件
env:
ENV_FILE_BASE64: ${{ secrets.ENV_FILE_BASE64 }}
run: echo $ENV_FILE_BASE64 | base64 --decode > .env
- name: 构建Flutter iOS版本(不进行代码签名)
run: flutter build ios --release --no-codesign
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ios
- name: 配置Fastlane匹配信息
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secretsMATCH_GIT_BASICAUTHORIZATION }}
run: |
echo "MATCH_PASSWORD=${MATCH_PASSWORD}" >> $GITHUB_ENV
AUTH=$(echo "$MATCH_GIT_BASIC_AUTHIZATION" | base64 --decode)
echo "MATCH_GIT_URL=https://$AUTH@github.com/YOUR_Github_USERNAME/your-certificates-repo" >> $GITHUB_ENV
- name: 为App Store Connect创建认证密钥
env:
APPSTORE_API_PRIVATE_KEY_BASE64: ${{ secrets.APPSTORE_API/Private_KEY_BASE64 }}
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
run: |
mkdir -p ~/.appstoreconnect/private_keys/
echo $APPSTORE_API_private_KEY_BASE64 | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8
- name: 生成Firebase服务账户JSON文件
if: ${{ github.ref_name == 'dev' }}
env:
FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE ACCOUNT.JSON }}
run: echo $FIREBASE_SERVICEACCOUNT_json > ios/firebase-service-account.json
- name: 将应用分发到Firebase App Distribution(开发环境)
if: ${{ github.ref_name == 'dev' }}
env:
FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID-ios }}
FIREBASE_SERVICE ACCOUNT_JSON_PATH: "firebase-service-account.json"
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE ISSUER ID }}
APP STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APP_store_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
run: bundle exec fastlane firebase
working-directory: ios
- name: 将应用分发到TestFlight(开发环境)
if: ${{ github.ref_name == 'dev' }}
env:
APP STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE ISSUER ID }}
APP STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APP STORE_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
run: bundle exec fastlane beta
working-directory: ios
- name: 将应用分发到Apple App Store(生产环境)
if: ${{ github.ref_name == 'prod' }}
env:
APP STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE ISSUER ID }}
APP STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APP STORE_CONNECT_API_KEY_KEY_FILEPATH: ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPSTORE_API_KEY_ID }}.p8
run: bundle exec fastlane prod
working-directory: ios
runs-on: macos-latest这一要求对于iOS构建来说是不可更改的。Xcode仅能在macOS上运行,而xcodebuild(Fastlane在内部使用的工具)也仅在macOS上可用。与Ubuntu相比,macOS构建环境的成本每分钟要高出大约十倍,因此Android项目才会选择使用Ubuntu。但对于iOS来说,没有其他替代方案。
flutter build ios --release --no-codesign这条命令会将Flutter的Dart代码以及原生iOS框架代码编译成发布版本,且不会进行任何代码签名操作。--no-codesign这个参数非常重要:因为此时签名证书尚未安装,所以Flutter的构建过程不应该尝试进行签名操作。Fastlane Match会在后续的处理步骤中下载并安装正确的签名证书,从而完成签名工作。
“配置Fastlane Match”这一步骤的作用非常关键。AUTH=$(echo "$MATCH_GIT_BASIC_AUTHORIZATION" | base64 --decode)这条命令会将Base64编码的username:token字符串解码成明文。echo "MATCH_GIT_URL=https://$AUTH@github.com/..." >> $GITHUB_ENV这条命令会将包含token的授权URL写入$GITHUB_ENV文件中,GitHub Actions会读取这个文件,从而将相应的环境变量传递给后续步骤。这种授权URL的格式https://username:token@github.com/...属于HTTP Basic Authentication类型,Git在非交互式环境中就是使用这种格式来传递认证信息的。
“创建认证密钥”这一步骤的作用是重新生成.p8格式的密钥文件。mkdir -p ~/.appstoreconnect/private_keys/这条命令会创建Fastlane所需用来存储密钥的目录。echo $APPSTORE_API_PRIVATE_KEY_BASE64 | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8这条命令会将解码后的密钥写入与app_store_connect_api_key命令所期望的文件名格式完全匹配的文件中。
对于dev分支来说,iOS开发流程会同时执行两个并行分布任务:firebase通道会生成ad-hoc格式的IPA文件并上传到Firebase App Distribution平台,而beta通道则会生成App Store兼容的IPA文件并上传到TestFlight平台。这两个任务都会在共享的设置步骤之后依次执行。因此,只要向dev分支进行一次推送操作,构建好的应用程序就会自动被发送到这两个分发渠道中。
截图:
Android和iOS开发流程运行中的状态:
Android开发流程完成后的状态:

已完成的iOS工作流程:
同时完成的Android和iOS工作流程:
Firebase应用分发——Android版本:
Firebase应用分发——iOS版本:
用于TestFlight的iOS版本构建:
完整的部署流程是如何运作的
当所有配置都准备就绪后,从将代码推送到dev分支开始,整个过程会按照以下顺序进行:
这两个工作流程是并行执行的,因此整个过程所花费的总时间大致取决于耗时较长的那个平台——通常情况下,由于Xcode的编译时间较长,iOS版本的部署会花费更长时间。
对于向prod分支推送代码的情况,整个流程的结构是相同的,但最终的分发步骤会将应用上传到Google Play Store(针对Android设备)或App Store Connect(针对iOS设备)。
最佳实践
保持证书仓库的私密性并控制访问权限
证书仓库中存放着使用“Match”密码加密后的iOS签名文件。尽管这些文件已经经过加密处理,但对待对这一仓库的访问仍应像对待生产环境中的数据库一样谨慎。对于不再需要的个人访问令牌,应及时予以撤销;切勿以明文形式在任何地方分享“Match”密码。
设定最低构建编号策略
自动化的持续集成流程要求每次上传时都必须使用唯一的构建编号。App Store Connect和Google Play都会拒绝那些具有重复构建编号的上传文件。因此,需要实施一种无需人工干预的版本控制机制。一个可靠的方法是使用GitHub Actions中的`GITHUB_RUN_NUMBER`变量——这个整数会在每次工作流运行时递增:
- name: 设置构建编号
run: |
BUILD_NUMBER=${{ github.run_number }}
# 对于Flutter项目,需要在pubspec.yaml文件中更新构建编号
sed -i '' "s/version: .*/version: 1.0.0+${BUILD_NUMBER}/" pubspec.yaml
`github.run_number`是GitHub提供的一个环境变量:对于仓库中的第一个工作流运行,该变量的初始值为1;此后每次运行时,其值都会增加1。这样一来,所有构建过程的编号都能保持唯一且呈递增趋势。`sed`命令会将`pubspec.yaml`文件中的版本信息替换为包含运行编号的新格式。
设置分支保护规则
在实现了自动化流程之后,还需要采取措施防止意外情况下直接对分支进行推送操作。在仓库的设置中,进入“分支”页面,为`main`、`develop`、`dev`和`prod`这些分支设置相应的保护规则。
特别是对于`prod`分支,可以考虑要求在合并代码之前必须获得至少一次拉取请求的批准,这样就能在正式发布之前设置一道人为审核关卡。
监控工作流的运行时间与成本
GitHub Actions的费用是根据运行所消耗的分钟数来计算的。macOS平台上的运行费用是Linux平台的10倍。请前往您所在GitHub组织的“设置”页面,然后选择“计费”选项,查看当前的使用情况。
缓存机制是效果最为显著的优化手段。对于Flutter项目来说,只需将`cache: true`这个配置添加到脚本中;而对于Ruby项目,则需要设置`bundler-cache: true`。在第一次运行之后,后续的运行过程如果能够利用到缓存数据,就可以完全跳过下载和提取文件的步骤。
将发布说明保存在文件中,而不仅仅是作为输入参数
在Fastfile配置文件中,`release_notes.txt`这个文件被设置为默认的发布说明存储位置。这意味着您可以在提交拉取请求时同时附上发布说明,而这些说明会自动出现在Firebase和TestFlight发布的通知中。请在项目根目录下创建这个文件,并在每次发布新版本时更新其中的内容。这样,发布说明就能与所描述的代码一起被保存在版本历史记录中了。
常见错误
在Fastlane中使用Xcode项目而非工作区
Flutter iOS项目始终使用工作区(Runner.xcworkspace),而不是项目文件(Runner.xcodeproj),因为CocoaPods依赖关系是在工作区层面进行配置的。如果将Runner.xcodeproj传递给build_app命令,会因为缺少依赖项而失败。因此,请务必使用workspace: "Runner.xcworkspace"。
未为iOS设置setup_ci
如果在before_all块中省略了setup.ci,那么当macOS等待keychain访问权限时,工作流程会无限期地停滞下去,因为这种等待永远不会结束。这看起来像是超时错误,但实际上问题出在设置上。因此,在任何用于持续集成测试的iOS Fastfile中,都必须包含before_all do setup_ci end这段代码。
在新项目中以只读模式运行Match工具
当首次使用新的应用标识符运行Match工具时,系统需要生成证书和配置文件。如果将readonly: true设置为true,Match工具就无法生成这些文件,从而会出现“未找到证书”的错误。因此,请务必将readonly: false设置为false。在生产环境中,有些团队会在初始设置完成后将readonly: true设置为true,以防止证书被意外重新生成,但在这种初始设置阶段,使用readonly: false才是正确的做法。
忘记增加构建编号
无论是Apple还是Google,都会拒绝那些版本号与之前上传过的构建版本相同的构建请求。如果你在没有增加构建编号的情况下将代码两次推送到dev分支,第二次推送就会失败。GITHUB_RUN_NUMBER这一最佳实践机制可以自动避免这种情况的发生。
在文件编码时添加尾随换行符
如果使用echo "content" | base64而不是echo -n "content" | base64进行编码,那么在编码之前字符串的末尾会添加一个换行符。当在持续集成环境中解码这些文件时,文件中就会包含这个原本不存在的换行符。对于MATCH_GIT_BASIC_AUTHORIZATION中的username:token字符串来说,尾随的换行符会导致认证失败,而这种错误看起来就像是权限问题。因此,在对非文件类型的字符串进行编码时,请务必使用echo -n命令。
为Firebase选择了错误的分发类型
iOS版本的Firebase应用分发需要使用ad-hoc分发证书,而不是App Store证书。将用App Store签名的IPA文件上传到Firebase是会失败的,因为ad-hoc构建版本就是专门用于在App Store之外直接在设备上分发的。正因如此,在iOS Fastfile中的firebase流程中明确指定了type: "adhoc"和export_method: "ad-hoc"这些参数。beta流程则使用type: "appstore",因为TestFlight需要App Store证书才能正常运行。
为Google Play服务账户授予不足的权限
在将应用上传到Play Store时,最常见的失败原因是API权限设置错误。该服务账户必须至少被配置为具有“发布管理员”权限,才能与您的Play Console应用关联起来。在Google Cloud中创建服务账户仅仅完成了设置过程的一半工作——您还必须在Play Console中为其配置API访问权限。如果忽略这一步骤,Fastlane在尝试上传应用时就会遇到403 Forbidden错误。
结论
您所建立的这套基础设施能够带来持续的收益。当您第一次将代码推送到dev环境,然后看到GitHub Actions界面中同时显示Android和iOS版本的构建过程已完成时,就会立刻感受到这套系统的价值。无论是第四次、第十次还是第五十次进行这样的操作,其带来的好处都会持续累积——因为整个部署过程是在您毫无察觉的情况下自动完成的。
本指南中介绍的架构涵盖了常见的使用场景,但所使用的工具(如GitHub Actions、Fastlane、Match)具有很高的灵活性,几乎可以适应任何类型的工作流程。团队可以根据实际需求添加自动化测试步骤,在构建完成后发送Slack通知,通过Git标签来管理版本号,同时还可以支持除了dev和prod之外的其他目标环境。您所建立的这个基础框架完全能够支撑这些扩展功能。
其中有一条原则尤其值得强调:请像对待生产环境中的代码一样谨慎地处理您的CI配置文件。在提交修改请求时,一定要仔细审核工作流程文件中的变更内容;对于那些不太显而易见的步骤,也要添加注释说明。同时,要将敏感信息妥善保管在专门的保密仓库中,而不要放在工作流程文件中。如果工作流程文件存在问题,就会导致构建失败——这与生产环境中的代码出错的原因是一样的:未经审核的修改、缺失的上下文信息以及未记录的假设条件等。
一旦建立了这样的工作流程,您的团队就能更快、更自信地发布新版本的应用程序,因为将代码交付给测试人员的过程不再是一个繁琐且容易出错的手动操作。这应该就是代码提交所带来的自然结果吧。
参考资料
GitHub Actions
GitHub Actions文档
关于工作流程语法、上下文设置、敏感信息管理以及运行器配置的详细说明。
用于在工作流程中检出代码库的官方动作。
一个由社区维护的动作,用于在GitHub Actions运行环境中安装Flutter SDK。
官方提供的Ruby动作,可用于安装指定版本的Ruby,并可选地运行Bundler工具。
关于运行器使用时间、计费方式以及macOS和Windows运行器的费用计算规则的说明。
Fastlane
Fastlane文档
涵盖了所有Fastlane命令的详细说明,包括upload_to_testflight、upload_to_play_store、match和build_app等。Fastlane Match文档
关于代码签名管理系统的详细说明,包括初始设置及证书更新流程。firebase_app_distribution插件文档
该插件为Fastlane添加了firebase_appdistribution命令,便于进行应用分发操作。
Apple
App Store Connect API文档
介绍了App Store Connect API密钥、所需角色以及.p8文件格式的相关信息。Apple代码签名指南
苹果官方提供的关于证书和配置文件的说明。
涵盖了测试者数量限制、构建文件有效期,以及上传与发布之间的处理时间等相关信息。
Google Play开发者API
Fastlane用于上传应用到Play Store的API相关说明,包括跟踪名称及所需权限信息。Firebase应用分发文档
关于测试用户组管理、版本发布说明以及CI/CD集成流程的详细信息。Google Cloud服务账户文档
创建和管理服务账户以及分配IAM角色的相关指南。
Flutter
Flutter构建文档
介绍了flutter build apk、flutter build appbundle和flutter build ios等命令及其参数设置。Flutter提供的Android应用签名指南
指导用户如何创建密钥库并配置Gradle以进行发布版本构建。Flutter的iOS部署指南
介绍了如何将应用部署到App Store和TestFlight平台。
相关文章
如何利用Claude与MCP构建以隐私保护为首要目标的医学图像去标识化系统
想象一下,让人工智能助手来处理成千上万的医学图像。它能够运行整个处理流程,跟踪进度,总结每一项决策,并告诉你哪些文件需要人工审核——而这一切过程,它根本不需要看到任何患者的个人信息。 乍一听,这似乎是不可能的。因为通常情况下,人工智能助手需要访问它们所要帮助处理的数据。 在这个教程中,你将构建一个不会直接查看敏感医学图像的人工智能系统。相反,它会通过精心设计的工具来协调整个去标识化处理流程,确保患者的所有数据都保留在你的机器上。 使这一切成为可能的技术是“模型上下文协议”(Model Context Protocol,简称MCP)。这一开放标准允许人工智能模型调用外部工具,而不仅仅依赖它们内置
阅读全文
如何让你的反重力技能具备可配置性(同时避免出现代码分支问题)
“反重力智能体技能”是一种非常有效的方法,可以帮助你一次性为人工智能智能体设定工作流程,并让它在各种场景中都能被重复使用。你只需编写一个简短的`SKILL.md`文件,将其放入相应的文件夹中,智能体在需要使用时就会自动加载这些配置。 然而,这类技能存在一个隐藏的局限性:它们是静态的。如果你下载了别人编写的技能代码,但希望它的行为有所改变,你就必须手动复制整个代码并进行修改。而且,最近你也应该注意到了,市面上有很多经过分叉修改的“技能版本”,这些版本往往很难进行维护。 在本次教程中,我将向你展示一种解决方法。这种方法可以让任何智能体技能读取特定项目中的配置文件,因此你完全可以使用现有的技能,只需
阅读全文
如何使用针对用户的OAuth访问机制来构建人工智能代理程序【完整手册】
当你的AI代理同时为多个人提供服务时,每一次工具调用都必须明确:该代理究竟是在代表哪位用户行事。让我们通过构建一个能够与Slack和GitHub连接的AI代理来学习如何解决这个问题。 当使用Slack时,系统会使用 해당用户的 workspace;而在GitHub上创建问题时,也会以该用户的身份在其有权访问的仓库中操作。虽然代理可能会犯错,但它绝对不能使用错误用户的权限来进行操作。 解决这个问题的方法分为两个部分,而这两个部分都在本教程的前半部分进行了讲解: 每位用户都需要单独授权。 Alice为自己授权Slack,Bob也为自己授权Slack。 代理传递的是标识符,而不是令牌。 像 alic
阅读全文
如何让你的副业项目被人们注意到,并吸引到愿意付费使用的用户
2022年,我在业余时间开发了一个小型微服务产品,最终以几千美元的价格将其卖了出去。如今,有了人工智能工具的帮助,开发这样的产品可能会更加容易。 但真正发生巨大变化的是获取关注的成本,而不是开发软件本身的成本。 我认为,在2026年,产品的分发渠道将比开发本身更为重要。在这篇文章中,我会与大家分享我在产品开发过程中所学到的经验,并试图劝阻大家在开始下一个项目之前,先不要急着直接投入编码工作。 读完这份指南后,你应该能够掌握一些实用的方法和思路,这些方法可以帮助你将自己那些充满热情的项目推向市场。 需要明确的是,这篇文章主要是针对那些正在开发数字产品的人,尤其是软件产品。不过,这些概念同样适用于
阅读全文