2026-06-05 23:19:14 -07:00
require "dotenv"
2026-06-25 20:51:01 -07:00
require "base64"
require "fileutils"
2026-06-25 21:03:43 -07:00
require "json"
require "net/http"
2026-06-05 23:19:14 -07:00
require "open3"
2026-06-25 21:03:43 -07:00
require "openssl"
2026-06-25 20:51:01 -07:00
require "securerandom"
2026-06-05 23:19:14 -07:00
require "shellwords"
2026-06-25 21:03:43 -07:00
require "uri"
2026-06-05 23:19:14 -07:00
require "yaml"
Dotenv . load ( File . expand_path ( "../.env" , __dir__ ))
default_platform ( :ios )
APP_IDENTIFIER = ENV . fetch ( "FASTLANE_APP_IDENTIFIER" , "net.buzzert.sybil2" )
TEAM_ID = ENV . fetch ( "FASTLANE_TEAM_ID" , "DQQH5H6GBD" )
APP_STORE_APPLE_ID = ENV . fetch ( "SYBIL_APP_STORE_APPLE_ID" , "6759442828" )
PROVIDER_PUBLIC_ID = ENV . fetch ( "SYBIL_PROVIDER_PUBLIC_ID" , "c043d167-ad88-4036-84ea-76c223f1b1b2" )
2026-06-25 20:51:01 -07:00
PROFILE_SPECIFIER = ENV [ "SYBIL_PROVISIONING_PROFILE_SPECIFIER" ]. to_s . strip . empty? ? "Sybil AppStore CI" : ENV [ "SYBIL_PROVISIONING_PROFILE_SPECIFIER" ]
2026-06-25 21:05:26 -07:00
SIGNING_CERTIFICATE_NAME = ENV [ "SYBIL_CODE_SIGN_IDENTITY" ]. to_s . strip . empty? ? "Apple Distribution: James Magahern (DQQH5H6GBD)" : ENV [ "SYBIL_CODE_SIGN_IDENTITY" ]
2026-06-25 21:25:13 -07:00
XCODE_CODE_SIGN_IDENTITY = ENV [ "SYBIL_XCODE_CODE_SIGN_IDENTITY" ]. to_s . strip . empty? ? "Apple Distribution" : ENV [ "SYBIL_XCODE_CODE_SIGN_IDENTITY" ]
2026-06-05 23:19:14 -07:00
IOS_ROOT = File . expand_path ( ".." , __dir__ )
PROJECT_FILE = File . join ( IOS_ROOT , "Sybil.xcodeproj" )
PROJECT_SPEC = File . join ( IOS_ROOT , "project.yml" )
APP_SPEC = File . join ( IOS_ROOT , "Apps/Sybil/project.yml" )
2026-06-25 20:51:01 -07:00
SIGNING_OUTPUT_DIR = File . join ( IOS_ROOT , "build/signing" )
2026-06-05 23:19:14 -07:00
SCHEME = "Sybil"
TARGET = "SybilApp"
def present? ( value )
! value . to_s . strip . empty?
end
def capture ( command )
stdout , stderr , status = Open3 . capture3 ( command )
return stdout . strip if status . success?
UI . user_error! ( "Command failed: #{ command } \n #{ stderr . strip } " )
end
2026-06-25 20:51:01 -07:00
def run_silent ( * command , error_message :)
_stdout , stderr , status = Open3 . capture3 ( * command )
return if status . success?
UI . user_error! ( " #{ error_message } \n #{ stderr . strip } " )
end
def user_keychains
capture ( "security list-keychains -d user" ) . lines . map { | line | line . strip . delete ( '"' ) } . reject ( & :empty? )
end
2026-06-05 23:19:14 -07:00
def app_project_settings
YAML . safe_load ( File . read ( APP_SPEC )) . fetch ( "targets" ) . fetch ( TARGET ) . fetch ( "settings" ) . fetch ( "base" )
end
2026-06-25 21:31:51 -07:00
def apply_release_signing_settings
require "xcodeproj"
project = Xcodeproj :: Project . open ( PROJECT_FILE )
target = project . targets . find { | candidate | candidate . name == TARGET }
UI . user_error! ( "Could not find target #{ TARGET } in #{ PROJECT_FILE } " ) unless target
target . build_configurations . each do | configuration |
next unless configuration . name == "Release"
settings = configuration . build_settings
settings [ "CODE_SIGN_STYLE" ] = "Manual"
settings [ "DEVELOPMENT_TEAM" ] = TEAM_ID
settings [ "PROVISIONING_PROFILE_SPECIFIER" ] = PROFILE_SPECIFIER
settings [ "CODE_SIGN_IDENTITY" ] = XCODE_CODE_SIGN_IDENTITY
settings [ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" ] = XCODE_CODE_SIGN_IDENTITY
2026-06-25 21:44:42 -07:00
if present? ( ENV [ "SYBIL_PROVISIONING_PROFILE_UUID" ] )
settings [ "PROVISIONING_PROFILE" ] = ENV [ "SYBIL_PROVISIONING_PROFILE_UUID" ]
settings [ "PROVISIONING_PROFILE[sdk=iphoneos*]" ] = ENV [ "SYBIL_PROVISIONING_PROFILE_UUID" ]
end
2026-06-25 21:31:51 -07:00
end
project . save
end
2026-06-05 23:19:14 -07:00
def local_marketing_version
app_project_settings . fetch ( "MARKETING_VERSION" ) . to_s
end
def local_build_number
app_project_settings . fetch ( "CURRENT_PROJECT_VERSION" ) . to_i
end
def normalize_version_tag ( tag )
2026-06-25 19:30:58 -07:00
version = tag . to_s . strip . sub ( %r{\Arelease/} , "" ) . sub ( /\Av/ , "" )
unless version . match? ( /\A\d+\.\d+\.\d+\z/ )
UI . user_error! ( "Release tag #{ tag . inspect } must look like release/v1.10.0" )
2026-06-05 23:19:14 -07:00
end
version
end
def release_version
tag = ENV [ "SYBIL_VERSION_TAG" ]
tag = capture ( "git describe --tags --abbrev=0" ) unless present? ( tag )
normalize_version_tag ( tag )
end
def xcode_build_setting ( key , value )
2026-06-25 21:25:13 -07:00
" #{ key . to_s . shellescape } = #{ value . to_s . shellescape } "
2026-06-05 23:19:14 -07:00
end
2026-06-25 21:03:43 -07:00
def env_line ( key , value )
" #{ key } = #{ value . to_s . shellescape } "
end
def base64url ( value )
Base64 . urlsafe_encode64 ( value ) . delete ( "=" )
end
def integer_to_fixed_bytes ( integer , length )
hex = integer . to_s ( 16 )
hex = "0 #{ hex } " if hex . length . odd?
[ hex ]. pack ( "H*" ) . rjust ( length , " \0 " ) [- length , length ]
end
def app_store_connect_private_key
key_path = ENV [ "APP_STORE_CONNECT_API_KEY_PATH" ]
key_content = ENV [ "APP_STORE_CONNECT_API_KEY_CONTENT" ]
pem = if present? ( key_path )
File . read ( key_path )
elsif present? ( key_content )
ENV [ "APP_STORE_CONNECT_API_KEY_CONTENT_BASE64" ]. to_s == "true" ? Base64 . decode64 ( key_content ) : key_content
end
UI . user_error! ( "App Store Connect API key content is required" ) unless present? ( pem )
OpenSSL :: PKey :: EC . new ( pem )
end
def app_store_connect_jwt
key_id = ENV [ "APP_STORE_CONNECT_API_KEY_ID" ]
issuer_id = ENV [ "APP_STORE_CONNECT_API_ISSUER_ID" ]
issuer_id = ENV [ "APP_STORE_CONNECT_API_KEY_ISSUER_ID" ] unless present? ( issuer_id )
UI . user_error! ( "App Store Connect API key id and issuer id are required" ) unless present? ( key_id ) && present? ( issuer_id )
header = { alg : "ES256" , kid : key_id , typ : "JWT" }
payload = { iss : issuer_id , iat : Time . now . to_i , exp : Time . now . to_i + 600 , aud : "appstoreconnect-v1" }
unsigned = [ base64url ( header . to_json ), base64url ( payload . to_json ) ]. join ( "." )
asn1_signature = app_store_connect_private_key . dsa_sign_asn1 ( OpenSSL :: Digest :: SHA256 . digest ( unsigned ))
signature_sequence = OpenSSL :: ASN1 . decode ( asn1_signature )
raw_signature = signature_sequence . value . map { | part | integer_to_fixed_bytes ( part . value , 32 ) } . join
[ unsigned , base64url ( raw_signature ) ]. join ( "." )
end
def app_store_connect_request ( method , path , payload = nil )
uri = URI ( "https://api.appstoreconnect.apple.com #{ path } " )
request_class = Net :: HTTP . const_get ( method . to_s . capitalize )
request = request_class . new ( uri )
request [ "Authorization" ] = "Bearer #{ app_store_connect_jwt } "
if payload
request [ "Content-Type" ] = "application/json"
request . body = payload . to_json
end
response = Net :: HTTP . start ( uri . hostname , uri . port , use_ssl : true ) { | http | http . request ( request ) }
return {} if response . is_a? ( Net :: HTTPSuccess ) && response . body . to_s . empty?
return JSON . parse ( response . body ) if response . is_a? ( Net :: HTTPSuccess )
UI . user_error! ( "App Store Connect API request failed: #{ method . to_s . upcase } #{ path } \n #{ response . body } " )
end
def bundle_id_resource_id
response = app_store_connect_request (
:get ,
"/v1/bundleIds?filter[identifier]= #{ URI . encode_www_form_component ( APP_IDENTIFIER ) } &limit=1"
)
id = response . fetch ( "data" , [] ) . first &. fetch ( "id" , nil )
UI . user_error! ( "Could not find App Store Connect bundle id resource for #{ APP_IDENTIFIER } " ) unless present? ( id )
id
end
def recreate_app_store_profile ( certificate_id )
existing = app_store_connect_request (
:get ,
"/v1/profiles?filter[name]= #{ URI . encode_www_form_component ( PROFILE_SPECIFIER ) } &limit=200"
)
existing . fetch ( "data" , [] ) . each do | profile |
app_store_connect_request ( :delete , "/v1/profiles/ #{ profile . fetch ( "id" ) } " )
end
payload = {
data : {
type : "profiles" ,
attributes : {
name : PROFILE_SPECIFIER ,
profileType : "IOS_APP_STORE"
},
relationships : {
bundleId : {
data : { type : "bundleIds" , id : bundle_id_resource_id }
},
certificates : {
data : [ { type : "certificates" , id : certificate_id } ]
}
}
}
}
response = app_store_connect_request ( :post , "/v1/profiles" , payload )
profile_content = response . dig ( "data" , "attributes" , "profileContent" )
UI . user_error! ( "App Store Connect profile response did not include profileContent" ) unless present? ( profile_content )
profile_path = File . join ( SIGNING_OUTPUT_DIR , "Sybil_AppStore_CI.mobileprovision" )
File . binwrite ( profile_path , Base64 . decode64 ( profile_content ))
install_dir = File . expand_path ( "~/Library/MobileDevice/Provisioning Profiles" )
FileUtils . mkdir_p ( install_dir )
FileUtils . cp ( profile_path , File . join ( install_dir , "Sybil_AppStore_CI.mobileprovision" ))
profile_path
end
2026-06-25 21:18:54 -07:00
def signing_identity_p12 ( p12_path , p12_password )
work_dir = File . join ( SIGNING_OUTPUT_DIR , "single-identity" )
FileUtils . rm_rf ( work_dir )
FileUtils . mkdir_p ( work_dir )
all_pem = File . join ( work_dir , "all.pem" )
stdout , stderr , status = Open3 . capture3 (
"openssl" , "pkcs12" ,
"-in" , p12_path ,
"-nodes" ,
"-passin" , "pass: #{ p12_password } " ,
"-out" , all_pem
)
UI . user_error! ( "Could not inspect exported p12 \n #{ stderr } \n #{ stdout } " ) unless status . success?
records = File . read ( all_pem ) . split ( /(?=Bag Attributes\n)/ )
cert_record = records . find do | record |
record . include? ( "friendlyName: #{ SIGNING_CERTIFICATE_NAME } " ) && record . include? ( "-----BEGIN CERTIFICATE-----" )
end
UI . user_error! ( "Could not find #{ SIGNING_CERTIFICATE_NAME } certificate in exported p12" ) unless cert_record
local_key_id = cert_record [ /localKeyID: (.+)/ , 1 ]. to_s . strip
UI . user_error! ( "Could not resolve localKeyID for #{ SIGNING_CERTIFICATE_NAME } " ) unless present? ( local_key_id )
key_record = records . find do | record |
record . include? ( "localKeyID: #{ local_key_id } " ) && record . include? ( "-----BEGIN PRIVATE KEY-----" )
end
UI . user_error! ( "Could not find private key for #{ SIGNING_CERTIFICATE_NAME } " ) unless key_record
cert_path = File . join ( work_dir , "identity.cer.pem" )
key_path = File . join ( work_dir , "identity.key.pem" )
File . write ( cert_path , cert_record [ /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m ] )
File . write ( key_path , key_record [ /-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----/m ] )
root_cer = File . join ( work_dir , "AppleIncRootCertificate.cer" )
wwdr_cer = File . join ( work_dir , "AppleWWDRCAG3.cer" )
root_pem = File . join ( work_dir , "AppleIncRootCertificate.pem" )
wwdr_pem = File . join ( work_dir , "AppleWWDRCAG3.pem" )
chain_pem = File . join ( work_dir , "chain.pem" )
run_silent ( "curl" , "-fsSL" , "https://www.apple.com/appleca/AppleIncRootCertificate.cer" , "-o" , root_cer , error_message : "Could not download Apple root certificate" )
run_silent ( "curl" , "-fsSL" , "https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer" , "-o" , wwdr_cer , error_message : "Could not download Apple WWDR G3 certificate" )
run_silent ( "openssl" , "x509" , "-inform" , "DER" , "-in" , root_cer , "-out" , root_pem , error_message : "Could not convert Apple root certificate" )
run_silent ( "openssl" , "x509" , "-inform" , "DER" , "-in" , wwdr_cer , "-out" , wwdr_pem , error_message : "Could not convert Apple WWDR G3 certificate" )
File . write ( chain_pem , File . read ( wwdr_pem ) + File . read ( root_pem ))
single_p12_path = File . join ( work_dir , "appstore-signing.p12" )
run_silent (
"openssl" , "pkcs12" , "-export" ,
"-inkey" , key_path ,
"-in" , cert_path ,
"-certfile" , chain_pem ,
"-name" , SIGNING_CERTIFICATE_NAME ,
"-out" , single_p12_path ,
"-passout" , "pass: #{ p12_password } " ,
error_message : "Could not create single-identity p12"
)
FileUtils . cp ( single_p12_path , p12_path )
ensure
FileUtils . rm_rf ( work_dir ) if present? ( work_dir )
end
2026-06-05 23:19:14 -07:00
def app_store_connect_key_options
key_id = ENV [ "APP_STORE_CONNECT_API_KEY_ID" ]
issuer_id = ENV [ "APP_STORE_CONNECT_API_ISSUER_ID" ]
2026-06-25 20:51:01 -07:00
issuer_id = ENV [ "APP_STORE_CONNECT_API_KEY_ISSUER_ID" ] unless present? ( issuer_id )
2026-06-05 23:19:14 -07:00
return nil unless present? ( key_id ) && present? ( issuer_id )
key_path = ENV [ "APP_STORE_CONNECT_API_KEY_PATH" ]
key_content = ENV [ "APP_STORE_CONNECT_API_KEY_CONTENT" ]
if present? ( key_path )
{
key_id : key_id ,
issuer_id : issuer_id ,
key_filepath : key_path
}
elsif present? ( key_content )
{
key_id : key_id ,
issuer_id : issuer_id ,
key_content : key_content ,
is_key_content_base64 : ENV [ "APP_STORE_CONNECT_API_KEY_CONTENT_BASE64" ]. to_s == "true"
}
end
end
platform :ios do
2026-06-25 20:51:01 -07:00
private_lane :load_app_store_connect_api_key do
options = app_store_connect_key_options
UI . user_error! ( "App Store Connect API key is required" ) unless options
app_store_connect_api_key ( options )
end
2026-06-05 23:19:14 -07:00
desc "Show the version Fastlane will stamp into the next TestFlight archive"
lane :version do
UI . message ( "Git tag version: #{ release_version } " )
UI . message ( "Checked-in app version: #{ local_marketing_version } " )
UI . message ( "Checked-in build number: #{ local_build_number } " )
end
2026-06-25 20:51:01 -07:00
desc "Create CI signing certificate/profile and write ignored secret material under build/signing"
lane :create_ci_signing do
api_key = load_app_store_connect_api_key
FileUtils . rm_rf ( SIGNING_OUTPUT_DIR )
FileUtils . mkdir_p ( SIGNING_OUTPUT_DIR )
2026-06-25 21:03:43 -07:00
cert_id = ENV [ "SYBIL_SIGNING_CERTIFICATE_ID" ]. to_s
keychain_path = nil
keychain_password = nil
p12_path = File . join ( SIGNING_OUTPUT_DIR , "appstore-signing.p12" )
2026-06-25 20:51:01 -07:00
p12_password = ENV [ "SYBIL_CI_P12_PASSWORD" ]. to_s
if p12_password . empty?
p12_password = SecureRandom . base64 ( 24 )
UI . important ( "Generated a p12 password for CI secrets." )
end
begin
2026-06-25 21:03:43 -07:00
if present? ( cert_id )
UI . message ( "Using existing signing certificate id #{ cert_id } " )
export_keychain = ENV [ "SYBIL_SIGNING_KEYCHAIN" ]. to_s
export_keychain = File . expand_path ( "~/Library/Keychains/login.keychain-db" ) unless present? ( export_keychain )
run_silent (
"security" , "export" , "-k" , export_keychain , "-t" , "identities" , "-f" , "pkcs12" , "-P" , p12_password , "-o" , p12_path ,
error_message : "Could not export the local CI signing identity"
)
else
keychain_path = File . join ( SIGNING_OUTPUT_DIR , "sybil_ci_signing.keychain-db" )
keychain_password = SecureRandom . base64 ( 24 )
run_silent (
"security" , "create-keychain" , "-p" , keychain_password , keychain_path ,
error_message : "Could not create temporary signing keychain"
)
run_silent (
"security" , "set-keychain-settings" , "-lut" , "21600" , keychain_path ,
error_message : "Could not configure temporary signing keychain"
)
run_silent (
"security" , "unlock-keychain" , "-p" , keychain_password , keychain_path ,
error_message : "Could not unlock temporary signing keychain"
)
run_silent (
"security" , "list-keychains" , "-d" , "user" , "-s" , keychain_path , * user_keychains ,
error_message : "Could not add temporary signing keychain to the user search list"
)
2026-06-25 20:51:01 -07:00
2026-06-25 21:03:43 -07:00
cert (
api_key : api_key ,
development : false ,
force : true ,
generate_apple_certs : true ,
keychain_password : keychain_password ,
keychain_path : keychain_path ,
output_path : SIGNING_OUTPUT_DIR ,
platform : "ios"
)
2026-06-25 20:51:01 -07:00
2026-06-25 21:03:43 -07:00
cert_id = lane_context [ SharedValues :: CERT_CERTIFICATE_ID ]
UI . user_error! ( "Could not resolve generated certificate id" ) unless present? ( cert_id )
run_silent (
"security" , "export" , "-k" , keychain_path , "-t" , "identities" , "-f" , "pkcs12" , "-P" , p12_password , "-o" , p12_path ,
error_message : "Could not export the generated CI signing identity"
)
end
2026-06-25 20:51:01 -07:00
UI . user_error! ( "Could not find exported p12 at #{ p12_path } " ) unless File . exist? ( p12_path )
2026-06-25 21:18:54 -07:00
signing_identity_p12 ( p12_path , p12_password )
2026-06-25 20:51:01 -07:00
2026-06-25 21:03:43 -07:00
profile_path = recreate_app_store_profile ( cert_id )
UI . user_error! ( "Could not resolve generated provisioning profile path" ) unless present? ( profile_path ) && File . exist? ( profile_path )
2026-06-25 20:51:01 -07:00
secrets_path = File . join ( SIGNING_OUTPUT_DIR , "ci-secrets.env" )
File . write (
secrets_path ,
[
2026-06-25 21:03:43 -07:00
env_line ( "APPSTORE_CERTIFICATES_FILE_BASE64" , Base64 . strict_encode64 ( File . binread ( p12_path ))),
env_line ( "APPSTORE_CERTIFICATES_PASSWORD" , p12_password ),
env_line ( "APPSTORE_PROVISIONING_PROFILE_BASE64" , Base64 . strict_encode64 ( File . binread ( profile_path ))),
env_line ( "SYBIL_PROVISIONING_PROFILE_SPECIFIER" , PROFILE_SPECIFIER )
2026-06-25 20:51:01 -07:00
]. join ( " \n " ) + " \n "
)
ensure
2026-06-25 21:03:43 -07:00
system ( "security" , "delete-keychain" , keychain_path , out : File :: NULL , err : File :: NULL ) if present? ( keychain_path ) && File . exist? ( keychain_path )
2026-06-25 20:51:01 -07:00
end
UI . success ( "Created CI signing files in #{ SIGNING_OUTPUT_DIR } " )
UI . important ( "Add the values from #{ secrets_path } as repository secrets." )
end
2026-06-05 23:19:14 -07:00
desc "Build Sybil and upload it to TestFlight"
lane :beta do
version = release_version
build_number = ENV [ "SYBIL_BUILD_NUMBER" ]. to_s
2026-06-25 20:51:01 -07:00
api_key = load_app_store_connect_api_key
2026-06-05 23:19:14 -07:00
unless present? ( build_number )
build_number = ( local_build_number + 1 ) . to_s
2026-06-25 20:51:01 -07:00
begin
latest = latest_testflight_build_number (
app_identifier : APP_IDENTIFIER ,
version : version ,
api_key : api_key ,
initial_build_number : local_build_number
) . to_i
build_number = [ latest + 1 , local_build_number + 1 ]. max . to_s
rescue StandardError => e
UI . important ( "Could not look up TestFlight build number: #{ e . message } " )
UI . important ( "Using checked-in build number + 1: #{ build_number } " )
2026-06-05 23:19:14 -07:00
end
end
UI . user_error! ( "Build number must be a positive integer" ) unless build_number . match? ( /\A[1-9]\d*\z/ )
sh ( "xcodegen --spec #{ PROJECT_SPEC . shellescape } " )
2026-06-25 21:31:51 -07:00
apply_release_signing_settings
2026-06-05 23:19:14 -07:00
xcode_args = [
xcode_build_setting ( "MARKETING_VERSION" , version ),
2026-06-25 21:44:42 -07:00
xcode_build_setting ( "CURRENT_PROJECT_VERSION" , build_number )
2026-06-25 21:07:38 -07:00
]
if present? ( ENV [ "SYBIL_SIGNING_KEYCHAIN_PATH" ] )
xcode_args << xcode_build_setting ( "OTHER_CODE_SIGN_FLAGS" , "--keychain #{ ENV . fetch ( "SYBIL_SIGNING_KEYCHAIN_PATH" ) } " )
end
xcode_args = xcode_args . join ( " " )
2026-06-05 23:19:14 -07:00
ipa_path = build_app (
project : PROJECT_FILE ,
scheme : SCHEME ,
clean : true ,
sdk : "iphoneos" ,
export_method : "app-store" ,
output_directory : File . join ( IOS_ROOT , "build/fastlane" ),
output_name : "Sybil- #{ version } - #{ build_number } .ipa" ,
xcargs : xcode_args ,
export_options : {
2026-06-25 20:51:01 -07:00
method : "app-store" ,
2026-06-05 23:19:14 -07:00
destination : "export" ,
2026-06-25 20:51:01 -07:00
signingStyle : "manual" ,
provisioningProfiles : {
APP_IDENTIFIER => PROFILE_SPECIFIER
},
2026-06-25 21:25:13 -07:00
signingCertificate : XCODE_CODE_SIGN_IDENTITY ,
2026-06-05 23:19:14 -07:00
teamID : TEAM_ID ,
manageAppVersionAndBuildNumber : false ,
uploadSymbols : true ,
stripSwiftSymbols : true
}
)
ipa_path ||= lane_context [ SharedValues :: IPA_OUTPUT_PATH ]
UI . user_error! ( "IPA export failed; no IPA path was returned" ) unless present? ( ipa_path ) && File . exist? ( ipa_path )
2026-06-25 20:51:01 -07:00
upload_to_testflight (
api_key : api_key ,
app_identifier : APP_IDENTIFIER ,
ipa : ipa_path ,
skip_waiting_for_build_processing : true
)
2026-06-05 23:19:14 -07:00
end
end