235 lines
6.7 KiB
Ruby
235 lines
6.7 KiB
Ruby
require 'fileutils'
|
|
require 'digest'
|
|
require 'open3'
|
|
require 'shellwords'
|
|
|
|
opt_out_usage
|
|
|
|
ROOT = File.expand_path('..', __dir__)
|
|
FLAVORS = {
|
|
'staging' => {
|
|
artifact_prefix: 'whitenoise-staging',
|
|
android_package_name: 'org.parres.whitenoise.staging',
|
|
ios_app_identifier: 'dev.ipf.whitenoise.staging',
|
|
},
|
|
'production' => {
|
|
artifact_prefix: 'whitenoise',
|
|
android_package_name: 'org.parres.whitenoise',
|
|
ios_app_identifier: 'org.parres.whitenoise',
|
|
},
|
|
}.freeze
|
|
|
|
def run_from_root(parts)
|
|
env_parts = [
|
|
'env',
|
|
'-u', 'BUNDLE_BIN_PATH',
|
|
'-u', 'BUNDLE_GEMFILE',
|
|
'-u', 'BUNDLE_PATH',
|
|
'-u', 'BUNDLER_VERSION',
|
|
'-u', 'BUNDLER_SETUP',
|
|
'-u', 'RUBYOPT',
|
|
]
|
|
sh("cd #{Shellwords.escape(ROOT)} && #{(env_parts + parts).shelljoin}")
|
|
end
|
|
|
|
def flavor_config(flavor)
|
|
config = FLAVORS[flavor.to_s]
|
|
UI.user_error!("Unknown release flavor: #{flavor}") unless config
|
|
config
|
|
end
|
|
|
|
def release_tag(options = {})
|
|
tag = options[:tag].to_s.strip
|
|
return tag unless tag.empty?
|
|
|
|
tag = ENV.fetch('RELEASE_TAG', '').to_s.strip
|
|
return tag unless tag.empty?
|
|
|
|
return ENV.fetch('GITHUB_REF_NAME', '').to_s.strip if ENV['GITHUB_REF_TYPE'] == 'tag'
|
|
|
|
''
|
|
end
|
|
|
|
def release_info(options = {})
|
|
tag = release_tag(options)
|
|
|
|
command = ['./scripts/validate_release_version.sh']
|
|
command += ['--tag', tag] unless tag.empty?
|
|
|
|
output, error, status = Open3.capture3(*command, chdir: ROOT)
|
|
UI.user_error!(error.strip.empty? ? output.strip : error.strip) unless status.success?
|
|
|
|
info = {}
|
|
output.lines.each do |line|
|
|
key, value = line.strip.split('=', 2)
|
|
info[key] = value if key && value
|
|
end
|
|
|
|
UI.user_error!('Release version could not be resolved') unless info['full_version']
|
|
UI.user_error!('Release build number could not be resolved') unless info['build_number']
|
|
|
|
info
|
|
end
|
|
|
|
def flutter_build_args(info)
|
|
['--build-name', info['version_name'], '--build-number', info['build_number']]
|
|
end
|
|
|
|
def release_output_dir(info, flavor, *parts)
|
|
File.join(ROOT, 'build', 'releases', "v#{info['full_version']}", flavor.to_s, *parts)
|
|
end
|
|
|
|
def stage_artifact(source, destination_dir, destination_name, checksum: false)
|
|
UI.user_error!("Missing artifact: #{source}") unless File.file?(source)
|
|
|
|
FileUtils.mkdir_p(destination_dir)
|
|
destination = File.join(destination_dir, destination_name)
|
|
FileUtils.cp(source, destination)
|
|
UI.message("Staged #{destination}")
|
|
|
|
if checksum
|
|
digest = Digest::SHA256.file(destination).hexdigest
|
|
checksum_path = "#{destination}.sha256"
|
|
File.write(checksum_path, "#{digest} #{destination_name}\n")
|
|
UI.message("Staged #{checksum_path}")
|
|
end
|
|
|
|
destination
|
|
end
|
|
|
|
def latest_ipa
|
|
Dir[File.join(ROOT, 'build', 'ios', 'ipa', '*.ipa')].max_by { |path| File.mtime(path) }
|
|
end
|
|
|
|
def android_aab(flavor)
|
|
candidates = Dir[File.join(ROOT, 'build', 'app', 'outputs', 'bundle', '**', "*#{flavor}*.aab")]
|
|
candidates.max_by { |path| File.mtime(path) }
|
|
end
|
|
|
|
def require_macos!
|
|
UI.user_error!('iOS release builds require macOS') unless RUBY_PLATFORM.include?('darwin')
|
|
end
|
|
|
|
def build_android_flavor(flavor, options, build_native: true)
|
|
info = release_info(options)
|
|
config = flavor_config(flavor)
|
|
args = flutter_build_args(info)
|
|
|
|
run_from_root(['./scripts/build_android.sh']) if build_native
|
|
run_from_root(['flutter', 'build', 'apk', '--flavor', flavor, '--split-per-abi'] + args)
|
|
run_from_root(['flutter', 'build', 'appbundle', '--flavor', flavor] + args)
|
|
|
|
apk_dir = File.join(ROOT, 'build', 'app', 'outputs', 'flutter-apk')
|
|
output_dir = release_output_dir(info, flavor, 'android')
|
|
|
|
%w[arm64-v8a armeabi-v7a x86_64].each do |abi|
|
|
source = File.join(apk_dir, "app-#{abi}-#{flavor}-release.apk")
|
|
unless File.file?(source)
|
|
next UI.important("Skipping missing optional APK: #{source}") if abi == 'armeabi-v7a'
|
|
|
|
UI.user_error!("Required APK was not found: #{source}")
|
|
end
|
|
|
|
stage_artifact(
|
|
source,
|
|
output_dir,
|
|
"#{config[:artifact_prefix]}-#{info['version_name']}-#{abi}.apk",
|
|
checksum: true,
|
|
)
|
|
end
|
|
|
|
source = android_aab(flavor)
|
|
UI.user_error!("#{flavor.capitalize} AAB was not found") unless source
|
|
|
|
stage_artifact(
|
|
source,
|
|
output_dir,
|
|
"#{config[:artifact_prefix]}-#{info['version_name']}.aab",
|
|
checksum: true,
|
|
)
|
|
end
|
|
|
|
def build_ios_flavor(flavor, options, build_native: true)
|
|
require_macos!
|
|
info = release_info(options)
|
|
config = flavor_config(flavor)
|
|
export_options = File.join(ROOT, 'ios', "ExportOptions.#{flavor}-ad-hoc.plist")
|
|
|
|
run_from_root(['./scripts/build_ios.sh']) if build_native
|
|
build_args = ['flutter', 'build', 'ipa', '--flavor', flavor]
|
|
build_args += if File.file?(export_options)
|
|
['--export-options-plist', export_options]
|
|
else
|
|
['--export-method', 'app-store']
|
|
end
|
|
run_from_root(build_args + flutter_build_args(info))
|
|
|
|
source = latest_ipa
|
|
UI.user_error!("#{flavor.capitalize} IPA was not found") unless source
|
|
|
|
stage_artifact(
|
|
source,
|
|
release_output_dir(info, flavor, 'ios'),
|
|
"#{config[:artifact_prefix]}-#{info['full_version']}.ipa",
|
|
)
|
|
end
|
|
|
|
def build_channel_release(flavor, options)
|
|
build_android_flavor(flavor, options)
|
|
|
|
if RUBY_PLATFORM.include?('darwin')
|
|
build_ios_flavor(flavor, options)
|
|
else
|
|
UI.important("Skipping #{flavor} iOS artifacts because this machine is not macOS")
|
|
end
|
|
end
|
|
|
|
desc 'Validate release version metadata'
|
|
lane :release_doctor do |options|
|
|
info = release_info(options)
|
|
UI.success("Release #{info['full_version']} is ready to build")
|
|
end
|
|
|
|
desc 'Build staging Android APKs and AAB'
|
|
lane :build_android_staging do |options|
|
|
build_android_flavor('staging', options)
|
|
end
|
|
|
|
desc 'Build production Android APKs and AAB'
|
|
lane :build_android_production do |options|
|
|
build_android_flavor('production', options)
|
|
end
|
|
|
|
desc 'Build staging iOS IPA for App Store Connect/TestFlight'
|
|
lane :build_ios_staging do |options|
|
|
build_ios_flavor('staging', options)
|
|
end
|
|
|
|
desc 'Build production iOS IPA for App Store Connect/TestFlight'
|
|
lane :build_ios_production do |options|
|
|
build_ios_flavor('production', options)
|
|
end
|
|
|
|
desc 'Build staging app release artifacts'
|
|
lane :build_staging_release do |options|
|
|
build_channel_release('staging', options)
|
|
end
|
|
|
|
desc 'Build production app release artifacts'
|
|
lane :build_production_release do |options|
|
|
build_channel_release('production', options)
|
|
end
|
|
|
|
desc 'Build staging and production release artifacts'
|
|
lane :build_all_release_artifacts do |options|
|
|
build_android_flavor('staging', options)
|
|
build_android_flavor('production', options, build_native: false)
|
|
|
|
if RUBY_PLATFORM.include?('darwin')
|
|
build_ios_flavor('staging', options)
|
|
build_ios_flavor('production', options, build_native: false)
|
|
else
|
|
UI.important('Skipping iOS artifacts because this machine is not macOS')
|
|
end
|
|
end
|