fastlane-plugin-fastci 0.0.20 → 0.0.40

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,52 @@
1
+ require 'fastlane/plugin/fastci/helper/fastci_zealot_helper'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ # 查询 iOS 应用已上传版本,是否跳过后续打包由调用方决定。
6
+ class CheckZealotVersionAction < ZealotVersionCheckAction
7
+ extend Fastlane::Helper::FastciZealotHelper
8
+
9
+ def self.run(params)
10
+ validate_zealot_connection!(zealot_parameters(params))
11
+ [
12
+ SharedValues::ZEALOT_VERSION_EXISTED,
13
+ SharedValues::ZEAALOT_ERROR_MESSAGE,
14
+ SharedValues::ZEALOT_LATEST_RELEASE_ID,
15
+ SharedValues::ZEALOT_LATEST_RELEASE_VERSION,
16
+ SharedValues::ZEALOT_LATEST_VERSION,
17
+ SharedValues::ZEALOT_LATEST_RELEASE_URL,
18
+ SharedValues::ZEALOT_LATEST_INSTALL_URL,
19
+ SharedValues::ZEALOT_LATEST_QRCODE_URL,
20
+ SharedValues::ZEALOT_LATEST_CONTEXT
21
+ ].each { |key| Actions.lane_context.delete(key) }
22
+
23
+ response = check_app_version(params)
24
+ # 上游非终止模式在网络错误时返回 false,不能继续读取 response.status。
25
+ return nil unless response
26
+
27
+ unless [200, 404].include?(response.status) && response.body.is_a?(Hash)
28
+ show_error("Zealot 版本查询失败(HTTP #{response.status})", params[:fail_on_error])
29
+ return nil
30
+ end
31
+
32
+ parse_response(response, params[:fail_on_error])
33
+ end
34
+
35
+ def self.available_options
36
+ zealot_options(ZealotVersionCheckAction)
37
+ end
38
+
39
+ def self.description
40
+ '按 Bundle ID 与版本号或 Git commit 查询 Zealot,返回是否已存在'
41
+ end
42
+
43
+ def self.example_code
44
+ ['check_zealot_version(bundle_id: "com.example.app", release_version: "1.0", build_version: "1")']
45
+ end
46
+
47
+ def self.is_supported?(platform)
48
+ platform == :ios
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,7 @@
1
1
  require 'gym'
2
2
  require 'fastlane_core'
3
3
  require 'fastlane/action'
4
+ require 'fastlane/plugin/fastci/helper/app_store_resources_helper'
4
5
  include Fastlane::Helper
5
6
 
6
7
  module Fastlane
@@ -17,7 +18,7 @@ module Fastlane
17
18
  is_detect_duplicity_code = params[:is_detect_duplicity_code] || false
18
19
  is_detect_unused_code = params[:is_detect_unused_code] || false
19
20
  is_detect_unused_image = params[:is_detect_unused_image] || false
20
- release_notes = params[:release_notes] || ''
21
+ store_options = store_upload_options(params, export_method)
21
22
  configuration = 'Release' if %w[app-store testFlight].include?(export_method)
22
23
  is_notice_dingding = params[:is_notice_dingding].nil? ? true : params[:is_notice_dingding]
23
24
  # 统一发送钉钉消息匿名函数
@@ -145,14 +146,7 @@ module Fastlane
145
146
  notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-打包完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
146
147
  send_dingding_notice.call(notiText)
147
148
 
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
- )
149
+ if upload_store_if_configured(store_options, version: version, build: build)
156
150
  notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-上传完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
157
151
  send_dingding_notice.call(notiText)
158
152
  end
@@ -182,6 +176,18 @@ module Fastlane
182
176
  end
183
177
  end
184
178
 
179
+ # 上传 Zealot
180
+ if CommonHelper.is_validate_string(Environment.zealot_token)
181
+ zealot_upload_info = other_action.upload_zealot(
182
+ file: ipa_path,
183
+ changelog: params[:changelog]
184
+ )
185
+ # dSYM 仅在 IPA 上传成功且显式开启时上传。
186
+ if zealot_upload_info && params[:is_upload_zealot_dsym]
187
+ other_action.upload_zealot_dsym
188
+ end
189
+ end
190
+
185
191
  send_dingding_notice.call(notiText)
186
192
  end
187
193
 
@@ -255,12 +261,75 @@ module Fastlane
255
261
  UI.message('*************| 脚本完成 |*************')
256
262
  end
257
263
 
264
+ # 构建前准备商店上传参数,开启资源功能却漏配路径时立即报错。
265
+ def self.store_upload_options(params, export_method)
266
+ config_path = AppStoreResourcesHelper.upload_config_path(params, app_store: export_method == 'app-store')
267
+ options = {
268
+ release_notes: params[:release_notes] || '',
269
+ isTestFlight: export_method == 'testFlight',
270
+ # 显式透传 false,避免下层 Action 从环境变量重新开启本次已关闭的功能。
271
+ app_store_resources_enabled: !config_path.nil?
272
+ }
273
+ options[:app_store_resources_config_path] = config_path if config_path
274
+ options
275
+ end
276
+
277
+ # 根据凭据或已开启的资源配置判断是否上传,单独存在资源路径不能触发上传。
278
+ def self.store_upload_configured?(options)
279
+ has_p8_key = CommonHelper.is_validate_string(Environment.connect_key_id) &&
280
+ CommonHelper.is_validate_string(Environment.connect_issuer_id)
281
+ has_api_key_json = CommonHelper.is_validate_string(Environment.app_store_connect_api_key_path)
282
+ has_p8_key || has_api_key_json || options[:app_store_resources_enabled]
283
+ end
284
+
285
+ # 上传本次产物,并返回是否执行了上传,供主流程决定是否发送完成通知。
286
+ def self.upload_store_if_configured(options, version:, build:)
287
+ return false unless store_upload_configured?(options)
288
+
289
+ upload_options = options.dup
290
+ if upload_options[:app_store_resources_enabled]
291
+ # 使用真实产物信息覆盖资源配置,避免选中 JSON 中残留的旧版本或 Build。
292
+ upload_options.merge!(
293
+ app_identifier: Environment.bundleID,
294
+ app_version: version.to_s,
295
+ build_number: build.to_s
296
+ )
297
+ end
298
+ other_action.upload_store(upload_options)
299
+ true
300
+ end
301
+
302
+ private_class_method :store_upload_options, :store_upload_configured?, :upload_store_if_configured
303
+
258
304
  def self.description
259
305
  '打包'
260
306
  end
261
307
 
262
308
  def self.available_options
263
309
  [
310
+ FastlaneCore::ConfigItem.new(
311
+ key: :app_store_resources_enabled,
312
+ env_name: 'APP_STORE_RESOURCES_ENABLED',
313
+ description: '商店资源总开关,默认关闭,仅 app-store 打包时生效',
314
+ optional: true,
315
+ default_value: false,
316
+ type: Boolean
317
+ ),
318
+ FastlaneCore::ConfigItem.new(
319
+ key: :app_store_resources_config_path,
320
+ env_name: 'APP_STORE_RESOURCES_CONFIG_PATH',
321
+ description: 'App Store 资源发布 JSON 路径,开启商店资源总开关后必填',
322
+ optional: true,
323
+ type: String
324
+ ),
325
+ FastlaneCore::ConfigItem.new(
326
+ key: :is_upload_zealot_dsym,
327
+ env_name: 'ZEALOT_UPLOAD_DSYM',
328
+ description: 'IPA 上传 Zealot 成功后继续上传 iOS dSYM',
329
+ optional: true,
330
+ default_value: false,
331
+ type: Boolean
332
+ ),
264
333
  FastlaneCore::ConfigItem.new(
265
334
  key: :scheme,
266
335
  description: '不采取默认配置,自定义 `scheme` 名称',
@@ -0,0 +1,65 @@
1
+ require 'fastlane/plugin/fastci/helper/fastci_zealot_helper'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ # 手动同步 Apple 开发者账号中的 iOS 设备,不挂入自动打包流程。
6
+ class SyncZealotDevicesAction < ZealotSyncDevicesAction
7
+ extend Fastlane::Helper::FastciZealotHelper
8
+
9
+ def self.run(params)
10
+ validate_zealot_connection!(zealot_parameters(params))
11
+ Actions.lane_context.delete(SharedValues::ZEAALOT_ERROR_MESSAGE)
12
+ devices = fetch_ios_devices(params)
13
+ print_table(build_table_data(params, devices), title: 'sync_zealot_devices')
14
+
15
+ # 沿用官方的设备上传协议,按实际 HTTP 结果统计,避免失败被计为成功。
16
+ failed = devices.count { |device| !sync_ios_device(params, device) }
17
+ result = { total: devices.size, succeeded: devices.size - failed, failed: failed }
18
+ UI.message("Zealot iOS 设备同步:成功 #{result[:succeeded]},失败 #{failed}")
19
+ result
20
+ end
21
+
22
+ def self.fetch_ios_devices(params)
23
+ require 'spaceship'
24
+
25
+ token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path])
26
+ if token
27
+ Spaceship::ConnectAPI.token = token
28
+ return Spaceship::ConnectAPI::Device.all(filter: { platform: 'IOS' })
29
+ end
30
+
31
+ credentials = CredentialsManager::AccountManager.new(user: params[:username])
32
+ Spaceship.login(credentials.user, credentials.password)
33
+ Spaceship.select_team
34
+ Spaceship::Device.all(mac: false)
35
+ end
36
+ private_class_method :fetch_ios_devices
37
+
38
+ def self.sync_ios_device(params, device)
39
+ response = sync_deivce(params, device)
40
+ return true if response.success?
41
+
42
+ show_error("Zealot 设备同步失败(HTTP #{response.status})", params[:fail_on_error])
43
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError => error
44
+ show_error("Zealot 设备同步请求失败:#{error.class}", params[:fail_on_error])
45
+ end
46
+ private_class_method :sync_ios_device
47
+
48
+ def self.available_options
49
+ zealot_options(ZealotSyncDevicesAction, excluding: [:platform])
50
+ end
51
+
52
+ def self.description
53
+ '同步 Apple 开发者账号中的 iOS 设备到 Zealot'
54
+ end
55
+
56
+ def self.example_code
57
+ ['sync_zealot_devices(api_key_path: "./app_store_connect_api_key.json")']
58
+ end
59
+
60
+ def self.is_supported?(platform)
61
+ platform == :ios
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,4 +1,6 @@
1
1
  require 'fastlane/action'
2
+ require 'json'
3
+ require 'fastlane/plugin/fastci/helper/app_store_resources_helper'
2
4
  include Fastlane::Helper
3
5
 
4
6
  module Fastlane
@@ -6,62 +8,141 @@ module Fastlane
6
8
  # 上传 AppStore
7
9
  class UploadStoreAction < Action
8
10
  def self.run(params)
11
+ resource_params, api_key_options = prepare_upload(params)
9
12
 
10
- release_notes = JSON.parse(params[:release_notes] || "") rescue nil
13
+ if params[:isTestFlight]
14
+ upload_testflight(api_key_options)
15
+ else
16
+ upload_app_store(resource_params, api_key_options)
17
+ end
18
+ end
11
19
 
12
- other_action.app_store_connect_api_key(
13
- key_id: Environment.connect_key_id,
14
- issuer_id: Environment.connect_issuer_id,
15
- key_filepath: File.expand_path("#{Environment.package_file_folder_name}/AuthKey_#{Environment.connect_key_id}.p8"),
16
- duration: 1200, # optional (maximum 1200)
17
- in_house: false # optional but may be required if using match/sigh
20
+ # 上传前统一准备认证和资源,确保配置错误或资源缺失在 IPA 上传前被发现。
21
+ def self.prepare_upload(params)
22
+ resource_params = load_store_resources(params)
23
+ api_key_options = AppStoreResourcesHelper.authentication_options(resource_params || {}, other_action)
24
+ if resource_params
25
+ resource_params.merge!(api_key_options)
26
+ resource_params = AppStoreResourcesHelper.prepare(resource_params)
27
+ end
28
+ [resource_params, api_key_options]
29
+ end
30
+
31
+ # 只有显式开启的 App Store 流程读取资源配置,关闭时不解析路径或更新说明。
32
+ def self.load_store_resources(params)
33
+ config_path = AppStoreResourcesHelper.upload_config_path(params, app_store: !params[:isTestFlight])
34
+ return nil unless config_path
35
+
36
+ AppStoreResourcesHelper.load_params(
37
+ app_store_resources_enabled: true,
38
+ config_path: config_path,
39
+ app_identifier: params[:app_identifier],
40
+ app_version: params[:app_version],
41
+ build_number: params[:build_number],
42
+ release_notes: parse_release_notes(params[:release_notes])
18
43
  )
44
+ end
19
45
 
20
- if params[:isTestFlight]
21
- UI.message("*************| 开始上传 TestFlight |*************")
22
- other_action.upload_to_testflight(
23
- skip_waiting_for_build_processing: true
24
- )
25
- else
26
- UI.message("*************| 开始上传 AppStore |*************")
27
-
28
- # 构建上传参数,只有当 release_notes 有效时才添加
29
- upload_options = {
30
- skip_metadata: false,
31
- skip_screenshots: true,
32
- run_precheck_before_submit: false,
33
- precheck_include_in_app_purchases: false,
34
- force: true,
35
- submit_for_review: false,
36
- automatic_release: false
37
- }
38
-
39
- # 只有当 release_notes 不为 nil 且不为空时才添加
40
- if release_notes && !release_notes.empty?
41
- upload_options[:release_notes] = release_notes
42
- end
43
-
44
- other_action.upload_to_app_store(upload_options)
46
+ # 保留原 JSON 字符串接口;空值或无效 JSON 不覆盖资源文件中的更新说明。
47
+ def self.parse_release_notes(value)
48
+ JSON.parse(value || '')
49
+ rescue JSON::ParserError, TypeError
50
+ nil
51
+ end
52
+
53
+ # TestFlight 沿用现有上传方式,不执行 App Store 资源流程。
54
+ def self.upload_testflight(api_key_options)
55
+ UI.message('*************| 开始上传 TestFlight |*************')
56
+ other_action.upload_to_testflight({ skip_waiting_for_build_processing: true }.merge(api_key_options))
57
+ end
58
+
59
+ # 先上传 IPA,再处理已校验的资源;返回 IPA 上传结果以兼容已有 lane。
60
+ def self.upload_app_store(resource_params, api_key_options)
61
+ UI.message('*************| 开始上传 AppStore |*************')
62
+ upload_options = app_store_upload_options(resource_params).merge(api_key_options)
63
+ upload_result = other_action.upload_to_app_store(upload_options)
64
+ if resource_params
65
+ # 新版本判断已在 IPA 上传前完成,之后直接使用确认过的资源上传开关。
66
+ other_action.app_store_resources(resource_params.merge(upload_metadata_on_new_version: false))
45
67
  end
68
+ upload_result
46
69
  end
47
70
 
71
+ # IPA 阶段始终关闭资源上传,避免默认目录或 Deliverfile 绕过资源总开关。
72
+ def self.app_store_upload_options(resource_params)
73
+ options = {
74
+ skip_metadata: true,
75
+ skip_screenshots: true,
76
+ # deliver 的截图同步分支不受 skip_screenshots 控制,必须一并关闭。
77
+ sync_screenshots: false,
78
+ overwrite_screenshots: false,
79
+ run_precheck_before_submit: false,
80
+ precheck_include_in_app_purchases: false,
81
+ force: true,
82
+ submit_for_review: false,
83
+ automatic_release: false
84
+ }
85
+ if resource_params
86
+ options[:app_identifier] = resource_params[:app_identifier]
87
+ options[:app_version] = resource_params[:app_version] if resource_params[:app_version]
88
+ end
89
+ options
90
+ end
91
+
92
+ private_class_method :prepare_upload, :load_store_resources, :parse_release_notes,
93
+ :upload_testflight, :upload_app_store, :app_store_upload_options
94
+
48
95
  def self.description
49
96
  "上传 AppStore"
50
97
  end
51
98
 
52
99
  def self.available_options
53
100
  [
101
+ FastlaneCore::ConfigItem.new(
102
+ key: :app_store_resources_enabled,
103
+ env_name: 'APP_STORE_RESOURCES_ENABLED',
104
+ description: '商店资源总开关,默认关闭;关闭时仅上传 IPA,不更新元数据或截图',
105
+ optional: true,
106
+ default_value: false,
107
+ type: Boolean
108
+ ),
54
109
  FastlaneCore::ConfigItem.new(
55
110
  key: :release_notes,
56
111
  description: "更新文案, 格式为 { \"zh-Hans\": \"修复问题\", \"en-US\": \"bugfix\"} JSON 字符串",
57
- optional: false,
112
+ optional: true,
58
113
  type: String
59
114
  ),
60
115
  FastlaneCore::ConfigItem.new(
61
116
  key: :isTestFlight,
62
117
  description: "是否为 TestFlight 打包",
63
- optional: false,
118
+ optional: true,
119
+ default_value: false,
64
120
  type: Boolean
121
+ ),
122
+ FastlaneCore::ConfigItem.new(
123
+ key: :app_store_resources_config_path,
124
+ env_name: 'APP_STORE_RESOURCES_CONFIG_PATH',
125
+ description: 'App Store 资源发布 JSON 路径,开启商店资源总开关后必填',
126
+ optional: true,
127
+ type: String
128
+ ),
129
+ FastlaneCore::ConfigItem.new(
130
+ key: :app_identifier,
131
+ description: '本次构建的 Bundle ID,覆盖资源配置',
132
+ optional: true,
133
+ type: String
134
+ ),
135
+ FastlaneCore::ConfigItem.new(
136
+ key: :app_version,
137
+ description: '本次构建的版本号,覆盖资源配置',
138
+ optional: true,
139
+ type: String
140
+ ),
141
+ FastlaneCore::ConfigItem.new(
142
+ key: :build_number,
143
+ description: '本次构建的 Build 号,覆盖资源配置',
144
+ optional: true,
145
+ type: String
65
146
  )
66
147
  ]
67
148
  end
@@ -72,4 +153,4 @@ module Fastlane
72
153
 
73
154
  end
74
155
  end
75
- end
156
+ end
@@ -0,0 +1,58 @@
1
+ require 'fastlane/plugin/fastci/helper/fastci_zealot_helper'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ # iOS IPA 上传:沿用官方传输与结果解析,避免复制 Zealot HTTP 协议。
6
+ class UploadZealotAction < ZealotAction
7
+ extend Fastlane::Helper::FastciZealotHelper
8
+
9
+ def self.run(params)
10
+ validate_zealot_connection!(zealot_parameters(params))
11
+ UI.user_error!('请指定 IPA 文件或先执行 gym/package') if params[:file].to_s.empty?
12
+
13
+ # 同一 lane 可多次上传,失败时不能返回上一次上传的地址。
14
+ output.each { |key, _description| Actions.lane_context.delete(key.to_sym) }
15
+ super(params)
16
+ return nil unless Actions.lane_context[SharedValues::ZEALOT_RELEASE_ID]
17
+
18
+ {
19
+ app_id: Actions.lane_context[SharedValues::ZEALOT_APP_ID],
20
+ release_id: Actions.lane_context[SharedValues::ZEALOT_RELEASE_ID],
21
+ release_url: Actions.lane_context[SharedValues::ZEALOT_RELEASE_URL],
22
+ install_url: Actions.lane_context[SharedValues::ZEALOT_INSTALL_URL],
23
+ qrcode_url: Actions.lane_context[SharedValues::ZEALOT_QRCODE_URL]
24
+ }
25
+ end
26
+
27
+ def self.available_options
28
+ options = zealot_options(ZealotAction, excluding: [:file])
29
+ options << FastlaneCore::ConfigItem.new(
30
+ key: :file,
31
+ env_name: 'ZEALOT_FILE',
32
+ description: 'iOS IPA 路径,默认使用本次 gym 的产物',
33
+ optional: true,
34
+ default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
35
+ type: String,
36
+ verify_block: proc do |value|
37
+ unless File.file?(value) && File.extname(value).casecmp('.ipa').zero?
38
+ UI.user_error!("不是有效的 IPA 文件:#{value}")
39
+ end
40
+ end
41
+ )
42
+ options
43
+ end
44
+
45
+ def self.description
46
+ '上传 iOS IPA 到 Zealot,返回应用、版本和下载地址'
47
+ end
48
+
49
+ def self.example_code
50
+ ['upload_zealot(file: "./App.ipa", changelog: "iOS 更新说明")']
51
+ end
52
+
53
+ def self.is_supported?(platform)
54
+ platform == :ios
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ require 'fastlane/plugin/fastci/helper/fastci_zealot_helper'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ # 固定走官方 iOS dSYM 流程,不暴露 Android 或 macOS 参数。
6
+ class UploadZealotDsymAction < ZealotDebugFileAction
7
+ extend Fastlane::Helper::FastciZealotHelper
8
+
9
+ def self.run(params)
10
+ values = zealot_parameters(params)
11
+ validate_zealot_connection!(values)
12
+ values[:platform] = 'ios'
13
+ values[:zip_file] ||= Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH] unless values[:path]
14
+ values[:path] ||= Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE] unless values[:zip_file]
15
+
16
+ unless values[:zip_file] || values[:path]
17
+ UI.user_error!('请指定 dSYM zip_file、xcarchive path,或先执行 gym/package')
18
+ end
19
+
20
+ if values[:zip_file] && !File.file?(values[:zip_file])
21
+ UI.user_error!("dSYM 压缩文件不存在:#{values[:zip_file]}")
22
+ end
23
+ if values[:path] && !File.directory?(values[:path])
24
+ UI.user_error!("Xcode archive 不存在:#{values[:path]}")
25
+ end
26
+
27
+ Actions.lane_context.delete(SharedValues::ZEAALOT_ERROR_MESSAGE)
28
+ super(FastlaneCore::Configuration.create(ZealotDebugFileAction.available_options, values))
29
+ end
30
+
31
+ # 上游只检查响应中的 error 字段;非成功 HTTP 状态不能计为上传成功。
32
+ def self.parse_response(response, upload_url, fail_on_error)
33
+ return unless response
34
+
35
+ unless response.success? && response.body.is_a?(Hash)
36
+ return show_error("Zealot dSYM 上传失败(HTTP #{response.status})", fail_on_error)
37
+ end
38
+
39
+ super
40
+ end
41
+ private_class_method :parse_response
42
+
43
+ def self.available_options
44
+ zealot_options(
45
+ ZealotDebugFileAction,
46
+ excluding: [:platform, :android_build_type, :android_flavor]
47
+ )
48
+ end
49
+
50
+ def self.description
51
+ '上传 iOS dSYM 到 Zealot,支持 zip_file、xcarchive 或本次 gym 产物'
52
+ end
53
+
54
+ def self.example_code
55
+ ['upload_zealot_dsym(zip_file: "./App.app.dSYM.zip")']
56
+ end
57
+
58
+ def self.is_supported?(platform)
59
+ platform == :ios
60
+ end
61
+ end
62
+ end
63
+ end