pindo 5.20.3 → 5.20.4

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.
@@ -366,9 +366,13 @@ module Pindo
366
366
  tasks << message_task
367
367
  end
368
368
 
369
- # 8. 创建媒体附件上传任务(如果需要,只依赖 Git 提交任务)
370
- if @args_media_flag
371
- # 获取 Git 管理类型的工作流(不同于 HTML 上传的工作流)
369
+ # 8. 获取 Git 管理类型的工作流(media、bind、send 共用,不同于 HTML 上传的包工作流)
370
+ # commit 记录挂在 Git 工作流下,媒体附件、包绑定、工作流消息都按它定位
371
+ git_app_info_obj = nil
372
+ git_workflow_info = nil
373
+ if @args_media_flag || @args_bind_flag || @args_send_flag
374
+ # 取不到就让异常冒泡:media/bind/send 都依赖它,静默跳过会让命令
375
+ # 明明没做用户要求的事却以 0 退出
372
376
  git_app_info_obj, git_workflow_info = PgyerHelper.share_instace.prepare_upload(
373
377
  working_directory: config[:project_path],
374
378
  conf: @args_conf,
@@ -376,48 +380,53 @@ module Pindo
376
380
  manage_type: "git"
377
381
  )
378
382
 
383
+ unless git_app_info_obj && git_workflow_info
384
+ raise Informative, "未获取到 Git 类型的工作流;请在 JPS 后台配置 manage_type=git 的工作流"
385
+ end
386
+ end
387
+
388
+ # 9. 创建媒体附件上传任务(如果需要,依赖 Git 提交/打 Tag 任务)
389
+ if @args_media_flag
379
390
  media_upload_task = Pindo::TaskSystem::JPSUploadMediaTask.new(
380
391
  [], # 空数组,自动从 JPSMedia/ 目录查找
381
392
  config[:project_path], # upload_path
382
393
  app_info_obj: git_app_info_obj,
383
394
  workflow_info: git_workflow_info,
384
- project_name: nil
395
+ project_name: nil,
396
+ required: @options[:media] || false # 仅用户显式 --media 时,找不到文件才算失败
385
397
  )
386
398
  # 依赖 Git 提交任务
387
399
  media_upload_task.dependencies << git_commit_task.id
400
+ # 同样依赖 GitTagTask:并发模式下媒体上传可能与 build/tag 并行,
401
+ # 不依赖它就会关联到构建前的 commit,与包绑定的 commit 不一致
402
+ media_upload_task.dependencies << git_tag_task.id if git_tag_task
388
403
  tasks << media_upload_task
389
404
  end
390
405
 
391
- # 9. 创建 Git Commit 绑定任务(如果需要,依赖上传任务)
406
+ # 10. 创建 Git Commit 绑定任务(如果需要,依赖上传任务)
392
407
  bind_package_task = nil # 声明变量以便后续工作流消息任务使用
393
408
  if @args_bind_flag
394
409
  bind_package_task = Pindo::TaskSystem::JPSBindPackageTask.new(
395
410
  nil, # app_version_list 为 nil,从依赖任务获取
396
411
  project_dir: config[:project_path],
397
- app_info_obj: config[:app_info_obj],
398
- workflow_info: config[:workflow_info],
412
+ app_info_obj: git_app_info_obj, # 使用 Git app_info_obj
413
+ workflow_info: git_workflow_info, # 使用 Git workflow_info
399
414
  project_name: nil
400
415
  )
401
- # 依赖上传任务和 Git 提交任务
416
+ # 依赖上传任务和 Git 任务
417
+ # GitTagTask 会提交构建注入文件并移动 HEAD,必须依赖它才能拿到 tag 所在的 commit
402
418
  bind_package_task.dependencies << upload_task.id
403
419
  bind_package_task.dependencies << git_commit_task.id
420
+ bind_package_task.dependencies << git_tag_task.id if git_tag_task
404
421
  tasks << bind_package_task
405
422
  end
406
423
 
407
- # 10. 创建工作流消息任务(如果需要,发送到测试群)
424
+ # 11. 创建工作流消息任务(如果需要,发送到测试群)
408
425
  if @args_send_flag
409
- # 获取 Git 工作流信息
410
- git_app_info_obj, git_workflow_info = PgyerHelper.share_instace.prepare_upload(
411
- working_directory: config[:project_path],
412
- conf: @args_conf,
413
- package_type: nil,
414
- manage_type: "git"
415
- )
416
-
417
426
  workflow_message_task = Pindo::TaskSystem::JPSWorkFlowMessageTask.new(
418
427
  project_id: git_app_info_obj["id"],
419
- workflow_id: git_workflow_info["id"],
420
- branch: 'master',
428
+ workflow_id: git_workflow_info[:workflow_id], # workflow_info 是 symbol key 结构,没有 "id"
429
+ # branch 不传:由任务从 Git 依赖任务取 resolve 之后的真实发布分支
421
430
  single: true,
422
431
  app_info_obj: git_app_info_obj,
423
432
  workflow_info: git_workflow_info
@@ -1780,69 +1780,113 @@ module Pindo
1780
1780
 
1781
1781
  # 获取特定 commit 已绑定的包 ID
1782
1782
  #
1783
+ # 走 project_package/bind_list(按 projectId 查项目下全部绑定),而不是
1784
+ # commit_log/preview(按 workflowId 过滤):写入接口 bind_commit 只认全局 commitId、
1785
+ # 且是覆盖式的,查询作用域必须与之一致,否则查漏的平台会在覆盖时被删掉。
1786
+ #
1787
+ # 绑定接口是覆盖式写入,调用方需要把已绑定的包一起提交,因此必须区分
1788
+ # 「确认没有绑定过」和「查不出来」——后者返回空数组会导致已有绑定被静默覆盖丢失。
1789
+ #
1783
1790
  # @param commit_id [String] Git commit SHA(必需)
1784
- # @param workflow_id [Integer, String] 工作流 ID(必需,API 需要)
1785
- # @return [Array<String>] 已绑定的包 ID 数组
1786
- def get_commit_bound_package_ids(commit_id:, workflow_id:)
1787
- return [] if commit_id.nil? || commit_id.empty?
1788
- return [] if workflow_id.nil?
1791
+ # @param project_id [String] JPS 项目 ID(必需)
1792
+ # @return [Array<String>, nil] 已绑定的包 ID 数组;查询失败或结构异常时返回 nil
1793
+ def get_commit_bound_package_ids(commit_id:, project_id:)
1794
+ if commit_id.nil? || commit_id.empty?
1795
+ Funlog.instance.fancyinfo_warning("commit_id 为空,无法查询已绑定的包")
1796
+ return nil
1797
+ end
1798
+
1799
+ if project_id.nil? || project_id.to_s.empty?
1800
+ Funlog.instance.fancyinfo_warning("project_id 为空,无法查询已绑定的包")
1801
+ return nil
1802
+ end
1789
1803
 
1790
1804
  begin
1791
- # 使用 commit_log/preview API 直接查询特定 commit
1792
- result = @pgyer_client.get_commit_log_preview(
1793
- workflowId: workflow_id,
1794
- commitIds: [commit_id],
1795
- params: { onlyCliff: false } # 需要完整信息
1796
- )
1805
+ result = @pgyer_client.get_project_package_bind_list(projectId: project_id)
1797
1806
 
1798
1807
  # 兼容两种响应格式
1799
1808
  response_code = result&.dig("code") || result&.dig("meta", "code")
1800
1809
 
1801
- if result && (response_code == 0 || response_code == 200)
1802
- # 从响应中提取 commit 信息
1803
- # preview API 可能返回数组或单个对象
1804
- commit_data = result&.dig("data")
1805
- commit_list = commit_data.is_a?(Array) ? commit_data : [commit_data].compact
1806
-
1807
- # 查找匹配的 commit
1808
- commit_info = commit_list.find { |item| item && item["commitId"] == commit_id }
1809
-
1810
- if commit_info && commit_info["bindVersions"]
1811
- # bindVersions 中提取所有平台的 projectPackageId
1812
- bind_versions = commit_info["bindVersions"]
1813
- package_ids = []
1814
-
1815
- # 遍历所有平台(ipa, apk, zip, mac, exe 等)
1816
- bind_versions.each do |_platform, version_info|
1817
- if version_info && version_info["projectPackageId"]
1818
- package_ids << version_info["projectPackageId"]
1819
- end
1810
+ unless result && (response_code == 0 || response_code == 200)
1811
+ error_msg = result&.dig("msg") || result&.dig("meta", "message") || "未知错误"
1812
+ Funlog.instance.fancyinfo_warning("查询项目 #{project_id} 的包绑定列表失败: #{error_msg}")
1813
+ debug_dump_bind_list_response(result)
1814
+ return nil
1815
+ end
1816
+
1817
+ data = result&.dig("data")
1818
+
1819
+ # 接口没有返回数据:项目下没有任何绑定,此时也就不存在会被覆盖丢失的绑定
1820
+ return [] if data.nil?
1821
+
1822
+ # 接口定义返回 map<String, List<ProjectPackagePreviewInfo>>。
1823
+ # 必须先校验类型再判空——[] 和 "" 都 respond_to?(:empty?),
1824
+ # 先判空会把结构异常的响应误判成「项目下无绑定」,随后覆盖写入清掉已有绑定。
1825
+ # 分组键的语义(commitId / 平台类型)不影响结果:每条记录自带 commitId,按它过滤即可。
1826
+ unless data.is_a?(Hash)
1827
+ Funlog.instance.fancyinfo_warning("包绑定列表结构异常(#{data.class}),无法确认已有绑定")
1828
+ debug_dump_bind_list_response(result)
1829
+ return nil
1830
+ end
1831
+
1832
+ # 空 map:确认项目下没有绑定
1833
+ return [] if data.empty?
1834
+
1835
+ package_ids = []
1836
+ data.each do |group_key, packages|
1837
+ unless packages.is_a?(Array)
1838
+ Funlog.instance.fancyinfo_warning("包绑定列表分组 #{group_key} 结构异常(#{packages.class}),无法确认已有绑定")
1839
+ return nil
1840
+ end
1841
+
1842
+ packages.each do |pkg|
1843
+ unless pkg.is_a?(Hash)
1844
+ Funlog.instance.fancyinfo_warning("包绑定列表分组 #{group_key} 中存在异常条目,无法确认已有绑定")
1845
+ return nil
1820
1846
  end
1821
1847
 
1822
- return package_ids.compact.uniq
1823
- else
1824
- # 如果没有找到或没有绑定信息,返回空数组(可能是首次绑定)
1825
- if ENV['PINDO_DEBUG']
1826
- puts "[PINDO_DEBUG] Commit #{commit_id[0..7]} 没有绑定信息"
1848
+ next unless pkg["commitId"] == commit_id
1849
+
1850
+ # 命中目标 commit 却拿不到包 id:静默跳过会得到残缺列表,
1851
+ # 覆盖写入时等于把那个包的绑定删掉
1852
+ package_id = pkg["id"]
1853
+ if package_id.nil? || package_id.to_s.strip.empty?
1854
+ Funlog.instance.fancyinfo_warning("Commit #{commit_id[0..7]} 的绑定记录缺少包 id,无法确认已有绑定")
1855
+ return nil
1827
1856
  end
1828
- return []
1829
- end
1830
- else
1831
- # 如果获取失败,返回空数组(不阻止绑定流程)
1832
- if ENV['PINDO_DEBUG']
1833
- error_msg = result&.dig("msg") || result&.dig("meta", "message") || "未知错误"
1834
- puts "[PINDO_DEBUG] 获取 commit 绑定包失败: #{error_msg}"
1857
+
1858
+ package_ids << package_id
1835
1859
  end
1836
- return []
1837
1860
  end
1861
+
1862
+ puts "[PINDO_DEBUG] Commit #{commit_id[0..7]} 已绑定 #{package_ids.size} 个包" if ENV['PINDO_DEBUG']
1863
+ package_ids.uniq
1838
1864
  rescue => e
1839
- # 异常时返回空数组,不阻止绑定流程
1840
- if ENV['PINDO_DEBUG']
1841
- puts "[PINDO_DEBUG] 获取 commit 绑定包异常: #{e.message}"
1842
- puts "[PINDO_DEBUG] Backtrace: #{e.backtrace[0..2].join("\n")}"
1843
- end
1844
- return []
1865
+ Funlog.instance.fancyinfo_warning("查询 commit 已绑定的包异常: #{e.message}")
1866
+ puts "[PINDO_DEBUG] Backtrace: #{e.backtrace[0..2].join("\n")}" if ENV['PINDO_DEBUG']
1867
+ return nil
1868
+ end
1869
+ end
1870
+
1871
+ # 按白名单输出包绑定列表响应的诊断信息
1872
+ #
1873
+ # ProjectPackagePreviewInfo 里含 cdnUrl、plistUrl、attachFiles、qrCodeUrl 等
1874
+ # 可直接下载安装包的链接,整体 inspect 会把它们写进 CI 日志,
1875
+ # 因此只输出定位问题所需的字段。
1876
+ # @param result [Hash, nil] 接口原始响应
1877
+ def debug_dump_bind_list_response(result)
1878
+ return unless ENV['PINDO_DEBUG']
1879
+
1880
+ code = result&.dig("code") || result&.dig("meta", "code")
1881
+ message = result&.dig("msg") || result&.dig("meta", "message")
1882
+ data = result&.dig("data")
1883
+ summary = if data.is_a?(Hash)
1884
+ data.map { |k, v| [k, v.is_a?(Array) ? "#{v.size} 条" : v.class.to_s] }.to_h
1885
+ else
1886
+ data.class.to_s
1845
1887
  end
1888
+
1889
+ puts "[PINDO_DEBUG] bind_list 响应: code=#{code.inspect} msg=#{message.inspect} data=#{summary.inspect}"
1846
1890
  end
1847
1891
 
1848
1892
  # 获取工作流中已绑定的包 ID(已废弃,请使用 get_commit_bound_package_ids)
@@ -1984,10 +2028,11 @@ module Pindo
1984
2028
  # @param project_id [String] 项目 ID
1985
2029
  # @param workflow_id [Integer] 工作流 ID
1986
2030
  # @param commit_id [String] Git commit SHA
1987
- # @param branch [String] 分支名(可选,默认 'master')
2031
+ # @param branch [String, nil] 分支名(可选,nil 时不发送分支字段——传错的分支名
2032
+ # 会让 commit 与分支错误关联,比不传更糟)
1988
2033
  # @param single [Boolean] 是否单个提交(可选,默认 true)
1989
2034
  # @return [Hash] 发送结果
1990
- def send_workflow_message(project_id:, workflow_id:, commit_id:, branch: 'master', single: true)
2035
+ def send_workflow_message(project_id:, workflow_id:, commit_id:, branch: nil, single: true)
1991
2036
  if project_id.nil? || project_id.empty?
1992
2037
  Funlog.instance.fancyinfo_error("project_id 不能为空")
1993
2038
  return { success: false, error: "project_id 不能为空" }
@@ -2009,9 +2054,10 @@ module Pindo
2009
2054
  workflowId: workflow_id,
2010
2055
  params: {
2011
2056
  single: single,
2057
+ # jpsclient 内部归一化为服务端要求的 branches 数组;nil 时不带该字段
2012
2058
  branch: branch,
2013
2059
  commitIds: [commit_id]
2014
- }
2060
+ }.compact
2015
2061
  )
2016
2062
 
2017
2063
  # 兼容两种响应格式
@@ -122,6 +122,12 @@ module Pindo
122
122
  # skip_without_tag 模式:跳过所有 git 同步操作,直接返回
123
123
  if @process_type == Pindo::UncommittedFilesProcessType::SKIP_WITHOUT_TAG
124
124
  coding_branch = get_current_branch_name
125
+ # 该模式不合并分支,代码就停在当前分支上,随 task_param 传给工作流消息任务的
126
+ # 分支必须是当前分支——用默认的 master 或探测出的发布分支,会发出
127
+ # 「feature 分支的 commit + master 分支名」这种自相矛盾的组合。
128
+ # detached HEAD 时取不到分支名(current_branch_name 返回 nil),保持 nil,
129
+ # 让下游工作流消息不带分支发送,而不是把字面量 "HEAD" 当成分支名。
130
+ @release_branch = Pindo::GitHandler.current_branch_name(local_repo_dir: root_dir)
125
131
  @build_number, @commit_hash = git_repo_helper.get_build_number_from_commit(project_dir: root_dir)
126
132
 
127
133
  # 版本号仍按正常优先级计算,仅"不打 tag / 不升版本",避免硬编码 0.0.0 污染 build.gradle 与下游上传参数
@@ -78,13 +78,19 @@ module Pindo
78
78
 
79
79
  # 4. 创建并推送 Tag
80
80
  tag_name = "#{@tag_pre}#{@build_version}"
81
- git_repo_helper.create_and_push_tag(
81
+ tag_result = git_repo_helper.create_and_push_tag(
82
82
  project_dir: root_dir,
83
83
  tag_name: tag_name,
84
84
  tag_type: @tag_type
85
85
  )
86
+
87
+ # 5. 固化 commit 快照:commit_build_changes 与分支合并都可能移动 HEAD,
88
+ # 下游绑定/消息任务必须拿到「tag 这一刻」的 commit,而不是它们读取时的 HEAD
89
+ capture_git_commit_info
90
+
91
+ tag_result
86
92
  ensure
87
- # 5. 还原 stash
93
+ # 6. 还原 stash
88
94
  restore_stash(root_dir, stash_name) if stash_saved
89
95
  end
90
96
  end
@@ -25,6 +25,9 @@ module Pindo
25
25
  @build_number = nil
26
26
  @commit_hash = nil
27
27
 
28
+ # commit 信息快照(由子类在 do_work 中调用 capture_git_commit_info 固化)
29
+ @git_commit_info = nil
30
+
28
31
  # 通过 project_path 获取 git_root_path
29
32
  @git_root_path = @project_path ? Pindo::GitHandler.git_root_directory(local_repo_dir: @project_path) : nil
30
33
 
@@ -91,6 +94,24 @@ module Pindo
91
94
  Pindo::GitHandler.git!(%W(-C #{@git_root_path} rev-parse --abbrev-ref HEAD)).strip
92
95
  end
93
96
 
97
+ # 固化当前 HEAD 的 commit 信息快照
98
+ #
99
+ # build_task_param 默认在被读取时才去读 HEAD,若期间有其它任务移动了 HEAD
100
+ # (例如 GitTagTask 会提交构建注入文件),不同时刻读到的 commit 就不一致。
101
+ # 子类在 do_work 中调用本方法,即可把「任务完成那一刻」的 commit 固定下来。
102
+ def capture_git_commit_info
103
+ return unless @git_root_path
104
+
105
+ # 传入任务自身的 tag 前缀,自定义 --tag_pre 时才能正确识别 HEAD 上的版本 tag
106
+ info = Pindo::GitHandler.get_latest_conventional_commit(
107
+ project_dir: @git_root_path,
108
+ tag_prefix: @tag_pre
109
+ )
110
+ @git_commit_info = info if info && info[:commit_id]
111
+ rescue => e
112
+ Funlog.instance.fancyinfo_warning("固化 git commit 信息失败: #{e.message}")
113
+ end
114
+
94
115
  # 构建任务参数(供其他任务使用)
95
116
  def build_task_param
96
117
  return {} unless @status == TaskStatus::SUCCESS
@@ -110,8 +131,12 @@ module Pindo
110
131
  # 添加 git commit 信息(供 JPSUploadMediaTask 等任务使用)
111
132
  if @git_root_path
112
133
  begin
113
- # 获取当前 HEAD commit 信息
114
- git_commit_info = Pindo::GitHandler.get_latest_conventional_commit(project_dir: @git_root_path)
134
+ # 优先使用任务内固化的快照;没有快照时回退到读取当前 HEAD
135
+ git_commit_info = @git_commit_info ||
136
+ Pindo::GitHandler.get_latest_conventional_commit(
137
+ project_dir: @git_root_path,
138
+ tag_prefix: @tag_pre
139
+ )
115
140
  if git_commit_info
116
141
  param[:git_commit_id] = git_commit_info[:commit_id]
117
142
  param[:git_commit_time] = git_commit_info[:commit_time]
@@ -27,8 +27,9 @@ module Pindo
27
27
  # @option options [String] :git_commit_time Git commit 时间(可选)
28
28
  # @option options [String] :git_commit_desc Git commit 描述(可选)
29
29
  # @option options [String] :project_dir 项目目录(可选,用于自动获取 commit 信息)
30
- # @option options [Hash] :app_info_obj JPS 应用信息对象(可选,继承自基类)
31
- # @option options [Hash] :workflow_info 工作流信息(可选,继承自基类)
30
+ # @option options [Hash] :app_info_obj JPS 应用信息对象(必需,用于取 project_id;
31
+ # 绑定与查询都以项目为作用域,不再依赖 workflow_info
32
+ # @option options [Hash] :workflow_info 工作流信息(可选,继承自基类,本任务不使用)
32
33
  # @option options [String] :project_name 项目名称(可选,继承自基类)
33
34
  def initialize(app_version_list = nil, options = {})
34
35
  # 兼容性处理:如果传入的是单个 Hash,转为数组
@@ -48,6 +49,11 @@ module Pindo
48
49
  # 设置任务优先级为 LOW,确保在上传任务之后执行
49
50
  options[:priority] ||= TaskPriority::LOW
50
51
 
52
+ # 重试延迟加随机抖动:绑定是「读取 → 合并 → 覆盖」,两个进程冲突后若都按固定
53
+ # 延迟重试,会在同一时刻再次相撞。抖动只降低碰撞概率,消除不了竞态——
54
+ # 根治需要服务端提供原子追加或 CAS(见 do_work 里的说明)。
55
+ options[:retry_delay] ||= self.class.default_retry_delay + rand(0..4)
56
+
51
57
  super("绑定 包到Git工作流", options)
52
58
  end
53
59
 
@@ -136,18 +142,24 @@ module Pindo
136
142
  raise "无法登录 JPS,请检查配置"
137
143
  end
138
144
 
139
- # 6. 获取工作流 ID
140
- workflow_id = @workflow_info&.dig(:workflow_id) || @workflow_info&.dig('id')
141
- unless workflow_id
142
- raise "缺少 workflow_info,无法获取 workflow_id"
145
+ # 6. 获取项目 ID(绑定与查询都以项目为作用域)
146
+ project_id = @app_info_obj&.dig("id") || @app_info_obj&.dig(:id)
147
+ unless project_id
148
+ raise "缺少 app_info_obj,无法获取 project_id"
143
149
  end
144
150
 
145
151
  # 7. 获取当前 commit 已绑定的包 ID
146
152
  existing_bound_ids = pgyer_helper.get_commit_bound_package_ids(
147
153
  commit_id: @git_commit_id,
148
- workflow_id: workflow_id
154
+ project_id: project_id
149
155
  )
150
156
 
157
+ # 查询失败(nil)时中止:绑定接口是覆盖式写入,带着不完整的列表提交
158
+ # 会把该 commit 上已绑定的其它平台包冲掉。宁可报错让用户重试或补绑。
159
+ if existing_bound_ids.nil?
160
+ raise "无法确认 Commit #{@git_commit_id[0..7]} 已绑定的包,为避免覆盖丢失已中止绑定,请重试"
161
+ end
162
+
151
163
  if existing_bound_ids.any?
152
164
  puts " 📋 当前 Commit 已绑定的包: #{existing_bound_ids.size} 个"
153
165
  end
@@ -172,6 +184,20 @@ module Pindo
172
184
 
173
185
  if bind_result[:success]
174
186
  Funlog.instance.fancyinfo_success("绑定成功!")
187
+
188
+ # 10. 写后回读校验
189
+ #
190
+ # 已知限制:「查询 → 合并 → 覆盖」跨进程不是原子的,客户端消除不了这个竞态。
191
+ # bind_commit 的入参只有 {commitId, projectPackageIds},既没有 revision/CAS
192
+ # 也没有追加语义,同项目下的其它接口同样不提供(已核对全部 project_package/
193
+ # commit_log 接口)。因此本校验只能发现「本次写入的包在回读时不见了」,
194
+ # 发现不了「本次写入覆盖掉了别人刚加的包」——后者要等对方回读时才暴露。
195
+ #
196
+ # 根治需要服务端支持,二选一即可:
197
+ # 1) bind_commit 增加 append 语义(只追加不覆盖)
198
+ # 2) 返回并校验 revision,做乐观并发控制
199
+ verify_bound_packages(pgyer_helper, project_id, merged_package_ids)
200
+
175
201
  puts ""
176
202
 
177
203
  {
@@ -193,6 +219,35 @@ module Pindo
193
219
 
194
220
  private
195
221
 
222
+ # 写后回读校验:确认期望绑定的包都在服务端的绑定列表里
223
+ #
224
+ # 查询失败时只告警不失败——包已经写进去了,回读失败不代表绑定失败。
225
+ # 只有确认「期望的包没在列表里」才判定为被并发覆盖,抛异常交给任务重试。
226
+ #
227
+ # @param pgyer_helper [PgyerHelper] 已登录的 helper
228
+ # @param project_id [String] 项目 ID
229
+ # @param expected_ids [Array<String>] 本次写入的包 ID 列表
230
+ def verify_bound_packages(pgyer_helper, project_id, expected_ids)
231
+ actual_ids = pgyer_helper.get_commit_bound_package_ids(
232
+ commit_id: @git_commit_id,
233
+ project_id: project_id
234
+ )
235
+
236
+ if actual_ids.nil?
237
+ Funlog.instance.fancyinfo_warning("绑定后回读校验未能完成,请到 JPS 确认绑定结果")
238
+ return
239
+ end
240
+
241
+ missing_ids = expected_ids - actual_ids
242
+ return if missing_ids.empty?
243
+
244
+ # 抛出后由任务的重试机制重新走「查询 → 合并 → 写入」,对两种成因都收敛:
245
+ # 并发覆盖时会重新读到对方的包并一起写回;服务端写后读延迟时下一次读到的即是新值。
246
+ raise "绑定结果校验失败:#{missing_ids.size} 个包未出现在绑定列表中" \
247
+ "(其它进程同时绑定覆盖了本次结果,或服务端写入尚未可见)," \
248
+ "缺失包 ID: #{missing_ids.join(', ')}"
249
+ end
250
+
196
251
  # 从依赖任务获取 app_version_list
197
252
  def fetch_app_version_from_dependencies
198
253
  # 获取所有 JPSUploadTask 的数据(可能有多个平台的上传任务)
@@ -210,10 +265,8 @@ module Pindo
210
265
 
211
266
  # 从依赖任务获取或自动获取 git commit 信息
212
267
  def fetch_git_commit_info
213
- # 1. 优先从 GitTagTask 依赖任务获取(打 tag 后的 commit 信息更准确)
214
- git_data = get_data_param_by_key(:git_tag)
215
- # 2. 降级到 GitCommitTask
216
- git_data = get_data_param_by_key(:git_commit) if git_data.nil? || git_data.empty?
268
+ # 1. 从依赖任务获取(优先 GitTagTask,降级 GitCommitTask)
269
+ git_data = git_commit_data_param
217
270
 
218
271
  if git_data && git_data[:task_param]
219
272
  param = git_data[:task_param]
@@ -222,7 +275,7 @@ module Pindo
222
275
  @git_commit_desc = param[:git_commit_desc] if param[:git_commit_desc]
223
276
  end
224
277
 
225
- # 3. 如果依赖任务都没有提供,且指定了 project_dir,使用自动获取
278
+ # 2. 如果依赖任务都没有提供,且指定了 project_dir,使用自动获取
226
279
  if (@git_commit_id.nil? || @git_commit_id.empty?) && @project_dir
227
280
  # 查找 git 仓库根目录
228
281
  git_root = Pindo::GitHandler.git_root_directory(local_repo_dir: @project_dir)
@@ -33,12 +33,16 @@ module Pindo
33
33
  # @option options [Hash] :app_info_obj JPS 应用信息对象(可选,继承自基类)
34
34
  # @option options [Hash] :workflow_info 工作流信息(可选,继承自基类)
35
35
  # @option options [String] :project_name 项目名称(可选,继承自基类)
36
+ # @option options [Boolean] :required 用户是否显式要求上传媒体(默认 false)。
37
+ # 为 true 时找不到媒体文件会让任务失败;--bind/--send 隐式带出的媒体上传保持
38
+ # 宽松,因为「项目里没有 JPSMedia 文件」是常态,不该让整条构建失败。
36
39
  def initialize(file_paths, upload_path, options = {})
37
40
  @file_paths = file_paths || [] # 要上传的文件路径列表
38
41
  @upload_path = upload_path # 项目路径
39
42
  @git_commit_id = options[:git_commit_id] # Git commit SHA
40
43
  @git_commit_time = options[:git_commit_time] # Git commit 时间
41
44
  @git_commit_desc = options[:git_commit_desc] # Git commit 描述
45
+ @required = options.fetch(:required, false) # 是否为用户显式要求
42
46
 
43
47
  # 设置任务优先级为 LOW,确保在其他任务之后执行
44
48
  options[:priority] ||= TaskPriority::LOW
@@ -97,10 +101,8 @@ module Pindo
97
101
 
98
102
  # 2. 如果没有提供 git_commit_id,尝试从依赖任务获取或自动获取
99
103
  if @git_commit_id.nil? || @git_commit_id.empty?
100
- # 2.1 优先从 GitTagTask 依赖任务获取(打 tag 后的 commit 信息更准确)
101
- git_data = get_data_param_by_key(:git_tag)
102
- # 2.2 降级到 GitCommitTask
103
- git_data = get_data_param_by_key(:git_commit) if git_data.nil? || git_data.empty?
104
+ # 2.1 从依赖任务获取(优先 GitTagTask,降级 GitCommitTask)
105
+ git_data = git_commit_data_param
104
106
 
105
107
  if git_data && git_data[:task_param]
106
108
  param = git_data[:task_param]
@@ -110,7 +112,7 @@ module Pindo
110
112
  @git_commit_desc = param[:git_commit_desc] if param[:git_commit_desc]
111
113
  end
112
114
 
113
- # 2.3 如果依赖任务都没有提供,使用 get_latest_conventional_commit
115
+ # 2.2 如果依赖任务都没有提供,回退到读取当前仓库状态
114
116
  if @git_commit_id.nil? || @git_commit_id.empty?
115
117
  git_info = Pindo::GitHandler.get_latest_conventional_commit(project_dir: git_root)
116
118
  @git_commit_id = git_info[:commit_id]
@@ -154,6 +156,13 @@ module Pindo
154
156
  puts " ❌ 没有找到有效的 media 文件"
155
157
  puts " 提示: 请在项目根目录创建 JPSMedia/ 目录并放入要上传的图片或视频文件"
156
158
  puts ""
159
+
160
+ # 用户显式传了 --media 却没有文件可传:任务系统只看异常不看 result[:success],
161
+ # 这里不抛出的话命令会以 0 退出,等于悄悄没做用户要求的事
162
+ if @required
163
+ raise "没有找到有效的 media 文件;请在 JPSMedia/ 目录放入图片或视频后重试"
164
+ end
165
+
157
166
  return {
158
167
  success: false,
159
168
  success_urls: [],
@@ -27,7 +27,7 @@ module Pindo
27
27
  # @option options [String] :git_commit_desc Git commit 描述(可选)
28
28
  # @option options [String] :project_id 项目 ID(可选,从 app_info_obj 获取)
29
29
  # @option options [Integer] :workflow_id 工作流 ID(可选,从 workflow_info 获取)
30
- # @option options [String] :branch 分支名(可选,默认 'master')
30
+ # @option options [String] :branch 分支名(可选,默认从依赖的 Git 任务取实际发布分支)
31
31
  # @option options [Boolean] :single 是否单个提交(默认 true)
32
32
  # @option options [Hash] :app_info_obj JPS 应用信息对象(可选,用于获取 project_id)
33
33
  # @option options [Hash] :workflow_info 工作流信息(可选,用于获取 workflow_id)
@@ -37,7 +37,9 @@ module Pindo
37
37
  @git_commit_desc = options[:git_commit_desc]
38
38
  @project_id = options[:project_id]
39
39
  @workflow_id = options[:workflow_id]
40
- @branch = options[:branch] || 'master'
40
+ # 不在这里兜底成 master:仓库主干可能是 main 或其它分支,
41
+ # 未显式指定时统一从依赖的 Git 任务取 resolve 之后的真实发布分支
42
+ @branch = options[:branch]
41
43
  @single = options.fetch(:single, true)
42
44
 
43
45
  # 消息发送任务优先级为 LOW,确保在其他任务之后执行
@@ -63,7 +65,10 @@ module Pindo
63
65
  # 2. 验证必需参数
64
66
  validate_required_params!
65
67
 
66
- # 3. 发送消息
68
+ # 3. 确定分支(取不到则不带分支发送)
69
+ resolve_branch!
70
+
71
+ # 4. 发送消息
67
72
  send_workflow_message
68
73
 
69
74
  puts " ✅ JPS 工作流消息发送完成\n"
@@ -86,23 +91,24 @@ module Pindo
86
91
 
87
92
  # 判断是否需要从依赖任务获取数据
88
93
  def need_fetch_dependencies?
89
- @git_commit_id.nil? || @project_id.nil? || @workflow_id.nil?
94
+ @git_commit_id.nil? || @project_id.nil? || @workflow_id.nil? || @branch.nil?
90
95
  end
91
96
 
92
97
  # 从依赖任务获取数据
93
98
  def fetch_data_from_dependencies
94
- # 1. 获取 commit 信息(优先从 GitTagTask,降级到 GitCommitTask)
95
- if @git_commit_id.nil?
96
- # 1.1 优先从 GitTagTask 依赖任务获取(打 tag 后的 commit 信息更准确)
97
- git_data = get_data_param_by_key(:git_tag)
98
- # 1.2 降级到 GitCommitTask
99
- git_data = get_data_param_by_key(:git_commit) if git_data.nil? || git_data.empty?
99
+ # 1. 获取 commit 信息与发布分支(优先从 GitTagTask,降级到 GitCommitTask)
100
+ if @git_commit_id.nil? || @branch.nil?
101
+ git_data = git_commit_data_param
100
102
 
101
103
  if git_data && git_data[:task_param]
102
104
  param = git_data[:task_param]
103
- @git_commit_id = param[:git_commit_id] if param[:git_commit_id]
104
- @git_commit_time = param[:git_commit_time] if param[:git_commit_time]
105
- @git_commit_desc = param[:git_commit_desc] if param[:git_commit_desc]
105
+ # 一律只在调用方没给值时才填充,避免显式传入的 commit 信息被依赖任务覆盖
106
+ @git_commit_id = param[:git_commit_id] if @git_commit_id.nil? && param[:git_commit_id]
107
+ @git_commit_time = param[:git_commit_time] if @git_commit_time.nil? && param[:git_commit_time]
108
+ @git_commit_desc = param[:git_commit_desc] if @git_commit_desc.nil? && param[:git_commit_desc]
109
+ # release_branch 是 GitCommitTask/GitTagTask 用 resolve_release_branch
110
+ # 探测出来的真实分支(master > main > 唯一远程分支 > 当前分支)
111
+ @branch = param[:release_branch] if @branch.nil? && param[:release_branch]
106
112
  end
107
113
  end
108
114
 
@@ -152,13 +158,25 @@ module Pindo
152
158
  end
153
159
  end
154
160
 
161
+ # 确定要发送的分支
162
+ #
163
+ # 取不到时保持 nil 而不是兜底成 master:branches 在服务端是可选字段,不传即可,
164
+ # 但传一个错的分支名(detached HEAD、无 Git 依赖等场景)会让 commit 与分支
165
+ # 错误关联,比不传更糟。
166
+ def resolve_branch!
167
+ return unless @branch.to_s.strip.empty?
168
+
169
+ @branch = nil
170
+ Funlog.instance.fancyinfo_warning("未取到当前分支,工作流消息将不带分支信息发送")
171
+ end
172
+
155
173
  # 发送工作流消息
156
174
  def send_workflow_message
157
175
  puts " 📨 发送工作流消息..."
158
176
  puts " Project ID: #{@project_id}"
159
177
  puts " Workflow ID: #{@workflow_id}"
160
178
  puts " Commit ID: #{@git_commit_id[0..7]}" if @git_commit_id
161
- puts " Branch: #{@branch}"
179
+ puts " Branch: #{@branch || '(不指定)'}"
162
180
  puts ""
163
181
 
164
182
  pgyer_helper = PgyerHelper.share_instace