510 lines
18 KiB
Ruby
510 lines
18 KiB
Ruby
# This file contains the fastlane.tools configuration
|
|
# You can find the documentation at https://docs.fastlane.tools
|
|
#
|
|
# For a list of all available actions, check out
|
|
#
|
|
# https://docs.fastlane.tools/actions
|
|
#
|
|
# For a list of all available plugins, check out
|
|
#
|
|
# https://docs.fastlane.tools/plugins/available-plugins
|
|
#
|
|
|
|
min_fastlane_version('2.156.0')
|
|
fastlane_require "dotenv"
|
|
fastlane_require "uri"
|
|
|
|
default_platform(:ios)
|
|
platform :ios do
|
|
|
|
#### Pre ####
|
|
|
|
before_all do
|
|
# Ensure used Xcode version
|
|
xcversion(version: "15.2")
|
|
end
|
|
|
|
#### Public ####
|
|
|
|
desc "Builds an adhoc ipa"
|
|
lane :adhoc do |options|
|
|
options[:adhoc] = true
|
|
if !options.has_key?(:build_number)
|
|
build_number = generate_build_number()
|
|
options = { build_number: build_number }.merge(options)
|
|
end
|
|
build_release(options)
|
|
end
|
|
|
|
desc "Builds an ipa for the App Store"
|
|
lane :app_store do |options|
|
|
UI.user_error!("'SENTRY_AUTH_TOKEN' environment variable should be set to use this lane, see Passbolt") unless !ENV["SENTRY_AUTH_TOKEN"].to_s.empty?
|
|
sentry_check_cli_installed
|
|
|
|
if !options.has_key?(:build_number)
|
|
build_number = generate_build_number()
|
|
options = { build_number: build_number }.merge(options)
|
|
end
|
|
build_release(options)
|
|
upload_dsyms_to_sentry
|
|
end
|
|
|
|
desc "Builds an Alpha ipa for pull request branches"
|
|
lane :alpha do
|
|
# Check we have all the tokens and tools necessary
|
|
UI.user_error!("'DIAWI_API_TOKEN' environment variable should be set to use this lane") unless !ENV["DIAWI_API_TOKEN"].to_s.empty?
|
|
|
|
# Generate the "Alpha" app variant
|
|
setup_app_variant(name: "Alpha")
|
|
# Builds an Ad Hoc IPA
|
|
adhoc
|
|
# Upload to Diawi
|
|
upload_to_diawi
|
|
end
|
|
|
|
desc "Upload IPA to Diawi"
|
|
lane :upload_to_diawi do
|
|
UI.user_error!("'DIAWI_API_TOKEN' environment variable should be set to use this lane") unless !ENV["DIAWI_API_TOKEN"].to_s.empty?
|
|
|
|
# Upload to Diawi
|
|
diawi(
|
|
token: ENV["DIAWI_API_TOKEN"],
|
|
wall_of_apps: false,
|
|
file: lane_context[SharedValues::IPA_OUTPUT_PATH]
|
|
)
|
|
|
|
# Get the Diawi link from Diawi action shared value
|
|
diawi_link = lane_context[SharedValues::UPLOADED_FILE_LINK_TO_DIAWI]
|
|
UI.command_output("Diawi link: " + diawi_link.to_s)
|
|
|
|
# Generate the Diawi QR code file link
|
|
diawi_app_id = URI(diawi_link).path.split('/').last
|
|
diawi_qr_code_link = "https://www.diawi.com/qrcode/link/#{diawi_app_id}"
|
|
UI.command_output("Diawi QR code link: " + diawi_qr_code_link.to_s)
|
|
|
|
# Set "DIAWI_FILE_LINK" to GitHub environment variables for Github actions
|
|
if is_ci
|
|
sh("echo DIAWI_FILE_LINK=#{diawi_link} >> $GITHUB_ENV")
|
|
sh("echo DIAWI_QR_CODE_LINK=#{diawi_qr_code_link} >> $GITHUB_ENV")
|
|
end
|
|
end
|
|
|
|
desc "Builds the ipa for the AppStore, then uploads it"
|
|
lane :deploy_release do |options|
|
|
# build the IPA
|
|
app_store(options)
|
|
# upload the IPA and metadata
|
|
deliver()
|
|
|
|
# We haven't yet implemented/tested deliver/upload_to_appstore properly so keep it manual for now
|
|
# UI.message("IPA is available at path '#{ENV['IPA_OUTPUT_PATH']}'. Please upload manually using Application Loader.")
|
|
# UI.confirm("Have you uploaded the IPA to the AppStore now?")
|
|
#
|
|
end
|
|
|
|
|
|
desc "Use an app variant. An app variant overwrite default project configuration or ressource files with custom values"
|
|
lane :setup_app_variant do |options|
|
|
appVariantScript = "../Variants/setup_app_variant.sh"
|
|
# Make sure app variant script can be executed
|
|
sh "chmod 775 #{appVariantScript}"
|
|
# Run app variant script with variant name
|
|
sh(appVariantScript, options[:name])
|
|
# Force reloading env variables
|
|
Dotenv.overload(".env.default")
|
|
end
|
|
|
|
desc "Build the app for simulator to ensure it compiles"
|
|
lane :build do |options|
|
|
xcodegen(spec: "project.yml")
|
|
cocoapods
|
|
|
|
app_name = "Riot"
|
|
build_app(
|
|
clean: true,
|
|
scheme: app_name,
|
|
derived_data_path: "./DerivedData/",
|
|
buildlog_path: "./DerivedData/Logs/",
|
|
# skip_package_ipa: true,
|
|
skip_archive: true,
|
|
destination: "generic/platform=iOS Simulator",
|
|
)
|
|
end
|
|
|
|
desc "Run tests"
|
|
lane :test do
|
|
xcodegen(spec: "project.yml")
|
|
cocoapods
|
|
|
|
run_tests(
|
|
workspace: "Riot.xcworkspace",
|
|
scheme: "RiotSwiftUI",
|
|
code_coverage: true,
|
|
# Test result configuration
|
|
result_bundle: true,
|
|
output_directory: "./build/test",
|
|
output_files: "riot.swiftui.unit.test.html,riot.swiftui.unit.test.report.junit.xml",
|
|
open_report: !is_ci?
|
|
)
|
|
|
|
run_tests(
|
|
workspace: "Riot.xcworkspace",
|
|
scheme: "Riot",
|
|
code_coverage: true,
|
|
# Test result configuration
|
|
result_bundle: true,
|
|
output_directory: "./build/test",
|
|
output_files: "riot.unit.test.html,riot.unit.test.report.junit.xml",
|
|
open_report: !is_ci?
|
|
)
|
|
|
|
slather(
|
|
cobertura_xml: true,
|
|
output_directory: "./build/test",
|
|
workspace: "Riot.xcworkspace",
|
|
proj: "Riot.xcodeproj",
|
|
scheme: "Riot",
|
|
binary_basename: "Element",
|
|
)
|
|
end
|
|
|
|
desc "Run UI tests"
|
|
lane :uitest do
|
|
xcodegen(spec: "project.yml")
|
|
cocoapods
|
|
|
|
run_tests(
|
|
workspace: "Riot.xcworkspace",
|
|
scheme: "RiotSwiftUITests",
|
|
device: "iPhone 15",
|
|
code_coverage: true,
|
|
# Test result configuration
|
|
result_bundle: true,
|
|
output_directory: "./build/test",
|
|
output_files: "riot.swiftui.ui.test.html,riot.swiftui.ui.test.report.junit.xml",
|
|
open_report: !is_ci?
|
|
)
|
|
|
|
slather(
|
|
cobertura_xml: true,
|
|
output_directory: "./build/test",
|
|
workspace: "Riot.xcworkspace",
|
|
proj: "Riot.xcodeproj",
|
|
scheme: "RiotSwiftUITests",
|
|
binary_basename: "RiotSwiftUI",
|
|
)
|
|
end
|
|
|
|
|
|
#### Private ####
|
|
|
|
desc "Download App Store or Ad-Hoc provisioning profiles"
|
|
private_lane :build_release do |options|
|
|
if is_ci
|
|
UI.user_error!("'APPSTORECONNECT_KEY_ID' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_ID"].to_s.empty?
|
|
UI.user_error!("'APPSTORECONNECT_KEY_ISSUER_ID' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_ISSUER_ID"].to_s.empty?
|
|
UI.user_error!("'APPSTORECONNECT_KEY_CONTENT' environment variable should be set to use this lane") unless !ENV["APPSTORECONNECT_KEY_CONTENT"].to_s.empty?
|
|
|
|
app_store_connect_api_key(
|
|
key_id: ENV["APPSTORECONNECT_KEY_ID"],
|
|
issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"],
|
|
key_content: ENV["APPSTORECONNECT_KEY_CONTENT"]
|
|
)
|
|
else
|
|
UI.user_error!("'APPLE_ID' environment variable should be set to use this lane") unless !ENV["APPLE_ID"].to_s.empty?
|
|
end
|
|
|
|
build_number = options[:build_number]
|
|
UI.user_error!("'build_number' parameter is missing") unless !build_number.to_s.empty?
|
|
|
|
# ad-hoc or app-store?
|
|
adhoc = options.fetch(:adhoc, false)
|
|
|
|
# Extract git information to show within the app
|
|
git_branch_name = options[:git_tag]
|
|
if git_branch_name.to_s.empty?
|
|
# Retrieve the current git branch as a fallback
|
|
git_branch_name = mx_git_branch
|
|
end
|
|
UI.user_error!("Unable to retrieve GIT tag or branch") unless !git_branch_name.to_s.empty?
|
|
|
|
# Fetch team id from Appfile
|
|
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
|
|
UI.user_error!("Fail to fetch team id from Appfile") unless !team_id.to_s.empty?
|
|
|
|
# Generate versioning preprocessor macros
|
|
additional_preprocessor_definitions_hash = release_versioning_preprocessor_definitions(git_branch: git_branch_name, build_number: build_number)
|
|
additional_preprocessor_definitions = additional_preprocessor_definitions_hash.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
|
|
|
|
# Generate xcodebuild additional arguments
|
|
xcargs_hash = {
|
|
"GCC_PREPROCESSOR_DEFINITIONS" => "$(GCC_PREPROCESSOR_DEFINITIONS) #{additional_preprocessor_definitions}",
|
|
# Fix XCode 14 code signing issues for Swift packages containing resources bundles.
|
|
"CODE_SIGN_STYLE" => "Manual",
|
|
}
|
|
|
|
xcargs = xcargs_hash.map { |k, v| "#{k}=#{v.shellescape}" }.join(" ")
|
|
|
|
# Generate the project
|
|
xcodegen(spec: "project.yml")
|
|
|
|
# Clear derived data
|
|
clear_derived_data(derived_data_path: ENV["DERIVED_DATA_PATH"])
|
|
|
|
# Setup project provisioning profiles
|
|
download_provisioning_profiles(adhoc: adhoc)
|
|
disable_automatic_code_signing
|
|
update_project_provisioning_profiles
|
|
|
|
# Update build number
|
|
update_build_number(build_number: build_number)
|
|
|
|
# Perform a pod install
|
|
cocoapods(repo_update: true)
|
|
|
|
# Select a config
|
|
if adhoc
|
|
export_method = "ad-hoc"
|
|
main_provisioning_profile = ENV["ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
|
|
share_extension_provisioning_profile = ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
siri_intents_provisioning_profile = ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
nse_provisioning_profile = ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
|
broadcast_upload_extension_provisioning_profile = ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
else
|
|
export_method = "app-store"
|
|
main_provisioning_profile = ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
|
|
share_extension_provisioning_profile = ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
siri_intents_provisioning_profile = ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
nse_provisioning_profile = ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
|
broadcast_upload_extension_provisioning_profile = ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
end
|
|
|
|
# Build app and create ipa
|
|
build_app(
|
|
clean: true,
|
|
scheme: ENV["SCHEME"],
|
|
xcargs: xcargs,
|
|
export_method: export_method,
|
|
derived_data_path: ENV["DERIVED_DATA_PATH"],
|
|
archive_path: ENV["ARCHIVE_PATH"],
|
|
output_directory: ENV["BUILD_OUTPUT_DIRECTORY"],
|
|
output_name: "#{ENV["IPA_NAME"]}.ipa",
|
|
buildlog_path: ENV["BUILD_LOG_DIRECTORY"],
|
|
codesigning_identity: ENV["APPSTORE_CODESIGNING_IDENTITY"],
|
|
skip_profile_detection: true,
|
|
export_options: {
|
|
method: export_method,
|
|
signingStyle: "manual",
|
|
teamID: team_id,
|
|
signingCertificate: ENV["APPSTORE_SIGNING_CERTIFICATE"],
|
|
provisioningProfiles: {
|
|
ENV["MAIN_BUNDLE_ID"] => main_provisioning_profile,
|
|
ENV["SHARE_EXTENSION_BUNDLE_ID"] => share_extension_provisioning_profile,
|
|
ENV["NSE_BUNDLE_ID"] => nse_provisioning_profile,
|
|
ENV["SIRI_INTENTS_EXTENSION_BUNDLE_ID"] => siri_intents_provisioning_profile,
|
|
ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"] => broadcast_upload_extension_provisioning_profile,
|
|
},
|
|
iCloudContainerEnvironment: "Production",
|
|
},
|
|
)
|
|
end
|
|
|
|
#### Private ####
|
|
|
|
desc "Download App Store or Ad-Hoc provisioning profiles"
|
|
private_lane :download_provisioning_profiles do |options|
|
|
adhoc = options.fetch(:adhoc, false)
|
|
|
|
output_path = ENV["PROVISIONING_PROFILES_PATH"]
|
|
skip_certificate_verification = false
|
|
|
|
main_provisioning_name = adhoc ? ENV["ADHOC_MAIN_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"]
|
|
share_extension_provisioning_name = adhoc ? ENV["ADHOC_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
siri_intents_provisioning_name = adhoc ? ENV["ADHOC_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
notification_service_extension_provisioning_name = adhoc ? ENV["ADHOC_NSE_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"]
|
|
broadcast_upload_extension_provisioning_name = adhoc ? ENV["ADHOC_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"] : ENV["APPSTORE_BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"]
|
|
|
|
# Main application
|
|
get_provisioning_profile(
|
|
app_identifier: ENV["MAIN_BUNDLE_ID"],
|
|
provisioning_name: main_provisioning_name,
|
|
ignore_profiles_with_different_name: true,
|
|
adhoc: adhoc,
|
|
skip_certificate_verification: skip_certificate_verification,
|
|
output_path: output_path,
|
|
filename: ENV["MAIN_PROVISIONING_PROFILE_FILENAME"],
|
|
readonly: true,
|
|
)
|
|
# Share extension
|
|
get_provisioning_profile(
|
|
app_identifier: ENV["SHARE_EXTENSION_BUNDLE_ID"],
|
|
provisioning_name: share_extension_provisioning_name,
|
|
ignore_profiles_with_different_name: true,
|
|
adhoc: adhoc,
|
|
skip_certificate_verification: skip_certificate_verification,
|
|
output_path: output_path,
|
|
filename: ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
|
|
readonly: true,
|
|
)
|
|
# Siri Intents extension
|
|
get_provisioning_profile(
|
|
app_identifier: ENV["SIRI_INTENTS_EXTENSION_BUNDLE_ID"],
|
|
provisioning_name: siri_intents_provisioning_name,
|
|
ignore_profiles_with_different_name: true,
|
|
adhoc: adhoc,
|
|
skip_certificate_verification: skip_certificate_verification,
|
|
output_path: output_path,
|
|
filename: ENV["SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
|
|
readonly: true,
|
|
)
|
|
# NSE
|
|
get_provisioning_profile(
|
|
app_identifier: ENV["NSE_BUNDLE_ID"],
|
|
provisioning_name: notification_service_extension_provisioning_name,
|
|
ignore_profiles_with_different_name: true,
|
|
adhoc: adhoc,
|
|
skip_certificate_verification: skip_certificate_verification,
|
|
output_path: output_path,
|
|
filename: ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
|
|
readonly: true,
|
|
)
|
|
# Broadcast Upload Extension
|
|
get_provisioning_profile(
|
|
app_identifier: ENV["BROADCAST_UPLOAD_EXTENSION_BUNDLE_ID"],
|
|
provisioning_name: broadcast_upload_extension_provisioning_name,
|
|
ignore_profiles_with_different_name: true,
|
|
adhoc: adhoc,
|
|
skip_certificate_verification: skip_certificate_verification,
|
|
output_path: output_path,
|
|
filename: ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
|
|
readonly: true,
|
|
)
|
|
end
|
|
|
|
desc "Update provisioning profiles for each target"
|
|
private_lane :update_project_provisioning_profiles do
|
|
provisioning_profiles_path = ENV["PROVISIONING_PROFILES_PATH"]
|
|
build_configuration = "Release"
|
|
xcodeproj = ENV["PROJECT_PATH"]
|
|
|
|
# Main application
|
|
update_project_provisioning(
|
|
xcodeproj: xcodeproj,
|
|
profile: "#{provisioning_profiles_path}#{ENV["MAIN_PROVISIONING_PROFILE_FILENAME"]}",
|
|
target_filter: ENV["MAIN_TARGET"],
|
|
build_configuration: build_configuration,
|
|
)
|
|
# Share extension
|
|
update_project_provisioning(
|
|
xcodeproj: xcodeproj,
|
|
profile: "#{provisioning_profiles_path}#{ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
|
|
target_filter: ENV["SHARE_EXTENSION_TARGET"],
|
|
build_configuration: build_configuration,
|
|
)
|
|
# Siri Intents extension
|
|
update_project_provisioning(
|
|
xcodeproj: xcodeproj,
|
|
profile: "#{provisioning_profiles_path}#{ENV["SIRI_INTENTS_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
|
|
target_filter: ENV["SIRI_INTENTS_EXTENSION_TARGET"],
|
|
build_configuration: build_configuration,
|
|
)
|
|
# NSE
|
|
update_project_provisioning(
|
|
xcodeproj: xcodeproj,
|
|
profile: "#{provisioning_profiles_path}#{ENV["NSE_PROVISIONING_PROFILE_FILENAME"]}",
|
|
target_filter: ENV["NSE_TARGET"],
|
|
build_configuration: build_configuration,
|
|
)
|
|
# Broadcast Upload Extension
|
|
update_project_provisioning(
|
|
xcodeproj: xcodeproj,
|
|
profile: "#{provisioning_profiles_path}#{ENV["BROADCAST_UPLOAD_EXTENSION_PROVISIONING_PROFILE_FILENAME"]}",
|
|
target_filter: ENV["BROADCAST_UPLOAD_EXTENSION_TARGET"],
|
|
build_configuration: build_configuration,
|
|
)
|
|
end
|
|
|
|
desc "Update application build number for all targets"
|
|
private_lane :update_build_number do |options|
|
|
build_number = options[:build_number]
|
|
|
|
update_file_content(
|
|
"../Config/AppVersion.xcconfig",
|
|
/(CURRENT_PROJECT_VERSION\s*=)\s*.*/ => "\\1 #{build_number}"
|
|
)
|
|
end
|
|
|
|
desc "Returns version identifiers hash to inject in GCC_PREPROCESSOR_DEFINITIONS for release builds"
|
|
private_lane :release_versioning_preprocessor_definitions do |options|
|
|
preprocessor_definitions = Hash.new
|
|
|
|
git_branch_name = options[:git_branch]
|
|
build_number = options[:build_number]
|
|
|
|
if !git_branch_name.to_s.empty?
|
|
preprocessor_definitions["GIT_BRANCH"] = git_branch_name.sub("origin/", "").sub("heads/", "")
|
|
end
|
|
|
|
if !build_number.to_s.empty?
|
|
preprocessor_definitions["BUILD_NUMBER"] = build_number
|
|
end
|
|
|
|
preprocessor_definitions
|
|
end
|
|
|
|
desc "Upload dsym files to Sentry to symbolicate crashes"
|
|
private_lane :upload_dsyms_to_sentry do
|
|
UI.user_error!("'SENTRY_AUTH_TOKEN' environment variable should be set to use this lane") unless !ENV["SENTRY_AUTH_TOKEN"].to_s.empty?
|
|
|
|
dsym_path = "#{ENV["BUILD_OUTPUT_DIRECTORY"]}/#{ENV["IPA_NAME"]}.app.dSYM.zip"
|
|
|
|
sentry_upload_dif(
|
|
auth_token: ENV["SENTRY_AUTH_TOKEN"],
|
|
org_slug: 'element',
|
|
project_slug: 'element-ios',
|
|
url: 'https://sentry.tools.element.io/',
|
|
path: dsym_path,
|
|
)
|
|
end
|
|
|
|
# git_branch can return an empty screen with some CI tools (like GH actions)
|
|
# The CI build script needs to define MX_GIT_BRANCH with the right branch.
|
|
def mx_git_branch
|
|
mx_git_branch = git_branch
|
|
if mx_git_branch == ""
|
|
ensure_env_vars(
|
|
env_vars: ['MX_GIT_BRANCH']
|
|
)
|
|
mx_git_branch = ENV["MX_GIT_BRANCH"]
|
|
end
|
|
return mx_git_branch
|
|
end
|
|
|
|
# Find the latest branch with the given name pattern in the given repo
|
|
def find_branch(repo_slug, pattern)
|
|
list = `git ls-remote --heads --sort=version:refname https://github.com/#{repo_slug} #{pattern}`
|
|
list.split("\n")
|
|
.map { |line| line.sub(%r{[0-9a-f]+\trefs/heads/}, '').chomp }
|
|
.last # Latest ref found, in "version:refname" semantic order
|
|
end
|
|
end
|
|
|
|
# Update an arbitrary file by applying some RegExp replacements to its content
|
|
#
|
|
# @param [String] file The path to the file that needs replacing
|
|
# @param [Hash<RegExp, String>] replacements A list of replacements to apply
|
|
#
|
|
def update_file_content(file, replacements)
|
|
content = File.read(file)
|
|
replacements.each do |pattern, replacement|
|
|
content.gsub!(pattern, replacement)
|
|
end
|
|
File.write(file, content)
|
|
end
|
|
|
|
# Generates a new build number based on timestamp
|
|
def generate_build_number()
|
|
require 'date'
|
|
return DateTime.now.strftime("%Y%m%d%H%M%S")
|
|
end
|