利用 Fastlane 自動化您的 Flutter 建置流程


摘要

本文探討如何利用 Fastlane 自動化 Flutter 建置流程,幫助開發者提升工作效率及軟體品質。 歸納要點:

  • 結合 Fastlane 與 Flutter 3.x 的新功能,提升建置流程的效率和穩定性,特別適用於大規模專案與多 Flavor 管理。
  • 透過模組化設計提高 Fastlane 腳本的可維護性,包括使用 Actions、Plugins 和 Helper functions,有效管理程式碼變更與風格一致性。
  • 深入整合 Fastlane 與雲端 CI/CD 平台,如 GitLab CI/CD 和 GitHub Actions,以優化建置流程並安全管理敏感資訊。
整體而言,Fastlane 是實現高效能 Flutter 開發的重要工具,其自動化能力對於大型專案尤為關鍵。


建立釋出版本可能是個繁瑣的過程。當你發現自己剛剛建立了一個帶有模擬後端的釋出版本,或者除錯變數設定為「始終開啟引導」,又或是版本錯誤時,這種情況實在讓人感到無比沮喪。


Fastlane自動化Flutter釋出:從CI/CD到Google Play Console一鍵部署

並不是說以上任何事情曾經發生在我身上。至少今天沒有。因為(請響起鼓聲):我已經使用 Fastlane 自動化了我的釋出建置流程。Fastlane 是一個開源自動化工具,簡化了 iOS 和 Android 應用程式的建置、測試和部署過程。

我希望自動化以下幾項工作:
檢查是否有 FIXME 註解。如果有的話,我們通常不會想要在這樣的情況下進行釋出...
更新 pubspec.yaml 中的版本號。
執行 flutter build appbundle。
建立一個包含原生除錯符號的 zip 檔案。
將 zip 檔案複製到 appbundle 資料夾中。
在檔案總管中開啟該資料夾,以便能方便地拖入 Google Play Console。

對於許多開發者而言,僅止於使用 Fastlane 來自動化單機端的建置流程是不夠的。要真正提升效率並確保應用程式的穩定性,必須將 Fastlane 整合進 CI/CD(持續整合/持續交付)系統中。在這過程中,需要考慮許多細節,例如如何利用 Git Hooks 確保每次提交都觸發自動化測試和建置;以及如何運用雲端服務(例如 Firebase, AWS CodePipeline, Azure DevOps)實現自動部署。同時,還需注意確保整個建置過程中的安全性,包括使用安全憑證管理和訪問控制以防止未授權訪問與修改。

在文章中提到對 FIXME 註解進行檢查時,可以結合靜態程式碼分析工具,如 SonarQube 或 Dart Analyzer,自動阻擋含有 FIXME 註解的版本發布,而非僅依賴人工檢查。而關於 Google Play Console 的自動上傳,更可以探索利用 Fastlane 的 `upload_to_play_store` action 配合 Google Play Developer API,以達成完全自動化的發布流程,省去手動拖曳步驟。

至於 Flutter 應用程式的安全性及版本控制策略,同樣不可忽視。在自動化過程中,需要謹慎處理敏感資訊如私鑰和憑證等最佳實踐,包括使用環境變數或秘密管理服務(例如 AWS Secrets Manager、Azure Key Vault、Google Cloud Secret Manager)來儲存敏感資訊。可透過強大的版本控制策略,如 Semantic Versioning (SemVer) 與 Gitflow 工作流程,加強版本號碼的一致性與可追蹤性。這些措施都是為了提升整體開發效率及應用安全性而設計。


我們需要安裝 Fastlane。這可以透過 Ruby Gems 來完成。(如果您尚未安裝 Ruby,請先按照以下說明進行安裝。)此操作可以在終端機中的任何資料夾內執行。

gem install fastlane

然後前往你的 Flutter 專案,在 lib/android 資料夾內執行 fastlane init。(如果你想要自動化 iOS 的話,也可以進入 ios 資料夾並執行 fastlane init。)

fastlane init

Fastlane 會提示您輸入資訊並建立所有必要的檔案。對我們來說,最重要的檔案是 android/fastlane/Fastfile。這個檔案包含了「 lanes」,即您希望執行的各種自動化任務。Fastlane 為您建立一個基本的 Fastfile,其中包含三個 lanes:執行所有測試 - 使用 gradle 提交構建到 Crashlytics - 說實話,我對此不太了解 :) 部署版本到 Google Play(也許在後續故事中會提及)。雖然目前這些功能對我來說不是很有用,但它很好地展示了 Fastfile 的結構:

default_platform(:android)  platform :android do   desc "Runs all the tests"   lane :test do     gradle(task: "test")   end    desc "Submit a new Beta Build to Crashlytics Beta"   lane :beta do     gradle(task: "clean assembleRelease")     crashlytics        # sh "your_script.sh"     # You can also use other beta testing services here   end    desc "Deploy a new version to the Google Play"   lane :deploy do     gradle(task: "clean assembleRelease")     upload_to_play_store   end end

您所有的「區域」都位於 platform:android 部分。每個區域都有一個名稱和描述。要執行該區域,請輸入 fastlane <section> <lane name> 例如:

fastlane android beta

讓我們建立一個新的工作區,並刪除不需要的任務。

default_platform(:android)  platform :android do      desc "Automate build process including native symbols zip"   lane :build_aab_and_symbols do      # we will add code here       end      end 

重要提示:我使用 PowerShell 作為我的終端,因此某些系統命令是以 PowerShell 而非 bash 的形式呈現。在我們的團隊中,有一條規則,任何暫時性的變更,通常與測試有關,但不僅限於此,都應標記為 //FIXME。這包括為了除錯而改變常數、使用模擬後端進行測試等。在 VSCode 中有一個方便的擴充功能,可以顯示所有 TODO 和 FIXME 註解,但我... 並不總是記得在釋出之前檢查這些(哎呀!)。那麼,以下是如何使用 fastlane 來檢查它的方法:

# find all FIXME comments in dart files within the lib folder result = sh("powershell.exe -Command \"Select-String -Path (Get-ChildItem -Recurse ../../lib -Include *.dart) -Pattern 'FIXME'\"")  # If we got any result, show me the comments if result != ""   UI.message("⚠️  FIXME comments found in the following locations:")   UI.message(result) # Print the found FIXME comments    # Ask the user if they want to continue   if UI.confirm("There are FIXME comments. Do you want to continue building?")     UI.message("Proceeding with build despite FIXME comments...")   else     UI.user_error!("Aborted due to FIXME comments.")   end end

Flutter Dart 程式碼 FIXME 註解搜尋與版本號更新工具

這段程式碼首先會在 /lib 資料夾內搜尋所有的 *.dart 檔案,尋找 FIXME 註解,然後將這些註解顯示給我,並詢問我是否想要繼續。例如,在建立縮短版的測試版本時,我希望在釋出中保留 FIXME 註解。這變得更加複雜,因為我希望能將引數傳遞給 lane:major - 更新主要版本號,例如從 1.2.1 變更為 2.0.0;minor - 更新次要版本號,例如從 1.2.1 變更為 1.3.0;patch - 更新修補版本號,例如從 1.2.1 變更為 1.2.2;none - 我已經更新過了(例如在之前被中止的執行中),不想再次更新,因此保持在 1.2.1。我們需要告訴這個 lane 它接收一個名為 type 的引數。

desc "Automate build process including native symbols zip"   lane :build_aab_and_symbols do |options|     # get a parameter with name 'type'     version_type = options[:type]     # check that 'type' is one of the 4 options     UI.user_error!("Please provide a version type: major, minor, patch or none") unless ["major", "minor", "patch","none"].include?(version_type)     # rest of the code here       end

然後我們可以透過呼叫來命名它

fastlane android build_aab_and_symbols type:major

現在我們需要更新版本:

if version_type != "none"     # Read the current version from ../../pubspec.yaml     pubspec_file = File.join("..", "pubspec.yaml")     pubspec_file = File.join("..", pubspec_file)     pubspec_content = File.read(pubspec_file)      # Find the current version line (e.g., version: 1.2.1+24)     current_version_line = pubspec_content.match(/version:\s*(\d+\.\d+\.\d+)\+(\d+)/)     UI.user_error!("Couldn't find version in pubspec.yaml") unless current_version_line      current_version = current_version_line[1] # e.g., "1.2.1"     version_code = current_version_line[2].to_i # e.g., 24      # Split the version into major, minor, and patch     major, minor, patch = current_version.split('.').map(&:to_i)      # Increment the version based on the version type     case version_type     when "major"       major += 1       minor = 0       patch = 0     when "minor"       minor += 1       patch = 0     when "patch"       patch += 1     end      # Increment versionCode (build number)     new_version_code = version_code + 1      # Build the new version string     new_version = "#{major}.#{minor}.#{patch}+#{new_version_code}"      # Update the pubspec.yaml content with the new version     new_pubspec_content = pubspec_content.gsub(/version:\s*\d+\.\d+\.\d+\+\d+/, "version: #{new_version}")     File.write(pubspec_file, new_pubspec_content)      UI.success("Version updated to #{new_version}")   else     UI.message("No version update needed")   end

我們檢查所傳遞的型別是否為空。如果需要更改版本,我們將從 pubspec.yaml 中獲取當前版本。請注意,它位於 Fastfile 之上兩層資料夾中。根據型別引數更新版本號,並用新版本替換當前版本。然後,向使用者(我 😄 )傳送一條訊息。這比我想像的要複雜得多。最初,我使用的是 zip:

    sh "zip -r native-debug-symbols.zip ./build/app/intermediates/merged_native_libs/release/out/lib"

但是,雖然這建立了一個壓縮檔案,我卻無法將其上傳到 Google Play。我不斷收到類似的無盡錯誤訊息:

The native debug symbols contain an unexpected file: x86_64\libsentry.so. 

我花了一些時間才發現問題出在路徑分隔符上:zip 使用反斜線(\),而 Google Play 則期望使用正斜線(/)。因此,解決方案是使用 Ruby 建立一個 zip 檔案,並採用 Unix 風格的斜線。

require 'zip'  def create_native_symbols_zip(source_dir, output_file)   Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|     Dir[File.join(source_dir, '**', '**')].each do |file|       # Replace backslashes with forward slashes for Unix-style paths       zipfile.add(file.sub(source_dir + '/', ''), file)     end   end end

在我們的領域內:

native_symbols_dir = File.expand_path("../../build/app/intermediates/merged_native_libs/release/out/lib", __dir__) output_zip = File.expand_path("./native-debug-symbols.zip", __dir__)  create_native_symbols_zip(native_symbols_dir, output_zip)

這比我預期的要複雜得多。不論我做了什麼,它都開啟了 Documents 資料夾,而不是正確的資料夾。因此,我沒有直接使用檔案管理器,而是使用了 powershell.exe -Command:

# get absolute path of folder and open in explorer absolute_release_path = File.expand_path("../../build/app/outputs/bundle/release/", __dir__)  sh "powershell.exe -Command \"Start-Process explorer (Resolve-Path '#{absolute_release_path}')\""

其餘的部分相當簡單明瞭。

# Build the app bundle sh "flutter build appbundle --release"  # Move the native symbols zip to the release directory sh "mv #{output_zip} ../../build/app/outputs/bundle/release/"

綜合所有要素:

 require 'zip'  default_platform(:android)  platform :android do      desc "Automate build process including native symbols zip"   lane :build_aab_and_symbols do |options|     # update version     version_type = options[:type]     UI.user_error!("Please provide a version type: major, minor, patch or none") unless ["major", "minor", "patch","none"].include?(version_type)     update_version(version_type)       # Build the app bundle     sh "flutter build appbundle --release"      # Create a zip file for native symbols     native_symbols_dir = File.expand_path("../../build/app/intermediates/merged_native_libs/release/out/lib", __dir__)     output_zip = File.expand_path("./native-debug-symbols.zip", __dir__)      UI.message("Creating ZIP with Ruby to ensure correct path separators...")     create_native_symbols_zip(native_symbols_dir, output_zip)       # Move the native symbols zip to the release directory     sh "mv #{output_zip} ../../build/app/outputs/bundle/release/"      # get absolute path of folder and open in explorer     absolute_release_path = File.expand_path("../../build/app/outputs/bundle/release/", __dir__)     sh " Start-Process explorer (Resolve-Path '#{absolute_release_path}')\""      end end  def create_native_symbols_zip(source_dir, output_file)   Zip::File.open(output_file, Zip::File::CREATE) do |zipfile|     Dir[File.join(source_dir, '**', '**')].each do |file|       # Replace backslashes with forward slashes for Unix-style paths       zipfile.add(file.sub(source_dir + '/', ''), file)     end   end end  def update_version(version_type)   if version_type != "none"      # Read the current version from pubspec.yaml     pubspec_file = File.join("..", "pubspec.yaml")     pubspec_file = File.join("..", pubspec_file)     pubspec_content = File.read(pubspec_file)      # Find the current version line (e.g., version: 1.2.1+24)     current_version_line = pubspec_content.match(/version:\s*(\d+\.\d+\.\d+)\+(\d+)/)     UI.user_error!("Couldn't find version in pubspec.yaml") unless current_version_line      current_version = current_version_line[1] # e.g., "1.2.1"     version_code = current_version_line[2].to_i # e.g., 24      # Split the version into major, minor, and patch     major, minor, patch = current_version.split('.').map(&:to_i)      # Increment the version based on the version type     case version_type     when "major"       major += 1       minor = 0       patch = 0     when "minor"       minor += 1       patch = 0     when "patch"       patch += 1     end      # Increment versionCode (build number)     new_version_code = version_code + 1      # Build the new version string     new_version = "#{major}.#{minor}.#{patch}+#{new_version_code}"      # Update the pubspec.yaml content with the new version     new_pubspec_content = pubspec_content.gsub(/version:\s*\d+\.\d+\.\d+\+\d+/, "version: #{new_version}")     File.write(pubspec_file, new_pubspec_content)      UI.success("Version updated to #{new_version}")   else     UI.message("No version update needed")   end end

而現在每當我想要建立一個版本時,我只需

fastlane android build_aab_and_symbols type:minor

去泡一杯茶吧 :)


Fastlane 的錯誤訊息出奇地清晰,因此故障排除變得相當簡單。如果找不到檔案,則表示您的路徑設定有誤。請注意,pubspec.yaml 檔案位於 Fastfile 上方的兩個資料夾中。我的 VSCode 終端使用 PowerShell,因此上述命令是適用於 PowerShell 的。如果您使用的是 bash,則請使用 bash 命令。例如,壓縮檔案的替代建議:

# using bash sh "zip -r native-debug-symbols.zip ./build/app/intermediates/stripped_native_libs/release/out" # using windows zip sh "powershell.exe -Command \"Compress-Archive -Path ./build/app/intermediates/stripped_native_libs/release/out/* -DestinationPath ./native-debug-symbols.zip\""

開啟資料夾的替代建議(老實說,我也不知道為什麼這個方法沒有奏效):

  sh "explorer.exe ./build/app/outputs/bundle/release/"

Fastlane 也支援將我們建立的 appbundle 和 zip 檔案上傳至 Google Play,但這又是另一個故事;) 祝你自動化愉快!我的心在碎裂。已經超過一年了,367 天。 #立即帶他們回家。你可以檢視我的免費開源線上遊戲 Space Short,以及我網站上的更多有趣內容。


GPG

專家

相關討論

❖ 相關專欄