All checks were successful
TestFlight / testflight (push) Successful in 1m27s
codesign resolves signing identities through the user keychain search list (first match wins) and ignores --keychain for the lookup. This runner hosts another project (Sybil-2) whose keychain holds the same Apple Distribution identity, so if that keychain is locked and appears earlier in the search list, codesign fails with errSecInternalComponent no matter how correctly our own keychain is set up. Prepend the fresh CI keychain to the search list for the build and always delete it afterward, which restores the original list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
187 lines
5.6 KiB
Ruby
187 lines
5.6 KiB
Ruby
require "shellwords"
|
|
require "xcodeproj"
|
|
|
|
default_platform(:ios)
|
|
|
|
APP_IDENTIFIER = "net.buzzert.attractor"
|
|
SCHEME = "Attractor"
|
|
TARGET_NAME = "App"
|
|
TEAM_ID = ENV["FASTLANE_TEAM_ID"].to_s.strip.empty? ? "DQQH5H6GBD" : ENV["FASTLANE_TEAM_ID"]
|
|
PROFILE_NAME = ENV["ATTRACTOR_PROVISIONING_PROFILE_SPECIFIER"].to_s.strip.empty? ? "Attractor AppStore CI" : ENV["ATTRACTOR_PROVISIONING_PROFILE_SPECIFIER"]
|
|
CI_KEYCHAIN_NAME = "attractor_ci_keychain"
|
|
CI_KEYCHAIN_PASSWORD = "attractor-ci-keychain-password"
|
|
CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db")
|
|
PROJECT_FILE = File.expand_path("../SBrowser.xcodeproj", __dir__)
|
|
|
|
def present?(value)
|
|
!value.to_s.strip.empty?
|
|
end
|
|
|
|
def ci?
|
|
present?(ENV["CI"])
|
|
end
|
|
|
|
def version_from_tag(tag)
|
|
patterns = [
|
|
%r{\Arelease/ios/v(\d+(?:\.\d+){1,2})\z},
|
|
%r{\AAttractor-(\d+(?:\.\d+){1,2})\z},
|
|
%r{\Av(\d+(?:\.\d+){1,2})\z}
|
|
]
|
|
|
|
patterns.each do |pattern|
|
|
match = tag.to_s.match(pattern)
|
|
return match[1] if match
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def release_version
|
|
candidates = [
|
|
ENV["ATTRACTOR_VERSION_TAG"],
|
|
ENV["GITHUB_REF_NAME"],
|
|
ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "")
|
|
]
|
|
|
|
candidates.each do |tag|
|
|
version = version_from_tag(tag)
|
|
return version if version
|
|
end
|
|
|
|
latest_tag = sh("git describe --tags --abbrev=0").strip
|
|
version = version_from_tag(latest_tag)
|
|
return version if version
|
|
|
|
candidates << latest_tag
|
|
UI.user_error!("Release tag must look like release/ios/v4.1, Attractor-4.1, or v4.1; got #{candidates.compact.inspect}")
|
|
end
|
|
|
|
# App Store Connect requires CFBundleVersion to be unique and strictly
|
|
# increasing app-wide. Attractor already has historical builds, so use a UTC
|
|
# timestamp by default instead of assuming a fresh CI run number is high enough.
|
|
def build_number
|
|
value = ENV["ATTRACTOR_BUILD_NUMBER"]
|
|
value = Time.now.utc.strftime("%Y%m%d%H%M%S") unless present?(value)
|
|
|
|
unless value.to_s.match?(/\A\d+\z/)
|
|
UI.user_error!("Build number must be numeric; got #{value.inspect}")
|
|
end
|
|
|
|
value.to_s
|
|
end
|
|
|
|
def stamp_project_versions(version:, build:)
|
|
project = Xcodeproj::Project.open(PROJECT_FILE)
|
|
target = project.targets.find { |candidate| candidate.name == TARGET_NAME }
|
|
UI.user_error!("Could not find target #{TARGET_NAME.inspect} in #{PROJECT_FILE}") unless target
|
|
|
|
target.build_configurations.each do |configuration|
|
|
configuration.build_settings["MARKETING_VERSION"] = version
|
|
configuration.build_settings["CURRENT_PROJECT_VERSION"] = build
|
|
end
|
|
|
|
project.save
|
|
end
|
|
|
|
platform :ios do
|
|
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
|
|
|
|
# CI signs headlessly, so match needs a fresh unlocked keychain to import
|
|
# into. codesign resolves identities through the user keychain search
|
|
# list (first match wins; the --keychain flag does not restrict the
|
|
# lookup), and other projects' keychains on this runner hold the same
|
|
# identity but are usually locked — so ours must come first. delete_keychain
|
|
# in the beta lane's ensure removes both the keychain and its search-list
|
|
# entry, which also keeps our (later locked) copy from shadowing those
|
|
# other projects.
|
|
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: 3600,
|
|
add_to_search_list: false
|
|
)
|
|
|
|
others = 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] + others).shelljoin}")
|
|
|
|
ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME
|
|
ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD
|
|
end
|
|
|
|
private_lane :sync_signing do |options|
|
|
match(
|
|
type: "appstore",
|
|
readonly: options.fetch(:readonly),
|
|
app_identifier: APP_IDENTIFIER,
|
|
team_id: TEAM_ID,
|
|
profile_name: PROFILE_NAME,
|
|
git_url: ENV.fetch("MATCH_GIT_URL"),
|
|
git_branch: ENV.fetch("MATCH_GIT_BRANCH", "master"),
|
|
git_full_name: "Attractor Release Bot",
|
|
git_user_email: "james.magahern@me.com",
|
|
api_key: options.fetch(:api_key)
|
|
)
|
|
end
|
|
|
|
desc "Create or update match signing assets"
|
|
lane :setup_signing do
|
|
sync_signing(api_key: app_store_api_key, readonly: false)
|
|
end
|
|
|
|
desc "Build and upload to TestFlight"
|
|
lane :beta do
|
|
prepare_ci_keychain
|
|
|
|
api_key = app_store_api_key
|
|
version = release_version
|
|
build = build_number
|
|
|
|
stamp_project_versions(version: version, build: build)
|
|
|
|
sync_signing(api_key: api_key, readonly: true)
|
|
|
|
build_app(
|
|
project: PROJECT_FILE,
|
|
scheme: SCHEME,
|
|
destination: "generic/platform=iOS",
|
|
export_method: "app-store",
|
|
codesigning_identity: "Apple Distribution",
|
|
xcargs: [
|
|
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
|
|
"CODE_SIGN_STYLE=Manual",
|
|
"CODE_SIGN_IDENTITY=Apple\\ Distribution",
|
|
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
|
|
].join(" "),
|
|
export_options: {
|
|
signingStyle: "manual",
|
|
teamID: TEAM_ID,
|
|
provisioningProfiles: {
|
|
APP_IDENTIFIER => PROFILE_NAME
|
|
}
|
|
}
|
|
)
|
|
|
|
upload_to_testflight(
|
|
api_key: api_key,
|
|
skip_waiting_for_build_processing: true
|
|
)
|
|
ensure
|
|
delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH)
|
|
end
|
|
end
|