pindo 5.12.2 → 5.13.2

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/android.rb +0 -1
  5. data/lib/pindo/command/appstore/adhocbuild.rb +258 -311
  6. data/lib/pindo/command/appstore/autobuild.rb +203 -0
  7. data/lib/pindo/command/appstore/autoresign.rb +35 -17
  8. data/lib/pindo/command/appstore/bundleid.rb +120 -0
  9. data/lib/pindo/command/appstore/cert.rb +212 -0
  10. data/lib/pindo/command/appstore/configproj.rb +81 -0
  11. data/lib/pindo/command/{deploy → appstore}/getitcinfo.rb +76 -91
  12. data/lib/pindo/command/appstore/iap.rb +788 -24
  13. data/lib/pindo/command/appstore/initconfig.rb +105 -0
  14. data/lib/pindo/command/appstore/itcapp.rb +95 -13
  15. data/lib/pindo/command/{deploy → appstore}/itcinfo.rb +90 -118
  16. data/lib/pindo/command/appstore/pem.rb +136 -0
  17. data/lib/pindo/command/appstore/pullconfig.rb +99 -0
  18. data/lib/pindo/command/appstore/quswark.rb +87 -0
  19. data/lib/pindo/command/appstore/quswauth.rb +67 -0
  20. data/lib/pindo/command/appstore/tag.rb +77 -0
  21. data/lib/pindo/command/appstore.rb +13 -1
  22. data/lib/pindo/command/env/quarkenv.rb +11 -13
  23. data/lib/pindo/command/env/swarkenv.rb +11 -16
  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 +10 -7
  27. data/lib/pindo/command/ios/cert.rb +27 -20
  28. data/lib/pindo/command/jps/upload.rb +3 -3
  29. data/lib/pindo/command/unity/autobuild.rb +2 -2
  30. data/lib/pindo/command/utils/clearcert.rb +2 -17
  31. data/lib/pindo/command/{deploy → utils}/fabric.rb +13 -13
  32. data/lib/pindo/command/utils/renewcert.rb +62 -38
  33. data/lib/pindo/command/utils/renewproj.rb +0 -3
  34. data/lib/pindo/command/{deploy → utils}/updateconfig.rb +6 -7
  35. data/lib/pindo/command/utils.rb +2 -0
  36. data/lib/pindo/command/web/autobuild.rb +2 -2
  37. data/lib/pindo/command.rb +30 -3
  38. data/lib/pindo/config/build_info_manager.rb +176 -0
  39. data/lib/pindo/config/ios_config_parser.rb +404 -0
  40. data/lib/pindo/module/android/android_build_helper.rb +110 -0
  41. data/lib/pindo/module/android/android_config_helper.rb +9 -5
  42. data/lib/pindo/module/android/gradle_helper.rb +88 -0
  43. data/lib/pindo/module/appstore/bundleid_helper.rb +349 -0
  44. data/lib/pindo/module/appstore/itcapp_helper.rb +228 -0
  45. data/lib/pindo/module/build/build_helper.rb +12 -0
  46. data/lib/pindo/module/build/swark_helper.rb +116 -77
  47. data/lib/pindo/module/cert/cert_helper.rb +74 -0
  48. data/lib/pindo/module/cert/pem_helper.rb +72 -0
  49. data/lib/pindo/module/cert/{xcodecerthelper.rb → xcode_cert_helper.rb} +208 -6
  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 +197 -0
  59. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +367 -0
  60. data/lib/pindo/module/task/model/build/{ios_dev_build_task.rb → ios_build_dev_task.rb} +37 -27
  61. data/lib/pindo/module/task/model/build/ios_build_task.rb +13 -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/task/pindo_task.rb +19 -10
  68. data/lib/pindo/module/unity/unity_helper.rb +2 -1
  69. data/lib/pindo/module/xcode/ipa_resign_helper.rb +210 -0
  70. data/lib/pindo/module/xcode/{xcodeappconfig.rb → xcode_app_config.rb} +79 -0
  71. data/lib/pindo/module/xcode/xcode_build_config.rb +152 -17
  72. data/lib/pindo/module/xcode/xcode_build_helper.rb +151 -1
  73. data/lib/pindo/module/xcode/xcode_swark_helper.rb +341 -0
  74. data/lib/pindo/options/core/global_options_state.rb +268 -0
  75. data/lib/pindo/options/core/option_configuration.rb +206 -0
  76. data/lib/pindo/options/core/option_initializer.rb +51 -0
  77. data/lib/pindo/options/core/option_item.rb +144 -0
  78. data/lib/pindo/options/core/option_value_parser.rb +54 -0
  79. data/lib/pindo/options/groups/build_options.rb +60 -0
  80. data/lib/pindo/options/groups/jps_options.rb +70 -0
  81. data/lib/pindo/options/groups/option_group.rb +73 -0
  82. data/lib/pindo/options/helpers/bundleid_selector.rb +103 -0
  83. data/lib/pindo/options/options.rb +14 -0
  84. data/lib/pindo/version.rb +1 -1
  85. metadata +49 -41
  86. data/lib/pindo/command/android/build.rb +0 -186
  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
@@ -4,8 +4,139 @@ require 'pindo/base/aeshelper'
4
4
 
5
5
  module Pindo
6
6
 
7
- module XcodeCertHelper
8
-
7
+ class XcodeCertHelper
8
+
9
+ class << self
10
+
11
+ # 标准化构建类型
12
+ # @param build_type [String, Symbol] 构建类型 ('dev', 'adhoc', 'release', 'development', 'appstore')
13
+ # @return [String] 标准化后的证书类型 ('development', 'adhoc', 'appstore')
14
+ def normalize_build_type(build_type)
15
+ case build_type.to_s.downcase
16
+ when 'dev', 'development'
17
+ 'development'
18
+ when 'adhoc'
19
+ 'adhoc'
20
+ when 'release', 'appstore'
21
+ 'appstore'
22
+ else
23
+ raise ArgumentError, "无效的构建类型: #{build_type},支持的类型: dev/development, adhoc, release/appstore"
24
+ end
25
+ end
26
+
27
+ # 安装证书并配置 Xcode 工程
28
+ # @param build_type [String, Symbol] 构建类型 ('dev', 'adhoc', 'release' 或 'development', 'adhoc', 'appstore')
29
+ # @param platform_type [String] 平台类型 ('ios', 'macos')
30
+ # @param project_dir [String] 项目目录
31
+ # @param config_file [String, nil] 配置文件路径(可选,默认从 project_dir 加载)
32
+ # @param skip_xcode_config [Boolean] 是否跳过 Xcode 配置
33
+ # @return [Hash] 返回证书信息 { provisioning_info_array:, team_id: }
34
+ def install_and_config_certs(
35
+ build_type:,
36
+ platform_type: 'ios',
37
+ project_dir: Dir.pwd,
38
+ config_file: nil,
39
+ skip_xcode_config: false
40
+ )
41
+ # 标准化构建类型
42
+ cert_type = normalize_build_type(build_type)
43
+
44
+ # 配置加载
45
+ require 'pindo/config/ios_config_parser'
46
+ config_parser = Pindo::IosConfigParser.instance
47
+ config_path = config_file || File.join(project_dir, 'config.json')
48
+ config_parser.load_config(config_file: config_path)
49
+
50
+ apple_id = config_parser.apple_id
51
+ bundle_id_map = config_parser.get_bundle_id_map
52
+
53
+ # 安装证书
54
+ require 'pindo/config/pindoconfig'
55
+ require 'pindo/base/git_handler'
56
+ require 'pindo/module/cert/cert_helper'
57
+
58
+ pindo_config = Pindoconfig.instance
59
+
60
+ cert_git_url = apple_id.eql?(pindo_config.demo_apple_id) ?
61
+ pindo_config.dev_cert_giturl :
62
+ pindo_config.deploy_cert_giturl
63
+
64
+ cert_reponame = cert_git_url.split("/").last.chomp(".git")
65
+ certs_dir = Pindo::GitHandler.getcode_to_dir(
66
+ reponame: cert_reponame,
67
+ remote_url: cert_git_url,
68
+ path: pindo_config.pindo_dir,
69
+ new_branch: apple_id
70
+ )
71
+
72
+ # 安装证书和配置文件
73
+ Pindo::CertHelper.install_certs(
74
+ cert_url: cert_git_url,
75
+ certs_dir: certs_dir,
76
+ cert_type: cert_type,
77
+ platform_type: platform_type
78
+ )
79
+
80
+ provisioning_info_array = Pindo::CertHelper.install_provisionfiles(
81
+ cert_url: cert_git_url,
82
+ certs_dir: certs_dir,
83
+ bundle_id_map: bundle_id_map,
84
+ cert_type: cert_type,
85
+ platform_type: platform_type
86
+ )
87
+
88
+ pindo_config.set_cert_info(dict: provisioning_info_array)
89
+
90
+ raise Informative, "未找到证书信息" if provisioning_info_array.nil? || provisioning_info_array.empty?
91
+
92
+ team_id = provisioning_info_array.first["team_id"]
93
+
94
+ # Swark 授权检查
95
+ main_bundle_id = config_parser.bundle_id
96
+ if config_parser.config_json && main_bundle_id
97
+ require 'pindo/module/build/swark_helper'
98
+ Pindo::SwarkHelper.add_swark_authorize_json(
99
+ build_type: cert_type,
100
+ config_json: config_parser.config_json,
101
+ team_id: team_id,
102
+ bundle_id: main_bundle_id,
103
+ deploy_repo_name: main_bundle_id
104
+ )
105
+ end
106
+
107
+ # 配置 Xcode 工程 (如果需要)
108
+ unless skip_xcode_config
109
+ project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by { |f| File.mtime(f) }
110
+ if project_fullname && File.exist?(project_fullname)
111
+ proj_name = File.basename(project_fullname, ".xcodeproj")
112
+
113
+ Funlog.instance.fancyinfo_start("正在给Xcode配置证书...")
114
+ config_project_cert(
115
+ new_proj_name: proj_name,
116
+ new_project_dir: project_dir,
117
+ cert_type: cert_type,
118
+ platform_type: platform_type,
119
+ team_id_vaule: team_id,
120
+ provisioning_info_array: provisioning_info_array
121
+ )
122
+
123
+ config_infoplist_cert(
124
+ new_proj_name: proj_name,
125
+ new_project_dir: project_dir,
126
+ icloud_id: config_parser.icloud_id,
127
+ group_id: config_parser.group_id,
128
+ provisioning_info_array: provisioning_info_array
129
+ )
130
+ Funlog.instance.fancyinfo_success("Xcode配置证书完成!")
131
+ end
132
+ end
133
+
134
+ {
135
+ provisioning_info_array: provisioning_info_array,
136
+ team_id: team_id
137
+ }
138
+ end
139
+
9
140
  def get_target_name_map
10
141
  return {
11
142
  "MainTarget" => "bundle_id",
@@ -28,7 +159,9 @@ module Pindo
28
159
 
29
160
  provisioning_info_array = []
30
161
 
31
- bundle_id_map = get_bundle_id_map
162
+ # 使用 IosConfigParser 单例获取 bundle_id_map
163
+ config_parser = Pindo::IosConfigParser.instance
164
+ bundle_id_map = config_parser.get_bundle_id_map
32
165
 
33
166
  bundle_id_map.each do |type, bundle_id_temp|
34
167
  provisioning_info = {}
@@ -224,7 +357,29 @@ module Pindo
224
357
  end
225
358
 
226
359
  if target.product_type.to_s.eql?(Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application]) then
227
- add_swark_entitlement(entitlements_plist_path:entitlements_plist_path, bundle_id_dict:provisioning_info_array)
360
+ # 调用 SwarkHelper 添加权限配置
361
+ # 读取 config.json(如果存在)
362
+ config_json = nil
363
+ config_json_file = File.join(new_project_dir, "config.json")
364
+ if File.exist?(config_json_file)
365
+ config_json = JSON.parse(File.read(config_json_file))
366
+ end
367
+
368
+ # 获取 team_id 和 bundle_id
369
+ team_id = provisioning_info_array.first["team_id"] if provisioning_info_array && provisioning_info_array.any?
370
+ bundle_id = provisioning_info_array.first["bundle_id"] if provisioning_info_array && provisioning_info_array.any?
371
+
372
+ if config_json && team_id && bundle_id
373
+ require 'pindo/module/build/swark_helper'
374
+ Pindo::SwarkHelper.add_swark_entitlement(
375
+ entitlements_plist_path: entitlements_plist_path,
376
+ bundle_id_dict: provisioning_info_array,
377
+ config_json: config_json,
378
+ team_id: team_id,
379
+ bundle_id: bundle_id
380
+ )
381
+ end
382
+
228
383
  if !icloud_id.nil?
229
384
  modify_entitlements_plist(entitlements_plist_path:entitlements_plist_path, icloud_id:icloud_id)
230
385
  end
@@ -315,6 +470,7 @@ module Pindo
315
470
  end
316
471
 
317
472
  def create_upload_cert_info(apple_id:nil, cert_type:nil, platform_type:nil)
473
+ pindo_single_config = Pindoconfig.instance
318
474
 
319
475
  cert_dest_dir = File.join(Dir.pwd, "cert")
320
476
  if !File.exist?(cert_dest_dir)
@@ -483,6 +639,52 @@ module Pindo
483
639
  return account_cert_set
484
640
  end
485
641
 
642
+ def get_create_cert_match_values(apple_id:nil, bundle_id_array:nil, build_type:nil, platform_type:nil, renew_flag:false)
643
+ pindo_single_config = Pindoconfig.instance
644
+
645
+ if build_type.eql?("appstore") && (apple_id.eql?(pindo_single_config.demo_apple_id))
646
+ raise Informative, "#{apple_id} 是测试账号,不能创建appstore证书!!!"
647
+ end
648
+ if !build_type.eql?("appstore") && !apple_id.eql?(pindo_single_config.demo_apple_id)
649
+ raise Informative, "账号#{apple_id} 不能创建dev或者adhoc证书!!!"
650
+ end
651
+
652
+ git_url = pindo_single_config.deploy_cert_giturl
653
+ if apple_id.eql?(pindo_single_config.demo_apple_id)
654
+ git_url = pindo_single_config.dev_cert_giturl
655
+ end
656
+
657
+ force_for_new_devices_flag = true
658
+ if build_type.eql?("appstore")
659
+ force_for_new_devices_flag = false
660
+ end
661
+
662
+ if platform_type.downcase.include?("macos") && build_type.eql?("adhoc")
663
+ build_type = "developer_id"
664
+ end
665
+
666
+ values = {
667
+ username:apple_id,
668
+ app_identifier: bundle_id_array,
669
+ type: build_type,
670
+ keychain_password:"goodcert1",
671
+ git_url: git_url,
672
+ readonly:!renew_flag,
673
+ force:renew_flag,
674
+ clone_branch_directly:!renew_flag,
675
+ include_mac_in_profiles:true,
676
+ include_all_certificates:true,
677
+ generate_apple_certs:true,
678
+ shallow_clone:!renew_flag,
679
+ git_branch: apple_id,
680
+ platform:platform_type,
681
+ force_for_new_devices:force_for_new_devices_flag
682
+ }
683
+ return values
684
+
685
+ end
686
+
687
+ end # class << self
486
688
 
487
- end
488
- end
689
+ end # class XcodeCertHelper
690
+ end # module Pindo
@@ -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