pindo 5.18.20 → 5.19.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +38 -60
  3. data/lib/pindo/command/android/autobuild.rb +8 -4
  4. data/lib/pindo/command/ios/autobuild.rb +8 -4
  5. data/lib/pindo/command/repo/create.rb +1 -1
  6. data/lib/pindo/command/repo/search.rb +1 -1
  7. data/lib/pindo/command/unity/autobuild.rb +1 -1
  8. data/lib/pindo/command/utils/installskills.rb +2 -2
  9. data/lib/pindo/command/utils/renewproj.rb +0 -1
  10. data/lib/pindo/command/web/autobuild.rb +8 -4
  11. data/lib/pindo/module/android/android_build_helper.rb +39 -16
  12. data/lib/pindo/module/android/gradle_helper.rb +8 -14
  13. data/lib/pindo/module/cert/cert_helper.rb +2 -2
  14. data/lib/pindo/module/pgyer/pgyerhelper.rb +18 -20
  15. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +1 -37
  16. data/lib/pindo/module/task/model/build/android_build_task.rb +40 -0
  17. data/lib/pindo/module/task/model/build/ios_build_adhoc_task.rb +5 -5
  18. data/lib/pindo/module/task/model/build/ios_build_appstore_task.rb +5 -5
  19. data/lib/pindo/module/task/model/build/ios_build_dev_task.rb +2 -2
  20. data/lib/pindo/module/task/model/build_task.rb +2 -0
  21. data/lib/pindo/module/task/model/git/git_commit_task.rb +11 -1
  22. data/lib/pindo/module/task/model/git/git_tag_task.rb +2 -2
  23. data/lib/pindo/module/task/model/jps/jps_workflow_message_task.rb +8 -10
  24. data/lib/pindo/module/task/pindo_task.rb +2 -2
  25. data/lib/pindo/module/task/task_manager.rb +14 -6
  26. data/lib/pindo/module/unity/nuget_helper.rb +5 -20
  27. data/lib/pindo/module/utils/git_repo_helper.rb +40 -45
  28. data/lib/pindo/module/xcode/ipa_resign_helper.rb +33 -62
  29. data/lib/pindo/options/groups/unity_options.rb +1 -1
  30. data/lib/pindo/version.rb +1 -1
  31. metadata +1 -1
@@ -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}")