fastlane-plugin-fastci 0.0.30 → 0.0.41

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.
@@ -0,0 +1,300 @@
1
+ require 'fastlane/action'
2
+ require 'fastlane/plugin/fastci/helper/app_store_resources_helper'
3
+ include Fastlane::Helper
4
+
5
+ module Fastlane
6
+ module Actions
7
+ # 校验并上传 App Store Connect metadata 和截图
8
+ class AppStoreResourcesAction < Action
9
+ # 独立资源入口:准备资源 -> 上传 -> 可选构建选择 -> 可选提交审核,全程不上传 IPA。
10
+ def self.run(params)
11
+ return nil unless resources_enabled?(params)
12
+
13
+ params = prepare_resources(params)
14
+ upload_resources(params)
15
+ select_build_and_submit_review(params)
16
+ end
17
+
18
+ # 总开关先于配置读取和认证检查,单独指定路径或资源子开关不能启用此能力。
19
+ def self.resources_enabled?(params)
20
+ return true if params[:app_store_resources_enabled] == true
21
+
22
+ UI.message('*************| 商店资源功能未开启,跳过(APP_STORE_RESOURCES_ENABLED=false)|*************')
23
+ false
24
+ end
25
+
26
+ # 统一读取配置、建立认证并准备资源目录,下载与校验规则由 Helper 负责。
27
+ def self.prepare_resources(params)
28
+ params = AppStoreResourcesHelper.load_params(params)
29
+ params.merge!(AppStoreResourcesHelper.authentication_options(params, other_action))
30
+ AppStoreResourcesHelper.prepare(params)
31
+ end
32
+
33
+ # 只上传配置指定的资源类别,无资源变化时跳过上传。
34
+ def self.upload_resources(params)
35
+ if params[:metadata_changed] || params[:screenshots_changed]
36
+ UI.message('*************| 开始上传 App Store Connect 资源 |*************')
37
+ other_action.upload_to_app_store(
38
+ AppStoreResourcesHelper.resource_options(params, params[:metadata_path], params[:screenshots_path])
39
+ )
40
+ UI.success('App Store Connect 资源上传完成')
41
+ else
42
+ UI.message('*************| 没有 metadata 或截图变更,跳过资源上传 |*************')
43
+ end
44
+ end
45
+
46
+ # 资源上传完成后,按配置选择精确构建并提交审核;两个步骤默认均不执行。
47
+ def self.select_build_and_submit_review(params)
48
+ # 提交审核也必须先确认精确构建,不能依赖 deliver 自动选择最新 Build。
49
+ if params[:select_build] || params[:submit_for_review]
50
+ AppStoreResourcesHelper.select_build(params)
51
+ end
52
+
53
+ if params[:submit_for_review]
54
+ UI.message('*************| 开始提交 App Store 审核 |*************')
55
+ other_action.upload_to_app_store(AppStoreResourcesHelper.review_options(params))
56
+ UI.success('App Store 审核提交完成')
57
+ end
58
+ end
59
+
60
+ private_class_method :resources_enabled?, :prepare_resources, :upload_resources, :select_build_and_submit_review
61
+
62
+ def self.description
63
+ '校验并上传 App Store Connect metadata 和截图'
64
+ end
65
+
66
+ # JSON 合并后也通过这组参数校验,使文件配置与直接调用拥有相同约束。
67
+ def self.available_options
68
+ [
69
+ FastlaneCore::ConfigItem.new(
70
+ key: :app_store_resources_enabled,
71
+ env_name: 'APP_STORE_RESOURCES_ENABLED',
72
+ description: '商店资源总开关,必须显式开启后才能使用资源上传、下载及发布流程',
73
+ optional: true,
74
+ default_value: false,
75
+ type: Boolean
76
+ ),
77
+ FastlaneCore::ConfigItem.new(
78
+ key: :config_path,
79
+ env_name: 'APP_STORE_RESOURCES_CONFIG_PATH',
80
+ description: '资源发布 JSON 配置文件路径,显式参数优先于配置文件',
81
+ optional: true,
82
+ default_value: nil,
83
+ # 路径校验由开启后的 load_params 执行,关闭功能时不读取或校验资源文件。
84
+ type: String
85
+ ),
86
+ FastlaneCore::ConfigItem.new(
87
+ key: :app_identifier,
88
+ description: 'App Store Connect App 的 Bundle ID',
89
+ optional: true,
90
+ type: String
91
+ ),
92
+ FastlaneCore::ConfigItem.new(
93
+ key: :app_version,
94
+ description: '要更新的 App Store 版本号',
95
+ optional: true,
96
+ default_value: nil,
97
+ type: String
98
+ ),
99
+ FastlaneCore::ConfigItem.new(
100
+ key: :metadata_path,
101
+ description: 'metadata 目录路径',
102
+ optional: true,
103
+ default_value: 'fastlane/metadata',
104
+ type: String
105
+ ),
106
+ FastlaneCore::ConfigItem.new(
107
+ key: :screenshots_path,
108
+ description: '截图目录路径',
109
+ optional: true,
110
+ default_value: 'fastlane/screenshots',
111
+ type: String
112
+ ),
113
+ FastlaneCore::ConfigItem.new(
114
+ key: :metadata_changed,
115
+ description: '是否上传 metadata',
116
+ optional: true,
117
+ default_value: true,
118
+ type: Boolean
119
+ ),
120
+ FastlaneCore::ConfigItem.new(
121
+ key: :screenshots_changed,
122
+ description: '是否上传截图',
123
+ optional: true,
124
+ default_value: false,
125
+ type: Boolean
126
+ ),
127
+ FastlaneCore::ConfigItem.new(
128
+ key: :upload_metadata_on_new_version,
129
+ description: '目标 App Store 版本不存在时是否强制上传 metadata',
130
+ optional: true,
131
+ default_value: true,
132
+ type: Boolean
133
+ ),
134
+ FastlaneCore::ConfigItem.new(
135
+ key: :download_missing_metadata,
136
+ description: 'metadata 缺失时是否从 App Store Connect 下载',
137
+ optional: true,
138
+ default_value: false,
139
+ type: Boolean
140
+ ),
141
+ FastlaneCore::ConfigItem.new(
142
+ key: :download_missing_screenshots,
143
+ description: '截图缺失时是否从 App Store Connect 下载',
144
+ optional: true,
145
+ default_value: false,
146
+ type: Boolean
147
+ ),
148
+ FastlaneCore::ConfigItem.new(
149
+ key: :use_live_version,
150
+ description: '下载截图时是否使用线上版本',
151
+ optional: true,
152
+ default_value: false,
153
+ type: Boolean
154
+ ),
155
+ FastlaneCore::ConfigItem.new(
156
+ key: :api_key,
157
+ description: 'App Store Connect API Key Hash',
158
+ optional: true,
159
+ sensitive: true,
160
+ type: Hash,
161
+ conflicting_options: [:api_key_path]
162
+ ),
163
+ FastlaneCore::ConfigItem.new(
164
+ key: :api_key_path,
165
+ description: 'App Store Connect API Key JSON 文件路径',
166
+ optional: true,
167
+ type: String,
168
+ conflicting_options: [:api_key],
169
+ verify_block: proc do |value|
170
+ UI.user_error!("API Key JSON 不存在: #{value}") unless File.file?(value)
171
+ end
172
+ ),
173
+ FastlaneCore::ConfigItem.new(
174
+ key: :username,
175
+ description: 'Apple ID 用户名,仅在未提供 API Key 时使用',
176
+ optional: true,
177
+ type: String
178
+ ),
179
+ FastlaneCore::ConfigItem.new(
180
+ key: :platform,
181
+ description: 'App Store Connect 平台',
182
+ optional: true,
183
+ default_value: 'ios',
184
+ type: String,
185
+ verify_block: proc { |value| UI.user_error!('资源上传仅支持 ios 平台') unless value == 'ios' }
186
+ ),
187
+ FastlaneCore::ConfigItem.new(
188
+ key: :sync_screenshots,
189
+ description: '是否让远端截图与本地截图同步',
190
+ optional: true,
191
+ default_value: false,
192
+ type: Boolean
193
+ ),
194
+ FastlaneCore::ConfigItem.new(
195
+ key: :overwrite_screenshots,
196
+ description: '上传前是否清空远端截图',
197
+ optional: true,
198
+ default_value: false,
199
+ type: Boolean
200
+ ),
201
+ FastlaneCore::ConfigItem.new(
202
+ key: :submit_for_review,
203
+ description: '上传资源后是否提交审核',
204
+ optional: true,
205
+ default_value: false,
206
+ type: Boolean
207
+ ),
208
+ FastlaneCore::ConfigItem.new(
209
+ key: :automatic_release,
210
+ description: '审核通过后是否自动发布',
211
+ optional: true,
212
+ default_value: false,
213
+ type: Boolean
214
+ ),
215
+ FastlaneCore::ConfigItem.new(
216
+ key: :select_build,
217
+ description: '是否按 app_version 和 build_number 自动选择构建',
218
+ optional: true,
219
+ default_value: false,
220
+ type: Boolean
221
+ ),
222
+ FastlaneCore::ConfigItem.new(
223
+ key: :build_number,
224
+ description: '要选择的精确 Build Number',
225
+ optional: true,
226
+ default_value: nil,
227
+ type: String
228
+ ),
229
+ FastlaneCore::ConfigItem.new(
230
+ key: :wait_for_build_processing,
231
+ description: '选择构建前是否等待 App Store Connect 处理完成',
232
+ optional: true,
233
+ default_value: true,
234
+ type: Boolean
235
+ ),
236
+ FastlaneCore::ConfigItem.new(
237
+ key: :build_processing_timeout,
238
+ description: '等待构建处理完成的超时时间,单位秒',
239
+ optional: true,
240
+ default_value: 3600,
241
+ type: Integer,
242
+ verify_block: proc { |value| UI.user_error!('构建处理超时必须大于 0') unless value.positive? }
243
+ ),
244
+ FastlaneCore::ConfigItem.new(
245
+ key: :build_processing_poll_interval,
246
+ description: '查询构建处理状态的时间间隔,单位秒',
247
+ optional: true,
248
+ default_value: 30,
249
+ type: Integer,
250
+ verify_block: proc { |value| UI.user_error!('构建轮询间隔必须大于 0') unless value.positive? }
251
+ ),
252
+ FastlaneCore::ConfigItem.new(
253
+ key: :build_processing_retry_limit,
254
+ description: '查询构建处理状态遇到网络错误时的最大重试次数',
255
+ optional: true,
256
+ default_value: 3,
257
+ type: Integer,
258
+ verify_block: proc { |value| UI.user_error!('构建查询重试次数不能小于 0') if value.negative? }
259
+ ),
260
+ FastlaneCore::ConfigItem.new(
261
+ key: :build_processing_retry_interval,
262
+ description: '查询构建处理状态网络错误后的重试等待时间,单位秒',
263
+ optional: true,
264
+ default_value: 15,
265
+ type: Integer,
266
+ verify_block: proc { |value| UI.user_error!('构建查询重试间隔不能小于 0') if value.negative? }
267
+ ),
268
+ FastlaneCore::ConfigItem.new(
269
+ key: :submission_information,
270
+ description: 'deliver 提交审核信息,例如出口合规声明',
271
+ optional: true,
272
+ default_value: nil,
273
+ type: Hash
274
+ ),
275
+ FastlaneCore::ConfigItem.new(
276
+ key: :release_notes,
277
+ description: '按语言配置的更新说明;优先于 metadata 中的 release_notes.txt',
278
+ optional: true,
279
+ type: Hash
280
+ ),
281
+ FastlaneCore::ConfigItem.new(
282
+ key: :force,
283
+ description: '是否跳过 HTML 预览确认',
284
+ optional: true,
285
+ default_value: true,
286
+ type: Boolean
287
+ )
288
+ ]
289
+ end
290
+
291
+ def self.is_supported?(platform)
292
+ platform == :ios
293
+ end
294
+
295
+ def self.category
296
+ :production
297
+ end
298
+ end
299
+ end
300
+ end
@@ -1,6 +1,8 @@
1
1
  require 'gym'
2
2
  require 'fastlane_core'
3
3
  require 'fastlane/action'
4
+ require 'fastlane/plugin/fastci/helper/app_store_resources_helper'
5
+ require 'fastlane/plugin/fastci/helper/package_notification_helper'
4
6
  include Fastlane::Helper
5
7
 
6
8
  module Fastlane
@@ -8,27 +10,14 @@ module Fastlane
8
10
  # 打包
9
11
  class PackageAction < Action
10
12
  def self.run(params)
13
+ notice = PackageNotificationHelper.new(params)
11
14
  # 入参配置
12
15
  configuration = params[:configuration] || 'Debug'
13
16
  export_method = params[:export_method] || 'development'
14
17
  build = params[:build] || nil
15
18
  version = params[:version] || nil
16
- is_analyze_swiftlint = params[:is_analyze_swiftlint] || false
17
- is_detect_duplicity_code = params[:is_detect_duplicity_code] || false
18
- is_detect_unused_code = params[:is_detect_unused_code] || false
19
- is_detect_unused_image = params[:is_detect_unused_image] || false
20
- release_notes = params[:release_notes] || ''
19
+ store_options = store_upload_options(params, export_method)
21
20
  configuration = 'Release' if %w[app-store testFlight].include?(export_method)
22
- is_notice_dingding = params[:is_notice_dingding].nil? ? true : params[:is_notice_dingding]
23
- # 统一发送钉钉消息匿名函数
24
- send_dingding_notice = lambda do |text|
25
- if is_notice_dingding
26
- DingdingHelper.sendMarkdown(text)
27
- else
28
- UI.message('*************| 跳过钉钉消息通知(is_notice_dingding=false)|*************')
29
- end
30
- end
31
- send_dingding_notice.call('测试一下')
32
21
  # 清理上一次的打包缓存
33
22
  FileUtils.rm_rf(Dir.glob("#{Constants.BUILD_LOG_DIR}/*"))
34
23
  FileUtils.rm_rf(Dir.glob("#{Constants.IPA_OUTPUT_DIR}/*"))
@@ -52,7 +41,6 @@ module Fastlane
52
41
  other_action.update_build_number(
53
42
  build: build
54
43
  )
55
- time = Time.new.strftime('%Y%m%d%H%M')
56
44
 
57
45
  # 获取版本号
58
46
  version_options = { target: scheme }
@@ -65,6 +53,7 @@ module Fastlane
65
53
  build = other_action.get_build_number(build_options)
66
54
  # 生成ipa包的名字格式
67
55
  ipaName = "#{scheme}_#{export_method}_#{version}_#{build}.ipa"
56
+ notice.build_info(version: version, build: build)
68
57
 
69
58
  # 获取 Extension 的 Bundle ID(可能有多个,用逗号分隔)
70
59
  extension_bundle_ids = Environment.extension_bundle_ids
@@ -119,11 +108,13 @@ module Fastlane
119
108
  gym_options[:export_options][:provisioningProfiles] = provisioningProfiles_map
120
109
  end
121
110
 
111
+ notice.stage(:local, '构建 IPA')
122
112
  other_action.gym(gym_options)
123
113
 
124
114
  UI.message('*************| 打包完成 |*************')
125
115
 
126
116
  UI.message('*************| 复制打包产物 |*************')
117
+ notice.stage(:local, '复制构建产物')
127
118
  # 定义桌面路径
128
119
  desktop_path = File.expand_path('~/Desktop')
129
120
  output_path = File.join(desktop_path, "BuildOutput_#{scheme}")
@@ -140,139 +131,153 @@ module Fastlane
140
131
  # system("git reset --hard")
141
132
 
142
133
  ipa_path = "#{Constants.IPA_OUTPUT_DIR}/#{ipaName}"
143
-
144
- if gym_method == 'app-store'
145
- notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-打包完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
146
- send_dingding_notice.call(notiText)
147
-
148
- if CommonHelper.is_validate_string(Environment.connect_key_id) && CommonHelper.is_validate_string(Environment.connect_issuer_id)
149
- # 根据 export_method 决定是否为 TestFlight
150
- is_test_flight = export_method == 'testFlight'
151
-
152
- other_action.upload_store(
153
- release_notes: release_notes,
154
- isTestFlight: is_test_flight
155
- )
156
- notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-上传完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
157
- send_dingding_notice.call(notiText)
158
- end
159
- else
160
- # 钉钉通知
161
- notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-打包完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
162
-
163
- # 上传蒲公英
164
- if CommonHelper.is_validate_string(Environment.pgy_api_key)
165
- pgy_upload_info = other_action.upload_pgy
166
- qrCode = pgy_upload_info['buildQRCodeURL']
167
-
168
- if CommonHelper.is_validate_string(qrCode)
169
- notiText << "\n\n⬇️⬇️⬇️ 扫码安装 ⬇️⬇️⬇️\n\n\n密码: #{Environment.pgy_password}\n![screenshot](#{qrCode})"
170
- end
171
- end
172
-
173
- # 上传 fir
174
- if CommonHelper.is_validate_string(Environment.fir_api_token)
175
- fir_upload_info = other_action.upload_fir(
176
- changelog: params[:changelog]
177
- )
178
- download_url = fir_upload_info[:download_url]
179
-
180
- if CommonHelper.is_validate_string(download_url)
181
- notiText << "\n\n⬇️⬇️⬇️ 点击链接安装 ⬇️⬇️⬇️\n\n\n密码: #{Environment.fir_password}\n[_点击下载_](#{download_url})"
182
- end
183
- end
184
-
185
- # 上传 Zealot
186
- if CommonHelper.is_validate_string(Environment.zealot_token)
187
- zealot_upload_info = other_action.upload_zealot(
188
- file: ipa_path,
189
- changelog: params[:changelog]
190
- )
191
- # dSYM 仅在 IPA 上传成功且显式开启时上传。
192
- if zealot_upload_info && params[:is_upload_zealot_dsym]
193
- other_action.upload_zealot_dsym
194
- end
195
- end
196
-
197
- send_dingding_notice.call(notiText)
198
- end
134
+ # 构建完成仅记录终端日志,钉钉成功通知由各分发平台上传结果触发。
135
+ distribute_package(params, notice, store_options, ipa_path: ipa_path, version: version, build: build)
199
136
 
200
137
  # Sentry 上传 dSYM
201
138
  if CommonHelper.is_validate_string(Environment.sentry_auth_token)
139
+ notice.stage(:sentry, '上传 dSYM')
202
140
  other_action.sentry_upload_dsym
203
141
  else
204
142
  UI.message('*************| 未配置 Sentry 跳过 dSYM 上传 |*************')
205
143
  end
206
144
 
207
- # 代码分析
208
- if is_analyze_swiftlint && gym_method != 'app-store'
209
- other_action.analyze_swiftlint(
210
- is_from_package: true,
211
- configuration: configuration
212
- )
213
- # 结果复制到桌面
214
- FileUtils.cp(SWIFTLINT_ANALYZE_HTML_FILE, target_path)
215
- UI.message('*************| 代码分析完成 |*************')
216
- else
217
- UI.message('*************| 跳过代码分析 |*************')
218
- end
145
+ run_package_checks(params, notice, gym_method: gym_method, scheme: scheme,
146
+ configuration: configuration, target_path: target_path)
219
147
 
220
- # 重复代码检查
221
- if is_detect_duplicity_code && gym_method != 'app-store'
222
- other_action.detect_duplicity_code(
223
- is_all: true
224
- )
225
- # 结果复制到桌面
226
- FileUtils.cp(DUPLICITY_CODE_HTML_FILE, target_path)
227
- UI.message('*************| 重复代码检查完成 |*************')
228
- else
229
- UI.message('*************| 跳过重复代码检查 |*************')
230
- end
148
+ UI.message('*************| 脚本完成 |*************')
149
+ rescue StandardError
150
+ notice&.failure(store_result: Actions.lane_context[SharedValues::FASTCI_STORE_UPLOAD_RESULT] || {})
151
+ raise
152
+ end
231
153
 
232
- # 无用代码检查
233
- if is_detect_unused_code && gym_method != 'app-store'
234
- other_action.detect_unused_code(
235
- scheme: scheme,
236
- is_from_package: true,
237
- configuration: configuration
238
- )
239
- # 结果复制到桌面
240
- FileUtils.cp(Constants.UNUSED_CODE_HTML_FILE, target_path)
241
- UI.message('*************| 无用代码检查完成 |*************')
154
+ # 各平台分别发送结果;后一个平台失败时,已经成功的平台仍有明确通知。
155
+ def self.distribute_package(params, notice, store_options, ipa_path:, version:, build:)
156
+ if %w[app-store testFlight].include?(params[:export_method])
157
+ platform = params[:export_method] == 'testFlight' ? :testflight : :app_store
158
+ notice.stage(platform, '准备商店上传')
159
+ Actions.lane_context.delete(SharedValues::FASTCI_STORE_UPLOAD_RESULT)
160
+ if upload_store_if_configured(store_options, version: version, build: build)
161
+ result = Actions.lane_context[SharedValues::FASTCI_STORE_UPLOAD_RESULT] || {}
162
+ notice.distribution_completed(platform, { uploaded: true }, store_result: result)
163
+ else
164
+ notice.store_skipped(platform)
165
+ end
242
166
  else
243
- UI.message('*************| 跳过无用代码检查 |*************')
167
+ distribute_test_package(params, notice, ipa_path)
244
168
  end
169
+ end
245
170
 
246
- # 无用图片检查
247
- if is_detect_unused_image && gym_method != 'app-store'
248
- other_action.detect_unused_image
249
- # 结果复制到桌面
250
- FileUtils.cp(Constants.UNUSED_IMAGE_HTML_FILE, target_path)
251
- UI.message('*************| 无用图片检查完成 |*************')
252
- else
253
- UI.message('*************| 跳过未使用图片检查 |*************')
171
+ # 保持原有上传顺序与开关;安装链接的整理交给通知 Helper。
172
+ def self.distribute_test_package(params, notice, ipa_path)
173
+ if CommonHelper.is_validate_string(Environment.pgy_api_key)
174
+ notice.stage(:pgy, '上传 IPA')
175
+ notice.distribution_completed(:pgy, other_action.upload_pgy)
254
176
  end
177
+ if CommonHelper.is_validate_string(Environment.fir_api_token)
178
+ notice.stage(:fir, '上传 IPA')
179
+ notice.distribution_completed(:fir, other_action.upload_fir(changelog: params[:changelog]))
180
+ end
181
+ return unless CommonHelper.is_validate_string(Environment.zealot_token)
182
+
183
+ notice.stage(:zealot, '上传 IPA')
184
+ result = other_action.upload_zealot(file: ipa_path, changelog: params[:changelog])
185
+ if result && params[:is_upload_zealot_dsym]
186
+ notice.stage(:zealot, '上传 dSYM(IPA 已上传)')
187
+ other_action.upload_zealot_dsym
188
+ result = result.merge(dsym_status: Actions.lane_context[SharedValues::ZEAALOT_ERROR_MESSAGE] ? '上传未完成' : '已上传')
189
+ end
190
+ notice.distribution_completed(:zealot, result)
191
+ end
255
192
 
256
- if is_analyze_swiftlint ||
257
- is_detect_duplicity_code ||
258
- is_detect_unused_code ||
259
- is_detect_unused_image
260
- # 钉钉通知
261
- notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-代码检查完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
262
- send_dingding_notice.call(notiText)
263
- else
264
- UI.message('*************| 跳过代码检查 |*************')
193
+ # 仅统计实际调用的检查,商店包跳过检查时不发送“检查完成”。
194
+ def self.run_package_checks(params, notice, gym_method:, scheme:, configuration:, target_path:)
195
+ return if gym_method == 'app-store'
196
+
197
+ checks = [
198
+ [:is_analyze_swiftlint, 'SwiftLint', :analyze_swiftlint,
199
+ { is_from_package: true, configuration: configuration }, Constants.SWIFTLINT_ANALYZE_HTML_FILE],
200
+ [:is_detect_duplicity_code, '重复代码', :detect_duplicity_code,
201
+ { is_all: true }, Constants.DUPLICITY_CODE_HTML_FILE],
202
+ [:is_detect_unused_code, '无用代码', :detect_unused_code,
203
+ { scheme: scheme, is_from_package: true, configuration: configuration }, Constants.UNUSED_CODE_HTML_FILE],
204
+ [:is_detect_unused_image, '无用图片', :detect_unused_image, {}, Constants.UNUSED_IMAGE_HTML_FILE]
205
+ ]
206
+ completed = []
207
+ checks.each do |key, name, action, options, report|
208
+ next unless params[key]
209
+
210
+ notice.stage(:checks, name)
211
+ options.empty? ? other_action.public_send(action) : other_action.public_send(action, **options)
212
+ FileUtils.cp(report, target_path)
213
+ completed << name
265
214
  end
215
+ notice.checks_completed(completed)
216
+ end
266
217
 
267
- UI.message('*************| 脚本完成 |*************')
218
+ private_class_method :distribute_package, :distribute_test_package, :run_package_checks
219
+
220
+ # 构建前准备商店上传参数,开启资源功能却漏配路径时立即报错。
221
+ def self.store_upload_options(params, export_method)
222
+ config_path = AppStoreResourcesHelper.upload_config_path(params, app_store: export_method == 'app-store')
223
+ options = {
224
+ release_notes: params[:release_notes] || '',
225
+ isTestFlight: export_method == 'testFlight',
226
+ # 显式透传 false,避免下层 Action 从环境变量重新开启本次已关闭的功能。
227
+ app_store_resources_enabled: !config_path.nil?
228
+ }
229
+ options[:app_store_resources_config_path] = config_path if config_path
230
+ options
231
+ end
232
+
233
+ # 根据凭据或已开启的资源配置判断是否上传,单独存在资源路径不能触发上传。
234
+ def self.store_upload_configured?(options)
235
+ has_p8_key = CommonHelper.is_validate_string(Environment.connect_key_id) &&
236
+ CommonHelper.is_validate_string(Environment.connect_issuer_id)
237
+ has_api_key_json = CommonHelper.is_validate_string(Environment.app_store_connect_api_key_path)
238
+ has_p8_key || has_api_key_json || options[:app_store_resources_enabled]
268
239
  end
269
240
 
241
+ # 上传本次产物,并返回是否执行了上传,供主流程决定是否发送完成通知。
242
+ def self.upload_store_if_configured(options, version:, build:)
243
+ return false unless store_upload_configured?(options)
244
+
245
+ upload_options = options.dup
246
+ if upload_options[:app_store_resources_enabled]
247
+ # 使用真实产物信息覆盖资源配置,避免选中 JSON 中残留的旧版本或 Build。
248
+ upload_options.merge!(
249
+ app_identifier: Environment.bundleID,
250
+ app_version: version.to_s,
251
+ build_number: build.to_s
252
+ )
253
+ end
254
+ other_action.upload_store(upload_options)
255
+ true
256
+ end
257
+
258
+ private_class_method :store_upload_options, :store_upload_configured?, :upload_store_if_configured
259
+
270
260
  def self.description
271
261
  '打包'
272
262
  end
273
263
 
274
264
  def self.available_options
275
265
  [
266
+ FastlaneCore::ConfigItem.new(
267
+ key: :app_store_resources_enabled,
268
+ env_name: 'APP_STORE_RESOURCES_ENABLED',
269
+ description: '商店资源总开关,默认关闭,仅 app-store 打包时生效',
270
+ optional: true,
271
+ default_value: false,
272
+ type: Boolean
273
+ ),
274
+ FastlaneCore::ConfigItem.new(
275
+ key: :app_store_resources_config_path,
276
+ env_name: 'APP_STORE_RESOURCES_CONFIG_PATH',
277
+ description: 'App Store 资源发布 JSON 路径,开启商店资源总开关后必填',
278
+ optional: true,
279
+ type: String
280
+ ),
276
281
  FastlaneCore::ConfigItem.new(
277
282
  key: :is_upload_zealot_dsym,
278
283
  env_name: 'ZEALOT_UPLOAD_DSYM',