fastlane 2.233.1 → 2.235.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 +4 -4
- data/README.md +105 -104
- data/bin/fastlane +2 -2
- data/deliver/lib/deliver/app_clip_header_image.rb +18 -0
- data/deliver/lib/deliver/commands_generator.rb +17 -0
- data/deliver/lib/deliver/download_app_clip_header_images.rb +64 -0
- data/deliver/lib/deliver/languages.rb +36 -3
- data/deliver/lib/deliver/loader.rb +41 -0
- data/deliver/lib/deliver/options.rb +23 -0
- data/deliver/lib/deliver/runner.rb +8 -0
- data/deliver/lib/deliver/upload_app_clip_default_experience_header_images.rb +188 -0
- data/deliver/lib/deliver/upload_app_clip_default_experience_metadata.rb +229 -0
- data/deliver/lib/deliver/upload_metadata.rb +71 -7
- data/deliver/lib/deliver/upload_price_tier.rb +4 -2
- data/deliver/lib/deliver/upload_screenshots.rb +1 -1
- data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +2 -2
- data/fastlane/lib/fastlane/actions/appium.rb +1 -1
- data/fastlane/lib/fastlane/actions/appledoc.rb +1 -1
- data/fastlane/lib/fastlane/actions/create_app_on_managed_play_store.rb +2 -2
- data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +100 -0
- data/fastlane/lib/fastlane/actions/gcovr.rb +1 -1
- data/fastlane/lib/fastlane/actions/get_version_number.rb +2 -12
- data/fastlane/lib/fastlane/actions/increment_version_number.rb +33 -0
- data/fastlane/lib/fastlane/actions/install_on_device.rb +1 -1
- data/fastlane/lib/fastlane/actions/ipa.rb +2 -2
- data/fastlane/lib/fastlane/actions/lcov.rb +1 -1
- data/fastlane/lib/fastlane/actions/notarize.rb +1 -148
- data/fastlane/lib/fastlane/actions/oclint.rb +1 -1
- data/fastlane/lib/fastlane/actions/slack.rb +9 -3
- data/fastlane/lib/fastlane/actions/sonar.rb +1 -1
- data/fastlane/lib/fastlane/actions/sourcedocs.rb +1 -1
- data/fastlane/lib/fastlane/actions/swiftlint.rb +1 -1
- data/fastlane/lib/fastlane/actions/testfairy.rb +1 -3
- data/fastlane/lib/fastlane/actions/validate_play_store_json_key.rb +4 -4
- data/fastlane/lib/fastlane/actions/xcodebuild.rb +1 -1
- data/fastlane/lib/fastlane/actions/xctool.rb +1 -1
- data/fastlane/lib/fastlane/documentation/markdown_docs_generator.rb +3 -1
- data/fastlane/lib/fastlane/helper/xcodebuild_formatter_helper.rb +1 -1
- data/fastlane/lib/fastlane/helper/xcodeproj_helper.rb +155 -0
- data/fastlane/lib/fastlane/helper/xcodes_helper.rb +1 -1
- data/fastlane/lib/fastlane/notification/slack.rb +9 -4
- data/fastlane/lib/fastlane/plugins/template/%gem_name%.gemspec.erb +1 -1
- data/fastlane/lib/fastlane/plugins/template/.rubocop.yml +2 -1
- data/fastlane/lib/fastlane/swift_runner_upgrader.rb +1 -1
- data/fastlane/lib/fastlane/version.rb +2 -2
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +36 -1
- data/fastlane/swift/Fastlane.swift +109 -29
- data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.pbxproj +4 -4
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +1 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane_core/lib/fastlane_core/clipboard.rb +1 -1
- data/fastlane_core/lib/fastlane_core/command_executor.rb +13 -5
- data/fastlane_core/lib/fastlane_core/helper.rb +14 -1
- data/fastlane_core/lib/fastlane_core/languages.rb +1 -1
- data/frameit/lib/frameit/dependency_checker.rb +1 -1
- data/frameit/lib/frameit/device_types.rb +18 -0
- data/frameit/lib/frameit/editor.rb +2 -2
- data/frameit/lib/frameit/offsets.rb +3 -2
- data/internal/README.md +11 -0
- data/match/lib/match/nuke.rb +60 -40
- data/match/lib/match/storage/git_storage.rb +1 -1
- data/pilot/lib/pilot/build_manager.rb +69 -2
- data/pilot/lib/pilot/options.rb +23 -0
- data/produce/lib/produce/available_default_languages.rb +12 -1
- data/scan/lib/scan/detect_values.rb +5 -0
- data/screengrab/lib/screengrab/reports_generator.rb +2 -2
- data/screengrab/lib/screengrab/runner.rb +14 -15
- data/sigh/lib/assets/resign.sh +6 -10
- data/sigh/lib/sigh/resign.rb +2 -2
- data/snapshot/lib/snapshot/reports_generator.rb +9 -2
- data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher.rb +1 -1
- data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher_base.rb +9 -9
- data/spaceship/lib/assets/languageMapping.json +66 -0
- data/spaceship/lib/spaceship/connect_api/models/app.rb +14 -1
- data/spaceship/lib/spaceship/connect_api/models/app_clip.rb +18 -0
- data/spaceship/lib/spaceship/connect_api/models/app_clip_app_store_review_detail.rb +34 -0
- data/spaceship/lib/spaceship/connect_api/models/app_clip_default_experience.rb +43 -0
- data/spaceship/lib/spaceship/connect_api/models/app_clip_default_experience_localization.rb +49 -0
- data/spaceship/lib/spaceship/connect_api/models/app_clip_header_image.rb +159 -0
- data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +5 -1
- data/spaceship/lib/spaceship/connect_api/models/beta_app_clip_invocation.rb +44 -0
- data/spaceship/lib/spaceship/connect_api/models/beta_app_clip_invocation_localization.rb +35 -0
- data/spaceship/lib/spaceship/connect_api/models/build_bundle.rb +1 -1
- data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +291 -0
- data/spaceship/lib/spaceship/connect_api.rb +7 -0
- data/spaceship/lib/spaceship/portal/portal_client.rb +1 -1
- data/spaceship/lib/spaceship/portal/provisioning_profile.rb +63 -25
- data/spaceship/lib/spaceship/two_step_or_factor_client.rb +2 -1
- data/supply/lib/supply/client.rb +75 -62
- data/supply/lib/supply/options.rb +2 -2
- data/supply/lib/supply/reader.rb +16 -0
- data/supply/lib/supply/uploader.rb +1 -1
- metadata +53 -41
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
require 'fastlane_core'
|
|
2
|
+
require 'spaceship/tunes/tunes'
|
|
3
|
+
require 'digest/md5'
|
|
4
|
+
|
|
5
|
+
require_relative 'module'
|
|
6
|
+
require_relative 'loader'
|
|
7
|
+
|
|
8
|
+
module Deliver
|
|
9
|
+
class UploadAppClipDefaultExperienceHeaderImages
|
|
10
|
+
UploadAppClipHeaderImageJob = Struct.new(:path, :localization)
|
|
11
|
+
|
|
12
|
+
def find_and_upload(options)
|
|
13
|
+
return if options[:edit_live] || options[:app_clip_header_images_path].nil?
|
|
14
|
+
|
|
15
|
+
app_clip_header_images = collect_app_clip_header_images(options)
|
|
16
|
+
|
|
17
|
+
app = Deliver.cache[:app]
|
|
18
|
+
|
|
19
|
+
platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
|
|
20
|
+
version = app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
|
|
21
|
+
UI.user_error!("Could not find a version to edit for app '#{app.name}' for '#{platform}'") unless version
|
|
22
|
+
|
|
23
|
+
app_clip_default_experience = version.app_clip_default_experience
|
|
24
|
+
UI.user_error!("Could not find a default app clip experience for version '#{version}'. Use the :app_clip_default_experience_subtitle and :app_clip_default_experience_action options to create and add metadata to the app clip default experience for this version.") unless app_clip_default_experience
|
|
25
|
+
|
|
26
|
+
UI.important("Will begin uploading app clip default experience header images for '#{version.version_string}' on App Store Connect")
|
|
27
|
+
UI.message("Starting with the upload of app clip header images...")
|
|
28
|
+
|
|
29
|
+
upload(app_clip_default_experience, app_clip_header_images)
|
|
30
|
+
|
|
31
|
+
UI.success("Successfully uploaded app clip default experience header images to App Store Connect")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def upload(app_clip_default_experience, app_clip_header_images)
|
|
35
|
+
# get the existing localizations and their header images
|
|
36
|
+
app_clip_default_experience_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience.id, includes: 'appClipHeaderImage')
|
|
37
|
+
|
|
38
|
+
# Create missing localizations for languages that have header images but no localization
|
|
39
|
+
app_clip_header_images.each do |header_image|
|
|
40
|
+
# Skip if language is nil or empty
|
|
41
|
+
next if header_image.language.nil? || header_image.language.empty?
|
|
42
|
+
|
|
43
|
+
existing_localization = app_clip_default_experience_localizations.find { |l| l.locale.eql?(header_image.language) }
|
|
44
|
+
next if existing_localization
|
|
45
|
+
|
|
46
|
+
UI.message("Creating app clip default experience localization for '#{header_image.language}'")
|
|
47
|
+
new_localization = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.create(
|
|
48
|
+
default_experience_id: app_clip_default_experience.id,
|
|
49
|
+
attributes: { locale: header_image.language }
|
|
50
|
+
)
|
|
51
|
+
app_clip_default_experience_localizations << new_localization
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Upload app clip header images
|
|
55
|
+
worker = FastlaneCore::QueueWorker.new do |job|
|
|
56
|
+
begin
|
|
57
|
+
localization = job.localization
|
|
58
|
+
|
|
59
|
+
# if there's an existing header image, it must be deleted before uploading the new one
|
|
60
|
+
unless localization.app_clip_header_image.nil?
|
|
61
|
+
localization.app_clip_header_image.delete!
|
|
62
|
+
UI.verbose("[#{localization.locale}] Removed existing header image")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
UI.verbose("[#{localization.locale}] Uploading '#{job.path}'...")
|
|
66
|
+
start_time = Time.now
|
|
67
|
+
Spaceship::ConnectAPI::AppClipHeaderImage.create(app_clip_default_experience_localization_id: localization.id, path: job.path, wait_for_processing: false)
|
|
68
|
+
UI.message("Uploaded '#{job.path}'... (#{Time.now - start_time} secs)")
|
|
69
|
+
rescue => error
|
|
70
|
+
UI.error(error)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
app_clip_header_images.each do |header_image|
|
|
75
|
+
localization = app_clip_default_experience_localizations.find { |l| l.locale.eql?(header_image.language) }
|
|
76
|
+
unless localization
|
|
77
|
+
UI.error("Could not find or create localization for #{header_image.language}")
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# check to see if it's already uploaded
|
|
82
|
+
checksum = UploadAppClipDefaultExperienceHeaderImages.calculate_checksum(header_image.path)
|
|
83
|
+
if !localization.app_clip_header_image.nil? && checksum.eql?(localization.app_clip_header_image.source_file_checksum)
|
|
84
|
+
UI.message("Skipping '#{header_image.path}' as it is already uploaded")
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# upload
|
|
89
|
+
worker.enqueue(UploadAppClipHeaderImageJob.new(header_image.path, localization))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
worker.start
|
|
93
|
+
|
|
94
|
+
UI.verbose('Uploading jobs are completed')
|
|
95
|
+
|
|
96
|
+
Helper.show_loading_indicator("Waiting for all the app clip header images to finish being processed...")
|
|
97
|
+
wait_for_complete(app_clip_default_experience.id)
|
|
98
|
+
Helper.hide_loading_indicator
|
|
99
|
+
|
|
100
|
+
UI.message("Successfully uploaded all app clip header images")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Verify all screenshots have been processed
|
|
104
|
+
# Functionality copied and modified from upload_screenshots.rb
|
|
105
|
+
def wait_for_complete(app_clip_default_experience_id)
|
|
106
|
+
loop do
|
|
107
|
+
# fetch
|
|
108
|
+
app_clip_default_experience_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience_id, includes: 'appClipHeaderImage')
|
|
109
|
+
header_images = app_clip_default_experience_localizations.map(&:app_clip_header_image)
|
|
110
|
+
|
|
111
|
+
# group states
|
|
112
|
+
states = header_images.each_with_object({}) do |header_image, hash|
|
|
113
|
+
next unless header_image
|
|
114
|
+
|
|
115
|
+
state = header_image.asset_delivery_state['state']
|
|
116
|
+
hash[state] ||= 0
|
|
117
|
+
hash[state] += 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0
|
|
121
|
+
return states unless is_processing
|
|
122
|
+
|
|
123
|
+
UI.verbose("There are still incomplete app clip header images - #{states}")
|
|
124
|
+
sleep(5)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def collect_app_clip_header_images(options)
|
|
129
|
+
app_clip_header_images = Loader.load_app_clip_header_images(options[:app_clip_header_images_path], options[:ignore_language_directory_validation])
|
|
130
|
+
|
|
131
|
+
# Apply default folder logic similar to metadata
|
|
132
|
+
assign_default_images(options, app_clip_header_images)
|
|
133
|
+
|
|
134
|
+
return app_clip_header_images
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# If the user has a 'default' language folder, assign those images to languages that don't have images
|
|
138
|
+
def assign_default_images(options, app_clip_header_images)
|
|
139
|
+
# Build a complete list of the required languages
|
|
140
|
+
enabled_languages = detect_languages(options, app_clip_header_images)
|
|
141
|
+
|
|
142
|
+
# Check if there's a default image (from 'default' folder)
|
|
143
|
+
default_image = app_clip_header_images.find do |img|
|
|
144
|
+
folder_name = File.basename(File.dirname(img.path))
|
|
145
|
+
folder_name.casecmp?("default")
|
|
146
|
+
end
|
|
147
|
+
return unless default_image
|
|
148
|
+
|
|
149
|
+
# For each enabled language, if there's no image, use the default
|
|
150
|
+
enabled_languages.each do |language|
|
|
151
|
+
next if language&.casecmp?("default")
|
|
152
|
+
|
|
153
|
+
existing_image = app_clip_header_images.find { |img| img.language.eql?(language) }
|
|
154
|
+
unless existing_image
|
|
155
|
+
UI.message("Using default folder image for language '#{language}'")
|
|
156
|
+
app_clip_header_images << Deliver::AppClipHeaderImage.new(default_image.path, language)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Remove the default image from the list (language is nil for default folder)
|
|
161
|
+
app_clip_header_images.reject! { |img| img.language.nil? }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def detect_languages(options, app_clip_header_images)
|
|
165
|
+
# Start with languages from the common detection method
|
|
166
|
+
enabled_languages = Languages.detect_languages(
|
|
167
|
+
options: options,
|
|
168
|
+
metadata_path: options[:app_clip_header_images_path],
|
|
169
|
+
ignore_validation: options[:ignore_language_directory_validation]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Also add languages from existing header images
|
|
173
|
+
app_clip_header_images.each do |header_image|
|
|
174
|
+
language = header_image.language
|
|
175
|
+
next if language.nil? || language.empty?
|
|
176
|
+
enabled_languages << language unless enabled_languages.include?(language)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
enabled_languages.uniq
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# helper method to mock this step in tests
|
|
183
|
+
def self.calculate_checksum(path)
|
|
184
|
+
bytes = File.binread(path)
|
|
185
|
+
Digest::MD5.hexdigest(bytes)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
require 'fastlane_core'
|
|
2
|
+
require 'spaceship'
|
|
3
|
+
|
|
4
|
+
require_relative 'module'
|
|
5
|
+
|
|
6
|
+
module Deliver
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
|
+
class UploadAppClipDefaultExperienceMetadata
|
|
9
|
+
LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES = {
|
|
10
|
+
app_clip_default_experience_subtitle: "app_clip_default_experience_subtitle"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
NON_LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES = {
|
|
14
|
+
app_clip_default_experience_action: "app_clip_default_experience_action"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
require_relative 'loader'
|
|
18
|
+
|
|
19
|
+
def upload_metadata(options)
|
|
20
|
+
# app clip default experience metadata is not editable in a live version
|
|
21
|
+
return if options[:edit_live] || options[:app_clip_default_experience_metadata_path].nil?
|
|
22
|
+
|
|
23
|
+
# load the metadata from the filesystem before uploading
|
|
24
|
+
load_from_filesystem(options)
|
|
25
|
+
|
|
26
|
+
# Assign default values to all languages
|
|
27
|
+
assign_defaults(options)
|
|
28
|
+
|
|
29
|
+
app = Deliver.cache[:app]
|
|
30
|
+
platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
|
|
31
|
+
version = fetch_edit_app_store_version(app, platform)
|
|
32
|
+
|
|
33
|
+
UI.important("Will begin uploading app clip default experience metadata for '#{version.version_string}' on App Store Connect")
|
|
34
|
+
|
|
35
|
+
# Currently only one app clip target per app is supported
|
|
36
|
+
app_clips = app.get_app_clips
|
|
37
|
+
app_clip = app_clips.first
|
|
38
|
+
|
|
39
|
+
# Validate options
|
|
40
|
+
subtitle_localized = options[:app_clip_default_experience_subtitle]
|
|
41
|
+
action = options[:app_clip_default_experience_action]
|
|
42
|
+
has_options_specified = !subtitle_localized.nil? || !action.nil?
|
|
43
|
+
|
|
44
|
+
if app_clip.nil?
|
|
45
|
+
UI.user_error!("A build with an app clip must be uploaded to App Store Connect before uploading the default experience metadata") if has_options_specified
|
|
46
|
+
# Nothing to do if the app clip is nil and no app clip options specified
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless has_options_specified
|
|
51
|
+
# Handle the default case where there is an app clip, but no options specified.
|
|
52
|
+
return copy_live_version_app_clip_default_experience_metadata(app: app, platform: platform, edit_version: version, app_clip: app_clip)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
UI.user_error!("You must provide at least the subtitle and action for an app clip default experience") if subtitle_localized.nil? || action.nil?
|
|
56
|
+
|
|
57
|
+
# see if there's an existing experience for this version
|
|
58
|
+
default_experience = version.app_clip_default_experience
|
|
59
|
+
if default_experience
|
|
60
|
+
# update the existing default experience
|
|
61
|
+
default_experience.update(attributes: { action: action })
|
|
62
|
+
UI.message("Updated app clip default experience")
|
|
63
|
+
else
|
|
64
|
+
# create a new default experience
|
|
65
|
+
default_experience = Spaceship::ConnectAPI::AppClipDefaultExperience.create(app_clip_id: app_clip.id, app_store_version_id: version.id, attributes: { action: action })
|
|
66
|
+
UI.important("Created default experience for version '#{version.version_string}'")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# update the subtitle localizations
|
|
70
|
+
upload_subtitle_localizations(app_clip_default_experience: default_experience, subtitle_localizations: subtitle_localized)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Loads the app clip default experience metadata files and stores them into the options object
|
|
74
|
+
def load_from_filesystem(options)
|
|
75
|
+
metadata_path = options[:app_clip_default_experience_metadata_path]
|
|
76
|
+
|
|
77
|
+
# Load localised data
|
|
78
|
+
ignore_validation = options[:ignore_language_directory_validation]
|
|
79
|
+
Loader.language_folders(metadata_path, ignore_validation).each do |lang_folder|
|
|
80
|
+
LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
|
|
81
|
+
path = File.join(lang_folder.path, "#{key}.txt")
|
|
82
|
+
next unless File.exist?(path)
|
|
83
|
+
|
|
84
|
+
UI.message("Loading '#{path}'...")
|
|
85
|
+
options[key] ||= {}
|
|
86
|
+
options[key][lang_folder.basename] ||= File.read(path).strip
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Load non localised data
|
|
91
|
+
NON_LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
|
|
92
|
+
path = File.join(metadata_path, "#{key}.txt")
|
|
93
|
+
next unless File.exist?(path)
|
|
94
|
+
|
|
95
|
+
UI.message("Loading '#{path}'...")
|
|
96
|
+
options[key] ||= File.read(path).strip
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# If the user is using the 'default' language, then assign values where they are needed
|
|
101
|
+
def assign_defaults(options)
|
|
102
|
+
# Build a complete list of the required languages
|
|
103
|
+
enabled_languages = detect_languages(options)
|
|
104
|
+
|
|
105
|
+
return unless enabled_languages.include?("default")
|
|
106
|
+
UI.message("Detected languages for app clip default experience: " + enabled_languages.to_s)
|
|
107
|
+
|
|
108
|
+
LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
|
|
109
|
+
current = options[key]
|
|
110
|
+
next unless current && current.kind_of?(Hash)
|
|
111
|
+
|
|
112
|
+
default = current["default"]
|
|
113
|
+
next if default.nil?
|
|
114
|
+
|
|
115
|
+
enabled_languages.each do |language|
|
|
116
|
+
value = current[language]
|
|
117
|
+
next unless value.nil?
|
|
118
|
+
|
|
119
|
+
current[language] = default
|
|
120
|
+
end
|
|
121
|
+
current.delete("default")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def detect_languages(options)
|
|
126
|
+
Languages.detect_languages(
|
|
127
|
+
options: options,
|
|
128
|
+
localized_values_keys: LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys,
|
|
129
|
+
metadata_path: options[:app_clip_default_experience_metadata_path],
|
|
130
|
+
ignore_validation: options[:ignore_language_directory_validation]
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# from upload_metadata.rb
|
|
135
|
+
def fetch_edit_app_store_version(app, platform, wait_time: 10)
|
|
136
|
+
retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do
|
|
137
|
+
app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def fetch_live_app_store_version(app, platform, wait_time: 10)
|
|
142
|
+
retry_if_nil("Cannot find live app store version", wait_time: wait_time) do
|
|
143
|
+
app.get_live_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# from upload_metadata.rb
|
|
148
|
+
def retry_if_nil(message, tries: 5, wait_time: 10)
|
|
149
|
+
loop do
|
|
150
|
+
tries -= 1
|
|
151
|
+
|
|
152
|
+
value = yield
|
|
153
|
+
return value if value
|
|
154
|
+
|
|
155
|
+
UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries})")
|
|
156
|
+
sleep(wait_time)
|
|
157
|
+
|
|
158
|
+
return nil if tries.zero?
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def upload_subtitle_localizations(app_clip_default_experience:, subtitle_localizations:)
|
|
163
|
+
localized_subtitle_attributes_by_locale = {}
|
|
164
|
+
subtitle_localizations.keys.each do |key|
|
|
165
|
+
localized_subtitle_attributes_by_locale[key] = {}
|
|
166
|
+
localized_subtitle_attributes_by_locale[key][:create_attributes] = { subtitle: subtitle_localizations[key], locale: key }
|
|
167
|
+
localized_subtitle_attributes_by_locale[key][:update_attributes] = { subtitle: subtitle_localizations[key] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# update the subtitle
|
|
171
|
+
existing_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience.id)
|
|
172
|
+
|
|
173
|
+
# from upload_metadata.rb
|
|
174
|
+
app_info_worker = FastlaneCore::QueueWorker.new do |locale|
|
|
175
|
+
UI.message("Uploading app clip default experience metadata to App Store Connect for localized subtitle '#{locale}'")
|
|
176
|
+
|
|
177
|
+
# find an existing localization
|
|
178
|
+
existing_localization = existing_localizations.find { |l| locale.to_s.eql?(l.locale) }
|
|
179
|
+
|
|
180
|
+
if existing_localization
|
|
181
|
+
# update existing
|
|
182
|
+
attributes = localized_subtitle_attributes_by_locale[locale][:update_attributes]
|
|
183
|
+
existing_localization.update(attributes: attributes)
|
|
184
|
+
UI.verbose("[#{locale}] Updated existing to #{attributes}")
|
|
185
|
+
else
|
|
186
|
+
# create new
|
|
187
|
+
attributes = localized_subtitle_attributes_by_locale[locale][:create_attributes]
|
|
188
|
+
Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.create(default_experience_id: app_clip_default_experience.id, attributes: attributes)
|
|
189
|
+
UI.verbose("[#{locale}] Created new with #{attributes}")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
app_info_worker.batch_enqueue(localized_subtitle_attributes_by_locale.keys)
|
|
193
|
+
app_info_worker.start
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Handle the case where an app clip exists, but the user did not specify any app clip default
|
|
197
|
+
# experience metadata options. We check if the current editable version already has the app clip
|
|
198
|
+
# default experience metadata. If not, copy over the live version's app clip default
|
|
199
|
+
# experience metadata. This mimics the default behavior of creating an App Store version from
|
|
200
|
+
# the ASC UI.
|
|
201
|
+
#
|
|
202
|
+
# This function will also produce warnings, but not outright fail, when it finds missing app
|
|
203
|
+
# clip metadata.
|
|
204
|
+
#
|
|
205
|
+
# As of 2022-05-18, App Store versions created by the ASC API do not "carry over" the live
|
|
206
|
+
# version's app clip default experience metadata, so we handle this for the user by default.
|
|
207
|
+
#
|
|
208
|
+
def copy_live_version_app_clip_default_experience_metadata(app:, platform:, edit_version:, app_clip:)
|
|
209
|
+
edit_version_default_experience = edit_version.app_clip_default_experience
|
|
210
|
+
if !edit_version_default_experience
|
|
211
|
+
live_version = fetch_live_app_store_version(app, platform)
|
|
212
|
+
# no live version to carry over metadata from
|
|
213
|
+
return if live_version.nil?
|
|
214
|
+
|
|
215
|
+
live_version_default_experience = live_version.app_clip_default_experience
|
|
216
|
+
# no live version default experience to carry over metadata from
|
|
217
|
+
return if live_version_default_experience.nil?
|
|
218
|
+
|
|
219
|
+
# create a default experience and use the live version default experience as a "template"
|
|
220
|
+
Spaceship::ConnectAPI::AppClipDefaultExperience.create(app_clip_id: app_clip.id, app_store_version_id: edit_version.id, template_default_experience_id: live_version_default_experience.id)
|
|
221
|
+
|
|
222
|
+
UI.message("Created the app clip default experience using the live version's app clip metadata as a template.")
|
|
223
|
+
else
|
|
224
|
+
# if the edit version app clip default experience already exists, just check it's values
|
|
225
|
+
UI.important("ASC requires the app clip default experience to contain a valid `action` value. To specify this value in fastlane use deliver's `app_clip_default_experience_action` option.") if edit_version_default_experience.action.nil?
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -59,6 +59,9 @@ module Deliver
|
|
|
59
59
|
demo_password: "demo_account_password",
|
|
60
60
|
notes: "notes"
|
|
61
61
|
}
|
|
62
|
+
APP_CLIP_REVIEW_INFORMATION_VALUES = {
|
|
63
|
+
invocation_urls: "invocation_urls"
|
|
64
|
+
}
|
|
62
65
|
|
|
63
66
|
# Localized app details values, that are editable in live state
|
|
64
67
|
LOCALISED_LIVE_VALUES = [:description, :release_notes, :support_url, :marketing_url, :promotional_text, :privacy_url]
|
|
@@ -72,7 +75,10 @@ module Deliver
|
|
|
72
75
|
# Directory name it contains review information
|
|
73
76
|
REVIEW_INFORMATION_DIR = "review_information"
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
# Directory name it contains app clip review information
|
|
79
|
+
APP_CLIP_REVIEW_INFORMATION_DIR = "app_clip_review_information"
|
|
80
|
+
|
|
81
|
+
ALL_META_SUB_DIRS = [TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR, REVIEW_INFORMATION_DIR, APP_CLIP_REVIEW_INFORMATION_DIR]
|
|
76
82
|
|
|
77
83
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
78
84
|
|
|
@@ -100,7 +106,7 @@ module Deliver
|
|
|
100
106
|
|
|
101
107
|
if options[:edit_live]
|
|
102
108
|
# not all values are editable when using live_version
|
|
103
|
-
version = app.get_live_app_store_version(platform: platform)
|
|
109
|
+
version = app.get_live_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
|
|
104
110
|
localised_options = LOCALISED_LIVE_VALUES
|
|
105
111
|
non_localised_options = NON_LOCALISED_LIVE_VALUES
|
|
106
112
|
|
|
@@ -349,6 +355,7 @@ module Deliver
|
|
|
349
355
|
end
|
|
350
356
|
|
|
351
357
|
review_information(version)
|
|
358
|
+
app_clip_review_information(version)
|
|
352
359
|
review_attachment_file(version)
|
|
353
360
|
app_rating(app_info)
|
|
354
361
|
end
|
|
@@ -433,9 +440,9 @@ module Deliver
|
|
|
433
440
|
.uniq
|
|
434
441
|
end
|
|
435
442
|
|
|
436
|
-
def fetch_edit_app_store_version(app, platform)
|
|
437
|
-
retry_if_nil("Cannot find edit app store version") do
|
|
438
|
-
app.get_edit_app_store_version(platform: platform)
|
|
443
|
+
def fetch_edit_app_store_version(app, platform, wait_time: 10)
|
|
444
|
+
retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do
|
|
445
|
+
app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
|
|
439
446
|
end
|
|
440
447
|
end
|
|
441
448
|
|
|
@@ -452,9 +459,8 @@ module Deliver
|
|
|
452
459
|
end
|
|
453
460
|
|
|
454
461
|
# Retries a block of code if the return value is nil, with an exponential backoff.
|
|
455
|
-
def retry_if_nil(message)
|
|
462
|
+
def retry_if_nil(message, wait_time: 10)
|
|
456
463
|
tries = options[:version_check_wait_retry_limit]
|
|
457
|
-
wait_time = 10
|
|
458
464
|
loop do
|
|
459
465
|
tries -= 1
|
|
460
466
|
|
|
@@ -633,6 +639,22 @@ module Deliver
|
|
|
633
639
|
next if path.nil?
|
|
634
640
|
options[:app_review_information][option_name] ||= File.read(path)
|
|
635
641
|
end
|
|
642
|
+
|
|
643
|
+
# Load app clip review information
|
|
644
|
+
options[:app_clip_review_information] ||= {}
|
|
645
|
+
resolve_app_clip_review_info_path = lambda do |option_name|
|
|
646
|
+
path = File.join(options[:metadata_path], APP_CLIP_REVIEW_INFORMATION_DIR, "#{option_name}.txt")
|
|
647
|
+
return nil unless File.exist?(path)
|
|
648
|
+
return nil if options[:app_clip_review_information][option_name].to_s.length > 0
|
|
649
|
+
return path
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# Then app clip load review information from new App Store Connect filenames
|
|
653
|
+
APP_CLIP_REVIEW_INFORMATION_VALUES.keys.each do |option_name|
|
|
654
|
+
path = resolve_app_clip_review_info_path.call(option_name)
|
|
655
|
+
next if path.nil?
|
|
656
|
+
options[:app_clip_review_information][option_name] ||= File.read(path)
|
|
657
|
+
end
|
|
636
658
|
end
|
|
637
659
|
|
|
638
660
|
private
|
|
@@ -684,6 +706,48 @@ module Deliver
|
|
|
684
706
|
end
|
|
685
707
|
end
|
|
686
708
|
|
|
709
|
+
def app_clip_review_information(version)
|
|
710
|
+
info = options[:app_clip_review_information]
|
|
711
|
+
return if info.nil? || info.empty?
|
|
712
|
+
|
|
713
|
+
UI.user_error!("`app_clip_review_information` must be a hash", show_github_issues: true) unless info.kind_of?(Hash)
|
|
714
|
+
info = info.transform_keys(&:to_sym)
|
|
715
|
+
attributes = {}
|
|
716
|
+
APP_CLIP_REVIEW_INFORMATION_VALUES.each do |key, attribute_name|
|
|
717
|
+
if info[key].kind_of?(Array)
|
|
718
|
+
attributes[attribute_name] = info[key].map { |value| value.to_s.strip } unless info[key].empty?
|
|
719
|
+
else
|
|
720
|
+
strip_value = info[key].to_s.strip
|
|
721
|
+
attributes[attribute_name] = strip_value unless strip_value.empty?
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
if attributes["invocation_urls"].kind_of?(String)
|
|
726
|
+
attributes["invocation_urls"] = attributes["invocation_urls"].split(", ")
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
UI.message("Uploading app clip review information to App Store Connect")
|
|
730
|
+
default_experience = version.app_clip_default_experience
|
|
731
|
+
if default_experience.nil?
|
|
732
|
+
# By this point the upload app clip default experience metadata step should have run and
|
|
733
|
+
# created a default experience, if not, we shouldn't create the default experience here.
|
|
734
|
+
UI.important("Could not upload app clip review information due to the app clip default experience missing.")
|
|
735
|
+
return
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
app_clip_app_store_review_detail = begin
|
|
739
|
+
Spaceship::ConnectAPI::AppClipDefaultExperience.get(app_clip_default_experience_id: default_experience.id, includes: "appClipAppStoreReviewDetail").app_clip_app_store_review_detail
|
|
740
|
+
rescue => error
|
|
741
|
+
UI.error("Error fetching app clip app store review detail - #{error.message}")
|
|
742
|
+
nil
|
|
743
|
+
end # errors if doesn't exist
|
|
744
|
+
if app_clip_app_store_review_detail
|
|
745
|
+
app_clip_app_store_review_detail.update(attributes: attributes)
|
|
746
|
+
else
|
|
747
|
+
Spaceship::ConnectAPI::AppClipAppStoreReviewDetail.create(app_clip_default_experience_id: default_experience.id, attributes: attributes)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
687
751
|
def review_attachment_file(version)
|
|
688
752
|
app_store_review_detail = version.fetch_app_store_review_detail
|
|
689
753
|
app_store_review_attachments = app_store_review_detail.app_store_review_attachments || []
|
|
@@ -13,8 +13,10 @@ module Deliver
|
|
|
13
13
|
|
|
14
14
|
attributes = {}
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
16
|
+
# nil leaves existing territory availability unchanged.
|
|
17
|
+
# [] intentionally removes the app from sale in all territories.
|
|
18
|
+
# Pass explicit territory IDs to make the app available only in those territories.
|
|
19
|
+
territory_ids = nil
|
|
18
20
|
|
|
19
21
|
# As of 2020-09-14:
|
|
20
22
|
# Official App Store Connect does not have an endpoint to get app prices for an app
|
|
@@ -201,7 +201,7 @@ module Deliver
|
|
|
201
201
|
iterator.each_app_screenshot.select { |_, _, app_screenshot| app_screenshot.error? }.each do |localization, _, app_screenshot|
|
|
202
202
|
UI.error("#{app_screenshot.file_name} for #{localization.locale} has error(s) - #{app_screenshot.error_messages.join(', ')}")
|
|
203
203
|
end
|
|
204
|
-
incomplete_screenshot_count = states.
|
|
204
|
+
incomplete_screenshot_count = states.except('COMPLETE').reduce(0) { |sum, (k, v)| sum + v }
|
|
205
205
|
UI.user_error!("Failed verification of all screenshots uploaded... #{incomplete_screenshot_count} incomplete screenshot(s) still exist")
|
|
206
206
|
else
|
|
207
207
|
UI.error("Failed to upload all screenshots... Tries remaining: #{tries}")
|
|
@@ -9,8 +9,8 @@ module Fastlane
|
|
|
9
9
|
|
|
10
10
|
class AppStoreConnectApiKeyAction < Action
|
|
11
11
|
def self.run(options)
|
|
12
|
-
key_id = options[:key_id]
|
|
13
|
-
issuer_id = options[:issuer_id]
|
|
12
|
+
key_id = options[:key_id]&.strip
|
|
13
|
+
issuer_id = options[:issuer_id]&.strip
|
|
14
14
|
key_content = options[:key_content]
|
|
15
15
|
is_key_content_base64 = options[:is_key_content_base64]
|
|
16
16
|
key_filepath = options[:key_filepath]
|
|
@@ -41,7 +41,7 @@ module Fastlane
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def self.detect_appium(params)
|
|
44
|
-
appium_path = params[:appium_path] ||
|
|
44
|
+
appium_path = params[:appium_path] || Helper.which('appium').to_s
|
|
45
45
|
|
|
46
46
|
if appium_path.empty?
|
|
47
47
|
if File.exist?(APPIUM_PATH_HOMEBREW)
|
|
@@ -56,7 +56,7 @@ module Fastlane
|
|
|
56
56
|
def self.run(params)
|
|
57
57
|
unless Helper.test?
|
|
58
58
|
UI.message("Install using `brew install appledoc`")
|
|
59
|
-
UI.user_error!("appledoc not installed")
|
|
59
|
+
UI.user_error!("appledoc not installed") unless Helper.which('appledoc')
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
params_hash = params.values
|
|
@@ -57,7 +57,7 @@ module Fastlane
|
|
|
57
57
|
short_option: "-j",
|
|
58
58
|
conflicting_options: [:json_key_data],
|
|
59
59
|
optional: true, # optional until it is possible specify either json_key OR json_key_data are required
|
|
60
|
-
description: "The path to a file
|
|
60
|
+
description: "The path to a Google credentials JSON file (Application Default, Workload Identity, or Service Account), used to authenticate with Google",
|
|
61
61
|
code_gen_sensitive: true,
|
|
62
62
|
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file),
|
|
63
63
|
default_value_dynamic: true,
|
|
@@ -70,7 +70,7 @@ module Fastlane
|
|
|
70
70
|
short_option: "-c",
|
|
71
71
|
conflicting_options: [:json_key],
|
|
72
72
|
optional: true,
|
|
73
|
-
description: "The raw
|
|
73
|
+
description: "The raw content of a Google credentials JSON file (Application Default, Workload Identity, or Service Account) used to authenticate with Google",
|
|
74
74
|
code_gen_sensitive: true,
|
|
75
75
|
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_data_raw),
|
|
76
76
|
default_value_dynamic: true,
|