Reset iOS TestFlight deployment
Some checks failed
TestFlight / testflight (push) Failing after 14s

This commit is contained in:
2026-06-25 22:41:00 -07:00
parent 272ad0bbf0
commit a68f1e50ca
9 changed files with 118 additions and 870 deletions

View File

@@ -1,9 +0,0 @@
require "dotenv"
Dotenv.load(File.expand_path("../.env", __dir__))
app_identifier(ENV.fetch("FASTLANE_APP_IDENTIFIER", "net.buzzert.sybil2"))
team_id(ENV.fetch("FASTLANE_TEAM_ID", "DQQH5H6GBD"))
apple_id(ENV["FASTLANE_USER"]) if ENV["FASTLANE_USER"].to_s.strip.length.positive?
itc_team_id(ENV["FASTLANE_ITC_TEAM_ID"]) if ENV["FASTLANE_ITC_TEAM_ID"].to_s.strip.length.positive?

View File

@@ -1,76 +0,0 @@
# TestFlight Release CI
Gitea Actions publishes iOS releases from tags that match:
```sh
release/vN.N.N
```
For example:
```sh
git tag release/v1.10.0
git push origin release/v1.10.0
```
The release job runs on the `xcode` runner label, creates the runner user's
login keychain from Gitea secrets, makes that keychain the user default for the
duration of the job, installs the App Store provisioning profile in both the
legacy MobileDevice directory and the Xcode UserData directory used by newer
Xcode releases, builds and uploads the app with fastlane, then creates or
updates the matching Gitea release with the generated IPA as an asset. The job
restores the previous user default keychain and deletes the user login keychain
and installed profiles in an `always()` cleanup step. No signing material is
installed into the system keychain.
Required repository secrets:
```text
APP_STORE_CONNECT_API_KEY_ID
APP_STORE_CONNECT_API_ISSUER_ID
APP_STORE_CONNECT_API_KEY_CONTENT
APPSTORE_CERTIFICATES_FILE_BASE64
APPSTORE_CERTIFICATES_PASSWORD
APPSTORE_PROVISIONING_PROFILE_BASE64
```
Generate or refresh the signing assets locally with:
```sh
cd ios
fastlane ios create_ci_signing
```
The generated `build/signing/ci-secrets.env` file is ignored by Git. Copy its
certificate and provisioning profile values into the repository secrets listed
above. The workflow uses the `Sybil AppStore CI` provisioning profile name by
default.
Fastlane keeps two signing names separate. `SYBIL_CODE_SIGN_IDENTITY` is the
exact certificate common name used when exporting a local p12 for secrets, while
`SYBIL_XCODE_CODE_SIGN_IDENTITY` defaults to the certificate SHA-1 fingerprint
that Xcode uses during archive. `SYBIL_EXPORT_SIGNING_CERTIFICATE` defaults to
the generic `Apple Distribution` selector used in the export options.
The Release signing settings are also present in `Apps/Sybil/project.yml` so
XcodeGen emits a manually signed App Store archive configuration. CI passes the
installed provisioning profile UUID to Fastlane as
`SYBIL_PROVISIONING_PROFILE_UUID`; Fastlane writes that UUID into the generated
project before archiving. CI also passes the temporary keychain path as
`CODE_SIGN_KEYCHAIN` so Xcode searches the disposable keychain for the imported
Distribution identity.
If the Apple team has reached the Distribution certificate limit, set
`SYBIL_SIGNING_CERTIFICATE_ID` to the portal id for a certificate whose private
key exists in the local login keychain before running `create_ci_signing`. The
lane will export the local identity and create the provisioning profile against
that existing certificate instead of creating another Distribution certificate.
If `create_ci_signing` fails with an expired or missing agreement error, the
Apple Developer Program account holder must accept the current agreements in
App Store Connect before new certificates or provisioning profiles can be
created through the API.
The workflow uses Gitea's built-in `GITEA_TOKEN` for release creation and asset
upload, with `contents: write` permissions. In Gitea this covers release asset
publication.

View File

@@ -1,485 +1,107 @@
require "dotenv"
require "base64"
require "fileutils"
require "json"
require "net/http"
require "open3"
require "openssl"
require "securerandom"
require "shellwords"
require "uri"
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")
PROFILE_SPECIFIER = ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"].to_s.strip.empty? ? "Sybil AppStore CI" : ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"]
SIGNING_CERTIFICATE_NAME = ENV["SYBIL_CODE_SIGN_IDENTITY"].to_s.strip.empty? ? "Apple Distribution: James Magahern (DQQH5H6GBD)" : ENV["SYBIL_CODE_SIGN_IDENTITY"]
XCODE_CODE_SIGN_IDENTITY = ENV["SYBIL_XCODE_CODE_SIGN_IDENTITY"].to_s.strip.empty? ? "6B74B268C4761720FB2051D01D8BB3E47B55D9F5" : ENV["SYBIL_XCODE_CODE_SIGN_IDENTITY"]
EXPORT_SIGNING_CERTIFICATE = ENV["SYBIL_EXPORT_SIGNING_CERTIFICATE"].to_s.strip.empty? ? "Apple Distribution" : ENV["SYBIL_EXPORT_SIGNING_CERTIFICATE"]
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")
SIGNING_OUTPUT_DIR = File.join(IOS_ROOT, "build/signing")
APP_IDENTIFIER = "net.buzzert.sybil2"
SCHEME = "Sybil"
TARGET = "SybilApp"
TEAM_ID = "DQQH5H6GBD"
PROFILE_NAME = "Sybil AppStore CI"
MATCH_BRANCH = "master"
def present?(value)
!value.to_s.strip.empty?
end
def capture(command)
stdout, stderr, status = Open3.capture3(command)
return stdout.strip if status.success?
def release_version
tag = ENV["SYBIL_VERSION_TAG"].to_s
tag = ENV["GITHUB_REF_NAME"].to_s if !present?(tag)
tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag)
tag = sh("git describe --tags --abbrev=0").strip if !present?(tag)
version = tag.sub(%r{\Arelease/}, "").sub(/\Av/, "")
UI.user_error!("Command failed: #{command}\n#{stderr.strip}")
end
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
def app_project_settings
YAML.safe_load(File.read(APP_SPEC)).fetch("targets").fetch(TARGET).fetch("settings").fetch("base")
end
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
settings["CODE_SIGN_KEYCHAIN"] = ENV["SYBIL_SIGNING_KEYCHAIN_PATH"] if present?(ENV["SYBIL_SIGNING_KEYCHAIN_PATH"])
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
end
project.save
end
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)
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")
UI.user_error!("Release tag must look like v1.2.3; got #{tag.inspect}")
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)
"#{key.to_s.shellescape}=#{value.to_s.shellescape}"
end
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
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
def app_store_connect_key_options
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)
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
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)
private_lane :app_store_api_key do
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
)
end
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}")
private_lane :sync_match_signing do |options|
%w[
APP_STORE_CONNECT_API_KEY
APP_STORE_CONNECT_API_KEY_PATH
SIGH_API_KEY
SIGH_API_KEY_PATH
].each { |key| ENV.delete(key) }
match_options = {
type: "appstore",
readonly: options.fetch(:readonly),
app_identifier: APP_IDENTIFIER,
team_id: TEAM_ID,
profile_name: PROFILE_NAME,
git_branch: MATCH_BRANCH,
git_full_name: "Sybil Release Bot",
git_user_email: "james.magahern@me.com",
api_key: app_store_api_key
}
match_options[:git_url] = ENV.fetch("MATCH_GIT_URL")
match(match_options)
end
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)
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")
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
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"
)
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"
)
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
UI.user_error!("Could not find exported p12 at #{p12_path}") unless File.exist?(p12_path)
signing_identity_p12(p12_path, p12_password)
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)
secrets_path = File.join(SIGNING_OUTPUT_DIR, "ci-secrets.env")
File.write(
secrets_path,
[
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)
].join("\n") + "\n"
)
ensure
system("security", "delete-keychain", keychain_path, out: File::NULL, err: File::NULL) if present?(keychain_path) && File.exist?(keychain_path)
end
UI.success("Created CI signing files in #{SIGNING_OUTPUT_DIR}")
UI.important("Add the values from #{secrets_path} as repository secrets.")
desc "Create or update match signing assets"
lane :setup_signing do
setup_ci
sync_match_signing(readonly: false)
end
desc "Build Sybil and upload it to TestFlight"
desc "Build and upload to TestFlight"
lane :beta do
version = release_version
build_number = ENV["SYBIL_BUILD_NUMBER"].to_s
api_key = load_app_store_connect_api_key
setup_ci if ENV["CI"]
unless present?(build_number)
build_number = (local_build_number + 1).to_s
api_key = app_store_api_key
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}")
end
end
sh("xcodegen --spec project.yml")
UI.user_error!("Build number must be a positive integer") unless build_number.match?(/\A[1-9]\d*\z/)
increment_version_number(
version_number: release_version,
xcodeproj: "Sybil.xcodeproj"
)
sh("xcodegen --spec #{PROJECT_SPEC.shellescape}")
apply_release_signing_settings
latest_build_number = latest_testflight_build_number(
app_identifier: APP_IDENTIFIER,
api_key: api_key,
initial_build_number: 0
)
xcode_args = [
xcode_build_setting("MARKETING_VERSION", version),
xcode_build_setting("CURRENT_PROJECT_VERSION", build_number),
xcode_build_setting("CODE_SIGN_STYLE", "Manual"),
xcode_build_setting("DEVELOPMENT_TEAM", TEAM_ID),
xcode_build_setting("PROVISIONING_PROFILE_SPECIFIER", PROFILE_SPECIFIER),
xcode_build_setting("CODE_SIGN_IDENTITY", XCODE_CODE_SIGN_IDENTITY)
]
if present?(ENV["SYBIL_PROVISIONING_PROFILE_UUID"])
xcode_args << xcode_build_setting("PROVISIONING_PROFILE", ENV.fetch("SYBIL_PROVISIONING_PROFILE_UUID"))
end
if present?(ENV["SYBIL_SIGNING_KEYCHAIN_PATH"])
xcode_args << xcode_build_setting("CODE_SIGN_KEYCHAIN", ENV.fetch("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(" ")
increment_build_number(
build_number: latest_build_number + 1,
xcodeproj: "Sybil.xcodeproj"
)
ipa_path = build_app(
project: PROJECT_FILE,
sync_match_signing(readonly: true)
build_app(
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: {
method: "app-store",
destination: "export",
signingStyle: "manual",
provisioningProfiles: {
APP_IDENTIFIER => PROFILE_SPECIFIER
},
signingCertificate: EXPORT_SIGNING_CERTIFICATE,
teamID: TEAM_ID,
manageAppVersionAndBuildNumber: false,
uploadSymbols: true,
stripSwiftSymbols: true
APP_IDENTIFIER => PROFILE_NAME
}
}
)
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)
upload_to_testflight(
api_key: api_key,
app_identifier: APP_IDENTIFIER,
ipa: ipa_path,
skip_waiting_for_build_processing: true
)
end

View File

@@ -1,48 +0,0 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## iOS
### ios version
```sh
[bundle exec] fastlane ios version
```
Show the version Fastlane will stamp into the next TestFlight archive
### ios create_ci_signing
```sh
[bundle exec] fastlane ios create_ci_signing
```
Create CI signing certificate/profile and write ignored secret material under build/signing
### ios beta
```sh
[bundle exec] fastlane ios beta
```
Build Sybil and upload it to TestFlight
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).