pindo 5.13.1 → 5.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +692 -0
  3. data/lib/pindo/command/android/autobuild.rb +2 -2
  4. data/lib/pindo/command/appstore/adhocbuild.rb +256 -311
  5. data/lib/pindo/command/appstore/autobuild.rb +203 -0
  6. data/lib/pindo/command/appstore/autoresign.rb +35 -17
  7. data/lib/pindo/command/appstore/bundleid.rb +120 -0
  8. data/lib/pindo/command/appstore/cert.rb +212 -0
  9. data/lib/pindo/command/appstore/configproj.rb +81 -0
  10. data/lib/pindo/command/{deploy → appstore}/getitcinfo.rb +76 -91
  11. data/lib/pindo/command/appstore/iap.rb +788 -24
  12. data/lib/pindo/command/appstore/initconfig.rb +105 -0
  13. data/lib/pindo/command/appstore/itcapp.rb +95 -13
  14. data/lib/pindo/command/{deploy → appstore}/itcinfo.rb +90 -118
  15. data/lib/pindo/command/appstore/pem.rb +136 -0
  16. data/lib/pindo/command/appstore/pullconfig.rb +99 -0
  17. data/lib/pindo/command/appstore/quswark.rb +87 -0
  18. data/lib/pindo/command/appstore/quswauth.rb +67 -0
  19. data/lib/pindo/command/appstore/tag.rb +77 -0
  20. data/lib/pindo/command/appstore.rb +13 -1
  21. data/lib/pindo/command/env/quarkenv.rb +11 -13
  22. data/lib/pindo/command/env/swarkenv.rb +11 -16
  23. data/lib/pindo/command/ios/applovin.rb +24 -182
  24. data/lib/pindo/command/ios/autobuild.rb +64 -43
  25. data/lib/pindo/command/ios/autoresign.rb +34 -19
  26. data/lib/pindo/command/ios/build.rb +9 -6
  27. data/lib/pindo/command/ios/cert.rb +27 -20
  28. data/lib/pindo/command/ios/podupdate.rb +6 -37
  29. data/lib/pindo/command/jps/upload.rb +3 -3
  30. data/lib/pindo/command/unity/autobuild.rb +2 -2
  31. data/lib/pindo/command/utils/clearcert.rb +2 -17
  32. data/lib/pindo/command/{deploy → utils}/fabric.rb +13 -13
  33. data/lib/pindo/command/utils/renewcert.rb +62 -38
  34. data/lib/pindo/command/utils/renewproj.rb +0 -3
  35. data/lib/pindo/command/{deploy → utils}/updateconfig.rb +6 -7
  36. data/lib/pindo/command/utils.rb +2 -0
  37. data/lib/pindo/command/web/autobuild.rb +2 -2
  38. data/lib/pindo/command.rb +30 -3
  39. data/lib/pindo/config/build_info_manager.rb +176 -0
  40. data/lib/pindo/config/ios_config_parser.rb +404 -0
  41. data/lib/pindo/module/android/android_config_helper.rb +9 -5
  42. data/lib/pindo/module/appstore/bundleid_helper.rb +349 -0
  43. data/lib/pindo/module/appstore/itcapp_helper.rb +228 -0
  44. data/lib/pindo/module/build/build_helper.rb +12 -0
  45. data/lib/pindo/module/build/swark_helper.rb +116 -77
  46. data/lib/pindo/module/cert/cert_helper.rb +74 -0
  47. data/lib/pindo/module/cert/pem_helper.rb +72 -0
  48. data/lib/pindo/module/cert/{xcodecerthelper.rb → xcode_cert_helper.rb} +211 -6
  49. data/lib/pindo/module/pgyer/pgyerhelper.rb +13 -5
  50. data/lib/pindo/module/task/model/appstore/appstore_task.rb +18 -0
  51. data/lib/pindo/module/task/model/appstore/appstore_upload_ipa_task.rb +151 -0
  52. data/lib/pindo/module/task/model/appstore/appstore_upload_metadata_task.rb +250 -0
  53. data/lib/pindo/module/task/model/appstore/appstore_upload_screenshot_task.rb +276 -0
  54. data/lib/pindo/module/task/model/build/android_build_adhoc_task.rb +210 -0
  55. data/lib/pindo/module/task/model/build/{android_dev_build_task.rb → android_build_dev_task.rb} +2 -2
  56. data/lib/pindo/module/task/model/build/android_build_gplay_task.rb +210 -0
  57. data/lib/pindo/module/task/model/build/android_build_task.rb +13 -0
  58. data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +342 -0
  59. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +341 -0
  60. data/lib/pindo/module/task/model/build/{ios_dev_build_task.rb → ios_build_dev_task.rb} +40 -59
  61. data/lib/pindo/module/task/model/build/ios_build_task.rb +23 -0
  62. data/lib/pindo/module/task/model/build/{web_dev_build_task.rb → web_build_dev_task.rb} +1 -1
  63. data/lib/pindo/module/task/model/build_task.rb +15 -12
  64. data/lib/pindo/module/task/model/jps_resign_task.rb +185 -0
  65. data/lib/pindo/module/task/model/{upload_task.rb → jps_upload_task.rb} +3 -3
  66. data/lib/pindo/module/task/model/unity_export_task.rb +3 -1
  67. data/lib/pindo/module/unity/unity_helper.rb +2 -1
  68. data/lib/pindo/module/xcode/applovin_xcode_helper.rb +271 -0
  69. data/lib/pindo/module/xcode/cocoapods_helper.rb +153 -0
  70. data/lib/pindo/module/xcode/ipa_resign_helper.rb +210 -0
  71. data/lib/pindo/module/xcode/{xcodeappconfig.rb → xcode_app_config.rb} +79 -0
  72. data/lib/pindo/module/xcode/xcode_build_config.rb +152 -17
  73. data/lib/pindo/module/xcode/xcode_build_helper.rb +151 -1
  74. data/lib/pindo/module/xcode/xcode_swark_helper.rb +341 -0
  75. data/lib/pindo/options/core/global_options_state.rb +268 -0
  76. data/lib/pindo/options/core/option_configuration.rb +206 -0
  77. data/lib/pindo/options/core/option_initializer.rb +51 -0
  78. data/lib/pindo/options/core/option_item.rb +144 -0
  79. data/lib/pindo/options/core/option_value_parser.rb +54 -0
  80. data/lib/pindo/options/groups/build_options.rb +60 -0
  81. data/lib/pindo/options/groups/jps_options.rb +70 -0
  82. data/lib/pindo/options/groups/option_group.rb +73 -0
  83. data/lib/pindo/options/helpers/bundleid_selector.rb +103 -0
  84. data/lib/pindo/options/options.rb +14 -0
  85. data/lib/pindo/version.rb +1 -1
  86. metadata +51 -40
  87. data/lib/pindo/command/appstore/import.rb +0 -259
  88. data/lib/pindo/command/deploy/build.rb +0 -250
  89. data/lib/pindo/command/deploy/bundleid.rb +0 -259
  90. data/lib/pindo/command/deploy/cert.rb +0 -202
  91. data/lib/pindo/command/deploy/check.rb +0 -93
  92. data/lib/pindo/command/deploy/configproj.rb +0 -120
  93. data/lib/pindo/command/deploy/confusecode.rb +0 -262
  94. data/lib/pindo/command/deploy/confuseproj.rb +0 -122
  95. data/lib/pindo/command/deploy/iap.rb +0 -826
  96. data/lib/pindo/command/deploy/initconfig.rb +0 -138
  97. data/lib/pindo/command/deploy/itcapp.rb +0 -146
  98. data/lib/pindo/command/deploy/pem.rb +0 -55
  99. data/lib/pindo/command/deploy/pullconfig.rb +0 -56
  100. data/lib/pindo/command/deploy/pushconfig.rb +0 -93
  101. data/lib/pindo/command/deploy/quswark.rb +0 -156
  102. data/lib/pindo/command/deploy/quswauth.rb +0 -76
  103. data/lib/pindo/command/deploy/reportbug.rb +0 -145
  104. data/lib/pindo/command/deploy/resign.rb +0 -300
  105. data/lib/pindo/command/deploy/tag.rb +0 -108
  106. data/lib/pindo/command/deploy/uploadipa.rb +0 -73
  107. data/lib/pindo/command/deploy.rb +0 -42
  108. data/lib/pindo/command/dev/autobuild.rb +0 -117
  109. data/lib/pindo/command/dev/build.rb +0 -94
  110. data/lib/pindo/command/dev/debug.rb +0 -112
  111. data/lib/pindo/module/task/model/build/android_release_build_task.rb +0 -29
  112. data/lib/pindo/module/task/model/build/ios_adhoc_build_task.rb +0 -53
  113. data/lib/pindo/module/task/model/build/ios_release_build_task.rb +0 -53
  114. data/lib/pindo/options/appconfigoptions.rb +0 -24
  115. data/lib/pindo/options/deployoptions.rb +0 -372
@@ -0,0 +1,18 @@
1
+ require_relative '../pindo_task'
2
+
3
+ module Pindo
4
+ module TaskSystem
5
+ # App Store 任务基类
6
+ # 所有 App Store 相关任务的抽象基类
7
+ # 子类包括:AppStoreUploadIpaTask、AppStoreUploadMetadataTask、AppStoreUploadScreenshotTask
8
+ class AppStoreTask < PindoTask
9
+ # 空基类,仅用于类型标识
10
+ # 所有具体实现由子类完成
11
+
12
+ # 任务类型
13
+ def self.task_type
14
+ :appstore
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,151 @@
1
+ require_relative 'appstore_task'
2
+ require_relative '../task_config'
3
+
4
+ module Pindo
5
+ module TaskSystem
6
+ # App Store IPA 上传任务
7
+ # 上传 IPA 文件到 App Store Connect
8
+ class AppStoreUploadIpaTask < AppStoreTask
9
+ attr_reader :ipa_path, :ipa_file
10
+
11
+ # 重试配置
12
+ def self.default_retry_mode
13
+ RetryMode::DELAYED # 延迟重试
14
+ end
15
+
16
+ def self.default_retry_count
17
+ 3 # 默认可以重试 3 次
18
+ end
19
+
20
+ def self.default_retry_delay
21
+ 30 # 默认延迟 30 秒(App Store 上传较慢)
22
+ end
23
+
24
+ # 初始化 IPA 上传任务
25
+ # @param ipa_path [String] 搜索 IPA 文件的路径
26
+ # @param ipa_file [String] 要上传的 IPA 文件(nil 表示自动查找)
27
+ # @param options [Hash] 选项
28
+ # @option options [String] :apple_id Apple ID(App Store Connect 账号)
29
+ # @option options [String] :app_password App专用密码或API Key
30
+ # @option options [String] :bundle_id Bundle ID
31
+ # @option options [Boolean] :skip_validation 是否跳过验证(默认 false)
32
+ # @option options [String] :platform 平台(ios/osx,默认 ios)
33
+ def initialize(ipa_path, ipa_file = nil, options = {})
34
+ @ipa_path = ipa_path # 搜索 IPA 文件的路径
35
+ @ipa_file = ipa_file # 要上传的 IPA 文件(nil 表示自动查找)
36
+
37
+ # App Store Connect 配置
38
+ @apple_id = options[:apple_id]
39
+ @app_password = options[:app_password]
40
+ @bundle_id = options[:bundle_id]
41
+ @skip_validation = options[:skip_validation] || false
42
+ @platform = options[:platform] || 'ios'
43
+
44
+ # 设置上传任务的优先级为 LOW,确保在构建任务之后执行
45
+ options[:priority] ||= TaskPriority::LOW
46
+
47
+ super("上传 IPA 到 App Store", options)
48
+ end
49
+
50
+ def validate
51
+ # 验证基本参数
52
+ unless @ipa_path && !@ipa_path.empty?
53
+ @error = "缺少必需参数: ipa_path"
54
+ return false
55
+ end
56
+
57
+ # IPA 文件会在 do_work 中查找,这里不验证
58
+
59
+ true
60
+ end
61
+
62
+ protected
63
+
64
+ def do_work
65
+ # 1. 确定上传的 IPA 文件
66
+ if @ipa_file && !@ipa_file.empty? && File.exist?(@ipa_file)
67
+ # 使用指定文件
68
+ file_to_upload = @ipa_file
69
+ puts " 使用指定的 IPA 文件: #{file_to_upload}"
70
+ else
71
+ # 在 ipa_path 下查找最新的 IPA 文件
72
+ file_to_upload = find_latest_ipa
73
+ unless file_to_upload && File.exist?(file_to_upload)
74
+ # 文件不存在,将重试次数设为 0,不再重试
75
+ @retry_count = 0
76
+ raise "未找到 IPA 文件(路径: #{@ipa_path})"
77
+ end
78
+ puts " 找到 IPA 文件: #{file_to_upload}"
79
+ end
80
+
81
+ # 2. 上传到 App Store Connect
82
+ upload_to_appstore(file_to_upload)
83
+
84
+ {
85
+ success: true,
86
+ ipa_path: file_to_upload
87
+ }
88
+ end
89
+
90
+ private
91
+
92
+ # 在 ipa_path 下查找最新的 IPA 文件
93
+ def find_latest_ipa
94
+ search_pattern = File.join(@ipa_path, "**", "*.ipa")
95
+ ipa_files = Dir.glob(search_pattern)
96
+
97
+ if ipa_files.any?
98
+ # 返回修改时间最新的文件
99
+ latest_ipa = ipa_files.max_by { |f| File.mtime(f) }
100
+ return latest_ipa
101
+ end
102
+
103
+ nil
104
+ end
105
+
106
+ # 上传 IPA 到 App Store Connect
107
+ def upload_to_appstore(ipa_file)
108
+ puts "\n开始上传 IPA 到 App Store Connect..."
109
+ puts " IPA 文件: #{ipa_file}"
110
+ puts " Apple ID: #{@apple_id}" if @apple_id
111
+ puts " Bundle ID: #{@bundle_id}" if @bundle_id
112
+ puts " 平台: #{@platform}"
113
+ puts " 跳过验证: #{@skip_validation}"
114
+
115
+ # 使用 xcrun altool 上传
116
+ upload_with_altool(ipa_file)
117
+
118
+ puts "✓ IPA 上传成功"
119
+ end
120
+
121
+ # 使用 xcrun altool 上传 IPA
122
+ def upload_with_altool(ipa_file)
123
+ # 构建 altool 命令
124
+ cmd = ["xcrun", "altool"]
125
+ cmd << "--upload-app"
126
+ cmd << "--type" << @platform
127
+ cmd << "--file" << ipa_file
128
+
129
+ # 添加认证参数
130
+ if @apple_id && @app_password
131
+ cmd << "--username" << @apple_id
132
+ cmd << "--password" << @app_password
133
+ end
134
+
135
+ # 跳过验证
136
+ if @skip_validation
137
+ cmd << "--skip-validation"
138
+ end
139
+
140
+ # 执行上传命令
141
+ puts " 执行命令: #{cmd.join(' ')}"
142
+
143
+ success = system(*cmd)
144
+
145
+ unless success
146
+ raise "上传失败:xcrun altool 命令执行出错(退出码: #{$?.exitstatus})"
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,250 @@
1
+ require_relative 'appstore_task'
2
+ require_relative '../task_config'
3
+
4
+ module Pindo
5
+ module TaskSystem
6
+ # App Store Metadata 上传任务
7
+ # 上传应用元数据(名称、描述、关键词等)到 App Store Connect
8
+ class AppStoreUploadMetadataTask < AppStoreTask
9
+ attr_reader :app_id, :metadata_path
10
+
11
+ # 重试配置
12
+ def self.default_retry_mode
13
+ RetryMode::DELAYED # 延迟重试
14
+ end
15
+
16
+ def self.default_retry_count
17
+ 2 # 默认可以重试 2 次
18
+ end
19
+
20
+ def self.default_retry_delay
21
+ 10 # 默认延迟 10 秒
22
+ end
23
+
24
+ # 初始化 Metadata 上传任务
25
+ # @param app_id [String] App Store Connect 应用 ID
26
+ # @param metadata_path [String] 元数据配置文件路径或目录
27
+ # @param options [Hash] 选项
28
+ # @option options [String] :apple_id Apple ID(App Store Connect 账号)
29
+ # @option options [String] :api_key_id API Key ID(使用 API Key 认证)
30
+ # @option options [String] :api_issuer_id API Issuer ID
31
+ # @option options [String] :api_key_path API Key 文件路径(.p8 文件)
32
+ # @option options [Hash] :metadata 元数据内容(如果不从文件读取)
33
+ # @option options [String] :locale 语言区域(默认 "zh-Hans")
34
+ # @option options [Boolean] :skip_screenshots 是否跳过截图上传(默认 false)
35
+ def initialize(app_id, metadata_path = nil, options = {})
36
+ @app_id = app_id # App Store Connect 应用 ID
37
+ @metadata_path = metadata_path # 元数据配置文件路径或目录
38
+
39
+ # App Store Connect 认证配置
40
+ @apple_id = options[:apple_id]
41
+ @api_key_id = options[:api_key_id]
42
+ @api_issuer_id = options[:api_issuer_id]
43
+ @api_key_path = options[:api_key_path]
44
+
45
+ # 元数据内容
46
+ @metadata = options[:metadata] || {}
47
+ @locale = options[:locale] || "zh-Hans"
48
+ @skip_screenshots = options[:skip_screenshots] || false
49
+
50
+ # 设置上传任务的优先级为 NORMAL
51
+ options[:priority] ||= TaskPriority::NORMAL
52
+
53
+ super("上传 Metadata 到 App Store", options)
54
+ end
55
+
56
+ def validate
57
+ # 验证基本参数
58
+ unless @app_id && !@app_id.empty?
59
+ @error = "缺少必需参数: app_id"
60
+ return false
61
+ end
62
+
63
+ # 验证认证信息(Apple ID 或 API Key)
64
+ has_apple_id_auth = @apple_id && !@apple_id.empty?
65
+ has_api_key_auth = @api_key_id && @api_issuer_id && @api_key_path
66
+
67
+ unless has_apple_id_auth || has_api_key_auth
68
+ @error = "缺少认证信息:需要提供 Apple ID 或 API Key"
69
+ return false
70
+ end
71
+
72
+ # 验证元数据来源
73
+ has_metadata_path = @metadata_path && !@metadata_path.empty?
74
+ has_metadata_content = @metadata && !@metadata.empty?
75
+
76
+ unless has_metadata_path || has_metadata_content
77
+ @error = "缺少元数据:需要提供 metadata_path 或 metadata 内容"
78
+ return false
79
+ end
80
+
81
+ true
82
+ end
83
+
84
+ protected
85
+
86
+ def do_work
87
+ # 1. 加载元数据
88
+ metadata_to_upload = load_metadata
89
+
90
+ # 2. 上传元数据到 App Store Connect
91
+ upload_metadata(metadata_to_upload)
92
+
93
+ {
94
+ success: true,
95
+ app_id: @app_id,
96
+ locale: @locale
97
+ }
98
+ end
99
+
100
+ private
101
+
102
+ # 加载元数据
103
+ def load_metadata
104
+ # 如果已经有元数据内容,直接使用
105
+ return @metadata unless @metadata.empty?
106
+
107
+ # 从文件或目录加载
108
+ if @metadata_path && File.exist?(@metadata_path)
109
+ if File.directory?(@metadata_path)
110
+ load_metadata_from_directory
111
+ else
112
+ load_metadata_from_file
113
+ end
114
+ else
115
+ raise "元数据路径不存在: #{@metadata_path}"
116
+ end
117
+ end
118
+
119
+ # 从目录加载元数据(Fastlane metadata 格式)
120
+ def load_metadata_from_directory
121
+ puts " 从目录加载元数据: #{@metadata_path}"
122
+
123
+ metadata = {}
124
+ locale_path = File.join(@metadata_path, @locale)
125
+
126
+ unless File.directory?(locale_path)
127
+ raise "未找到语言目录: #{locale_path}"
128
+ end
129
+
130
+ # 读取各个元数据文件
131
+ metadata_files = {
132
+ name: 'name.txt',
133
+ subtitle: 'subtitle.txt',
134
+ description: 'description.txt',
135
+ keywords: 'keywords.txt',
136
+ marketing_url: 'marketing_url.txt',
137
+ support_url: 'support_url.txt',
138
+ privacy_url: 'privacy_policy_url.txt',
139
+ promotional_text: 'promotional_text.txt',
140
+ release_notes: 'release_notes.txt'
141
+ }
142
+
143
+ metadata_files.each do |key, filename|
144
+ file_path = File.join(locale_path, filename)
145
+ if File.exist?(file_path)
146
+ content = File.read(file_path).strip
147
+ metadata[key] = content unless content.empty?
148
+ end
149
+ end
150
+
151
+ metadata
152
+ end
153
+
154
+ # 从 JSON/YAML 文件加载元数据
155
+ def load_metadata_from_file
156
+ puts " 从文件加载元数据: #{@metadata_path}"
157
+
158
+ case File.extname(@metadata_path)
159
+ when '.json'
160
+ require 'json'
161
+ JSON.parse(File.read(@metadata_path))
162
+ when '.yml', '.yaml'
163
+ require 'yaml'
164
+ YAML.load_file(@metadata_path)
165
+ else
166
+ raise "不支持的元数据文件格式: #{@metadata_path}"
167
+ end
168
+ end
169
+
170
+ # 上传元数据到 App Store Connect
171
+ def upload_metadata(metadata)
172
+ puts "\n开始上传 Metadata 到 App Store Connect..."
173
+ puts " App ID: #{@app_id}"
174
+ puts " 语言区域: #{@locale}"
175
+ puts " 元数据项: #{metadata.keys.join(', ')}"
176
+
177
+ # 使用 Fastlane deliver 或 API 上传元数据
178
+ if use_api_key?
179
+ upload_with_api(metadata)
180
+ else
181
+ upload_with_fastlane(metadata)
182
+ end
183
+
184
+ puts "✓ Metadata 上传成功"
185
+ end
186
+
187
+ # 判断是否使用 API Key 认证
188
+ def use_api_key?
189
+ @api_key_id && @api_issuer_id && @api_key_path
190
+ end
191
+
192
+ # 使用 App Store Connect API 上传
193
+ def upload_with_api(metadata)
194
+ puts " 使用 App Store Connect API 上传..."
195
+
196
+ # 这里需要调用 App Store Connect API Helper
197
+ # require 'pindo/module/appstore/appstore_metadata_connect_api_helper'
198
+
199
+ # 示例实现(需要根据实际 Helper 调整)
200
+ # AppstoreMetadataConnectApiHelper.upload_metadata(
201
+ # app_id: @app_id,
202
+ # api_key_id: @api_key_id,
203
+ # api_issuer_id: @api_issuer_id,
204
+ # api_key_path: @api_key_path,
205
+ # locale: @locale,
206
+ # metadata: metadata
207
+ # )
208
+
209
+ # 临时占位实现
210
+ puts " TODO: 实现 API 上传逻辑"
211
+ raise "API 上传功能尚未实现,请使用 Fastlane 方式"
212
+ end
213
+
214
+ # 使用 Fastlane deliver 上传
215
+ def upload_with_fastlane(metadata)
216
+ puts " 使用 Fastlane deliver 上传..."
217
+
218
+ # 构建 deliver 命令参数
219
+ fastlane_cmd = ["fastlane", "deliver"]
220
+ fastlane_cmd << "--app_identifier" << @app_id
221
+ fastlane_cmd << "--username" << @apple_id if @apple_id
222
+
223
+ # 如果有元数据路径,使用路径
224
+ if @metadata_path && File.exist?(@metadata_path)
225
+ fastlane_cmd << "--metadata_path" << @metadata_path
226
+ end
227
+
228
+ # 跳过截图
229
+ if @skip_screenshots
230
+ fastlane_cmd << "--skip_screenshots"
231
+ end
232
+
233
+ # 跳过二进制文件上传
234
+ fastlane_cmd << "--skip_binary_upload"
235
+
236
+ # 自动提交审核(可选)
237
+ # fastlane_cmd << "--submit_for_review"
238
+
239
+ # 执行命令
240
+ puts " 执行命令: #{fastlane_cmd.join(' ')}"
241
+
242
+ success = system(*fastlane_cmd)
243
+
244
+ unless success
245
+ raise "上传失败:Fastlane deliver 命令执行出错(退出码: #{$?.exitstatus})"
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,276 @@
1
+ require_relative 'appstore_task'
2
+ require_relative '../task_config'
3
+
4
+ module Pindo
5
+ module TaskSystem
6
+ # App Store Screenshot 上传任务
7
+ # 上传应用截图到 App Store Connect
8
+ class AppStoreUploadScreenshotTask < AppStoreTask
9
+ attr_reader :app_id, :screenshot_path
10
+
11
+ # 重试配置
12
+ def self.default_retry_mode
13
+ RetryMode::DELAYED # 延迟重试
14
+ end
15
+
16
+ def self.default_retry_count
17
+ 2 # 默认可以重试 2 次
18
+ end
19
+
20
+ def self.default_retry_delay
21
+ 15 # 默认延迟 15 秒
22
+ end
23
+
24
+ # 初始化 Screenshot 上传任务
25
+ # @param app_id [String] App Store Connect 应用 ID
26
+ # @param screenshot_path [String] 截图目录路径
27
+ # @param options [Hash] 选项
28
+ # @option options [String] :apple_id Apple ID(App Store Connect 账号)
29
+ # @option options [String] :api_key_id API Key ID(使用 API Key 认证)
30
+ # @option options [String] :api_issuer_id API Issuer ID
31
+ # @option options [String] :api_key_path API Key 文件路径(.p8 文件)
32
+ # @option options [String] :locale 语言区域(默认 "zh-Hans")
33
+ # @option options [Array<String>] :device_types 设备类型(如 ["iPhone 6.5", "iPad Pro"])
34
+ # @option options [Boolean] :overwrite 是否覆盖已有截图(默认 false)
35
+ def initialize(app_id, screenshot_path, options = {})
36
+ @app_id = app_id # App Store Connect 应用 ID
37
+ @screenshot_path = screenshot_path # 截图目录路径
38
+
39
+ # App Store Connect 认证配置
40
+ @apple_id = options[:apple_id]
41
+ @api_key_id = options[:api_key_id]
42
+ @api_issuer_id = options[:api_issuer_id]
43
+ @api_key_path = options[:api_key_path]
44
+
45
+ # 截图配置
46
+ @locale = options[:locale] || "zh-Hans"
47
+ @device_types = options[:device_types] || []
48
+ @overwrite = options[:overwrite] || false
49
+
50
+ # 设置上传任务的优先级为 LOW(截图上传可以最后执行)
51
+ options[:priority] ||= TaskPriority::LOW
52
+
53
+ super("上传 Screenshot 到 App Store", options)
54
+ end
55
+
56
+ def validate
57
+ # 验证基本参数
58
+ unless @app_id && !@app_id.empty?
59
+ @error = "缺少必需参数: app_id"
60
+ return false
61
+ end
62
+
63
+ unless @screenshot_path && !@screenshot_path.empty?
64
+ @error = "缺少必需参数: screenshot_path"
65
+ return false
66
+ end
67
+
68
+ # 验证截图路径
69
+ unless File.directory?(@screenshot_path)
70
+ @error = "截图路径不存在或不是目录: #{@screenshot_path}"
71
+ return false
72
+ end
73
+
74
+ # 验证认证信息(Apple ID 或 API Key)
75
+ has_apple_id_auth = @apple_id && !@apple_id.empty?
76
+ has_api_key_auth = @api_key_id && @api_issuer_id && @api_key_path
77
+
78
+ unless has_apple_id_auth || has_api_key_auth
79
+ @error = "缺少认证信息:需要提供 Apple ID 或 API Key"
80
+ return false
81
+ end
82
+
83
+ true
84
+ end
85
+
86
+ protected
87
+
88
+ def do_work
89
+ # 1. 扫描截图文件
90
+ screenshots = scan_screenshots
91
+
92
+ if screenshots.empty?
93
+ puts " 警告: 未找到任何截图文件"
94
+ return {
95
+ success: true,
96
+ app_id: @app_id,
97
+ screenshot_count: 0
98
+ }
99
+ end
100
+
101
+ # 2. 上传截图到 App Store Connect
102
+ upload_screenshots(screenshots)
103
+
104
+ {
105
+ success: true,
106
+ app_id: @app_id,
107
+ locale: @locale,
108
+ screenshot_count: screenshots.size
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ # 扫描截图文件
115
+ def scan_screenshots
116
+ puts " 扫描截图目录: #{@screenshot_path}"
117
+
118
+ screenshots = []
119
+
120
+ # 支持的截图格式
121
+ valid_extensions = ['.png', '.jpg', '.jpeg']
122
+
123
+ # 扫描目录结构
124
+ # 标准 Fastlane 结构: screenshots/{locale}/{device_type}/*.png
125
+ locale_path = File.join(@screenshot_path, @locale)
126
+
127
+ if File.directory?(locale_path)
128
+ # 有语言目录
129
+ scan_locale_directory(locale_path, screenshots, valid_extensions)
130
+ else
131
+ # 直接扫描根目录
132
+ scan_root_directory(@screenshot_path, screenshots, valid_extensions)
133
+ end
134
+
135
+ puts " 找到 #{screenshots.size} 个截图文件"
136
+ screenshots
137
+ end
138
+
139
+ # 扫描语言目录
140
+ def scan_locale_directory(locale_path, screenshots, valid_extensions)
141
+ # 遍历设备类型目录
142
+ Dir.glob(File.join(locale_path, "*")).each do |device_dir|
143
+ next unless File.directory?(device_dir)
144
+
145
+ device_type = File.basename(device_dir)
146
+
147
+ # 如果指定了设备类型,跳过不匹配的
148
+ unless @device_types.empty?
149
+ next unless @device_types.include?(device_type)
150
+ end
151
+
152
+ # 扫描设备目录下的截图
153
+ Dir.glob(File.join(device_dir, "*")).each do |file|
154
+ next unless File.file?(file)
155
+ next unless valid_extensions.include?(File.extname(file).downcase)
156
+
157
+ screenshots << {
158
+ path: file,
159
+ device_type: device_type,
160
+ locale: @locale,
161
+ filename: File.basename(file)
162
+ }
163
+ end
164
+ end
165
+ end
166
+
167
+ # 扫描根目录(无语言/设备分类)
168
+ def scan_root_directory(root_path, screenshots, valid_extensions)
169
+ Dir.glob(File.join(root_path, "**", "*")).each do |file|
170
+ next unless File.file?(file)
171
+ next unless valid_extensions.include?(File.extname(file).downcase)
172
+
173
+ screenshots << {
174
+ path: file,
175
+ device_type: 'unknown',
176
+ locale: @locale,
177
+ filename: File.basename(file)
178
+ }
179
+ end
180
+ end
181
+
182
+ # 上传截图到 App Store Connect
183
+ def upload_screenshots(screenshots)
184
+ puts "\n开始上传 Screenshot 到 App Store Connect..."
185
+ puts " App ID: #{@app_id}"
186
+ puts " 语言区域: #{@locale}"
187
+ puts " 截图数量: #{screenshots.size}"
188
+ puts " 覆盖模式: #{@overwrite}"
189
+
190
+ # 按设备类型分组
191
+ screenshots_by_device = screenshots.group_by { |s| s[:device_type] }
192
+
193
+ screenshots_by_device.each do |device_type, device_screenshots|
194
+ puts "\n 上传 #{device_type} 截图 (#{device_screenshots.size} 张)..."
195
+
196
+ device_screenshots.each_with_index do |screenshot, index|
197
+ puts " [#{index + 1}/#{device_screenshots.size}] #{screenshot[:filename]}"
198
+ end
199
+ end
200
+
201
+ # 使用 Fastlane deliver 或 API 上传截图
202
+ if use_api_key?
203
+ upload_with_api(screenshots)
204
+ else
205
+ upload_with_fastlane(screenshots)
206
+ end
207
+
208
+ puts "✓ Screenshot 上传成功"
209
+ end
210
+
211
+ # 判断是否使用 API Key 认证
212
+ def use_api_key?
213
+ @api_key_id && @api_issuer_id && @api_key_path
214
+ end
215
+
216
+ # 使用 App Store Connect API 上传
217
+ def upload_with_api(screenshots)
218
+ puts " 使用 App Store Connect API 上传..."
219
+
220
+ # 这里需要调用 App Store Connect API Helper
221
+ # require 'pindo/module/appstore/appstore_metadata_connect_api_helper'
222
+
223
+ # 示例实现(需要根据实际 Helper 调整)
224
+ # screenshots.each do |screenshot|
225
+ # AppstoreMetadataConnectApiHelper.upload_screenshot(
226
+ # app_id: @app_id,
227
+ # api_key_id: @api_key_id,
228
+ # api_issuer_id: @api_issuer_id,
229
+ # api_key_path: @api_key_path,
230
+ # locale: screenshot[:locale],
231
+ # device_type: screenshot[:device_type],
232
+ # screenshot_path: screenshot[:path],
233
+ # overwrite: @overwrite
234
+ # )
235
+ # end
236
+
237
+ # 临时占位实现
238
+ puts " TODO: 实现 API 上传逻辑"
239
+ raise "API 上传功能尚未实现,请使用 Fastlane 方式"
240
+ end
241
+
242
+ # 使用 Fastlane deliver 上传
243
+ def upload_with_fastlane(screenshots)
244
+ puts " 使用 Fastlane deliver 上传..."
245
+
246
+ # 构建 deliver 命令参数
247
+ fastlane_cmd = ["fastlane", "deliver"]
248
+ fastlane_cmd << "--app_identifier" << @app_id
249
+ fastlane_cmd << "--username" << @apple_id if @apple_id
250
+
251
+ # 截图路径
252
+ fastlane_cmd << "--screenshots_path" << @screenshot_path
253
+
254
+ # 覆盖已有截图
255
+ if @overwrite
256
+ fastlane_cmd << "--overwrite_screenshots"
257
+ end
258
+
259
+ # 跳过元数据上传
260
+ fastlane_cmd << "--skip_metadata"
261
+
262
+ # 跳过二进制文件上传
263
+ fastlane_cmd << "--skip_binary_upload"
264
+
265
+ # 执行命令
266
+ puts " 执行命令: #{fastlane_cmd.join(' ')}"
267
+
268
+ success = system(*fastlane_cmd)
269
+
270
+ unless success
271
+ raise "上传失败:Fastlane deliver 命令执行出错(退出码: #{$?.exitstatus})"
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end