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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +339 -0
  3. data/README.md +38 -0
  4. data/bin/drawText +19 -0
  5. data/ext/drawText/extconf.rb +36 -0
  6. data/lib/fastlane/plugin/wpmreleasetoolkit.rb +16 -0
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/README.md +20 -0
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +53 -0
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +171 -0
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +63 -0
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +103 -0
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +83 -0
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +54 -0
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +69 -0
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +58 -0
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +77 -0
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +89 -0
  18. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +79 -0
  19. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +68 -0
  20. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +63 -0
  21. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +44 -0
  22. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +79 -0
  23. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +115 -0
  24. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +71 -0
  25. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +44 -0
  26. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +44 -0
  27. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +44 -0
  28. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotifx_prechecks.rb +78 -0
  29. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +106 -0
  30. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +51 -0
  31. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +52 -0
  32. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +56 -0
  33. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +63 -0
  34. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +56 -0
  35. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +59 -0
  36. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +91 -0
  37. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +89 -0
  38. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +64 -0
  39. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +90 -0
  40. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +170 -0
  41. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +247 -0
  42. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +57 -0
  43. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +56 -0
  44. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +81 -0
  45. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +96 -0
  46. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +139 -0
  47. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +57 -0
  48. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +86 -0
  49. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +139 -0
  50. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +134 -0
  51. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +77 -0
  52. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +79 -0
  53. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +92 -0
  54. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +74 -0
  55. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +78 -0
  56. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +68 -0
  57. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +87 -0
  58. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +114 -0
  59. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +62 -0
  60. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +60 -0
  61. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +70 -0
  62. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +63 -0
  63. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +40 -0
  64. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +52 -0
  65. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +64 -0
  66. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +47 -0
  67. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +60 -0
  68. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +121 -0
  69. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotifx_prechecks.rb +78 -0
  70. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +167 -0
  71. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +44 -0
  72. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +93 -0
  73. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +44 -0
  74. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +40 -0
  75. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +81 -0
  76. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +56 -0
  77. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +46 -0
  78. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb +152 -0
  79. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +44 -0
  80. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +359 -0
  81. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb +475 -0
  82. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb +91 -0
  83. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +282 -0
  84. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb +51 -0
  85. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb +93 -0
  86. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +224 -0
  87. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +135 -0
  88. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb +74 -0
  89. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +80 -0
  90. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +208 -0
  91. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb +348 -0
  92. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +107 -0
  93. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb +182 -0
  94. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +399 -0
  95. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +21 -0
  96. data/lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb +40 -0
  97. data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +86 -0
  98. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +5 -0
  99. 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