Embed an iOS and Catalyst share extension using existing discovery and player APIs. Share saved players through an App Group snapshot, preserve the queue, and verify advancing playback before reporting success. Cover URL provider transport, settings migration, discovery addresses, duplicate handling, cancellation, and partial enqueue/play outcomes with focused tests. Document authentication and signing limits.
264 lines
7.4 KiB
Ruby
264 lines
7.4 KiB
Ruby
require "fileutils"
|
|
require "open3"
|
|
require "shellwords"
|
|
require "tempfile"
|
|
|
|
default_platform(:ios)
|
|
|
|
BUNDLE_IDENTIFIER = "net.buzzert.QueueCube"
|
|
SHARE_BUNDLE_IDENTIFIER = "net.buzzert.QueueCube.Share"
|
|
DEVELOPMENT_TEAM = "DQQH5H6GBD"
|
|
PROVISIONING_PROFILE_NAME = "QueueCube AppStore CI"
|
|
SHARE_PROVISIONING_PROFILE_NAME = "QueueCube Share AppStore CI"
|
|
CI_KEYCHAIN_NAME = "queuecube_ci_keychain"
|
|
CI_KEYCHAIN_PASSWORD = "queuecube-ci-keychain-password"
|
|
CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db")
|
|
APP_ROOT = File.expand_path("..", File.expand_path(__dir__))
|
|
IOS_PROJECT_DIR = File.join(APP_ROOT, "ios")
|
|
XCODE_PROJECT = File.join(IOS_PROJECT_DIR, "QueueCube.xcodeproj")
|
|
SCHEME = "QueueCube"
|
|
ARCHIVE_PATH = File.join(IOS_PROJECT_DIR, "build", "#{SCHEME}.xcarchive")
|
|
EXPORT_PATH = File.join(IOS_PROJECT_DIR, "build", "upload")
|
|
|
|
def shell_command(*parts)
|
|
parts.flatten.map { |part| part.to_s.shellescape }.join(" ")
|
|
end
|
|
|
|
def archive_path
|
|
ARCHIVE_PATH
|
|
end
|
|
|
|
def export_path
|
|
EXPORT_PATH
|
|
end
|
|
|
|
def git_output(*args)
|
|
stdout, stderr, status = Open3.capture3("git", *args, chdir: APP_ROOT)
|
|
UI.user_error!("git #{args.join(' ')} failed: #{stderr.strip}") unless status.success?
|
|
|
|
stdout.strip
|
|
end
|
|
|
|
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+){0,2})\z},
|
|
%r{\Av?(\d+(?:\.\d+){0,2})\z}
|
|
]
|
|
|
|
patterns.each do |pattern|
|
|
match = tag.to_s.match(pattern)
|
|
return match[1] if match
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def app_version
|
|
candidates = [
|
|
ENV["QUEUECUBE_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 = git_output("describe", "--tags", "--abbrev=0")
|
|
version = version_from_tag(latest_tag)
|
|
return version if version
|
|
|
|
candidates << latest_tag
|
|
UI.user_error!("Release tag must look like 3.2, v3.2, or release/ios/v3.2; got #{candidates.compact.inspect}")
|
|
end
|
|
|
|
def build_number
|
|
value = ENV["QUEUECUBE_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 upload_export_options
|
|
<<~PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>method</key>
|
|
<string>app-store-connect</string>
|
|
<key>signingStyle</key>
|
|
<string>manual</string>
|
|
<key>teamID</key>
|
|
<string>#{DEVELOPMENT_TEAM}</string>
|
|
<key>signingCertificate</key>
|
|
<string>Apple Distribution</string>
|
|
<key>provisioningProfiles</key>
|
|
<dict>
|
|
<key>#{BUNDLE_IDENTIFIER}</key>
|
|
<string>#{PROVISIONING_PROFILE_NAME}</string>
|
|
<key>#{SHARE_BUNDLE_IDENTIFIER}</key>
|
|
<string>#{SHARE_PROVISIONING_PROFILE_NAME}</string>
|
|
</dict>
|
|
<key>manageAppVersionAndBuildNumber</key>
|
|
<false/>
|
|
<key>stripSwiftSymbols</key>
|
|
<true/>
|
|
<key>uploadSymbols</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
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 imports the distribution identity into a
|
|
# fresh keychain. Put it first because this shared runner may have locked
|
|
# keychains from other projects containing the same identity.
|
|
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|
|
|
{
|
|
BUNDLE_IDENTIFIER => PROVISIONING_PROFILE_NAME,
|
|
SHARE_BUNDLE_IDENTIFIER => SHARE_PROVISIONING_PROFILE_NAME
|
|
}.each do |identifier, profile_name|
|
|
match(
|
|
type: "appstore",
|
|
readonly: options.fetch(:readonly),
|
|
app_identifier: identifier,
|
|
team_id: DEVELOPMENT_TEAM,
|
|
profile_name: profile_name,
|
|
git_url: ENV.fetch("MATCH_GIT_URL"),
|
|
git_branch: ENV.fetch("MATCH_GIT_BRANCH", "master"),
|
|
git_full_name: "QueueCube Release Bot",
|
|
git_user_email: "james.magahern@me.com",
|
|
api_key: options.fetch(:api_key)
|
|
)
|
|
end
|
|
end
|
|
|
|
desc "Create or update match signing assets"
|
|
lane :setup_signing do
|
|
sync_signing(api_key: app_store_api_key, readonly: false)
|
|
end
|
|
|
|
private_lane :build_release do
|
|
prepare_ci_keychain
|
|
|
|
api_key = app_store_api_key
|
|
version = app_version
|
|
build = build_number
|
|
UI.message("Using QueueCube version #{version} (build #{build}) from git")
|
|
|
|
sync_signing(api_key: api_key, readonly: true)
|
|
|
|
FileUtils.rm_rf(archive_path)
|
|
FileUtils.rm_rf(export_path)
|
|
|
|
Dir.chdir(IOS_PROJECT_DIR) do
|
|
sh(shell_command(
|
|
"xcodebuild",
|
|
"-project", XCODE_PROJECT,
|
|
"-scheme", SCHEME,
|
|
"-configuration", "Release",
|
|
"-destination", "generic/platform=iOS",
|
|
"-archivePath", archive_path,
|
|
"-allowProvisioningUpdates",
|
|
"clean",
|
|
"archive",
|
|
"DEVELOPMENT_TEAM=#{DEVELOPMENT_TEAM}",
|
|
"MARKETING_VERSION=#{version}",
|
|
"CURRENT_PROJECT_VERSION=#{build}",
|
|
"CODE_SIGN_STYLE=Manual",
|
|
"CODE_SIGN_IDENTITY=Apple Distribution",
|
|
"QUEUECUBE_APP_PROFILE=#{PROVISIONING_PROFILE_NAME}",
|
|
"QUEUECUBE_SHARE_PROFILE=#{SHARE_PROVISIONING_PROFILE_NAME}"
|
|
))
|
|
end
|
|
|
|
export_options = Tempfile.new(["queuecube-export-options", ".plist"])
|
|
export_options.write(upload_export_options)
|
|
export_options.close
|
|
|
|
FileUtils.rm_rf(export_path)
|
|
Dir.chdir(IOS_PROJECT_DIR) do
|
|
sh(shell_command(
|
|
"xcodebuild",
|
|
"-exportArchive",
|
|
"-archivePath", archive_path,
|
|
"-exportPath", export_path,
|
|
"-exportOptionsPlist", export_options.path,
|
|
"-allowProvisioningUpdates"
|
|
))
|
|
end
|
|
|
|
ipa_path = Dir[File.join(export_path, "*.ipa")].first
|
|
UI.user_error!("No IPA found in #{export_path}") unless ipa_path
|
|
|
|
{ api_key: api_key, ipa_path: ipa_path }
|
|
ensure
|
|
export_options&.unlink
|
|
delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH)
|
|
end
|
|
|
|
desc "Build an App Store-signed QueueCube IPA without uploading it"
|
|
lane :build do
|
|
build_release
|
|
end
|
|
|
|
desc "Build QueueCube for iOS and upload the archive to TestFlight"
|
|
lane :beta do
|
|
release = build_release
|
|
|
|
upload_to_testflight(
|
|
api_key: release.fetch(:api_key),
|
|
app_identifier: BUNDLE_IDENTIFIER,
|
|
ipa: release.fetch(:ipa_path),
|
|
skip_waiting_for_build_processing: true,
|
|
uses_non_exempt_encryption: false
|
|
)
|
|
end
|
|
end
|