require "shellwords" default_platform(:ios) APP_IDENTIFIER = "net.buzzert.XaibatsuControlPanel" TEAM_ID = "DQQH5H6GBD" ROOT_DIR = File.expand_path("..", __dir__) PROJECT = File.join(ROOT_DIR, "XIONControlPanel.xcodeproj") SCHEME = "XIONControlPanel" INFO_PLIST = "XIONControlPanel/SupportingFiles/Info.plist" ARCHIVE_PATH = File.join(ROOT_DIR, "build/XaibatsuControlPanel.xcarchive") IPA_PATH = File.join(ROOT_DIR, "build/XaibatsuControlPanel.ipa") PROFILE_NAME = "match AppStore #{APP_IDENTIFIER}" CI_KEYCHAIN_NAME = "xaibatsu_control_panel_ci_keychain" CI_KEYCHAIN_PASSWORD = "xaibatsu-control-panel-ci-keychain-password" CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db") def present?(value) !value.to_s.strip.empty? end def ci? present?(ENV["CI"]) end def read_plist_value(path, key) Fastlane::Actions.sh( "/usr/bin/plutil -extract #{key.shellescape} raw #{path.shellescape}", log: false ).strip end def app_store_connect_key app_store_connect_api_key( key_id: ENV.fetch("APP_STORE_CONNECT_KEY_ID"), issuer_id: ENV.fetch("APP_STORE_CONNECT_ISSUER_ID"), key_content: ENV.fetch("APP_STORE_CONNECT_KEY_CONTENT"), is_key_content_base64: true, duration: 1_200, in_house: false ) end def release_metadata tag = ENV["RELEASE_TAG"] || ENV["GITHUB_REF_NAME"] match = tag&.match(%r{\Arelease/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z}) UI.user_error!("Expected a release tag in the form release/vX.X.X; got #{tag.inspect}") unless match build_number = ENV["GITEA_BUILD_NUMBER"] || ENV["GITHUB_RUN_NUMBER"] unless build_number&.match?(/\A[1-9]\d*\z/) UI.user_error!("The Gitea run number must be a positive integer; got #{build_number.inspect}") end { version: "#{match[1]}.#{match[2]}.#{match[3]}", build_number: build_number } end platform :ios do # Keep this app's unlocked signing identity first in the shared runner's # keychain search list. Other projects use the same distribution identity, # and their locked keychains can otherwise shadow this one. private_lane :prepare_ci_keychain do next unless ci? delete_keychain(name: CI_KEYCHAIN_NAME) if File.file?(CI_KEYCHAIN_DB_PATH) create_keychain( name: CI_KEYCHAIN_NAME, password: CI_KEYCHAIN_PASSWORD, unlock: true, timeout: 3_600, add_to_search_list: false ) other_keychains = sh("security list-keychains -d user", log: false) .scan(/"([^"]+)"/) .flatten .reject { |path| path.include?(CI_KEYCHAIN_NAME) } sh("security list-keychains -d user -s #{([CI_KEYCHAIN_DB_PATH] + other_keychains).shelljoin}") ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD end desc "Create or renew the App Store provisioning profile in the Match repository" lane :prepare_signing do match( type: "appstore", app_identifier: APP_IDENTIFIER, team_id: TEAM_ID, api_key: app_store_connect_key, readonly: false ) end desc "Build a release tag and upload it to TestFlight" lane :release do |options| metadata = release_metadata api_key = app_store_connect_key prepare_ci_keychain match( type: "appstore", app_identifier: APP_IDENTIFIER, team_id: TEAM_ID, api_key: api_key, readonly: true ) update_info_plist( plist_path: INFO_PLIST, block: proc do |plist| plist["CFBundleShortVersionString"] = metadata[:version] plist["CFBundleVersion"] = metadata[:build_number] end ) stamped_version = get_info_plist_value(path: INFO_PLIST, key: "CFBundleShortVersionString").to_s stamped_build = get_info_plist_value(path: INFO_PLIST, key: "CFBundleVersion").to_s UI.user_error!("Failed to stamp the release version") unless stamped_version == metadata[:version] UI.user_error!("Failed to stamp the Gitea build number") unless stamped_build == metadata[:build_number] build_app( project: PROJECT, scheme: SCHEME, configuration: "Release", clean: true, archive_path: ARCHIVE_PATH, output_directory: File.dirname(IPA_PATH), output_name: File.basename(IPA_PATH), export_method: "app-store", export_options: { signingStyle: "manual", provisioningProfiles: { APP_IDENTIFIER => PROFILE_NAME }, manageAppVersionAndBuildNumber: false } ) archived_plist = File.join( ARCHIVE_PATH, "Products", "Applications", "XION.app", "Info.plist" ) archived_version = read_plist_value(archived_plist, "CFBundleShortVersionString") archived_build = read_plist_value(archived_plist, "CFBundleVersion") UI.user_error!("Archived version does not match the release tag") unless archived_version == metadata[:version] UI.user_error!("Archived build does not match Gitea") unless archived_build == metadata[:build_number] if options[:skip_upload] UI.important("Skipping the TestFlight upload for local validation") else upload_to_testflight( api_key: api_key, app_identifier: APP_IDENTIFIER, ipa: IPA_PATH, skip_waiting_for_build_processing: true, uses_non_exempt_encryption: false ) end ensure delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH) end end