fastlane-plugin-wpmreleasetoolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +339 -0
- data/README.md +38 -0
- data/bin/drawText +19 -0
- data/ext/drawText/extconf.rb +36 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit.rb +16 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/README.md +20 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +53 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +171 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +103 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +83 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +54 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +69 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +58 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +115 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +71 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +106 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +59 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +90 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +170 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +247 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +96 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +134 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +92 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +87 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +114 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +62 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +70 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +47 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +121 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +167 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +46 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb +152 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +359 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb +475 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +282 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +224 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +135 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +80 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +208 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb +348 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +107 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb +182 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +399 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +21 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +5 -0
- metadata +449 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Helper
|
|
6
|
+
module Ios
|
|
7
|
+
class L10nHelper
|
|
8
|
+
SWIFTGEN_VERSION = '6.4.0'
|
|
9
|
+
DEFAULT_BASE_LANG = 'en'
|
|
10
|
+
CONFIG_FILE_NAME = 'swiftgen-stringtypes.yml'
|
|
11
|
+
|
|
12
|
+
attr_reader :install_path
|
|
13
|
+
attr_reader :version
|
|
14
|
+
|
|
15
|
+
# @param [String] install_path The path to install SwiftGen to. Usually something like "$PROJECT_DIR/vendor/swiftgen/#{SWIFTGEN_VERSION}".
|
|
16
|
+
# It's recommended to provide an absolute path here rather than a relative one, to ensure it's not dependant on where the action is run from.
|
|
17
|
+
# @param [String] version The version of SwiftGen to use. This will be used both:
|
|
18
|
+
# - to check if the current version located in `install_path`, if it already exists, is the expected one
|
|
19
|
+
# - to know which version to download if there is not one installed in `install_path` yet
|
|
20
|
+
#
|
|
21
|
+
def initialize(install_path:, version: SWIFTGEN_VERSION)
|
|
22
|
+
@install_path = install_path
|
|
23
|
+
@version = version || SWIFTGEN_VERSION
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if SwiftGen is installed in the provided `install_path` and if so if the installed version matches the expected `version`
|
|
27
|
+
#
|
|
28
|
+
def check_swiftgen_installed
|
|
29
|
+
return false unless File.exist?(swiftgen_bin)
|
|
30
|
+
|
|
31
|
+
vers_string = `#{swiftgen_bin} --version`
|
|
32
|
+
# The SwiftGen version string has this format:
|
|
33
|
+
#
|
|
34
|
+
# SwiftGen v6.4.0 (Stencil v0.13.1, StencilSwiftKit v2.7.2, SwiftGenKit v6.4.0)
|
|
35
|
+
return vers_string.include?("SwiftGen v#{version}")
|
|
36
|
+
rescue
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Download the ZIP of SwiftGen for the requested `version` and install it in the `install_path`
|
|
41
|
+
#
|
|
42
|
+
# @note This action nukes anything at `install_path` – if something already exists – prior to install SwiftGen there
|
|
43
|
+
#
|
|
44
|
+
def install_swiftgen!
|
|
45
|
+
UI.message "Installing SwiftGen #{version} into #{install_path}"
|
|
46
|
+
Dir.mktmpdir do |tmpdir|
|
|
47
|
+
zipfile = File.join(tmpdir, "swiftgen-#{version}.zip")
|
|
48
|
+
Action.sh('curl', '--fail', '--location', '-o', zipfile, "https://github.com/SwiftGen/SwiftGen/releases/download/#{version}/swiftgen-#{version}.zip")
|
|
49
|
+
extracted_dir = File.join(tmpdir, "swiftgen-#{version}")
|
|
50
|
+
Action.sh('unzip', zipfile, '-d', extracted_dir)
|
|
51
|
+
|
|
52
|
+
FileUtils.rm_rf(install_path) if File.exist?(install_path)
|
|
53
|
+
FileUtils.mkdir_p(install_path)
|
|
54
|
+
FileUtils.cp_r("#{extracted_dir}/.", install_path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Install SwiftGen if necessary (if not installed yet with the expected version), then run the checks and returns the violations found, if any
|
|
59
|
+
#
|
|
60
|
+
# @param [String] input_dir The path (ideally absolute) to the directory containing the `.lproj` folders to parse
|
|
61
|
+
# @param [String] base_lang The code name (i.e the basename of one of the `.lproj` folders) of the locale to use as the baseline
|
|
62
|
+
# @return [Hash<String, String>] A hash whose keys are the language codes (basename of `.lproj` folders) for which violations were found,
|
|
63
|
+
# and the values are the output of the `diff` showing these violations.
|
|
64
|
+
#
|
|
65
|
+
def run(input_dir:, base_lang: DEFAULT_BASE_LANG, only_langs: nil)
|
|
66
|
+
check_swiftgen_installed || install_swiftgen!
|
|
67
|
+
find_diffs(input_dir: input_dir, base_lang: base_lang, only_langs: only_langs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##################
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Path to the swiftgen binary installed at install_path
|
|
75
|
+
def swiftgen_bin
|
|
76
|
+
"#{install_path}/bin/swiftgen"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Name to use for the generated files / output files of SwiftGen for each locale. Those files will be generated in the temporary directory to then diff them.
|
|
80
|
+
def output_filename(lang)
|
|
81
|
+
"L10nParamsList.#{lang}.txt"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The Stencil template that we want SwiftGen to use to generate the output.
|
|
85
|
+
# It iterates on every "table" (`.strings` file, in most cases there's only one, `Localizable.strings`),
|
|
86
|
+
# and for each, iterates on every entry found to print the key and the corresponding types parsed by SwiftGen from the placeholders found in that translation
|
|
87
|
+
def template_content
|
|
88
|
+
<<~TEMPLATE
|
|
89
|
+
{% macro recursiveBlock table item %}
|
|
90
|
+
{% for string in item.strings %}
|
|
91
|
+
"{{string.key}}" => [{{string.types|join:","}}]
|
|
92
|
+
{% endfor %}
|
|
93
|
+
{% for child in item.children %}
|
|
94
|
+
{% call recursiveBlock table child %}
|
|
95
|
+
{% endfor %}
|
|
96
|
+
{% endmacro %}
|
|
97
|
+
|
|
98
|
+
{% for table in tables %}
|
|
99
|
+
{% call recursiveBlock table.name table.levels %}
|
|
100
|
+
{% endfor %}
|
|
101
|
+
TEMPLATE
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Create the template file and the config file, in the temp dir, to be used by SwiftGen when parsing the input files.
|
|
105
|
+
#
|
|
106
|
+
# @return [(String, Array<String>)] A tuple of (config_file_absolute_path, Array<langs>)
|
|
107
|
+
#
|
|
108
|
+
def generate_swiftgen_config!(input_dir, output_dir, only_langs: nil)
|
|
109
|
+
# Create the template file
|
|
110
|
+
template_path = File.absolute_path(File.join(output_dir, 'strings-types.stencil'))
|
|
111
|
+
File.write(template_path, template_content)
|
|
112
|
+
|
|
113
|
+
# Dynamically create a SwiftGen config which will cover all supported languages
|
|
114
|
+
langs = Dir.chdir(input_dir) do
|
|
115
|
+
Dir.glob('*.lproj/Localizable.strings').map { |loc_file| File.basename(File.dirname(loc_file), '.lproj') }
|
|
116
|
+
end.sort
|
|
117
|
+
langs.select! { |lang| only_langs.include?(lang) } unless only_langs.nil?
|
|
118
|
+
|
|
119
|
+
config = {
|
|
120
|
+
'input_dir' => input_dir,
|
|
121
|
+
'output_dir' => output_dir,
|
|
122
|
+
'strings' => langs.map do |lang|
|
|
123
|
+
{
|
|
124
|
+
'inputs' => ["#{lang}.lproj/Localizable.strings"],
|
|
125
|
+
# Choose an unlikely separator (instead of the default '.') to avoid creating needlessly complex Stencil Context nested
|
|
126
|
+
# structure just because we have '.' in the English sentences we use (instead of structured reverse-dns notation) for the keys
|
|
127
|
+
'options' => { 'separator' => '____' },
|
|
128
|
+
'outputs' => [{
|
|
129
|
+
'templatePath' => template_path,
|
|
130
|
+
'output' => output_filename(lang)
|
|
131
|
+
}]
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Write SwiftGen config file
|
|
137
|
+
config_file = File.join(output_dir, CONFIG_FILE_NAME)
|
|
138
|
+
File.write(config_file, config.to_yaml)
|
|
139
|
+
|
|
140
|
+
return [config_file, langs]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Because we use English copy verbatim as key names, some keys are the same except for the upper/lowercase.
|
|
144
|
+
# We need to sort the output again because SwiftGen only sort case-insensitively so that means keys that are
|
|
145
|
+
# the same except case might be in swapped order for different outputs
|
|
146
|
+
#
|
|
147
|
+
# @param [String] dir The temporary directory in which the file to sort lines for is located
|
|
148
|
+
# @param [String] lang The code for the locale we need to sort the output lines for
|
|
149
|
+
#
|
|
150
|
+
def sort_file_lines!(dir, lang)
|
|
151
|
+
file = File.join(dir, output_filename(lang))
|
|
152
|
+
return nil unless File.exist?(file)
|
|
153
|
+
|
|
154
|
+
sorted_lines = File.readlines(file).sort
|
|
155
|
+
File.write(file, sorted_lines.join)
|
|
156
|
+
return file
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Prepares the template and config files, then run SwiftGen, run `diff` on each generated output against the baseline, and returns a Hash of the violations found.
|
|
160
|
+
#
|
|
161
|
+
# @param [String] input_dir The directory where the `.lproj` folders to scan are located
|
|
162
|
+
# @param [String] base_lang The base language used as source of truth that all other languages will be compared against
|
|
163
|
+
# @param [Array<String>] only_langs The list of languages to limit the generation for. Useful to focus only on a couple of issues or just one language
|
|
164
|
+
# @return [Hash<String, String>] A hash of violations, keyed by language code, whose values are the diff output.
|
|
165
|
+
#
|
|
166
|
+
# @note The returned Hash contains keys only for locales with violations. Locales parsed but without any violations found will not appear in the resulting hash.
|
|
167
|
+
#
|
|
168
|
+
def find_diffs(input_dir:, base_lang:, only_langs: nil)
|
|
169
|
+
Dir.mktmpdir('a8c-lint-translations-') do |tmpdir|
|
|
170
|
+
# Run SwiftGen
|
|
171
|
+
langs = only_langs.nil? ? nil : (only_langs + [base_lang]).uniq
|
|
172
|
+
(config_file, langs) = generate_swiftgen_config!(input_dir, tmpdir, only_langs: langs)
|
|
173
|
+
Action.sh(swiftgen_bin, 'config', 'run', '--config', config_file)
|
|
174
|
+
|
|
175
|
+
# Run diffs
|
|
176
|
+
base_file = sort_file_lines!(tmpdir, base_lang)
|
|
177
|
+
langs.delete(base_lang)
|
|
178
|
+
return Hash[langs.map do |lang|
|
|
179
|
+
file = sort_file_lines!(tmpdir, lang)
|
|
180
|
+
# If the lang ends up not having any translation at all (e.g. a `.lproj` without any `.strings` file in it but maybe just a storyboard or assets catalog), ignore it
|
|
181
|
+
next nil if file.nil? || only_empty_lines?(file)
|
|
182
|
+
|
|
183
|
+
# Compute the diff
|
|
184
|
+
diff = `diff -U0 "#{base_file}" "#{file}"`
|
|
185
|
+
# Remove the lines starting with `---`/`+++` which contains the file names (which are temp files we don't want to expose in the final diff to users)
|
|
186
|
+
# Note: We still keep the `@@ from-file-line-numbers to-file-line-numbers @@` lines to help the user know the index of the key to find it faster,
|
|
187
|
+
# and also to serve as a clear visual separator between diff entries in the output.
|
|
188
|
+
# Those numbers won't be matching the original `.strings` file line numbers because they are line numbers in the SwiftGen-generated intermediate
|
|
189
|
+
# file instead, but they can still give an indication at the index in the list of keys at which this difference is located.
|
|
190
|
+
diff.gsub!(/^(---|\+\+\+).*\n/, '')
|
|
191
|
+
diff.empty? ? nil : [lang, diff]
|
|
192
|
+
end.compact]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns true if the file only contains empty lines, i.e. lines that only contains whitespace (space, tab, CR, LF)
|
|
197
|
+
def only_empty_lines?(file)
|
|
198
|
+
File.open(file) do |f|
|
|
199
|
+
while (line = f.gets)
|
|
200
|
+
return false unless line.strip.empty?
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
return true
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
module Fastlane
|
|
2
|
+
module Helper
|
|
3
|
+
module Ios
|
|
4
|
+
# A module containing helper methods to manipulate/extract/bump iOS version strings in xcconfig files
|
|
5
|
+
#
|
|
6
|
+
module VersionHelper
|
|
7
|
+
# The index for the major version number part
|
|
8
|
+
MAJOR_NUMBER = 0
|
|
9
|
+
# The index for the minor version number part
|
|
10
|
+
MINOR_NUMBER = 1
|
|
11
|
+
# The index for the hotfix version number part
|
|
12
|
+
HOTFIX_NUMBER = 2
|
|
13
|
+
# The index for the build version number part
|
|
14
|
+
BUILD_NUMBER = 3
|
|
15
|
+
|
|
16
|
+
# Returns the public-facing version string.
|
|
17
|
+
#
|
|
18
|
+
# @return [String] The public-facing version number, extracted from the VERSION_LONG entry of the xcconfig file.
|
|
19
|
+
# - If this version is a hotfix (more than 2 parts and 3rd part is non-zero), returns the "X.Y.Z" formatted string
|
|
20
|
+
# - Otherwise (not a hotfix / 3rd part of version is 0), returns "X.Y" formatted version number
|
|
21
|
+
#
|
|
22
|
+
def self.get_public_version
|
|
23
|
+
version = get_build_version
|
|
24
|
+
vp = get_version_parts(version)
|
|
25
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}" unless is_hotfix?(version)
|
|
26
|
+
|
|
27
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Compute the name of the next release version.
|
|
31
|
+
#
|
|
32
|
+
# @param [String] version The current version that we want to increment
|
|
33
|
+
#
|
|
34
|
+
# @return [String] The predicted next version, in the form of "X.Y".
|
|
35
|
+
# Corresponds to incrementing the minor part, except if it reached 10
|
|
36
|
+
# (in that case we go to the next major version, as decided in our versioning conventions)
|
|
37
|
+
#
|
|
38
|
+
def self.calc_next_release_version(version)
|
|
39
|
+
vp = get_version_parts(version)
|
|
40
|
+
vp[MINOR_NUMBER] += 1
|
|
41
|
+
if vp[MINOR_NUMBER] == 10
|
|
42
|
+
vp[MAJOR_NUMBER] += 1
|
|
43
|
+
vp[MINOR_NUMBER] = 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Return the short version string "X.Y" from the full version.
|
|
50
|
+
#
|
|
51
|
+
# @param [String] version The version to convert to a short version
|
|
52
|
+
#
|
|
53
|
+
# @return [String] A version string consisting of only the first 2 parts "X.Y"
|
|
54
|
+
#
|
|
55
|
+
def self.get_short_version_string(version)
|
|
56
|
+
vp = get_version_parts(version)
|
|
57
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compute the name of the previous release version.
|
|
61
|
+
#
|
|
62
|
+
# @param [String] version The current version we want to decrement
|
|
63
|
+
#
|
|
64
|
+
# @return [String] The predicted previous version, in the form of "X.Y".
|
|
65
|
+
# Corresponds to decrementing the minor part, or decrement the major and set minor to 9 if minor was 0.
|
|
66
|
+
#
|
|
67
|
+
def self.calc_prev_release_version(version)
|
|
68
|
+
vp = get_version_parts(version)
|
|
69
|
+
if vp[MINOR_NUMBER] == 0
|
|
70
|
+
vp[MAJOR_NUMBER] -= 1
|
|
71
|
+
vp[MINOR_NUMBER] = 9
|
|
72
|
+
else
|
|
73
|
+
vp[MINOR_NUMBER] -= 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Compute the name of the next build version.
|
|
80
|
+
#
|
|
81
|
+
# @param [String] version The current version we want to increment
|
|
82
|
+
#
|
|
83
|
+
# @return [String] The predicted next build version, in the form of "X.Y.Z.N".
|
|
84
|
+
# Corresponds to incrementing the last (4th) component N of the version.
|
|
85
|
+
#
|
|
86
|
+
def self.calc_next_build_version(version)
|
|
87
|
+
vp = get_version_parts(version)
|
|
88
|
+
vp[BUILD_NUMBER] += 1
|
|
89
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}.#{vp[BUILD_NUMBER]}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Compute the name of the next hotfix version.
|
|
93
|
+
#
|
|
94
|
+
# @param [String] version The current version we want to increment
|
|
95
|
+
#
|
|
96
|
+
# @return [String] The predicted next hotfix version, in the form of "X.Y.Z".
|
|
97
|
+
# Corresponds to incrementing the 3rd component of the version.
|
|
98
|
+
#
|
|
99
|
+
def self.calc_next_hotfix_version(version)
|
|
100
|
+
vp = get_version_parts(version)
|
|
101
|
+
vp[HOTFIX_NUMBER] += 1
|
|
102
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Compute the name of the previous build version.
|
|
106
|
+
#
|
|
107
|
+
# @param [String] version The current version we want to decrement
|
|
108
|
+
#
|
|
109
|
+
# @return [String] The predicted previous build version, in the form of "X.Y.Z.N".
|
|
110
|
+
# Corresponds to decrementing the last (4th) component N of the version.
|
|
111
|
+
#
|
|
112
|
+
def self.calc_prev_build_version(version)
|
|
113
|
+
vp = get_version_parts(version)
|
|
114
|
+
vp[BUILD_NUMBER] -= 1 unless vp[BUILD_NUMBER] == 0
|
|
115
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}.#{vp[BUILD_NUMBER]}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Compute the name of the previous hotfix version.
|
|
119
|
+
#
|
|
120
|
+
# @param [String] version The current version we want to decrement
|
|
121
|
+
#
|
|
122
|
+
# @return [String] The predicted previous hotfix version, in the form of "X.Y.Z", or "X.Y" if Z is 0.
|
|
123
|
+
# Corresponds to decrementing the 3rd component Z of the version, striping it if it ends up being zero.
|
|
124
|
+
#
|
|
125
|
+
def self.calc_prev_hotfix_version(version)
|
|
126
|
+
vp = get_version_parts(version)
|
|
127
|
+
vp[HOTFIX_NUMBER] -= 1 unless vp[HOTFIX_NUMBER] == 0
|
|
128
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}" unless vp[HOTFIX_NUMBER] == 0
|
|
129
|
+
|
|
130
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Create an internal version number, for which the build number is based on today's date.
|
|
134
|
+
#
|
|
135
|
+
# @param [String] version The current version to create an internal version name for.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] The internal version, in the form of "X.Y.Z.YYYYMMDD".
|
|
138
|
+
#
|
|
139
|
+
def self.create_internal_version(version)
|
|
140
|
+
vp = get_version_parts(version)
|
|
141
|
+
d = DateTime.now
|
|
142
|
+
todayDate = d.strftime('%Y%m%d')
|
|
143
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}.#{todayDate}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Return the build number value incremented by one.
|
|
147
|
+
#
|
|
148
|
+
# @param [String|Int|nil] build_number The build number to increment
|
|
149
|
+
#
|
|
150
|
+
# @return [Int] The incremented build number, or 0 if it was `nil`.
|
|
151
|
+
#
|
|
152
|
+
def self.bump_build_number(build_number)
|
|
153
|
+
build_number.nil? ? 0 : build_number.to_i + 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Determines if a version number corresponds to a hotfix
|
|
157
|
+
#
|
|
158
|
+
# @param [String] version The version number to test
|
|
159
|
+
#
|
|
160
|
+
# @return [Bool] True if the version number has a non-zero 3rd component, meaning that it is a hotfix version.
|
|
161
|
+
#
|
|
162
|
+
def self.is_hotfix?(version)
|
|
163
|
+
vp = get_version_parts(version)
|
|
164
|
+
return (vp.length > 2) && (vp[HOTFIX_NUMBER] != 0)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns the current value of the `VERSION_LONG` key from the public xcconfig file
|
|
168
|
+
#
|
|
169
|
+
# @return [String] The current version according to the public xcconfig file.
|
|
170
|
+
#
|
|
171
|
+
def self.get_build_version
|
|
172
|
+
versions = get_version_strings()[0]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns the current value of the `VERSION_LONG` key from the internal xcconfig file
|
|
176
|
+
#
|
|
177
|
+
# @return [String] The current version according to the internal xcconfig file.
|
|
178
|
+
#
|
|
179
|
+
def self.get_internal_version
|
|
180
|
+
get_version_strings()[1]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Prints the current and next release version numbers to stdout, then return the next release version
|
|
184
|
+
#
|
|
185
|
+
# @return [String] The next release version to use after bumping the currently used public version.
|
|
186
|
+
#
|
|
187
|
+
def self.bump_version_release
|
|
188
|
+
# Bump release
|
|
189
|
+
current_version = get_public_version()
|
|
190
|
+
UI.message("Current version: #{current_version}")
|
|
191
|
+
new_version = calc_next_release_version(current_version)
|
|
192
|
+
UI.message("New version: #{new_version}")
|
|
193
|
+
verified_version = verify_version(new_version)
|
|
194
|
+
|
|
195
|
+
return verified_version
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Updates the `app_version` entry in the `Deliverfile`
|
|
199
|
+
#
|
|
200
|
+
# @param [String] new_version The new value to set the `app_version` entry to.
|
|
201
|
+
# @raise [UserError] If the Deliverfile was not found.
|
|
202
|
+
#
|
|
203
|
+
def self.update_fastlane_deliver(new_version)
|
|
204
|
+
fd_file = './fastlane/Deliverfile'
|
|
205
|
+
if File.exist?(fd_file)
|
|
206
|
+
Action.sh("sed -i '' \"s/app_version.*/app_version \\\"#{new_version}\\\"/\" #{fd_file}")
|
|
207
|
+
else
|
|
208
|
+
UI.user_error!("Can't find #{fd_file}.")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Update the `.xcconfig` files (the public one, and the internal one if it exists) with the new version strings.
|
|
213
|
+
#
|
|
214
|
+
# @env PUBLIC_CONFIG_FILE The path to the xcconfig file containing the public version numbers.
|
|
215
|
+
# @env INTERNAL_CONFIG_FILE The path to the xcconfig file containing the internal version numbers. Can be nil.
|
|
216
|
+
#
|
|
217
|
+
# @param [String] new_version The new version number to use as `VERSION_LONG` for the public xcconfig file
|
|
218
|
+
# @param [String] new_version_short The new version number to use for `VERSION_SHORT` (for both public and internal xcconfig files)
|
|
219
|
+
# @param [String] internal_version The new version number to use as `VERSION_LONG` for the interrnal xcconfig file, if it exists
|
|
220
|
+
#
|
|
221
|
+
def self.update_xc_configs(new_version, new_version_short, internal_version)
|
|
222
|
+
update_xc_config(ENV['PUBLIC_CONFIG_FILE'], new_version, new_version_short)
|
|
223
|
+
update_xc_config(ENV['INTERNAL_CONFIG_FILE'], internal_version, new_version_short) unless ENV['INTERNAL_CONFIG_FILE'].nil?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Updates an xcconfig file with new values for VERSION_SHORT and VERSION_LONG entries.
|
|
227
|
+
# Also bumps the BUILD_NUMBER value from that config file if there is one present.
|
|
228
|
+
#
|
|
229
|
+
# @param [String] file_path The path to the xcconfig file
|
|
230
|
+
# @param [String] new_version The new version number to use for VERSION_LONG
|
|
231
|
+
# @param [String] new_version_short The new version number to use for VERSION_SHORT
|
|
232
|
+
# @raise [UserError] If the xcconfig file was not found
|
|
233
|
+
#
|
|
234
|
+
def self.update_xc_config(file_path, new_version, new_version_short)
|
|
235
|
+
if File.exist?(file_path)
|
|
236
|
+
UI.message("Updating #{file_path} to version #{new_version_short}/#{new_version}")
|
|
237
|
+
Action.sh("sed -i '' \"$(awk '/^VERSION_SHORT/{ print NR; exit }' \"#{file_path}\")s/=.*/=#{new_version_short}/\" \"#{file_path}\"")
|
|
238
|
+
Action.sh("sed -i '' \"$(awk '/^VERSION_LONG/{ print NR; exit }' \"#{file_path}\")s/=.*/=#{new_version}/\" \"#{file_path}\"")
|
|
239
|
+
|
|
240
|
+
build_number = read_build_number_from_config_file(file_path)
|
|
241
|
+
unless build_number.nil?
|
|
242
|
+
new_build_number = bump_build_number(build_number)
|
|
243
|
+
Action.sh("sed -i '' \"$(awk '/^BUILD_NUMBER/{ print NR; exit }' \"#{file_path}\")s/=.*/=#{new_build_number}/\" \"#{file_path}\"")
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
UI.user_error!("#{file_path} not found")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#----------------------------------------
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
# Split a version string into its 4 parts, ensuring its parts count is valid
|
|
254
|
+
#
|
|
255
|
+
# @param [String] version The version string to split into parts
|
|
256
|
+
# @return [Array<String>] An array of exactly 4 elements, containing each part of the version string.
|
|
257
|
+
# @note If the original version string contains less than 4 parts, the returned array is filled with zeros at the end to always contain 4 items.
|
|
258
|
+
# @raise [UserError] Interrupts the lane if the provided version contains _more_ than 4 parts
|
|
259
|
+
#
|
|
260
|
+
def self.get_version_parts(version)
|
|
261
|
+
parts = version.split('.')
|
|
262
|
+
parts = parts.fill('0', parts.length...4).map { |chr| chr.to_i }
|
|
263
|
+
UI.user_error!("Bad version string: #{version}") if parts.length > 4
|
|
264
|
+
|
|
265
|
+
return parts
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Extract the VERSION_LONG entry from an `xcconfig` file
|
|
269
|
+
#
|
|
270
|
+
# @param [String] filePath The path to the `.xcconfig` file to read the value from
|
|
271
|
+
# @return [String] The long version found in said xcconfig file, or nil if not found
|
|
272
|
+
#
|
|
273
|
+
def self.read_long_version_from_config_file(filePath)
|
|
274
|
+
read_from_config_file('VERSION_LONG', filePath)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Extract the BUILD_NUMBER entry from an `xcconfig` file
|
|
278
|
+
#
|
|
279
|
+
# @param [String] filePath The path to the `.xcconfig` file to read the value from
|
|
280
|
+
# @return [String] The build number found in said xcconfig file, or nil if not found
|
|
281
|
+
#
|
|
282
|
+
def self.read_build_number_from_config_file(filePath)
|
|
283
|
+
read_from_config_file('BUILD_NUMBER', filePath)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Read the value of a given key from an `.xcconfig` file.
|
|
287
|
+
#
|
|
288
|
+
# @param [String] key The xcconfig key to get the value for
|
|
289
|
+
# @param [String] filePath The path to the `.xcconfig` file to read the value from
|
|
290
|
+
#
|
|
291
|
+
# @return [String] The value for the given key, or `nil` if the key was not found.
|
|
292
|
+
#
|
|
293
|
+
def self.read_from_config_file(key, filePath)
|
|
294
|
+
File.open(filePath, 'r') do |f|
|
|
295
|
+
f.each_line do |line|
|
|
296
|
+
line = line.strip()
|
|
297
|
+
return line.split('=')[1] if line.start_with?("#{key}=")
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
return nil
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Read the version numbers from the xcconfig file
|
|
305
|
+
#
|
|
306
|
+
# @env PUBLIC_CONFIG_FILE The path to the xcconfig file containing the public version numbers.
|
|
307
|
+
# @env INTERNAL_CONFIG_FILE The path to the xcconfig file containing the internal version numbers. Can be nil.
|
|
308
|
+
#
|
|
309
|
+
# @return [String] Array of long version strings found.
|
|
310
|
+
# The first element is always present and contains the version extracted from the public config file
|
|
311
|
+
# The second element is the version extracted from the internal config file, only present if one was provided.
|
|
312
|
+
def self.get_version_strings
|
|
313
|
+
version_strings = []
|
|
314
|
+
version_strings << read_long_version_from_config_file(ENV['PUBLIC_CONFIG_FILE'])
|
|
315
|
+
version_strings << read_long_version_from_config_file(ENV['INTERNAL_CONFIG_FILE']) unless ENV['INTERNAL_CONFIG_FILE'].nil?
|
|
316
|
+
|
|
317
|
+
return version_strings
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Ensure that the version provided is only composed of number parts and return the validated string
|
|
321
|
+
#
|
|
322
|
+
# @param [String] version The version string to validate
|
|
323
|
+
# @return [String] The version string, re-validated as being a string of the form `X.Y.Z.T`
|
|
324
|
+
# @raise [UserError] Interrupts the lane with a user_error! if the version contains non-numberic parts
|
|
325
|
+
#
|
|
326
|
+
def self.verify_version(version)
|
|
327
|
+
v_parts = get_version_parts(version)
|
|
328
|
+
|
|
329
|
+
v_parts.each do |part|
|
|
330
|
+
UI.user_error!('Version value can only contains numbers.') unless is_int?(part)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
"#{v_parts[MAJOR_NUMBER]}.#{v_parts[MINOR_NUMBER]}.#{v_parts[HOTFIX_NUMBER]}.#{v_parts[BUILD_NUMBER]}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Check if a string is an integer
|
|
337
|
+
#
|
|
338
|
+
# @param [String] string The string to test
|
|
339
|
+
#
|
|
340
|
+
# @return [Bool] true if the string is representing an integer value, false if not
|
|
341
|
+
#
|
|
342
|
+
def self.is_int? string
|
|
343
|
+
true if Integer(string) rescue false
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|