pindo 5.18.20 → 5.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 004b03010300629da1e55b7c6b34eada7cd76e55247deb8f76865d7446ef3572
4
- data.tar.gz: 44757caa7ef3e6ae5253bd17d4a0c35ebe0c4cb6120d7ee2840c1a010b6800d2
3
+ metadata.gz: db02bf00df627b07423ebd53475c538b60a4d5d8d567b2bb81a61390aaae73bc
4
+ data.tar.gz: 5252fb968e7bde86a095a78dce92599c9614061c1da9666e8cf2bea03927c8c6
5
5
  SHA512:
6
- metadata.gz: 05bd2c5264163a1ec12aa545e165f2bee0d832e2c038cd30d506c932842c024f9664d6ecd51c7aa80a71f061ffcaf4747a56caac6223b2c34fe4887845681fc6
7
- data.tar.gz: 03d477b345f080c1dfe28e5367786d2147029e86aac185c4aa698b8d17acc98064bee17958060be7d1f44ebb5a41e2790a228a9a4b4699d5d9e2bb6f99709a1d
6
+ metadata.gz: fa95f45065b4a8635c4d12c5a4c1d3a95e678df97724103857e055d9088d8f8613fddef8786229163bbd8ad01c87242e396e5b79e1c5f4d92411b61192e3bde1
7
+ data.tar.gz: 931bd858133452f7cc5a3edafc60cbe7c4f56b5d0f8da6d720f24c23f7974cfe2c823e0bbc7e1d1a04bc8f99faa581ef7c52ae99b0676ec6cfc4e407411e3e05
@@ -86,8 +86,7 @@ module Pindo
86
86
  end
87
87
 
88
88
  def add_branch(local_repo_dir: nil, branch: nil)
89
-
90
- current=Dir.pwd
89
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
91
90
  result = false
92
91
 
93
92
  if !local_branch_exists?(local_repo_dir: local_repo_dir, branch: branch)
@@ -97,13 +96,11 @@ module Pindo
97
96
 
98
97
  git_remote!(local_repo_dir, %W(-C #{local_repo_dir} push origin #{branch}:#{branch}))
99
98
 
100
- Dir.chdir(current)
101
99
  return result
102
100
  end
103
101
 
104
102
  def remove_branch(local_repo_dir: nil, branch: nil)
105
- current=Dir.pwd
106
-
103
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
107
104
  result = false
108
105
  if File.exist?(local_repo_dir)
109
106
  current_branch = git!(%W(-C #{local_repo_dir} rev-parse --abbrev-ref HEAD)).strip
@@ -120,51 +117,40 @@ module Pindo
120
117
  else
121
118
  result = false
122
119
  end
123
- Dir.chdir(current)
124
120
  return result
125
121
  end
126
122
 
127
123
 
128
124
  def local_branch_exists?(local_repo_dir: nil, branch: nil)
129
- current=Dir.pwd
125
+ # 命令改带 -C,去掉 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
130
126
  result = false
131
127
 
132
128
  if File.exist?(local_repo_dir)
133
- Dir.chdir(local_repo_dir)
134
-
135
- res_data = Executable.capture_command('git', %W(rev-parse --verify #{branch}), :capture => :out)
136
- # puts "=====1"
137
- # puts res_data
138
- # res_data = git!(%W(-C #{local_repo_dir} --no-pager branch --list origin/#{branch} --no-color -r))
129
+ res_data = Executable.capture_command('git', %W(-C #{local_repo_dir} rev-parse --verify #{branch}), :capture => :out)
139
130
  result = !res_data.nil? && !res_data.empty?
140
131
  else
141
132
  result = false
142
133
  end
143
- Dir.chdir(current)
144
134
  return result
145
135
  end
146
136
 
147
137
  def remote_branch_exists?(local_repo_dir: nil, branch: nil)
148
- current=Dir.pwd
138
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
149
139
  result = false
150
140
  if File.exist?(local_repo_dir)
151
- Dir.chdir(local_repo_dir)
152
141
  res_data = git_remote!(local_repo_dir, %W(-C #{local_repo_dir} ls-remote --heads origin refs/heads/#{branch}))
153
142
  result = !res_data.nil? && !res_data.empty?
154
143
  else
155
144
  result = false
156
145
  end
157
- Dir.chdir(current)
158
146
  return result
159
147
  end
160
148
 
161
149
 
162
150
  def add_tag(local_repo_dir: nil, tag_name: nil)
163
-
164
- current=Dir.pwd
151
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
165
152
  result = false
166
153
  if File.exist?(local_repo_dir)
167
- Dir.chdir(local_repo_dir)
168
154
  if !local_tag_exists?(local_repo_dir: local_repo_dir, tag_name: tag_name)
169
155
  git!(%W(-C #{local_repo_dir} tag #{tag_name}))
170
156
  result = true
@@ -173,7 +159,6 @@ module Pindo
173
159
  else
174
160
  result = false
175
161
  end
176
- Dir.chdir(current)
177
162
  return result
178
163
  end
179
164
 
@@ -207,11 +192,9 @@ module Pindo
207
192
  end
208
193
 
209
194
  def remove_tag(local_repo_dir: nil, tag_name: nil)
210
-
195
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
211
196
  result = false
212
- current=Dir.pwd
213
197
  if File.exist?(local_repo_dir)
214
- Dir.chdir(local_repo_dir)
215
198
  if local_tag_exists?(local_repo_dir: local_repo_dir, tag_name: tag_name)
216
199
  git!(%W(-C #{local_repo_dir} tag -d #{tag_name}))
217
200
  result = true
@@ -223,7 +206,6 @@ module Pindo
223
206
  else
224
207
  result = false
225
208
  end
226
- Dir.chdir(current)
227
209
  return result
228
210
 
229
211
  end
@@ -253,9 +235,7 @@ module Pindo
253
235
  end
254
236
 
255
237
  def git_latest_commit_id(local_repo_dir:nil)
256
-
257
- current=Dir.pwd
258
-
238
+ # 命令已带 -C,无需 Dir.chdir(并发模式下 chdir 会污染全局 cwd)
259
239
  unless File.exist?(File::join(local_repo_dir, ".git"))
260
240
  return nil
261
241
  end
@@ -263,16 +243,9 @@ module Pindo
263
243
  commit_id = nil
264
244
 
265
245
  if File.exist?(local_repo_dir)
266
- Dir.chdir(local_repo_dir)
267
246
  current_branch = git!(%W(-C #{local_repo_dir} rev-parse --abbrev-ref HEAD)).strip
268
- # puts "current_branch: #{current_branch}"
269
-
270
- # git log -n 1 --pretty=format:"commit %H"
271
- # format_str = "commit %H"
272
- # latest_log_info = git!(%W(-C #{current} log -n 1 --pretty=#{format_str} #{current_branch})).strip
273
247
  commit_id = git!(%W(-C #{local_repo_dir} rev-parse #{current_branch})).strip
274
248
  end
275
- Dir.chdir(current)
276
249
 
277
250
  return commit_id
278
251
  end
@@ -362,9 +335,11 @@ module Pindo
362
335
  rescue StandardError => e
363
336
  Funlog.instance.fancyinfo_error("仓库#{local_repo_dir}更新失败!")
364
337
  raise Informative, e.to_s
338
+ ensure
339
+ # 无论成功失败都还原 cwd,避免异常路径下污染全局工作目录
340
+ Dir.chdir(current)
365
341
  end
366
342
 
367
- Dir.chdir(current)
368
343
  return local_repo_dir
369
344
  end
370
345
 
@@ -464,12 +439,8 @@ module Pindo
464
439
  end
465
440
 
466
441
  def git_addpush_repo(path:nil, message:"res", commit_file_params:nil)
467
- current=Dir.pwd
468
- Dir.chdir(path)
469
-
470
- # gitee.com 仓库自动去除代理
442
+ # gitee.com 仓库自动去除代理;is_gitee_repo? 命令已带 -C,无需 chdir
471
443
  if is_gitee_repo?(local_repo_dir: path)
472
- Dir.chdir(current)
473
444
  return without_proxy { git_addpush_repo_impl(path: path, message: message, commit_file_params: commit_file_params) }
474
445
  end
475
446
 
@@ -512,10 +483,10 @@ module Pindo
512
483
  rescue => error
513
484
  # puts(error.to_s)
514
485
  raise Informative, "\n#{path}\n 仓库失败 !!!"
486
+ ensure
487
+ # 无论成功失败都还原 cwd,避免异常路径下污染全局工作目录
488
+ Dir.chdir(current)
515
489
  end
516
-
517
-
518
- Dir.chdir(current)
519
490
  end
520
491
 
521
492
 
@@ -548,8 +519,11 @@ module Pindo
548
519
  return nil
549
520
  end
550
521
 
551
- # 将字符串转换为数组
552
- files_list.is_a?(String) ? files_list.split("\n").reject(&:empty?) : files_list
522
+ # git!/IO#readpartial 返回的是 ASCII-8BIT 字符串,含非 ASCII 字节(如中文路径、
523
+ # core.quotePath=false 时的原始多字节)会与下游 UTF-8 字面量拼接失败,这里统一
524
+ # 标成 UTF-8 并 scrub 无效字节,避免上层 `puts "...#{file}"` 抛 Encoding::CompatibilityError
525
+ raw_list = files_list.is_a?(String) ? files_list.split("\n") : Array(files_list)
526
+ raw_list.map { |f| f.dup.force_encoding('UTF-8').scrub }.reject(&:empty?)
553
527
  rescue => e
554
528
  Funlog.instance.fancyinfo_error("获取未提交文件列表失败: #{e.message}")
555
529
  nil
@@ -624,14 +598,13 @@ module Pindo
624
598
  puts
625
599
 
626
600
  # 将文件列表分行并用红色显示
627
- if files.is_a?(String)
628
- files.split("\n").each do |file|
629
- puts " • #{file}".red unless file.empty?
630
- end
631
- else
632
- files.each do |file|
633
- puts " • #{file}".red unless file.empty?
634
- end
601
+ # 防御性编码归一化:git! 返回的是 ASCII-8BIT,与 UTF-8 字面量 " • " 拼接会抛
602
+ # Encoding::CompatibilityError;此处再兜一层,避免其它调用方传入 BINARY 字符串时崩溃
603
+ file_lines = files.is_a?(String) ? files.split("\n") : Array(files)
604
+ file_lines.each do |file|
605
+ next if file.nil? || file.empty?
606
+ safe_file = file.dup.force_encoding('UTF-8').scrub
607
+ puts " • #{safe_file}".red
635
608
  end
636
609
 
637
610
  puts
@@ -43,7 +43,7 @@ module Pindo
43
43
  def initialize(argv)
44
44
 
45
45
  @args_repo_name = argv.shift_argument
46
- @args_login_flag = argv.flag?('force', false)
46
+ @args_login_flag = argv.flag?('login', false)
47
47
  @repo_name = argv.option('repo')
48
48
  @org_name = argv.option('org')
49
49
 
@@ -48,7 +48,7 @@ module Pindo
48
48
  def initialize(argv)
49
49
 
50
50
  @args_repo_name = argv.shift_argument
51
- @args_login_flag = argv.flag?('force', false)
51
+ @args_login_flag = argv.flag?('login', false)
52
52
  @repo_name = argv.option('repo')
53
53
  @org_name = argv.option('org')
54
54
 
@@ -159,8 +159,8 @@ module Pindo
159
159
  Funlog.fancyinfo_start("克隆仓库: #{url}...")
160
160
  Funlog.info("仓库 URL: #{url}") if ENV['PINDO_DEBUG']
161
161
 
162
- # 克隆仓库
163
- success = system("git clone #{url} #{temp_dir} --quiet 2>/dev/null")
162
+ # 克隆仓库(数组参数形式,避免 URL 中的 shell 元字符注入)
163
+ success = system("git", "clone", url, temp_dir, "--quiet", err: File::NULL)
164
164
 
165
165
  unless success && File.exist?(File.join(temp_dir, '.git'))
166
166
  Funlog.fancyinfo_error("仓库克隆失败: #{url}")
@@ -142,7 +142,6 @@ module Pindo
142
142
  new_project_dir = File.join(File::expand_path(current_dir + "/../") , temp_dir_name)
143
143
 
144
144
  if File.exist?(new_project_dir)
145
- system 'sh rm -rf ' + new_project_dir
146
145
  FileUtils.rm_rf(new_project_dir)
147
146
  end
148
147
  FileUtils.mkdir(new_project_dir)
@@ -6,6 +6,7 @@ require_relative 'java_env_helper'
6
6
  require_relative 'keystore_helper'
7
7
  require_relative 'workflow_gradle_injector'
8
8
  require 'fileutils'
9
+ require 'find'
9
10
 
10
11
  module Pindo
11
12
  class AndroidBuildHelper
@@ -24,6 +25,10 @@ module Pindo
24
25
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
25
26
  raise ArgumentError, "项目目录不存在" unless File.directory?(project_dir)
26
27
 
28
+ # 构建前先清理 macOS 自动生成的 .DS_Store
29
+ # 避免其被带入 base / asset pack 模块的 assets/ 导致冲突,或引起 Gradle clean 在遍历删除时出现竞态
30
+ prune_ds_store_files!(project_dir)
31
+
27
32
  # 设置 Java Home 环境变量
28
33
  Pindo::JavaEnvHelper.setup_java_home_from_project(project_dir)
29
34
 
@@ -396,20 +401,19 @@ module Pindo
396
401
  puts ' AAB 构建前先执行 Gradle clean'
397
402
  gradle_clean_resilient!(project_path)
398
403
 
399
- cmd = "cd \"#{project_path}\" && ./gradlew --no-daemon #{bundle_task}"
400
- system(cmd)
404
+ # 数组参数 + chdir,避免 project_path 拼接进 shell
405
+ system("./gradlew", "--no-daemon", bundle_task, chdir: project_path)
401
406
  end
402
407
 
403
408
  # clean 失败(占用、.DS_Store 竞态等)时:停 Daemon → 再删 .DS_Store → 物理删除各模块 build/,与 Gradle clean 产物一致,不中断后续 bundle
404
409
  private def gradle_clean_resilient!(project_path)
405
- prune_ds_store_before_gradle_clean!(project_path)
406
- cmd = %(cd "#{project_path}" && ./gradlew --no-daemon clean)
407
- return if system(cmd)
410
+ prune_ds_store_files!(project_path)
411
+ return if system("./gradlew", "--no-daemon", "clean", chdir: project_path)
408
412
 
409
413
  puts ' ⚠ Gradle clean 失败,正在自动恢复:--stop、清理 .DS_Store、删除各模块 build/ ...'
410
- system(%(cd "#{project_path}" && ./gradlew --stop), out: File::NULL, err: File::NULL)
414
+ system("./gradlew", "--stop", chdir: project_path, out: File::NULL, err: File::NULL)
411
415
  sleep 0.3
412
- prune_ds_store_before_gradle_clean!(project_path)
416
+ prune_ds_store_files!(project_path)
413
417
  remove_android_module_build_dirs!(project_path)
414
418
  end
415
419
 
@@ -445,18 +449,37 @@ module Pindo
445
449
  puts " ⚠ 物理删除 build 目录时出现异常(将继续尝试 bundle): #{e.message}"
446
450
  end
447
451
 
448
- # macOS clean 遍历删除 build 目录时 Finder 可能写入 .DS_Store,Gradle 会报 Unable to delete / New files were found
449
- private def prune_ds_store_before_gradle_clean!(project_path)
450
- return unless project_path && File.directory?(project_path)
451
-
452
- system('find', project_path, '-name', '.DS_Store', '-type', 'f', '-delete',
453
- out: File::NULL, err: File::NULL)
452
+ # 清理 macOS 自动生成的 .DS_Store 文件
453
+ # 作用:
454
+ # 1) 避免被带入 base 与 asset pack 模块的 assets/ 造成打包冲突
455
+ # 2) 避免 Gradle clean 遍历删除 build 目录时 Finder 回写 .DS_Store 触发
456
+ # Unable to delete / New files were found 的竞态错误
457
+ private def prune_ds_store_files!(project_path)
458
+ return 0 unless project_path && File.directory?(project_path)
459
+
460
+ count = 0
461
+ Find.find(project_path) do |path|
462
+ next unless File.basename(path) == '.DS_Store'
463
+ next unless File.file?(path)
464
+
465
+ begin
466
+ File.delete(path)
467
+ count += 1
468
+ rescue StandardError
469
+ # 个别文件删除失败不影响整体流程
470
+ next
471
+ end
472
+ end
473
+ puts " 已清理 #{count} 个 .DS_Store 文件" if count.positive?
474
+ count
475
+ rescue StandardError => e
476
+ puts " ⚠ 清理 .DS_Store 异常:#{e.message}"
477
+ 0
454
478
  end
455
479
 
456
480
  def build_so_library(project_path)
457
- # 编译so库 (Thread Safe)
458
- cmd = "cd \"#{project_path}\" && ./gradlew unityLibrary:BuildIl2CppTask"
459
- system(cmd)
481
+ # 编译so库 (Thread Safe);数组参数 + chdir,避免 project_path 拼接进 shell
482
+ system("./gradlew", "unityLibrary:BuildIl2CppTask", chdir: project_path)
460
483
  end
461
484
 
462
485
  def copy_so_files(source_path, target_path)
@@ -518,18 +518,16 @@ module Pindo
518
518
  return false
519
519
  end
520
520
 
521
- # 3. 使用unzip -t测试ZIP文件完整性
522
- test_cmd = "unzip -t '#{zip_file}' > /dev/null 2>&1"
523
- system(test_cmd)
521
+ # 3. 使用unzip -t测试ZIP文件完整性(数组参数,避免路径注入)
522
+ system("unzip", "-t", zip_file, out: File::NULL, err: File::NULL)
524
523
  rescue => e
525
524
  false
526
525
  end
527
526
 
528
527
  # 使用curl下载文件
529
528
  def download_with_curl(url, output_path)
530
- # 简化的curl命令,避免特殊字符问题
531
- cmd = "curl -L -o '#{output_path}' '#{url}'"
532
- system(cmd)
529
+ # 数组参数形式,避免 URL/路径中的 shell 元字符注入
530
+ system("curl", "-L", "-o", output_path, url)
533
531
  rescue => e
534
532
  false
535
533
  end
@@ -537,9 +535,8 @@ module Pindo
537
535
  # 解压Gradle分发包
538
536
  def extract_gradle_distribution(zip_file, extract_dir, gradle_version)
539
537
  begin
540
- # 使用unzip命令解压
541
- extract_cmd = "cd '#{File.dirname(extract_dir)}' && unzip -q '#{zip_file}'"
542
- if system(extract_cmd)
538
+ # 使用unzip命令解压(数组参数 + chdir,避免路径注入)
539
+ if system("unzip", "-q", zip_file, chdir: File.dirname(extract_dir))
543
540
  # 设置执行权限
544
541
  gradle_script = File.join(extract_dir, "bin/gradle")
545
542
  if File.exist?(gradle_script)
@@ -577,11 +574,8 @@ module Pindo
577
574
 
578
575
  # 使用官方Gradle命令生成wrapper
579
576
  def generate_gradle_wrapper_official(project_path, gradle_version)
580
- Dir.chdir(project_path) do
581
- # 使用gradle wrapper命令
582
- cmd = "gradle wrapper"
583
- system(cmd)
584
- end
577
+ # 数组参数 + chdir,避免 shell 解析
578
+ system("gradle", "wrapper", chdir: project_path)
585
579
  rescue => e
586
580
  false
587
581
  end
@@ -165,9 +165,9 @@ module Pindo
165
165
  puts "找到 #{identity_ids.size} 个证书:"
166
166
  puts identity_ids
167
167
 
168
- # 删除证书
168
+ # 删除证书(数组参数形式,避免 identity_id 拼接进 shell)
169
169
  identity_ids.each do |identity_id|
170
- system "security delete-certificate -Z #{identity_id}"
170
+ system("security", "delete-certificate", "-Z", identity_id)
171
171
  end
172
172
 
173
173
  # 清理 Provisioning Profiles
@@ -710,14 +710,10 @@ module Pindo
710
710
  FileUtils.rm_rf(ipa_file_upload)
711
711
  end
712
712
 
713
- current_dir = Dir.pwd
714
713
  if File.exist?(ipa_base_dir)
715
- Dir.chdir(ipa_base_dir)
716
714
  base_name = File.basename(mac_app_path)
717
- command = "zip -qry \"#{ipa_file_upload}\" \"#{base_name}\""
718
- puts command
719
- system command
720
- Dir.chdir(current_dir)
715
+ # 数组参数 + chdir,避免 shell 注入与 cwd 污染
716
+ system("zip", "-qry", ipa_file_upload, base_name, chdir: ipa_base_dir)
721
717
  end
722
718
  end
723
719
  if !ipa_file_upload.nil? &&
@@ -738,19 +734,18 @@ module Pindo
738
734
  rescue StandardError => e
739
735
 
740
736
  end
741
- current_project_dir = Dir.pwd
742
737
  web_build_name = web_build_name.downcase.strip.gsub(/[\s\-_]/, '')
743
738
  web_res_zip_fullname = File.join(web_zip_dir, web_build_name + "_" + web_build_version + ".zip")
744
739
  if File.exist?(web_res_zip_fullname)
745
740
  FileUtils.rm_rf(web_res_zip_fullname)
746
741
  end
747
742
  Zip::File.open(web_res_zip_fullname, Zip::File::CREATE) do |zipfile|
748
- Dir.chdir web_res_path
749
- Dir.glob("**/*").reject {|fn| File.directory?(fn) }.each do |file|
750
- zipfile.add(file.sub(web_res_path + '/', ''), file)
743
+ Dir.chdir(web_res_path) do
744
+ Dir.glob("**/*").reject {|fn| File.directory?(fn) }.each do |file|
745
+ zipfile.add(file.sub(web_res_path + '/', ''), file)
746
+ end
751
747
  end
752
748
  end
753
- Dir.chdir(current_project_dir)
754
749
  ipa_file_upload = web_res_zip_fullname
755
750
  end
756
751
  if !ipa_file_upload.nil? &&
@@ -767,12 +762,11 @@ module Pindo
767
762
  FileUtils.rm_rf(windows_zip_fullname)
768
763
  end
769
764
 
770
- current_project_dir = Dir.pwd
771
765
  Zip::File.open(windows_zip_fullname, Zip::File::CREATE) do |zipfile|
772
- Dir.chdir windows_build_dir
773
- zipfile.add(windows_exe_name, windows_exe_name)
766
+ Dir.chdir(windows_build_dir) do
767
+ zipfile.add(windows_exe_name, windows_exe_name)
768
+ end
774
769
  end
775
- Dir.chdir(current_project_dir)
776
770
  ipa_file_upload = windows_zip_fullname
777
771
  end
778
772
  unless !ipa_file_upload.nil? && File.exist?(ipa_file_upload)
@@ -800,14 +794,14 @@ module Pindo
800
794
  end
801
795
 
802
796
  Zip::File.open(server_zipfile_name, Zip::File::CREATE) do |zipfile|
803
- Dir.chdir server_file_directory
804
- Dir.glob("**/*").reject {|fn| File.directory?(fn) }.each do |file|
805
- zipfile.add(file.sub(server_file_directory + '/', ''), file)
797
+ Dir.chdir(server_file_directory) do
798
+ Dir.glob("**/*").reject {|fn| File.directory?(fn) }.each do |file|
799
+ zipfile.add(file.sub(server_file_directory + '/', ''), file)
800
+ end
806
801
  end
807
802
  end
808
803
 
809
804
  addtach_file = server_zipfile_name
810
- Dir.chdir current_project_dir
811
805
  end
812
806
 
813
807
  puts
@@ -1539,6 +1533,7 @@ module Pindo
1539
1533
  if git_cliff_installed
1540
1534
  temp_dir = Dir.pwd
1541
1535
  Dir.chdir(current_git_root_path)
1536
+ begin
1542
1537
 
1543
1538
  # 检查 HEAD 是否有 tag
1544
1539
  head_tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
@@ -1605,7 +1600,10 @@ module Pindo
1605
1600
  description = "版本更新"
1606
1601
  end
1607
1602
 
1608
- Dir.chdir(temp_dir)
1603
+ ensure
1604
+ # ensure 保证异常路径也还原 cwd,避免污染全局工作目录
1605
+ Dir.chdir(temp_dir)
1606
+ end
1609
1607
  else
1610
1608
  # git-cliff 未安装,提示用户并退出
1611
1609
  puts "\n\e[31m错误: git-cliff 未安装\e[0m"
@@ -58,43 +58,7 @@ module Pindo
58
58
  build_android_project
59
59
  end
60
60
 
61
- def find_output
62
- # 搜索 APK 和 AAB 文件
63
- search_paths = []
64
- search_paths.concat(TaskConfig::BUILD_OUTPUT_PATTERNS[:apk].map { |p| File.join(@project_path, p) })
65
- search_paths.concat(TaskConfig::BUILD_OUTPUT_PATTERNS[:aab].map { |p| File.join(@project_path, p) })
66
-
67
- package_files = []
68
- search_paths.each do |pattern|
69
- package_files.concat(Dir.glob(pattern))
70
- end
71
-
72
- # 过滤测试包和未签名包
73
- package_files.reject! do |f|
74
- basename = File.basename(f).downcase
75
- TaskConfig::EXCLUDED_PATTERNS.any? { |pattern| basename.include?(pattern) }
76
- end
77
-
78
- if package_files.any?
79
- # 优先返回 AAB,其次 APK
80
- aab_files = package_files.select { |f| f.end_with?(".aab") }
81
- apk_files = package_files.select { |f| f.end_with?(".apk") }
82
-
83
- latest_package = if aab_files.any?
84
- aab_files.max_by { |f| File.mtime(f) }
85
- elsif apk_files.any?
86
- apk_files.max_by { |f| File.mtime(f) }
87
- else
88
- package_files.max_by { |f| File.mtime(f) }
89
- end
90
-
91
- puts " 找到 Android 包文件: #{latest_package}"
92
- latest_package
93
- else
94
- puts " 警告: 未找到 APK/AAB 文件"
95
- nil
96
- end
97
- end
61
+ # find_output 已上移到 AndroidBuildTask 基类(dev/adhoc/gplay 共用)
98
62
 
99
63
  private
100
64
 
@@ -1,4 +1,5 @@
1
1
  require_relative '../build_task'
2
+ require_relative '../../task_config'
2
3
 
3
4
  module Pindo
4
5
  module TaskSystem
@@ -12,6 +13,45 @@ module Pindo
12
13
  def required_resources
13
14
  (super || []) + [{ type: :gradle, directory: @project_path }]
14
15
  end
16
+
17
+ # 查找构建产物(APK/AAB),三个子类(dev/adhoc/gplay)共用同一套 glob 逻辑
18
+ # 搜索路径与过滤规则与 pindo jps upload 查找 Android 包保持一致;优先 AAB,其次 APK
19
+ def find_output
20
+ search_paths = []
21
+ search_paths.concat(TaskConfig::BUILD_OUTPUT_PATTERNS[:apk].map { |p| File.join(@project_path, p) })
22
+ search_paths.concat(TaskConfig::BUILD_OUTPUT_PATTERNS[:aab].map { |p| File.join(@project_path, p) })
23
+
24
+ package_files = []
25
+ search_paths.each do |pattern|
26
+ package_files.concat(Dir.glob(pattern))
27
+ end
28
+
29
+ # 过滤测试包和未签名包
30
+ package_files.reject! do |f|
31
+ basename = File.basename(f).downcase
32
+ TaskConfig::EXCLUDED_PATTERNS.any? { |pattern| basename.include?(pattern) }
33
+ end
34
+
35
+ if package_files.any?
36
+ # 优先返回 AAB,其次 APK
37
+ aab_files = package_files.select { |f| f.end_with?(".aab") }
38
+ apk_files = package_files.select { |f| f.end_with?(".apk") }
39
+
40
+ latest_package = if aab_files.any?
41
+ aab_files.max_by { |f| File.mtime(f) }
42
+ elsif apk_files.any?
43
+ apk_files.max_by { |f| File.mtime(f) }
44
+ else
45
+ package_files.max_by { |f| File.mtime(f) }
46
+ end
47
+
48
+ puts " 找到 Android 包文件: #{latest_package}"
49
+ latest_package
50
+ else
51
+ # 构建已执行但产物缺失,视为失败,不静默返回 nil
52
+ raise Informative, "构建完成但未找到 APK/AAB 文件"
53
+ end
54
+ end
15
55
  end
16
56
  end
17
57
  end
@@ -41,7 +41,7 @@ module Pindo
41
41
 
42
42
  #准备构建
43
43
  def prepare_build
44
- Dir.chdir(@project_path)
44
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
45
45
 
46
46
  # 0. 清理 Firebase Shell Script
47
47
  cleanup_firebase_shell
@@ -98,8 +98,8 @@ module Pindo
98
98
  puts " 找到 IPA 文件: #{latest_ipa}"
99
99
  latest_ipa
100
100
  else
101
- puts " 警告: 未找到 IPA 文件"
102
- nil
101
+ # 构建已执行但产物缺失,视为失败,不静默返回 nil
102
+ raise Informative, "构建完成但未找到 IPA 文件 (#{build_path})"
103
103
  end
104
104
  end
105
105
 
@@ -289,7 +289,7 @@ module Pindo
289
289
  puts " 处理 Quark/Swark..."
290
290
 
291
291
  require 'pindo/module/xcode/xcode_swark_helper'
292
- Dir.chdir(@project_path)
292
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
293
293
 
294
294
  if xcode_build_type.include?("quark")
295
295
  Pindo::XcodeSwarkHelper.quark_run(
@@ -377,7 +377,7 @@ module Pindo
377
377
 
378
378
  # 编译 iOS 工程
379
379
  def build_ios_project
380
- Dir.chdir(@project_path)
380
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
381
381
 
382
382
  # 使用 XcodeBuildHelper 进行构建
383
383
  ipa_file = Pindo::XcodeBuildHelper.build_project(
@@ -40,7 +40,7 @@ module Pindo
40
40
 
41
41
  #准备构建
42
42
  def prepare_build
43
- Dir.chdir(@project_path)
43
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
44
44
 
45
45
  # 0. 清理 Firebase Shell Script
46
46
  cleanup_firebase_shell
@@ -81,8 +81,8 @@ module Pindo
81
81
  puts " 找到 IPA 文件: #{latest_ipa}"
82
82
  latest_ipa
83
83
  else
84
- puts " 警告: 未找到 IPA 文件"
85
- nil
84
+ # 构建已执行但产物缺失,视为失败,不静默返回 nil
85
+ raise Informative, "构建完成但未找到 IPA 文件 (#{build_path})"
86
86
  end
87
87
  end
88
88
 
@@ -262,7 +262,7 @@ module Pindo
262
262
  puts " 处理 Quark/Swark..."
263
263
 
264
264
  require 'pindo/module/xcode/xcode_swark_helper'
265
- Dir.chdir(@project_path)
265
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
266
266
 
267
267
  if xcode_build_type.include?("quark")
268
268
  Pindo::XcodeSwarkHelper.quark_run(
@@ -350,7 +350,7 @@ module Pindo
350
350
 
351
351
  # 编译 iOS 工程
352
352
  def build_ios_project
353
- Dir.chdir(@project_path)
353
+ # Dir.chdir(@project_path) # Removed for thread safety(下游均传入绝对路径)
354
354
 
355
355
  # 使用 XcodeBuildHelper 进行构建
356
356
  ipa_file = Pindo::XcodeBuildHelper.build_project(
@@ -91,8 +91,8 @@ module Pindo
91
91
  return latest_output
92
92
  end
93
93
 
94
- puts " 警告: 未找到 IPA / APP / 可执行文件"
95
- nil
94
+ # 构建已执行但产物缺失,视为失败,不静默返回 nil
95
+ raise Informative, "构建完成但未找到 IPA / APP / 可执行文件 (#{build_dir})"
96
96
  end
97
97
 
98
98
  # 处理 CocoaPods 依赖(Dev 构建)
@@ -180,6 +180,8 @@ module Pindo
180
180
  execute_build
181
181
 
182
182
  # 查找输出文件
183
+ # 注意:是否「找不到产物即失败」由各子类 find_output 自行决定(已实现的子类会 raise;
184
+ # 未实现的 TODO 桩 AndroidBuildAdhocTask/AndroidBuildGPlayTask 仍返回 nil,保持原行为)
183
185
  @output_path = find_output
184
186
 
185
187
  {
@@ -13,7 +13,7 @@ module Pindo
13
13
  :git_commit
14
14
  end
15
15
 
16
- # 重试配置:失败后重试 1
16
+ # 重试配置:失败后重试 2 次(共 3 次尝试)
17
17
  def self.default_retry_count
18
18
  2
19
19
  end
@@ -69,6 +69,16 @@ module Pindo
69
69
  root_dir = @git_root_path
70
70
  git_repo_helper = Pindo::GitRepoHelper.share_instance
71
71
 
72
+ # 0. 先拉取远程 tag,确保本地 tag 列表是最新的
73
+ # 否则本地 tag 落后于远程时(如远程被 CI/其他机器打了更高 tag),
74
+ # 后续基于本地 tag 计算的版本会偏低,导致本该新建的版本 tag 与远程已有 tag 冲突而被漏打。
75
+ # 尽力而为:网络不可达时仅告警,用本地 tag 降级计算,不阻断构建。
76
+ begin
77
+ Pindo::GitHandler.git_remote!(root_dir, %W(-C #{root_dir} fetch --tags --force origin))
78
+ rescue => e
79
+ Funlog.instance.fancyinfo_warning("拉取远程 tag 失败,将使用本地 tag 计算版本: #{e.message.lines.first&.strip}")
80
+ end
81
+
72
82
  # 1. 如果 fixed_version 为 nil,尝试从 HEAD 的 tag 获取版本
73
83
  # 必须在 check_gitignore 之前检查,因为 check_gitignore 可能创建提交导致 HEAD 移动
74
84
  if (@fixed_version.nil? || @fixed_version.empty?) &&
@@ -12,7 +12,7 @@ module Pindo
12
12
  :git_tag
13
13
  end
14
14
 
15
- # 重试配置:失败后重试 1
15
+ # 重试配置:失败后重试 2 次(共 3 次尝试)
16
16
  def self.default_retry_count
17
17
  2
18
18
  end
@@ -39,7 +39,7 @@ module Pindo
39
39
  protected
40
40
 
41
41
  def do_work
42
- # 检查是否为 Git 仓库,如果不是则直接成功返回
42
+ # 检查是否为 Git 仓库,如果不是则直接成功返回(非 Git 仓库目录下跳过打 tag)
43
43
  unless valid_git_repository?
44
44
  Funlog.instance.fancyinfo_warning("当前目录不是Git仓库,跳过Git打Tag")
45
45
  return {
@@ -15,6 +15,11 @@ module Pindo
15
15
  :jps_workflow_message
16
16
  end
17
17
 
18
+ # 工作流消息失败重试 1 次(覆盖 JPSTask 默认的重试 3 次)
19
+ def self.default_retry_count
20
+ 1
21
+ end
22
+
18
23
  # 初始化工作流消息发送任务
19
24
  # @param options [Hash] 选项
20
25
  # @option options [String] :git_commit_id Git commit SHA(可选,从依赖任务获取)
@@ -70,17 +75,10 @@ module Pindo
70
75
  workflow_id: @workflow_id
71
76
  }
72
77
  rescue => e
73
- # 外层错误保护:消息发送失败不应影响整体流程
74
- puts " ⚠️ JPS 工作流消息发送发生错误: #{e.message}"
78
+ # 工作流消息是终端任务,失败需明确报错中断(不再伪造 success)
79
+ puts " JPS 工作流消息发送失败: #{e.message}"
75
80
  puts " ⚠️ 错误堆栈: #{e.backtrace.first(3).join("\n ")}" if ENV['PINDO_DEBUG'] && e.backtrace
76
- puts " ℹ️ 消息发送失败不影响主流程\n"
77
-
78
- # 消息发送失败不抛出异常,返回成功但带警告
79
- {
80
- success: true,
81
- warning: e.message,
82
- git_commit_id: @git_commit_id
83
- }
81
+ raise Informative, "JPS 工作流消息发送失败: #{e.message}"
84
82
  end
85
83
  end
86
84
 
@@ -446,9 +446,9 @@ module Pindo
446
446
  end
447
447
  end
448
448
 
449
- # 计算当前重试次数
449
+ # 计算当前重试次数(调用时 @retry_count 已递减,无需再 +1)
450
450
  def current_retry_attempt
451
- @max_retry_count - @retry_count + 1
451
+ @max_retry_count - @retry_count
452
452
  end
453
453
 
454
454
  # 立即重试
@@ -118,12 +118,19 @@ module Pindo
118
118
  # 输出执行摘要
119
119
  @reporter.print_execution_summary
120
120
 
121
- # 检查失败任务,有失败则抛出异常(确保进程返回非零退出码)
122
- report = execution_report
123
- if report[:failed] > 0
124
- failed_tasks = @queue.completed_snapshot.select { |t| t.status == TaskStatus::FAILED }
125
- failed_names = failed_tasks.map(&:name).join(', ')
126
- raise Informative, "#{report[:failed]} 个任务执行失败: #{failed_names}"
121
+ # 失败闸门:FAILED / CANCELLED / 残留 PENDING(依赖阻塞等导致从未执行)都视为
122
+ # 「未成功完成」,统一抛异常确保进程非零退出码,避免任务没跑成却静默成功
123
+ completed = @queue.completed_snapshot
124
+ failed_tasks = completed.select { |t| t.status == TaskStatus::FAILED }
125
+ cancelled_tasks = completed.select { |t| t.status == TaskStatus::CANCELLED }
126
+ unfinished_tasks = @queue.pending_snapshot
127
+
128
+ if failed_tasks.any? || cancelled_tasks.any? || unfinished_tasks.any?
129
+ parts = []
130
+ parts << "失败: #{failed_tasks.map(&:name).join(', ')}" if failed_tasks.any?
131
+ parts << "取消: #{cancelled_tasks.map(&:name).join(', ')}" if cancelled_tasks.any?
132
+ parts << "未执行: #{unfinished_tasks.map(&:name).join(', ')}" if unfinished_tasks.any?
133
+ raise Informative, "存在未成功完成的任务 —— #{parts.join(';')}"
127
134
  end
128
135
  end
129
136
 
@@ -138,6 +145,7 @@ module Pindo
138
145
  completed: completed.count,
139
146
  success: completed.count { |t| t.status == TaskStatus::SUCCESS },
140
147
  failed: completed.count { |t| t.status == TaskStatus::FAILED },
148
+ cancelled: completed.count { |t| t.status == TaskStatus::CANCELLED },
141
149
  tasks: (pending + completed).map do |task|
142
150
  {
143
151
  id: task.id,
@@ -505,9 +505,8 @@ module Pindo
505
505
  puts " 使用工具: nuget pack"
506
506
  puts
507
507
 
508
- cmd = "cd \"#{package_dir}\" && nuget pack \"#{File.basename(nuspec_file)}\" -OutputDirectory \"#{output_dir}\""
509
-
510
- success = system(cmd)
508
+ # 数组参数 + chdir,避免路径中的 shell 元字符注入
509
+ success = system("nuget", "pack", File.basename(nuspec_file), "-OutputDirectory", output_dir, chdir: package_dir)
511
510
 
512
511
  unless success
513
512
  raise Informative, "NuGet 打包失败,请检查 .nuspec 文件格式"
@@ -573,10 +572,8 @@ module Pindo
573
572
  begin
574
573
  # 查找是否存在与 nuspec 版本匹配的 tag(支持 v1.1.3, V1.1.3, 1.1.3 等格式)
575
574
  matching_tag = nil
576
- temp_dir = Dir.pwd
577
- Dir.chdir(git_root)
578
575
 
579
- # 获取所有 tags
576
+ # 获取所有 tags(命令已带 -C,无需 chdir)
580
577
  all_tags = Pindo::GitHandler.git!(%W(-C #{git_root} tag -l)).split("\n")
581
578
 
582
579
  # 查找匹配当前版本的 tag(不区分大小写,支持 v 前缀)
@@ -585,17 +582,12 @@ module Pindo
585
582
  tag_version.downcase == nuspec_version.downcase
586
583
  end
587
584
 
588
- Dir.chdir(temp_dir)
589
-
590
585
  # 确定获取哪个范围的 commits
591
586
  git_range = if matching_tag
592
587
  # 找到匹配的 tag,获取前一个 tag 到这个 tag 的 commits
593
588
  puts "📝 找到版本 tag: #{matching_tag},生成该版本的 release notes..."
594
589
 
595
- temp_dir = Dir.pwd
596
- Dir.chdir(git_root)
597
-
598
- # 获取所有 tags 并按版本号排序
590
+ # 获取所有 tags 并按版本号排序(命令已带 -C,无需 chdir)
599
591
  all_tags = Pindo::GitHandler.git!(%W(-C #{git_root} tag -l)).split("\n")
600
592
  sorted_tags = all_tags.sort_by do |tag|
601
593
  version_str = tag.gsub(/^(v|V|release[\s_-]*)/i, '')
@@ -609,8 +601,6 @@ module Pindo
609
601
  prev_tag = sorted_tags[tag_index - 1]
610
602
  end
611
603
 
612
- Dir.chdir(temp_dir)
613
-
614
604
  if prev_tag
615
605
  "#{prev_tag}..#{matching_tag}"
616
606
  else
@@ -635,10 +625,7 @@ module Pindo
635
625
  end
636
626
  end
637
627
 
638
- # 获取 commit messages(包含完整的 body)
639
- temp_dir = Dir.pwd
640
- Dir.chdir(git_root)
641
-
628
+ # 获取 commit messages(包含完整的 body;命令已带 -C,无需 chdir
642
629
  # 使用特殊分隔符来区分不同的 commits
643
630
  separator = "---COMMIT-SEPARATOR---"
644
631
  commits_raw = Pindo::GitHandler.git!(%W(-C #{git_root} log #{git_range} --pretty=format:%B#{separator}))
@@ -646,8 +633,6 @@ module Pindo
646
633
  # 按分隔符拆分成单个 commits
647
634
  commits = commits_raw.split(separator).map(&:strip).reject(&:empty?)
648
635
 
649
- Dir.chdir(temp_dir)
650
-
651
636
  # 过滤只保留符合规范的 commits(支持带 scope 和不带 scope 格式)
652
637
  # 匹配格式: feat:, feat(scope):, fix:, fix(api): 等
653
638
  conventional_commit_regex = /^(feat|fix|docs|doc|perf|refactor|style|test|chore|revert)(\([^)]+\))?:/i
@@ -330,53 +330,34 @@ module Pindo
330
330
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
331
331
  raise ArgumentError, "tag名称不能为空" if tag_name.nil? || tag_name.empty?
332
332
 
333
+ local_exists = Pindo::GitHandler.local_tag_exists?(local_repo_dir: project_dir, tag_name: tag_name)
334
+
335
+ # 建本地 tag 是纯本地操作,不依赖远程可达,网络故障也能留下本地 tag;
336
+ # 删远程残留与推送一律交给 push_tag_with_verify,推送失败时本地 tag 仍保留,可事后手动补推。
333
337
  case tag_type
334
338
  when Pindo::CreateTagType::NONE
339
+ # 不创建 tag
335
340
  Funlog.instance.fancyinfo_success("跳过创建 Tag")
336
- return nil
337
- when Pindo::CreateTagType::RECREATE
338
- # 删除旧 tag(本地和远程)
339
- if Pindo::GitHandler.remote_tag_exists?(local_repo_dir: project_dir, tag_name: tag_name)
340
- Funlog.instance.fancyinfo_update("删除远程 Tag: #{tag_name}")
341
- Pindo::GitHandler.git_remote!(project_dir, %W(-C #{project_dir} push origin :refs/tags/#{tag_name}))
342
- end
343
- if Pindo::GitHandler.local_tag_exists?(local_repo_dir: project_dir, tag_name: tag_name)
344
- Funlog.instance.fancyinfo_update("删除本地 Tag: #{tag_name}")
345
- Pindo::GitHandler.git!(%W(-C #{project_dir} tag -d #{tag_name}))
346
- end
347
- when Pindo::CreateTagType::NEW
348
- local_exists = Pindo::GitHandler.local_tag_exists?(local_repo_dir: project_dir, tag_name: tag_name)
349
- remote_exists = Pindo::GitHandler.remote_tag_exists?(local_repo_dir: project_dir, tag_name: tag_name)
350
-
351
- # 如果 tag 在 HEAD 上,不需要重新创建
341
+ nil
342
+ when Pindo::CreateTagType::NEW, Pindo::CreateTagType::RECREATE
343
+ # 本地 tag 已在 HEAD:无需重建,仅确保推送到远程(RECREATE 在 HEAD 原地重建也是空转,同样适用)
352
344
  if local_exists && Pindo::GitHandler.is_tag_at_head?(git_root_dir: project_dir, tag_name: tag_name)
353
- if !remote_exists
354
- # 本地存在且在 HEAD,但远程不存在,推送到远程
355
- push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
356
- Funlog.instance.fancyinfo_success("推送 Tag 到远程: #{tag_name}")
357
- else
358
- Funlog.instance.fancyinfo_success("Tag 已存在且在 HEAD: #{tag_name}")
345
+ push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
346
+ tag_name
347
+ else
348
+ # 本地 tag 不存在或不在 HEAD:删旧重建后推送
349
+ if local_exists
350
+ Funlog.instance.fancyinfo_update("删除本地旧 Tag: #{tag_name}")
351
+ Pindo::GitHandler.git!(%W(-C #{project_dir} tag -d #{tag_name}))
359
352
  end
360
- return tag_name
361
- end
362
-
363
- # tag 不在 HEAD 上,需要删除后重新创建
364
- if remote_exists
365
- Funlog.instance.fancyinfo_update("删除远程 Tag: #{tag_name}")
366
- Pindo::GitHandler.git_remote!(project_dir, %W(-C #{project_dir} push origin :refs/tags/#{tag_name}))
367
- end
368
- if local_exists
369
- Funlog.instance.fancyinfo_update("删除本地 Tag: #{tag_name}")
370
- Pindo::GitHandler.git!(%W(-C #{project_dir} tag -d #{tag_name}))
353
+ Pindo::GitHandler.git!(%W(-C #{project_dir} tag #{tag_name}))
354
+ Funlog.instance.fancyinfo_success("已创建本地 Tag: #{tag_name}")
355
+ push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
356
+ tag_name
371
357
  end
372
- # 继续创建新 tag
358
+ else
359
+ raise ArgumentError, "未知的 tag_type: #{tag_type}"
373
360
  end
374
-
375
- # 创建并推送 tag
376
- Pindo::GitHandler.git!(%W(-C #{project_dir} tag #{tag_name}))
377
- push_tag_with_verify(project_dir: project_dir, tag_name: tag_name)
378
- Funlog.instance.fancyinfo_success("创建并推送 Tag: #{tag_name}")
379
- tag_name
380
361
  end
381
362
 
382
363
  # 推送 tag 到远程并校验是否真正推送成功,失败则重试
@@ -393,23 +374,37 @@ module Pindo
393
374
  loop do
394
375
  attempt += 1
395
376
  begin
377
+ # 远程若残留指向不同 commit 的同名 tag,先删除再推送,避免 non-fast-forward 被拒
378
+ remote_commit = Pindo::GitHandler.remote_tag_commit(local_repo_dir: project_dir, tag_name: tag_name)
379
+ if remote_commit == local_commit
380
+ Funlog.instance.fancyinfo_success("已确认 Tag 推送到远程: #{tag_name}")
381
+ return true
382
+ elsif remote_commit
383
+ Funlog.instance.fancyinfo_update("删除远程残留 Tag(远程 #{remote_commit[0..7]} ≠ 本地 #{local_commit[0..7]}): #{tag_name}")
384
+ Pindo::GitHandler.git_remote!(project_dir, %W(-C #{project_dir} push origin :refs/tags/#{tag_name}))
385
+ end
386
+
396
387
  Pindo::GitHandler.git_remote!(project_dir, %W(-C #{project_dir} push origin #{tag_name}))
397
388
  rescue => e
398
389
  first_line = e.message.lines.first&.strip
399
390
  Funlog.instance.fancyinfo_warning("推送 Tag 失败(第 #{attempt}/#{max_retries} 次): #{first_line}")
400
391
  end
401
392
 
402
- # 校验远程 tag 指向的 commit 是否与本地一致,确认推送成功
403
- remote_commit = Pindo::GitHandler.remote_tag_commit(local_repo_dir: project_dir, tag_name: tag_name)
393
+ # 校验远程 tag 指向的 commit 是否与本地一致,确认推送成功(探测失败按未确认处理,不中断重试)
394
+ remote_commit = begin
395
+ Pindo::GitHandler.remote_tag_commit(local_repo_dir: project_dir, tag_name: tag_name)
396
+ rescue
397
+ nil
398
+ end
404
399
  if remote_commit && remote_commit == local_commit
405
400
  Funlog.instance.fancyinfo_success("已确认 Tag 推送到远程: #{tag_name}")
406
401
  return true
407
- elsif remote_commit
408
- Funlog.instance.fancyinfo_warning("远程 Tag 指向不同 commit(远程 #{remote_commit[0..7]} ≠ 本地 #{local_commit[0..7]}),疑似残留旧 Tag: #{tag_name}")
409
402
  end
410
403
 
411
404
  if attempt >= max_retries
412
- raise Informative, "Tag 推送失败,已重试 #{max_retries} 次仍未在远程确认: #{tag_name}"
405
+ # 本地 tag 已创建,仅远程未确认 —— 保留本地 tag,提示可手动补推
406
+ Funlog.instance.fancyinfo_warning("本地 Tag 已创建并保留,可稍后手动执行: git push origin #{tag_name}")
407
+ raise Informative, "Tag 推送失败,已重试 #{max_retries} 次仍未在远程确认: #{tag_name}(本地 Tag 已保留,可手动补推)"
413
408
  end
414
409
 
415
410
  Funlog.instance.fancyinfo_update("远程未确认 Tag,#{attempt}s 后重试推送: #{tag_name}")
@@ -86,10 +86,10 @@ module Pindo
86
86
  tmp_dir = File.join(ipa_dir, tmp_time)
87
87
  FileUtils.mkdir(tmp_dir) unless File.exist?(tmp_dir)
88
88
 
89
- # 解压 IPA
90
- command = "unzip -q \"#{resign_ipa_full_name}\" -d #{tmp_dir}"
91
- puts command
92
- system command
89
+ # 解压 IPA(数组参数避免注入;失败立即报错,不静默继续)
90
+ unless system("unzip", "-q", resign_ipa_full_name, "-d", tmp_dir)
91
+ raise Informative, "IPA 解压失败: #{resign_ipa_full_name}"
92
+ end
93
93
 
94
94
  payload_path = File.join(tmp_dir, "Payload")
95
95
  if File.exist?(payload_path)
@@ -129,14 +129,13 @@ module Pindo
129
129
  # 修改 Info.plist 中的 Bundle ID
130
130
  modify_ipa_info_plist(modify_content_path: modify_content_path, bundle_id: new_bundle_id)
131
131
 
132
- # 重新打包 IPA
133
- current_dir = Dir.pwd
132
+ # 重新打包 IPA(数组参数 + chdir,避免 shell;* 由 Ruby 展开;失败立即报错)
134
133
  if File.exist?(payload_path)
135
- Dir.chdir(tmp_dir)
136
- command = "zip -qry \"#{resign_ipa_full_name}\" * "
137
- puts command
138
- system command
139
- Dir.chdir(current_dir)
134
+ # 排除点文件,对齐原 shell `*` 语义(避免把 .DS_Store 等打进 IPA 根目录)
135
+ entries = Dir.children(tmp_dir).reject { |e| e.start_with?('.') }
136
+ unless system("zip", "-qry", resign_ipa_full_name, *entries, chdir: tmp_dir)
137
+ raise Informative, "IPA 重新打包失败: #{resign_ipa_full_name}"
138
+ end
140
139
  end
141
140
 
142
141
  # 清理临时目录
@@ -160,24 +159,21 @@ module Pindo
160
159
 
161
160
  # 确保 PlistBuddy 可用
162
161
  unless File.exist?("/usr/local/bin/PlistBuddy")
163
- command = 'ln -s /usr/libexec/PlistBuddy /usr/local/bin/PlistBuddy'
164
- system command
162
+ system("ln", "-s", "/usr/libexec/PlistBuddy", "/usr/local/bin/PlistBuddy")
165
163
  end
166
164
 
167
- # 读取旧的 Bundle ID
168
- old_bundle_id_command = '/usr/local/bin/PlistBuddy -c "Print :CFBundleIdentifier" ' + main_info_plist
169
- puts old_bundle_id_command
165
+ # 读取旧的 Bundle ID(数组参数,避免路径拼接进 shell)
170
166
  old_bundle_id = ""
171
- IO.popen(old_bundle_id_command) { |f| old_bundle_id = f.gets }
172
- old_bundle_id = old_bundle_id.strip
167
+ IO.popen(["/usr/local/bin/PlistBuddy", "-c", "Print :CFBundleIdentifier", main_info_plist]) { |f| old_bundle_id = f.gets }
168
+ old_bundle_id = old_bundle_id.to_s.strip
173
169
  puts "旧 Bundle ID: #{old_bundle_id}"
174
170
 
175
- # 修改主应用的 Bundle ID
171
+ # 修改主应用的 Bundle ID(失败立即报错)
176
172
  new_main_bundle_id = bundle_id
177
173
 
178
- exchange_bundleid_command = '/usr/local/bin/PlistBuddy -c "Set :CFBundleIdentifier ' + new_main_bundle_id + '" ' + main_info_plist
179
- puts exchange_bundleid_command
180
- system exchange_bundleid_command
174
+ unless system("/usr/local/bin/PlistBuddy", "-c", "Set :CFBundleIdentifier #{new_main_bundle_id}", main_info_plist)
175
+ raise Informative, "修改主应用 Bundle ID 失败: #{main_info_plist}"
176
+ end
181
177
 
182
178
  # 如果 Bundle ID 发生变化,更新 URL Schemes
183
179
  if old_bundle_id != new_main_bundle_id
@@ -193,10 +189,8 @@ module Pindo
193
189
  appex_path = File.join(plugin_path, file)
194
190
  tmp_info_plist = File.join(appex_path, "Info.plist")
195
191
 
196
- old_bundle_id_command = '/usr/local/bin/PlistBuddy -c "Print :CFBundleIdentifier" ' + tmp_info_plist
197
- puts old_bundle_id_command
198
192
  old_plugin_bundle_id = ""
199
- IO.popen(old_bundle_id_command) { |f| old_plugin_bundle_id = f.gets }
193
+ IO.popen(["/usr/local/bin/PlistBuddy", "-c", "Print :CFBundleIdentifier", tmp_info_plist]) { |f| old_plugin_bundle_id = f.gets }
200
194
  puts "old_bundle_id = #{old_plugin_bundle_id}"
201
195
 
202
196
  # 根据插件类型修改 Bundle ID
@@ -219,9 +213,9 @@ module Pindo
219
213
  plugin_types.each do |suffix|
220
214
  if old_bundle_id.include?(suffix)
221
215
  new_plugin_bundle_id = new_base_bundle_id + suffix
222
- exchange_bundleid_command = '/usr/local/bin/PlistBuddy -c "Set :CFBundleIdentifier ' + new_plugin_bundle_id + '" ' + plist_path
223
- puts exchange_bundleid_command
224
- system exchange_bundleid_command
216
+ unless system("/usr/local/bin/PlistBuddy", "-c", "Set :CFBundleIdentifier #{new_plugin_bundle_id}", plist_path)
217
+ puts " ⚠ 修改插件 Bundle ID 失败: #{plist_path}"
218
+ end
225
219
  break
226
220
  end
227
221
  end
@@ -322,49 +316,26 @@ module Pindo
322
316
  puts " 旧 Scheme: #{old_scheme} (基于 #{old_bundle_id})"
323
317
  puts " 新 Scheme: #{new_scheme} (基于 #{new_bundle_id})"
324
318
 
325
- # 将 binary plist 转换为 XML 格式
326
- convert_command = "plutil -convert xml1 \"#{plist_path}\" 2>/dev/null"
327
- unless system(convert_command)
319
+ # 将 binary plist 转换为 XML 格式(数组参数,避免 plist_path 拼接进 shell)
320
+ unless system("plutil", "-convert", "xml1", plist_path, err: File::NULL)
328
321
  puts " ✗ 转换 plist 为 XML 失败"
329
322
  return
330
323
  end
331
324
 
332
- # 检查文件中是否存在旧的 scheme
333
- check_command = "grep -q \"#{old_scheme}\" \"#{plist_path}\""
334
- scheme_exists = system(check_command)
335
-
336
- unless scheme_exists
325
+ # Ruby 文件操作做字面量替换,彻底规避 sed/grep 的 shell 注入
326
+ content = File.read(plist_path)
327
+ unless content.include?(old_scheme)
337
328
  puts " ✓ 没有找到需要更新的 URL Scheme"
338
- # 转换回 binary 格式
339
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
340
- system(convert_back_command)
329
+ system("plutil", "-convert", "binary1", plist_path, err: File::NULL)
341
330
  return
342
331
  end
343
332
 
344
- # 使用 sed 进行全文替换
345
- # 使用临时文件避免 sed -i 在不同系统上的兼容性问题
346
- temp_file = "#{plist_path}.tmp"
347
- sed_command = "sed 's/#{old_scheme}/#{new_scheme}/g' \"#{plist_path}\" > \"#{temp_file}\" && mv \"#{temp_file}\" \"#{plist_path}\""
333
+ replace_count = content.scan(old_scheme).size
334
+ File.write(plist_path, content.gsub(old_scheme, new_scheme))
348
335
 
349
- if system(sed_command)
350
- # 统计替换次数
351
- count_command = "grep -o \"#{new_scheme}\" \"#{plist_path}\" | wc -l"
352
- replace_count = `#{count_command}`.strip.to_i
353
-
354
- # 转换回 binary 格式
355
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
356
- system(convert_back_command)
357
-
358
- puts " ✓ 已替换 #{replace_count} 处 URL Scheme: #{old_scheme} → #{new_scheme}"
359
- else
360
- puts " ✗ 替换 URL Scheme 失败"
361
- # 清理临时文件
362
- FileUtils.rm_f(temp_file) if File.exist?(temp_file)
363
-
364
- # 转换回 binary 格式(即使失败也要转回)
365
- convert_back_command = "plutil -convert binary1 \"#{plist_path}\" 2>/dev/null"
366
- system(convert_back_command)
367
- end
336
+ # 转换回 binary 格式
337
+ system("plutil", "-convert", "binary1", plist_path, err: File::NULL)
338
+ puts " 已替换 #{replace_count} 处 URL Scheme: #{old_scheme} #{new_scheme}"
368
339
  end
369
340
 
370
341
  end
data/lib/pindo/version.rb CHANGED
@@ -6,7 +6,7 @@ require 'time'
6
6
 
7
7
  module Pindo
8
8
 
9
- VERSION = "5.18.20"
9
+ VERSION = "5.19.1"
10
10
 
11
11
  class VersionCheck
12
12
  RUBYGEMS_API = 'https://rubygems.org/api/v1/gems/pindo.json'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pindo
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.18.20
4
+ version: 5.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - wade