Files
QueueCube/fastlane/Fastfile
T
buzzert 8723955705 ios: add share extension to queue and play video links
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.
2026-10-20 22:08:12 -07:00

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