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,359 @@
|
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require 'open-uri'
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?('UI')
|
|
8
|
+
|
|
9
|
+
module Helper
|
|
10
|
+
module Android
|
|
11
|
+
module LocalizeHelper
|
|
12
|
+
# Checks if string_line has the content_override flag set
|
|
13
|
+
def self.skip_string_by_tag(string_line)
|
|
14
|
+
skip = string_line.attr('content_override') == 'true' unless string_line.attr('content_override').nil?
|
|
15
|
+
if skip
|
|
16
|
+
puts " - Skipping #{string_line.attr('name')} string"
|
|
17
|
+
return true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Checks if string_name is in the excluesion list
|
|
24
|
+
def self.skip_string_by_exclusion_list(library, string_name)
|
|
25
|
+
return false unless library.key?(:exclusions)
|
|
26
|
+
|
|
27
|
+
skip = library[:exclusions].include?(string_name)
|
|
28
|
+
if skip
|
|
29
|
+
puts " - Skipping #{string_name} string"
|
|
30
|
+
return true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Merge string_line into main_string
|
|
35
|
+
def self.merge_string(main_strings, library, string_line)
|
|
36
|
+
string_name = string_line.attr('name')
|
|
37
|
+
string_content = string_line.content
|
|
38
|
+
|
|
39
|
+
# Skip strings in the exclusions list
|
|
40
|
+
return :skipped if skip_string_by_exclusion_list(library, string_name)
|
|
41
|
+
|
|
42
|
+
# Search for the string in the main file
|
|
43
|
+
result = :added
|
|
44
|
+
main_strings.xpath('//string').each do |this_string|
|
|
45
|
+
if this_string.attr('name') == string_name
|
|
46
|
+
# Skip if the string has the content_override tag
|
|
47
|
+
return :skipped if skip_string_by_tag(this_string)
|
|
48
|
+
|
|
49
|
+
# If nodes are equivalent, skip
|
|
50
|
+
return :found if string_line =~ this_string
|
|
51
|
+
|
|
52
|
+
# The string needs an update
|
|
53
|
+
result = :updated
|
|
54
|
+
if this_string.attr('tools:ignore').nil?
|
|
55
|
+
# It can be updated, so remove the current one and move ahead
|
|
56
|
+
this_string.remove
|
|
57
|
+
break
|
|
58
|
+
else
|
|
59
|
+
# It has the tools:ignore flag, so update the content without touching the other attributes
|
|
60
|
+
this_string.content = string_content
|
|
61
|
+
return result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# String not found, or removed because needing update and not in the exclusion list: add to the main file
|
|
67
|
+
main_strings.xpath('//string').last().add_next_sibling("\n#{' ' * 4}#{string_line.to_xml().strip}")
|
|
68
|
+
return result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Verify a string
|
|
72
|
+
def self.verify_string(main_strings, library, string_line)
|
|
73
|
+
string_name = string_line.attr('name')
|
|
74
|
+
string_content = string_line.content
|
|
75
|
+
|
|
76
|
+
# Skip strings in the exclusions list
|
|
77
|
+
return if skip_string_by_exclusion_list(library, string_name)
|
|
78
|
+
|
|
79
|
+
# Search for the string in the main file
|
|
80
|
+
main_strings.xpath('//string').each do |this_string|
|
|
81
|
+
if this_string.attr('name') == string_name
|
|
82
|
+
# Skip if the string has the content_override tag
|
|
83
|
+
return if skip_string_by_tag(this_string)
|
|
84
|
+
|
|
85
|
+
# Update if needed
|
|
86
|
+
UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if this_string.content != string_content
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# String not found and not in the exclusion list:
|
|
92
|
+
UI.user_error!("String #{string_name} [#{string_content}] was found in library #{library[:library]} but not in the main file.")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.merge_lib(main, library)
|
|
96
|
+
UI.message("Merging #{library[:library]} strings into #{main}")
|
|
97
|
+
main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
|
98
|
+
lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
|
99
|
+
|
|
100
|
+
updated_count = 0
|
|
101
|
+
untouched_count = 0
|
|
102
|
+
added_count = 0
|
|
103
|
+
skipped_count = 0
|
|
104
|
+
lib_strings.xpath('//string').each do |string_line|
|
|
105
|
+
res = merge_string(main_strings, library, string_line)
|
|
106
|
+
case res
|
|
107
|
+
when :updated
|
|
108
|
+
puts "#{string_line.attr('name')} updated."
|
|
109
|
+
updated_count = updated_count + 1
|
|
110
|
+
when :found
|
|
111
|
+
untouched_count = untouched_count + 1
|
|
112
|
+
when :added
|
|
113
|
+
puts "#{string_line.attr('name')} added."
|
|
114
|
+
added_count = added_count + 1
|
|
115
|
+
when :skipped
|
|
116
|
+
skipped_count = skipped_count + 1
|
|
117
|
+
else
|
|
118
|
+
UI.user_error!("Internal Error! #{res}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
File.open(main, 'w:UTF-8') do |f|
|
|
123
|
+
f.write(main_strings.to_xml(indent: 4))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
UI.message("Done (#{added_count} added, #{updated_count} updated, #{untouched_count} untouched, #{skipped_count} skipped).")
|
|
127
|
+
return (added_count + updated_count) != 0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.verify_diff(diff_string, main_strings, lib_strings, library)
|
|
131
|
+
if diff_string.start_with?('name=')
|
|
132
|
+
diff_string.slice!('name="')
|
|
133
|
+
|
|
134
|
+
end_index = diff_string.index('"')
|
|
135
|
+
end_index ||= diff_string.length # Use the whole string if there's no '"'
|
|
136
|
+
|
|
137
|
+
diff_string = diff_string.slice(0..(end_index - 1))
|
|
138
|
+
|
|
139
|
+
lib_strings.xpath('//string').each do |string_line|
|
|
140
|
+
res = verify_string(main_strings, library, string_line) if string_line.attr('name') == diff_string
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.verify_lib(main, library, source_diff)
|
|
146
|
+
UI.message("Checking #{library[:library]} strings vs #{main}")
|
|
147
|
+
main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
|
148
|
+
lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
|
149
|
+
|
|
150
|
+
verify_local_diff(main, library, main_strings, lib_strings)
|
|
151
|
+
verify_pr_diff(main, library, main_strings, lib_strings, source_diff) unless source_diff.nil?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.verify_local_diff(main, library, main_strings, lib_strings)
|
|
155
|
+
`git diff #{main}`.each_line do |line|
|
|
156
|
+
if line.start_with?('+ ') || line.start_with?('- ')
|
|
157
|
+
diffs = line.gsub(/\s+/m, ' ').strip.split(' ')
|
|
158
|
+
diffs.each do |diff|
|
|
159
|
+
verify_diff(diff, main_strings, lib_strings, library)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.verify_pr_diff(main, library, main_strings, lib_strings, source_diff)
|
|
166
|
+
source_diff.each_line do |line|
|
|
167
|
+
if line.start_with?('+ ') || line.start_with?('- ')
|
|
168
|
+
diffs = line.gsub(/\s+/m, ' ').strip.split(' ')
|
|
169
|
+
diffs.each do |diff|
|
|
170
|
+
verify_diff(diff, main_strings, lib_strings, library)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
########
|
|
177
|
+
# @!group Downloading translations from GlotPress
|
|
178
|
+
########
|
|
179
|
+
|
|
180
|
+
# Create the `available_languages.xml` file.
|
|
181
|
+
#
|
|
182
|
+
# @param [String] res_dir The relative path to the `…/src/main/res` directory
|
|
183
|
+
# @param [Array<String>] locale_codes The list of locale codes to include in the available_languages.xml file
|
|
184
|
+
#
|
|
185
|
+
def self.create_available_languages_file(res_dir:, locale_codes:)
|
|
186
|
+
doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
|
187
|
+
xml.comment('Warning: Auto-generated file, do not edit.')
|
|
188
|
+
xml.resources do
|
|
189
|
+
xml.send(:'string-array', name: 'available_languages', translatable: 'false') do
|
|
190
|
+
locale_codes.each { |code| xml.item(code.gsub('-r', '_')) }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
File.write(File.join(res_dir, 'values', 'available_languages.xml'), doc.to_xml)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Download translations from GlotPress
|
|
198
|
+
#
|
|
199
|
+
# @param [String] res_dir The relative path to the `…/src/main/res` directory.
|
|
200
|
+
# @param [String] glotpress_project_url The base URL to the glotpress project to download the strings from.
|
|
201
|
+
# @param [Hash{String=>String}, Array] glotpress_filters
|
|
202
|
+
# The filters to apply when exporting strings from GlotPress.
|
|
203
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
|
204
|
+
# If an array of Hashes is provided instead of a single Hash, this method will perform as many
|
|
205
|
+
# export requests as items in this array, then merge all the results – useful for OR-ing multiple filters.
|
|
206
|
+
# @param [Array<Hash{Symbol=>String}>] locales_map
|
|
207
|
+
# An array of locales to download. Each item in the array must be a Hash
|
|
208
|
+
# with keys `:glotpress` and `:android` containing the respective locale codes.
|
|
209
|
+
#
|
|
210
|
+
def self.download_from_glotpress(res_dir:, glotpress_project_url:, glotpress_filters: { status: 'current' }, locales_map:)
|
|
211
|
+
glotpress_filters = [glotpress_filters] unless glotpress_filters.is_a?(Array)
|
|
212
|
+
|
|
213
|
+
attributes_to_copy = %w[formatted] # Attributes that we want to replicate into translated `string.xml` files
|
|
214
|
+
orig_file = File.join(res_dir, 'values', 'strings.xml')
|
|
215
|
+
orig_xml = File.open(orig_file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
|
|
216
|
+
orig_attributes = orig_xml.xpath('//string').map { |tag| [tag['name'], tag.attributes.select { |k, _| attributes_to_copy.include?(k) }] }.to_h
|
|
217
|
+
|
|
218
|
+
locales_map.each do |lang_codes|
|
|
219
|
+
all_xml_documents = glotpress_filters.map do |filters|
|
|
220
|
+
UI.message "Downloading translations for '#{lang_codes[:android]}' from GlotPress (#{lang_codes[:glotpress]}) [#{filters}]..."
|
|
221
|
+
download_glotpress_export_file(project_url: glotpress_project_url, locale: lang_codes[:glotpress], filters: filters)
|
|
222
|
+
end.compact
|
|
223
|
+
next if all_xml_documents.empty?
|
|
224
|
+
|
|
225
|
+
# Merge all XMLs together
|
|
226
|
+
merged_xml = merge_xml_documents(all_xml_documents)
|
|
227
|
+
|
|
228
|
+
# Process XML (text substitutions, replicate attributes, quick-lint string)
|
|
229
|
+
merged_xml.xpath('//string').each do |string_tag|
|
|
230
|
+
apply_substitutions(string_tag)
|
|
231
|
+
orig_attributes[string_tag['name']]&.each { |k, v| string_tag[k] = v }
|
|
232
|
+
quick_lint(string_tag, lang_codes[:android])
|
|
233
|
+
end
|
|
234
|
+
merged_xml.xpath('//string-array/item').each { |item_tag| apply_substitutions(item_tag) }
|
|
235
|
+
|
|
236
|
+
# Save
|
|
237
|
+
lang_dir = File.join(res_dir, "values-#{lang_codes[:android]}")
|
|
238
|
+
FileUtils.mkdir(lang_dir) unless Dir.exist?(lang_dir)
|
|
239
|
+
lang_file = File.join(lang_dir, 'strings.xml')
|
|
240
|
+
File.open(lang_file, 'w') { |f| merged_xml.write_to(f, encoding: Encoding::UTF_8.to_s, indent: 4) }
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
#####################
|
|
245
|
+
# Private Helpers
|
|
246
|
+
#####################
|
|
247
|
+
|
|
248
|
+
# Downloads the export from GlotPress for a given locale and given filters
|
|
249
|
+
#
|
|
250
|
+
# @param [String] project_url The URL to the GlotPress project to export from.
|
|
251
|
+
# @param [String] locale The GlotPress locale code to download strings for.
|
|
252
|
+
# @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
|
|
253
|
+
# Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
|
|
254
|
+
# @return [Nokogiri::XML] the download XML document, parsed as a Nokogiri::XML object
|
|
255
|
+
#
|
|
256
|
+
def self.download_glotpress_export_file(project_url:, locale:, filters:)
|
|
257
|
+
query_params = filters.transform_keys { |k| "filters[#{k}]" }.merge(format: 'android')
|
|
258
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations?#{URI.encode_www_form(query_params)}")
|
|
259
|
+
begin
|
|
260
|
+
uri.open { |f| Nokogiri::XML(f.read.gsub("\t", ' '), nil, Encoding::UTF_8.to_s) }
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
UI.error "Error downloading #{locale} - #{e.message}"
|
|
263
|
+
return nil
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
private_class_method :download_glotpress_export_file
|
|
267
|
+
|
|
268
|
+
# Merge multiple Nokogiri::XML `strings.xml` documents together
|
|
269
|
+
#
|
|
270
|
+
# @param [Array<Nokogiri::XML::Document>] all_xmls Array of the Nokogiri XML documents to merge together
|
|
271
|
+
# @return [Nokogiri::XML::Document] The merged document.
|
|
272
|
+
#
|
|
273
|
+
# @note The first document in the array is used as starting point. Then string/resources from other docs are merged into it.
|
|
274
|
+
# If a string/resource with a given `name` is present in multiple documents, the node of the last one wins.
|
|
275
|
+
#
|
|
276
|
+
def self.merge_xml_documents(all_xmls)
|
|
277
|
+
return all_xmls.first if all_xmls.count <= 1
|
|
278
|
+
|
|
279
|
+
merged_xml = all_xmls.first.dup # Use the first XML as starting point
|
|
280
|
+
resources_node = merged_xml.xpath('/resources').first
|
|
281
|
+
# For each other XML, find all the nodes with a name attribute, and merge them in
|
|
282
|
+
all_xmls.drop(1).each do |other_xml|
|
|
283
|
+
other_xml.xpath('/resources/*[@name]').each do |other_node|
|
|
284
|
+
existing_node = merged_xml.xpath("//#{other_node.name}[@name='#{other_node['name']}']").first
|
|
285
|
+
if existing_node.nil?
|
|
286
|
+
resources_node << ' ' << other_node << "\n"
|
|
287
|
+
else
|
|
288
|
+
existing_node.replace(other_node)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
merged_xml
|
|
293
|
+
end
|
|
294
|
+
private_class_method :merge_xml_documents
|
|
295
|
+
|
|
296
|
+
# Apply some common text substitutions to tag contents
|
|
297
|
+
#
|
|
298
|
+
# @param [Nokogiri::XML::Node] tag The XML tag/node to apply substitutions to
|
|
299
|
+
#
|
|
300
|
+
def self.apply_substitutions(tag)
|
|
301
|
+
tag.content = tag.content.gsub('...', '…')
|
|
302
|
+
end
|
|
303
|
+
private_class_method :apply_substitutions
|
|
304
|
+
|
|
305
|
+
# Perform some quick basic checks about an individual `<string>` tag and print warnings accordingly
|
|
306
|
+
#
|
|
307
|
+
# @param [Nokogiri::XML::Node] string_tag The XML tag/node to check
|
|
308
|
+
# @param [String] lang The language we are currently processing. Used for providing context during logging / warning message
|
|
309
|
+
#
|
|
310
|
+
def self.quick_lint(string_tag, lang)
|
|
311
|
+
if string_tag['formatted'] == 'false' && string_tag.content.include?('%%')
|
|
312
|
+
UI.important "Warning: [#{lang}] translation for '#{string_tag['name']}' has attribute formatted=false, but still contains escaped '%%' in translation."
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
private_class_method :quick_lint
|
|
316
|
+
|
|
317
|
+
# @!endgroup
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Source: https://stackoverflow.com/questions/7825258/determine-if-two-nokogiri-nodes-are-equivalent?rq=1
|
|
324
|
+
# There may be better solutions now that Ruby supports canonicalization.
|
|
325
|
+
module Nokogiri
|
|
326
|
+
module XML
|
|
327
|
+
class Node
|
|
328
|
+
# Return true if this node is content-equivalent to other, false otherwise
|
|
329
|
+
def =~(other)
|
|
330
|
+
return true if self == other
|
|
331
|
+
return false unless name == other.name
|
|
332
|
+
|
|
333
|
+
stype = node_type
|
|
334
|
+
otype = other.node_type
|
|
335
|
+
return false unless stype == otype
|
|
336
|
+
|
|
337
|
+
sa = attributes
|
|
338
|
+
oa = other.attributes
|
|
339
|
+
return false unless sa.length == oa.length
|
|
340
|
+
|
|
341
|
+
sa = sa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
|
|
342
|
+
oa = oa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
|
|
343
|
+
return false unless sa == oa
|
|
344
|
+
|
|
345
|
+
skids = children
|
|
346
|
+
okids = other.children
|
|
347
|
+
return false unless skids.length == okids.length
|
|
348
|
+
return false if stype == TEXT_NODE && (content != other.content)
|
|
349
|
+
|
|
350
|
+
sns = namespace
|
|
351
|
+
ons = other.namespace
|
|
352
|
+
return false if !sns ^ !ons
|
|
353
|
+
return false if sns && (sns.href != ons.href)
|
|
354
|
+
|
|
355
|
+
skids.to_enum.with_index.all? { |ski, i| ski =~ okids[i] }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
module Fastlane
|
|
2
|
+
module Helper
|
|
3
|
+
module Android
|
|
4
|
+
# A module containing helper methods to manipulate/extract/bump Android version strings in gradle files
|
|
5
|
+
#
|
|
6
|
+
module VersionHelper
|
|
7
|
+
# The key used in internal version Hash objects to hold the versionName value
|
|
8
|
+
VERSION_NAME = 'name'
|
|
9
|
+
# The key used in internal version Hash objects to hold the versionCode value
|
|
10
|
+
VERSION_CODE = 'code'
|
|
11
|
+
# The index for the major version number part
|
|
12
|
+
MAJOR_NUMBER = 0
|
|
13
|
+
# The index for the minor version number part
|
|
14
|
+
MINOR_NUMBER = 1
|
|
15
|
+
# The index for the hotfix version number part
|
|
16
|
+
HOTFIX_NUMBER = 2
|
|
17
|
+
# The prefix used in front of the versionName for alpha versions
|
|
18
|
+
ALPHA_PREFIX = 'alpha-'
|
|
19
|
+
# The suffix used in the versionName for RC (beta) versions
|
|
20
|
+
RC_SUFFIX = '-rc'
|
|
21
|
+
|
|
22
|
+
# Returns the public-facing version string.
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# "1.2" # Assuming build.gradle contains versionName "1.2"
|
|
26
|
+
# "1.2" # Assuming build.gradle contains versionName "1.2.0"
|
|
27
|
+
# "1.2.3" # Assuming build.gradle contains versionName "1.2.3"
|
|
28
|
+
#
|
|
29
|
+
# @return [String] The public-facing version number, extracted from the `versionName` of the `build.gradle` file.
|
|
30
|
+
# - If this version is a hotfix (more than 2 parts and 3rd part is non-zero), returns the "X.Y.Z" formatted string
|
|
31
|
+
# - Otherwise (not a hotfix / 3rd part of version is 0), returns "X.Y" formatted version number
|
|
32
|
+
#
|
|
33
|
+
def self.get_public_version
|
|
34
|
+
version = get_release_version
|
|
35
|
+
vp = get_version_parts(version[VERSION_NAME])
|
|
36
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}" unless is_hotfix?(version)
|
|
37
|
+
|
|
38
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Extract the version name and code from the `vanilla` flavor of the `$PROJECT_NAME/build.gradle file`
|
|
42
|
+
# or for the defaultConfig if `HAS_ALPHA_VERSION` is not defined.
|
|
43
|
+
#
|
|
44
|
+
# @env HAS_ALPHA_VERSION If set (with any value), indicates that the project uses `vanilla` flavor.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash] A hash with 2 keys "name" and "code" containing the extracted version name and code, respectively
|
|
47
|
+
#
|
|
48
|
+
def self.get_release_version
|
|
49
|
+
section = ENV['HAS_ALPHA_VERSION'].nil? ? 'defaultConfig' : 'vanilla {'
|
|
50
|
+
gradle_path = self.gradle_path
|
|
51
|
+
name = get_version_name_from_gradle_file(gradle_path, section)
|
|
52
|
+
code = get_version_build_from_gradle_file(gradle_path, section)
|
|
53
|
+
return { VERSION_NAME => name, VERSION_CODE => code }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Extract the version name and code from the `defaultConfig` of the `$PROJECT_NAME/build.gradle` file
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] A hash with 2 keys `"name"` and `"code"` containing the extracted version name and code, respectively,
|
|
59
|
+
# or `nil` if `$HAS_ALPHA_VERSION` is not defined.
|
|
60
|
+
#
|
|
61
|
+
def self.get_alpha_version
|
|
62
|
+
return nil if ENV['HAS_ALPHA_VERSION'].nil?
|
|
63
|
+
|
|
64
|
+
section = 'defaultConfig'
|
|
65
|
+
gradle_path = self.gradle_path
|
|
66
|
+
name = get_version_name_from_gradle_file(gradle_path, section)
|
|
67
|
+
code = get_version_build_from_gradle_file(gradle_path, section)
|
|
68
|
+
return { VERSION_NAME => name, VERSION_CODE => code }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Determines if a version name corresponds to an alpha version (starts with `"alpha-"`` prefix)
|
|
72
|
+
#
|
|
73
|
+
# @param [String] version The version name to check
|
|
74
|
+
#
|
|
75
|
+
# @return [Bool] true if the version name starts with the `ALPHA_PREFIX`, false otherwise.
|
|
76
|
+
#
|
|
77
|
+
# @private
|
|
78
|
+
#
|
|
79
|
+
def self.is_alpha_version?(version)
|
|
80
|
+
version[VERSION_NAME].start_with?(ALPHA_PREFIX)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if this versionName corresponds to a beta, i.e. contains some `-rc` suffix
|
|
84
|
+
#
|
|
85
|
+
# @param [String] version The versionName string to check for
|
|
86
|
+
#
|
|
87
|
+
# @return [Bool] True if the version string contains `-rc`, indicating it is a beta version.
|
|
88
|
+
#
|
|
89
|
+
def self.is_beta_version?(version)
|
|
90
|
+
version[VERSION_NAME].include?(RC_SUFFIX)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the version name and code to use for the final release.
|
|
94
|
+
#
|
|
95
|
+
# - The final version name corresponds to the beta's versionName, without the `-rc` suffix
|
|
96
|
+
# - The final version code corresponds to the versionCode for the alpha (or for the beta if alpha_version is nil) incremented by one.
|
|
97
|
+
#
|
|
98
|
+
# @param [Hash] beta_version The version hash for the beta (vanilla flavor), containing values for keys "name" and "code"
|
|
99
|
+
# @param [Hash] alpha_version The version hash for the alpha (defaultConfig), containing values for keys "name" and "code",
|
|
100
|
+
# or `nil` if no alpha version to consider.
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] A version hash with keys "name" and "code", containing the version name and code to use for final release.
|
|
103
|
+
#
|
|
104
|
+
def self.calc_final_release_version(beta_version, alpha_version)
|
|
105
|
+
version_name = beta_version[VERSION_NAME].split('-')[0]
|
|
106
|
+
version_code = alpha_version.nil? ? beta_version[VERSION_CODE] + 1 : alpha_version[VERSION_CODE] + 1
|
|
107
|
+
|
|
108
|
+
{ VERSION_NAME => version_name, VERSION_CODE => version_code }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns the version name and code to use for the next alpha.
|
|
112
|
+
#
|
|
113
|
+
# - The next version name corresponds to the `alpha_version`'s name incremented by one (alpha-42 => alpha-43)
|
|
114
|
+
# - The next version code corresponds to the `version`'s code incremented by one.
|
|
115
|
+
#
|
|
116
|
+
# @param [Hash] version The version hash for the current beta or release, containing values for keys "name" and "code"
|
|
117
|
+
# @param [Hash] alpha_version The version hash for the current alpha (defaultConfig), containing values for keys "name" and "code"
|
|
118
|
+
#
|
|
119
|
+
# @return [Hash] A version hash with keys "name" and "code", containing the version name and code to use for final release.
|
|
120
|
+
#
|
|
121
|
+
def self.calc_next_alpha_version(version, alpha_version)
|
|
122
|
+
# Bump alpha name
|
|
123
|
+
alpha_number = alpha_version[VERSION_NAME].sub(ALPHA_PREFIX, '')
|
|
124
|
+
alpha_name = "#{ALPHA_PREFIX}#{alpha_number.to_i() + 1}"
|
|
125
|
+
|
|
126
|
+
# Bump alpha code
|
|
127
|
+
alpha_code = version[VERSION_CODE] + 1
|
|
128
|
+
|
|
129
|
+
{ VERSION_NAME => alpha_name, VERSION_CODE => alpha_code }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Compute the version name and code to use for the next beta (`X.Y.Z-rc-N`).
|
|
133
|
+
#
|
|
134
|
+
# - The next version name corresponds to the `version`'s name with the value after the `-rc-` suffix incremented by one,
|
|
135
|
+
# or with `-rc-1` added if there was no previous rc suffix (if `version` was not a beta but a release)
|
|
136
|
+
# - The next version code corresponds to the `alpha_version`'s (or `version`'s if `alpha_version` is nil) code, incremented by one.
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# calc_next_beta_version({"name": "1.2.3", "code": 456}) #=> {"name": "1.2.3-rc-1", "code": 457}
|
|
140
|
+
# calc_next_beta_version({"name": "1.2.3-rc-2", "code": 456}) #=> {"name": "1.2.3-rc-3", "code": 457}
|
|
141
|
+
# calc_next_beta_version({"name": "1.2.3", "code": 456}, {"name": "alpha-1.2.3", "code": 457}) #=> {"name": "1.2.3-rc-1", "code": 458}
|
|
142
|
+
#
|
|
143
|
+
# @param [Hash] version The version hash for the current beta or release, containing values for keys "name" and "code"
|
|
144
|
+
# @param [Hash] alpha_version The version hash for the alpha, containing values for keys "name" and "code",
|
|
145
|
+
# or `nil` if no alpha version to consider.
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash] A hash with keys `"name"` and `"code"` containing the next beta version name and code.
|
|
148
|
+
#
|
|
149
|
+
def self.calc_next_beta_version(version, alpha_version = nil)
|
|
150
|
+
# Bump version name
|
|
151
|
+
beta_number = is_beta_version?(version) ? version[VERSION_NAME].split('-')[2].to_i + 1 : 1
|
|
152
|
+
version_name = "#{version[VERSION_NAME].split('-')[0]}#{RC_SUFFIX}-#{beta_number}"
|
|
153
|
+
|
|
154
|
+
# Bump version code
|
|
155
|
+
version_code = alpha_version.nil? ? version[VERSION_CODE] + 1 : alpha_version[VERSION_CODE] + 1
|
|
156
|
+
{ VERSION_NAME => version_name, VERSION_CODE => version_code }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Compute the version name to use for the next release (`"X.Y"`).
|
|
160
|
+
#
|
|
161
|
+
# @param [String] version The version name (string) to increment
|
|
162
|
+
#
|
|
163
|
+
# @return [String] The version name for the next release
|
|
164
|
+
#
|
|
165
|
+
def self.calc_next_release_short_version(version)
|
|
166
|
+
v = self.calc_next_release_base_version(VERSION_NAME => version, VERSION_CODE => nil)
|
|
167
|
+
return v[VERSION_NAME]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Compute the next release version name for the given version, without incrementing the version code
|
|
171
|
+
#
|
|
172
|
+
# - The version name sees its minor version part incremented by one (and carried to next major if it reaches 10)
|
|
173
|
+
# - The version code is unchanged. This method is intended to be called internally by other methods taking care of the version code bump.
|
|
174
|
+
#
|
|
175
|
+
# @param [Hash] version A version hash, with keys `"name"` and `"code"`, containing the version to increment
|
|
176
|
+
#
|
|
177
|
+
# @return [Hash] Hash containing the next release version name ("X.Y") and code.
|
|
178
|
+
#
|
|
179
|
+
def self.calc_next_release_base_version(version)
|
|
180
|
+
version_name = remove_beta_suffix(version[VERSION_NAME])
|
|
181
|
+
vp = get_version_parts(version_name)
|
|
182
|
+
vp[MINOR_NUMBER] += 1
|
|
183
|
+
if vp[MINOR_NUMBER] == 10
|
|
184
|
+
vp[MAJOR_NUMBER] += 1
|
|
185
|
+
vp[MINOR_NUMBER] = 0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
{ VERSION_NAME => "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}", VERSION_CODE => version[VERSION_CODE] }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Compute the name of the next version to use after code freeze, by incrementing the current version name and making it a `-rc-1`
|
|
192
|
+
#
|
|
193
|
+
# @example
|
|
194
|
+
# calc_next_release_version({"name": "1.2", "code": 456}) #=> {"name":"1.3-rc-1", "code": 457}
|
|
195
|
+
# calc_next_release_version({"name": "1.2.3", "code": 456}) #=> {"name":"1.3-rc-1", "code": 457}
|
|
196
|
+
# calc_next_release_version({"name": "1.2", "code": 456}, {"name":"alpha-1.2", "code": 457}) #=> {"name":"1.3-rc-1", "code": 458}
|
|
197
|
+
#
|
|
198
|
+
# @param [Hash] version The current version hash, with keys `"name"` and `"code"`
|
|
199
|
+
# @param [Hash] alpha_version The current alpha version hash, with keys `"name"` and `"code"`, or nil if no alpha version
|
|
200
|
+
#
|
|
201
|
+
# @return [Hash] The hash containing the version name and code to use after release cut
|
|
202
|
+
#
|
|
203
|
+
def self.calc_next_release_version(version, alpha_version = nil)
|
|
204
|
+
nv = calc_next_release_base_version(VERSION_NAME => version[VERSION_NAME], VERSION_CODE => alpha_version.nil? ? version[VERSION_CODE] : [version[VERSION_CODE], alpha_version[VERSION_CODE]].max)
|
|
205
|
+
calc_next_beta_version(nv)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Compute the name and code of the next hotfix version.
|
|
209
|
+
#
|
|
210
|
+
# @param [String] hotfix_version_name The next version name we want for the hotfix
|
|
211
|
+
# @param [String] hotfix_version_code The next version code we want for the hotfix
|
|
212
|
+
#
|
|
213
|
+
# @return [Hash] The predicted next hotfix version, as a Hash containing the keys `"name"` and `"code"`
|
|
214
|
+
#
|
|
215
|
+
def self.calc_next_hotfix_version(hotfix_version_name, hotfix_version_code)
|
|
216
|
+
{ VERSION_NAME => hotfix_version_name, VERSION_CODE => hotfix_version_code }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Compute the name of the previous release version, by decrementing the minor version number
|
|
220
|
+
#
|
|
221
|
+
# @example
|
|
222
|
+
# calc_prev_release_version("1.2") => "1.1"
|
|
223
|
+
# calc_prev_release_version("1.2.3") => "1.1"
|
|
224
|
+
# calc_prev_release_version("3.0") => "2.9"
|
|
225
|
+
#
|
|
226
|
+
# @param [String] version The version string to decrement
|
|
227
|
+
#
|
|
228
|
+
# @return [String] A 2-parts version string "X.Y" corresponding to the guessed previous release version.
|
|
229
|
+
#
|
|
230
|
+
def self.calc_prev_release_version(version)
|
|
231
|
+
vp = get_version_parts(version)
|
|
232
|
+
if vp[MINOR_NUMBER] == 0
|
|
233
|
+
vp[MAJOR_NUMBER] -= 1
|
|
234
|
+
vp[MINOR_NUMBER] = 9
|
|
235
|
+
else
|
|
236
|
+
vp[MINOR_NUMBER] -= 1
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Determines if a version name corresponds to a hotfix
|
|
243
|
+
#
|
|
244
|
+
# @param [String] version The version number to test
|
|
245
|
+
#
|
|
246
|
+
# @return [Bool] True if the version number has a non-zero 3rd component, meaning that it is a hotfix version.
|
|
247
|
+
#
|
|
248
|
+
def self.is_hotfix?(version)
|
|
249
|
+
return false if is_alpha_version?(version)
|
|
250
|
+
|
|
251
|
+
vp = get_version_parts(version[VERSION_NAME])
|
|
252
|
+
return (vp.length > 2) && (vp[HOTFIX_NUMBER] != 0)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Prints the current and next release version names to stdout, then returns the next release version
|
|
256
|
+
#
|
|
257
|
+
# @return [String] The next release version name to use after bumping the currently used release version.
|
|
258
|
+
#
|
|
259
|
+
def self.bump_version_release
|
|
260
|
+
# Bump release
|
|
261
|
+
current_version = get_release_version()
|
|
262
|
+
UI.message("Current version: #{current_version[VERSION_NAME]}")
|
|
263
|
+
new_version = calc_next_release_base_version(current_version)
|
|
264
|
+
UI.message("New version: #{new_version[VERSION_NAME]}")
|
|
265
|
+
verified_version = verify_version(new_version[VERSION_NAME])
|
|
266
|
+
|
|
267
|
+
return verified_version
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Update the `build.gradle` file with new `versionName` and `versionCode` values, both or the `defaultConfig` and `vanilla` flavors
|
|
271
|
+
#
|
|
272
|
+
# @param [Hash] new_version_beta The version hash for the beta (vanilla flavor), containing values for keys "name" and "code"
|
|
273
|
+
# @param [Hash] new_version_alpha The version hash for the alpha (defaultConfig), containing values for keys "name" and "code"
|
|
274
|
+
# @env HAS_ALPHA_VERSION If set (with any value), indicates that the project uses `vanilla` flavor.
|
|
275
|
+
#
|
|
276
|
+
def self.update_versions(new_version_beta, new_version_alpha)
|
|
277
|
+
self.update_version(new_version_beta, ENV['HAS_ALPHA_VERSION'].nil? ? 'defaultConfig' : 'vanilla {')
|
|
278
|
+
self.update_version(new_version_alpha, 'defaultConfig') unless new_version_alpha.nil?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Compute the name of the previous hotfix version.
|
|
282
|
+
#
|
|
283
|
+
# @param [String] version_name The current version name we want to decrement
|
|
284
|
+
#
|
|
285
|
+
# @return [String] The predicted previous hotfix version, in the form of "X.Y.Z", or "X.Y" if Z is 0.
|
|
286
|
+
# Corresponds to decrementing the 3rd component Z of the version, stripping it if it ends up being zero.
|
|
287
|
+
#
|
|
288
|
+
def self.calc_prev_hotfix_version_name(version_name)
|
|
289
|
+
vp = get_version_parts(version_name)
|
|
290
|
+
vp[HOTFIX_NUMBER] -= 1 unless vp[HOTFIX_NUMBER] == 0
|
|
291
|
+
return "#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}.#{vp[HOTFIX_NUMBER]}" unless vp[HOTFIX_NUMBER] == 0
|
|
292
|
+
|
|
293
|
+
"#{vp[MAJOR_NUMBER]}.#{vp[MINOR_NUMBER]}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Extract the value of a import key from build.gradle
|
|
297
|
+
#
|
|
298
|
+
# @param [String] import_key The key to look for
|
|
299
|
+
# @return [String] The value of the key, or nil if not found
|
|
300
|
+
#
|
|
301
|
+
def self.get_library_version_from_gradle_config(import_key:)
|
|
302
|
+
gradle_file_path = File.join(ENV['PROJECT_ROOT_FOLDER'] || '.', 'build.gradle')
|
|
303
|
+
|
|
304
|
+
return nil unless File.exist?(gradle_file_path)
|
|
305
|
+
|
|
306
|
+
File.open(gradle_file_path, 'r') do |f|
|
|
307
|
+
text = f.read
|
|
308
|
+
text.match(/^\s*(?:\w*\.)?#{Regexp.escape(import_key)}\s*=\s*['"](.*?)["']/m)&.captures&.first
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
#----------------------------------------
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
# Remove the beta suffix (part after the `-`) from a version string
|
|
316
|
+
#
|
|
317
|
+
# @param [String] version The version string to remove the suffix from
|
|
318
|
+
#
|
|
319
|
+
# @return [String] The part of the version string without the beta suffix, i.e. the part before the first dash.
|
|
320
|
+
#
|
|
321
|
+
# @example remove_beta_suffix("1.2.3-rc.4") => "1.2.3"
|
|
322
|
+
#
|
|
323
|
+
def self.remove_beta_suffix(version)
|
|
324
|
+
version.split('-')[0]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Split a version string into its individual integer parts
|
|
328
|
+
#
|
|
329
|
+
# @param [String] version The version string to split, e.g. "1.2.3.4"
|
|
330
|
+
#
|
|
331
|
+
# @return [Array<Int>] An array of integers containing the individual integer parts of the version.
|
|
332
|
+
# Always contains 3 items at minimum (0 are added to the end if the original string contains less than 3 parts)
|
|
333
|
+
#
|
|
334
|
+
def self.get_version_parts(version)
|
|
335
|
+
parts = version.split('.').map(&:to_i)
|
|
336
|
+
parts.fill(0, parts.length...3) # add 0 if needed to ensure array has at least 3 components
|
|
337
|
+
return parts
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Extract the versionName from a build.gradle file
|
|
341
|
+
#
|
|
342
|
+
# @param [String] file_path The path to the `.gradle` file
|
|
343
|
+
# @param [String] section The name of the section we expect the keyword to be in, e.g. "defaultConfig" or "vanilla"
|
|
344
|
+
#
|
|
345
|
+
# @return [String] The value of the versionName attribute as found in the build.gradle file and for this section.
|
|
346
|
+
#
|
|
347
|
+
def self.get_version_name_from_gradle_file(file_path, section)
|
|
348
|
+
res = get_keyword_from_gradle_file(file_path, section, 'versionName')
|
|
349
|
+
res = res.tr('\"', '') unless res.nil?
|
|
350
|
+
return res
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Extract the versionCode rom a build.gradle file
|
|
354
|
+
#
|
|
355
|
+
# @param [String] file_path The path to the `.gradle` file
|
|
356
|
+
# @param [String] section The name of the section we expect the keyword to be in, e.g. "defaultConfig" or "vanilla"
|
|
357
|
+
#
|
|
358
|
+
# @return [String] The value of the versionCode attribute as found in the build.gradle file and for this section.
|
|
359
|
+
#
|
|
360
|
+
def self.get_version_build_from_gradle_file(file_path, section)
|
|
361
|
+
res = get_keyword_from_gradle_file(file_path, section, 'versionCode')
|
|
362
|
+
return res.to_i
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Extract the value for a specific keyword in a specific section of a `.gradle` file
|
|
366
|
+
#
|
|
367
|
+
# @todo: This implementation is very fragile. This should be done parsing the file in a proper way.
|
|
368
|
+
# Leveraging gradle itself is probably the easiest way.
|
|
369
|
+
#
|
|
370
|
+
# @param [String] file_path The path of the `.gradle` file to extract the value from
|
|
371
|
+
# @param [String] section The name of the section from which we want to extract this keyword from. For example `defaultConfig` or `myFlavor`
|
|
372
|
+
# @param [String] keyword The keyword (key name) we want the value for
|
|
373
|
+
#
|
|
374
|
+
# @return [String] Returns the value for that keyword in the section of the `.gradle` file, or nil if not found.
|
|
375
|
+
#
|
|
376
|
+
def self.get_keyword_from_gradle_file(file_path, section, keyword)
|
|
377
|
+
found_section = false
|
|
378
|
+
File.open(file_path, 'r') do |file|
|
|
379
|
+
file.each_line do |line|
|
|
380
|
+
if !found_section
|
|
381
|
+
found_section = true if line.include?(section)
|
|
382
|
+
else
|
|
383
|
+
return line.split(' ')[1] if line.include?(keyword) && !line.include?("\"#{keyword}\"") && !line.include?("P#{keyword}")
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
return nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Ensure that a version string is correctly formatted (that is, each of its parts is a number) and returns the 2-parts version number
|
|
391
|
+
#
|
|
392
|
+
# @param [String] version The version string to verify
|
|
393
|
+
#
|
|
394
|
+
# @return [String] The "major.minor" version string, only with the first 2 components
|
|
395
|
+
# @raise [UserError] If any of the parts of the version string is not a number
|
|
396
|
+
#
|
|
397
|
+
def self.verify_version(version)
|
|
398
|
+
v_parts = get_version_parts(version)
|
|
399
|
+
|
|
400
|
+
v_parts.each do |part|
|
|
401
|
+
UI.user_error!('Version value can only contains numbers.') unless is_int?(part)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
"#{v_parts[MAJOR_NUMBER]}.#{v_parts[MINOR_NUMBER]}"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Check if a string is an integer.
|
|
408
|
+
#
|
|
409
|
+
# @param [String] string The string to test
|
|
410
|
+
#
|
|
411
|
+
# @return [Bool] true if the string is representing an integer value, false if not
|
|
412
|
+
#
|
|
413
|
+
def self.is_int? string
|
|
414
|
+
true if Integer(string) rescue false
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# The path to the build.gradle file for the project.
|
|
418
|
+
#
|
|
419
|
+
# @env PROJECT_ROOT_FOLDER The path to the root of the project (the folder containing the `.git` directory).
|
|
420
|
+
# @env PROJECT_NAME The name of the project, i.e. the name of the subdirectory containing the project's `build.gradle` file.
|
|
421
|
+
#
|
|
422
|
+
# @return [String] The path of the `build.gradle` file inside the project subfolder in the project's repo
|
|
423
|
+
#
|
|
424
|
+
def self.gradle_path
|
|
425
|
+
UI.user_error!("You need to set the \`PROJECT_ROOT_FOLDER\` environment variable to the path to the project's root") if ENV['PROJECT_ROOT_FOLDER'].nil?
|
|
426
|
+
UI.user_error!("You need to set the \`PROJECT_NAME\` environment variable to the relative path to the project subfolder name") if ENV['PROJECT_NAME'].nil?
|
|
427
|
+
File.join(ENV['PROJECT_ROOT_FOLDER'], ENV['PROJECT_NAME'], 'build.gradle')
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Update both the versionName and versionCode of the build.gradle file to the specified version.
|
|
431
|
+
#
|
|
432
|
+
# @param [Hash] version The version hash, containing values for keys "name" and "code"
|
|
433
|
+
# @param [String] section The name of the section to update in the build.gradle file, e.g. "defaultConfig" or "vanilla"
|
|
434
|
+
#
|
|
435
|
+
# @todo This implementation is very fragile. This should be done parsing the file in a proper way.
|
|
436
|
+
# Leveraging gradle itself is probably the easiest way.
|
|
437
|
+
#
|
|
438
|
+
def self.update_version(version, section)
|
|
439
|
+
gradle_path = self.gradle_path
|
|
440
|
+
temp_file = Tempfile.new('fastlaneIncrementVersion')
|
|
441
|
+
found_section = false
|
|
442
|
+
version_updated = 0
|
|
443
|
+
File.open(gradle_path, 'r') do |file|
|
|
444
|
+
file.each_line do |line|
|
|
445
|
+
if !found_section
|
|
446
|
+
temp_file.puts line
|
|
447
|
+
found_section = true if line.include? section
|
|
448
|
+
else
|
|
449
|
+
if version_updated < 2
|
|
450
|
+
if line.include?('versionName') && !line.include?('"versionName"') && !line.include?('PversionName')
|
|
451
|
+
version_name = line.split(' ')[1].tr('\"', '')
|
|
452
|
+
line.sub!(version_name, version[VERSION_NAME].to_s)
|
|
453
|
+
version_updated = version_updated + 1
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
if line.include? 'versionCode'
|
|
457
|
+
version_code = line.split(' ')[1]
|
|
458
|
+
line.sub!(version_code, version[VERSION_CODE].to_s)
|
|
459
|
+
version_updated = version_updated + 1
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
temp_file.puts line
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
file.close
|
|
466
|
+
end
|
|
467
|
+
temp_file.rewind
|
|
468
|
+
temp_file.close
|
|
469
|
+
FileUtils.mv(temp_file.path, gradle_path)
|
|
470
|
+
temp_file.unlink
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|